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;
|
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
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import org.junit.Test;
|
|||||||
public class LinearClockTest {
|
public class LinearClockTest {
|
||||||
@Test
|
@Test
|
||||||
public void advance() {
|
public void advance() {
|
||||||
final LinearClock linearClock = new LinearClock(100L);
|
final LinearClock linearClock = new LinearClock(1_000L, 10);
|
||||||
linearClock.setIndex(2);
|
linearClock.setIndex(2);
|
||||||
Assert.assertEquals(200, linearClock.getUs());
|
Assert.assertEquals(200, linearClock.getUs());
|
||||||
linearClock.advance();
|
linearClock.advance();
|
||||||
|
@ -6,7 +6,7 @@ import org.junit.Test;
|
|||||||
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);
|
||||||
|
@ -22,7 +22,6 @@ public class StreamHeaderBoxTest {
|
|||||||
Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC());
|
Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC());
|
||||||
Assert.assertEquals(0, streamHeaderBox.getInitialFrames());
|
Assert.assertEquals(0, streamHeaderBox.getInitialFrames());
|
||||||
Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1);
|
Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1);
|
||||||
Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample());
|
|
||||||
Assert.assertEquals(11805L, streamHeaderBox.getLength());
|
Assert.assertEquals(11805L, streamHeaderBox.getLength());
|
||||||
Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize());
|
Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize());
|
||||||
}
|
}
|
||||||
|
@ -51,4 +51,23 @@ public class UnboundedIntArrayTest {
|
|||||||
//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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user