Updated seek

This commit is contained in:
Dustin 2022-01-28 12:47:43 -07:00
parent c41dc2360f
commit 1d85bf2456
18 changed files with 495 additions and 196 deletions

View File

@ -1,11 +1,10 @@
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 {
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
} }

View File

@ -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 {
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() {

View File

@ -17,12 +17,15 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
/** /**
* Based on the official MicroSoft spec * Based on the official MicroSoft spec
* https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference * https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
*/ */
public class AviExtractor implements Extractor { 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 final long UINT_MASK = 0xffffffffL;
static long getUInt(ByteBuffer byteBuffer) { static long getUInt(ByteBuffer byteBuffer) {
@ -69,7 +72,7 @@ public class AviExtractor implements Extractor {
@VisibleForTesting @VisibleForTesting
static final int STATE_SEEK_START = 4; 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); static final int RIFF = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24);
@ -92,6 +95,9 @@ public class AviExtractor implements Extractor {
ExtractorOutput output; ExtractorOutput output;
private AviHeaderBox aviHeader; private AviHeaderBox aviHeader;
private long durationUs = C.TIME_UNSET; private long durationUs = C.TIME_UNSET;
/**
* AviTracks by StreamId
*/
private AviTrack[] aviTracks = new AviTrack[0]; private AviTrack[] aviTracks = new AviTrack[0];
//At the start of the movi tag //At the start of the movi tag
private long moviOffset; private long moviOffset;
@ -210,13 +216,17 @@ public class AviExtractor implements Extractor {
final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class);
final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class);
if (streamHeader == null) { if (streamHeader == null) {
Log.w(TAG, "Missing Stream Header"); w("Missing Stream Header");
return null; return null;
} }
//i(streamHeader.toString());
if (streamFormat == null) { if (streamFormat == null) {
Log.w(TAG, "Missing Stream Format"); w("Missing Stream Format");
return null; return null;
} }
final long durationUs = streamHeader.getDurationUs();
//Initial estimate
final int length = streamHeader.getLength();
final Format.Builder builder = new Format.Builder(); final Format.Builder builder = new Format.Builder();
builder.setId(streamId); builder.setId(streamId);
final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); final int suggestedBufferSize = streamHeader.getSuggestedBufferSize();
@ -242,18 +252,20 @@ public class AviExtractor implements Extractor {
builder.setSampleMimeType(mimeType); builder.setSampleMimeType(mimeType);
if (MimeTypes.VIDEO_H264.equals(mimeType)) { if (MimeTypes.VIDEO_H264.equals(mimeType)) {
final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample()); final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, durationUs,
aviTrack = new AviTrack(streamId, videoFormat, avcChunkPeeker.getPicCountClock(), trackOutput); length);
aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, avcChunkPeeker.getPicCountClock(),
trackOutput);
aviTrack.setChunkPeeker(avcChunkPeeker); aviTrack.setChunkPeeker(avcChunkPeeker);
} else { } else {
aviTrack = new AviTrack(streamId, videoFormat, aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO,
new LinearClock(streamHeader.getUsPerSample()), trackOutput); new LinearClock(durationUs, length), trackOutput);
if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { if (MimeTypes.VIDEO_MP4V.equals(mimeType)) {
aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput)); aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput));
} }
} }
trackOutput.format(builder.build()); trackOutput.format(builder.build());
durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); this.durationUs = durationUs;
} else if (streamHeader.isAudio()) { } else if (streamHeader.isAudio()) {
final AudioFormat audioFormat = streamFormat.getAudioFormat(); final AudioFormat audioFormat = streamFormat.getAudioFormat();
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); 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())); builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData()));
} }
trackOutput.format(builder.build()); trackOutput.format(builder.build());
aviTrack = new AviTrack(streamId, audioFormat, new LinearClock(streamHeader.getUsPerSample()), aviTrack = new AviTrack(streamId, C.TRACK_TYPE_AUDIO,
trackOutput); new LinearClock(durationUs, length), trackOutput);
aviTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES);
}else { }else {
aviTrack = null; aviTrack = null;
} }
@ -343,6 +356,27 @@ public class AviExtractor implements Extractor {
return null; 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 * Reads the index and sets the keyFrames and creates the SeekMap
* @param input * @param input
@ -353,86 +387,93 @@ public class AviExtractor implements Extractor {
final AviTrack videoTrack = getVideoTrack(); final AviTrack videoTrack = getVideoTrack();
if (videoTrack == null) { if (videoTrack == null) {
output.seekMap(new SeekMap.Unseekable(getDuration())); output.seekMap(new SeekMap.Unseekable(getDuration()));
Log.w(TAG, "No video track found"); w("No video track found");
return; return;
} }
final int videoId = videoTrack.id;
final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024));
final byte[] bytes = indexByteBuffer.array(); final byte[] bytes = indexByteBuffer.array();
final int[] chunkCounts = new int[aviTracks.length]; //These are ints/2
final UnboundedIntArray[] seekOffsets = new UnboundedIntArray[aviTracks.length]; final UnboundedIntArray keyFrameOffsetsDiv2 = new UnboundedIntArray();
for (int i=0;i<seekOffsets.length;i++) { final int[] keyFrameCounts = new int[aviTracks.length];
seekOffsets[i] = new UnboundedIntArray(); final UnboundedIntArray[] seekIndexes = new UnboundedIntArray[aviTracks.length];
for (int i=0;i<seekIndexes.length;i++) {
seekIndexes[i] = new UnboundedIntArray();
} }
//TODO: Change this to min frame rate final long usPerVideoChunk = videoTrack.getClock().getUs(1);
final int seekFrameRate = (int)(1f/(videoTrack.getClock().usPerChunk / 1_000_000f) * 2); //Chunks in 2 seconds
final int chunksPerKeyFrame = (int)(MIN_KEY_FRAME_RATE_US / usPerVideoChunk);
final UnboundedIntArray keyFrameList = new UnboundedIntArray(); final HashMap<Integer, Integer> tagMap = new HashMap<>();
while (remaining > 0) { while (remaining > 0) {
final int toRead = Math.min(indexByteBuffer.remaining(), remaining); final int toRead = Math.min(indexByteBuffer.remaining(), remaining);
input.readFully(bytes, indexByteBuffer.position(), toRead); input.readFully(bytes, indexByteBuffer.position(), toRead);
indexByteBuffer.limit(indexByteBuffer.position() + toRead);
remaining -= toRead; remaining -= toRead;
while (indexByteBuffer.remaining() >= 16) { while (indexByteBuffer.remaining() >= 16) {
final int chunkId = indexByteBuffer.getInt(); 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); final AviTrack aviTrack = getAviTrack(chunkId);
if (aviTrack == null) { if (aviTrack == null) {
if (chunkId != AviExtractor.REC_) { if (chunkId != AviExtractor.REC_) {
Log.w(TAG, "Unknown Track Type: " + toString(chunkId)); w("Unknown Track Type: " + toString(chunkId));
} }
indexByteBuffer.position(indexByteBuffer.position() + 12); indexByteBuffer.position(indexByteBuffer.position() + 12);
continue; continue;
} }
final int flags = indexByteBuffer.getInt(); final int flags = indexByteBuffer.getInt();
final int offset = indexByteBuffer.getInt(); final int offset = indexByteBuffer.getInt();
indexByteBuffer.position(indexByteBuffer.position() + 4); //Skip size
//int size = indexByteBuffer.getInt(); //indexByteBuffer.position(indexByteBuffer.position() + 4);
if (aviTrack.isVideo()) { final int size = indexByteBuffer.getInt();
if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
keyFrameList.add(chunkCounts[aviTrack.id]); if (aviTrack.isVideo()) {
} int indexSize = seekIndexes[videoId].getSize();
if (chunkCounts[aviTrack.id] % seekFrameRate == 0) { if (indexSize == 0 || aviTrack.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) {
seekOffsets[aviTrack.id].add(offset); keyFrameOffsetsDiv2.add(offset / 2);
for (int i=0;i<seekOffsets.length;i++) { for (AviTrack seekTrack : aviTracks) {
if (i != aviTrack.id) { if (seekTrack != null) {
seekOffsets[i].add(chunkCounts[i]); seekIndexes[seekTrack.id].add(seekTrack.chunks);
}
} }
} }
} }
keyFrameCounts[aviTrack.id]++;
} }
chunkCounts[aviTrack.id]++; aviTrack.chunks++;
aviTrack.size+=size;
} }
indexByteBuffer.compact(); indexByteBuffer.compact();
} }
//Set the keys frames if (videoTrack.chunks == keyFrameCounts[videoTrack.id]) {
if (!videoTrack.isAllKeyFrames()) { videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES);
final int[] keyFrames = keyFrameList.getArray(); } else {
videoTrack.setKeyFrames(keyFrames); videoTrack.setKeyFrames(seekIndexes[videoId].getArray());
} }
//Correct the timings final AviSeekMap seekMap = new AviSeekMap(videoId, videoTrack.clock.durationUs, videoTrack.chunks,
durationUs = chunkCounts[videoTrack.id] * videoTrack.getClock().usPerChunk; 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<chunkCounts.length;i++) {
final AviTrack aviTrack = aviTracks[i];
if (aviTrack != null && aviTrack.isAudio()) {
final long calcUsPerSample = (durationUs/chunkCounts[i]);
final LinearClock linearClock = aviTrack.getClock();
final float deltaPercent = Math.abs(calcUsPerSample - linearClock.usPerChunk) / (float)linearClock.usPerChunk;
if (deltaPercent >.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); setSeekMap(seekMap);
} }
@Nullable @Nullable
private AviTrack getAviTrack(int chunkId) { private AviTrack getAviTrack(int chunkId) {
final int streamId = getStreamId(chunkId); for (AviTrack aviTrack : aviTracks) {
if (streamId >= 0 && streamId < aviTracks.length) { if (aviTrack.handlesChunkId(chunkId)) {
return aviTracks[streamId]; return aviTrack;
}
} }
return null; return null;
} }
@ -525,6 +566,7 @@ public class AviExtractor implements Extractor {
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
//i("Seek pos=" + position +", us="+timeUs);
chunkHandler = null; chunkHandler = null;
if (position <= 0) { if (position <= 0) {
if (moviOffset != 0) { if (moviOffset != 0) {
@ -551,6 +593,11 @@ public class AviExtractor implements Extractor {
//Intentionally blank //Intentionally blank
} }
@VisibleForTesting
void setAviTracks(AviTrack[] aviTracks) {
this.aviTracks = aviTracks;
}
private static void w(String message) { private static void w(String message) {
try { try {
Log.w(TAG, message); Log.w(TAG, message);
@ -558,4 +605,12 @@ public class AviExtractor implements Extractor {
//Catch not mocked for tests //Catch not mocked for tests
} }
} }
private static void i(String message) {
try {
Log.i(TAG, message);
} catch (RuntimeException e) {
//Catch not mocked for tests
}
}
} }

View File

@ -1,34 +1,32 @@
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 {
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);
}
}
} }
} }
} }

View File

@ -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;
* 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 {
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 {
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();
} }
} }

View File

@ -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();
}

View File

@ -1,12 +1,22 @@
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 {
} }
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;
} }
} }

View File

@ -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 {
@Override @Override
public long getUs() { public long getUs() {
return picIndex * usPerChunk; return getUs(picIndex);
} }
} }

View File

@ -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 {
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 {
// 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();
// }
} }

View File

@ -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;
} }

View File

@ -1,11 +1,10 @@
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 {
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;
}
} }

View File

@ -1,5 +1,6 @@
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 {
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);
}
} }

View File

@ -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<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));
}
}

View File

@ -1,5 +1,6 @@
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;
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 {
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;
} }
} }

View File

@ -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();

View File

@ -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 {
@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 {
@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);

View File

@ -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());
} }

View File

@ -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
}
}
} }