adventurous-orange-57668
12/15/2025, 10:56 PMasync def stop_recording(self) -> AudioFileInfo | None:
"""Stop the active egress recording and wait for upload to complete.
Returns:
AudioFileInfo with file details if successful, None on error
"""
if not self._egress_id:
logger.debug("No active egress to stop")
return None
egress_id = self._egress_id
try:
logger.info(f"Stopping egress recording, egress_id={egress_id}...")
await self.livekit_api.egress.stop_egress(
egress_proto.StopEgressRequest(egress_id=egress_id)
)
logger.info(
f"Stop request sent for egress_id={egress_id}, waiting for upload to complete..."
)
# Wait for the egress to complete and get file info
file_info = await self._wait_for_completion(egress_id)
self._egress_id = None
self._expected_filepath = None
return file_info
except Exception as e:
logger.error(f"Failed to stop egress recording: {e}")
return None
async def _wait_for_completion(
self,
egress_id: str,
timeout: float = 60.0,
poll_interval: float = 1.0,
file_results_timeout: float = 10.0,
) -> AudioFileInfo | None:
"""Wait for egress to complete and return file info.
Args:
egress_id: The egress ID to wait for
timeout: Maximum time to wait in seconds
poll_interval: Time between status checks in seconds
file_results_timeout: Additional time to wait for file_results after
EGRESS_COMPLETE status (handles S3 upload delay on LiveKit Cloud)
Returns:
AudioFileInfo with file details if successful, None on error/timeout
"""
start_time = asyncio.get_event_loop().time()
last_status = None
complete_time: float | None = None # Track when EGRESS_COMPLETE was first seen
while True:
elapsed = asyncio.get_event_loop().time() - start_time
if elapsed > timeout:
logger.error(
f"Timeout waiting for egress completion after {timeout}s, "
f"egress_id={egress_id}, last_status={last_status}"
)
return None
try:
# List egress by ID to get current status
response = await self.livekit_api.egress.list_egress(
egress_proto.ListEgressRequest(egress_id=egress_id)
)
if not response.items:
logger.warning(f"Egress {egress_id} not found in list response")
await asyncio.sleep(poll_interval)
continue
egress_info = response.items[0]
status = egress_info.status
last_status = egress_proto.EgressStatus.Name(status)
logger.debug(
f"Egress status: {last_status}, egress_id={egress_id}, "
f"elapsed={elapsed:.1f}s"
)
# Check terminal states
if status == egress_proto.EgressStatus.EGRESS_COMPLETE:
# Extract file info from results
if egress_info.file_results:
file_result = egress_info.file_results[0]
file_info = AudioFileInfo(
filename=file_result.filename,
location=file_result.location,
duration=file_result.duration,
size=file_result.size,
)
logger.info(
f"Egress completed successfully! "
f"File uploaded to: {file_info.location}, "
f"filename={file_info.filename}, "
f"duration={file_info.duration}ns, "
f"size={file_info.size} bytes"
)
return file_info
else:
# file_results may not be populated immediately on LiveKit Cloud
# Continue polling for a short time to allow S3 upload to complete
current_time = asyncio.get_event_loop().time()
if complete_time is None:
complete_time = current_time
# Log detailed egress info for debugging S3 upload issues
logger.warning(
f"Egress completed but file_results empty - "
f"egress_id={egress_info.egress_id}, "
f"room_name={egress_info.room_name}, "
f"error={egress_info.error or 'None'}, "
f"stream_results={egress_info.stream_results}, "
f"segment_results={egress_info.segment_results}, "
f"waiting up to {file_results_timeout}s for upload"
)
time_since_complete = current_time - complete_time
if time_since_complete >= file_results_timeout:
logger.warning(
f"Egress completed but no file_results found after "
f"{file_results_timeout}s wait, egress_id={egress_id}"
)
return None
# Continue polling for file_results
await asyncio.sleep(poll_interval)
continue
elif status == egress_proto.EgressStatus.EGRESS_FAILED:
error_msg = egress_info.error or "Unknown error"
logger.error(f"Egress failed: {error_msg}, egress_id={egress_id}")
return None
elif status == egress_proto.EgressStatus.EGRESS_ABORTED:
logger.warning(f"Egress was aborted, egress_id={egress_id}")
return None
elif status == egress_proto.EgressStatus.EGRESS_LIMIT_REACHED:
logger.warning(f"Egress limit reached, egress_id={egress_id}")
# Still try to get file info as partial recording may exist
if egress_info.file_results:
file_result = egress_info.file_results[0]
return AudioFileInfo(
filename=file_result.filename,
location=file_result.location,
duration=file_result.duration,
size=file_result.size,
)
return None
# Still in progress, wait and poll again
await asyncio.sleep(poll_interval)
except Exception as e:
logger.error(f"Error polling egress status: {e}")
await asyncio.sleep(poll_interval)