diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 79913d2aa9..6c989c5639 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -565,4 +565,22 @@ public final class ParsableByteArray { position += length; return value; } + + /** + * The data from the end of the buffer is copied to the front + * The limit() because the bytesLeft() and position is zero + */ + public void compact() { + if (bytesLeft() == 0) { + limit = 0; + } else { + final ByteBuffer byteBuffer = ByteBuffer.wrap(data); + byteBuffer.limit(limit); + byteBuffer.position(position); + byteBuffer.compact(); + byteBuffer.flip(); + limit = byteBuffer.limit(); + } + position = 0; + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java similarity index 70% rename from library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java index b3ecf270d2..0d67032d53 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java @@ -15,6 +15,8 @@ */ 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; @@ -27,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 AvcChunkPeeker 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; @@ -35,36 +37,43 @@ public class AvcChunkPeeker extends NalChunkPeeker { private static final int NAL_TYPE_PPS = 8; private static final int NAL_TYPE_AUD = 9; - private final PicCountClock picCountClock; private final Format.Builder formatBuilder; - private final TrackOutput trackOutput; private float pixelWidthHeightRatio = 1f; private NalUnitUtil.SpsData spsData; - public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, LinearClock clock) { - super(16); + public AvcChunkHandler(int id, @NonNull TrackOutput trackOutput, + @NonNull ChunkClock clock, Format.Builder formatBuilder) { + super(id, trackOutput, clock, 16); this.formatBuilder = formatBuilder; - this.trackOutput = trackOutput; - picCountClock = new PicCountClock(clock.durationUs, clock.length); } - public PicCountClock getClock() { - return picCountClock; + @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 ChunkClock, 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 @@ -90,7 +99,7 @@ public class AvcChunkPeeker extends NalChunkPeeker { picCountClock.setPicCount(frameNum); return; } - picCountClock.setIndex(picCountClock.getIndex()); + clock.setIndex(clock.getIndex()); } @VisibleForTesting @@ -98,11 +107,22 @@ public class AvcChunkPeeker extends NalChunkPeeker { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); - 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 we can have B Frames, upgrade to PicCountClock + final PicCountClock picCountClock; + if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) { + picCountClock = new PicCountClock(clock.durationUs, clock.chunks); + picCountClock.setIndex(clock.getIndex()); + clock = picCountClock; + } 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 +141,17 @@ public class AvcChunkPeeker extends NalChunkPeeker { case 2: case 3: case 4: - updatePicCountClock(nalTypeOffset); + if (clock instanceof PicCountClock) { + updatePicCountClock(nalTypeOffset, (PicCountClock)clock); + } return; - case NAL_TYPE_IDR: - picCountClock.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/extractor/avi/AviExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java index ded38ab0a6..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 @@ -103,7 +103,7 @@ public class AviExtractor implements Extractor { @VisibleForTesting static final int STATE_READ_IDX1 = 2; @VisibleForTesting - static final int STATE_READ_SAMPLES = 3; + static final int STATE_READ_CHUNKS = 3; @VisibleForTesting static final int STATE_SEEK_START = 4; @@ -127,9 +127,9 @@ public class AviExtractor implements Extractor { private AviHeaderBox aviHeader; private long durationUs = C.TIME_UNSET; /** - * AviTracks by StreamId + * ChunkHandlers by StreamId */ - private AviTrack[] aviTracks = new AviTrack[0]; + private ChunkHandler[] chunkHandlers = new ChunkHandler[0]; //At the start of the movi tag private long moviOffset; private long moviEnd; @@ -137,7 +137,7 @@ public class AviExtractor implements Extractor { AviSeekMap aviSeekMap; //Set if a chunk is only partially read - private transient AviTrack chunkHandler; + private transient ChunkHandler chunkHandler; /** * @@ -218,7 +218,7 @@ public class AviExtractor implements Extractor { } @VisibleForTesting - AviTrack parseStream(final ListBox streamList, int streamId) { + ChunkHandler parseStream(final ListBox streamList, int streamId) { final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); if (streamHeader == null) { @@ -243,7 +243,8 @@ public class AviExtractor implements Extractor { if (streamName != null) { builder.setLabel(streamName.getName()); } - final AviTrack aviTrack; + final ChunkClock clock = new ChunkClock(durationUs, length); + final ChunkHandler chunkHandler; if (streamHeader.isVideo()) { final VideoFormat videoFormat = streamFormat.getVideoFormat(); final String mimeType = videoFormat.getMimeType(); @@ -257,17 +258,12 @@ public class AviExtractor implements Extractor { builder.setFrameRate(streamHeader.getFrameRate()); builder.setSampleMimeType(mimeType); - final LinearClock clock = new LinearClock(durationUs, length); - aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, clock, trackOutput); - if (MimeTypes.VIDEO_H264.equals(mimeType)) { - final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, clock); - aviTrack.setClock(avcChunkPeeker.getClock()); - aviTrack.setChunkPeeker(avcChunkPeeker); + chunkHandler = new AvcChunkHandler(streamId, trackOutput, clock, builder); + } else if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { + chunkHandler = new Mp4vChunkHandler(streamId, trackOutput, clock, builder); } else { - if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { - aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput)); - } + chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_VIDEO, trackOutput, clock); } trackOutput.format(builder.build()); this.durationUs = durationUs; @@ -294,13 +290,18 @@ public class AviExtractor implements Extractor { builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); } trackOutput.format(builder.build()); - aviTrack = new AviTrack(streamId, C.TRACK_TYPE_AUDIO, - new LinearClock(durationUs, length), trackOutput); - aviTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); + if (MimeTypes.AUDIO_MPEG.equals(mimeType)) { + chunkHandler = new MpegAudioChunkHandler(streamId, trackOutput, clock, + audioFormat.getSamplesPerSecond()); + } else { + chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_AUDIO, + trackOutput, clock); + } + chunkHandler.setKeyFrames(ChunkHandler.ALL_KEY_FRAMES); }else { - aviTrack = null; + chunkHandler = null; } - return aviTrack; + return chunkHandler; } private int readTracks(ExtractorInput input) throws IOException { @@ -312,7 +313,7 @@ public class AviExtractor implements Extractor { if (aviHeader == null) { throw new IOException("AviHeader not found"); } - aviTracks = new AviTrack[aviHeader.getStreams()]; + chunkHandlers = new ChunkHandler[aviHeader.getStreams()]; //This is usually wrong, so it will be overwritten by video if present durationUs = aviHeader.getTotalFrames() * (long)aviHeader.getMicroSecPerFrame(); @@ -320,7 +321,7 @@ public class AviExtractor implements Extractor { for (Box box : headerList.getChildren()) { if (box instanceof ListBox && ((ListBox) box).getListType() == ListBox.TYPE_STRL) { final ListBox streamList = (ListBox) box; - aviTracks[streamId] = parseStream(streamList, streamId); + chunkHandlers[streamId] = parseStream(streamList, streamId); streamId++; } } @@ -356,32 +357,32 @@ public class AviExtractor implements Extractor { } @VisibleForTesting - AviTrack getVideoTrack() { - for (@Nullable AviTrack aviTrack : aviTracks) { - if (aviTrack != null && aviTrack.isVideo()) { - return aviTrack; + ChunkHandler getVideoTrack() { + for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { + if (chunkHandler != null && chunkHandler.isVideo()) { + return chunkHandler; } } return null; } void fixTimings(final int[] keyFrameCounts, final long videoDuration) { - for (final AviTrack aviTrack : aviTracks) { - if (aviTrack != null) { - if (aviTrack.isAudio()) { - final long durationUs = aviTrack.getClock().durationUs; - i("Audio #" + aviTrack.id + " chunks: " + aviTrack.chunks + " us=" + durationUs + - " size=" + aviTrack.size); - final LinearClock linearClock = aviTrack.getClock(); + for (@Nullable final ChunkHandler chunkHandler : chunkHandlers) { + if (chunkHandler != null) { + if (chunkHandler.isAudio()) { + final long durationUs = chunkHandler.getClock().durationUs; + i("Audio #" + chunkHandler.getId() + " chunks: " + chunkHandler.chunks + " us=" + durationUs + + " size=" + chunkHandler.size); + final ChunkClock linearClock = chunkHandler.getClock(); //If the audio track duration is off from the video by >5 % recalc using video if ((durationUs - videoDuration) / (float)videoDuration > .05f) { - w("Audio #" + aviTrack.id + " duration is off using videoDuration"); + w("Audio #" + chunkHandler.getId() + " duration is off using videoDuration"); linearClock.setDuration(videoDuration); } - linearClock.setLength(aviTrack.chunks); - if (aviTrack.chunks != keyFrameCounts[aviTrack.id]) { - w("Audio is not all key frames chunks=" + aviTrack.chunks + " keyFrames=" + - keyFrameCounts[aviTrack.id]); + linearClock.setChunks(chunkHandler.chunks); + if (chunkHandler.chunks != keyFrameCounts[chunkHandler.getId()]) { + w("Audio is not all key frames chunks=" + chunkHandler.chunks + " keyFrames=" + + keyFrameCounts[chunkHandler.getId()]); } } } @@ -392,7 +393,7 @@ public class AviExtractor implements Extractor { * Reads the index and sets the keyFrames and creates the SeekMap */ void readIdx1(ExtractorInput input, int remaining) throws IOException { - final AviTrack videoTrack = getVideoTrack(); + final ChunkHandler videoTrack = getVideoTrack(); if (videoTrack == null) { output.seekMap(new SeekMap.Unseekable(getDuration())); w("No video track found"); @@ -406,14 +407,14 @@ public class AviExtractor implements Extractor { final ByteBuffer firstEntry = AviExtractor.allocate(16); input.peekFully(firstEntry.array(), 0, 16); - final int videoId = videoTrack.id; + final int videoId = videoTrack.getId(); final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); final byte[] bytes = indexByteBuffer.array(); //These are ints/2 final UnboundedIntArray keyFrameOffsetsDiv2 = new UnboundedIntArray(); - final int[] keyFrameCounts = new int[aviTracks.length]; - final UnboundedIntArray[] seekIndexes = new UnboundedIntArray[aviTracks.length]; + final int[] keyFrameCounts = new int[chunkHandlers.length]; + final UnboundedIntArray[] seekIndexes = new UnboundedIntArray[chunkHandlers.length]; 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 (@Nullable ChunkHandler seekTrack : chunkHandlers) { if (seekTrack != null) { - seekIndexes[seekTrack.id].add(seekTrack.chunks); + seekIndexes[seekTrack.getId()].add(seekTrack.chunks); } } } } - keyFrameCounts[aviTrack.id]++; + keyFrameCounts[chunkHandler.getId()]++; } - aviTrack.chunks++; - aviTrack.size+=size; + chunkHandler.chunks++; + chunkHandler.size+=size; } indexByteBuffer.compact(); } - if (videoTrack.chunks == keyFrameCounts[videoTrack.id]) { - videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); + if (videoTrack.chunks == keyFrameCounts[videoTrack.getId()]) { + 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 (@Nullable 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(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; } } @@ -586,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, aviTracks); + setIndexes(aviSeekMap.getIndexes(position)); } } } - void resetClocks() { - for (@Nullable AviTrack aviTrack : aviTracks) { - if (aviTrack != null) { - aviTrack.getClock().setIndex(0); + private void setIndexes(@NonNull int[] indexes) { + for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { + if (chunkHandler != null) { + chunkHandler.setIndex(indexes[chunkHandler.getId()]); } } } @@ -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..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,17 +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 AviTrack[] aviTracks) { + /** + * 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 0) { trackOutput.sampleMetadata( clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); } - final LinearClock clock = getClock(); //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); clock.advance(); } + + /** + * Gets the streamId. + * @return The unique stream id for this file + */ + public int getId() { + return ((chunkId >> 8) & 0xf) + (chunkId & 0xf) * 10; + } + + /** + * A seek occurred + * @param index of the chunk + */ + public void setIndex(int index) { + getClock().setIndex(index); + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java deleted file mode 100644 index 0cfd2a97e6..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.avi; - -import com.google.android.exoplayer2.extractor.ExtractorInput; -import java.io.IOException; - -/** - * Peeks for import data in the chunk stream. - */ -public interface ChunkPeeker { - void peek(ExtractorInput input, final int size) throws IOException; -} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkHandler.java similarity index 92% rename from library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkHandler.java index 518172cc77..13f490fcd9 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkHandler.java @@ -26,7 +26,7 @@ import java.io.IOException; /** * Peeks an MP4V stream looking for pixelWidthHeightRatio data */ -public class Mp4vChunkPeeker extends NalChunkPeeker { +public class Mp4vChunkHandler extends NalChunkHandler { @VisibleForTesting static final byte SEQUENCE_START_CODE = (byte)0xb0; @VisibleForTesting @@ -36,15 +36,14 @@ public class Mp4vChunkPeeker extends NalChunkPeeker { static final int Extended_PAR = 0xf; private final Format.Builder formatBuilder; - private final TrackOutput trackOutput; @VisibleForTesting() float pixelWidthHeightRatio = 1f; - public Mp4vChunkPeeker(@NonNull Format.Builder formatBuilder, @NonNull TrackOutput trackOutput) { - super(5); + public Mp4vChunkHandler(int id, @NonNull TrackOutput trackOutput, + @NonNull ChunkClock clock, @NonNull Format.Builder formatBuilder) { + super(id, trackOutput, clock, 5); this.formatBuilder = formatBuilder; - this.trackOutput = trackOutput; } @Override 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 new file mode 100644 index 0000000000..a66d6d105f --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +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; + +/** + * Resolves several issues with Mpeg Audio + * 1. That muxers don't always mux MPEG audio on the frame boundary + * 2. That some codecs can't handle multiple or partial frames (Pixels) + */ +public class MpegAudioChunkHandler extends ChunkHandler { + private final MpegAudioUtil.Header header = new MpegAudioUtil.Header(); + private final ParsableByteArray scratch = new ParsableByteArray(8); + private final int samplesPerSecond; + //Bytes remaining in the Mpeg Audio frame + private int frameRemaining; + private long timeUs = 0L; + + MpegAudioChunkHandler(int id, @NonNull TrackOutput trackOutput, @NonNull ChunkClock clock, + int samplesPerSecond) { + super(id, TYPE_AUDIO, trackOutput, clock); + this.samplesPerSecond = samplesPerSecond; + } + + @Override + public boolean newChunk(int size, @NonNull ExtractorInput input) throws IOException { + if (size == 0) { + //Empty frame, advance the clock and sync + clock.advance(); + syncTime(); + return true; + } + 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. + if (scratch.limit() == size) { + scratch.setPosition(0); + trackOutput.sampleData(scratch, size); + scratch.reset(0); + done(size); + } + return true; + } + return false; + } + + /** + * Read from input to scratch + * @param bytes to attempt to read + * @return {@link C#RESULT_END_OF_INPUT} or number of bytes read. + */ + int readScratch(ExtractorInput input, int bytes) throws IOException { + final int toRead = Math.min(bytes, chunkRemaining); + final int read = input.read(scratch.getData(), scratch.limit(), toRead); + if (read == C.RESULT_END_OF_INPUT) { + return read; + } + chunkRemaining -= read; + scratch.setLimit(scratch.limit() + read); + return read; + } + + /** + * Attempt to find a frame header in the input + * @return true if a frame header was found + */ + @VisibleForTesting + boolean findFrame(ExtractorInput input) throws IOException { + scratch.reset(0); + scratch.ensureCapacity(scratch.limit() + chunkRemaining); + int toRead = 4; + while (chunkRemaining > 0 && readScratch(input, toRead) != C.RESULT_END_OF_INPUT) { + while (scratch.bytesLeft() >= 4) { + if (header.setForHeaderData(scratch.readInt())) { + scratch.skipBytes(-4); + return true; + } + scratch.skipBytes(-3); + } + // 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; + } + + /** + * Process the chunk by breaking it in Mpeg audio frames + * @return true if the chunk has been completely processed + */ + @VisibleForTesting + boolean process(ExtractorInput input) throws IOException { + if (frameRemaining == 0) { + //Find the next frame + if (findFrame(input)) { + final int scratchBytes = scratch.bytesLeft(); + trackOutput.sampleData(scratch, scratchBytes); + frameRemaining = header.frameSize - scratchBytes; + } else { + return true; + } + } + final int bytes = trackOutput.sampleData(input, Math.min(frameRemaining, chunkRemaining), false); + frameRemaining -= bytes; + if (frameRemaining == 0) { + trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, header.frameSize, 0, null); + //Log.d(AviExtractor.TAG, "MP3: us=" + us); + timeUs += header.samplesPerFrame * C.MICROS_PER_SECOND / samplesPerSecond; + } + chunkRemaining -= bytes; + return chunkRemaining == 0; + } + + @Override + 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(); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + long getTimeUs() { + return timeUs; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + int getFrameRemaining() { + return frameRemaining; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkHandler.java similarity index 88% rename from library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkHandler.java index e5be793ae4..0b8cb2ffd9 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkHandler.java @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.extractor.avi; +import androidx.annotation.NonNull; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; import java.io.IOException; import java.util.Arrays; @@ -23,7 +25,7 @@ import java.util.Arrays; * Generic base class for NAL (0x00 0x00 0x01) chunk headers * Theses are used by AVC and MP4V (XVID) */ -public abstract class NalChunkPeeker implements ChunkPeeker { +public abstract class NalChunkHandler extends ChunkHandler { private static final int SEEK_PEEK_SIZE = 256; private final int peekSize; @@ -31,6 +33,15 @@ public abstract class NalChunkPeeker implements ChunkPeeker { transient byte[] buffer; transient int pos; + NalChunkHandler(int id, @NonNull TrackOutput trackOutput, + @NonNull ChunkClock clock, int peakSize) { + super(id, TYPE_VIDEO, trackOutput, clock); + if (peakSize < 5) { + throw new IllegalArgumentException("Peak size must at least be 5"); + } + this.peekSize = peakSize; + } + abstract void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException; /** @@ -100,15 +111,13 @@ public abstract class NalChunkPeeker implements ChunkPeeker { return -1; } - public NalChunkPeeker(int peakSize) { - if (peakSize < 5) { - throw new IllegalArgumentException("Peak size must at least be 5"); - } - this.peekSize = peakSize; - } - abstract boolean skip(byte nalType); + public boolean newChunk(int size, ExtractorInput input) throws IOException { + peek(input, size); + return super.newChunk(size, input); + } + public void peek(ExtractorInput input, final int size) throws IOException { buffer = new byte[peekSize]; if (!input.peekFully(buffer, 0, peekSize, true)) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java index ccec181b4c..c7896f36f1 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -20,7 +20,7 @@ import androidx.annotation.VisibleForTesting; /** * Properly calculates the frame time for H264 frames using PicCount */ -public class PicCountClock extends LinearClock { +public class PicCountClock extends ChunkClock { //The frame as a calculated from the picCount private int picIndex; private int lastPicCount; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java index e450d14848..7e3437b9d0 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java @@ -16,13 +16,6 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.util.Log; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.nio.BufferOverflowException; -import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -39,7 +32,6 @@ public class ResidentBox extends Box { /** * Returns shallow copy of this ByteBuffer with the position at 0 - * @return */ @NonNull public ByteBuffer getByteBuffer() { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java index 357bc9ecfe..03d1757047 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.extractor.avi; import java.nio.ByteBuffer; /** - * Human readable stream name + * Box containing a human readable stream name */ public class StreamNameBox extends ResidentBox { public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24); 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/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(); } 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 da9ddec141..09a8e03dc5 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,17 +35,17 @@ 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 AvcChunkPeeker avcChunkPeeker; + private AvcChunkHandler avcChunkHandler; @Before public void before() { fakeTrackOutput = new FakeTrackOutput(false); - avcChunkPeeker = new AvcChunkPeeker(FORMAT_BUILDER_AVC, fakeTrackOutput, - new LinearClock(10_000_000L, 24 * 10)); + avcChunkHandler = new AvcChunkHandler(0, fakeTrackOutput, + new ChunkClock(10_000_000L, 24 * 10), FORMAT_BUILDER_AVC); } private void peekStreamHeader() throws IOException { @@ -55,25 +55,26 @@ public class AvcChunkPeekerTest { final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build(); - avcChunkPeeker.peek(input, bytes.length); + avcChunkHandler.peek(input, bytes.length); } @Test public void peek_givenStreamHeader() throws IOException { peekStreamHeader(); - final PicCountClock picCountClock = avcChunkPeeker.getClock(); + final PicCountClock picCountClock = avcChunkHandler.getPicCountClock(); + Assert.assertNotNull(picCountClock); Assert.assertEquals(64, picCountClock.getMaxPicCount()); - Assert.assertEquals(0, avcChunkPeeker.getSpsData().picOrderCountType); + Assert.assertEquals(0, avcChunkHandler.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 = avcChunkHandler.getPicCountClock(); final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build(); - avcChunkPeeker.peek(input, P_SLICE.length); + avcChunkHandler.newChunk(P_SLICE.length, input); Assert.assertEquals(12, picCountClock.getLastPicCount()); } 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 6e85eeaa5d..a8d5aa4274 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 @@ -108,26 +108,26 @@ public class AviExtractorRoboTest { Assert.assertEquals(AviExtractor.STATE_FIND_MOVI, aviExtractor.state); - final AviTrack aviTrack = aviExtractor.getVideoTrack(); - Assert.assertEquals(aviTrack.getClock().durationUs, streamHeaderBox.getDurationUs()); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); + Assert.assertEquals(chunkHandler.getClock().durationUs, streamHeaderBox.getDurationUs()); } @Test public void readSamples_fragmentedChunk() throws IOException { AviExtractor aviExtractor = AviExtractorTest.setupVideoAviExtractor(); - final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); final int size = 24 + 16; final ByteBuffer byteBuffer = AviExtractor.allocate(size + 8); - byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(chunkHandler.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())); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder())); - final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.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 ead5808eaa..205ddab5c6 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 @@ -165,7 +165,7 @@ public class AviExtractorTest { Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24))); } - private void assertIdx1(AviSeekMap aviSeekMap, AviTrack videoTrack, int keyFrames, + private void assertIdx1(AviSeekMap aviSeekMap, ChunkHandler videoTrack, int keyFrames, int keyFrameRate) { Assert.assertEquals(keyFrames, videoTrack.keyFrames.length); @@ -192,9 +192,9 @@ public class AviExtractorTest { final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds final int keyFrames = secs * DataHelper.FPS / keyFrameRate; final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate); - final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs); - final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); - aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); + final ChunkHandler videoTrack = DataHelper.getVideoChunkHandler(secs); + final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs); + aviExtractor.setChunkHandlers(new ChunkHandler[]{videoTrack, audioTrack}); aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); aviExtractor.state = AviExtractor.STATE_READ_IDX1; aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024); @@ -213,7 +213,7 @@ public class AviExtractorTest { final AviSeekMap aviSeekMap = aviExtractor.aviSeekMap; assertIdx1(aviSeekMap, videoTrack, keyFrames, keyFrameRate); - Assert.assertEquals(AviExtractor.STATE_READ_SAMPLES, aviExtractor.state); + Assert.assertEquals(AviExtractor.STATE_READ_CHUNKS, aviExtractor.state); Assert.assertEquals(DataHelper.MOVI_OFFSET + 4, positionHolder.position); } @@ -225,8 +225,8 @@ public class AviExtractorTest { final int secs = 9; final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate); - final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); - aviExtractor.setAviTracks(new AviTrack[]{audioTrack}); + final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs); + aviExtractor.setChunkHandlers(new ChunkHandler[]{audioTrack}); final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder() .setData(idx1.array()).build(); @@ -250,9 +250,9 @@ public class AviExtractorTest { junk.putInt(0); idx1.flip(); junk.put(idx1); - final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs); - final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); - aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); + final ChunkHandler videoTrack = DataHelper.getVideoChunkHandler(secs); + final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs); + aviExtractor.setChunkHandlers(new ChunkHandler[]{videoTrack, audioTrack}); final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). setData(junk.array()).build(); @@ -268,16 +268,16 @@ public class AviExtractorTest { aviExtractor.init(fakeExtractorOutput); final int secs = 4; final ByteBuffer idx1 = DataHelper.getIndex(secs, 1); - final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs); - final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); - aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); + final ChunkHandler videoTrack = DataHelper.getVideoChunkHandler(secs); + final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs); + aviExtractor.setChunkHandlers(new ChunkHandler[]{videoTrack, audioTrack}); final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). setData(idx1.array()).build(); aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); //We should be throttled to 2 key frame per second - Assert.assertSame(AviTrack.ALL_KEY_FRAMES, videoTrack.keyFrames); + Assert.assertSame(ChunkHandler.ALL_KEY_FRAMES, videoTrack.keyFrames); } @Test @@ -389,12 +389,12 @@ public class AviExtractorTest { final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); aviExtractor.init(fakeExtractorOutput); - final AviTrack aviTrack = DataHelper.getVideoAviTrack(9); - aviExtractor.setAviTracks(new AviTrack[]{aviTrack}); + final ChunkHandler chunkHandler = DataHelper.getVideoChunkHandler(9); + aviExtractor.setChunkHandlers(new ChunkHandler[]{chunkHandler}); final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build(); - aviTrack.trackOutput.format(format); + chunkHandler.trackOutput.format(format); - aviExtractor.state = AviExtractor.STATE_READ_SAMPLES; + aviExtractor.state = AviExtractor.STATE_READ_CHUNKS; aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024); return aviExtractor; } @@ -403,9 +403,9 @@ public class AviExtractorTest { public void readSamples_givenAtEndOfInput() throws IOException { AviExtractor aviExtractor = setupVideoAviExtractor(); aviExtractor.setMovi(0, 0); - final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); final ByteBuffer byteBuffer = AviExtractor.allocate(32); - byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(chunkHandler.chunkId); byteBuffer.putInt(24); final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build(); @@ -415,47 +415,47 @@ public class AviExtractorTest { @Test public void readSamples_completeChunk() throws IOException { AviExtractor aviExtractor = setupVideoAviExtractor(); - final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); final ByteBuffer byteBuffer = AviExtractor.allocate(32); - byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(chunkHandler.chunkId); byteBuffer.putInt(24); final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) .build(); - Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder())); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(input, new PositionHolder())); - final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput; Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length); } @Test public void readSamples_givenLeadingZeros() throws IOException { AviExtractor aviExtractor = setupVideoAviExtractor(); - final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); final ByteBuffer byteBuffer = AviExtractor.allocate(48); byteBuffer.position(16); - byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(chunkHandler.chunkId); byteBuffer.putInt(24); final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) .build(); - Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder())); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(input, new PositionHolder())); - final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput; Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length); } @Test public void seek_givenPosition0() throws IOException { final AviExtractor aviExtractor = setupVideoAviExtractor(); - final AviTrack aviTrack = aviExtractor.getVideoTrack(); - aviExtractor.setChunkHandler(aviTrack); - aviTrack.getClock().setIndex(10); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); + aviExtractor.setChunkHandler(chunkHandler); + chunkHandler.getClock().setIndex(10); aviExtractor.seek(0L, 0L); Assert.assertNull(aviExtractor.getChunkHandler()); - Assert.assertEquals(0, aviTrack.getClock().getIndex()); + Assert.assertEquals(0, chunkHandler.getClock().getIndex()); Assert.assertEquals(aviExtractor.state, AviExtractor.STATE_SEEK_START); @@ -470,18 +470,18 @@ public class AviExtractorTest { final AviExtractor aviExtractor = setupVideoAviExtractor(); final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); aviExtractor.aviSeekMap = aviSeekMap; - final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); final long position = DataHelper.MOVI_OFFSET + aviSeekMap.keyFrameOffsetsDiv2[1] * 2L; aviExtractor.seek(position, 0L); - Assert.assertEquals(aviSeekMap.seekIndexes[aviTrack.id][1], aviTrack.getClock().getIndex()); + Assert.assertEquals(aviSeekMap.seekIndexes[chunkHandler.getId()][1], chunkHandler.getClock().getIndex()); } @Test - public void getAviTrack_givenListWithNull() { + public void getChunkHandler_givenListWithNull() { final AviExtractor aviExtractor = new AviExtractor(); - final AviTrack aviTrack = DataHelper.getAudioAviTrack(9); - aviExtractor.setAviTracks(new AviTrack[]{null, aviTrack}); - Assert.assertSame(aviTrack, aviExtractor.getAviTrack(aviTrack.chunkId)); + final ChunkHandler chunkHandler = DataHelper.getAudioChunkHandler(9); + aviExtractor.setChunkHandlers(new ChunkHandler[]{null, chunkHandler}); + Assert.assertSame(chunkHandler, aviExtractor.getChunkHandler(chunkHandler.chunkId)); } @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 91aded8755..60e12289c9 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,7 +15,6 @@ */ 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; @@ -23,26 +22,25 @@ import org.junit.Test; public class AviSeekMapTest { @Test - public void setFrames_givenExactSeekPointMatch() { + public void getFrames_givenExactSeekPointMatch() { final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.seekOffset; final int secs = 4; - final AviTrack[] aviTracks = new AviTrack[]{DataHelper.getVideoAviTrack(secs), - DataHelper.getAudioAviTrack(secs)}; + final ChunkHandler[] chunkHandlers = new ChunkHandler[]{DataHelper.getVideoChunkHandler(secs), + DataHelper.getAudioChunkHandler(secs)}; - aviSeekMap.setFrames(position, C.MICROS_PER_SECOND, aviTracks); - for (int i=0;i list = new ArrayList<>(2); list.add(streamHeaderBox); list.add(streamFormatBox); - return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()), + return new ListBox(streamHeaderBox.getSize() + streamFormatBox.getSize(), ListBox.TYPE_STRL, list); } @@ -104,18 +103,16 @@ public class DataHelper { return byteBuffer; } - public static AviTrack getVideoAviTrack(int sec) { + public static ChunkHandler getVideoChunkHandler(int sec) { final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); - return new AviTrack(0, C.TRACK_TYPE_VIDEO, - new LinearClock(sec * 1_000_000L, sec * FPS), - fakeTrackOutput); + return new ChunkHandler(0, ChunkHandler.TYPE_VIDEO, fakeTrackOutput, + new ChunkClock(sec * 1_000_000L, sec * FPS)); } - public static AviTrack getAudioAviTrack(int sec) { + public static ChunkHandler getAudioChunkHandler(int sec) { final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); - return new AviTrack(AUDIO_ID, C.TRACK_TYPE_AUDIO, - new LinearClock(sec * 1_000_000L, sec * FPS * AUDIO_PER_VIDEO), - fakeTrackOutput); + return new ChunkHandler(AUDIO_ID, ChunkHandler.TYPE_AUDIO, fakeTrackOutput, + new ChunkClock(sec * 1_000_000L, sec * FPS * AUDIO_PER_VIDEO)); } public static AviSeekMap getAviSeekMap() { @@ -153,8 +150,8 @@ public class DataHelper { */ public static ByteBuffer getIndex(final int secs, final int keyFrameRate, int offset) { final int videoFrames = secs * FPS; - final int videoChunkId = AviTrack.getVideoChunkId(0); - final int audioChunkId = AviTrack.getAudioChunkId(1); + final int videoChunkId = ChunkHandler.TYPE_VIDEO | ChunkHandler.getChunkIdLower(0); + final int audioChunkId = ChunkHandler.TYPE_AUDIO | ChunkHandler.getChunkIdLower(1); final ByteBuffer byteBuffer = AviExtractor.allocate((videoFrames + videoFrames*AUDIO_PER_VIDEO) * 16); for (int v=0;v