From 1528b8b5ee4c43036253d1d2ab674ee65fe44ba8 Mon Sep 17 00:00:00 2001 From: Dustin Date: Wed, 2 Feb 2022 14:35:52 -0700 Subject: [PATCH 1/9] Found out RESULT_SEEK is a bad thing. Greatly improved Extractor efficiency. --- .../extractor/avi/AviExtractor.java | 52 +++++++++---------- .../extractor/avi/StreamHeaderBox.java | 3 +- .../extractor/avi/AviExtractorRoboTest.java | 18 +++++++ .../extractor/avi/AviExtractorTest.java | 44 ++++------------ .../extractor/avi/AviSeekMapTest.java | 5 +- .../extractor/avi/StreamHeaderBoxTest.java | 1 + 6 files changed, 59 insertions(+), 64 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java index dd06a90c26..ded38ab0a6 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java @@ -72,15 +72,6 @@ public class AviExtractor implements Extractor { } } - static int alignPositionHolder(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) { - final long position = input.getPosition(); - if ((position & 1) == 1) { - seekPosition.position = position + 1; - return RESULT_SEEK; - } - return RESULT_CONTINUE; - } - @NonNull static ByteBuffer allocate(int bytes) { final byte[] buffer = new byte[bytes]; @@ -503,47 +494,56 @@ public class AviExtractor implements Extractor { return null; } - int readSamples(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { + int readSamples(@NonNull ExtractorInput input) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { chunkHandler = null; - return alignPositionHolder(input, seekPosition); + alignInput(input); } } else { - ByteBuffer byteBuffer = allocate(8); + final int toRead = 8; + ByteBuffer byteBuffer = allocate(toRead); final byte[] bytes = byteBuffer.array(); alignInput(input); - input.readFully(bytes, 0, 1); + input.readFully(bytes, 0, toRead); + //This is super inefficient, but should be rare while (bytes[0] == 0) { - input.readFully(bytes, 0, 1); + for (int i=1;i= moviEnd) { - return RESULT_END_OF_INPUT; - } - input.readFully(bytes, 1, 7); final int chunkId = byteBuffer.getInt(); if (chunkId == ListBox.LIST) { - seekPosition.position = input.getPosition() + 8; - return RESULT_SEEK; + input.skipFully(8); + return RESULT_CONTINUE; } final int size = byteBuffer.getInt(); if (chunkId == JUNK) { - seekPosition.position = alignPosition(input.getPosition() + size); - return RESULT_SEEK; + input.skipFully(size); + alignInput(input); + return RESULT_CONTINUE; } final AviTrack aviTrack = getAviTrack(chunkId); if (aviTrack == null) { - seekPosition.position = alignPosition(input.getPosition() + size); + input.skipFully(size); + alignInput(input); w("Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + " size=" + size + " moviEnd=" + moviEnd); - return RESULT_SEEK; + return RESULT_CONTINUE; } if (aviTrack.newChunk(chunkId, size, input)) { - return alignPositionHolder(input, seekPosition); + alignInput(input); } else { chunkHandler = aviTrack; } } + if (input.getPosition() == input.getLength()) { + return C.RESULT_END_OF_INPUT; + } return RESULT_CONTINUE; } @@ -551,7 +551,7 @@ public class AviExtractor implements Extractor { public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { switch (state) { case STATE_READ_SAMPLES: - return readSamples(input, seekPosition); + return readSamples(input); case STATE_SEEK_START: state = STATE_READ_SAMPLES; seekPosition.position = moviOffset + 4; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 5486d43d7f..58a8859efa 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; /** @@ -46,7 +47,7 @@ public class StreamHeaderBox extends ResidentBox { } public long getDurationUs() { - return 1_000_000L * getScale() * getLength() / getRate(); + return C.MICROS_PER_SECOND * getScale() * getLength() / getRate(); } public int getSteamType() { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index acfd3ae6b1..6e85eeaa5d 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -112,4 +112,22 @@ public class AviExtractorRoboTest { Assert.assertEquals(aviTrack.getClock().durationUs, streamHeaderBox.getDurationUs()); } + @Test + public void readSamples_fragmentedChunk() throws IOException { + AviExtractor aviExtractor = AviExtractorTest.setupVideoAviExtractor(); + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final int size = 24 + 16; + final ByteBuffer byteBuffer = AviExtractor.allocate(size + 8); + byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(size); + + final ExtractorInput chunk = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + setSimulatePartialReads(true).build(); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder())); + + Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(chunk, new PositionHolder())); + + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; + Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index c6a4d7842d..ead5808eaa 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -303,28 +303,6 @@ public class AviExtractorTest { Assert.assertEquals(0, aviExtractor.aviSeekMap.seekOffset); } - @Test - public void alignPositionHolder_givenOddPosition() { - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). - setData(new byte[4]).build(); - fakeExtractorInput.setPosition(1); - final PositionHolder positionHolder = new PositionHolder(); - final int result = AviExtractor.alignPositionHolder(fakeExtractorInput, positionHolder); - Assert.assertEquals(Extractor.RESULT_SEEK, result); - Assert.assertEquals(2, positionHolder.position); - } - - @Test - public void alignPositionHolder_givenEvenPosition() { - - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). - setData(new byte[4]).build(); - fakeExtractorInput.setPosition(2); - final PositionHolder positionHolder = new PositionHolder(); - final int result = AviExtractor.alignPositionHolder(fakeExtractorInput, positionHolder); - Assert.assertEquals(Extractor.RESULT_CONTINUE, result); - } - @Test public void readHeaderList_givenBadHeader() throws IOException { final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[32]).build(); @@ -405,7 +383,7 @@ public class AviExtractorTest { Assert.assertEquals(64 * 1024 + 8, positionHolder.position); } - private AviExtractor setupVideoAviExtractor() { + static AviExtractor setupVideoAviExtractor() { final AviExtractor aviExtractor = new AviExtractor(); aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); @@ -444,31 +422,27 @@ public class AviExtractorTest { final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) .build(); - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(input, new PositionHolder())); + Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder())); final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length); } @Test - public void readSamples_fragmentedChunk() throws IOException { + public void readSamples_givenLeadingZeros() throws IOException { AviExtractor aviExtractor = setupVideoAviExtractor(); final AviTrack aviTrack = aviExtractor.getVideoTrack(); - final int size = 24 + 16; - final ByteBuffer byteBuffer = AviExtractor.allocate(32); + final ByteBuffer byteBuffer = AviExtractor.allocate(48); + byteBuffer.position(16); byteBuffer.putInt(aviTrack.chunkId); - byteBuffer.putInt(size); + byteBuffer.putInt(24); - final ExtractorInput chunk0 = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) .build(); - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk0, new PositionHolder())); - - final ExtractorInput chunk1 = new FakeExtractorInput.Builder().setData(new byte[16]) - .build(); - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk1, new PositionHolder())); + Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder())); final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; - Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); + Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length); } @Test diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java index 8328d9a3f5..91aded8755 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; import org.junit.Assert; import org.junit.Test; @@ -29,7 +30,7 @@ public class AviSeekMapTest { final AviTrack[] aviTracks = new AviTrack[]{DataHelper.getVideoAviTrack(secs), DataHelper.getAudioAviTrack(secs)}; - aviSeekMap.setFrames(position, 1_000_000L, aviTracks); + aviSeekMap.setFrames(position, C.MICROS_PER_SECOND, aviTracks); for (int i=0;i= chunksPerKeyFrame) { + if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) { keyFrameOffsetsDiv2.add(offset / 2); - for (AviTrack seekTrack : aviTracks) { + for (ChunkHandler seekTrack : chunkHandlers) { if (seekTrack != null) { seekIndexes[seekTrack.getId()].add(seekTrack.chunks); } } } } - keyFrameCounts[aviTrack.getId()]++; + keyFrameCounts[chunkHandler.getId()]++; } - aviTrack.chunks++; - aviTrack.size+=size; + chunkHandler.chunks++; + chunkHandler.size+=size; } indexByteBuffer.compact(); } if (videoTrack.chunks == keyFrameCounts[videoTrack.getId()]) { - videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); + videoTrack.setKeyFrames(ChunkHandler.ALL_KEY_FRAMES); } else { videoTrack.setKeyFrames(seekIndexes[videoId].getArray()); } @@ -485,16 +486,16 @@ public class AviExtractor implements Extractor { @Nullable @VisibleForTesting - AviTrack getAviTrack(int chunkId) { - for (AviTrack aviTrack : aviTracks) { - if (aviTrack != null && aviTrack.handlesChunkId(chunkId)) { - return aviTrack; + ChunkHandler getChunkHandler(int chunkId) { + for (ChunkHandler chunkHandler : chunkHandlers) { + if (chunkHandler != null && chunkHandler.handlesChunkId(chunkId)) { + return chunkHandler; } } return null; } - int readSamples(@NonNull ExtractorInput input) throws IOException { + int readChunks(@NonNull ExtractorInput input) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { chunkHandler = null; @@ -527,21 +528,21 @@ public class AviExtractor implements Extractor { alignInput(input); return RESULT_CONTINUE; } - final AviTrack aviTrack = getAviTrack(chunkId); - if (aviTrack == null) { + final ChunkHandler chunkHandler = getChunkHandler(chunkId); + if (chunkHandler == null) { input.skipFully(size); alignInput(input); w("Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + " size=" + size + " moviEnd=" + moviEnd); return RESULT_CONTINUE; } - if (aviTrack.newChunk(chunkId, size, input)) { + if (chunkHandler.newChunk(chunkId, size, input)) { alignInput(input); } else { - chunkHandler = aviTrack; + this.chunkHandler = chunkHandler; } } - if (input.getPosition() == input.getLength()) { + if (input.getPosition() >= moviEnd) { return C.RESULT_END_OF_INPUT; } return RESULT_CONTINUE; @@ -550,10 +551,10 @@ public class AviExtractor implements Extractor { @Override public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { switch (state) { - case STATE_READ_SAMPLES: - return readSamples(input); + case STATE_READ_CHUNKS: + return readChunks(input); case STATE_SEEK_START: - state = STATE_READ_SAMPLES; + state = STATE_READ_CHUNKS; seekPosition.position = moviOffset + 4; return RESULT_SEEK; case STATE_READ_TRACKS: @@ -573,7 +574,7 @@ public class AviExtractor implements Extractor { output.seekMap(new SeekMap.Unseekable(getDuration())); } seekPosition.position = moviOffset + 4; - state = STATE_READ_SAMPLES; + state = STATE_READ_CHUNKS; return RESULT_SEEK; } } @@ -591,15 +592,15 @@ public class AviExtractor implements Extractor { } } else { if (aviSeekMap != null) { - aviSeekMap.setFrames(position, timeUs, aviTracks); + aviSeekMap.setFrames(position, timeUs, chunkHandlers); } } } void resetClocks() { - for (@Nullable AviTrack aviTrack : aviTracks) { - if (aviTrack != null) { - aviTrack.getClock().setIndex(0); + for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { + if (chunkHandler != null) { + chunkHandler.getClock().setIndex(0); } } } @@ -610,8 +611,8 @@ public class AviExtractor implements Extractor { } @VisibleForTesting - void setAviTracks(AviTrack[] aviTracks) { - this.aviTracks = aviTracks; + void setChunkHandlers(ChunkHandler[] chunkHandlers) { + this.chunkHandlers = chunkHandlers; } @VisibleForTesting(otherwise = VisibleForTesting.NONE) @@ -626,13 +627,13 @@ public class AviExtractor implements Extractor { } @VisibleForTesting(otherwise = VisibleForTesting.NONE) - AviTrack getChunkHandler() { + ChunkHandler getChunkHandler() { return chunkHandler; } @VisibleForTesting(otherwise = VisibleForTesting.NONE) - void setChunkHandler(final AviTrack aviTrack) { - chunkHandler = aviTrack; + void setChunkHandler(final ChunkHandler chunkHandler) { + this.chunkHandler = chunkHandler; } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index fe662ca615..edd8f87683 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -99,17 +99,18 @@ public class AviSeekMap implements SeekMap { //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); } - public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) { + public void setFrames(final long position, final long timeUs, final ChunkHandler[] chunkHandlers) { final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2)); if (index < 0) { throw new IllegalArgumentException("Position: " + position); } - for (int i=0;i Date: Fri, 4 Feb 2022 19:52:10 -0700 Subject: [PATCH 4/9] Optimize AvcChunkHandler to use normal ChunkClock if no B Frames. --- .../extractor/avi/AvcChunkHandler.java | 60 +++++++++++++------ .../android/exoplayer2/util/NalUnitUtil.java | 6 +- .../extractor/avi/AvcChunkPeekerTest.java | 13 ++-- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java index 90bc6bb351..368a11fcbf 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -43,28 +44,36 @@ public class AvcChunkHandler extends NalChunkPeeker { public AvcChunkHandler(int id, @NonNull TrackOutput trackOutput, @NonNull ChunkClock clock, Format.Builder formatBuilder) { - super(id, trackOutput, new PicCountClock(clock.durationUs, clock.chunks), 16); + super(id, trackOutput, clock, 16); this.formatBuilder = formatBuilder; } - @NonNull - @Override - public PicCountClock getClock() { - return (PicCountClock) clock; + @Nullable + @VisibleForTesting + PicCountClock getPicCountClock() { + if (clock instanceof PicCountClock) { + return (PicCountClock)clock; + } else { + return null; + } } @Override boolean skip(byte nalType) { - return false; + if (clock instanceof PicCountClock) { + return false; + } else { + //If the clock is regular clock, skip "normal" frames + return nalType >= 0 && nalType <= NAL_TYPE_IDR; + } } /** * Greatly simplified way to calculate the picOrder * Full logic is here * https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/video/h264_poc.cc - * @param nalTypeOffset */ - void updatePicCountClock(final int nalTypeOffset) { + void updatePicCountClock(final int nalTypeOffset, final PicCountClock picCountClock) { final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length); //slide_header() in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice @@ -84,10 +93,10 @@ public class AvcChunkHandler extends NalChunkPeeker { if (spsData.picOrderCountType == 0) { int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); - getClock().setPicCount(picOrderCountLsb); + picCountClock.setPicCount(picOrderCountLsb); return; } else if (spsData.picOrderCountType == 2) { - getClock().setPicCount(frameNum); + picCountClock.setPicCount(frameNum); return; } clock.setIndex(clock.getIndex()); @@ -98,11 +107,20 @@ public class AvcChunkHandler extends NalChunkPeeker { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); - if (spsData.picOrderCountType == 0) { - getClock().setMaxPicCount(1 << spsData.picOrderCntLsbLength, 2); - } else if (spsData.picOrderCountType == 2) { - //Plus one because we double the frame number - getClock().setMaxPicCount(1 << spsData.frameNumLength, 1); + //If we have B Frames, upgrade to PicCountClock + final PicCountClock picCountClock; + if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) { + clock = picCountClock = new PicCountClock(clock.durationUs, clock.chunks); + } else { + picCountClock = getPicCountClock(); + } + if (picCountClock != null) { + if (spsData.picOrderCountType == 0) { + picCountClock.setMaxPicCount(1 << spsData.picOrderCntLsbLength, 2); + } else if (spsData.picOrderCountType == 2) { + //Plus one because we double the frame number + picCountClock.setMaxPicCount(1 << spsData.frameNumLength, 1); + } } if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { pixelWidthHeightRatio = spsData.pixelWidthHeightRatio; @@ -121,11 +139,17 @@ public class AvcChunkHandler extends NalChunkPeeker { case 2: case 3: case 4: - updatePicCountClock(nalTypeOffset); + if (clock instanceof PicCountClock) { + updatePicCountClock(nalTypeOffset, (PicCountClock)clock); + } return; - case NAL_TYPE_IDR: - getClock().syncIndexes(); + case NAL_TYPE_IDR: { + final PicCountClock picCountClock = getPicCountClock(); + if (picCountClock != null) { + picCountClock.syncIndexes(); + } return; + } case NAL_TYPE_AUD: case NAL_TYPE_SEI: case NAL_TYPE_PPS: { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index 44014bda8e..a1d290b5e5 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -33,6 +33,7 @@ public final class NalUnitUtil { public final int constraintsFlagsAndReservedZero2Bits; public final int levelIdc; public final int seqParameterSetId; + public final int maxNumRefFrames; public final int width; public final int height; public final float pixelWidthHeightRatio; @@ -48,6 +49,7 @@ public final class NalUnitUtil { int constraintsFlagsAndReservedZero2Bits, int levelIdc, int seqParameterSetId, + int maxNumRefFrames, int width, int height, float pixelWidthHeightRatio, @@ -61,6 +63,7 @@ public final class NalUnitUtil { this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits; this.levelIdc = levelIdc; this.seqParameterSetId = seqParameterSetId; + this.maxNumRefFrames = maxNumRefFrames; this.width = width; this.height = height; this.pixelWidthHeightRatio = pixelWidthHeightRatio; @@ -367,7 +370,7 @@ public final class NalUnitUtil { data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] } } - data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames + int maxNumRefFrames = data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames data.skipBit(); // gaps_in_frame_num_value_allowed_flag int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1; @@ -427,6 +430,7 @@ public final class NalUnitUtil { constraintsFlagsAndReservedZero2Bits, levelIdc, seqParameterSetId, + maxNumRefFrames, frameWidth, frameHeight, pixelWidthHeightRatio, diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java index bb0ab70263..82716243ba 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java @@ -35,8 +35,8 @@ public class AvcChunkPeekerTest { setSampleMimeType(MimeTypes.VIDEO_H264). setWidth(1280).setHeight(720).setFrameRate(24000f/1001f); - private static final byte[] P_SLICE = {00,00,00,01,0x41,(byte)0x9A,0x13,0x36,0x21,0x3A,0x5F, - (byte)0xFE,(byte)0x9E,0x10,00,00}; + private static final byte[] P_SLICE = {0,0,0,1,0x41,(byte)0x9A,0x13,0x36,0x21,0x3A,0x5F, + (byte)0xFE,(byte)0x9E,0x10,0,0}; private FakeTrackOutput fakeTrackOutput; private AvcChunkHandler avcChunkPeeker; @@ -61,19 +61,20 @@ public class AvcChunkPeekerTest { @Test public void peek_givenStreamHeader() throws IOException { peekStreamHeader(); - final PicCountClock picCountClock = avcChunkPeeker.getClock(); + final PicCountClock picCountClock = avcChunkPeeker.getPicCountClock(); + Assert.assertNotNull(picCountClock); Assert.assertEquals(64, picCountClock.getMaxPicCount()); Assert.assertEquals(0, avcChunkPeeker.getSpsData().picOrderCountType); Assert.assertEquals(1.18f, fakeTrackOutput.lastFormat.pixelWidthHeightRatio, 0.01f); } @Test - public void peek_givenStreamHeaderAndPSlice() throws IOException { + public void newChunk_givenStreamHeaderAndPSlice() throws IOException { peekStreamHeader(); - final PicCountClock picCountClock = avcChunkPeeker.getClock(); + final PicCountClock picCountClock = avcChunkPeeker.getPicCountClock(); final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build(); - avcChunkPeeker.peek(input, P_SLICE.length); + avcChunkPeeker.newChunk(0, P_SLICE.length, input); Assert.assertEquals(12, picCountClock.getLastPicCount()); } From 84d3f62e8806b7eee64aa88da2be38e74533c4dc Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 5 Feb 2022 07:06:25 -0700 Subject: [PATCH 5/9] Fix DefaultExtractorsFactoryTest --- .../exoplayer2/extractor/DefaultExtractorsFactoryTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java index 1c6ce7b70c..b7dbb58f7f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import com.google.android.exoplayer2.extractor.avi.AviExtractor; import com.google.android.exoplayer2.extractor.flac.FlacExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor; import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor; @@ -69,6 +70,7 @@ public final class DefaultExtractorsFactoryTest { AdtsExtractor.class, Ac3Extractor.class, Ac4Extractor.class, + AviExtractor.class, Mp3Extractor.class, JpegExtractor.class) .inOrder(); @@ -112,6 +114,7 @@ public final class DefaultExtractorsFactoryTest { AdtsExtractor.class, Ac3Extractor.class, Ac4Extractor.class, + AviExtractor.class, JpegExtractor.class) .inOrder(); } From 14c842e5030e0958df74498b38eaac030204d88a Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 5 Feb 2022 07:06:58 -0700 Subject: [PATCH 6/9] Add coverage for max ref frames --- .../java/com/google/android/exoplayer2/util/NalUnitUtilTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java index 483f70fd96..ed5a62f7c5 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java @@ -134,6 +134,7 @@ public final class NalUnitUtilTest { assertThat(data.pixelWidthHeightRatio).isEqualTo(1.0f); assertThat(data.picOrderCountType).isEqualTo(0); assertThat(data.separateColorPlaneFlag).isFalse(); + assertThat(data.maxNumRefFrames).isEqualTo(4); } @Test From 3a9f1f9a34190748315cb1a49d6f517a59ef822a Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 5 Feb 2022 07:09:29 -0700 Subject: [PATCH 7/9] Improved comments, improved naming consistency. --- .../extractor/avi/AvcChunkHandler.java | 10 ++- .../extractor/avi/AviExtractor.java | 18 ++--- .../exoplayer2/extractor/avi/AviSeekMap.java | 17 +++-- .../extractor/avi/ChunkHandler.java | 74 ++++++++++++++----- .../extractor/avi/Mp4vChunkHandler.java | 2 +- ...andler.java => MpegAudioChunkHandler.java} | 65 ++++++++++------ ...lChunkPeeker.java => NalChunkHandler.java} | 8 +- .../exoplayer2/extractor/avi/ResidentBox.java | 8 -- .../extractor/avi/StreamNameBox.java | 2 +- .../extractor/avi/AvcChunkPeekerTest.java | 14 ++-- .../extractor/avi/AviSeekMapTest.java | 10 +-- .../extractor/avi/ChunkHandlerTest.java | 30 -------- ...nkPeeker.java => MockNalChunkHandler.java} | 4 +- .../extractor/avi/NalChunkPeekerTest.java | 9 +-- 14 files changed, 147 insertions(+), 124 deletions(-) rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{Mp3ChunkHandler.java => MpegAudioChunkHandler.java} (65%) rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{NalChunkPeeker.java => NalChunkHandler.java} (93%) delete mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ChunkHandlerTest.java rename library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/{MockNalChunkPeeker.java => MockNalChunkHandler.java} (90%) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java index 368a11fcbf..0d67032d53 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java @@ -29,7 +29,7 @@ import java.io.IOException; * Corrects the time and PAR for H264 streams * AVC is very rare in AVI due to the rise of the mp4 container */ -public class AvcChunkHandler extends NalChunkPeeker { +public class AvcChunkHandler extends NalChunkHandler { private static final int NAL_TYPE_MASK = 0x1f; private static final int NAL_TYPE_IDR = 5; //I Frame private static final int NAL_TYPE_SEI = 6; @@ -63,7 +63,7 @@ public class AvcChunkHandler extends NalChunkPeeker { if (clock instanceof PicCountClock) { return false; } else { - //If the clock is regular clock, skip "normal" frames + //If the clock is ChunkClock, skip "normal" frames return nalType >= 0 && nalType <= NAL_TYPE_IDR; } } @@ -107,10 +107,12 @@ public class AvcChunkHandler extends NalChunkPeeker { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); - //If we have B Frames, upgrade to PicCountClock + //If we can have B Frames, upgrade to PicCountClock final PicCountClock picCountClock; if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) { - clock = picCountClock = new PicCountClock(clock.durationUs, clock.chunks); + picCountClock = new PicCountClock(clock.durationUs, clock.chunks); + picCountClock.setIndex(clock.getIndex()); + clock = picCountClock; } else { picCountClock = getPicCountClock(); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java index 3831210a15..a854e3b86f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java @@ -291,7 +291,7 @@ public class AviExtractor implements Extractor { } trackOutput.format(builder.build()); if (MimeTypes.AUDIO_MPEG.equals(mimeType)) { - chunkHandler = new Mp3ChunkHandler(streamId, trackOutput, clock, + chunkHandler = new MpegAudioChunkHandler(streamId, trackOutput, clock, audioFormat.getSamplesPerSecond()); } else { chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_AUDIO, @@ -367,7 +367,7 @@ public class AviExtractor implements Extractor { } void fixTimings(final int[] keyFrameCounts, final long videoDuration) { - for (final ChunkHandler chunkHandler : chunkHandlers) { + for (@Nullable final ChunkHandler chunkHandler : chunkHandlers) { if (chunkHandler != null) { if (chunkHandler.isAudio()) { final long durationUs = chunkHandler.getClock().durationUs; @@ -452,7 +452,7 @@ public class AviExtractor implements Extractor { int indexSize = seekIndexes[videoId].getSize(); if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) { keyFrameOffsetsDiv2.add(offset / 2); - for (ChunkHandler seekTrack : chunkHandlers) { + for (@Nullable ChunkHandler seekTrack : chunkHandlers) { if (seekTrack != null) { seekIndexes[seekTrack.getId()].add(seekTrack.chunks); } @@ -487,7 +487,7 @@ public class AviExtractor implements Extractor { @Nullable @VisibleForTesting ChunkHandler getChunkHandler(int chunkId) { - for (ChunkHandler chunkHandler : chunkHandlers) { + for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { if (chunkHandler != null && chunkHandler.handlesChunkId(chunkId)) { return chunkHandler; } @@ -536,7 +536,7 @@ public class AviExtractor implements Extractor { + " size=" + size + " moviEnd=" + moviEnd); return RESULT_CONTINUE; } - if (chunkHandler.newChunk(chunkId, size, input)) { + if (chunkHandler.newChunk(size, input)) { alignInput(input); } else { this.chunkHandler = chunkHandler; @@ -587,20 +587,20 @@ public class AviExtractor implements Extractor { chunkHandler = null; if (position <= 0) { if (moviOffset != 0) { - resetClocks(); + setIndexes(new int[chunkHandlers.length]); state = STATE_SEEK_START; } } else { if (aviSeekMap != null) { - aviSeekMap.setFrames(position, timeUs, chunkHandlers); + setIndexes(aviSeekMap.getIndexes(position)); } } } - void resetClocks() { + private void setIndexes(@NonNull int[] indexes) { for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { if (chunkHandler != null) { - chunkHandler.getClock().setIndex(0); + chunkHandler.setIndex(indexes[chunkHandler.getId()]); } } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index edd8f87683..f709a68e82 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -99,18 +99,23 @@ public class AviSeekMap implements SeekMap { //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); } - public void setFrames(final long position, final long timeUs, final ChunkHandler[] chunkHandlers) { + /** + * Get the ChunkClock indexes by stream id + * @param position seek position in the file + */ + @NonNull + public int[] getIndexes(final long position) { final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2)); if (index < 0) { throw new IllegalArgumentException("Position: " + position); } - for (int i=0;i Date: Sat, 5 Feb 2022 09:32:19 -0700 Subject: [PATCH 8/9] MpegAudioChunkHandler cleanup and Tests --- .../extractor/avi/MpegAudioChunkHandler.java | 31 +++-- .../avi/MpegAudioChunkHandlerTest.java | 116 ++++++++++++++++++ .../assets/extractordumps/avi/frame.mp3.dump | Bin 0 -> 417 bytes 3 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java create mode 100644 testdata/src/test/assets/extractordumps/avi/frame.mp3.dump diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java index 59a7aff775..ac10b3dcb7 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java @@ -51,7 +51,12 @@ public class MpegAudioChunkHandler extends ChunkHandler { syncTime(); return true; } - chunkRemaining = size; + this.size = chunkRemaining = size; + return resume(input); + } + + @Override + boolean resume(@NonNull ExtractorInput input) throws IOException { if (process(input)) { // Fail Over: If the scratch is the entire chunk, we didn't find a MP3 header. // Dump the chunk as is and hope the decoder can handle it. @@ -59,17 +64,8 @@ public class MpegAudioChunkHandler extends ChunkHandler { scratch.setPosition(0); trackOutput.sampleData(scratch, size); scratch.reset(0); + done(size); } - clock.advance(); - return true; - } - return false; - } - - @Override - boolean resume(@NonNull ExtractorInput input) throws IOException { - if (process(input)) { - clock.advance(); return true; } return false; @@ -101,7 +97,6 @@ public class MpegAudioChunkHandler extends ChunkHandler { scratch.ensureCapacity(scratch.limit() + chunkRemaining); int toRead = 4; while (chunkRemaining > 0 && readScratch(input, toRead) != C.RESULT_END_OF_INPUT) { - readScratch(input, toRead); while (scratch.bytesLeft() >= 4) { if (header.setForHeaderData(scratch.readInt())) { scratch.skipBytes(-4); @@ -127,7 +122,7 @@ public class MpegAudioChunkHandler extends ChunkHandler { trackOutput.sampleData(scratch, scratchBytes); frameRemaining = header.frameSize - scratchBytes; } else { - return chunkRemaining == 0; + return true; } } final int bytes = trackOutput.sampleData(input, Math.min(frameRemaining, chunkRemaining), false); @@ -151,4 +146,14 @@ public class MpegAudioChunkHandler extends ChunkHandler { timeUs = clock.getUs(); frameRemaining = 0; } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + long getTimeUs() { + return timeUs; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + int getFrameRemaining() { + return frameRemaining; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java new file mode 100644 index 0000000000..c37f370331 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java @@ -0,0 +1,116 @@ +package com.google.android.exoplayer2.extractor.avi; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.MpegAudioUtil; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MpegAudioChunkHandlerTest { + private static final int FPS = 24; + private Format MP3_FORMAT = new Format.Builder().setChannelCount(2). + setSampleMimeType(MimeTypes.AUDIO_MPEG).setSampleRate(44100).build(); + private static final long CHUNK_MS = C.MICROS_PER_SECOND / FPS; + private final MpegAudioUtil.Header header = new MpegAudioUtil.Header(); + private FakeTrackOutput fakeTrackOutput; + private MpegAudioChunkHandler mpegAudioChunkHandler; + private byte[] mp3Frame; + private long frameUs; + + @Before + public void before() throws IOException { + fakeTrackOutput = new FakeTrackOutput(false); + fakeTrackOutput.format(MP3_FORMAT); + mpegAudioChunkHandler = new MpegAudioChunkHandler(0, fakeTrackOutput, + new ChunkClock(C.MICROS_PER_SECOND, FPS), MP3_FORMAT.sampleRate); + + if (mp3Frame == null) { + final Context context = ApplicationProvider.getApplicationContext(); + mp3Frame = TestUtil.getByteArray(context,"extractordumps/avi/frame.mp3.dump"); + header.setForHeaderData(ByteBuffer.wrap(mp3Frame).getInt()); + //About 26ms + frameUs = header.samplesPerFrame * C.MICROS_PER_SECOND / header.sampleRate; + } + } + + @Test + public void newChunk_givenNonMpegData() throws IOException { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[1024]). + build(); + + mpegAudioChunkHandler.newChunk((int)input.getLength(), input); + Assert.assertEquals(1024, fakeTrackOutput.getSampleData(0).length); + Assert.assertEquals(CHUNK_MS, mpegAudioChunkHandler.getClock().getUs()); + } + @Test + public void newChunk_givenEmptyChunk() throws IOException { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]). + build(); + mpegAudioChunkHandler.newChunk((int)input.getLength(), input); + Assert.assertEquals(C.MICROS_PER_SECOND / 24, mpegAudioChunkHandler.getClock().getUs()); + } + + @Test + public void setIndex_given12frames() { + mpegAudioChunkHandler.setIndex(12); + Assert.assertEquals(500_000L, mpegAudioChunkHandler.getTimeUs()); + } + + @Test + public void newChunk_givenSingleFrame() throws IOException { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(mp3Frame).build(); + + mpegAudioChunkHandler.newChunk(mp3Frame.length, input); + Assert.assertArrayEquals(mp3Frame, fakeTrackOutput.getSampleData(0)); + Assert.assertEquals(frameUs, mpegAudioChunkHandler.getTimeUs()); + } + + @Test + public void newChunk_givenSeekAndFragmentedFrames() throws IOException { + ByteBuffer byteBuffer = ByteBuffer.allocate(mp3Frame.length * 2); + byteBuffer.put(mp3Frame, mp3Frame.length / 2, mp3Frame.length / 2); + byteBuffer.put(mp3Frame); + final int remainder = byteBuffer.remaining(); + byteBuffer.put(mp3Frame, 0, remainder); + + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + build(); + + mpegAudioChunkHandler.setIndex(1); //Seek + Assert.assertFalse(mpegAudioChunkHandler.newChunk(byteBuffer.capacity(), input)); + Assert.assertArrayEquals(mp3Frame, fakeTrackOutput.getSampleData(0)); + Assert.assertEquals(frameUs + CHUNK_MS, mpegAudioChunkHandler.getTimeUs()); + + Assert.assertTrue(mpegAudioChunkHandler.resume(input)); + Assert.assertEquals(header.frameSize - remainder, mpegAudioChunkHandler.getFrameRemaining()); + } + + @Test + public void newChunk_givenTwoFrames() throws IOException { + ByteBuffer byteBuffer = ByteBuffer.allocate(mp3Frame.length * 2); + byteBuffer.put(mp3Frame); + byteBuffer.put(mp3Frame); + + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + build(); + Assert.assertFalse(mpegAudioChunkHandler.newChunk(byteBuffer.capacity(), input)); + Assert.assertEquals(1, fakeTrackOutput.getSampleCount()); + Assert.assertEquals(0L, fakeTrackOutput.getSampleTimeUs(0)); + + Assert.assertTrue(mpegAudioChunkHandler.resume(input)); + Assert.assertEquals(2, fakeTrackOutput.getSampleCount()); + Assert.assertEquals(frameUs, fakeTrackOutput.getSampleTimeUs(1)); + } +} diff --git a/testdata/src/test/assets/extractordumps/avi/frame.mp3.dump b/testdata/src/test/assets/extractordumps/avi/frame.mp3.dump new file mode 100644 index 0000000000000000000000000000000000000000..2f6ff74b38f808199693c0368f0ea9183c63c317 GIT binary patch literal 417 tcmezWdqN5W{|5$!Oa=x94h9BZ1qKF2AX) Date: Sat, 5 Feb 2022 10:19:26 -0700 Subject: [PATCH 9/9] MpegAudioChunkHandler seek fixes --- .../extractor/avi/MpegAudioChunkHandler.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java index ac10b3dcb7..a66d6d105f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -104,7 +105,9 @@ public class MpegAudioChunkHandler extends ChunkHandler { } scratch.skipBytes(-3); } - toRead = Math.min(chunkRemaining, 128); + // 16 is small, but if we end up reading multiple frames into scratch, things get complicated. + // We should only loop on seek, so this is the lesser of the evils. + toRead = Math.min(chunkRemaining, 16); } return false; } @@ -140,11 +143,17 @@ public class MpegAudioChunkHandler extends ChunkHandler { public void setIndex(int index) { super.setIndex(index); syncTime(); + if (frameRemaining != 0) { + // We have a partial frame in the output, no way to clear it, so just send it as is. + // Next frame should be key frame, so the codec should recover. + trackOutput.sampleMetadata(timeUs, 0, header.frameSize - frameRemaining, + 0, null); + frameRemaining = 0; + } } private void syncTime() { timeUs = clock.getUs(); - frameRemaining = 0; } @VisibleForTesting(otherwise = VisibleForTesting.NONE)