Skip to content

perf(stream): share H264 capture frames & enhance H264(Webrtc) frame rate - #888

Merged
imguoguo merged 2 commits into
sipeed:mainfrom
watermeko:perf/shareh264source
Sep 1, 2026
Merged

perf(stream): share H264 capture frames & enhance H264(Webrtc) frame rate#888
imguoguo merged 2 commits into
sipeed:mainfrom
watermeko:perf/shareh264source

Conversation

@watermeko

@watermeko watermeko commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Share a single H.264 capture source between Direct and WebRTC streaming.
  • Reduce unnecessary runtime dependencies from libkvm.so.

Details

The Direct and WebRTC streaming paths previously read H.264 frames independently from the same encoder queue. When both modes were active, consumers could compete for frames and receive incomplete streams.

This change routes both paths through a shared H.264 source so the encoder is read only once per frame.

Each H.264 subscription now uses a bounded non-blocking hand-off. If a subscriber falls behind and its queue overflows, buffered frames are discarded and that subscriber waits for the next keyframe before receiving P-frames again. This prevents a slow WebRTC consumer from applying backpressure to the shared source and interrupting Direct viewers.

The WebRTC interceptor configuration is kept intentionally narrow:

  • NACK generation and response are enabled for packet-loss recovery.
  • RTCP Sender and Receiver Reports are enabled.
  • TWCC and unrelated default interceptors are not enabled because they add significant CPU overhead on the target device and are not currently consumed by the application.

The RTP packetization MTU is set to 1200 to remain safe across IPv6, TURN, VPN, and other paths with reduced MTU.

The libkvm.so link now uses --as-needed. This removes unused OpenCV dependencies from the final ELF, including libopencv_video.so.409, without affecting the OpenCV symbols used by the library.

Route Direct and WebRTC consumers through one H264 capture source to avoid duplicate VENC reads. Overlap WebRTC capture with ordered RTP/SRTP writes and packetize each access unit once for all WebRTC clients.

Add libopencv_video.so.409 into dl_lib.
@yuzi-co

yuzi-co commented Aug 28, 2026

Copy link
Copy Markdown

Sharing one capture between the two H.264 paths is the right idea. direct/streamer.go and webrtc/manager.go each run their own ticker and each call ReadH264 on the same encoder, so two viewers on different modes make two readers compete for one frame queue and each gets a subset. Worth fixing.

Three things I would look at before this lands.

A slow WebRTC peer stalls the direct viewers. H264Subscription.send selects on s.frames <- frame and <-s.done with no default, so it blocks whenever the 4-deep channel is full. Behind it, sendVideoStream does samples <- sample on a 1-deep channel that the writer goroutine drains with a track write. So one peer on a bad link fills samples, which blocks sendVideoStream, which stops draining subscription, which fills frames, which blocks H264Source.run. The direct viewers share that loop, so they stop getting frames too. The old code had each path independently bounded by its own ticker, so this is a new coupling rather than a pre-existing one.

A non-blocking hand-off fixes it: a one-deep slot per subscriber that either refuses a frame while one is pending or replaces it, and a counter for what was dropped. The subscriber that fell behind then needs a keyframe, which the source already knows about from result == 3.

libopencv_video.so.409 is not a dependency of libkvm.so. The committed library records it in DT_NEEDED, but nothing in it is used. Checked against the exact binary this PR adds:

libkvm.so undefined symbols:       320
libopencv_video exported FUNCs:    694
intersection:                        0

The entry comes from MaixCDK: its vision component requires the whole opencv package, so the linker writes one NEEDED per opencv module whether or not a symbol is taken from it. On the device it resolves from /usr/lib and drags in libopencv_dnn, libopencv_calib3d, libopencv_features2d and libopencv_flann behind it, so the loader maps about 6 MB that nothing calls, on a board with roughly 158 MB of usable RAM after the ION carveout.

I understand why the file is here: the cross-linker reports every dependency as not found without it. Removing the spurious entry does the same job and costs no repository space:

patchelf --remove-needed libopencv_video.so.409 libkvm.so

The frame rate gain is partly loss repair being switched off. webrtc.WithInterceptorRegistry(&interceptor.Registry{}) replaces the default registry with an empty one, which takes out the NACK responder and TWCC along with everything else. Higher throughput on a clean link, nothing to repair a dropped packet on a link that is not clean. If the goal is only to stop the default interceptors re-packetising, RegisterDefaultInterceptors on a registry with the parts you want is narrower than an empty one. Same question for dropping the playout-delay extension: worth saying in the description that the 60 FPS number is measured with those off.

For context, MaxPacketSize going 1200 to 1450 is above the 1280 byte IPv6 minimum MTU, so a v6 path without PMTUD working will fragment or drop these.

@watermeko

Copy link
Copy Markdown
Collaborator Author

@yuzi-co Your response has been very helpful! I’ll take your advice and submit more suitable code later.

Keep slow WebRTC consumers from blocking the shared H264 source by dropping buffered frames until the next keyframe. Restore the WebRTC NACK and RTCP report interceptors while retaining a safe RTP MTU, and enable linker --as-needed for libkvm to remove unused OpenCV dependencies.
yuzi-co added a commit to yuzi-co/IronKVM that referenced this pull request Aug 28, 2026
Direct mode and WebRTC mode each ran their own ticker and each called
ReadH264. There is one encoder behind that call and it hands whichever
caller asks whatever frame is ready, so two loops do not each receive the
stream: they divide it. A viewer on direct mode and a viewer on WebRTC
at the same time therefore each got roughly every second frame of a GOP,
which decodes to nothing useful, and the board paid for the capture twice
to produce it. The frame rate counter was updated by both loops as well,
so the figure reported was double the frames captured.

stream.H264Source is one capture loop for both. It starts with the first
subscriber and stops after the last one leaves, and it owns the ticker,
the screen snapshot and the FPS changes that the two loops each kept a
copy of.

The hand-off is a FrameSlot, not a channel the producer waits on. The
capture loop must never block on a delivery path: the two paths share it
now, so a WebRTC writer waiting on a slow peer would otherwise stop the
direct viewers as well. A path that is not ready has the frame refused
and counted, which is the policy FrameSlot already documents for H.264
and the same one each path uses for its own clients.

Demand is asked before the encoder is read, because a read costs the
board whether or not anyone takes the result. Direct mode acknowledges
frames and stops asking while a viewer is behind, and that gate survives:
the source reads nothing while every subscriber is quiet.

Each path still reports capture status under its own mode, so a failed
read is delivered rather than swallowed. A status nobody reports is a
stream that fails with nothing said.

The delivery loops now wait on frames rather than on a ticker, so they
need their own way to notice the last viewer leaving. A one second idle
tick does it. Nothing is being served in the meantime, and stopIfIdle
rechecks the client count under the mutex, so a viewer arriving inside
that second is not stranded.

The idea is upstream PR sipeed#888. Its own hand-off blocks the capture loop on
a full four-deep channel, with a WebRTC track write behind that, so one
slow peer stalls the direct viewers too. That part is not taken.
@imguoguo
imguoguo merged commit 7f95fe9 into sipeed:main Sep 1, 2026
1 check passed
@watermeko
watermeko deleted the perf/shareh264source branch September 1, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants