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 0000000000..2f6ff74b38 Binary files /dev/null and b/testdata/src/test/assets/extractordumps/avi/frame.mp3.dump differ