From 7ea2d75fcdc72132b3d0c0d94e1300f291452fb5 Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 24 Jan 2022 16:02:37 -0700 Subject: [PATCH] Refactor Clock logic. Refactor peeking for MP4V and AVC. Moved AVI above MP3. --- .../extractor/DefaultExtractorsFactory.java | 2 +- .../exoplayer2/extractor/avi/AvcAviTrack.java | 173 ------- .../extractor/avi/AvcChunkPeeker.java | 112 +++++ .../extractor/avi/AviExtractor.java | 442 ++++++++++-------- .../extractor/avi/AviHeaderBox.java | 31 +- .../exoplayer2/extractor/avi/AviSeekMap.java | 54 ++- .../exoplayer2/extractor/avi/AviTrack.java | 75 ++- .../exoplayer2/extractor/avi/ChunkPeeker.java | 8 + .../exoplayer2/extractor/avi/LinearClock.java | 27 ++ .../extractor/avi/Mp4vAviTrack.java | 81 ---- .../extractor/avi/Mp4vChunkPeeker.java | 76 +++ .../extractor/avi/NalChunkPeeker.java | 113 +++++ .../extractor/avi/PicCountClock.java | 62 +++ .../extractor/avi/StreamHeaderBox.java | 1 + .../extractor/avi/AviExtractorTest.java | 101 ++++ .../exoplayer2/extractor/avi/BitBuffer.java | 46 ++ .../exoplayer2/extractor/avi/DataHelper.java | 8 + .../extractor/avi/MockNalChunkPeeker.java | 22 + .../extractor/avi/Mp4vAviTrackTest.java | 51 -- .../extractor/avi/Mp4vChunkPeekerTest.java | 65 +++ .../extractor/avi/NalChunkPeekerTest.java | 55 +++ 21 files changed, 1020 insertions(+), 585 deletions(-) delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java delete mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 793762569c..5c5f89afce 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -98,9 +98,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { FileTypes.ADTS, FileTypes.AC3, FileTypes.AC4, + FileTypes.AVI, FileTypes.MP3, FileTypes.JPEG, - FileTypes.AVI, }; private static final FlacExtensionLoader FLAC_EXTENSION_LOADER = new FlacExtensionLoader(); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java deleted file mode 100644 index 4b3e6f8de5..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.google.android.exoplayer2.extractor.avi; - -import androidx.annotation.NonNull; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -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.NalUnitUtil; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; -import java.io.IOException; - -public class AvcAviTrack extends AviTrack{ - private static final int NAL_TYPE_IRD = 5; - private static final int NAL_TYPE_SEI = 6; - private static final int NAL_TYPE_SPS = 7; - private static final int NAL_MASK = 0x1f; - private Format.Builder formatBuilder; - private float pixelWidthHeightRatio = 1f; - private NalUnitUtil.SpsData spsData; - //The frame as a calculated from the picCount - private int picFrame; - private int lastPicCount; - //Largest picFrame, used when we hit an I frame - private int maxPicFrame =-1; - private int maxPicCount; - private int posHalf; - private int negHalf; - - AvcAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput, - @NonNull Format.Builder formatBuilder) { - super(id, streamHeaderBox, trackOutput); - this.formatBuilder = formatBuilder; - } - - public void setFormatBuilder(Format.Builder formatBuilder) { - this.formatBuilder = formatBuilder; - } - - private int seekNal(final ParsableByteArray parsableByteArray) { - final byte[] buffer = parsableByteArray.getData(); - for (int i=parsableByteArray.getPosition();i= 0) { - if (nal == NAL_TYPE_SPS) { - spsData = NalUnitUtil.parseSpsNalUnitPayload(parsableByteArray.getData(), parsableByteArray.getPosition(), parsableByteArray.capacity()); - maxPicCount = 1 << (spsData.picOrderCntLsbLength); - posHalf = maxPicCount / 2; //Not sure why pics are 2x - negHalf = -posHalf; - if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { - formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio); - trackOutput.format(formatBuilder.build()); - } - Log.d(AviExtractor.TAG, "SPS Frame: maxPicCount=" + maxPicCount); - } else if (nal == NAL_TYPE_IRD) { - processIdr(); - } - } - parsableByteArray.setPosition(0); - trackOutput.sampleData(parsableByteArray, parsableByteArray.capacity()); - int flags = 0; - if (isKeyFrame()) { - flags |= C.BUFFER_FLAG_KEY_FRAME; - } - trackOutput.sampleMetadata(getUs(frame), flags, parsableByteArray.capacity(), 0, null); - Log.d(AviExtractor.TAG, "SPS Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame()); - advance(); - } - - @Override - int getUsFrame() { - return picFrame; - } - - @Override - void seekFrame(int frame) { - super.seekFrame(frame); - this.picFrame = frame; - lastPicCount = 0; - } - - int getPicOrderCountLsb(byte[] peek) { - if (peek[3] != 1) { - return -1; - } - final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, 5, peek.length); - //slide_header() - in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice - in.readUnsignedExpGolombCodedInt(); //slice_type - in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id - if (spsData.separateColorPlaneFlag) { - in.skipBits(2); //colour_plane_id - } - in.readBits(spsData.frameNumLength); //frame_num - if (!spsData.frameMbsOnlyFlag) { - boolean field_pic_flag = in.readBit(); // field_pic_flag - if (field_pic_flag) { - in.readBit(); // bottom_field_flag - } - } - //We skip IDR in the switch - if (spsData.picOrderCountType == 0) { - int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); - //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); - return picOrderCountLsb; - } - return -1; - } - - @Override - public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { - final int peekSize = Math.min(size, 16); - byte[] peek = new byte[peekSize]; - input.peekFully(peek, 0, peekSize); - final int nalType = peek[4] & NAL_MASK; - switch (nalType) { - case 1: - case 2: - case 3: - case 4: { - final int picCount = getPicOrderCountLsb(peek); - if (picCount < 0) { - Log.d(AviExtractor.TAG, "Error getting PicOrder"); - seekFrame(frame); - } - int delta = picCount - lastPicCount; - if (delta < negHalf) { - delta += maxPicCount; - } else if (delta > posHalf) { - delta -= maxPicCount; - } - picFrame += delta / 2; - lastPicCount = picCount; - if (maxPicFrame < picFrame) { - maxPicFrame = picFrame; - } - break; - } - case NAL_TYPE_IRD: - processIdr(); - break; - case NAL_TYPE_SEI: - case NAL_TYPE_SPS: - readSps(size, input); - return true; - } - return super.newChunk(tag, size, input); - } -} 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/AvcChunkPeeker.java new file mode 100644 index 0000000000..67620785e5 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java @@ -0,0 +1,112 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.NalUnitUtil; +import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import java.io.IOException; + +public class AvcChunkPeeker extends NalChunkPeeker { + private static final int NAL_TYPE_MASK = 0x1f; + private static final int NAL_TYPE_IRD = 5; + private static final int NAL_TYPE_SEI = 6; + private static final int NAL_TYPE_SPS = 7; + private static final int NAL_TYPE_PPS = 8; + + 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, long usPerChunk) { + super(16); + this.formatBuilder = formatBuilder; + this.trackOutput = trackOutput; + picCountClock = new PicCountClock(usPerChunk); + } + + public PicCountClock getPicCountClock() { + return picCountClock; + } + + @Override + boolean skip(byte nalType) { + return false; + } + + void updatePicCountClock(final int nalTypeOffset) { + final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length); + //slide_header() + in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice + in.readUnsignedExpGolombCodedInt(); //slice_type + in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id + if (spsData.separateColorPlaneFlag) { + in.skipBits(2); //colour_plane_id + } + in.readBits(spsData.frameNumLength); //frame_num + if (!spsData.frameMbsOnlyFlag) { + boolean field_pic_flag = in.readBit(); // field_pic_flag + if (field_pic_flag) { + in.readBit(); // bottom_field_flag + } + } + //We skip IDR in the switch + if (spsData.picOrderCountType == 0) { + int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); + //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); + picCountClock.setPicCount(picOrderCountLsb); + return; + } + picCountClock.setIndex(picCountClock.getIndex()); + } + + private int readSps(ExtractorInput input, int nalTypeOffset) throws IOException { + final int spsStart = nalTypeOffset + 1; + nalTypeOffset = seekNextNal(input, spsStart); + spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); + picCountClock.setMaxPicCount(1 << (spsData.picOrderCntLsbLength)); + if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { + pixelWidthHeightRatio = spsData.pixelWidthHeightRatio; + formatBuilder.setPixelWidthHeightRatio(pixelWidthHeightRatio); + trackOutput.format(formatBuilder.build()); + } + return nalTypeOffset; + } + + @Override + void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException { + while (true) { + final int nalType = buffer[nalTypeOffset] & NAL_TYPE_MASK; + switch (nalType) { + case 1: + case 2: + case 3: + case 4: + updatePicCountClock(nalTypeOffset); + return; + case NAL_TYPE_IRD: + picCountClock.syncIndexes(); + return; + case NAL_TYPE_SEI: + case NAL_TYPE_PPS: { + nalTypeOffset = seekNextNal(input, nalTypeOffset); + //Usually chunks have other NALs after these, so just continue + break; + } + case NAL_TYPE_SPS: + nalTypeOffset = readSps(input, nalTypeOffset); + //Sometimes video frames lurk after these + break; + default: + return; + } + if (nalTypeOffset < 0) { + return; + } + compact(); + } + } +} 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 8f0dda0d56..18fc767061 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 @@ -1,8 +1,8 @@ package com.google.android.exoplayer2.extractor.avi; -import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; @@ -17,9 +17,6 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; /** * Based on the official MicroSoft spec @@ -43,13 +40,19 @@ public class AviExtractor implements Extractor { } static final String TAG = "AviExtractor"; - private static final int PEEK_BYTES = 28; + @VisibleForTesting + static final int PEEK_BYTES = 28; - private static final int STATE_READ_TRACKS = 0; - private static final int STATE_FIND_MOVI = 1; - private static final int STATE_READ_IDX1 = 2; - private static final int STATE_READ_SAMPLES = 3; - private static final int STATE_SEEK_START = 4; + @VisibleForTesting + static final int STATE_READ_TRACKS = 0; + @VisibleForTesting + static final int STATE_FIND_MOVI = 1; + @VisibleForTesting + static final int STATE_READ_IDX1 = 2; + @VisibleForTesting + static final int STATE_READ_SAMPLES = 3; + @VisibleForTesting + static final int STATE_SEEK_START = 4; private static final int AVIIF_KEYFRAME = 16; @@ -68,16 +71,17 @@ public class AviExtractor implements Extractor { static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds - private int state; - private ExtractorOutput output; + @VisibleForTesting + int state; + @VisibleForTesting + ExtractorOutput output; private AviHeaderBox aviHeader; private long durationUs = C.TIME_UNSET; - private SparseArray idTrackMap = new SparseArray<>(); + private AviTrack[] aviTracks = new AviTrack[0]; //At the start of the movi tag private long moviOffset; private long moviEnd; private AviSeekMap aviSeekMap; - private int flags; // private long indexOffset; //Usually chunkStart @@ -99,49 +103,42 @@ public class AviExtractor implements Extractor { return position; } - public AviExtractor() { - this(0); - } - - public AviExtractor(int flags) { - this.flags = flags; - } - - @Override - public boolean sniff(ExtractorInput input) throws IOException { - return peekHeaderList(input); - } - - static ByteBuffer allocate(int bytes) { - final byte[] buffer = new byte[bytes]; - final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - return byteBuffer; - } - - private void setSeekMap(AviSeekMap aviSeekMap) { - this.aviSeekMap = aviSeekMap; - output.seekMap(aviSeekMap); - } - - static boolean peekHeaderList(ExtractorInput input) throws IOException { - final ByteBuffer byteBuffer = allocate(PEEK_BYTES); - input.peekFully(byteBuffer.array(), 0, PEEK_BYTES); + /** + * + * @param input + * @param bytes Must be at least 20 + */ + @Nullable + private ByteBuffer getAviBuffer(ExtractorInput input, int bytes) throws IOException { + if (input.getLength() < bytes) { + return null; + } + final ByteBuffer byteBuffer = allocate(bytes); + input.peekFully(byteBuffer.array(), 0, bytes); final int riff = byteBuffer.getInt(); if (riff != AviExtractor.RIFF) { - return false; + return null; } long reportedLen = getUInt(byteBuffer) + byteBuffer.position(); final long inputLen = input.getLength(); if (inputLen != C.LENGTH_UNSET && inputLen != reportedLen) { - Log.w(TAG, "Header length doesn't match stream length"); + w("Header length doesn't match stream length"); } int avi = byteBuffer.getInt(); if (avi != AviExtractor.AVI_) { - return false; + return null; } final int list = byteBuffer.getInt(); if (list != ListBox.LIST) { + return null; + } + return byteBuffer; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + final ByteBuffer byteBuffer = getAviBuffer(input, PEEK_BYTES); + if (byteBuffer == null) { return false; } //Len @@ -157,27 +154,25 @@ public class AviExtractor implements Extractor { return true; } + static ByteBuffer allocate(int bytes) { + final byte[] buffer = new byte[bytes]; + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return byteBuffer; + } + + private void setSeekMap(AviSeekMap aviSeekMap) { + this.aviSeekMap = aviSeekMap; + output.seekMap(aviSeekMap); + } + @Nullable ListBox readHeaderList(ExtractorInput input) throws IOException { - final ByteBuffer byteBuffer = allocate(20); - input.readFully(byteBuffer.array(), 0, byteBuffer.capacity()); - final int riff = byteBuffer.getInt(); - if (riff != AviExtractor.RIFF) { - return null; - } - long reportedLen = getUInt(byteBuffer) + byteBuffer.position(); - final long inputLen = input.getLength(); - if (inputLen != C.LENGTH_UNSET && inputLen != reportedLen) { - Log.w(TAG, "Header length doesn't match stream length"); - } - final int avi = byteBuffer.getInt(); - if (avi != AviExtractor.AVI_) { - return null; - } - final int list = byteBuffer.getInt(); - if (list != ListBox.LIST) { + final ByteBuffer byteBuffer = getAviBuffer(input, 20); + if (byteBuffer == null) { return null; } + input.skipFully(20); final int listSize = byteBuffer.getInt(); final ListBox listBox = ListBox.newInstance(listSize, new BoxFactory(), input); if (listBox.getListType() != ListBox.TYPE_HDRL) { @@ -196,11 +191,74 @@ public class AviExtractor implements Extractor { this.output = output; } - private static Box peekNext(final List streams, int i, int type) { - if (i + 1 < streams.size() && streams.get(i + 1).getType() == type) { - return streams.get(i + 1); + private void parseStream(final ListBox streamList, int streamId) { + final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); + final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); + if (streamHeader == null) { + Log.w(TAG, "Missing Stream Header"); + return; + } + if (streamFormat == null) { + Log.w(TAG, "Missing Stream Format"); + return; + } + final Format.Builder builder = new Format.Builder(); + builder.setId(streamId); + final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); + if (suggestedBufferSize != 0) { + builder.setMaxInputSize(suggestedBufferSize); + } + final StreamNameBox streamName = streamList.getChild(StreamNameBox.class); + if (streamName != null) { + builder.setLabel(streamName.getName()); + } + if (streamHeader.isVideo()) { + final String mimeType = streamHeader.getMimeType(); + if (mimeType == null) { + Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); + return; + } + final VideoFormat videoFormat = streamFormat.getVideoFormat(); + final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO); + builder.setWidth(videoFormat.getWidth()); + builder.setHeight(videoFormat.getHeight()); + builder.setFrameRate(streamHeader.getFrameRate()); + builder.setSampleMimeType(mimeType); + + final AviTrack aviTrack = new AviTrack(streamId, streamHeader, trackOutput); + if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { + Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(builder, trackOutput); + aviTrack.setChunkPeeker(mp4vChunkPeeker); + } else if (MimeTypes.VIDEO_H264.equals(mimeType)) { + final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample()); + aviTrack.setClock(avcChunkPeeker.getPicCountClock()); + aviTrack.setChunkPeeker(avcChunkPeeker); + } + trackOutput.format(builder.build()); + durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); + aviTracks[streamId] = aviTrack; + } else if (streamHeader.isAudio()) { + final AudioFormat audioFormat = streamFormat.getAudioFormat(); + final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); + final String mimeType = audioFormat.getMimeType(); + builder.setSampleMimeType(mimeType); + //builder.setCodecs(audioFormat.getCodec()); + builder.setChannelCount(audioFormat.getChannels()); + builder.setSampleRate(audioFormat.getSamplesPerSecond()); + if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) { + final short bps = audioFormat.getBitsPerSample(); + if (bps == 8) { + builder.setPcmEncoding(C.ENCODING_PCM_8BIT); + } else if (bps == 16){ + builder.setPcmEncoding(C.ENCODING_PCM_16BIT); + } + } + if (MimeTypes.AUDIO_AAC.equals(mimeType) && audioFormat.getCbSize() > 0) { + builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); + } + trackOutput.format(builder.build()); + aviTracks[streamId] = new AviTrack(streamId, streamHeader, trackOutput); } - return null; } private int readTracks(ExtractorInput input) throws IOException { @@ -212,83 +270,15 @@ public class AviExtractor implements Extractor { if (aviHeader == null) { throw new IOException("AviHeader not found"); } + aviTracks = new AviTrack[aviHeader.getStreams()]; //This is usually wrong, so it will be overwritten by video if present - durationUs = aviHeader.getFrames() * (long)aviHeader.getMicroSecPerFrame(); + durationUs = aviHeader.getTotalFrames() * (long)aviHeader.getMicroSecPerFrame(); int streamId = 0; for (Box box : headerList.getChildren()) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { final ListBox streamList = (ListBox) box; - final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); - final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); - if (streamHeader == null) { - Log.w(TAG, "Missing Stream Header"); - continue; - } - if (streamFormat == null) { - Log.w(TAG, "Missing Stream Format"); - continue; - } - final Format.Builder builder = new Format.Builder(); - builder.setId(streamId); - final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); - if (suggestedBufferSize != 0) { - builder.setMaxInputSize(suggestedBufferSize); - } - final StreamNameBox streamName = streamList.getChild(StreamNameBox.class); - if (streamName != null) { - builder.setLabel(streamName.getName()); - } - if (streamHeader.isVideo()) { - final String mimeType = streamHeader.getMimeType(); - if (mimeType == null) { - Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); - continue; - } - final VideoFormat videoFormat = streamFormat.getVideoFormat(); - final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO); - builder.setWidth(videoFormat.getWidth()); - builder.setHeight(videoFormat.getHeight()); - builder.setFrameRate(streamHeader.getFrameRate()); - builder.setSampleMimeType(mimeType); - - final AviTrack aviTrack; - switch (mimeType) { - case MimeTypes.VIDEO_MP4V: - aviTrack = new Mp4vAviTrack(streamId, streamHeader, trackOutput, builder); - break; - case MimeTypes.VIDEO_H264: - aviTrack = new AvcAviTrack(streamId, streamHeader, trackOutput, builder); - break; - default: - aviTrack = new AviTrack(streamId, streamHeader, trackOutput); - } - trackOutput.format(builder.build()); - idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), aviTrack); - durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); - } else if (streamHeader.isAudio()) { - final AudioFormat audioFormat = streamFormat.getAudioFormat(); - final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); - final String mimeType = audioFormat.getMimeType(); - builder.setSampleMimeType(mimeType); - //builder.setCodecs(audioFormat.getCodec()); - builder.setChannelCount(audioFormat.getChannels()); - builder.setSampleRate(audioFormat.getSamplesPerSecond()); - if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) { - final short bps = audioFormat.getBitsPerSample(); - if (bps == 8) { - builder.setPcmEncoding(C.ENCODING_PCM_8BIT); - } else if (bps == 16){ - builder.setPcmEncoding(C.ENCODING_PCM_16BIT); - } - } - if (MimeTypes.AUDIO_AAC.equals(mimeType) && audioFormat.getCbSize() > 0) { - builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); - } - trackOutput.format(builder.build()); - idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24), - new AviTrack(streamId, streamHeader, trackOutput)); - } + parseStream(streamList, streamId); streamId++; } } @@ -323,6 +313,15 @@ public class AviExtractor implements Extractor { return RESULT_SEEK; } + private AviTrack getVideoTrack() { + for (@Nullable AviTrack aviTrack : aviTracks) { + if (aviTrack != null && aviTrack.isVideo()) { + return aviTrack; + } + } + return null; + } + /** * Reads the index and sets the keyFrames and creates the SeekMap * @param input @@ -330,27 +329,20 @@ public class AviExtractor implements Extractor { * @throws IOException */ void readIdx1(ExtractorInput input, int remaining) throws IOException { - final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); - final byte[] bytes = indexByteBuffer.array(); - - final HashMap audioIdFrameMap = new HashMap<>(); - AviTrack videoTrack = null; - //Video seek offsets - UnboundedIntArray videoSeekOffset = new UnboundedIntArray(); - for (int i=0;i= 16) { - final int id = indexByteBuffer.getInt(); - final AviTrack aviTrack = idTrackMap.get(id); + final int chunkId = indexByteBuffer.getInt(); + final AviTrack aviTrack = getAviTrack(chunkId); if (aviTrack == null) { - if (id != AviExtractor.REC_) { - Log.w(TAG, "Unknown Track Type: " + toString(id)); + if (chunkId != AviExtractor.REC_) { + Log.w(TAG, "Unknown Track Type: " + toString(chunkId)); } indexByteBuffer.position(indexByteBuffer.position() + 12); continue; @@ -374,56 +366,80 @@ public class AviExtractor implements Extractor { //int size = indexByteBuffer.getInt(); if (aviTrack.isVideo()) { if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { - keyFrameList.add(aviTrack.frame); + keyFrameList.add(chunkCounts[aviTrack.id]); } - if (aviTrack.frame % seekFrameRate == 0) { - - videoSeekOffset.add(offset); - for (Map.Entry entry : audioIdFrameMap.entrySet()) { - final int audioId = entry.getKey(); - final UnboundedIntArray videoFrameMap = entry.getValue(); - final AviTrack audioTrack = idTrackMap.get(audioId); - videoFrameMap.add(audioTrack.frame); + if (chunkCounts[aviTrack.id] % seekFrameRate == 0) { + seekOffsets[aviTrack.id].add(offset); + for (int i=0;i idFrameArray = new SparseArray<>(); - for (Map.Entry entry : audioIdFrameMap.entrySet()) { - entry.getValue().pack(); - idFrameArray.put(entry.getKey(), entry.getValue().getArray()); - final AviTrack aviTrack = idTrackMap.get(entry.getKey()); - //Sometimes this value is way off - long calcUsPerSample = (getDuration()/aviTrack.frame); - float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample; - if (deltaPercent >.01) { - aviTrack.usPerSample = getDuration()/aviTrack.frame; - Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); + for (int i=0;i.01) { + Log.i(TAG, "Updating stream " + i + " calcUsPerSample=" + calcUsPerSample + " reported=" + linearClock.usPerChunk); + linearClock.usPerChunk = calcUsPerSample; + } } } - final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.getArray(), - idFrameArray, moviOffset, getDuration()); + final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekOffsets, seekFrameRate, moviOffset, getDuration()); setSeekMap(seekMap); - resetFrames(); + } + + private static int getStreamId(int chunkId) { + final int upperChar = chunkId & 0xff; + if (Character.isDigit(upperChar)) { + final int lowerChar = (chunkId >> 8) & 0xff; + if (Character.isDigit(upperChar)) { + return (lowerChar & 0xf) + ((upperChar & 0xf) * 10); + } + } + return -1; + } + + @Nullable + private AviTrack getAviTrack(int chunkId) { + final int streamId = getStreamId(chunkId); + if (streamId >= 0) { + return aviTracks[streamId]; + } + return null; + } + + int checkAlign(final ExtractorInput input, PositionHolder seekPosition) { + final long position = input.getPosition(); + if ((position & 1) ==1) { + seekPosition.position = position +1; + return RESULT_SEEK; + } + return RESULT_CONTINUE; } int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { chunkHandler = null; + return checkAlign(input, seekPosition); } } else { ByteBuffer byteBuffer = allocate(8); @@ -437,24 +453,27 @@ public class AviExtractor implements Extractor { return RESULT_END_OF_INPUT; } input.readFully(bytes, 1, 7); - final int id = byteBuffer.getInt(); - final int size = byteBuffer.getInt(); - AviTrack sampleTrack = idTrackMap.get(id); - if (sampleTrack == null) { - if (id == ListBox.LIST) { - seekPosition.position = input.getPosition() + 4; - } else { - seekPosition.position = alignPosition(input.getPosition() + size); - if (id != JUNK) { - Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8) - + " size=" + size + " moviEnd=" + moviEnd); - } - } + final int chunkId = byteBuffer.getInt(); + if (chunkId == ListBox.LIST) { + seekPosition.position = input.getPosition() + 8; return RESULT_SEEK; + } + final int size = byteBuffer.getInt(); + if (chunkId == JUNK) { + seekPosition.position = alignPosition(input.getPosition() + size); + return RESULT_SEEK; + } + final AviTrack aviTrack = getAviTrack(chunkId); + if (aviTrack == null) { + seekPosition.position = alignPosition(input.getPosition() + size); + Log.w(TAG, "Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + + " size=" + size + " moviEnd=" + moviEnd); + return RESULT_SEEK; + } + if (aviTrack.newChunk(chunkId, size, input)) { + return checkAlign(input, seekPosition); } else { - if (!sampleTrack.newChunk(id, size, input)) { - chunkHandler = sampleTrack; - } + chunkHandler = aviTrack; } } return RESULT_CONTINUE; @@ -489,7 +508,6 @@ public class AviExtractor implements Extractor { state = STATE_READ_SAMPLES; return RESULT_SEEK; } - } return RESULT_CONTINUE; } @@ -499,24 +517,34 @@ public class AviExtractor implements Extractor { chunkHandler = null; if (position <= 0) { if (moviOffset != 0) { - resetFrames(); + resetClocks(); state = STATE_SEEK_START; } } else { if (aviSeekMap != null) { - aviSeekMap.setFrames(position, timeUs, idTrackMap); + aviSeekMap.setFrames(position, timeUs, aviTracks); } } } - void resetFrames() { - for (int i=0;i audioIdMap; + final long moviOffset; final long duration; - public AviSeekMap(AviTrack videoTrack, int seekIndexFactor, int[] videoFrameOffsetMap, - SparseArray audioIdMap, long moviOffset, long duration) { - this.videoTrack = videoTrack; + public AviSeekMap(AviTrack videoTrack, UnboundedIntArray[] seekOffsets, int seekIndexFactor, long moviOffset, long duration) { + videoUsPerChunk = videoTrack.getClock().usPerChunk; + videoStreamId = videoTrack.id; this.seekIndexFactor = seekIndexFactor; - this.videoFrameOffsetMap = videoFrameOffsetMap; - this.audioIdMap = audioIdMap; this.moviOffset = moviOffset; this.duration = duration; + this.seekOffsets = new int[seekOffsets.length][]; + for (int i=0;i= videoFrameOffsetMap.length) { - reqFrameIndex = videoFrameOffsetMap.length - 1; + if (reqFrameIndex >= seekOffsets[videoStreamId].length) { + reqFrameIndex = seekOffsets[videoStreamId].length - 1; } return reqFrameIndex; } @@ -53,23 +55,29 @@ public class AviSeekMap implements SeekMap { @Override public SeekPoints getSeekPoints(long timeUs) { final int seekFrameIndex = getSeekFrameIndex(timeUs); - int offset = videoFrameOffsetMap[seekFrameIndex]; - final long outUs = seekFrameIndex * seekIndexFactor * videoTrack.usPerSample; + int offset = seekOffsets[videoStreamId][seekFrameIndex]; + final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk; final long position = offset + moviOffset; Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); return new SeekPoints(new SeekPoint(outUs, position)); } - public void setFrames(final long position, final long timeUs, final SparseArray idTrackMap) { + public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) { final int seekFrameIndex = getSeekFrameIndex(timeUs); - videoTrack.seekFrame(seekFrameIndex * seekIndexFactor); - for (int i=0;i= 0; + return Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0; } //Hack: Exo needs at least one frame before it starts playback - return frame == 0; + //return clock.getIndex() == 0; + return false; + } + + public void setForceKeyFrame(boolean v) { + forceKeyFrame = v; } public void setKeyFrames(int[] keyFrames) { this.keyFrames = keyFrames; } - public long getUs() { - return getUs(getUsFrame()); - } - - public long getUs(final int myFrame) { - return myFrame * usPerSample; - } - public boolean isVideo() { return streamHeaderBox.isVideo(); } @@ -87,23 +99,10 @@ public class AviTrack { return streamHeaderBox.isAudio(); } - public void advance() { - frame++; - } - - /** - * Get the frame number used to calculate the timeUs - * @return - */ - int getUsFrame() { - return frame; - } - - void seekFrame(int frame) { - this.frame = frame; - } - public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { + if (chunkPeeker != null) { + chunkPeeker.peek(input, size); + } final int remaining = size - trackOutput.sampleData(input, size, false); if (remaining == 0) { done(size); @@ -127,8 +126,8 @@ public class AviTrack { void done(final int size) { trackOutput.sampleMetadata( - getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); + clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame()); - advance(); + clock.advance(); } } 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 new file mode 100644 index 0000000000..84eb49e459 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java @@ -0,0 +1,8 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.IOException; + +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/LinearClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java new file mode 100644 index 0000000000..b313e501e5 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java @@ -0,0 +1,27 @@ +package com.google.android.exoplayer2.extractor.avi; + +public class LinearClock { + long usPerChunk; + + int index; + + public LinearClock(long usPerChunk) { + this.usPerChunk = usPerChunk; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public void advance() { + index++; + } + + public long getUs() { + return index * usPerChunk; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java deleted file mode 100644 index d5a02962c8..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java +++ /dev/null @@ -1,81 +0,0 @@ -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; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; -import java.io.IOException; - -public class Mp4vAviTrack extends AviTrack { - private static final byte SEQUENCE_START_CODE = (byte)0xb0; - private static final int LAYER_START_CODE = 0x20; - private static final float[] ASPECT_RATIO = {0f, 1f, 12f/11f, 10f/11f, 16f/11f, 40f/33f}; - private static final int Extended_PAR = 0xf; - private final Format.Builder formatBuilder; - float pixelWidthHeightRatio = 1f; - - Mp4vAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput, - @NonNull Format.Builder formatBuilder) { - super(id, streamHeaderBox, trackOutput); - this.formatBuilder = formatBuilder; - } - - @VisibleForTesting - void processLayerStart(@NonNull final ParsableNalUnitBitArray in) { - in.skipBit(); // random_accessible_vol - in.skipBits(8); // video_object_type_indication - boolean is_object_layer_identifier = in.readBit(); - if (is_object_layer_identifier) { - in.skipBits(7); // video_object_layer_verid, video_object_layer_priority - } - int aspect_ratio_info = in.readBits(4); - final float aspectRatio; - if (aspect_ratio_info == Extended_PAR) { - float par_width = (float)in.readBits(8); - float par_height = (float)in.readBits(8); - aspectRatio = par_width / par_height; - } else { - aspectRatio = ASPECT_RATIO[aspect_ratio_info]; - } - if (aspectRatio != pixelWidthHeightRatio) { - trackOutput.format(formatBuilder.setPixelWidthHeightRatio(aspectRatio).build()); - pixelWidthHeightRatio = aspectRatio; - } - } - - @VisibleForTesting - @Nullable - static ParsableNalUnitBitArray findLayerStart(ExtractorInput input, final int peekSize) - throws IOException { - byte[] peek = new byte[peekSize]; - input.peekFully(peek, 0, peekSize); - for (int i = 4;i 0) { + if (buffer.length - pos < SEEK_PEEK_SIZE && remaining > 0) { + append(input, Math.min(SEEK_PEEK_SIZE, remaining)); + } + final int nalOffset = seekNal(); + if (nalOffset > 0) { + return nalOffset; + } + } + pos = buffer.length; + 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 void peek(ExtractorInput input, final int size) throws IOException { + buffer = new byte[peekSize]; + if (!input.peekFully(buffer, 0, peekSize, true)) { + return; + } + pos = 0; + int nalTypeOffset = getNalTypeOffset(); + if (nalTypeOffset < 0 || skip(buffer[nalTypeOffset])) { + input.resetPeekPosition(); + return; + } + remaining = size - peekSize; + processChunk(input, nalTypeOffset); + input.resetPeekPosition(); + } + +// @VisibleForTesting(otherwise = VisibleForTesting.NONE) +// void setBuffer(byte[] buffer) { +// this.buffer = buffer; +// } +} 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 new file mode 100644 index 0000000000..cc7017bc85 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -0,0 +1,62 @@ +package com.google.android.exoplayer2.extractor.avi; + +/** + * Properly calculates the frame time for H264 frames using PicCount + */ +public class PicCountClock extends LinearClock { + //The frame as a calculated from the picCount + private int picIndex; + private int lastPicCount; + //Largest picFrame, used when we hit an I frame + private int maxPicIndex =-1; + private int maxPicCount; + private int posHalf; + private int negHalf; + + public PicCountClock(long usPerFrame) { + super(usPerFrame); + } + + public void setMaxPicCount(int maxPicCount) { + this.maxPicCount = maxPicCount; + posHalf = maxPicCount / 2; //Not sure why pics are 2x + negHalf = -posHalf; + } + + /** + * Done on seek. May cause sync issues if frame picCount != 0 (I frames are always 0) + * @param index + */ + @Override + public void setIndex(int index) { + super.setIndex(index); + syncIndexes(); + } + + public void setPicCount(int picCount) { + int delta = picCount - lastPicCount; + if (delta < negHalf) { + delta += maxPicCount; + } else if (delta > posHalf) { + delta -= maxPicCount; + } + picIndex += delta / 2; + lastPicCount = picCount; + if (maxPicIndex < picIndex) { + maxPicIndex = picIndex; + } + } + + /** + * Handle key frame + */ + public void syncIndexes() { + lastPicCount = 0; + maxPicIndex = picIndex = getIndex(); + } + + @Override + public long getUs() { + return picIndex * usPerChunk; + } +} 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 703bb9aa24..8e4ce29d4a 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 @@ -36,6 +36,7 @@ public class StreamHeaderBox extends ResidentBox { STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); STREAM_MAP.put(XVID, mimeType); STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); + STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); } 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 new file mode 100644 index 0000000000..25afad0039 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -0,0 +1,101 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class AviExtractorTest { + @Test + public void init_givenFakeExtractorOutput() { + AviExtractor aviExtractor = new AviExtractor(); + FakeExtractorOutput output = new FakeExtractorOutput(); + aviExtractor.init(output); + + Assert.assertEquals(AviExtractor.STATE_READ_TRACKS, aviExtractor.state); + Assert.assertEquals(output, aviExtractor.output); + } + + + private boolean sniff(ByteBuffer byteBuffer) { + AviExtractor aviExtractor = new AviExtractor(); + FakeExtractorInput input = new FakeExtractorInput.Builder() + .setData(byteBuffer.array()).build(); + try { + return aviExtractor.sniff(input); + } catch (IOException e) { + Assert.fail(e.getMessage()); + return false; + } + } + + @Test + public void peek_givenTooFewByte() { + Assert.assertFalse(sniff(AviExtractor.allocate(AviExtractor.PEEK_BYTES - 1))); + } + + @Test + public void peek_givenAllZero() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiff() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_List() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + byteBuffer.putInt(ListBox.LIST); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_ListHdrl() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(64); + byteBuffer.putInt(ListBox.TYPE_HDRL); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_ListHdrlAvih() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(64); + byteBuffer.putInt(ListBox.TYPE_HDRL); + byteBuffer.putInt(AviHeaderBox.AVIH); + Assert.assertTrue(sniff(byteBuffer)); + } + + @Test + public void toString_givenKnownString() { + final int riff = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); + Assert.assertEquals("RIFF", AviExtractor.toString(riff)); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java new file mode 100644 index 0000000000..4775d93872 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java @@ -0,0 +1,46 @@ +package com.google.android.exoplayer2.extractor.avi; + +import java.nio.BufferOverflowException; + +public class BitBuffer { + private long work; + int bits; + + public void push(boolean b) { + grow(1); + if (b) { + work |= 1L; + } + } + + void grow(int bits) { + if (this.bits + bits > 64) { + throw new BufferOverflowException(); + } + this.bits += bits; + work <<= bits; + } + + public void push(int bits, int value) { + int mask = (1 << bits) - 1; + if ((value & mask) != value) { + throw new IllegalArgumentException("Expected only " + bits + " bits, got " + value); + } + grow(bits); + work |= (value & 0xffffffffL); + } + + public byte[] getBytes() { + //Byte align + grow(8 - bits % 8); + final int count = bits / 8; + final byte[] bytes = new byte[count]; + for (int i=count -1; i >= 0;i--) { + bytes[i] = (byte)(work & 0xff); + work >>=8; + } + work = 0L; + bits = 0; + return bytes; + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 2731878d90..246220ed0d 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -50,4 +50,12 @@ public class DataHelper { bytes = Arrays.copyOf(bytes, bytes.length + 1); return new StreamNameBox(StreamNameBox.STRN, bytes.length, ByteBuffer.wrap(bytes)); } + + public static ByteBuffer appendNal(final ByteBuffer byteBuffer, byte nalType) { + byteBuffer.put((byte)0); + byteBuffer.put((byte)0); + byteBuffer.put((byte) 1); + byteBuffer.put(nalType); + return byteBuffer; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java new file mode 100644 index 0000000000..0be4368b5c --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java @@ -0,0 +1,22 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.IOException; + +public class MockNalChunkPeeker extends NalChunkPeeker { + private boolean skip; + public MockNalChunkPeeker(int peakSize, boolean skip) { + super(peakSize); + this.skip = skip; + } + + @Override + void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException { + + } + + @Override + boolean skip(byte nalType) { + return skip; + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java deleted file mode 100644 index c14352f4aa..0000000000 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.google.android.exoplayer2.extractor.avi; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.FakeTrackOutput; -import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; -import java.io.IOException; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class Mp4vAviTrackTest { - - @Test - public void isSequenceStart_givenSequence() throws IOException { - final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); - Assert.assertTrue(Mp4vAviTrack.isSequenceStart(input)); - } - - @Test - public void findLayerStart_givenSequence() throws IOException { - final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); - final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input, - (int)input.getLength()); - //Offset 0x12 - Assert.assertEquals(8, bitArray.readBits(8)); - } - - @Test - public void findLayerStart_givenAllZeros() throws IOException { - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). - setData(new byte[128]).build(); - Assert.assertNull(Mp4vAviTrack.findLayerStart(fakeExtractorInput, 128)); - } - - @Test - public void pixelWidthHeightRatio_givenSequence() throws IOException { - final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); - final Format.Builder formatBuilder = new Format.Builder(); - final Mp4vAviTrack mp4vAviTrack = new Mp4vAviTrack(0, DataHelper.getVidsStreamHeader(), - fakeTrackOutput, formatBuilder); - final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); - mp4vAviTrack.newChunk(0, (int)input.getLength(), input); -// final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input, -// (int)input.getLength()); -// mp4vAviTrack.processLayerStart(bitArray); - Assert.assertEquals(mp4vAviTrack.pixelWidthHeightRatio, 1.2121212, 0.01); - } -} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java new file mode 100644 index 0000000000..baa6277ad1 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java @@ -0,0 +1,65 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class Mp4vChunkPeekerTest { + + private ByteBuffer makeSequence() { + return DataHelper.appendNal(AviExtractor.allocate(32),Mp4vChunkPeeker.SEQUENCE_START_CODE); + } + + @Test + public void peek_givenNoSequence() throws IOException { + ByteBuffer byteBuffer = makeSequence(); + final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); + final Format.Builder formatBuilder = new Format.Builder(); + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + .build(); + final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput); + mp4vChunkPeeker.peek(input, (int) input.getLength()); + Assert.assertEquals(1f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01); + } + + @Test + public void peek_givenAspectRatio() throws IOException { + final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); + final Format.Builder formatBuilder = new Format.Builder(); + final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput); + final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); + + mp4vChunkPeeker.peek(input, (int) input.getLength()); + Assert.assertEquals(1.2121212, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01); + } + + @Test + public void peek_givenCustomAspectRatio() throws IOException { + ByteBuffer byteBuffer = makeSequence(); + byteBuffer.putInt(0x5555); + DataHelper.appendNal(byteBuffer, (byte)Mp4vChunkPeeker.LAYER_START_CODE); + + BitBuffer bitBuffer = new BitBuffer(); + bitBuffer.push(false); //random_accessible_vol + bitBuffer.push(8, 8); //video_object_type_indication + bitBuffer.push(true); // is_object_layer_identifier + bitBuffer.push(7, 7); // video_object_layer_verid, video_object_layer_priority + bitBuffer.push(4, Mp4vChunkPeeker.Extended_PAR); + bitBuffer.push(8, 16); + bitBuffer.push(8, 9); + final byte bytes[] = bitBuffer.getBytes(); + byteBuffer.put(bytes); + + final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); + final Format.Builder formatBuilder = new Format.Builder(); + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + .build(); + final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput); + mp4vChunkPeeker.peek(input, (int) input.getLength()); + Assert.assertEquals(16f/9f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01); + } +} \ No newline at end of file diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java new file mode 100644 index 0000000000..2708015f4f --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java @@ -0,0 +1,55 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class NalChunkPeekerTest { + @Test + public void construct_givenTooSmallPeekSize() { + try { + new MockNalChunkPeeker(4, false); + Assert.fail(); + } catch (IllegalArgumentException e) { + //Intentionally blank + } + } + + @Test + public void peek_givenNoData() { + final FakeExtractorInput input = new FakeExtractorInput.Builder().build(); + final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false); + try { + peeker.peek(input, 10); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } + @Test + public void peek_givenNoNal() { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[10]).build(); + final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false); + try { + peeker.peek(input, 10); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } + @Test + public void peek_givenAlwaysSkip() { + final ByteBuffer byteBuffer = AviExtractor.allocate(10); + DataHelper.appendNal(byteBuffer, (byte)32); + + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build(); + final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, true); + try { + peeker.peek(input, 10); + Assert.assertEquals(0, input.getPeekPosition()); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } +}