|
@@ -197,7 +197,7 @@ async def _read_ffmpeg_stderr(process: asyncio.subprocess.Process) -> str | None
|
|
|
# Some printer firmwares (notably P2S) drop RTSP sessions after a few seconds,
|
|
# Some printer firmwares (notably P2S) drop RTSP sessions after a few seconds,
|
|
|
# so we transparently respawn ffmpeg to keep the MJPEG stream alive.
|
|
# so we transparently respawn ffmpeg to keep the MJPEG stream alive.
|
|
|
_RTSP_MAX_RECONNECTS = 30
|
|
_RTSP_MAX_RECONNECTS = 30
|
|
|
-_RTSP_RECONNECT_DELAY = 1.0 # seconds between respawns
|
|
|
|
|
|
|
+_RTSP_RECONNECT_DELAY = 0.2 # seconds between respawns
|
|
|
|
|
|
|
|
|
|
|
|
|
async def generate_rtsp_mjpeg_stream(
|
|
async def generate_rtsp_mjpeg_stream(
|
|
@@ -244,6 +244,14 @@ async def generate_rtsp_mjpeg_stream(
|
|
|
"1024000", # 1MB buffer
|
|
"1024000", # 1MB buffer
|
|
|
"-max_delay",
|
|
"-max_delay",
|
|
|
"500000", # 0.5 seconds max delay
|
|
"500000", # 0.5 seconds max delay
|
|
|
|
|
+ "-probesize",
|
|
|
|
|
+ "32", # Minimal probing for fast start
|
|
|
|
|
+ "-analyzeduration",
|
|
|
|
|
+ "0", # Skip format analysis
|
|
|
|
|
+ "-fflags",
|
|
|
|
|
+ "nobuffer", # Reduce internal buffering
|
|
|
|
|
+ "-flags",
|
|
|
|
|
+ "low_delay", # Minimize decode latency
|
|
|
"-i",
|
|
"-i",
|
|
|
camera_url,
|
|
camera_url,
|
|
|
"-f",
|
|
"-f",
|
|
@@ -291,11 +299,18 @@ async def generate_rtsp_mjpeg_stream(
|
|
|
if disconnect_event and disconnect_event.is_set():
|
|
if disconnect_event and disconnect_event.is_set():
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
- # Spawn ffmpeg
|
|
|
|
|
|
|
+ # Spawn ffmpeg — enable GnuTLS debug output in debug mode
|
|
|
|
|
+ env = None
|
|
|
|
|
+ if logger.isEnabledFor(logging.DEBUG):
|
|
|
|
|
+ import os
|
|
|
|
|
+
|
|
|
|
|
+ env = {**os.environ, "GNUTLS_DEBUG_LEVEL": "2"}
|
|
|
|
|
+
|
|
|
process = await asyncio.create_subprocess_exec(
|
|
process = await asyncio.create_subprocess_exec(
|
|
|
*cmd,
|
|
*cmd,
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
+ env=env,
|
|
|
**spawn_kwargs,
|
|
**spawn_kwargs,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -306,7 +321,7 @@ async def generate_rtsp_mjpeg_stream(
|
|
|
_spawned_ffmpeg_pids[process.pid] = _time.time()
|
|
_spawned_ffmpeg_pids[process.pid] = _time.time()
|
|
|
|
|
|
|
|
# Give ffmpeg a moment to start and check for immediate failures
|
|
# Give ffmpeg a moment to start and check for immediate failures
|
|
|
- await asyncio.sleep(0.5)
|
|
|
|
|
|
|
+ await asyncio.sleep(0.1)
|
|
|
if process.returncode is not None:
|
|
if process.returncode is not None:
|
|
|
stderr = await process.stderr.read()
|
|
stderr = await process.stderr.read()
|
|
|
stderr_text = stderr.decode(errors="replace")
|
|
stderr_text = stderr.decode(errors="replace")
|
|
@@ -486,19 +501,12 @@ async def camera_stream(
|
|
|
|
|
|
|
|
async def external_stream_wrapper():
|
|
async def external_stream_wrapper():
|
|
|
"""Wrap external stream to track start/stop and update frame times."""
|
|
"""Wrap external stream to track start/stop and update frame times."""
|
|
|
- frame_interval = 1.0 / fps
|
|
|
|
|
- last_yield_time = 0.0
|
|
|
|
|
try:
|
|
try:
|
|
|
async for frame in generate_mjpeg_stream(
|
|
async for frame in generate_mjpeg_stream(
|
|
|
printer.external_camera_url, printer.external_camera_type, fps
|
|
printer.external_camera_url, printer.external_camera_type, fps
|
|
|
):
|
|
):
|
|
|
- # Rate limit to prevent overwhelming browser
|
|
|
|
|
- current_time = time.time()
|
|
|
|
|
- elapsed = current_time - last_yield_time
|
|
|
|
|
- if elapsed < frame_interval:
|
|
|
|
|
- await asyncio.sleep(frame_interval - elapsed)
|
|
|
|
|
- last_yield_time = time.time()
|
|
|
|
|
- _last_frame_times[printer_id] = last_yield_time
|
|
|
|
|
|
|
+ # generate_mjpeg_stream already rate-limits; just track frame times
|
|
|
|
|
+ _last_frame_times[printer_id] = time.time()
|
|
|
yield frame
|
|
yield frame
|
|
|
finally:
|
|
finally:
|
|
|
_active_external_streams.discard(printer_id)
|
|
_active_external_streams.discard(printer_id)
|
|
@@ -1274,11 +1282,12 @@ async def cleanup_orphaned_streams():
|
|
|
cleaned = 0
|
|
cleaned = 0
|
|
|
now = time.time()
|
|
now = time.time()
|
|
|
|
|
|
|
|
- # Collect PIDs that are legitimately in-use (active stream, process alive)
|
|
|
|
|
|
|
+ # Collect PIDs that are legitimately in-use (active streams + any tracked spawn)
|
|
|
active_pids = {proc.pid for proc in _active_streams.values() if proc.returncode is None}
|
|
active_pids = {proc.pid for proc in _active_streams.values() if proc.returncode is None}
|
|
|
|
|
+ active_pids.update(_spawned_ffmpeg_pids.keys())
|
|
|
|
|
|
|
|
# 1. /proc scan — catch ALL orphaned Bambu ffmpeg processes on the system.
|
|
# 1. /proc scan — catch ALL orphaned Bambu ffmpeg processes on the system.
|
|
|
- # Any ffmpeg with rtsps://bblp: that is NOT in an active stream is orphaned.
|
|
|
|
|
|
|
+ # Any ffmpeg with rtsps://bblp: that is NOT tracked by us is orphaned.
|
|
|
for pid in _scan_bambu_ffmpeg_pids():
|
|
for pid in _scan_bambu_ffmpeg_pids():
|
|
|
if pid in active_pids:
|
|
if pid in active_pids:
|
|
|
continue
|
|
continue
|