fix: pass videoCodec=H265 to JMuxer for HEVC streams (closes #23)#24
fix: pass videoCodec=H265 to JMuxer for HEVC streams (closes #23)#24josha wants to merge 4 commits into
Conversation
JMuxer defaults videoCodec to "H264"; the muxed client handler never overrode it, so HEVC cameras (T86P2/4G LTE Cam S330, S340 in H.265 mode) produced fMP4 with an avcC sample description over HEVC NAL units. The Rebroadcast plugin's downstream ffmpeg then ran into "timeout waiting for data" and HomeKit/WebRTC live view never started. Snapshot path was unaffected (writes raw bitstream to ffmpeg with -f hevc) but appeared to fail because Rebroadcast wedged the upstream session. Codec isn't known when the muxer client first connects, so handleMuxedClient is now async: register the socket as pending (counts as a consumer so the upstream livestream still starts), await waitForVideoMetadata, then build the JMuxer with videoCodec="H264" or "H265". Also tightens the H.265 snapshot resolver to require an actual IRAP slice (types 16-23) rather than accepting parameter-set-only events. T86P2 happens to bundle VPS+SPS+PPS+IDR so this didn't bite, but cameras that deliver parameter sets out-of-band would resolve snapshots with bytes ffmpeg couldn't decode. Plus: log the actual captured-stream codec in "Captured ... keyframe" instead of always saying "H.264". H.264 path is unchanged. Tested live on T86P2 (H.265-only) and S340 (both H.264 and H.265 modes).
When the upstream HomeBase / eufy-security-ws is wedged (accepts CMD_START_REALTIME_MEDIA but returns no P2P data, observed as 'Stopping the station stream ... no data for 5 seconds' / 'Result data for command not received' in the WS server logs), the previous ensureLivestreamState recursion would issue another startLivestream every 30 seconds forever as long as any consumer kept livestreamIntendedState=true. Each extra command piles backpressure onto an already-stuck HomeBase and prolongs the wedge. Add a consecutive-no-data counter. After MAX_NO_DATA_STARTS (=3) back-to-back startLivestream attempts that never produced a video event, give up: clear intent, emit streamError, and stop the recursive 30s timer. Counter resets to zero the instant video data actually arrives (livestreamActualState=true), so this only kicks in for genuinely-stuck sessions, not slow-warmup ones. Recovery is consumer-initiated: a fresh muxer or snapshot request sets intent back to true and the cycle restarts. Combined with a physical HomeBase reset (the actual fix for a wedged upstream), this keeps the plugin from making the problem worse while the user recovers the hardware.
Battery / cellular Eufy cameras (T8170 S340 deep-sleep wake, T86P2 4G LTE Cam S330 cold P2P) routinely take 30–45s to deliver their first IDR after startLivestream. Observed empirically: Patio (T86P2) consistently delivered video on the third 30s retry of ensureLivestreamState (≈45s after the request), with the camera responding correctly to the underlying eufy-security-ws commands the whole time. The previous 15s timeouts caused two failure modes during that warm-up window: 1. `handleMuxedClient` timed out and built JMuxer with the wrong default codec (H.264) just before real H.265 frames arrived. The fMP4 it produced was un-decodable; Rebroadcast/WebRTC saw no usable output and gave up. 2. `captureSnapshot` timed out and reported "no keyframe received" even though one was about to arrive seconds later. Bumping both defaults to 60s lets the camera's natural startLivestream retry cycle complete before either consumer abandons the session. Existing user-supplied timeouts (via RequestPictureOptions.timeout) are respected unchanged.
|
Adding a third commit based on live-camera evidence from this evening's testing:
Battery and 4G LTE Eufy cameras (T8170 SoloCam S340 deep-sleep wake, T86P2 4G LTE Cam S330 cold P2P) routinely take 30–45s to deliver their first IDR after the upstream-issued With the previous 15s timeouts:
Bumping both Verified end-to-end live in HomeKit on Front Door (T86P2 H.265): live view, snapshot, audio all working after this change. |
When bropat's P2P session to a station goes zombie (startLivestream acked but no LIVESTREAM_VIDEO_DATA events flow), the plugin now detects the wedge and recycles the per-station P2P session via station.disconnect/connect — recovering without a HomeBase reboot or plugin reload. Two detection paths, both routing through a shared markUpstreamWedged(): - Cold-start: existing 3-consecutive-no-data-starts counter. - Mid-session: new data-flow watchdog tracking lastVideoDataAt. Fires when intent=true, prior data existed, >15s since last frame, and consumers are still attached. Catches the common "stream was working, then stopped" pattern the counter misses. Recycle waits for the actual STATION_EVENTS.CONNECTED event (or CONNECTION_ERROR / 30s timeout) before declaring complete, since station.connect() returns when bropat accepts the command, not when P2P is re-established (10–25s for cellular cameras / cold HomeBase). Stream server defers startLivestream while a recycle is in flight, and auto-arms once it clears if consumers are still waiting — so a HomeKit session arriving mid-recovery gets video without the user retrying. Battery-safe gating: watchdog requires totalConsumers > 0; no auto-restart of the livestream after recycle in absence of consumers; recycle is rate-limited to once per 5 minutes per device. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Fixes #23 — H.265 streams (T86P2 4G LTE Cam S330, T8170 SoloCam S340 in H.265 mode) reach the plugin correctly identified as
codec: H265but downstream consumers (Rebroadcast prebuffer, HomeKit, WebRTC) time out waiting for fMP4 frames they can never decode.Root cause:
JMuxerdefaults itsvideoCodecoption to"H264". The currenthandleMuxedClientnever overrides it, so the muxer builds an AVCC sample description over HEVC NAL units and emits un-decodable fMP4. H.264 cameras happen to work because the default matches reality.Fix: Make
handleMuxedClientasync, hold the socket in a newpendingMuxerSocketsset (so it still counts as a consumer and the upstream livestream starts), awaitwaitForVideoMetadata, then constructJMuxerwithvideoCodec: "H265"when the stream metadata says HEVC.Commits
fix(eufy-stream-server): pass videoCodec=H265 to JMuxer for HEVC streamsCaptured H.264 keyframelog to use the actual stream codecfix(eufy-stream-server): cap startLivestream retries on wedged upstreamconsecutiveNoDataStartscounter; after 3 consecutivestartLivestreamattempts with no video data, emitstreamErrorand stop the recursive 30s retry timerlivestreamActualStateflips trueacknowledged: truebut returns no data — observed ineufy-security-wslogs asResult data for command not received/Stopping the station stream ... no data for 5 seconds). Live diagnostic showed this pattern drained battery-camera batteries during multi-hour test sessionsDiagnostic methodology
I instrumented the live stream pipeline with per-event byte/NAL-type logging (temporary
diagnosticbranch, not in this PR) on real T86P2 + T8170 cameras connected to a HomeBase S380. The logs definitively showed:start=Annex-B/4Bon every event)-f hevc)This rules out the keyframe-detection hypothesis and the codec-misidentification hypothesis from the earlier triage. The actual bug is one line of muxer wiring.
Hypothesis disposition (from the issue)
IDR_W_RADL(19)correctly detected; snapshots succeedevent.metadata.videoCodecis correctly"H265"videoCodec: "H265"produces fMP4 with the wrong sample description box; live consumers can't decode itWhy the previous "NAL type 14 / data partitioning" patch existed
H.265 PREFIX_SEI_NUT (NAL type 39) has first-byte
0x4E. Interpreted as H.264 (which expectsbyte0 & 0x1Ffor the type), that's type14= Data Partitioning. The earlierERROR_RESILIENT_CAMERA_TYPES/ NAL-type-14 fix was masking the underlying codec confusion for some streams; this PR addresses it at the actual source.Verification
JMuxer emitting fMP4 (first chunk: ... codec=H265 ...)log line observed for all H.265 cameras after this fixeufy-security-wsdelivers audio metadata but never invokes theLIVESTREAM_VIDEO_DATAevent. This appears to be an upstream / Eufy P2P bridge reliability issue, not a plugin-layer issue; verified by attaching diagnostic logging and observing zero video events while audio metadata is captured. Left to upstream maintainers (bropat / eufy-security-client).Notes for reviewers
packages/eufy-stream-server/src/jmuxer.d.tsgains avideoCodecfield. JMuxer 2.1.0 (current dep) supports"H264"and"H265"natively via separate remuxers; no upstream change needed.handleMuxedClientis nowasyncand waits up to the defaultwaitForVideoMetadatatimeout (10s) before either constructing the muxer with the correct codec or closing the socket. Closing on timeout is intentional — it lets the downstream consumer (Rebroadcast) retry rather than building a wrong-codec muxer that wedges the consumer.🤖 Generated with Claude Code