Skip to content

Streaming improvements: 4G quality tuning and stream service robustness #375

Description

@MateoLostanlen

Streams are sent over 4G links with variable quality. After reviewing pyro_camera_api/services/stream.py, core/config.py and services/anonymizer_rtsp.py, here is a list of potential improvements to study, split between encoding/transport tuning and code robustness.

Quality / 4G transport tuning

1. SRT_LATENCY = 50 is too low for 4G (biggest lever)

SRT latency is the retransmission window: a lost packet can only be recovered within that budget. On 4G (RTT 50–150 ms with jitter), 50 ms means most lost packets are unrecoverable → corruption and freezes when the connection dips. Rule of thumb is 3–4× RTT; for mobile links 400–800 ms is standard. Only cost is a fixed extra delay on the viewer side. (core/config.py)

2. Restream command has no -maxrate / -bufsize

build_ffmpeg_restream_cmd only sets -b:v 700k (average target, unconstrained VBV). Keyframes and scene changes can burst to several Mbps, which is exactly what saturates a weak uplink and triggers loss. The anonymizer encoder already does it right (maxrate 900k, bufsize 800k); the restream path should match.

3. Consider capped CRF instead of fixed bitrate

Scenes are mostly static landscape. With ABR at 700k we spend 700k even when nothing moves. Capped CRF (-crf 23 -maxrate 900k -bufsize 800k) gives equal-or-better quality, lower bitrate on static scenes (good for 4G data budgets), and the cap still protects the link during motion. build_encoder_cmd already supports it (use_crf=True), it is just not enabled.

4. Re-evaluate tune zerolatency + rc-lookahead=0

This disables mbtree and lookahead, costing roughly 10–20% quality per bit. Justified if an operator steers PTZ live with sub-second glass-to-glass; but once SRT latency is raised to ~500 ms (point 1), the ~100–200 ms saved by zerolatency is no longer the dominant term. If the stream is mostly for verification/monitoring, dropping it buys quality for free.

5. Use available CPU headroom for a slower preset

SystemController skips inference while a stream is active, so the CPU is mostly idle during streaming. At 640×360@10fps, preset fast (or even medium) instead of veryfast should be affordable and gives better quality at the same bitrate. Needs a CPU test on the actual device.

6. Source quality is the ceiling

Both modes ingest the Reolink substream (h264Preview_01_sub), already heavily compressed by the camera. Re-encoding a mushy ~500k source at 700k cannot restore detail and adds a transcode generation loss. Options: raise the substream bitrate/resolution in the camera settings (cheap, big win), or for the non-anonymized path use -c:v copy restream (zero transcode loss, near-zero CPU) and control bandwidth via the camera encoder settings.

Minor

  • gop_size 14 at 10 fps = keyframe every 1.4 s; fine for fast viewer join, but each I-frame is a bitrate spike. With VBV caps in place, a ~2–3 s GOP would give a small quality gain.
  • pkt_size=1316 is correct for SRT over typical MTU.

Code robustness (services/stream.py)

7. Half-dead pipelines are never stopped

is_pipeline_running requires both decoder and encoder threads alive. If one crashes, stop_any_running_stream skips the pipeline: the surviving thread keeps pushing to SRT forever, the entry stays in workers, /stream/status reports no stream, and the engine resumes inference while the encoder still consumes bandwidth. Stop logic should stop a pipeline if at least one thread is alive.

8. No locking around start/stop

start_stream, stop_stream and the idle-stopper thread all mutate app.state.stream_workers / stream_processes without a lock. Two near-simultaneous starts (or a start racing the idle stopper) can both pass the "already running" check and spawn two pipelines to the same SRT output. A single threading.Lock around check-then-start and stop would fix it.

9. Killed ffmpeg processes are never reaped

In stop_any_running_stream, after proc.kill() there is no proc.wait(), and the entry is popped right after, so the process stays a zombie until the API restarts. Also worth closing proc.stderr so the log thread exits promptly.

10. is_thread_alive pokes at the private _thread attribute

Fragile coupling to the worker classes. Add an is_alive() method on RTSPDecoderWorker / EncoderWorker / AnonymizerWorker instead.

11. Avoidable _APP global

set_app_for_stream exists only so stop_stream_if_idle can find the app. main.py starts that thread itself and could pass the app as an argument, removing the global and the app=None fallback.

Suggested first pass

Low-risk, directly addressing bad-connection days: bump SRT_LATENCY to ~500, add maxrate/bufsize to the restream command, enable capped CRF on the anonymizer encoder, and fix the half-dead pipeline stop logic (point 7).

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions