Commit 1d85bf24 by Dustin

Updated seek

parent c41dc236
Showing with 386 additions and 142 deletions
package com.google.android.exoplayer2.extractor.avi; package com.google.android.exoplayer2.extractor.avi;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class AudioFormat implements IStreamFormat { public class AudioFormat {
public static final short WAVE_FORMAT_PCM = 1; public static final short WAVE_FORMAT_PCM = 1;
static final short WAVE_FORMAT_AAC = 0xff; static final short WAVE_FORMAT_AAC = 0xff;
private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55;
...@@ -62,15 +61,5 @@ public class AudioFormat implements IStreamFormat { ...@@ -62,15 +61,5 @@ public class AudioFormat implements IStreamFormat {
return data; return data;
} }
@Override
public boolean isAllKeyFrames() {
return true;
}
@Override
public @C.TrackType int getTrackType() {
return C.TRACK_TYPE_AUDIO;
}
//TODO: Deal with WAVEFORMATEXTENSIBLE //TODO: Deal with WAVEFORMATEXTENSIBLE
} }
...@@ -8,6 +8,10 @@ import com.google.android.exoplayer2.util.NalUnitUtil; ...@@ -8,6 +8,10 @@ import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
import java.io.IOException; 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 { public class AvcChunkPeeker extends NalChunkPeeker {
private static final int NAL_TYPE_MASK = 0x1f; private static final int NAL_TYPE_MASK = 0x1f;
private static final int NAL_TYPE_IRD = 5; private static final int NAL_TYPE_IRD = 5;
...@@ -22,11 +26,12 @@ public class AvcChunkPeeker extends NalChunkPeeker { ...@@ -22,11 +26,12 @@ public class AvcChunkPeeker extends NalChunkPeeker {
private float pixelWidthHeightRatio = 1f; private float pixelWidthHeightRatio = 1f;
private NalUnitUtil.SpsData spsData; 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); super(16);
this.formatBuilder = formatBuilder; this.formatBuilder = formatBuilder;
this.trackOutput = trackOutput; this.trackOutput = trackOutput;
picCountClock = new PicCountClock(usPerChunk); picCountClock = new PicCountClock(durationUs, length);
} }
public PicCountClock getPicCountClock() { public PicCountClock getPicCountClock() {
......
package com.google.android.exoplayer2.extractor.avi; package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.SeekPoint;
import java.util.Arrays;
public class AviSeekMap implements SeekMap { public class AviSeekMap implements SeekMap {
final int videoId;
final long videoUsPerChunk; 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; 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) { public AviSeekMap(int videoId, long usDuration, int videoChunks, int[] keyFrameOffsetsDiv2,
videoUsPerChunk = videoTrack.getClock().usPerChunk; UnboundedIntArray[] seekIndexes, long moviOffset) {
videoStreamId = videoTrack.id; this.videoId = videoId;
this.seekIndexFactor = seekIndexFactor; this.videoUsPerChunk = usDuration / videoChunks;
this.moviOffset = moviOffset; this.duration = usDuration;
this.duration = duration; this.keyFrameOffsetsDiv2 = keyFrameOffsetsDiv2;
this.seekOffsets = new int[seekOffsets.length][]; this.seekIndexes = new int[seekIndexes.length][];
for (int i=0;i<seekOffsets.length;i++) { for (int i=0;i<seekIndexes.length;i++) {
this.seekOffsets[i] = seekOffsets[i].getArray(); this.seekIndexes[i] = seekIndexes[i].getArray();
} }
this.moviOffset = moviOffset;
} }
@Override @Override
...@@ -41,42 +39,55 @@ public class AviSeekMap implements SeekMap { ...@@ -41,42 +39,55 @@ public class AviSeekMap implements SeekMap {
return duration; return duration;
} }
private int getSeekFrameIndex(long timeUs) { private int getSeekIndex(long timeUs) {
final int reqFrame = (int)(timeUs / videoUsPerChunk); final int reqFrame = (int)(timeUs / videoUsPerChunk);
int reqFrameIndex = reqFrame / seekIndexFactor; return Arrays.binarySearch(seekIndexes[videoId], reqFrame);
if (reqFrameIndex >= seekOffsets[videoStreamId].length) { }
reqFrameIndex = seekOffsets[videoStreamId].length - 1;
@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 @NonNull
@Override @Override
public SeekPoints getSeekPoints(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
final int seekFrameIndex = getSeekFrameIndex(timeUs); final int index = getSeekIndex(timeUs);
int offset = seekOffsets[videoStreamId][seekFrameIndex]; if (index >= 0) {
final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk; return new SeekPoints(getSeekPoint(index));
final long position = offset + moviOffset; }
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); 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) { 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<aviTracks.length;i++) { for (int i=0;i<aviTracks.length;i++) {
final AviTrack aviTrack = aviTracks[i]; final AviTrack aviTrack = aviTracks[i];
if (aviTrack != null) { final LinearClock clock = aviTrack.getClock();
final LinearClock clock = aviTrack.getClock(); clock.setIndex(seekIndexes[i][index]);
if (aviTrack.isVideo()) { // Log.d(AviExtractor.TAG, "Frame: " + (aviTrack.isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " frame=" + clock.getIndex() + " key=" + aviTrack.isKeyFrame());
//TODO: Although this works, it leads to partial frames being painted
aviTrack.setForceKeyFrame(true);
clock.setIndex(seekFrameIndex * seekIndexFactor);
} else {
final int offset = seekOffsets[i][seekFrameIndex];
clock.setIndex(offset);
}
}
} }
} }
} }
...@@ -5,6 +5,7 @@ import androidx.annotation.Nullable; ...@@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Log;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -12,42 +13,68 @@ import java.util.Arrays; ...@@ -12,42 +13,68 @@ import java.util.Arrays;
* Collection of info about a track * Collection of info about a track
*/ */
public class AviTrack { public class AviTrack {
public static final int[] ALL_KEY_FRAMES = new int[0];
final int id; final int id;
final @C.TrackType int trackType;
@NonNull @NonNull
final LinearClock clock; final LinearClock clock;
/**
* True indicates all frames are key frames (e.g. Audio, MJPEG)
*/
final boolean allKeyFrames;
final @C.TrackType int trackType;
@NonNull @NonNull
final TrackOutput trackOutput; final TrackOutput trackOutput;
boolean forceKeyFrame; final int chunkId;
final int chunkIdAlt;
@Nullable @Nullable
ChunkPeeker chunkPeeker; ChunkPeeker chunkPeeker;
int chunks;
int size;
/** /**
* Key is frame number value is offset * Ordered list of key frame chunk indexes
*/ */
@Nullable int[] keyFrames = new int[0];
int[] keyFrames;
transient int chunkSize; transient int chunkSize;
transient int chunkRemaining; transient int chunkRemaining;
AviTrack(int id, @NonNull IStreamFormat streamFormat, @NonNull LinearClock clock, private static int getChunkIdLower(int id) {
int tens = id / 10;
int ones = id % 10;
return ('0' + tens) | (('0' + ones) << 8);
}
public static int getVideoChunkId(int id) {
return getChunkIdLower(id) | ('d' << 16) | ('c' << 24);
}
public static int getAudioChunkId(int id) {
return getChunkIdLower(id) | ('w' << 16) | ('b' << 24);
}
AviTrack(int id, final @C.TrackType int trackType, @NonNull LinearClock clock,
@NonNull TrackOutput trackOutput) { @NonNull TrackOutput trackOutput) {
this.id = id; this.id = id;
this.clock = clock; this.clock = clock;
this.allKeyFrames = streamFormat.isAllKeyFrames(); this.trackType = trackType;
this.trackType = streamFormat.getTrackType();
this.trackOutput = trackOutput; this.trackOutput = trackOutput;
if (isVideo()) {
chunkId = getVideoChunkId(id);
chunkIdAlt = getChunkIdLower(id) | ('d' << 16) | ('b' << 24);
} else if (isAudio()) {
chunkId = getAudioChunkId(id);
chunkIdAlt = 0xffff;
} else {
throw new IllegalArgumentException("Unknown Track Type: " + trackType);
}
}
public boolean handlesChunkId(int chunkId) {
return this.chunkId == chunkId || chunkIdAlt == chunkId;
} }
public LinearClock getClock() { public LinearClock getClock() {
...@@ -58,30 +85,16 @@ public class AviTrack { ...@@ -58,30 +85,16 @@ public class AviTrack {
this.chunkPeeker = chunkPeeker; this.chunkPeeker = chunkPeeker;
} }
public boolean isAllKeyFrames() { /**
return allKeyFrames; *
* @param keyFrames null means all key frames
*/
void setKeyFrames(@NonNull final int[] keyFrames) {
this.keyFrames = keyFrames;
} }
public boolean isKeyFrame() { public boolean isKeyFrame() {
if (allKeyFrames) { return keyFrames == ALL_KEY_FRAMES || Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0;
return true;
}
if (forceKeyFrame) {
forceKeyFrame = false;
return true;
}
if (keyFrames != null) {
return Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0;
}
return false;
}
public void setForceKeyFrame(boolean v) {
forceKeyFrame = v;
}
public void setKeyFrames(int[] keyFrames) {
this.keyFrames = keyFrames;
} }
public boolean isVideo() { public boolean isVideo() {
...@@ -130,7 +143,8 @@ public class AviTrack { ...@@ -130,7 +143,8 @@ public class AviTrack {
void done(final int size) { void done(final int size) {
trackOutput.sampleMetadata( trackOutput.sampleMetadata(
clock.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()); final LinearClock clock = getClock();
// Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame());
clock.advance(); clock.advance();
} }
} }
package com.google.android.exoplayer2.extractor.avi;
import com.google.android.exoplayer2.C;
public interface IStreamFormat {
String getMimeType();
boolean isAllKeyFrames();
@C.TrackType int getTrackType();
}
package com.google.android.exoplayer2.extractor.avi; package com.google.android.exoplayer2.extractor.avi;
public class LinearClock { public class LinearClock {
long usPerChunk; long durationUs;
int length;
int index; int index;
public LinearClock(long usPerChunk) { public LinearClock(long durationUs, int length) {
this.usPerChunk = usPerChunk; 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() { public int getIndex() {
...@@ -22,6 +32,11 @@ public class LinearClock { ...@@ -22,6 +32,11 @@ public class LinearClock {
} }
public long getUs() { 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;
} }
} }
...@@ -15,8 +15,8 @@ public class PicCountClock extends LinearClock { ...@@ -15,8 +15,8 @@ public class PicCountClock extends LinearClock {
private int posHalf; private int posHalf;
private int negHalf; private int negHalf;
public PicCountClock(long usPerFrame) { public PicCountClock(long durationUs, int length) {
super(usPerFrame); super(durationUs, length);
} }
public void setMaxPicCount(int maxPicCount) { public void setMaxPicCount(int maxPicCount) {
...@@ -59,6 +59,6 @@ public class PicCountClock extends LinearClock { ...@@ -59,6 +59,6 @@ public class PicCountClock extends LinearClock {
@Override @Override
public long getUs() { public long getUs() {
return picIndex * usPerChunk; return getUs(picIndex);
} }
} }
...@@ -30,11 +30,8 @@ public class StreamHeaderBox extends ResidentBox { ...@@ -30,11 +30,8 @@ public class StreamHeaderBox extends ResidentBox {
return getRate() / (float)getScale(); return getRate() / (float)getScale();
} }
/** public long getDurationUs() {
* @return sample duration in us return getScale() * getLength() * 1_000_000L / getRate();
*/
public long getUsPerSample() {
return getScale() * 1_000_000L / getRate();
} }
public int getSteamType() { public int getSteamType() {
...@@ -59,12 +56,12 @@ public class StreamHeaderBox extends ResidentBox { ...@@ -59,12 +56,12 @@ public class StreamHeaderBox extends ResidentBox {
public int getRate() { public int getRate() {
return byteBuffer.getInt(24); return byteBuffer.getInt(24);
} }
// 28 - dwStart //28 - dwStart - doesn't seem to ever be set
// public int getStart() { // public int getStart() {
// return byteBuffer.getInt(28); // return byteBuffer.getInt(28);
// } // }
public long getLength() { public int getLength() {
return byteBuffer.getInt(32) & AviExtractor.UINT_MASK; return byteBuffer.getInt(32);
} }
public int getSuggestedBufferSize() { public int getSuggestedBufferSize() {
...@@ -75,4 +72,8 @@ public class StreamHeaderBox extends ResidentBox { ...@@ -75,4 +72,8 @@ public class StreamHeaderBox extends ResidentBox {
// public int getSampleSize() { // public int getSampleSize() {
// return byteBuffer.getInt(44); // return byteBuffer.getInt(44);
// } // }
// public String toString() {
// return "scale=" + getScale() + " rate=" + getRate() + " length=" + getLength() + " us=" + getDurationUs();
// }
} }
...@@ -29,6 +29,13 @@ public class UnboundedIntArray { ...@@ -29,6 +29,13 @@ public class UnboundedIntArray {
array[size++] = v; array[size++] = v;
} }
public int get(final int index) {
if (index >= size) {
throw new ArrayIndexOutOfBoundsException(index + ">=" + size);
}
return array[index];
}
public int getSize() { public int getSize() {
return size; return size;
} }
......
package com.google.android.exoplayer2.extractor.avi; package com.google.android.exoplayer2.extractor.avi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.HashMap; import java.util.HashMap;
public class VideoFormat implements IStreamFormat { public class VideoFormat {
static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24);
...@@ -56,14 +55,4 @@ public class VideoFormat implements IStreamFormat { ...@@ -56,14 +55,4 @@ public class VideoFormat implements IStreamFormat {
public String getMimeType() { public String getMimeType() {
return STREAM_MAP.get(getCompression()); return STREAM_MAP.get(getCompression());
} }
@Override
public boolean isAllKeyFrames() {
return MimeTypes.VIDEO_MJPEG.equals(getMimeType());
}
@Override
public int getTrackType() {
return C.TRACK_TYPE_VIDEO;
}
} }
package com.google.android.exoplayer2.extractor.avi; 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.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import java.io.IOException; import java.io.IOException;
...@@ -148,4 +149,100 @@ public class AviExtractorTest { ...@@ -148,4 +149,100 @@ public class AviExtractorTest {
Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24))); 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);
}
} }
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<aviTracks.length;i++) {
Assert.assertEquals(aviSeekMap.seekIndexes[i][1], aviTracks[i].getClock().getIndex());
}
}
@Test
public void setFrames_givenBadPosition() {
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
final AviTrack[] aviTracks = new AviTrack[2];
try {
aviSeekMap.setFrames(1L, 1_000_000L, aviTracks);
Assert.fail();
} catch (IllegalArgumentException e) {
//Intentionally blank
}
}
@Test
public void getSeekPoints_givenNonKeyFrameUs() {
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
//Time before the 1st keyFrame
final long us = aviSeekMap.seekIndexes[0][1] * aviSeekMap.videoUsPerChunk - 100L;
final SeekMap.SeekPoints seekPoints = aviSeekMap.getSeekPoints(us);
Assert.assertEquals(aviSeekMap.seekIndexes[0][0] * aviSeekMap.videoUsPerChunk,
seekPoints.first.timeUs);
Assert.assertEquals(aviSeekMap.seekIndexes[0][1] * aviSeekMap.videoUsPerChunk,
seekPoints.second.timeUs);
}
@Test
public void getFirstSeekIndex_atZeroIndex() {
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
Assert.assertEquals(0, aviSeekMap.getFirstSeekIndex(-1));
}
}
package com.google.android.exoplayer2.extractor.avi; package com.google.android.exoplayer2.extractor.avi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import java.io.File; import java.io.File;
...@@ -11,6 +12,14 @@ import java.util.ArrayList; ...@@ -11,6 +12,14 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
public class DataHelper { public class DataHelper {
static final int FPS = 24;
static final long VIDEO_US = 1_000_000L / FPS;
static final int AUDIO_PER_VIDEO = 4;
static final int VIDEO_SIZE = 4096;
static final int AUDIO_SIZE = 256;
static final int AUDIO_ID = 1;
private static final long AUDIO_US = VIDEO_US / AUDIO_PER_VIDEO;
//Base path "\ExoPlayer\library\extractor\." //Base path "\ExoPlayer\library\extractor\."
private static final File RELATIVE_PATH = new File("../../testdata/src/test/assets/extractordumps/avi/"); private static final File RELATIVE_PATH = new File("../../testdata/src/test/assets/extractordumps/avi/");
public static FakeExtractorInput getInput(final String fileName) throws IOException { public static FakeExtractorInput getInput(final String fileName) throws IOException {
...@@ -87,18 +96,62 @@ public class DataHelper { ...@@ -87,18 +96,62 @@ public class DataHelper {
byteBuffer.put(nalType); byteBuffer.put(nalType);
return byteBuffer; return byteBuffer;
} }
public static AviSeekMap getAviSeekMap() throws IOException {
final FakeTrackOutput output = new FakeTrackOutput(false); public static AviTrack getVideoAviTrack(int sec) {
final AviTrack videoTrack = new AviTrack(0, final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
DataHelper.getVideoStreamFormat().getVideoFormat(), new LinearClock(100), output); return new AviTrack(0, C.TRACK_TYPE_VIDEO,
new LinearClock(sec * 1_000_000L, sec * FPS),
fakeTrackOutput);
}
public static AviTrack getAudioAviTrack(int sec) {
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
return new AviTrack(AUDIO_ID, C.TRACK_TYPE_AUDIO,
new LinearClock(sec * 1_000_000L, sec * FPS * AUDIO_PER_VIDEO),
fakeTrackOutput);
}
public static AviSeekMap getAviSeekMap() {
final int[] keyFrameOffsetsDiv2= {4, 1024};
final UnboundedIntArray videoArray = new UnboundedIntArray(); final UnboundedIntArray videoArray = new UnboundedIntArray();
videoArray.add(0); videoArray.add(0);
videoArray.add(1024); videoArray.add(4);
final UnboundedIntArray audioArray = new UnboundedIntArray(); final UnboundedIntArray audioArray = new UnboundedIntArray();
audioArray.add(0); audioArray.add(0);
audioArray.add(128); audioArray.add(128);
return new AviSeekMap(videoTrack, return new AviSeekMap(0, 100L, 8, keyFrameOffsetsDiv2,
new UnboundedIntArray[]{videoArray, audioArray}, 24, 0L, 0L); new UnboundedIntArray[]{videoArray, audioArray}, 4096);
}
private static void putIndex(final ByteBuffer byteBuffer, int chunkId, int flags, int offset,
int size) {
byteBuffer.putInt(chunkId);
byteBuffer.putInt(flags);
byteBuffer.putInt(offset);
byteBuffer.putInt(size);
}
/**
*
* @param secs Number of seconds
* @param keyFrameRate Key frame rate 1= every frame, 2=every other, ...
*/
public static ByteBuffer getIndex(final int secs, final int keyFrameRate) {
final int videoFrames = secs * FPS;
final int videoChunkId = AviTrack.getVideoChunkId(0);
final int audioChunkId = AviTrack.getAudioChunkId(1);
int offset = 4;
final ByteBuffer byteBuffer = AviExtractor.allocate((videoFrames + videoFrames*AUDIO_PER_VIDEO) * 16);
for (int v=0;v<videoFrames;v++) {
putIndex(byteBuffer, videoChunkId, (v % keyFrameRate == 0) ? AviExtractor.AVIIF_KEYFRAME : 0,
offset, VIDEO_SIZE);
offset += VIDEO_SIZE;
for (int a=0;a<AUDIO_PER_VIDEO;a++) {
putIndex(byteBuffer, audioChunkId,AviExtractor.AVIIF_KEYFRAME, offset, AUDIO_SIZE);
offset += AUDIO_SIZE;
}
}
return byteBuffer;
} }
} }
...@@ -9,7 +9,7 @@ import org.junit.Test; ...@@ -9,7 +9,7 @@ import org.junit.Test;
public class LinearClockTest { public class LinearClockTest {
@Test @Test
public void advance() { public void advance() {
final LinearClock linearClock = new LinearClock(100L); final LinearClock linearClock = new LinearClock(1_000L, 10);
linearClock.setIndex(2); linearClock.setIndex(2);
Assert.assertEquals(200, linearClock.getUs()); Assert.assertEquals(200, linearClock.getUs());
linearClock.advance(); linearClock.advance();
......
...@@ -6,7 +6,7 @@ import org.junit.Test; ...@@ -6,7 +6,7 @@ import org.junit.Test;
public class PicCountClockTest { public class PicCountClockTest {
@Test @Test
public void us_givenTwoStepsForward() { public void us_givenTwoStepsForward() {
final PicCountClock picCountClock = new PicCountClock(100); final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setMaxPicCount(16*2); picCountClock.setMaxPicCount(16*2);
picCountClock.setPicCount(2*2); picCountClock.setPicCount(2*2);
Assert.assertEquals(2*100, picCountClock.getUs()); Assert.assertEquals(2*100, picCountClock.getUs());
...@@ -14,7 +14,7 @@ public class PicCountClockTest { ...@@ -14,7 +14,7 @@ public class PicCountClockTest {
@Test @Test
public void us_givenThreeStepsBackwards() { public void us_givenThreeStepsBackwards() {
final PicCountClock picCountClock = new PicCountClock(100); final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setMaxPicCount(16*2); picCountClock.setMaxPicCount(16*2);
picCountClock.setPicCount(4*2); // 400ms picCountClock.setPicCount(4*2); // 400ms
Assert.assertEquals(400, picCountClock.getUs()); Assert.assertEquals(400, picCountClock.getUs());
...@@ -24,14 +24,14 @@ public class PicCountClockTest { ...@@ -24,14 +24,14 @@ public class PicCountClockTest {
@Test @Test
public void setIndex_given3Chunks() { public void setIndex_given3Chunks() {
final PicCountClock picCountClock = new PicCountClock(100); final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setIndex(3); picCountClock.setIndex(3);
Assert.assertEquals(3*100, picCountClock.getUs()); Assert.assertEquals(3*100, picCountClock.getUs());
} }
@Test @Test
public void us_giveWrapBackwards() { public void us_giveWrapBackwards() {
final PicCountClock picCountClock = new PicCountClock(100); final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setMaxPicCount(16*2); picCountClock.setMaxPicCount(16*2);
//Need to walk up no faster than maxPicCount / 2 //Need to walk up no faster than maxPicCount / 2
picCountClock.setPicCount(7*2); picCountClock.setPicCount(7*2);
......
...@@ -22,7 +22,6 @@ public class StreamHeaderBoxTest { ...@@ -22,7 +22,6 @@ public class StreamHeaderBoxTest {
Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC()); Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC());
Assert.assertEquals(0, streamHeaderBox.getInitialFrames()); Assert.assertEquals(0, streamHeaderBox.getInitialFrames());
Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1); Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1);
Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample());
Assert.assertEquals(11805L, streamHeaderBox.getLength()); Assert.assertEquals(11805L, streamHeaderBox.getLength());
Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize()); Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize());
} }
......
...@@ -51,4 +51,23 @@ public class UnboundedIntArrayTest { ...@@ -51,4 +51,23 @@ public class UnboundedIntArrayTest {
//Intentionally blank //Intentionally blank
} }
} }
@Test
public void get_givenValidIndex() {
final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(4);
unboundedIntArray.add(1);
unboundedIntArray.add(2);
Assert.assertEquals(1, unboundedIntArray.get(0));
}
@Test
public void get_givenOutOfBounds() {
final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(4);
try {
unboundedIntArray.get(0);
Assert.fail();
} catch (ArrayIndexOutOfBoundsException e) {
//Intentionally blank
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment