diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 9bc18df309..2388e0d508 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -2295,10 +2295,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // Process any batched data. checkState(!outputStreamEnded); if (bypassBatchBuffer.hasSamples()) { - boolean isDecodeOnly = - bypassBatchBuffer.getLastSampleTimeUs() < getLastResetPositionUs() - && (outputFormat == null - || !Objects.equals(outputFormat.sampleMimeType, MimeTypes.AUDIO_OPUS)); if (processOutputBuffer( positionUs, elapsedRealtimeUs, @@ -2308,7 +2304,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /* bufferFlags= */ 0, bypassBatchBuffer.getSampleCount(), bypassBatchBuffer.getFirstSampleTimeUs(), - isDecodeOnly, + isDecodeOnly(getLastResetPositionUs(), bypassBatchBuffer.getLastSampleTimeUs()), bypassBatchBuffer.isEndOfStream(), checkNotNull(outputFormat))) { // The batch buffer has been fully processed. @@ -2405,8 +2401,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { bypassSampleBuffer.format = outputFormat; handleInputBufferSupplementalData(bypassSampleBuffer); } - oggOpusAudioPacketizer.packetize( - bypassSampleBuffer, checkNotNull(outputFormat).initializationData); + if (OpusUtil.needToDecodeOpusFrame( + getLastResetPositionUs(), bypassSampleBuffer.timeUs)) { + // Packetize as long as frame does not precede the last reset position by more than + // seek-preroll. + oggOpusAudioPacketizer.packetize( + bypassSampleBuffer, checkNotNull(outputFormat).initializationData); + } } if (!haveBypassBatchBufferAndNewSampleSameDecodeOnlyState() || !bypassBatchBuffer.append(bypassSampleBuffer)) { @@ -2428,11 +2429,31 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return true; } long lastResetPositionUs = getLastResetPositionUs(); - boolean batchBufferIsDecodeOnly = bypassBatchBuffer.getLastSampleTimeUs() < lastResetPositionUs; - boolean sampleBufferIsDecodeOnly = bypassSampleBuffer.timeUs < lastResetPositionUs; + boolean batchBufferIsDecodeOnly = + isDecodeOnly(lastResetPositionUs, bypassBatchBuffer.getLastSampleTimeUs()); + boolean sampleBufferIsDecodeOnly = isDecodeOnly(lastResetPositionUs, bypassSampleBuffer.timeUs); return batchBufferIsDecodeOnly == sampleBufferIsDecodeOnly; } + /** + * Returns if based on target play time and frame time that the frame should be decode-only. + * + *
If the format is Opus, then the frame is decode-only if the frame time precedes the start + * time by more than seek-preroll. + * + * @param startTimeUs The time to start playing at. + * @param frameTimeUs The time of the sample. + * @return Whether the frame is decode-only. + */ + private boolean isDecodeOnly(long startTimeUs, long frameTimeUs) { + // Opus frames that precede the target position by more than seek-preroll should be skipped. + return frameTimeUs < startTimeUs + && (outputFormat == null + || !Objects.equals(outputFormat.sampleMimeType, MimeTypes.AUDIO_OPUS) + || !OpusUtil.needToDecodeOpusFrame( + /* startTimeUs= */ startTimeUs, /* frameTimeUs= */ frameTimeUs)); + } + private static boolean isMediaCodecException(IllegalStateException error) { if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) { return true; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java index 086b628eff..4a390fa524 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java @@ -149,6 +149,23 @@ public class OpusUtil { return ((header[11] & 0xFF) << 8) | (header[10] & 0xFF); } + /** + * Returns whether an Opus frame should be sent to the decoder as it is either past the start + * position or within the seek-preroll duration. + * + *
The measure of whether an Opus frame should not be decoded is if its time precedes the start + * position by more than the default seek-preroll value. + * + * @param startTimeUs The time to start playing at. + * @param frameTimeUs The time of the Opus sample. + * @return Whether the frame should be decoded. + */ + public static boolean needToDecodeOpusFrame(long startTimeUs, long frameTimeUs) { + // Divide by 1000 in rhs value to convert nanoseconds to microseconds. + return (startTimeUs - frameTimeUs) + <= (sampleCountToNanoseconds(DEFAULT_SEEK_PRE_ROLL_SAMPLES) / 1000); + } + private static long getPacketDurationUs(byte packetByte0, byte packetByte1) { // See RFC6716, Sections 3.1 and 3.2. int toc = packetByte0 & 0xFF; diff --git a/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpusWithSeek.dump b/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpusWithSeek.dump index bae80c8ffb..ce3b22b19c 100644 --- a/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpusWithSeek.dump +++ b/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpusWithSeek.dump @@ -1,8 +1,7 @@ SinkDump (OggOpus): - buffers.length = 6 - buffers[0] = length 207, hash D462AF66 - buffers[1] = length 3891, hash FE9EE7C1 - buffers[2] = length 3732, hash C2249BC1 - buffers[3] = length 3731, hash A9384B0F - buffers[4] = length 4091, hash 9631FA86 - buffers[5] = length 776, hash 4BC27E65 + buffers.length = 5 + buffers[0] = length 3979, hash 63A08FAD + buffers[1] = length 3696, hash ABE4B611 + buffers[2] = length 3770, hash 67EAAA4 + buffers[3] = length 4057, hash C53B8DF3 + buffers[4] = length 926, hash B21B9A24