Skip to content

[Bug]: MediaCodecEncoder holds MediaCodec output buffer until fully written downstream, stalling the encoder under network backpressure #302

Description

@filippoadessi

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

  1. 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.
  2. 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.
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions