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

View File

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

View File

@ -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<seekOffsets.length;i++) {
seekOffsets[i] = new UnboundedIntArray();
//These are ints/2
final UnboundedIntArray keyFrameOffsetsDiv2 = new UnboundedIntArray();
final int[] keyFrameCounts = new int[aviTracks.length];
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 int seekFrameRate = (int)(1f/(videoTrack.getClock().usPerChunk / 1_000_000f) * 2);
final UnboundedIntArray keyFrameList = new UnboundedIntArray();
final long usPerVideoChunk = videoTrack.getClock().getUs(1);
//Chunks in 2 seconds
final int chunksPerKeyFrame = (int)(MIN_KEY_FRAME_RATE_US / usPerVideoChunk);
final HashMap<Integer, Integer> 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();
//Skip size
//indexByteBuffer.position(indexByteBuffer.position() + 4);
final int size = indexByteBuffer.getInt();
if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
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<seekOffsets.length;i++) {
if (i != aviTrack.id) {
seekOffsets[i].add(chunkCounts[i]);
int indexSize = seekIndexes[videoId].getSize();
if (indexSize == 0 || aviTrack.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) {
keyFrameOffsetsDiv2.add(offset / 2);
for (AviTrack seekTrack : aviTracks) {
if (seekTrack != null) {
seekIndexes[seekTrack.id].add(seekTrack.chunks);
}
}
}
}
chunkCounts[aviTrack.id]++;
keyFrameCounts[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<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);
}
@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
}
}
}

View File

@ -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.length;i++) {
this.seekOffsets[i] = seekOffsets[i].getArray();
public AviSeekMap(int videoId, long usDuration, int videoChunks, int[] keyFrameOffsetsDiv2,
UnboundedIntArray[] seekIndexes, long moviOffset) {
this.videoId = videoId;
this.videoUsPerChunk = usDuration / videoChunks;
this.duration = usDuration;
this.keyFrameOffsetsDiv2 = keyFrameOffsetsDiv2;
this.seekIndexes = new int[seekIndexes.length][];
for (int i=0;i<seekIndexes.length;i++) {
this.seekIndexes[i] = seekIndexes[i].getArray();
}
this.moviOffset = moviOffset;
}
@Override
@ -41,42 +39,55 @@ public class AviSeekMap implements SeekMap {
return duration;
}
private int getSeekFrameIndex(long timeUs) {
private int getSeekIndex(long timeUs) {
final int reqFrame = (int)(timeUs / videoUsPerChunk);
int reqFrameIndex = reqFrame / seekIndexFactor;
if (reqFrameIndex >= seekOffsets[videoStreamId].length) {
reqFrameIndex = seekOffsets[videoStreamId].length - 1;
return Arrays.binarySearch(seekIndexes[videoId], reqFrame);
}
return reqFrameIndex;
@VisibleForTesting
int getFirstSeekIndex(int index) {
int firstIndex = -index - 2;
if (firstIndex < 0) {
firstIndex = 0;
}
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<aviTracks.length;i++) {
final AviTrack aviTrack = aviTracks[i];
if (aviTrack != null) {
final LinearClock clock = aviTrack.getClock();
if (aviTrack.isVideo()) {
//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);
}
}
clock.setIndex(seekIndexes[i][index]);
// Log.d(AviExtractor.TAG, "Frame: " + (aviTrack.isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " frame=" + clock.getIndex() + " key=" + aviTrack.isKeyFrame());
}
}
}

View File

@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Log;
import java.io.IOException;
import java.util.Arrays;
@ -12,42 +13,68 @@ import java.util.Arrays;
* Collection of info about a track
*/
public class AviTrack {
public static final int[] ALL_KEY_FRAMES = new int[0];
final int id;
final @C.TrackType int trackType;
@NonNull
final LinearClock clock;
/**
* True indicates all frames are key frames (e.g. Audio, MJPEG)
*/
final boolean allKeyFrames;
final @C.TrackType int trackType;
@NonNull
final TrackOutput trackOutput;
boolean forceKeyFrame;
final int chunkId;
final int chunkIdAlt;
@Nullable
ChunkPeeker chunkPeeker;
int chunks;
int size;
/**
* Key is frame number value is offset
* Ordered list of key frame chunk indexes
*/
@Nullable
int[] keyFrames;
int[] keyFrames = new int[0];
transient int chunkSize;
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) {
this.id = id;
this.clock = clock;
this.allKeyFrames = streamFormat.isAllKeyFrames();
this.trackType = streamFormat.getTrackType();
this.trackType = trackType;
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() {
@ -58,30 +85,16 @@ public class AviTrack {
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() {
if (allKeyFrames) {
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;
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();
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import java.io.File;
@ -11,6 +12,14 @@ import java.util.ArrayList;
import java.util.Arrays;
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\."
private static final File RELATIVE_PATH = new File("../../testdata/src/test/assets/extractordumps/avi/");
public static FakeExtractorInput getInput(final String fileName) throws IOException {
@ -87,18 +96,62 @@ public class DataHelper {
byteBuffer.put(nalType);
return byteBuffer;
}
public static AviSeekMap getAviSeekMap() throws IOException {
final FakeTrackOutput output = new FakeTrackOutput(false);
final AviTrack videoTrack = new AviTrack(0,
DataHelper.getVideoStreamFormat().getVideoFormat(), new LinearClock(100), output);
public static AviTrack getVideoAviTrack(int sec) {
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
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();
videoArray.add(0);
videoArray.add(1024);
videoArray.add(4);
final UnboundedIntArray audioArray = new UnboundedIntArray();
audioArray.add(0);
audioArray.add(128);
return new AviSeekMap(videoTrack,
new UnboundedIntArray[]{videoArray, audioArray}, 24, 0L, 0L);
return new AviSeekMap(0, 100L, 8, keyFrameOffsetsDiv2,
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 {
@Test
public void advance() {
final LinearClock linearClock = new LinearClock(100L);
final LinearClock linearClock = new LinearClock(1_000L, 10);
linearClock.setIndex(2);
Assert.assertEquals(200, linearClock.getUs());
linearClock.advance();

View File

@ -6,7 +6,7 @@ import org.junit.Test;
public class PicCountClockTest {
@Test
public void us_givenTwoStepsForward() {
final PicCountClock picCountClock = new PicCountClock(100);
final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setMaxPicCount(16*2);
picCountClock.setPicCount(2*2);
Assert.assertEquals(2*100, picCountClock.getUs());
@ -14,7 +14,7 @@ public class PicCountClockTest {
@Test
public void us_givenThreeStepsBackwards() {
final PicCountClock picCountClock = new PicCountClock(100);
final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setMaxPicCount(16*2);
picCountClock.setPicCount(4*2); // 400ms
Assert.assertEquals(400, picCountClock.getUs());
@ -24,14 +24,14 @@ public class PicCountClockTest {
@Test
public void setIndex_given3Chunks() {
final PicCountClock picCountClock = new PicCountClock(100);
final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setIndex(3);
Assert.assertEquals(3*100, picCountClock.getUs());
}
@Test
public void us_giveWrapBackwards() {
final PicCountClock picCountClock = new PicCountClock(100);
final PicCountClock picCountClock = new PicCountClock(10_000L, 100);
picCountClock.setMaxPicCount(16*2);
//Need to walk up no faster than maxPicCount / 2
picCountClock.setPicCount(7*2);

View File

@ -22,7 +22,6 @@ public class StreamHeaderBoxTest {
Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC());
Assert.assertEquals(0, streamHeaderBox.getInitialFrames());
Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1);
Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample());
Assert.assertEquals(11805L, streamHeaderBox.getLength());
Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize());
}

View File

@ -51,4 +51,23 @@ public class UnboundedIntArrayTest {
//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
}
}
}