Files
plezy/android/app/proguard-rules.pro
T
edde746 1995fc6b75 refactor(player): demux through random-access IO instead of unwinding libavformat
Resuming or seeking a large MKV on the Android FFmpeg demux path buffered
for minutes with no error and no fallback. The cause was the seam, not the
container: libavformat's demuxers own their seeking — end-of-file index,
back to the header, binary search — while media3's Extractor is forward
only, so every backward jump had to abort the in-flight libavformat call
with a synthetic IO error and replay the whole open. matroskadec attempts
its deferred Cues parse exactly once per context, so that abort burned the
index for the session and every later seek fell back to libavformat's
linear generic scan, reading the file up to the target.

That protocol had produced a bug of this shape repeatedly (a NULL deref
resuming find_stream_info across an abort, a use-after-free on fallback
teardown, matroskadec resync skipping the keyframe cluster) and it was held
together by four empirical retry budgets whose exhaustion degraded silently
— which is why #2096 looked like an unbounded spinner instead of an error.

Give libavformat what it requires instead. FfmpegRandomAccessSource serves
any absolute position from a second DataSource built by the same factory
media3 uses, cloning the DataSpec media3 opened for the item so Cronet,
download caches, SAF and per-item request headers still apply. Reads that
the loader's ExtractorInput can serve still go through it, so sample
delivery keeps feeding media3's byte accounting, back-pressure and
load-error policy; the extractor nudges the loader to follow the demuxer
with one RESULT_SEEK, which is an optimization that can never stall
playback because the read succeeds either way.

Deleted with the protocol: the synthetic AVERROR_NEED_SEEK unwind, the
24 MB replay cache, the Cues priming seek and its read guard, the sticky
AVIO error clearing, and all four budgets (seekAttempts, stickyRecovers,
MAX_RECONCILES, MAX_OPEN_LOADER_ROUND_TRIPS). Opens run straight through,
seeks are one avformat_seek_file on the loader thread, and an IO failure is
an IOException that reaches media3's retry policy instead of a silent
degradation. The demuxer is now the only component that resolves seeks, so
the extractor's sample-derived seek index is gone too.

Verified on a SEI Robotics Android TV box against Jellyfin: the DV P8.1
28.4 GB MKV resume that started at a 4m28s spinner opens in 1.1 s and
resolves its seek in one call, a mid-playback scrub resolves in 1 ms from
the in-memory index, a 2 GB MKV resume opens in 0.5 s, fresh playback needs
no index read at all, and MP4/AVI still open and deliver. New coverage:
FfmpegRandomAccessSourceTest pins the read/reopen contract, and
FfmpegExtractorSeekTest asserts on-device that a cued seek lands on target
without walking the file and that a cueless file still reaches it.

close #2096
2026-08-24 23:02:23 +02:00

32 lines
1.7 KiB
Prolog

# Flutter turns minification on for every release build (FlutterPlugin sets
# releaseBuildType.isMinifyEnabled), and appends this file when it exists. Anything the
# app reaches only by name — reflection or JNI — therefore needs an explicit keep here.
# The bundled Media3 FFmpeg audio decoder (ALAC, DTS, DTS-HD, TrueHD, ...).
#
# DefaultRenderersFactory instantiates FfmpegAudioRenderer through Class.forName and no
# app code references it, so R8 shrinks the class away; media3's own consumer rules only
# -keepclassmembers its constructor, which neither keeps the class nor pins its name.
# ffmpeg_jni.cc separately resolves FfmpegAudioDecoder and its growOutputBuffer callback
# by name in JNI_OnLoad, and returns JNI_ERR when either is missing, which fails the whole
# System.loadLibrary("ffmpegJNI") call.
#
# Without these keeps a release build silently loses every codec this decoder adds:
# TrueHD/DTS-HD land on MediaCodecAudioRenderer, which has no decoder for them, and
# playback bails to the mpv fallback and loses ExoPlayer's Dolby Vision handling (#1703).
-keep class androidx.media3.decoder.ffmpeg.** { *; }
# growOutputBuffer's JNI descriptor names this type, so it may not be renamed either.
-keep class androidx.media3.decoder.SimpleDecoderOutputBuffer { *; }
# ffmpeg_demuxer_jni.cc resolves the AVIO input proxy's callbacks by name
# (FindClass on the interface, GetMethodID for position/read/readAt/length).
# Keeping the interface members pins the names on every implementation,
# including the anonymous proxy inside FfmpegExtractor.
-keep interface com.edde746.plezy.exoplayer.FfmpegDemuxerJni$Input {
long position();
int read(byte[], int);
int readAt(long, byte[], int);
long length();
}