fix(android): fast step-down + opt-in HD repair for a main with no fmtp (#590) - #591
Merged
Conversation
A camera whose H.265 main advertises an RTSP SDP with no `a=fmtp` (parameter sets only in-band, seen on a Uniview LPR main) is rejected by Media3's RTSP client with `IllegalArgumentException: missing attribute fmtp`. Two problems, two fixes: 1. Fast step-down (client, on by default). The failure surfaces wrapped in an IO code (2000), so the reactive ladder retried it across the whole back-off curve (~30 s) before dropping to the H.264 sub. It is deterministic, so add `missing attribute fmtp` to the step-down signatures next to `processAggregationPacket`: the broken main now drops to SD on the first failure. Matched on the exception-chain message only, not by widening any error code, so the #560 eager-SD fix is preserved. 2. Opt-in HD repair (server, default off). A per-camera, detection-driven repaired main `<name>_mainv`, advertised as `rtsp_mainv_url` and tried by Android right after the raw main (`main -> mainv -> subv -> sub -> mobile`). It is a full-res H.265->H.264 transcode gated behind `MAIN_REPAIR_TRANSCODE_ENABLED` (default off) because it costs recorder CPU while a fullscreen viewer is attached. Detection reuses `sdp_video_lacks_fmtp` on the main producer, sticky-unknown like `_subv`. Why a transcode and not the cheaper `_subv`-style copy: verified against the real stream (ffprobe + RTP capture) that a copy-remux restores fmtp but go2rtc then emits an HEVC parameter-set Aggregation Packet at each keyframe that Media3 cannot depacketize, so only a re-encode plays on Android. With the repair off (default) nothing changes except the instant step-down; on, the SD downgrade becomes HD at recorder CPU cost. The cheapest fix of all, when the camera allows it, is still to set its main to H.264 in the camera UI. Tests: LiveStreamFallbackTest (fmtp fast step-down, MAINV chain ordering/badge); go2rtc mainv_name/mainv_src (transcode-not-copy, no alias collision); playback mainv_url gating. Docs: DECISIONS, COMPONENT-MAP, camera-compatibility.json, .env.example, docker-compose.yml, environment-reference.md. Fixes #590 Signed-off-by: badbread <badbread@users.noreply.github.com>
# Conflicts: # docs/DECISIONS.md
# Conflicts: # docs/DECISIONS.md
badbread
added a commit
that referenced
this pull request
Aug 8, 2026
#592) * fix(api): flag the _mainv repair from the SERVED SDP, not the producer #591's _mainv detection reused sdp_video_lacks_fmtp on the MAIN producer SDP. The reference LPR camera (Uniview H.265) publishes a producer fmtp with sprop-sps+sprop-pps but no sprop-vps, so the producer verdict is Some(false) ("has fmtp") and the camera is never flagged -- yet go2rtc, unable to build a complete HEVC parameter set, serves consumers an SDP with no a=fmtp at all, which is what makes Media3 throw 'missing attribute fmtp'. The merged repair was therefore inert for exactly the camera it targeted. Detect on the SDP go2rtc SERVES to RTSP consumers instead: add stream_served_video_lacks_fmtp + StreamIndex::video_lacks_fmtp_served, used for _mainv only. _subv keeps the producer side (a sub is often un-consumed, so it has no served SDP); the main is always consumed by the recorder, so its served SDP is reliably present. Verified against the live prod go2rtc: LPR served SDP lacks fmtp while frontyard/driveway/garage (H.265, playable) serve a complete fmtp with sprop-vps. Unit tests use the real captured SDPs. DECISIONS.md corrected. Signed-off-by: badbread <badbread@users.noreply.github.com> * review: sync mainv docs to served-SDP, soften 'always consumed', add no-verdict debug log Addresses the Fable review of #592: - config.rs / dto.rs doc comments still described PRODUCER-side detection (sdp_video_lacks_fmtp); point them at stream_served_video_lacks_fmtp / the served SDP (same-change doc-sync). - 'the main is always consumed by the recorder' is only true for the default record-from-main policy; soften in the go2rtc.rs docstring, the reconcile comment, and the DECISIONS correction (a record-from-sub main has no RTSP consumer -> served verdict None -> unrepaired, by design). - add a reconcile debug log when the repair is enabled but a main has no served-SDP verdict, so 'enabled but silently does nothing' is observable. - extra unit cases: webrtc-before-rtsp ordering, and a mid-handshake rtsp consumer (no sdp) skipped for the next one. record-from-sub main-repair blindness and the sub-side incomplete-fmtp class are tracked as follow-ups, not this PR. Signed-off-by: badbread <badbread@users.noreply.github.com> --------- Signed-off-by: badbread <badbread@users.noreply.github.com> Co-authored-by: badbread <badbread@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #590.
Two independent fixes for a camera whose main stream advertises an RTSP SDP
with no
a=fmtp(parameter sets in-band only), which Media3's RTSP client rejectswith
IllegalArgumentException: missing attribute fmtp. Captured on device via theCrumbLiveFallbacklogging (#561) on a Uniview H.265 LPR main.1. Fast step-down (client, ships on)
The failure arrives wrapped in an IO error code (
2000), so the reactive fallbackladder retried it across the whole back-off curve (~30 s) before dropping to the
H.264 sub. It is deterministic (identical SDP every attempt), so
missing attribute fmtpis added toLiveStreamFallback's deterministic step-down signatures next toprocessAggregationPacket. The broken main now steps down to SD on the firstfailure.
Narrow by design: matched on the exception-chain message, not by widening any error
code to step down — that would reintroduce the #560 eager-SD regression. A unit
test pins that an fmtp-signature error steps down immediately while a generic 2000
IO error still needs the multi-failure threshold.
2. Opt-in HD repair
_mainv(server, default OFF)A per-camera, detection-driven repaired main
<name>_mainv, advertised asrtsp_mainv_urland tried by Android right after the raw main(
main → mainv → subv → sub → mobile; wall no-submain → mainv → mobile). It is afull-resolution H.265→H.264 transcode, gated behind
MAIN_REPAIR_TRANSCODE_ENABLED(defaultfalse) because it costs recorder CPU(libx264) while a fullscreen viewer is attached — go2rtc spawns it lazily, so idle
cost is zero. Detection reuses
sdp_video_lacks_fmtpon the MAIN producer (alwayswarm, the recorder consumes the main through go2rtc), sticky-unknown exactly like
_subv.Why a transcode and not the cheaper
_subv-style copyVerified empirically against the real LPR stream (ffprobe + tshark RTP capture on
the recorder's embedded go2rtc):
ffmpeg:<name>#video=copyprocessAggregationPacketffmpeg:<name>#video=h264(this PR)So a copy-remux only trades the fmtp rejection for an Aggregation-Packet crash
(androidx/media#1008). Only a re-encode plays on Android. Full analysis in the
docs/DECISIONS.md2026-08-08_mainventry.Behavior
fmtp-less main is SD (H.264 sub) on Android, same as before but immediate.
camera UI (no server transcode). Documented in
camera-compatibility.jsonand theenv reference.
Tests / verification (gate green on dev2)
cargo fmt --all --check,clippy --all-targets -D warnings,cargo test --workspaceagainst a throwaway Postgres (DB-backed tests confirmed to run). New:mainv_name_and_src_shapes,mainv_src_is_a_transcode_not_a_copy,mainv_src_is_never_an_rtsp_alias_collision,rtsp_mainv_url_*../gradlew testDebugUnitTest assembleDebug. New:the missing-fmtp failure steps down at once…,fullscreen chain puts the repaired main right after the raw main,metered fullscreen keeps the repaired main a last resort…,a no-sub camera puts the repaired main before the transcode…,the repaired main rung is absent when the server does not publish one.docker compose configvalidated on real Docker.What still needs the maintainer's phone: end-to-end HD playback of the
transcoded
_mainvon the LPR (enableMAIN_REPAIR_TRANSCODE_ENABLED, redeploy,open the LPR fullscreen on Android). The client fast-step-down and all stream-level
facts (SDP fmtp, FU-vs-AP packetization) are verified on the server; only the
on-device render of the transcoded main is unverified.
Do not merge — for maintainer review.