mirror of
https://github.com/androidx/media.git
synced 2025-05-14 11:09:53 +08:00
Updated seek
This commit is contained in:
parent
c41dc2360f
commit
1d85bf2456
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
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]);
|
||||
//Skip size
|
||||
//indexByteBuffer.position(indexByteBuffer.position() + 4);
|
||||
final int size = indexByteBuffer.getInt();
|
||||
if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
|
||||
if (aviTrack.isVideo()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keyFrameCounts[aviTrack.id]++;
|
||||
}
|
||||
chunkCounts[aviTrack.id]++;
|
||||
aviTrack.chunks++;
|
||||
aviTrack.size+=size;
|
||||
}
|
||||
indexByteBuffer.compact();
|
||||
}
|
||||
//Set the keys frames
|
||||
if (!videoTrack.isAllKeyFrames()) {
|
||||
final int[] keyFrames = keyFrameList.getArray();
|
||||
videoTrack.setKeyFrames(keyFrames);
|
||||
if (videoTrack.chunks == keyFrameCounts[videoTrack.id]) {
|
||||
videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES);
|
||||
} else {
|
||||
videoTrack.setKeyFrames(seekIndexes[videoId].getArray());
|
||||
}
|
||||
|
||||
//Correct the timings
|
||||
durationUs = chunkCounts[videoTrack.id] * videoTrack.getClock().usPerChunk;
|
||||
final AviSeekMap seekMap = new AviSeekMap(videoId, videoTrack.clock.durationUs, videoTrack.chunks,
|
||||
keyFrameOffsetsDiv2.getArray(), seekIndexes, moviOffset);
|
||||
|
||||
i("Video chunks=" + videoTrack.chunks + " us=" + seekMap.getDurationUs());
|
||||
|
||||
//Needs to be called after the duration is updated
|
||||
updateAudioTiming(keyFrameCounts, durationUs);
|
||||
|
||||
for (int i=0;i<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
int getFirstSeekIndex(int index) {
|
||||
int firstIndex = -index - 2;
|
||||
if (firstIndex < 0) {
|
||||
firstIndex = 0;
|
||||
}
|
||||
return reqFrameIndex;
|
||||
return firstIndex;
|
||||
}
|
||||
|
||||
private SeekPoint getSeekPoint(int index) {
|
||||
long offset = keyFrameOffsetsDiv2[index] * 2L;
|
||||
final long outUs = seekIndexes[videoId][index] * videoUsPerChunk;
|
||||
final long position = offset + moviOffset;
|
||||
return new SeekPoint(outUs, position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SeekPoints getSeekPoints(long timeUs) {
|
||||
final int seekFrameIndex = getSeekFrameIndex(timeUs);
|
||||
int offset = seekOffsets[videoStreamId][seekFrameIndex];
|
||||
final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk;
|
||||
final long position = offset + moviOffset;
|
||||
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
||||
final int index = getSeekIndex(timeUs);
|
||||
if (index >= 0) {
|
||||
return new SeekPoints(getSeekPoint(index));
|
||||
}
|
||||
final int firstSeekIndex = getFirstSeekIndex(index);
|
||||
if (firstSeekIndex + 1 < keyFrameOffsetsDiv2.length) {
|
||||
return new SeekPoints(getSeekPoint(firstSeekIndex), getSeekPoint(firstSeekIndex+1));
|
||||
} else {
|
||||
return new SeekPoints(getSeekPoint(firstSeekIndex));
|
||||
}
|
||||
|
||||
return new SeekPoints(new SeekPoint(outUs, position));
|
||||
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
||||
}
|
||||
|
||||
public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) {
|
||||
final int seekFrameIndex = getSeekFrameIndex(timeUs);
|
||||
final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - moviOffset) / 2));
|
||||
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("Position: " + position);
|
||||
}
|
||||
for (int i=0;i<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);
|
||||
}
|
||||
}
|
||||
final LinearClock clock = aviTrack.getClock();
|
||||
clock.setIndex(seekIndexes[i][index]);
|
||||
// Log.d(AviExtractor.TAG, "Frame: " + (aviTrack.isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " frame=" + clock.getIndex() + " key=" + aviTrack.isKeyFrame());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
// }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user