From 1d85bf2456b11c68388c45736b0b535363f09b46 Mon Sep 17 00:00:00 2001 From: Dustin Date: Fri, 28 Jan 2022 12:47:43 -0700 Subject: [PATCH] Updated seek --- .../exoplayer2/extractor/avi/AudioFormat.java | 13 +- .../extractor/avi/AvcChunkPeeker.java | 9 +- .../extractor/avi/AviExtractor.java | 163 ++++++++++++------ .../exoplayer2/extractor/avi/AviSeekMap.java | 97 ++++++----- .../exoplayer2/extractor/avi/AviTrack.java | 86 +++++---- .../extractor/avi/IStreamFormat.java | 9 - .../exoplayer2/extractor/avi/LinearClock.java | 23 ++- .../extractor/avi/PicCountClock.java | 6 +- .../extractor/avi/StreamHeaderBox.java | 17 +- .../extractor/avi/UnboundedIntArray.java | 7 + .../exoplayer2/extractor/avi/VideoFormat.java | 13 +- .../extractor/avi/AviExtractorTest.java | 97 +++++++++++ .../extractor/avi/AviSeekMapTest.java | 54 ++++++ .../exoplayer2/extractor/avi/DataHelper.java | 67 ++++++- .../extractor/avi/LinearClockTest.java | 2 +- .../extractor/avi/PicCountClockTest.java | 8 +- .../extractor/avi/StreamHeaderBoxTest.java | 1 - .../extractor/avi/UnboundedIntArrayTest.java | 19 ++ 18 files changed, 495 insertions(+), 196 deletions(-) delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java 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 65bc631340..fb421b76e2 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,11 +1,10 @@ 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 implements IStreamFormat { +public class AudioFormat { public static final short WAVE_FORMAT_PCM = 1; static final short WAVE_FORMAT_AAC = 0xff; private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; @@ -62,15 +61,5 @@ public class AudioFormat implements IStreamFormat { 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 bd2544bb97..0fa888a266 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 @@ -8,6 +8,10 @@ import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; import java.io.IOException; +/** + * Corrects the time and PAR for H264 streams + * H264 is very rare in AVI due to the rise of mp4 + */ public class AvcChunkPeeker extends NalChunkPeeker { private static final int NAL_TYPE_MASK = 0x1f; private static final int NAL_TYPE_IRD = 5; @@ -22,11 +26,12 @@ public class AvcChunkPeeker extends NalChunkPeeker { private float pixelWidthHeightRatio = 1f; private NalUnitUtil.SpsData spsData; - public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, long usPerChunk) { + public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, long durationUs, + int length) { super(16); this.formatBuilder = formatBuilder; this.trackOutput = trackOutput; - picCountClock = new PicCountClock(usPerChunk); + picCountClock = new PicCountClock(durationUs, length); } public PicCountClock getPicCountClock() { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java index 29e262252d..614719112e 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 @@ -17,12 +17,15 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collections; +import java.util.HashMap; /** * Based on the official MicroSoft spec * https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference */ public class AviExtractor implements Extractor { + //Minimum time between keyframes in the SeekMap + static final long MIN_KEY_FRAME_RATE_US = 2_000_000L; static final long UINT_MASK = 0xffffffffL; static long getUInt(ByteBuffer byteBuffer) { @@ -69,7 +72,7 @@ public class AviExtractor implements Extractor { @VisibleForTesting static final int STATE_SEEK_START = 4; - private static final int AVIIF_KEYFRAME = 16; + static final int AVIIF_KEYFRAME = 16; static final int RIFF = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); @@ -92,6 +95,9 @@ public class AviExtractor implements Extractor { ExtractorOutput output; private AviHeaderBox aviHeader; private long durationUs = C.TIME_UNSET; + /** + * AviTracks by StreamId + */ private AviTrack[] aviTracks = new AviTrack[0]; //At the start of the movi tag private long moviOffset; @@ -210,13 +216,17 @@ public class AviExtractor implements Extractor { final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); if (streamHeader == null) { - Log.w(TAG, "Missing Stream Header"); + w("Missing Stream Header"); return null; } + //i(streamHeader.toString()); if (streamFormat == null) { - Log.w(TAG, "Missing Stream Format"); + w("Missing Stream Format"); return null; } + final long durationUs = streamHeader.getDurationUs(); + //Initial estimate + final int length = streamHeader.getLength(); final Format.Builder builder = new Format.Builder(); builder.setId(streamId); final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); @@ -242,18 +252,20 @@ public class AviExtractor implements Extractor { builder.setSampleMimeType(mimeType); if (MimeTypes.VIDEO_H264.equals(mimeType)) { - final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample()); - aviTrack = new AviTrack(streamId, videoFormat, avcChunkPeeker.getPicCountClock(), trackOutput); + final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, durationUs, + length); + aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, avcChunkPeeker.getPicCountClock(), + trackOutput); aviTrack.setChunkPeeker(avcChunkPeeker); } else { - aviTrack = new AviTrack(streamId, videoFormat, - new LinearClock(streamHeader.getUsPerSample()), trackOutput); + aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, + new LinearClock(durationUs, length), trackOutput); if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput)); } } trackOutput.format(builder.build()); - durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); + this.durationUs = durationUs; } else if (streamHeader.isAudio()) { final AudioFormat audioFormat = streamFormat.getAudioFormat(); final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); @@ -274,8 +286,9 @@ public class AviExtractor implements Extractor { builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); } trackOutput.format(builder.build()); - aviTrack = new AviTrack(streamId, audioFormat, new LinearClock(streamHeader.getUsPerSample()), - trackOutput); + aviTrack = new AviTrack(streamId, C.TRACK_TYPE_AUDIO, + new LinearClock(durationUs, length), trackOutput); + aviTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); }else { aviTrack = null; } @@ -343,6 +356,27 @@ public class AviExtractor implements Extractor { return null; } + void updateAudioTiming(final int[] keyFrameCounts, final long videoDuration) { + for (final AviTrack aviTrack : aviTracks) { + if (aviTrack != null && 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(); + //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"); + 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]); + } + } + } + } + /** * Reads the index and sets the keyFrames and creates the SeekMap * @param input @@ -353,86 +387,93 @@ public class AviExtractor implements Extractor { final AviTrack videoTrack = getVideoTrack(); if (videoTrack == null) { output.seekMap(new SeekMap.Unseekable(getDuration())); - Log.w(TAG, "No video track found"); + w("No video track found"); return; } + final int videoId = videoTrack.id; final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); final byte[] bytes = indexByteBuffer.array(); - final int[] chunkCounts = new int[aviTracks.length]; - final UnboundedIntArray[] seekOffsets = new UnboundedIntArray[aviTracks.length]; - for (int i=0;i tagMap = new HashMap<>(); while (remaining > 0) { final int toRead = Math.min(indexByteBuffer.remaining(), remaining); input.readFully(bytes, indexByteBuffer.position(), toRead); + indexByteBuffer.limit(indexByteBuffer.position() + toRead); remaining -= toRead; while (indexByteBuffer.remaining() >= 16) { final int chunkId = indexByteBuffer.getInt(); + Integer count = tagMap.get(chunkId); + if (count == null) { + count = 1; + } else { + count += 1; + } + tagMap.put(chunkId, count); final AviTrack aviTrack = getAviTrack(chunkId); if (aviTrack == null) { if (chunkId != AviExtractor.REC_) { - Log.w(TAG, "Unknown Track Type: " + toString(chunkId)); + w("Unknown Track Type: " + toString(chunkId)); } indexByteBuffer.position(indexByteBuffer.position() + 12); continue; } final int flags = indexByteBuffer.getInt(); final int offset = indexByteBuffer.getInt(); - indexByteBuffer.position(indexByteBuffer.position() + 4); - //int size = indexByteBuffer.getInt(); - if (aviTrack.isVideo()) { - if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { - keyFrameList.add(chunkCounts[aviTrack.id]); - } - if (chunkCounts[aviTrack.id] % seekFrameRate == 0) { - seekOffsets[aviTrack.id].add(offset); - for (int i=0;i= chunksPerKeyFrame) { + keyFrameOffsetsDiv2.add(offset / 2); + for (AviTrack seekTrack : aviTracks) { + if (seekTrack != null) { + seekIndexes[seekTrack.id].add(seekTrack.chunks); + } } } } + keyFrameCounts[aviTrack.id]++; } - chunkCounts[aviTrack.id]++; + aviTrack.chunks++; + aviTrack.size+=size; } indexByteBuffer.compact(); } - //Set the keys frames - if (!videoTrack.isAllKeyFrames()) { - final int[] keyFrames = keyFrameList.getArray(); - videoTrack.setKeyFrames(keyFrames); + if (videoTrack.chunks == keyFrameCounts[videoTrack.id]) { + videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); + } else { + videoTrack.setKeyFrames(seekIndexes[videoId].getArray()); } - //Correct the timings - durationUs = chunkCounts[videoTrack.id] * videoTrack.getClock().usPerChunk; + final AviSeekMap seekMap = new AviSeekMap(videoId, videoTrack.clock.durationUs, videoTrack.chunks, + keyFrameOffsetsDiv2.getArray(), seekIndexes, moviOffset); + + i("Video chunks=" + videoTrack.chunks + " us=" + seekMap.getDurationUs()); + + //Needs to be called after the duration is updated + updateAudioTiming(keyFrameCounts, durationUs); - 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, seekOffsets, seekFrameRate, moviOffset, getDuration()); setSeekMap(seekMap); } @Nullable private AviTrack getAviTrack(int chunkId) { - final int streamId = getStreamId(chunkId); - if (streamId >= 0 && streamId < aviTracks.length) { - return aviTracks[streamId]; + for (AviTrack aviTrack : aviTracks) { + if (aviTrack.handlesChunkId(chunkId)) { + return aviTrack; + } } return null; } @@ -525,6 +566,7 @@ public class AviExtractor implements Extractor { @Override public void seek(long position, long timeUs) { + //i("Seek pos=" + position +", us="+timeUs); chunkHandler = null; if (position <= 0) { if (moviOffset != 0) { @@ -551,6 +593,11 @@ public class AviExtractor implements Extractor { //Intentionally blank } + @VisibleForTesting + void setAviTracks(AviTrack[] aviTracks) { + this.aviTracks = aviTracks; + } + private static void w(String message) { try { Log.w(TAG, message); @@ -558,4 +605,12 @@ public class AviExtractor implements Extractor { //Catch not mocked for tests } } + + private static void i(String message) { + try { + Log.i(TAG, message); + } catch (RuntimeException e) { + //Catch not mocked for tests + } + } } 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 e228756848..e5539039eb 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 @@ -1,34 +1,32 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; +import java.util.Arrays; public class AviSeekMap implements SeekMap { + final int videoId; final long videoUsPerChunk; - final int videoStreamId; - /** - * Number of frames per index - * i.e. videoFrameOffsetMap[1] is frame 1 * seekIndexFactor - */ - final int seekIndexFactor; - //Seek offsets by streamId, for video, this is the actual offset, for audio, this is the chunkId - final int[][] seekOffsets; - //Holds a map of video frameIds to audioFrameIds for each audioId - - final long moviOffset; final long duration; + //These are ints / 2 + final int[] keyFrameOffsetsDiv2; + //Seek chunk indexes by streamId + final int[][] seekIndexes; + final long moviOffset; - public AviSeekMap(AviTrack videoTrack, UnboundedIntArray[] seekOffsets, int seekIndexFactor, long moviOffset, long duration) { - videoUsPerChunk = videoTrack.getClock().usPerChunk; - videoStreamId = videoTrack.id; - this.seekIndexFactor = seekIndexFactor; - this.moviOffset = moviOffset; - this.duration = duration; - this.seekOffsets = new int[seekOffsets.length][]; - for (int i=0;i= seekOffsets[videoStreamId].length) { - reqFrameIndex = seekOffsets[videoStreamId].length - 1; + return Arrays.binarySearch(seekIndexes[videoId], reqFrame); + } + + @VisibleForTesting + int getFirstSeekIndex(int index) { + int firstIndex = -index - 2; + if (firstIndex < 0) { + firstIndex = 0; } - return reqFrameIndex; + return firstIndex; + } + + private SeekPoint getSeekPoint(int index) { + long offset = keyFrameOffsetsDiv2[index] * 2L; + final long outUs = seekIndexes[videoId][index] * videoUsPerChunk; + final long position = offset + moviOffset; + return new SeekPoint(outUs, position); } @NonNull @Override public SeekPoints getSeekPoints(long timeUs) { - final int seekFrameIndex = getSeekFrameIndex(timeUs); - 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); + final int index = getSeekIndex(timeUs); + if (index >= 0) { + return new SeekPoints(getSeekPoint(index)); + } + final int firstSeekIndex = getFirstSeekIndex(index); + if (firstSeekIndex + 1 < keyFrameOffsetsDiv2.length) { + return new SeekPoints(getSeekPoint(firstSeekIndex), getSeekPoint(firstSeekIndex+1)); + } else { + return new SeekPoints(getSeekPoint(firstSeekIndex)); + } - return new SeekPoints(new SeekPoint(outUs, position)); + //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); } public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) { - final int seekFrameIndex = getSeekFrameIndex(timeUs); + final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - moviOffset) / 2)); + + if (index < 0) { + throw new IllegalArgumentException("Position: " + position); + } for (int i=0;i= 0; - } - return false; - } - - public void setForceKeyFrame(boolean v) { - forceKeyFrame = v; - } - - public void setKeyFrames(int[] keyFrames) { - this.keyFrames = keyFrames; + return keyFrames == ALL_KEY_FRAMES || Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0; } public boolean isVideo() { @@ -130,7 +143,8 @@ public class AviTrack { void done(final int size) { trackOutput.sampleMetadata( 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()); + final LinearClock clock = getClock(); +// Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); clock.advance(); } } 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 deleted file mode 100644 index 0211b197dc..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java +++ /dev/null @@ -1,9 +0,0 @@ -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/LinearClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java index b313e501e5..03fcdbd795 100644 --- 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 @@ -1,12 +1,22 @@ package com.google.android.exoplayer2.extractor.avi; public class LinearClock { - long usPerChunk; + long durationUs; + int length; int index; - public LinearClock(long usPerChunk) { - this.usPerChunk = usPerChunk; + public LinearClock(long durationUs, int length) { + this.durationUs = durationUs; + this.length = length; + } + + public void setDuration(long durationUs) { + this.durationUs = durationUs; + } + + public void setLength(int length) { + this.length = length; } public int getIndex() { @@ -22,6 +32,11 @@ public class LinearClock { } public long getUs() { - return index * usPerChunk; + return getUs(index); + } + + long getUs(int index) { + //Doing this the hard way lessens round errors + return durationUs * index / length; } } 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 7f76f00296..7e515f786f 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 @@ -15,8 +15,8 @@ public class PicCountClock extends LinearClock { private int posHalf; private int negHalf; - public PicCountClock(long usPerFrame) { - super(usPerFrame); + public PicCountClock(long durationUs, int length) { + super(durationUs, length); } public void setMaxPicCount(int maxPicCount) { @@ -59,6 +59,6 @@ public class PicCountClock extends LinearClock { @Override public long getUs() { - return picIndex * usPerChunk; + return getUs(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 6cfbb61fb1..fffc43f016 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 @@ -30,11 +30,8 @@ public class StreamHeaderBox extends ResidentBox { return getRate() / (float)getScale(); } - /** - * @return sample duration in us - */ - public long getUsPerSample() { - return getScale() * 1_000_000L / getRate(); + public long getDurationUs() { + return getScale() * getLength() * 1_000_000L / getRate(); } public int getSteamType() { @@ -59,12 +56,12 @@ public class StreamHeaderBox extends ResidentBox { public int getRate() { return byteBuffer.getInt(24); } - // 28 - dwStart + //28 - dwStart - doesn't seem to ever be set // public int getStart() { // return byteBuffer.getInt(28); // } - public long getLength() { - return byteBuffer.getInt(32) & AviExtractor.UINT_MASK; + public int getLength() { + return byteBuffer.getInt(32); } public int getSuggestedBufferSize() { @@ -75,4 +72,8 @@ public class StreamHeaderBox extends ResidentBox { // public int getSampleSize() { // return byteBuffer.getInt(44); // } + +// public String toString() { +// return "scale=" + getScale() + " rate=" + getRate() + " length=" + getLength() + " us=" + getDurationUs(); +// } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java index 6efe3d74a5..61455d19b6 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java @@ -29,6 +29,13 @@ public class UnboundedIntArray { array[size++] = v; } + public int get(final int index) { + if (index >= size) { + throw new ArrayIndexOutOfBoundsException(index + ">=" + size); + } + return array[index]; + } + public int getSize() { return size; } 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 f849eed932..af25396002 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,11 +1,10 @@ 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 { +public class VideoFormat { static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); @@ -56,14 +55,4 @@ public class VideoFormat implements IStreamFormat { 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/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index f9325cc1de..f969979b8a 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 @@ -1,5 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import java.io.IOException; @@ -148,4 +149,100 @@ public class AviExtractorTest { Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24))); } + private void assertIdx1(AviSeekMap aviSeekMap, AviTrack videoTrack, int keyFrames, int keyFrameRate) { + Assert.assertEquals(keyFrames, videoTrack.keyFrames.length); + + final int framesPerKeyFrame = 24 * 3; + //This indirectly verifies the number of video chunks + Assert.assertEquals(9 * DataHelper.FPS, videoTrack.chunks); + + Assert.assertEquals(2 * framesPerKeyFrame, videoTrack.keyFrames[2]); + + Assert.assertEquals(2 * keyFrameRate * DataHelper.AUDIO_PER_VIDEO, + aviSeekMap.seekIndexes[DataHelper.AUDIO_ID][2]); + Assert.assertEquals(4L + 2 * keyFrameRate * DataHelper.VIDEO_SIZE + + 2 * keyFrameRate * DataHelper.AUDIO_SIZE * DataHelper.AUDIO_PER_VIDEO, + aviSeekMap.keyFrameOffsetsDiv2[2] * 2L); + + } + + @Test + public void readIdx1_given9secsAv() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final int secs = 9; + 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 FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + final AviSeekMap aviSeekMap = aviExtractor.aviSeekMap; + assertIdx1(aviSeekMap, videoTrack, keyFrames, keyFrameRate); + } + @Test + public void readIdx1_givenNoVideo() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + 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 FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + Assert.assertTrue(fakeExtractorOutput.seekMap instanceof SeekMap.Unseekable); + } + + @Test + public void readIdx1_givenJunkInIndex() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final int secs = 9; + final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds + final int keyFrames = secs * DataHelper.FPS / keyFrameRate; + final ByteBuffer idx1 = DataHelper.getIndex(9, keyFrameRate); + final ByteBuffer junk = AviExtractor.allocate(idx1.capacity() + 16); + junk.putInt(AviExtractor.JUNK); + junk.putInt(0); + junk.putInt(0); + 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 FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(junk.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + + assertIdx1(aviExtractor.aviSeekMap, videoTrack, keyFrames, keyFrameRate); + } + + @Test + public void readIdx1_givenAllKeyFrames() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + 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 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); + } } 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 new file mode 100644 index 0000000000..a20660edd3 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java @@ -0,0 +1,54 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.extractor.SeekMap; +import org.junit.Assert; +import org.junit.Test; + +public class AviSeekMapTest { + + @Test + public void setFrames_givenExactSeekPointMatch() { + final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); + final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.moviOffset; + final int secs = 4; + final AviTrack[] aviTracks = new AviTrack[]{DataHelper.getVideoAviTrack(secs), + DataHelper.getAudioAviTrack(secs)}; + + aviSeekMap.setFrames(position, 1_000_000L, aviTracks); + for (int i=0;i