diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java index 0a8b03ce1f..65bc631340 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java @@ -1,10 +1,11 @@ package com.google.android.exoplayer2.extractor.avi; import android.util.SparseArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; -public class AudioFormat { +public class AudioFormat implements IStreamFormat { public static final short WAVE_FORMAT_PCM = 1; static final short WAVE_FORMAT_AAC = 0xff; private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; @@ -60,5 +61,16 @@ public class AudioFormat { temp.get(data); return data; } + + @Override + public boolean isAllKeyFrames() { + return true; + } + + @Override + public @C.TrackType int getTrackType() { + return C.TRACK_TYPE_AUDIO; + } + //TODO: Deal with WAVEFORMATEXTENSIBLE } 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 index 67620785e5..bd2544bb97 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/AvcChunkPeeker.java @@ -1,5 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -37,6 +38,12 @@ public class AvcChunkPeeker extends NalChunkPeeker { return false; } + /** + * 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) { final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length); //slide_header() @@ -63,7 +70,8 @@ public class AvcChunkPeeker extends NalChunkPeeker { picCountClock.setIndex(picCountClock.getIndex()); } - private int readSps(ExtractorInput input, int nalTypeOffset) throws IOException { + @VisibleForTesting + int readSps(ExtractorInput input, int nalTypeOffset) throws IOException { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); 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 18fc767061..411b6f8d99 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 @@ -39,6 +39,21 @@ public class AviExtractor implements Extractor { return sb.toString(); } + static long alignPosition(long position) { + if ((position & 1) == 1) { + position++; + } + return position; + } + + static void alignInput(ExtractorInput input) throws IOException { + // This isn't documented anywhere, but most files are aligned to even bytes + // and can have gaps of zeros + if ((input.getPosition() & 1) == 1) { + input.skipFully(1); + } + } + static final String TAG = "AviExtractor"; @VisibleForTesting static final int PEEK_BYTES = 28; @@ -81,28 +96,14 @@ public class AviExtractor implements Extractor { //At the start of the movi tag private long moviOffset; private long moviEnd; - private AviSeekMap aviSeekMap; + @VisibleForTesting + AviSeekMap aviSeekMap; // private long indexOffset; //Usually chunkStart //If partial read private transient AviTrack chunkHandler; - static void alignInput(ExtractorInput input) throws IOException { - // This isn't documented anywhere, but most files are aligned to even bytes - // and can have gaps of zeros - if ((input.getPosition() & 1) == 1) { - input.skipFully(1); - } - } - - static long alignPosition(long position) { - if ((position & 1) == 1) { - position++; - } - return position; - } - /** * * @param input @@ -161,7 +162,20 @@ public class AviExtractor implements Extractor { return byteBuffer; } - private void setSeekMap(AviSeekMap aviSeekMap) { + @VisibleForTesting + 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; + } + + @VisibleForTesting + void setSeekMap(AviSeekMap aviSeekMap) { this.aviSeekMap = aviSeekMap; output.seekMap(aviSeekMap); } @@ -191,16 +205,17 @@ public class AviExtractor implements Extractor { this.output = output; } - private void parseStream(final ListBox streamList, int streamId) { + @VisibleForTesting + AviTrack 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; + return null; } if (streamFormat == null) { Log.w(TAG, "Missing Stream Format"); - return; + return null; } final Format.Builder builder = new Format.Builder(); builder.setId(streamId); @@ -212,31 +227,33 @@ public class AviExtractor implements Extractor { if (streamName != null) { builder.setLabel(streamName.getName()); } + final AviTrack aviTrack; if (streamHeader.isVideo()) { - final String mimeType = streamHeader.getMimeType(); + final VideoFormat videoFormat = streamFormat.getVideoFormat(); + final String mimeType = videoFormat.getMimeType(); if (mimeType == null) { Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); - return; + return null; } - 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)) { + if (MimeTypes.VIDEO_H264.equals(mimeType)) { final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample()); - aviTrack.setClock(avcChunkPeeker.getPicCountClock()); + aviTrack = new AviTrack(streamId, videoFormat, avcChunkPeeker.getPicCountClock(), trackOutput); aviTrack.setChunkPeeker(avcChunkPeeker); + } else { + aviTrack = new AviTrack(streamId, videoFormat, + new LinearClock(streamHeader.getUsPerSample()), trackOutput); + if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { + aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput)); + } } 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); @@ -257,8 +274,12 @@ public class AviExtractor implements Extractor { builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); } trackOutput.format(builder.build()); - aviTracks[streamId] = new AviTrack(streamId, streamHeader, trackOutput); + aviTrack = new AviTrack(streamId, audioFormat, new LinearClock(streamHeader.getUsPerSample()), + trackOutput); + }else { + aviTrack = null; } + return aviTrack; } private int readTracks(ExtractorInput input) throws IOException { @@ -278,7 +299,7 @@ public class AviExtractor implements Extractor { for (Box box : headerList.getChildren()) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { final ListBox streamList = (ListBox) box; - parseStream(streamList, streamId); + aviTracks[streamId] = parseStream(streamList, streamId); streamId++; } } @@ -343,7 +364,8 @@ public class AviExtractor implements Extractor { for (int i=0;i 0) { @@ -406,17 +428,6 @@ public class AviExtractor implements Extractor { setSeekMap(seekMap); } - 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); 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 edb6324a61..e228756848 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 @@ -3,7 +3,6 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.Log; public class AviSeekMap implements SeekMap { final long videoUsPerChunk; @@ -58,7 +57,7 @@ public class AviSeekMap implements SeekMap { 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); + //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); return new SeekPoints(new SeekPoint(outUs, position)); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index 61168935b7..cbad3b1df2 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -5,7 +5,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.Arrays; @@ -16,23 +15,22 @@ public class AviTrack { final int id; @NonNull - final StreamHeaderBox streamHeaderBox; + final LinearClock clock; - @NonNull - LinearClock clock; - - @Nullable - ChunkPeeker chunkPeeker; /** * True indicates all frames are key frames (e.g. Audio, MJPEG) */ - boolean allKeyFrames; + final boolean allKeyFrames; + final @C.TrackType int trackType; + + @NonNull + final TrackOutput trackOutput; boolean forceKeyFrame; - @NonNull - TrackOutput trackOutput; + @Nullable + ChunkPeeker chunkPeeker; /** * Key is frame number value is offset @@ -43,22 +41,19 @@ public class AviTrack { transient int chunkSize; transient int chunkRemaining; - AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) { + AviTrack(int id, @NonNull IStreamFormat streamFormat, @NonNull LinearClock clock, + @NonNull TrackOutput trackOutput) { this.id = id; + this.clock = clock; + this.allKeyFrames = streamFormat.isAllKeyFrames(); + this.trackType = streamFormat.getTrackType(); this.trackOutput = trackOutput; - this.streamHeaderBox = streamHeaderBox; - clock = new LinearClock(streamHeaderBox.getUsPerSample()); - this.allKeyFrames = streamHeaderBox.isAudio() || (MimeTypes.VIDEO_MJPEG.equals(streamHeaderBox.getMimeType())); } public LinearClock getClock() { return clock; } - public void setClock(LinearClock clock) { - this.clock = clock; - } - public void setChunkPeeker(ChunkPeeker chunkPeeker) { this.chunkPeeker = chunkPeeker; } @@ -78,8 +73,6 @@ public class AviTrack { if (keyFrames != null) { return Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0; } - //Hack: Exo needs at least one frame before it starts playback - //return clock.getIndex() == 0; return false; } @@ -92,11 +85,11 @@ public class AviTrack { } public boolean isVideo() { - return streamHeaderBox.isVideo(); + return trackType == C.TRACK_TYPE_VIDEO; } public boolean isAudio() { - return streamHeaderBox.isAudio(); + return trackType == C.TRACK_TYPE_AUDIO; } public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { @@ -114,7 +107,13 @@ public class AviTrack { } } - public boolean resume(ExtractorInput input) throws IOException { + /** + * Resume a partial read of a chunk + * @param input + * @return + * @throws IOException + */ + boolean resume(ExtractorInput input) throws IOException { chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false); if (chunkRemaining == 0) { done(chunkSize); @@ -124,6 +123,10 @@ public class AviTrack { } } + /** + * Done reading a chunk + * @param size + */ void done(final int size) { trackOutput.sampleMetadata( clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java new file mode 100644 index 0000000000..0211b197dc --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java @@ -0,0 +1,9 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.C; + +public interface IStreamFormat { + String getMimeType(); + boolean isAllKeyFrames(); + @C.TrackType int getTrackType(); +} 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 cc7017bc85..7f76f00296 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 @@ -4,6 +4,8 @@ package com.google.android.exoplayer2.extractor.avi; * Properly calculates the frame time for H264 frames using PicCount */ public class PicCountClock extends LinearClock { + //I believe this is 2 because there is a bottom pic order and a top pic order + private static final int STEP = 2; //The frame as a calculated from the picCount private int picIndex; private int lastPicCount; @@ -19,7 +21,7 @@ public class PicCountClock extends LinearClock { public void setMaxPicCount(int maxPicCount) { this.maxPicCount = maxPicCount; - posHalf = maxPicCount / 2; //Not sure why pics are 2x + posHalf = maxPicCount / STEP; negHalf = -posHalf; } @@ -40,7 +42,7 @@ public class PicCountClock extends LinearClock { } else if (delta > posHalf) { delta -= maxPicCount; } - picIndex += delta / 2; + picIndex += delta / STEP; lastPicCount = picCount; if (maxPicIndex < picIndex) { maxPicIndex = picIndex; 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 8e4ce29d4a..6cfbb61fb1 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 @@ -1,7 +1,5 @@ package com.google.android.exoplayer2.extractor.avi; -import android.util.SparseArray; -import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; /** @@ -16,31 +14,6 @@ public class StreamHeaderBox extends ResidentBox { //Videos Stream static final int VIDS = 'v' | ('i' << 8) | ('d' << 16) | ('s' << 24); - static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); - - private static final SparseArray STREAM_MAP = new SparseArray<>(); - - static { - //Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG - final String mimeType = MimeTypes.VIDEO_MP4V; - //final String mimeType = MimeTypes.VIDEO_H263; - - //I've never seen an Android devices that actually supports MP42 - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42"); - //Samsung seems to support the rare MP43. - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43"); - STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); - 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); - } - StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); } @@ -64,11 +37,6 @@ public class StreamHeaderBox extends ResidentBox { return getScale() * 1_000_000L / getRate(); } - public String getMimeType() { - return STREAM_MAP.get(getFourCC()); - } - - public int getSteamType() { return byteBuffer.getInt(0); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java index 55b1e67b0b..f849eed932 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java @@ -1,8 +1,38 @@ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; +import java.util.HashMap; + +public class VideoFormat implements IStreamFormat { + + static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); + + private static final HashMap STREAM_MAP = new HashMap<>(); + + static { + //Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG + final String mimeType = MimeTypes.VIDEO_MP4V; + //final String mimeType = MimeTypes.VIDEO_H263; + + //I've never seen an Android devices that actually supports MP42 + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42"); + //Samsung seems to support the rare MP43. + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43"); + STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); + 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); + STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); + } -public class VideoFormat { private final ByteBuffer byteBuffer; public VideoFormat(final ByteBuffer byteBuffer) { @@ -17,5 +47,23 @@ public class VideoFormat { public int getHeight() { return byteBuffer.getInt(8); } + // 12 - biPlanes + // 14 - biBitCount + public int getCompression() { + return byteBuffer.getInt(16); + } + public String getMimeType() { + return STREAM_MAP.get(getCompression()); + } + + @Override + public boolean isAllKeyFrames() { + return MimeTypes.VIDEO_MJPEG.equals(getMimeType()); + } + + @Override + public int getTrackType() { + return C.TRACK_TYPE_VIDEO; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java index 63cfd5c108..cc1860749c 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java @@ -13,7 +13,7 @@ public class AudioFormatTest { @Test public void getters_givenAacStreamFormat() throws IOException { - final StreamFormatBox streamFormatBox = DataHelper.getAudioStreamFormat(); + final StreamFormatBox streamFormatBox = DataHelper.getAacStreamFormat(); final AudioFormat audioFormat = streamFormatBox.getAudioFormat(); Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType()); Assert.assertEquals(2, audioFormat.getChannels()); @@ -21,5 +21,6 @@ public class AudioFormatTest { Assert.assertEquals(48000, audioFormat.getSamplesPerSecond()); Assert.assertEquals(0, audioFormat.getBitsPerSample()); //Not meaningful for AAC Assert.assertArrayEquals(CODEC_PRIVATE, audioFormat.getCodecData()); + Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType()); } } 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 new file mode 100644 index 0000000000..315e504d7d --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -0,0 +1,38 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class AviExtractorRoboTest { + + @Test + public void parseStream_givenH264StreamList() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = DataHelper.getVideoStreamList(); + aviExtractor.parseStream(streamList, 0); + FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); + Assert.assertEquals(MimeTypes.VIDEO_H264, trackOutput.lastFormat.sampleMimeType); + } + + @Test + public void parseStream_givenAacStreamList() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = DataHelper.getAacStreamList(); + aviExtractor.parseStream(streamList, 0); + FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); + Assert.assertEquals(MimeTypes.AUDIO_AAC, trackOutput.lastFormat.sampleMimeType); + } + +} 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 25afad0039..f9325cc1de 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 @@ -98,4 +98,54 @@ public class AviExtractorTest { final int riff = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); Assert.assertEquals("RIFF", AviExtractor.toString(riff)); } + + @Test + public void alignPosition_givenOddPosition() { + Assert.assertEquals(2, AviExtractor.alignPosition(1)); + } + + @Test + public void alignPosition_givenEvenPosition() { + Assert.assertEquals(2, AviExtractor.alignPosition(2)); + } + + @Test + public void alignInput_givenOddPosition() throws IOException { + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[16]).build(); + fakeExtractorInput.setPosition(1); + AviExtractor.alignInput(fakeExtractorInput); + Assert.assertEquals(2, fakeExtractorInput.getPosition()); + } + @Test + + public void alignInput_givenEvenPosition() throws IOException { + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[16]).build(); + fakeExtractorInput.setPosition(4); + AviExtractor.alignInput(fakeExtractorInput); + Assert.assertEquals(4, fakeExtractorInput.getPosition()); + } + + @Test + public void setSeekMap_givenStubbedSeekMap() throws IOException { + final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + aviExtractor.setSeekMap(aviSeekMap); + Assert.assertEquals(aviSeekMap, fakeExtractorOutput.seekMap); + Assert.assertEquals(aviSeekMap, aviExtractor.aviSeekMap); + } + + @Test + public void getStreamId_givenInvalidStreamId() { + Assert.assertEquals(-1, AviExtractor.getStreamId(AviExtractor.JUNK)); + } + + @Test + public void getStreamId_givenValidStreamId() { + Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24))); + } + } 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 246220ed0d..21c087fd29 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 @@ -1,11 +1,13 @@ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.Arrays; public class DataHelper { @@ -31,7 +33,14 @@ public class DataHelper { return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); } - public static StreamFormatBox getAudioStreamFormat() throws IOException { + public static StreamHeaderBox getAudioStreamHeader() throws IOException { + final byte[] buffer = getBytes("auds_stream_header.dump"); + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); + } + + public static StreamFormatBox getAacStreamFormat() throws IOException { final byte[] buffer = getBytes("aac_stream_format.dump"); final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); @@ -45,6 +54,26 @@ public class DataHelper { return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer); } + public static ListBox getVideoStreamList() throws IOException { + final StreamHeaderBox streamHeaderBox = getVidsStreamHeader(); + final StreamFormatBox streamFormatBox = getVideoStreamFormat(); + final ArrayList list = new ArrayList<>(2); + list.add(streamHeaderBox); + list.add(streamFormatBox); + return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()), + AviExtractor.STRL, list); + } + + public static ListBox getAacStreamList() throws IOException { + final StreamHeaderBox streamHeaderBox = getAudioStreamHeader(); + final StreamFormatBox streamFormatBox = getAacStreamFormat(); + final ArrayList list = new ArrayList<>(2); + list.add(streamHeaderBox); + list.add(streamFormatBox); + return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()), + AviExtractor.STRL, list); + } + public static StreamNameBox getStreamNameBox(final String name) { byte[] bytes = name.getBytes(); bytes = Arrays.copyOf(bytes, bytes.length + 1); @@ -58,4 +87,18 @@ public class DataHelper { byteBuffer.put(nalType); return byteBuffer; } + public static AviSeekMap getAviSeekMap() throws IOException { + + final FakeTrackOutput output = new FakeTrackOutput(false); + final AviTrack videoTrack = new AviTrack(0, + DataHelper.getVideoStreamFormat().getVideoFormat(), new LinearClock(100), output); + final UnboundedIntArray videoArray = new UnboundedIntArray(); + videoArray.add(0); + videoArray.add(1024); + final UnboundedIntArray audioArray = new UnboundedIntArray(); + audioArray.add(0); + audioArray.add(128); + return new AviSeekMap(videoTrack, + new UnboundedIntArray[]{videoArray, audioArray}, 24, 0L, 0L); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java new file mode 100644 index 0000000000..305f01440c --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java @@ -0,0 +1,18 @@ +package com.google.android.exoplayer2.extractor.avi; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Most of this is covered by the PicOrderClockTest + */ +public class LinearClockTest { + @Test + public void advance() { + final LinearClock linearClock = new LinearClock(100L); + linearClock.setIndex(2); + Assert.assertEquals(200, linearClock.getUs()); + linearClock.advance(); + Assert.assertEquals(300, linearClock.getUs()); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java new file mode 100644 index 0000000000..1c18a4ee5b --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java @@ -0,0 +1,45 @@ +package com.google.android.exoplayer2.extractor.avi; + +import org.junit.Assert; +import org.junit.Test; + +public class PicCountClockTest { + @Test + public void us_givenTwoStepsForward() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setMaxPicCount(16*2); + picCountClock.setPicCount(2*2); + Assert.assertEquals(2*100, picCountClock.getUs()); + } + + @Test + public void us_givenThreeStepsBackwards() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setMaxPicCount(16*2); + picCountClock.setPicCount(4*2); // 400ms + Assert.assertEquals(400, picCountClock.getUs()); + picCountClock.setPicCount(1*2); + Assert.assertEquals(1*100, picCountClock.getUs()); + } + + @Test + public void setIndex_given3Chunks() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setIndex(3); + Assert.assertEquals(3*100, picCountClock.getUs()); + } + + @Test + public void us_giveWrapBackwards() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setMaxPicCount(16*2); + //Need to walk up no faster than maxPicCount / 2 + picCountClock.setPicCount(7*2); + picCountClock.setPicCount(11*2); + picCountClock.setPicCount(15*2); + picCountClock.setPicCount(1*2); + Assert.assertEquals(17*100, picCountClock.getUs()); + picCountClock.setPicCount(14*2); + Assert.assertEquals(14*100, picCountClock.getUs()); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java index 03b732d1f1..f8e8436288 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java @@ -19,11 +19,10 @@ public class StreamHeaderBoxTest { Assert.assertTrue(streamHeaderBox.isVideo()); Assert.assertFalse(streamHeaderBox.isAudio()); Assert.assertEquals(StreamHeaderBox.VIDS, streamHeaderBox.getSteamType()); - Assert.assertEquals(StreamHeaderBox.XVID, streamHeaderBox.getFourCC()); + Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC()); Assert.assertEquals(0, streamHeaderBox.getInitialFrames()); Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1); Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample()); - Assert.assertEquals(MimeTypes.VIDEO_MP4V, streamHeaderBox.getMimeType()); Assert.assertEquals(11805L, streamHeaderBox.getLength()); Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize()); } diff --git a/testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump b/testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump new file mode 100644 index 0000000000..4224a479f6 Binary files /dev/null and b/testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump differ