Version
3.2.0
Environment that reproduces the issue
Samsung Galaxy S10 (SM-G973F) - Android 12
RTMP/SRT/... Server
SRT to MediaMTX v1.21.0 (self-hosted) over a real mobile/variable-bandwidth link, re-transported as RTSP and read via SRT
Audio configuration
No audio (video-only stream)
Video configuration
- HEVC (H.265)
- profile: Main
- resolution: 1920x1080 (adaptive down to 1280x720 based on measured bitrate)
- fps: 30
- bitrate: 4 Mbps target
- gopDurationInS: 1.0
Is it reproducible in the demos application?
Yes
Reproduction steps
- Stream over a link with limited/variable bandwidth (e.g. a real mobile network, or an artificially throttled link below the configured bitrate) using StreamPack's SRT (or RTMP) endpoint.
- On the server side, have it re-transport the incoming stream as RTSP (or SRT read-pull) to a third client, while also serving it via a segmented protocol (e.g. HLS) for comparison.
- Capture the raw encoded frames being written to the SrtSink (or equivalent endpoint sink) — e.g. by hooking IEndpointInternal.write()/Sink.write() and dumping the packet bytes with timestamps — and look at the time gaps between consecutive video access units.
Expected result
The encoder should keep producing output frames at a steady rate (matching the configured fps) regardless of transient network slowness, decoupling encode throughput from network throughput — as RootEncoder does.
Actual result
Under real network backpressure, the H.265 elementary stream captured at the sink shows bursts of exactly ~4 frames (matching MediaCodec's small output buffer pool, typically ~4 buffers) followed by multi-second gaps of complete silence, then a fresh keyframe. Camera/surface input keeps feeding continuously and unaffected during these gaps (confirmed via app-side capture logs) — only the encoder's output stalls.
Root cause: FrameFactory.createFrame() in MediaCodecEncoder.kt wraps codec.getOutputBuffer(index) by reference (zero-copy) and only calls codec.releaseOutputBuffer(index, false) inside the Frame's onClosed callback, which fires only after the frame is fully consumed by the entire downstream pipeline (channel -> muxer -> endpoint -> network write/send). If that final network write blocks for any real amount of time (e.g. SrtSink.write() -> socket.send() under congestion), MediaCodec exhausts its small output buffer pool and stops calling onOutputBufferAvailable entirely until the network call returns.
Impact: any output expecting a continuous stream (RTSP, SRT read/pull) breaks or times out during these stalls (mediamtx's RTSP/SRT-read sessions on our end opened and closed within ~4s, delivering zero bytes), while segmented outputs (HLS) mask the problem by just starting a new segment/muxer session.
Additional context
Isolated by comparing against RootEncoder (pedroSG94/RootEncoder) under the exact same network conditions, which does not exhibit this stall — it copies encoder output into a queue immediately (BaseSender's queue.take() producer/consumer pattern), decoupling encode speed from network speed.
We patched this locally in FrameFactory.createFrame() by copying the buffer content into a new ByteBuffer immediately and releasing the MediaCodec output buffer right away, instead of deferring release to Frame.close():
private fun createFrame(
codec: MediaCodec, index: Int, ptsInUs: Long, isKeyFrame: Boolean, tag: String
): Frame {
val buffer = requireNotNull(codec.getOutputBuffer(index))
val extra = if (isKeyFrame || !isVideo) extra!! else null
val sourceBuffer = if (extraBuffers != null) buffer.removePrefixes(extraBuffers) else buffer
val rawBuffer = ByteBuffer.allocate(sourceBuffer.remaining())
rawBuffer.put(sourceBuffer)
rawBuffer.rewind()
try {
codec.releaseOutputBuffer(index, false)
} catch (t: Throwable) {
Logger.w(tag, "Failed to release output buffer for code: ${t.message}")
}
return pool.get(rawBuffer, ptsInUs, null, isKeyFrame, extra, outputFormat)
}
This adds one buffer copy per frame but fixes the stall reliably in our testing (verified with two consecutive 20s RTSP captures showing continuous ~30fps delivery with no gaps, vs. 8 frames total with multi-second gaps before the fix). Note this issue is independent of the PCR overflow bug we reported separately (#301) — we hit both in the same investigation but they have distinct root causes and this one is the more impactful of the two.
Relevant logs output
Version
3.2.0
Environment that reproduces the issue
Samsung Galaxy S10 (SM-G973F) - Android 12
RTMP/SRT/... Server
SRT to MediaMTX v1.21.0 (self-hosted) over a real mobile/variable-bandwidth link, re-transported as RTSP and read via SRT
Audio configuration
No audio (video-only stream)
Video configuration
Is it reproducible in the demos application?
Yes
Reproduction steps
Expected result
The encoder should keep producing output frames at a steady rate (matching the configured fps) regardless of transient network slowness, decoupling encode throughput from network throughput — as RootEncoder does.
Actual result
Under real network backpressure, the H.265 elementary stream captured at the sink shows bursts of exactly ~4 frames (matching MediaCodec's small output buffer pool, typically ~4 buffers) followed by multi-second gaps of complete silence, then a fresh keyframe. Camera/surface input keeps feeding continuously and unaffected during these gaps (confirmed via app-side capture logs) — only the encoder's output stalls.
Root cause: FrameFactory.createFrame() in MediaCodecEncoder.kt wraps codec.getOutputBuffer(index) by reference (zero-copy) and only calls codec.releaseOutputBuffer(index, false) inside the Frame's onClosed callback, which fires only after the frame is fully consumed by the entire downstream pipeline (channel -> muxer -> endpoint -> network write/send). If that final network write blocks for any real amount of time (e.g. SrtSink.write() -> socket.send() under congestion), MediaCodec exhausts its small output buffer pool and stops calling onOutputBufferAvailable entirely until the network call returns.
Impact: any output expecting a continuous stream (RTSP, SRT read/pull) breaks or times out during these stalls (mediamtx's RTSP/SRT-read sessions on our end opened and closed within ~4s, delivering zero bytes), while segmented outputs (HLS) mask the problem by just starting a new segment/muxer session.
Additional context
Isolated by comparing against RootEncoder (pedroSG94/RootEncoder) under the exact same network conditions, which does not exhibit this stall — it copies encoder output into a queue immediately (BaseSender's queue.take() producer/consumer pattern), decoupling encode speed from network speed.
We patched this locally in FrameFactory.createFrame() by copying the buffer content into a new ByteBuffer immediately and releasing the MediaCodec output buffer right away, instead of deferring release to Frame.close():
private fun createFrame(
codec: MediaCodec, index: Int, ptsInUs: Long, isKeyFrame: Boolean, tag: String
): Frame {
val buffer = requireNotNull(codec.getOutputBuffer(index))
val extra = if (isKeyFrame || !isVideo) extra!! else null
val sourceBuffer = if (extraBuffers != null) buffer.removePrefixes(extraBuffers) else buffer
}
This adds one buffer copy per frame but fixes the stall reliably in our testing (verified with two consecutive 20s RTSP captures showing continuous ~30fps delivery with no gaps, vs. 8 frames total with multi-second gaps before the fix). Note this issue is independent of the PCR overflow bug we reported separately (#301) — we hit both in the same investigation but they have distinct root causes and this one is the more impactful of the two.
Relevant logs output