video: drain libavcodec's reorder buffer at end-of-stream

Closes H1 from the pre-ship review (the known-limit doc note added in
c0d55ba). The previous workaround was "first play-through truncates
the last ~16 frames; replay is fine because flush_buffers clears
libavcodec." That trade-off was OK for shipping but the proper fix
is to drain the reorder buffer before propagating EOS to ExoPlayer.

Media3's SimpleDecoder short-circuits the end-of-stream input buffer
and never invokes the subclass's decode(), so there's no hook to send
avcodec_send_packet(NULL). Every override worth overriding (decode
loop, queue methods, flush) is final on SimpleDecoder. So we
vendor a copy as FfmpegSimpleDecoder (Apache 2.0 attribution at the
top of the file) with one structural change: an EOS-drain state. On
EOS input, signalEndOfInput() flushes libavcodec's reorder queue,
then drainAtEndOfStream() is called on successive output buffers
until it reports DRAIN_DONE — at which point the loop attaches
BUFFER_FLAG_END_OF_STREAM and resumes normal teardown.

Everything else mirrors SimpleDecoder verbatim so upstream
improvements are cheap to pull forward.

  - FfmpegSimpleDecoder.java: vendored base class.
  - ffmpegVideoSignalEos JNI: sends avcodec_send_packet(NULL).
  - FfmpegVideoDecoder: extends the new base; signalEndOfInput
    forwards to the JNI; drainAtEndOfStream re-uses the existing
    ffmpegVideoReceiveFrame so per-frame PTS recovery and the
    pending_frame path from c0d55ba continue to work during drain.
This commit is contained in:
agra
2026-05-29 07:45:48 +03:00
parent c0d55babf3
commit dc47fc0159
3 changed files with 422 additions and 11 deletions

View File

@@ -444,6 +444,26 @@ VIDEO_DECODER_FUNC(jint, ffmpegVideoReceiveFrame, jlong handle,
return VIDEO_DECODER_SUCCESS;
}
// Signals libavcodec to flush its reorder buffer so the Java side can
// pull the remaining frames out via successive ffmpegVideoReceiveFrame
// calls. Used during end-of-stream drain: SimpleDecoder's EOS short-
// circuit hides the EOS input from our decode() override, so without
// this hook libavcodec's buffered tail (~16 frames for iOS H.264
// High@3.1) would be lost on first play-through.
VIDEO_DECODER_FUNC(jint, ffmpegVideoSignalEos, jlong handle) {
if (!handle) {
LOGE("ffmpegVideoSignalEos: null handle");
return VIDEO_DECODER_ERROR_OTHER;
}
UxFfmpegVideoContext* ctx = (UxFfmpegVideoContext*)handle;
int result = avcodec_send_packet(ctx->codec_ctx, nullptr);
if (result < 0 && result != AVERROR_EOF) {
logError("avcodec_send_packet(NULL)", result);
return transformError(result);
}
return VIDEO_DECODER_SUCCESS;
}
VIDEO_DECODER_FUNC(void, ffmpegVideoFlush, jlong handle) {
if (!handle) return;
UxFfmpegVideoContext* ctx = (UxFfmpegVideoContext*)handle;