mirror of
https://github.com/androidx/media.git
synced 2025-05-12 01:59:50 +08:00
Merge branch 'everything' into avi
# Conflicts: # library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java # library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java # library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java # library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java
This commit is contained in:
commit
193ece6d18
@ -565,4 +565,22 @@ public final class ParsableByteArray {
|
|||||||
position += length;
|
position += length;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data from the end of the buffer is copied to the front
|
||||||
|
* The limit() because the bytesLeft() and position is zero
|
||||||
|
*/
|
||||||
|
public void compact() {
|
||||||
|
if (bytesLeft() == 0) {
|
||||||
|
limit = 0;
|
||||||
|
} else {
|
||||||
|
final ByteBuffer byteBuffer = ByteBuffer.wrap(data);
|
||||||
|
byteBuffer.limit(limit);
|
||||||
|
byteBuffer.position(position);
|
||||||
|
byteBuffer.compact();
|
||||||
|
byteBuffer.flip();
|
||||||
|
limit = byteBuffer.limit();
|
||||||
|
}
|
||||||
|
position = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
@ -27,7 +29,7 @@ import java.io.IOException;
|
|||||||
* Corrects the time and PAR for H264 streams
|
* Corrects the time and PAR for H264 streams
|
||||||
* AVC is very rare in AVI due to the rise of the mp4 container
|
* AVC is very rare in AVI due to the rise of the mp4 container
|
||||||
*/
|
*/
|
||||||
public class AvcChunkPeeker extends NalChunkPeeker {
|
public class AvcChunkHandler extends NalChunkHandler {
|
||||||
private static final int NAL_TYPE_MASK = 0x1f;
|
private static final int NAL_TYPE_MASK = 0x1f;
|
||||||
private static final int NAL_TYPE_IDR = 5; //I Frame
|
private static final int NAL_TYPE_IDR = 5; //I Frame
|
||||||
private static final int NAL_TYPE_SEI = 6;
|
private static final int NAL_TYPE_SEI = 6;
|
||||||
@ -35,36 +37,43 @@ public class AvcChunkPeeker extends NalChunkPeeker {
|
|||||||
private static final int NAL_TYPE_PPS = 8;
|
private static final int NAL_TYPE_PPS = 8;
|
||||||
private static final int NAL_TYPE_AUD = 9;
|
private static final int NAL_TYPE_AUD = 9;
|
||||||
|
|
||||||
private final PicCountClock picCountClock;
|
|
||||||
private final Format.Builder formatBuilder;
|
private final Format.Builder formatBuilder;
|
||||||
private final TrackOutput trackOutput;
|
|
||||||
|
|
||||||
private float pixelWidthHeightRatio = 1f;
|
private float pixelWidthHeightRatio = 1f;
|
||||||
private NalUnitUtil.SpsData spsData;
|
private NalUnitUtil.SpsData spsData;
|
||||||
|
|
||||||
public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, LinearClock clock) {
|
public AvcChunkHandler(int id, @NonNull TrackOutput trackOutput,
|
||||||
super(16);
|
@NonNull ChunkClock clock, Format.Builder formatBuilder) {
|
||||||
|
super(id, trackOutput, clock, 16);
|
||||||
this.formatBuilder = formatBuilder;
|
this.formatBuilder = formatBuilder;
|
||||||
this.trackOutput = trackOutput;
|
|
||||||
picCountClock = new PicCountClock(clock.durationUs, clock.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PicCountClock getClock() {
|
@Nullable
|
||||||
return picCountClock;
|
@VisibleForTesting
|
||||||
|
PicCountClock getPicCountClock() {
|
||||||
|
if (clock instanceof PicCountClock) {
|
||||||
|
return (PicCountClock)clock;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean skip(byte nalType) {
|
boolean skip(byte nalType) {
|
||||||
return false;
|
if (clock instanceof PicCountClock) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
//If the clock is ChunkClock, skip "normal" frames
|
||||||
|
return nalType >= 0 && nalType <= NAL_TYPE_IDR;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Greatly simplified way to calculate the picOrder
|
* Greatly simplified way to calculate the picOrder
|
||||||
* Full logic is here
|
* Full logic is here
|
||||||
* https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/video/h264_poc.cc
|
* https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/video/h264_poc.cc
|
||||||
* @param nalTypeOffset
|
|
||||||
*/
|
*/
|
||||||
void updatePicCountClock(final int nalTypeOffset) {
|
void updatePicCountClock(final int nalTypeOffset, final PicCountClock picCountClock) {
|
||||||
final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length);
|
final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length);
|
||||||
//slide_header()
|
//slide_header()
|
||||||
in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice
|
in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice
|
||||||
@ -90,7 +99,7 @@ public class AvcChunkPeeker extends NalChunkPeeker {
|
|||||||
picCountClock.setPicCount(frameNum);
|
picCountClock.setPicCount(frameNum);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
picCountClock.setIndex(picCountClock.getIndex());
|
clock.setIndex(clock.getIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@ -98,11 +107,22 @@ public class AvcChunkPeeker extends NalChunkPeeker {
|
|||||||
final int spsStart = nalTypeOffset + 1;
|
final int spsStart = nalTypeOffset + 1;
|
||||||
nalTypeOffset = seekNextNal(input, spsStart);
|
nalTypeOffset = seekNextNal(input, spsStart);
|
||||||
spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos);
|
spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos);
|
||||||
if (spsData.picOrderCountType == 0) {
|
//If we can have B Frames, upgrade to PicCountClock
|
||||||
picCountClock.setMaxPicCount(1 << spsData.picOrderCntLsbLength, 2);
|
final PicCountClock picCountClock;
|
||||||
} else if (spsData.picOrderCountType == 2) {
|
if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) {
|
||||||
//Plus one because we double the frame number
|
picCountClock = new PicCountClock(clock.durationUs, clock.chunks);
|
||||||
picCountClock.setMaxPicCount(1 << spsData.frameNumLength, 1);
|
picCountClock.setIndex(clock.getIndex());
|
||||||
|
clock = picCountClock;
|
||||||
|
} else {
|
||||||
|
picCountClock = getPicCountClock();
|
||||||
|
}
|
||||||
|
if (picCountClock != null) {
|
||||||
|
if (spsData.picOrderCountType == 0) {
|
||||||
|
picCountClock.setMaxPicCount(1 << spsData.picOrderCntLsbLength, 2);
|
||||||
|
} else if (spsData.picOrderCountType == 2) {
|
||||||
|
//Plus one because we double the frame number
|
||||||
|
picCountClock.setMaxPicCount(1 << spsData.frameNumLength, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) {
|
if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) {
|
||||||
pixelWidthHeightRatio = spsData.pixelWidthHeightRatio;
|
pixelWidthHeightRatio = spsData.pixelWidthHeightRatio;
|
||||||
@ -121,11 +141,17 @@ public class AvcChunkPeeker extends NalChunkPeeker {
|
|||||||
case 2:
|
case 2:
|
||||||
case 3:
|
case 3:
|
||||||
case 4:
|
case 4:
|
||||||
updatePicCountClock(nalTypeOffset);
|
if (clock instanceof PicCountClock) {
|
||||||
|
updatePicCountClock(nalTypeOffset, (PicCountClock)clock);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case NAL_TYPE_IDR:
|
case NAL_TYPE_IDR: {
|
||||||
picCountClock.syncIndexes();
|
final PicCountClock picCountClock = getPicCountClock();
|
||||||
|
if (picCountClock != null) {
|
||||||
|
picCountClock.syncIndexes();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
case NAL_TYPE_AUD:
|
case NAL_TYPE_AUD:
|
||||||
case NAL_TYPE_SEI:
|
case NAL_TYPE_SEI:
|
||||||
case NAL_TYPE_PPS: {
|
case NAL_TYPE_PPS: {
|
@ -103,7 +103,7 @@ public class AviExtractor implements Extractor {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int STATE_READ_IDX1 = 2;
|
static final int STATE_READ_IDX1 = 2;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int STATE_READ_SAMPLES = 3;
|
static final int STATE_READ_CHUNKS = 3;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int STATE_SEEK_START = 4;
|
static final int STATE_SEEK_START = 4;
|
||||||
|
|
||||||
@ -127,9 +127,9 @@ public class AviExtractor implements Extractor {
|
|||||||
private AviHeaderBox aviHeader;
|
private AviHeaderBox aviHeader;
|
||||||
private long durationUs = C.TIME_UNSET;
|
private long durationUs = C.TIME_UNSET;
|
||||||
/**
|
/**
|
||||||
* AviTracks by StreamId
|
* ChunkHandlers by StreamId
|
||||||
*/
|
*/
|
||||||
private AviTrack[] aviTracks = new AviTrack[0];
|
private ChunkHandler[] chunkHandlers = new ChunkHandler[0];
|
||||||
//At the start of the movi tag
|
//At the start of the movi tag
|
||||||
private long moviOffset;
|
private long moviOffset;
|
||||||
private long moviEnd;
|
private long moviEnd;
|
||||||
@ -137,7 +137,7 @@ public class AviExtractor implements Extractor {
|
|||||||
AviSeekMap aviSeekMap;
|
AviSeekMap aviSeekMap;
|
||||||
|
|
||||||
//Set if a chunk is only partially read
|
//Set if a chunk is only partially read
|
||||||
private transient AviTrack chunkHandler;
|
private transient ChunkHandler chunkHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -218,7 +218,7 @@ public class AviExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
AviTrack parseStream(final ListBox streamList, int streamId) {
|
ChunkHandler parseStream(final ListBox streamList, int streamId) {
|
||||||
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) {
|
||||||
@ -243,7 +243,8 @@ public class AviExtractor implements Extractor {
|
|||||||
if (streamName != null) {
|
if (streamName != null) {
|
||||||
builder.setLabel(streamName.getName());
|
builder.setLabel(streamName.getName());
|
||||||
}
|
}
|
||||||
final AviTrack aviTrack;
|
final ChunkClock clock = new ChunkClock(durationUs, length);
|
||||||
|
final ChunkHandler chunkHandler;
|
||||||
if (streamHeader.isVideo()) {
|
if (streamHeader.isVideo()) {
|
||||||
final VideoFormat videoFormat = streamFormat.getVideoFormat();
|
final VideoFormat videoFormat = streamFormat.getVideoFormat();
|
||||||
final String mimeType = videoFormat.getMimeType();
|
final String mimeType = videoFormat.getMimeType();
|
||||||
@ -257,17 +258,12 @@ public class AviExtractor implements Extractor {
|
|||||||
builder.setFrameRate(streamHeader.getFrameRate());
|
builder.setFrameRate(streamHeader.getFrameRate());
|
||||||
builder.setSampleMimeType(mimeType);
|
builder.setSampleMimeType(mimeType);
|
||||||
|
|
||||||
final LinearClock clock = new LinearClock(durationUs, length);
|
|
||||||
aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, clock, trackOutput);
|
|
||||||
|
|
||||||
if (MimeTypes.VIDEO_H264.equals(mimeType)) {
|
if (MimeTypes.VIDEO_H264.equals(mimeType)) {
|
||||||
final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, clock);
|
chunkHandler = new AvcChunkHandler(streamId, trackOutput, clock, builder);
|
||||||
aviTrack.setClock(avcChunkPeeker.getClock());
|
} else if (MimeTypes.VIDEO_MP4V.equals(mimeType)) {
|
||||||
aviTrack.setChunkPeeker(avcChunkPeeker);
|
chunkHandler = new Mp4vChunkHandler(streamId, trackOutput, clock, builder);
|
||||||
} else {
|
} else {
|
||||||
if (MimeTypes.VIDEO_MP4V.equals(mimeType)) {
|
chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_VIDEO, trackOutput, clock);
|
||||||
aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
trackOutput.format(builder.build());
|
trackOutput.format(builder.build());
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
@ -294,13 +290,18 @@ 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, C.TRACK_TYPE_AUDIO,
|
if (MimeTypes.AUDIO_MPEG.equals(mimeType)) {
|
||||||
new LinearClock(durationUs, length), trackOutput);
|
chunkHandler = new MpegAudioChunkHandler(streamId, trackOutput, clock,
|
||||||
aviTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES);
|
audioFormat.getSamplesPerSecond());
|
||||||
|
} else {
|
||||||
|
chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_AUDIO,
|
||||||
|
trackOutput, clock);
|
||||||
|
}
|
||||||
|
chunkHandler.setKeyFrames(ChunkHandler.ALL_KEY_FRAMES);
|
||||||
}else {
|
}else {
|
||||||
aviTrack = null;
|
chunkHandler = null;
|
||||||
}
|
}
|
||||||
return aviTrack;
|
return chunkHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int readTracks(ExtractorInput input) throws IOException {
|
private int readTracks(ExtractorInput input) throws IOException {
|
||||||
@ -312,7 +313,7 @@ public class AviExtractor implements Extractor {
|
|||||||
if (aviHeader == null) {
|
if (aviHeader == null) {
|
||||||
throw new IOException("AviHeader not found");
|
throw new IOException("AviHeader not found");
|
||||||
}
|
}
|
||||||
aviTracks = new AviTrack[aviHeader.getStreams()];
|
chunkHandlers = new ChunkHandler[aviHeader.getStreams()];
|
||||||
//This is usually wrong, so it will be overwritten by video if present
|
//This is usually wrong, so it will be overwritten by video if present
|
||||||
durationUs = aviHeader.getTotalFrames() * (long)aviHeader.getMicroSecPerFrame();
|
durationUs = aviHeader.getTotalFrames() * (long)aviHeader.getMicroSecPerFrame();
|
||||||
|
|
||||||
@ -320,7 +321,7 @@ public class AviExtractor implements Extractor {
|
|||||||
for (Box box : headerList.getChildren()) {
|
for (Box box : headerList.getChildren()) {
|
||||||
if (box instanceof ListBox && ((ListBox) box).getListType() == ListBox.TYPE_STRL) {
|
if (box instanceof ListBox && ((ListBox) box).getListType() == ListBox.TYPE_STRL) {
|
||||||
final ListBox streamList = (ListBox) box;
|
final ListBox streamList = (ListBox) box;
|
||||||
aviTracks[streamId] = parseStream(streamList, streamId);
|
chunkHandlers[streamId] = parseStream(streamList, streamId);
|
||||||
streamId++;
|
streamId++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,32 +357,32 @@ public class AviExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
AviTrack getVideoTrack() {
|
ChunkHandler getVideoTrack() {
|
||||||
for (@Nullable AviTrack aviTrack : aviTracks) {
|
for (@Nullable ChunkHandler chunkHandler : chunkHandlers) {
|
||||||
if (aviTrack != null && aviTrack.isVideo()) {
|
if (chunkHandler != null && chunkHandler.isVideo()) {
|
||||||
return aviTrack;
|
return chunkHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void fixTimings(final int[] keyFrameCounts, final long videoDuration) {
|
void fixTimings(final int[] keyFrameCounts, final long videoDuration) {
|
||||||
for (final AviTrack aviTrack : aviTracks) {
|
for (@Nullable final ChunkHandler chunkHandler : chunkHandlers) {
|
||||||
if (aviTrack != null) {
|
if (chunkHandler != null) {
|
||||||
if (aviTrack.isAudio()) {
|
if (chunkHandler.isAudio()) {
|
||||||
final long durationUs = aviTrack.getClock().durationUs;
|
final long durationUs = chunkHandler.getClock().durationUs;
|
||||||
i("Audio #" + aviTrack.id + " chunks: " + aviTrack.chunks + " us=" + durationUs +
|
i("Audio #" + chunkHandler.getId() + " chunks: " + chunkHandler.chunks + " us=" + durationUs +
|
||||||
" size=" + aviTrack.size);
|
" size=" + chunkHandler.size);
|
||||||
final LinearClock linearClock = aviTrack.getClock();
|
final ChunkClock linearClock = chunkHandler.getClock();
|
||||||
//If the audio track duration is off from the video by >5 % recalc using video
|
//If the audio track duration is off from the video by >5 % recalc using video
|
||||||
if ((durationUs - videoDuration) / (float)videoDuration > .05f) {
|
if ((durationUs - videoDuration) / (float)videoDuration > .05f) {
|
||||||
w("Audio #" + aviTrack.id + " duration is off using videoDuration");
|
w("Audio #" + chunkHandler.getId() + " duration is off using videoDuration");
|
||||||
linearClock.setDuration(videoDuration);
|
linearClock.setDuration(videoDuration);
|
||||||
}
|
}
|
||||||
linearClock.setLength(aviTrack.chunks);
|
linearClock.setChunks(chunkHandler.chunks);
|
||||||
if (aviTrack.chunks != keyFrameCounts[aviTrack.id]) {
|
if (chunkHandler.chunks != keyFrameCounts[chunkHandler.getId()]) {
|
||||||
w("Audio is not all key frames chunks=" + aviTrack.chunks + " keyFrames=" +
|
w("Audio is not all key frames chunks=" + chunkHandler.chunks + " keyFrames=" +
|
||||||
keyFrameCounts[aviTrack.id]);
|
keyFrameCounts[chunkHandler.getId()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -392,7 +393,7 @@ public class AviExtractor implements Extractor {
|
|||||||
* Reads the index and sets the keyFrames and creates the SeekMap
|
* Reads the index and sets the keyFrames and creates the SeekMap
|
||||||
*/
|
*/
|
||||||
void readIdx1(ExtractorInput input, int remaining) throws IOException {
|
void readIdx1(ExtractorInput input, int remaining) throws IOException {
|
||||||
final AviTrack videoTrack = getVideoTrack();
|
final ChunkHandler videoTrack = getVideoTrack();
|
||||||
if (videoTrack == null) {
|
if (videoTrack == null) {
|
||||||
output.seekMap(new SeekMap.Unseekable(getDuration()));
|
output.seekMap(new SeekMap.Unseekable(getDuration()));
|
||||||
w("No video track found");
|
w("No video track found");
|
||||||
@ -406,14 +407,14 @@ public class AviExtractor implements Extractor {
|
|||||||
final ByteBuffer firstEntry = AviExtractor.allocate(16);
|
final ByteBuffer firstEntry = AviExtractor.allocate(16);
|
||||||
input.peekFully(firstEntry.array(), 0, 16);
|
input.peekFully(firstEntry.array(), 0, 16);
|
||||||
|
|
||||||
final int videoId = videoTrack.id;
|
final int videoId = videoTrack.getId();
|
||||||
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();
|
||||||
|
|
||||||
//These are ints/2
|
//These are ints/2
|
||||||
final UnboundedIntArray keyFrameOffsetsDiv2 = new UnboundedIntArray();
|
final UnboundedIntArray keyFrameOffsetsDiv2 = new UnboundedIntArray();
|
||||||
final int[] keyFrameCounts = new int[aviTracks.length];
|
final int[] keyFrameCounts = new int[chunkHandlers.length];
|
||||||
final UnboundedIntArray[] seekIndexes = new UnboundedIntArray[aviTracks.length];
|
final UnboundedIntArray[] seekIndexes = new UnboundedIntArray[chunkHandlers.length];
|
||||||
for (int i=0;i<seekIndexes.length;i++) {
|
for (int i=0;i<seekIndexes.length;i++) {
|
||||||
seekIndexes[i] = new UnboundedIntArray();
|
seekIndexes[i] = new UnboundedIntArray();
|
||||||
}
|
}
|
||||||
@ -435,8 +436,8 @@ public class AviExtractor implements Extractor {
|
|||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
tagMap.put(chunkId, count);
|
tagMap.put(chunkId, count);
|
||||||
final AviTrack aviTrack = getAviTrack(chunkId);
|
final ChunkHandler chunkHandler = getChunkHandler(chunkId);
|
||||||
if (aviTrack == null) {
|
if (chunkHandler == null) {
|
||||||
if (chunkId != AviExtractor.REC_) {
|
if (chunkId != AviExtractor.REC_) {
|
||||||
w("Unknown Track Type: " + toString(chunkId));
|
w("Unknown Track Type: " + toString(chunkId));
|
||||||
}
|
}
|
||||||
@ -447,26 +448,26 @@ public class AviExtractor implements Extractor {
|
|||||||
final int offset = indexByteBuffer.getInt();
|
final int offset = indexByteBuffer.getInt();
|
||||||
final int size = indexByteBuffer.getInt();
|
final int size = indexByteBuffer.getInt();
|
||||||
if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
|
if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
|
||||||
if (aviTrack.isVideo()) {
|
if (chunkHandler.isVideo()) {
|
||||||
int indexSize = seekIndexes[videoId].getSize();
|
int indexSize = seekIndexes[videoId].getSize();
|
||||||
if (indexSize == 0 || aviTrack.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) {
|
if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) {
|
||||||
keyFrameOffsetsDiv2.add(offset / 2);
|
keyFrameOffsetsDiv2.add(offset / 2);
|
||||||
for (AviTrack seekTrack : aviTracks) {
|
for (@Nullable ChunkHandler seekTrack : chunkHandlers) {
|
||||||
if (seekTrack != null) {
|
if (seekTrack != null) {
|
||||||
seekIndexes[seekTrack.id].add(seekTrack.chunks);
|
seekIndexes[seekTrack.getId()].add(seekTrack.chunks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyFrameCounts[aviTrack.id]++;
|
keyFrameCounts[chunkHandler.getId()]++;
|
||||||
}
|
}
|
||||||
aviTrack.chunks++;
|
chunkHandler.chunks++;
|
||||||
aviTrack.size+=size;
|
chunkHandler.size+=size;
|
||||||
}
|
}
|
||||||
indexByteBuffer.compact();
|
indexByteBuffer.compact();
|
||||||
}
|
}
|
||||||
if (videoTrack.chunks == keyFrameCounts[videoTrack.id]) {
|
if (videoTrack.chunks == keyFrameCounts[videoTrack.getId()]) {
|
||||||
videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES);
|
videoTrack.setKeyFrames(ChunkHandler.ALL_KEY_FRAMES);
|
||||||
} else {
|
} else {
|
||||||
videoTrack.setKeyFrames(seekIndexes[videoId].getArray());
|
videoTrack.setKeyFrames(seekIndexes[videoId].getArray());
|
||||||
}
|
}
|
||||||
@ -485,16 +486,16 @@ public class AviExtractor implements Extractor {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
AviTrack getAviTrack(int chunkId) {
|
ChunkHandler getChunkHandler(int chunkId) {
|
||||||
for (AviTrack aviTrack : aviTracks) {
|
for (@Nullable ChunkHandler chunkHandler : chunkHandlers) {
|
||||||
if (aviTrack != null && aviTrack.handlesChunkId(chunkId)) {
|
if (chunkHandler != null && chunkHandler.handlesChunkId(chunkId)) {
|
||||||
return aviTrack;
|
return chunkHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int readSamples(@NonNull ExtractorInput input) throws IOException {
|
int readChunks(@NonNull ExtractorInput input) throws IOException {
|
||||||
if (chunkHandler != null) {
|
if (chunkHandler != null) {
|
||||||
if (chunkHandler.resume(input)) {
|
if (chunkHandler.resume(input)) {
|
||||||
chunkHandler = null;
|
chunkHandler = null;
|
||||||
@ -527,21 +528,21 @@ public class AviExtractor implements Extractor {
|
|||||||
alignInput(input);
|
alignInput(input);
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
final AviTrack aviTrack = getAviTrack(chunkId);
|
final ChunkHandler chunkHandler = getChunkHandler(chunkId);
|
||||||
if (aviTrack == null) {
|
if (chunkHandler == null) {
|
||||||
input.skipFully(size);
|
input.skipFully(size);
|
||||||
alignInput(input);
|
alignInput(input);
|
||||||
w("Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8)
|
w("Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8)
|
||||||
+ " size=" + size + " moviEnd=" + moviEnd);
|
+ " size=" + size + " moviEnd=" + moviEnd);
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
if (aviTrack.newChunk(chunkId, size, input)) {
|
if (chunkHandler.newChunk(size, input)) {
|
||||||
alignInput(input);
|
alignInput(input);
|
||||||
} else {
|
} else {
|
||||||
chunkHandler = aviTrack;
|
this.chunkHandler = chunkHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (input.getPosition() == input.getLength()) {
|
if (input.getPosition() >= moviEnd) {
|
||||||
return C.RESULT_END_OF_INPUT;
|
return C.RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
@ -550,10 +551,10 @@ public class AviExtractor implements Extractor {
|
|||||||
@Override
|
@Override
|
||||||
public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException {
|
public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_READ_SAMPLES:
|
case STATE_READ_CHUNKS:
|
||||||
return readSamples(input);
|
return readChunks(input);
|
||||||
case STATE_SEEK_START:
|
case STATE_SEEK_START:
|
||||||
state = STATE_READ_SAMPLES;
|
state = STATE_READ_CHUNKS;
|
||||||
seekPosition.position = moviOffset + 4;
|
seekPosition.position = moviOffset + 4;
|
||||||
return RESULT_SEEK;
|
return RESULT_SEEK;
|
||||||
case STATE_READ_TRACKS:
|
case STATE_READ_TRACKS:
|
||||||
@ -573,7 +574,7 @@ public class AviExtractor implements Extractor {
|
|||||||
output.seekMap(new SeekMap.Unseekable(getDuration()));
|
output.seekMap(new SeekMap.Unseekable(getDuration()));
|
||||||
}
|
}
|
||||||
seekPosition.position = moviOffset + 4;
|
seekPosition.position = moviOffset + 4;
|
||||||
state = STATE_READ_SAMPLES;
|
state = STATE_READ_CHUNKS;
|
||||||
return RESULT_SEEK;
|
return RESULT_SEEK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -586,20 +587,20 @@ public class AviExtractor implements Extractor {
|
|||||||
chunkHandler = null;
|
chunkHandler = null;
|
||||||
if (position <= 0) {
|
if (position <= 0) {
|
||||||
if (moviOffset != 0) {
|
if (moviOffset != 0) {
|
||||||
resetClocks();
|
setIndexes(new int[chunkHandlers.length]);
|
||||||
state = STATE_SEEK_START;
|
state = STATE_SEEK_START;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (aviSeekMap != null) {
|
if (aviSeekMap != null) {
|
||||||
aviSeekMap.setFrames(position, timeUs, aviTracks);
|
setIndexes(aviSeekMap.getIndexes(position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetClocks() {
|
private void setIndexes(@NonNull int[] indexes) {
|
||||||
for (@Nullable AviTrack aviTrack : aviTracks) {
|
for (@Nullable ChunkHandler chunkHandler : chunkHandlers) {
|
||||||
if (aviTrack != null) {
|
if (chunkHandler != null) {
|
||||||
aviTrack.getClock().setIndex(0);
|
chunkHandler.setIndex(indexes[chunkHandler.getId()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -610,8 +611,8 @@ public class AviExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void setAviTracks(AviTrack[] aviTracks) {
|
void setChunkHandlers(ChunkHandler[] chunkHandlers) {
|
||||||
this.aviTracks = aviTracks;
|
this.chunkHandlers = chunkHandlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
@ -626,13 +627,13 @@ public class AviExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
AviTrack getChunkHandler() {
|
ChunkHandler getChunkHandler() {
|
||||||
return chunkHandler;
|
return chunkHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
void setChunkHandler(final AviTrack aviTrack) {
|
void setChunkHandler(final ChunkHandler chunkHandler) {
|
||||||
chunkHandler = aviTrack;
|
this.chunkHandler = chunkHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
@ -99,17 +99,23 @@ public class AviSeekMap implements SeekMap {
|
|||||||
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) {
|
/**
|
||||||
|
* Get the ChunkClock indexes by stream id
|
||||||
|
* @param position seek position in the file
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public int[] getIndexes(final long position) {
|
||||||
final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2));
|
final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2));
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new IllegalArgumentException("Position: " + position);
|
throw new IllegalArgumentException("Position: " + position);
|
||||||
}
|
}
|
||||||
for (int i=0;i<aviTracks.length;i++) {
|
final int[] indexes = new int[seekIndexes.length];
|
||||||
final AviTrack aviTrack = aviTracks[i];
|
for (int i=0;i<indexes.length;i++) {
|
||||||
final LinearClock clock = aviTrack.getClock();
|
if (seekIndexes[i].length > index) {
|
||||||
clock.setIndex(seekIndexes[i][index]);
|
indexes[i] = seekIndexes[i][index];
|
||||||
// Log.d(AviExtractor.TAG, "Frame: " + (aviTrack.isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " frame=" + clock.getIndex() + " key=" + aviTrack.isKeyFrame());
|
}
|
||||||
}
|
}
|
||||||
|
return indexes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,23 +18,23 @@ package com.google.android.exoplayer2.extractor.avi;
|
|||||||
/**
|
/**
|
||||||
* A clock that is linearly derived from the current chunk index of a given stream
|
* A clock that is linearly derived from the current chunk index of a given stream
|
||||||
*/
|
*/
|
||||||
public class LinearClock {
|
public class ChunkClock {
|
||||||
long durationUs;
|
long durationUs;
|
||||||
int length;
|
int chunks;
|
||||||
|
|
||||||
int index;
|
int index;
|
||||||
|
|
||||||
public LinearClock(long durationUs, int length) {
|
public ChunkClock(long durationUs, int chunks) {
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
this.length = length;
|
this.chunks = chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDuration(long durationUs) {
|
public void setDuration(long durationUs) {
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLength(int length) {
|
public void setChunks(int length) {
|
||||||
this.length = length;
|
this.chunks = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getIndex() {
|
public int getIndex() {
|
||||||
@ -55,6 +55,6 @@ public class LinearClock {
|
|||||||
|
|
||||||
long getUs(int index) {
|
long getUs(int index) {
|
||||||
//Doing this the hard way lessens round errors
|
//Doing this the hard way lessens round errors
|
||||||
return durationUs * index / length;
|
return durationUs * index / chunks;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,37 +16,52 @@
|
|||||||
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.Nullable;
|
import androidx.annotation.VisibleForTesting;
|
||||||
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection of info about a track.
|
* Handles chunk data from a given stream.
|
||||||
* This acts a bridge between AVI and ExoPlayer structures
|
* This acts a bridge between AVI and ExoPlayer
|
||||||
*/
|
*/
|
||||||
public class AviTrack {
|
public class ChunkHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant meaning all frames are considered key frames
|
||||||
|
*/
|
||||||
public static final int[] ALL_KEY_FRAMES = new int[0];
|
public static final int[] ALL_KEY_FRAMES = new int[0];
|
||||||
|
|
||||||
final int id;
|
public static int TYPE_VIDEO = ('d' << 16) | ('c' << 24);
|
||||||
|
public static int TYPE_AUDIO = ('w' << 16) | ('b' << 24);
|
||||||
final @C.TrackType int trackType;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
LinearClock clock;
|
ChunkClock clock;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
final TrackOutput trackOutput;
|
final TrackOutput trackOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chunk id as it appears in the index and the movi
|
||||||
|
*/
|
||||||
final int chunkId;
|
final int chunkId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secondary chunk id. Bad muxers sometimes use uncompressed for key frames
|
||||||
|
*/
|
||||||
final int chunkIdAlt;
|
final int chunkIdAlt;
|
||||||
|
|
||||||
@Nullable
|
/**
|
||||||
ChunkPeeker chunkPeeker;
|
* Number of chunks as calculated by the index
|
||||||
|
*/
|
||||||
int chunks;
|
int chunks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size total size of the stream in bytes calculated by the index
|
||||||
|
*/
|
||||||
int size;
|
int size;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,60 +69,52 @@ public class AviTrack {
|
|||||||
*/
|
*/
|
||||||
int[] keyFrames = new int[0];
|
int[] keyFrames = new int[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the current chunk in bytes
|
||||||
|
*/
|
||||||
transient int chunkSize;
|
transient int chunkSize;
|
||||||
|
/**
|
||||||
|
* Bytes remaining in the chunk to be processed
|
||||||
|
*/
|
||||||
transient int chunkRemaining;
|
transient int chunkRemaining;
|
||||||
|
|
||||||
private static int getChunkIdLower(int id) {
|
/**
|
||||||
|
* Get stream id in ASCII
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static int getChunkIdLower(int id) {
|
||||||
int tens = id / 10;
|
int tens = id / 10;
|
||||||
int ones = id % 10;
|
int ones = id % 10;
|
||||||
return ('0' + tens) | (('0' + ones) << 8);
|
return ('0' + tens) | (('0' + ones) << 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getVideoChunkId(int id) {
|
ChunkHandler(int id, int chunkType, @NonNull TrackOutput trackOutput, @NonNull ChunkClock clock) {
|
||||||
return getChunkIdLower(id) | ('d' << 16) | ('c' << 24);
|
this.chunkId = getChunkIdLower(id) | chunkType;
|
||||||
}
|
|
||||||
|
|
||||||
public static int getAudioChunkId(int id) {
|
|
||||||
return getChunkIdLower(id) | ('w' << 16) | ('b' << 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
AviTrack(int id, @C.TrackType int trackType, @NonNull LinearClock clock,
|
|
||||||
@NonNull TrackOutput trackOutput) {
|
|
||||||
this.id = id;
|
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.trackType = trackType;
|
|
||||||
this.trackOutput = trackOutput;
|
this.trackOutput = trackOutput;
|
||||||
if (isVideo()) {
|
if (isVideo()) {
|
||||||
chunkId = getVideoChunkId(id);
|
|
||||||
chunkIdAlt = getChunkIdLower(id) | ('d' << 16) | ('b' << 24);
|
chunkIdAlt = getChunkIdLower(id) | ('d' << 16) | ('b' << 24);
|
||||||
} else if (isAudio()) {
|
|
||||||
chunkId = getAudioChunkId(id);
|
|
||||||
chunkIdAlt = 0xffff;
|
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown Track Type: " + trackType);
|
chunkIdAlt = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return true if this can handle the chunkId
|
||||||
|
*/
|
||||||
public boolean handlesChunkId(int chunkId) {
|
public boolean handlesChunkId(int chunkId) {
|
||||||
return this.chunkId == chunkId || chunkIdAlt == chunkId;
|
return this.chunkId == chunkId || chunkIdAlt == chunkId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public LinearClock getClock() {
|
public ChunkClock getClock() {
|
||||||
return clock;
|
return clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClock(@NonNull LinearClock clock) {
|
|
||||||
this.clock = clock;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChunkPeeker(ChunkPeeker chunkPeeker) {
|
|
||||||
this.chunkPeeker = chunkPeeker;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Sets the list of key frames
|
||||||
* @param keyFrames null means all key frames
|
* @param keyFrames list of frame indexes or {@link #ALL_KEY_FRAMES}
|
||||||
*/
|
*/
|
||||||
void setKeyFrames(@NonNull final int[] keyFrames) {
|
void setKeyFrames(@NonNull final int[] keyFrames) {
|
||||||
this.keyFrames = keyFrames;
|
this.keyFrames = keyFrames;
|
||||||
@ -118,33 +125,34 @@ public class AviTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVideo() {
|
public boolean isVideo() {
|
||||||
return trackType == C.TRACK_TYPE_VIDEO;
|
return (chunkId & TYPE_VIDEO) == TYPE_VIDEO;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAudio() {
|
public boolean isAudio() {
|
||||||
return trackType == C.TRACK_TYPE_AUDIO;
|
return (chunkId & TYPE_AUDIO) == TYPE_AUDIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
/**
|
||||||
if (chunkPeeker != null) {
|
* Process a new chunk
|
||||||
chunkPeeker.peek(input, size);
|
* @param size total size of the chunk
|
||||||
}
|
* @return True if the chunk has been completely processed. False implies {@link #resume}
|
||||||
final int remaining = size - trackOutput.sampleData(input, size, false);
|
* will be called
|
||||||
if (remaining == 0) {
|
*/
|
||||||
|
public boolean newChunk(int size, @NonNull ExtractorInput input) throws IOException {
|
||||||
|
final int sampled = trackOutput.sampleData(input, size, false);
|
||||||
|
if (sampled == size) {
|
||||||
done(size);
|
done(size);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
chunkSize = size;
|
chunkSize = size;
|
||||||
chunkRemaining = remaining;
|
chunkRemaining = size - sampled;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume a partial read of a chunk
|
* Resume a partial read of a chunk
|
||||||
* @param input
|
* May be called multiple times
|
||||||
* @return
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
*/
|
||||||
boolean resume(ExtractorInput input) throws IOException {
|
boolean resume(ExtractorInput input) throws IOException {
|
||||||
chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false);
|
chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false);
|
||||||
@ -157,16 +165,31 @@ public class AviTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Done reading a chunk
|
* Done reading a chunk. Send the timing info and advance the clock
|
||||||
* @param size
|
* @param size the amount of data passed to the trackOutput
|
||||||
*/
|
*/
|
||||||
void done(final int size) {
|
void done(final int size) {
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
final LinearClock clock = getClock();
|
|
||||||
//Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame());
|
//Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame());
|
||||||
clock.advance();
|
clock.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the streamId.
|
||||||
|
* @return The unique stream id for this file
|
||||||
|
*/
|
||||||
|
public int getId() {
|
||||||
|
return ((chunkId >> 8) & 0xf) + (chunkId & 0xf) * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A seek occurred
|
||||||
|
* @param index of the chunk
|
||||||
|
*/
|
||||||
|
public void setIndex(int index) {
|
||||||
|
getClock().setIndex(index);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Peeks for import data in the chunk stream.
|
|
||||||
*/
|
|
||||||
public interface ChunkPeeker {
|
|
||||||
void peek(ExtractorInput input, final int size) throws IOException;
|
|
||||||
}
|
|
@ -26,7 +26,7 @@ import java.io.IOException;
|
|||||||
/**
|
/**
|
||||||
* Peeks an MP4V stream looking for pixelWidthHeightRatio data
|
* Peeks an MP4V stream looking for pixelWidthHeightRatio data
|
||||||
*/
|
*/
|
||||||
public class Mp4vChunkPeeker extends NalChunkPeeker {
|
public class Mp4vChunkHandler extends NalChunkHandler {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final byte SEQUENCE_START_CODE = (byte)0xb0;
|
static final byte SEQUENCE_START_CODE = (byte)0xb0;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@ -36,15 +36,14 @@ public class Mp4vChunkPeeker extends NalChunkPeeker {
|
|||||||
static final int Extended_PAR = 0xf;
|
static final int Extended_PAR = 0xf;
|
||||||
|
|
||||||
private final Format.Builder formatBuilder;
|
private final Format.Builder formatBuilder;
|
||||||
private final TrackOutput trackOutput;
|
|
||||||
|
|
||||||
@VisibleForTesting()
|
@VisibleForTesting()
|
||||||
float pixelWidthHeightRatio = 1f;
|
float pixelWidthHeightRatio = 1f;
|
||||||
|
|
||||||
public Mp4vChunkPeeker(@NonNull Format.Builder formatBuilder, @NonNull TrackOutput trackOutput) {
|
public Mp4vChunkHandler(int id, @NonNull TrackOutput trackOutput,
|
||||||
super(5);
|
@NonNull ChunkClock clock, @NonNull Format.Builder formatBuilder) {
|
||||||
|
super(id, trackOutput, clock, 5);
|
||||||
this.formatBuilder = formatBuilder;
|
this.formatBuilder = formatBuilder;
|
||||||
this.trackOutput = trackOutput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.audio.MpegAudioUtil;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves several issues with Mpeg Audio
|
||||||
|
* 1. That muxers don't always mux MPEG audio on the frame boundary
|
||||||
|
* 2. That some codecs can't handle multiple or partial frames (Pixels)
|
||||||
|
*/
|
||||||
|
public class MpegAudioChunkHandler extends ChunkHandler {
|
||||||
|
private final MpegAudioUtil.Header header = new MpegAudioUtil.Header();
|
||||||
|
private final ParsableByteArray scratch = new ParsableByteArray(8);
|
||||||
|
private final int samplesPerSecond;
|
||||||
|
//Bytes remaining in the Mpeg Audio frame
|
||||||
|
private int frameRemaining;
|
||||||
|
private long timeUs = 0L;
|
||||||
|
|
||||||
|
MpegAudioChunkHandler(int id, @NonNull TrackOutput trackOutput, @NonNull ChunkClock clock,
|
||||||
|
int samplesPerSecond) {
|
||||||
|
super(id, TYPE_AUDIO, trackOutput, clock);
|
||||||
|
this.samplesPerSecond = samplesPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean newChunk(int size, @NonNull ExtractorInput input) throws IOException {
|
||||||
|
if (size == 0) {
|
||||||
|
//Empty frame, advance the clock and sync
|
||||||
|
clock.advance();
|
||||||
|
syncTime();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.size = chunkRemaining = size;
|
||||||
|
return resume(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean resume(@NonNull ExtractorInput input) throws IOException {
|
||||||
|
if (process(input)) {
|
||||||
|
// Fail Over: If the scratch is the entire chunk, we didn't find a MP3 header.
|
||||||
|
// Dump the chunk as is and hope the decoder can handle it.
|
||||||
|
if (scratch.limit() == size) {
|
||||||
|
scratch.setPosition(0);
|
||||||
|
trackOutput.sampleData(scratch, size);
|
||||||
|
scratch.reset(0);
|
||||||
|
done(size);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read from input to scratch
|
||||||
|
* @param bytes to attempt to read
|
||||||
|
* @return {@link C#RESULT_END_OF_INPUT} or number of bytes read.
|
||||||
|
*/
|
||||||
|
int readScratch(ExtractorInput input, int bytes) throws IOException {
|
||||||
|
final int toRead = Math.min(bytes, chunkRemaining);
|
||||||
|
final int read = input.read(scratch.getData(), scratch.limit(), toRead);
|
||||||
|
if (read == C.RESULT_END_OF_INPUT) {
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
chunkRemaining -= read;
|
||||||
|
scratch.setLimit(scratch.limit() + read);
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to find a frame header in the input
|
||||||
|
* @return true if a frame header was found
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
boolean findFrame(ExtractorInput input) throws IOException {
|
||||||
|
scratch.reset(0);
|
||||||
|
scratch.ensureCapacity(scratch.limit() + chunkRemaining);
|
||||||
|
int toRead = 4;
|
||||||
|
while (chunkRemaining > 0 && readScratch(input, toRead) != C.RESULT_END_OF_INPUT) {
|
||||||
|
while (scratch.bytesLeft() >= 4) {
|
||||||
|
if (header.setForHeaderData(scratch.readInt())) {
|
||||||
|
scratch.skipBytes(-4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
scratch.skipBytes(-3);
|
||||||
|
}
|
||||||
|
// 16 is small, but if we end up reading multiple frames into scratch, things get complicated.
|
||||||
|
// We should only loop on seek, so this is the lesser of the evils.
|
||||||
|
toRead = Math.min(chunkRemaining, 16);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the chunk by breaking it in Mpeg audio frames
|
||||||
|
* @return true if the chunk has been completely processed
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
boolean process(ExtractorInput input) throws IOException {
|
||||||
|
if (frameRemaining == 0) {
|
||||||
|
//Find the next frame
|
||||||
|
if (findFrame(input)) {
|
||||||
|
final int scratchBytes = scratch.bytesLeft();
|
||||||
|
trackOutput.sampleData(scratch, scratchBytes);
|
||||||
|
frameRemaining = header.frameSize - scratchBytes;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int bytes = trackOutput.sampleData(input, Math.min(frameRemaining, chunkRemaining), false);
|
||||||
|
frameRemaining -= bytes;
|
||||||
|
if (frameRemaining == 0) {
|
||||||
|
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, header.frameSize, 0, null);
|
||||||
|
//Log.d(AviExtractor.TAG, "MP3: us=" + us);
|
||||||
|
timeUs += header.samplesPerFrame * C.MICROS_PER_SECOND / samplesPerSecond;
|
||||||
|
}
|
||||||
|
chunkRemaining -= bytes;
|
||||||
|
return chunkRemaining == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIndex(int index) {
|
||||||
|
super.setIndex(index);
|
||||||
|
syncTime();
|
||||||
|
if (frameRemaining != 0) {
|
||||||
|
// We have a partial frame in the output, no way to clear it, so just send it as is.
|
||||||
|
// Next frame should be key frame, so the codec should recover.
|
||||||
|
trackOutput.sampleMetadata(timeUs, 0, header.frameSize - frameRemaining,
|
||||||
|
0, null);
|
||||||
|
frameRemaining = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncTime() {
|
||||||
|
timeUs = clock.getUs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
long getTimeUs() {
|
||||||
|
return timeUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
int getFrameRemaining() {
|
||||||
|
return frameRemaining;
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ import java.util.Arrays;
|
|||||||
* Generic base class for NAL (0x00 0x00 0x01) chunk headers
|
* Generic base class for NAL (0x00 0x00 0x01) chunk headers
|
||||||
* Theses are used by AVC and MP4V (XVID)
|
* Theses are used by AVC and MP4V (XVID)
|
||||||
*/
|
*/
|
||||||
public abstract class NalChunkPeeker implements ChunkPeeker {
|
public abstract class NalChunkHandler extends ChunkHandler {
|
||||||
private static final int SEEK_PEEK_SIZE = 256;
|
private static final int SEEK_PEEK_SIZE = 256;
|
||||||
private final int peekSize;
|
private final int peekSize;
|
||||||
|
|
||||||
@ -31,6 +33,15 @@ public abstract class NalChunkPeeker implements ChunkPeeker {
|
|||||||
transient byte[] buffer;
|
transient byte[] buffer;
|
||||||
transient int pos;
|
transient int pos;
|
||||||
|
|
||||||
|
NalChunkHandler(int id, @NonNull TrackOutput trackOutput,
|
||||||
|
@NonNull ChunkClock clock, int peakSize) {
|
||||||
|
super(id, TYPE_VIDEO, trackOutput, clock);
|
||||||
|
if (peakSize < 5) {
|
||||||
|
throw new IllegalArgumentException("Peak size must at least be 5");
|
||||||
|
}
|
||||||
|
this.peekSize = peakSize;
|
||||||
|
}
|
||||||
|
|
||||||
abstract void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException;
|
abstract void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,15 +111,13 @@ public abstract class NalChunkPeeker implements ChunkPeeker {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NalChunkPeeker(int peakSize) {
|
|
||||||
if (peakSize < 5) {
|
|
||||||
throw new IllegalArgumentException("Peak size must at least be 5");
|
|
||||||
}
|
|
||||||
this.peekSize = peakSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract boolean skip(byte nalType);
|
abstract boolean skip(byte nalType);
|
||||||
|
|
||||||
|
public boolean newChunk(int size, ExtractorInput input) throws IOException {
|
||||||
|
peek(input, size);
|
||||||
|
return super.newChunk(size, input);
|
||||||
|
}
|
||||||
|
|
||||||
public void peek(ExtractorInput input, final int size) throws IOException {
|
public void peek(ExtractorInput input, final int size) throws IOException {
|
||||||
buffer = new byte[peekSize];
|
buffer = new byte[peekSize];
|
||||||
if (!input.peekFully(buffer, 0, peekSize, true)) {
|
if (!input.peekFully(buffer, 0, peekSize, true)) {
|
@ -20,7 +20,7 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
/**
|
/**
|
||||||
* Properly calculates the frame time for H264 frames using PicCount
|
* Properly calculates the frame time for H264 frames using PicCount
|
||||||
*/
|
*/
|
||||||
public class PicCountClock extends LinearClock {
|
public class PicCountClock extends ChunkClock {
|
||||||
//The frame as a calculated from the picCount
|
//The frame as a calculated from the picCount
|
||||||
private int picIndex;
|
private int picIndex;
|
||||||
private int lastPicCount;
|
private int lastPicCount;
|
||||||
|
@ -16,13 +16,6 @@
|
|||||||
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.Nullable;
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
|
||||||
import com.google.android.exoplayer2.util.Log;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.nio.BufferOverflowException;
|
|
||||||
import java.nio.BufferUnderflowException;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
@ -39,7 +32,6 @@ public class ResidentBox extends Box {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns shallow copy of this ByteBuffer with the position at 0
|
* Returns shallow copy of this ByteBuffer with the position at 0
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public ByteBuffer getByteBuffer() {
|
public ByteBuffer getByteBuffer() {
|
||||||
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer2.extractor.avi;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Human readable stream name
|
* Box containing a human readable stream name
|
||||||
*/
|
*/
|
||||||
public class StreamNameBox extends ResidentBox {
|
public class StreamNameBox extends ResidentBox {
|
||||||
public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24);
|
public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24);
|
||||||
|
@ -33,6 +33,7 @@ public final class NalUnitUtil {
|
|||||||
public final int constraintsFlagsAndReservedZero2Bits;
|
public final int constraintsFlagsAndReservedZero2Bits;
|
||||||
public final int levelIdc;
|
public final int levelIdc;
|
||||||
public final int seqParameterSetId;
|
public final int seqParameterSetId;
|
||||||
|
public final int maxNumRefFrames;
|
||||||
public final int width;
|
public final int width;
|
||||||
public final int height;
|
public final int height;
|
||||||
public final float pixelWidthHeightRatio;
|
public final float pixelWidthHeightRatio;
|
||||||
@ -48,6 +49,7 @@ public final class NalUnitUtil {
|
|||||||
int constraintsFlagsAndReservedZero2Bits,
|
int constraintsFlagsAndReservedZero2Bits,
|
||||||
int levelIdc,
|
int levelIdc,
|
||||||
int seqParameterSetId,
|
int seqParameterSetId,
|
||||||
|
int maxNumRefFrames,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
float pixelWidthHeightRatio,
|
float pixelWidthHeightRatio,
|
||||||
@ -61,6 +63,7 @@ public final class NalUnitUtil {
|
|||||||
this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits;
|
this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits;
|
||||||
this.levelIdc = levelIdc;
|
this.levelIdc = levelIdc;
|
||||||
this.seqParameterSetId = seqParameterSetId;
|
this.seqParameterSetId = seqParameterSetId;
|
||||||
|
this.maxNumRefFrames = maxNumRefFrames;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
|
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
|
||||||
@ -367,7 +370,7 @@ public final class NalUnitUtil {
|
|||||||
data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]
|
data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames
|
int maxNumRefFrames = data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames
|
||||||
data.skipBit(); // gaps_in_frame_num_value_allowed_flag
|
data.skipBit(); // gaps_in_frame_num_value_allowed_flag
|
||||||
|
|
||||||
int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;
|
int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;
|
||||||
@ -427,6 +430,7 @@ public final class NalUnitUtil {
|
|||||||
constraintsFlagsAndReservedZero2Bits,
|
constraintsFlagsAndReservedZero2Bits,
|
||||||
levelIdc,
|
levelIdc,
|
||||||
seqParameterSetId,
|
seqParameterSetId,
|
||||||
|
maxNumRefFrames,
|
||||||
frameWidth,
|
frameWidth,
|
||||||
frameHeight,
|
frameHeight,
|
||||||
pixelWidthHeightRatio,
|
pixelWidthHeightRatio,
|
||||||
|
@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.avi.AviExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
|
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor;
|
import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor;
|
||||||
@ -69,6 +70,7 @@ public final class DefaultExtractorsFactoryTest {
|
|||||||
AdtsExtractor.class,
|
AdtsExtractor.class,
|
||||||
Ac3Extractor.class,
|
Ac3Extractor.class,
|
||||||
Ac4Extractor.class,
|
Ac4Extractor.class,
|
||||||
|
AviExtractor.class,
|
||||||
Mp3Extractor.class,
|
Mp3Extractor.class,
|
||||||
JpegExtractor.class)
|
JpegExtractor.class)
|
||||||
.inOrder();
|
.inOrder();
|
||||||
@ -112,6 +114,7 @@ public final class DefaultExtractorsFactoryTest {
|
|||||||
AdtsExtractor.class,
|
AdtsExtractor.class,
|
||||||
Ac3Extractor.class,
|
Ac3Extractor.class,
|
||||||
Ac4Extractor.class,
|
Ac4Extractor.class,
|
||||||
|
AviExtractor.class,
|
||||||
JpegExtractor.class)
|
JpegExtractor.class)
|
||||||
.inOrder();
|
.inOrder();
|
||||||
}
|
}
|
||||||
|
@ -35,17 +35,17 @@ public class AvcChunkPeekerTest {
|
|||||||
setSampleMimeType(MimeTypes.VIDEO_H264).
|
setSampleMimeType(MimeTypes.VIDEO_H264).
|
||||||
setWidth(1280).setHeight(720).setFrameRate(24000f/1001f);
|
setWidth(1280).setHeight(720).setFrameRate(24000f/1001f);
|
||||||
|
|
||||||
private static final byte[] P_SLICE = {00,00,00,01,0x41,(byte)0x9A,0x13,0x36,0x21,0x3A,0x5F,
|
private static final byte[] P_SLICE = {0,0,0,1,0x41,(byte)0x9A,0x13,0x36,0x21,0x3A,0x5F,
|
||||||
(byte)0xFE,(byte)0x9E,0x10,00,00};
|
(byte)0xFE,(byte)0x9E,0x10,0,0};
|
||||||
|
|
||||||
private FakeTrackOutput fakeTrackOutput;
|
private FakeTrackOutput fakeTrackOutput;
|
||||||
private AvcChunkPeeker avcChunkPeeker;
|
private AvcChunkHandler avcChunkHandler;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
fakeTrackOutput = new FakeTrackOutput(false);
|
fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
avcChunkPeeker = new AvcChunkPeeker(FORMAT_BUILDER_AVC, fakeTrackOutput,
|
avcChunkHandler = new AvcChunkHandler(0, fakeTrackOutput,
|
||||||
new LinearClock(10_000_000L, 24 * 10));
|
new ChunkClock(10_000_000L, 24 * 10), FORMAT_BUILDER_AVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void peekStreamHeader() throws IOException {
|
private void peekStreamHeader() throws IOException {
|
||||||
@ -55,25 +55,26 @@ public class AvcChunkPeekerTest {
|
|||||||
|
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build();
|
||||||
|
|
||||||
avcChunkPeeker.peek(input, bytes.length);
|
avcChunkHandler.peek(input, bytes.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void peek_givenStreamHeader() throws IOException {
|
public void peek_givenStreamHeader() throws IOException {
|
||||||
peekStreamHeader();
|
peekStreamHeader();
|
||||||
final PicCountClock picCountClock = avcChunkPeeker.getClock();
|
final PicCountClock picCountClock = avcChunkHandler.getPicCountClock();
|
||||||
|
Assert.assertNotNull(picCountClock);
|
||||||
Assert.assertEquals(64, picCountClock.getMaxPicCount());
|
Assert.assertEquals(64, picCountClock.getMaxPicCount());
|
||||||
Assert.assertEquals(0, avcChunkPeeker.getSpsData().picOrderCountType);
|
Assert.assertEquals(0, avcChunkHandler.getSpsData().picOrderCountType);
|
||||||
Assert.assertEquals(1.18f, fakeTrackOutput.lastFormat.pixelWidthHeightRatio, 0.01f);
|
Assert.assertEquals(1.18f, fakeTrackOutput.lastFormat.pixelWidthHeightRatio, 0.01f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void peek_givenStreamHeaderAndPSlice() throws IOException {
|
public void newChunk_givenStreamHeaderAndPSlice() throws IOException {
|
||||||
peekStreamHeader();
|
peekStreamHeader();
|
||||||
final PicCountClock picCountClock = avcChunkPeeker.getClock();
|
final PicCountClock picCountClock = avcChunkHandler.getPicCountClock();
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build();
|
||||||
|
|
||||||
avcChunkPeeker.peek(input, P_SLICE.length);
|
avcChunkHandler.newChunk(P_SLICE.length, input);
|
||||||
|
|
||||||
Assert.assertEquals(12, picCountClock.getLastPicCount());
|
Assert.assertEquals(12, picCountClock.getLastPicCount());
|
||||||
}
|
}
|
||||||
|
@ -108,26 +108,26 @@ public class AviExtractorRoboTest {
|
|||||||
|
|
||||||
Assert.assertEquals(AviExtractor.STATE_FIND_MOVI, aviExtractor.state);
|
Assert.assertEquals(AviExtractor.STATE_FIND_MOVI, aviExtractor.state);
|
||||||
|
|
||||||
final AviTrack aviTrack = aviExtractor.getVideoTrack();
|
final ChunkHandler chunkHandler = aviExtractor.getVideoTrack();
|
||||||
Assert.assertEquals(aviTrack.getClock().durationUs, streamHeaderBox.getDurationUs());
|
Assert.assertEquals(chunkHandler.getClock().durationUs, streamHeaderBox.getDurationUs());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void readSamples_fragmentedChunk() throws IOException {
|
public void readSamples_fragmentedChunk() throws IOException {
|
||||||
AviExtractor aviExtractor = AviExtractorTest.setupVideoAviExtractor();
|
AviExtractor aviExtractor = AviExtractorTest.setupVideoAviExtractor();
|
||||||
final AviTrack aviTrack = aviExtractor.getVideoTrack();
|
final ChunkHandler chunkHandler = aviExtractor.getVideoTrack();
|
||||||
final int size = 24 + 16;
|
final int size = 24 + 16;
|
||||||
final ByteBuffer byteBuffer = AviExtractor.allocate(size + 8);
|
final ByteBuffer byteBuffer = AviExtractor.allocate(size + 8);
|
||||||
byteBuffer.putInt(aviTrack.chunkId);
|
byteBuffer.putInt(chunkHandler.chunkId);
|
||||||
byteBuffer.putInt(size);
|
byteBuffer.putInt(size);
|
||||||
|
|
||||||
final ExtractorInput chunk = new FakeExtractorInput.Builder().setData(byteBuffer.array()).
|
final ExtractorInput chunk = new FakeExtractorInput.Builder().setData(byteBuffer.array()).
|
||||||
setSimulatePartialReads(true).build();
|
setSimulatePartialReads(true).build();
|
||||||
Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder()));
|
Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder()));
|
||||||
|
|
||||||
Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(chunk, new PositionHolder()));
|
Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder()));
|
||||||
|
|
||||||
final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput;
|
final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput;
|
||||||
Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length);
|
Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,7 +165,7 @@ 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,
|
private void assertIdx1(AviSeekMap aviSeekMap, ChunkHandler videoTrack, int keyFrames,
|
||||||
int keyFrameRate) {
|
int keyFrameRate) {
|
||||||
Assert.assertEquals(keyFrames, videoTrack.keyFrames.length);
|
Assert.assertEquals(keyFrames, videoTrack.keyFrames.length);
|
||||||
|
|
||||||
@ -192,9 +192,9 @@ public class AviExtractorTest {
|
|||||||
final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds
|
final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds
|
||||||
final int keyFrames = secs * DataHelper.FPS / keyFrameRate;
|
final int keyFrames = secs * DataHelper.FPS / keyFrameRate;
|
||||||
final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate);
|
final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate);
|
||||||
final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs);
|
final ChunkHandler videoTrack = DataHelper.getVideoChunkHandler(secs);
|
||||||
final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs);
|
final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs);
|
||||||
aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack});
|
aviExtractor.setChunkHandlers(new ChunkHandler[]{videoTrack, audioTrack});
|
||||||
aviExtractor.setAviHeader(DataHelper.createAviHeaderBox());
|
aviExtractor.setAviHeader(DataHelper.createAviHeaderBox());
|
||||||
aviExtractor.state = AviExtractor.STATE_READ_IDX1;
|
aviExtractor.state = AviExtractor.STATE_READ_IDX1;
|
||||||
aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024);
|
aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024);
|
||||||
@ -213,7 +213,7 @@ public class AviExtractorTest {
|
|||||||
final AviSeekMap aviSeekMap = aviExtractor.aviSeekMap;
|
final AviSeekMap aviSeekMap = aviExtractor.aviSeekMap;
|
||||||
assertIdx1(aviSeekMap, videoTrack, keyFrames, keyFrameRate);
|
assertIdx1(aviSeekMap, videoTrack, keyFrames, keyFrameRate);
|
||||||
|
|
||||||
Assert.assertEquals(AviExtractor.STATE_READ_SAMPLES, aviExtractor.state);
|
Assert.assertEquals(AviExtractor.STATE_READ_CHUNKS, aviExtractor.state);
|
||||||
Assert.assertEquals(DataHelper.MOVI_OFFSET + 4, positionHolder.position);
|
Assert.assertEquals(DataHelper.MOVI_OFFSET + 4, positionHolder.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,8 +225,8 @@ public class AviExtractorTest {
|
|||||||
final int secs = 9;
|
final int secs = 9;
|
||||||
final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds
|
final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds
|
||||||
final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate);
|
final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate);
|
||||||
final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs);
|
final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs);
|
||||||
aviExtractor.setAviTracks(new AviTrack[]{audioTrack});
|
aviExtractor.setChunkHandlers(new ChunkHandler[]{audioTrack});
|
||||||
|
|
||||||
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder()
|
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder()
|
||||||
.setData(idx1.array()).build();
|
.setData(idx1.array()).build();
|
||||||
@ -250,9 +250,9 @@ public class AviExtractorTest {
|
|||||||
junk.putInt(0);
|
junk.putInt(0);
|
||||||
idx1.flip();
|
idx1.flip();
|
||||||
junk.put(idx1);
|
junk.put(idx1);
|
||||||
final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs);
|
final ChunkHandler videoTrack = DataHelper.getVideoChunkHandler(secs);
|
||||||
final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs);
|
final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs);
|
||||||
aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack});
|
aviExtractor.setChunkHandlers(new ChunkHandler[]{videoTrack, audioTrack});
|
||||||
|
|
||||||
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().
|
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().
|
||||||
setData(junk.array()).build();
|
setData(junk.array()).build();
|
||||||
@ -268,16 +268,16 @@ public class AviExtractorTest {
|
|||||||
aviExtractor.init(fakeExtractorOutput);
|
aviExtractor.init(fakeExtractorOutput);
|
||||||
final int secs = 4;
|
final int secs = 4;
|
||||||
final ByteBuffer idx1 = DataHelper.getIndex(secs, 1);
|
final ByteBuffer idx1 = DataHelper.getIndex(secs, 1);
|
||||||
final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs);
|
final ChunkHandler videoTrack = DataHelper.getVideoChunkHandler(secs);
|
||||||
final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs);
|
final ChunkHandler audioTrack = DataHelper.getAudioChunkHandler(secs);
|
||||||
aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack});
|
aviExtractor.setChunkHandlers(new ChunkHandler[]{videoTrack, audioTrack});
|
||||||
|
|
||||||
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().
|
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().
|
||||||
setData(idx1.array()).build();
|
setData(idx1.array()).build();
|
||||||
aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength());
|
aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength());
|
||||||
|
|
||||||
//We should be throttled to 2 key frame per second
|
//We should be throttled to 2 key frame per second
|
||||||
Assert.assertSame(AviTrack.ALL_KEY_FRAMES, videoTrack.keyFrames);
|
Assert.assertSame(ChunkHandler.ALL_KEY_FRAMES, videoTrack.keyFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -389,12 +389,12 @@ public class AviExtractorTest {
|
|||||||
final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
|
final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
|
||||||
aviExtractor.init(fakeExtractorOutput);
|
aviExtractor.init(fakeExtractorOutput);
|
||||||
|
|
||||||
final AviTrack aviTrack = DataHelper.getVideoAviTrack(9);
|
final ChunkHandler chunkHandler = DataHelper.getVideoChunkHandler(9);
|
||||||
aviExtractor.setAviTracks(new AviTrack[]{aviTrack});
|
aviExtractor.setChunkHandlers(new ChunkHandler[]{chunkHandler});
|
||||||
final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build();
|
final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build();
|
||||||
aviTrack.trackOutput.format(format);
|
chunkHandler.trackOutput.format(format);
|
||||||
|
|
||||||
aviExtractor.state = AviExtractor.STATE_READ_SAMPLES;
|
aviExtractor.state = AviExtractor.STATE_READ_CHUNKS;
|
||||||
aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024);
|
aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024);
|
||||||
return aviExtractor;
|
return aviExtractor;
|
||||||
}
|
}
|
||||||
@ -403,9 +403,9 @@ public class AviExtractorTest {
|
|||||||
public void readSamples_givenAtEndOfInput() throws IOException {
|
public void readSamples_givenAtEndOfInput() throws IOException {
|
||||||
AviExtractor aviExtractor = setupVideoAviExtractor();
|
AviExtractor aviExtractor = setupVideoAviExtractor();
|
||||||
aviExtractor.setMovi(0, 0);
|
aviExtractor.setMovi(0, 0);
|
||||||
final AviTrack aviTrack = aviExtractor.getVideoTrack();
|
final ChunkHandler chunkHandler = aviExtractor.getVideoTrack();
|
||||||
final ByteBuffer byteBuffer = AviExtractor.allocate(32);
|
final ByteBuffer byteBuffer = AviExtractor.allocate(32);
|
||||||
byteBuffer.putInt(aviTrack.chunkId);
|
byteBuffer.putInt(chunkHandler.chunkId);
|
||||||
byteBuffer.putInt(24);
|
byteBuffer.putInt(24);
|
||||||
|
|
||||||
final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build();
|
final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build();
|
||||||
@ -415,47 +415,47 @@ public class AviExtractorTest {
|
|||||||
@Test
|
@Test
|
||||||
public void readSamples_completeChunk() throws IOException {
|
public void readSamples_completeChunk() throws IOException {
|
||||||
AviExtractor aviExtractor = setupVideoAviExtractor();
|
AviExtractor aviExtractor = setupVideoAviExtractor();
|
||||||
final AviTrack aviTrack = aviExtractor.getVideoTrack();
|
final ChunkHandler chunkHandler = aviExtractor.getVideoTrack();
|
||||||
final ByteBuffer byteBuffer = AviExtractor.allocate(32);
|
final ByteBuffer byteBuffer = AviExtractor.allocate(32);
|
||||||
byteBuffer.putInt(aviTrack.chunkId);
|
byteBuffer.putInt(chunkHandler.chunkId);
|
||||||
byteBuffer.putInt(24);
|
byteBuffer.putInt(24);
|
||||||
|
|
||||||
final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
||||||
.build();
|
.build();
|
||||||
Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder()));
|
Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(input, new PositionHolder()));
|
||||||
|
|
||||||
final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput;
|
final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput;
|
||||||
Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length);
|
Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void readSamples_givenLeadingZeros() throws IOException {
|
public void readSamples_givenLeadingZeros() throws IOException {
|
||||||
AviExtractor aviExtractor = setupVideoAviExtractor();
|
AviExtractor aviExtractor = setupVideoAviExtractor();
|
||||||
final AviTrack aviTrack = aviExtractor.getVideoTrack();
|
final ChunkHandler chunkHandler = aviExtractor.getVideoTrack();
|
||||||
final ByteBuffer byteBuffer = AviExtractor.allocate(48);
|
final ByteBuffer byteBuffer = AviExtractor.allocate(48);
|
||||||
byteBuffer.position(16);
|
byteBuffer.position(16);
|
||||||
byteBuffer.putInt(aviTrack.chunkId);
|
byteBuffer.putInt(chunkHandler.chunkId);
|
||||||
byteBuffer.putInt(24);
|
byteBuffer.putInt(24);
|
||||||
|
|
||||||
final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
||||||
.build();
|
.build();
|
||||||
Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder()));
|
Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(input, new PositionHolder()));
|
||||||
|
|
||||||
final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput;
|
final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput;
|
||||||
Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length);
|
Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void seek_givenPosition0() throws IOException {
|
public void seek_givenPosition0() throws IOException {
|
||||||
final AviExtractor aviExtractor = setupVideoAviExtractor();
|
final AviExtractor aviExtractor = setupVideoAviExtractor();
|
||||||
final AviTrack aviTrack = aviExtractor.getVideoTrack();
|
final ChunkHandler chunkHandler = aviExtractor.getVideoTrack();
|
||||||
aviExtractor.setChunkHandler(aviTrack);
|
aviExtractor.setChunkHandler(chunkHandler);
|
||||||
aviTrack.getClock().setIndex(10);
|
chunkHandler.getClock().setIndex(10);
|
||||||
|
|
||||||
aviExtractor.seek(0L, 0L);
|
aviExtractor.seek(0L, 0L);
|
||||||
|
|
||||||
Assert.assertNull(aviExtractor.getChunkHandler());
|
Assert.assertNull(aviExtractor.getChunkHandler());
|
||||||
Assert.assertEquals(0, aviTrack.getClock().getIndex());
|
Assert.assertEquals(0, chunkHandler.getClock().getIndex());
|
||||||
Assert.assertEquals(aviExtractor.state, AviExtractor.STATE_SEEK_START);
|
Assert.assertEquals(aviExtractor.state, AviExtractor.STATE_SEEK_START);
|
||||||
|
|
||||||
|
|
||||||
@ -470,18 +470,18 @@ public class AviExtractorTest {
|
|||||||
final AviExtractor aviExtractor = setupVideoAviExtractor();
|
final AviExtractor aviExtractor = setupVideoAviExtractor();
|
||||||
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
||||||
aviExtractor.aviSeekMap = aviSeekMap;
|
aviExtractor.aviSeekMap = aviSeekMap;
|
||||||
final AviTrack aviTrack = aviExtractor.getVideoTrack();
|
final ChunkHandler chunkHandler = aviExtractor.getVideoTrack();
|
||||||
final long position = DataHelper.MOVI_OFFSET + aviSeekMap.keyFrameOffsetsDiv2[1] * 2L;
|
final long position = DataHelper.MOVI_OFFSET + aviSeekMap.keyFrameOffsetsDiv2[1] * 2L;
|
||||||
aviExtractor.seek(position, 0L);
|
aviExtractor.seek(position, 0L);
|
||||||
Assert.assertEquals(aviSeekMap.seekIndexes[aviTrack.id][1], aviTrack.getClock().getIndex());
|
Assert.assertEquals(aviSeekMap.seekIndexes[chunkHandler.getId()][1], chunkHandler.getClock().getIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getAviTrack_givenListWithNull() {
|
public void getChunkHandler_givenListWithNull() {
|
||||||
final AviExtractor aviExtractor = new AviExtractor();
|
final AviExtractor aviExtractor = new AviExtractor();
|
||||||
final AviTrack aviTrack = DataHelper.getAudioAviTrack(9);
|
final ChunkHandler chunkHandler = DataHelper.getAudioChunkHandler(9);
|
||||||
aviExtractor.setAviTracks(new AviTrack[]{null, aviTrack});
|
aviExtractor.setChunkHandlers(new ChunkHandler[]{null, chunkHandler});
|
||||||
Assert.assertSame(aviTrack, aviExtractor.getAviTrack(aviTrack.chunkId));
|
Assert.assertSame(chunkHandler, aviExtractor.getChunkHandler(chunkHandler.chunkId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -15,7 +15,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.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -23,26 +22,25 @@ import org.junit.Test;
|
|||||||
public class AviSeekMapTest {
|
public class AviSeekMapTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setFrames_givenExactSeekPointMatch() {
|
public void getFrames_givenExactSeekPointMatch() {
|
||||||
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
||||||
final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.seekOffset;
|
final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.seekOffset;
|
||||||
final int secs = 4;
|
final int secs = 4;
|
||||||
final AviTrack[] aviTracks = new AviTrack[]{DataHelper.getVideoAviTrack(secs),
|
final ChunkHandler[] chunkHandlers = new ChunkHandler[]{DataHelper.getVideoChunkHandler(secs),
|
||||||
DataHelper.getAudioAviTrack(secs)};
|
DataHelper.getAudioChunkHandler(secs)};
|
||||||
|
|
||||||
aviSeekMap.setFrames(position, C.MICROS_PER_SECOND, aviTracks);
|
int[] indexes = aviSeekMap.getIndexes(position);
|
||||||
for (int i=0;i<aviTracks.length;i++) {
|
for (int i=0;i<chunkHandlers.length;i++) {
|
||||||
Assert.assertEquals(aviSeekMap.seekIndexes[i][1], aviTracks[i].getClock().getIndex());
|
Assert.assertEquals(aviSeekMap.seekIndexes[i][1], indexes[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setFrames_givenBadPosition() {
|
public void setFrames_givenBadPosition() {
|
||||||
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
||||||
final AviTrack[] aviTracks = new AviTrack[2];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
aviSeekMap.setFrames(1L, C.MICROS_PER_SECOND, aviTracks);
|
aviSeekMap.getIndexes(1L);
|
||||||
Assert.fail();
|
Assert.fail();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
//Intentionally blank
|
//Intentionally blank
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
|
||||||
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
public class AviTrackTest {
|
|
||||||
@Test
|
|
||||||
public void setClock_givenLinearClock() {
|
|
||||||
final LinearClock linearClock = new LinearClock(1_000_000L, 30);
|
|
||||||
final AviTrack aviTrack = DataHelper.getVideoAviTrack(1);
|
|
||||||
aviTrack.setClock(linearClock);
|
|
||||||
|
|
||||||
Assert.assertSame(linearClock, aviTrack.getClock());
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.avi;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -76,7 +75,7 @@ public class DataHelper {
|
|||||||
final ArrayList<Box> list = new ArrayList<>(2);
|
final ArrayList<Box> list = new ArrayList<>(2);
|
||||||
list.add(streamHeaderBox);
|
list.add(streamHeaderBox);
|
||||||
list.add(streamFormatBox);
|
list.add(streamFormatBox);
|
||||||
return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()),
|
return new ListBox(streamHeaderBox.getSize() + streamFormatBox.getSize(),
|
||||||
ListBox.TYPE_STRL, list);
|
ListBox.TYPE_STRL, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,18 +103,16 @@ public class DataHelper {
|
|||||||
return byteBuffer;
|
return byteBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AviTrack getVideoAviTrack(int sec) {
|
public static ChunkHandler getVideoChunkHandler(int sec) {
|
||||||
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
return new AviTrack(0, C.TRACK_TYPE_VIDEO,
|
return new ChunkHandler(0, ChunkHandler.TYPE_VIDEO, fakeTrackOutput,
|
||||||
new LinearClock(sec * 1_000_000L, sec * FPS),
|
new ChunkClock(sec * 1_000_000L, sec * FPS));
|
||||||
fakeTrackOutput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AviTrack getAudioAviTrack(int sec) {
|
public static ChunkHandler getAudioChunkHandler(int sec) {
|
||||||
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
return new AviTrack(AUDIO_ID, C.TRACK_TYPE_AUDIO,
|
return new ChunkHandler(AUDIO_ID, ChunkHandler.TYPE_AUDIO, fakeTrackOutput,
|
||||||
new LinearClock(sec * 1_000_000L, sec * FPS * AUDIO_PER_VIDEO),
|
new ChunkClock(sec * 1_000_000L, sec * FPS * AUDIO_PER_VIDEO));
|
||||||
fakeTrackOutput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AviSeekMap getAviSeekMap() {
|
public static AviSeekMap getAviSeekMap() {
|
||||||
@ -153,8 +150,8 @@ public class DataHelper {
|
|||||||
*/
|
*/
|
||||||
public static ByteBuffer getIndex(final int secs, final int keyFrameRate, int offset) {
|
public static ByteBuffer getIndex(final int secs, final int keyFrameRate, int offset) {
|
||||||
final int videoFrames = secs * FPS;
|
final int videoFrames = secs * FPS;
|
||||||
final int videoChunkId = AviTrack.getVideoChunkId(0);
|
final int videoChunkId = ChunkHandler.TYPE_VIDEO | ChunkHandler.getChunkIdLower(0);
|
||||||
final int audioChunkId = AviTrack.getAudioChunkId(1);
|
final int audioChunkId = ChunkHandler.TYPE_AUDIO | ChunkHandler.getChunkIdLower(1);
|
||||||
final ByteBuffer byteBuffer = AviExtractor.allocate((videoFrames + videoFrames*AUDIO_PER_VIDEO) * 16);
|
final ByteBuffer byteBuffer = AviExtractor.allocate((videoFrames + videoFrames*AUDIO_PER_VIDEO) * 16);
|
||||||
|
|
||||||
for (int v=0;v<videoFrames;v++) {
|
for (int v=0;v<videoFrames;v++) {
|
||||||
|
@ -24,7 +24,7 @@ import org.junit.Test;
|
|||||||
public class LinearClockTest {
|
public class LinearClockTest {
|
||||||
@Test
|
@Test
|
||||||
public void advance() {
|
public void advance() {
|
||||||
final LinearClock linearClock = new LinearClock(1_000L, 10);
|
final ChunkClock linearClock = new ChunkClock(1_000L, 10);
|
||||||
linearClock.setIndex(2);
|
linearClock.setIndex(2);
|
||||||
Assert.assertEquals(200, linearClock.getUs());
|
Assert.assertEquals(200, linearClock.getUs());
|
||||||
linearClock.advance();
|
linearClock.advance();
|
||||||
|
@ -16,12 +16,13 @@
|
|||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class MockNalChunkPeeker extends NalChunkPeeker {
|
public class MockNalChunkHandler extends NalChunkHandler {
|
||||||
private boolean skip;
|
private boolean skip;
|
||||||
public MockNalChunkPeeker(int peakSize, boolean skip) {
|
public MockNalChunkHandler(int peakSize, boolean skip) {
|
||||||
super(peakSize);
|
super(0, new FakeTrackOutput(false), new ChunkClock(1_000_000L, 24), peakSize);
|
||||||
this.skip = skip;
|
this.skip = skip;
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ import org.junit.runner.RunWith;
|
|||||||
public class Mp4vChunkPeekerTest {
|
public class Mp4vChunkPeekerTest {
|
||||||
|
|
||||||
private ByteBuffer makeSequence() {
|
private ByteBuffer makeSequence() {
|
||||||
return DataHelper.appendNal(AviExtractor.allocate(32),Mp4vChunkPeeker.SEQUENCE_START_CODE);
|
return DataHelper.appendNal(AviExtractor.allocate(32), Mp4vChunkHandler.SEQUENCE_START_CODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -42,7 +42,8 @@ public class Mp4vChunkPeekerTest {
|
|||||||
final Format.Builder formatBuilder = new Format.Builder();
|
final Format.Builder formatBuilder = new Format.Builder();
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
||||||
.build();
|
.build();
|
||||||
final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput);
|
final Mp4vChunkHandler mp4vChunkPeeker = new Mp4vChunkHandler(0, fakeTrackOutput,
|
||||||
|
new ChunkClock(1_000_000L, 24), formatBuilder);
|
||||||
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
||||||
Assert.assertEquals(1f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
Assert.assertEquals(1f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
||||||
}
|
}
|
||||||
@ -54,7 +55,8 @@ public class Mp4vChunkPeekerTest {
|
|||||||
final Context context = ApplicationProvider.getApplicationContext();
|
final Context context = ApplicationProvider.getApplicationContext();
|
||||||
final byte[] bytes = TestUtil.getByteArray(context, "extractordumps/avi/mp4v_sequence.dump");
|
final byte[] bytes = TestUtil.getByteArray(context, "extractordumps/avi/mp4v_sequence.dump");
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build();
|
||||||
final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput);
|
final Mp4vChunkHandler mp4vChunkPeeker = new Mp4vChunkHandler(0, fakeTrackOutput,
|
||||||
|
new ChunkClock(1_000_000L, 24), formatBuilder);
|
||||||
|
|
||||||
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
||||||
Assert.assertEquals(1.2121212, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
Assert.assertEquals(1.2121212, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
||||||
@ -64,14 +66,14 @@ public class Mp4vChunkPeekerTest {
|
|||||||
public void peek_givenCustomAspectRatio() throws IOException {
|
public void peek_givenCustomAspectRatio() throws IOException {
|
||||||
ByteBuffer byteBuffer = makeSequence();
|
ByteBuffer byteBuffer = makeSequence();
|
||||||
byteBuffer.putInt(0x5555);
|
byteBuffer.putInt(0x5555);
|
||||||
DataHelper.appendNal(byteBuffer, (byte)Mp4vChunkPeeker.LAYER_START_CODE);
|
DataHelper.appendNal(byteBuffer, (byte) Mp4vChunkHandler.LAYER_START_CODE);
|
||||||
|
|
||||||
BitBuffer bitBuffer = new BitBuffer();
|
BitBuffer bitBuffer = new BitBuffer();
|
||||||
bitBuffer.push(false); //random_accessible_vol
|
bitBuffer.push(false); //random_accessible_vol
|
||||||
bitBuffer.push(8, 8); //video_object_type_indication
|
bitBuffer.push(8, 8); //video_object_type_indication
|
||||||
bitBuffer.push(true); // is_object_layer_identifier
|
bitBuffer.push(true); // is_object_layer_identifier
|
||||||
bitBuffer.push(7, 7); // video_object_layer_verid, video_object_layer_priority
|
bitBuffer.push(7, 7); // video_object_layer_verid, video_object_layer_priority
|
||||||
bitBuffer.push(4, Mp4vChunkPeeker.Extended_PAR);
|
bitBuffer.push(4, Mp4vChunkHandler.Extended_PAR);
|
||||||
bitBuffer.push(8, 16);
|
bitBuffer.push(8, 16);
|
||||||
bitBuffer.push(8, 9);
|
bitBuffer.push(8, 9);
|
||||||
final byte bytes[] = bitBuffer.getBytes();
|
final byte bytes[] = bitBuffer.getBytes();
|
||||||
@ -81,7 +83,8 @@ public class Mp4vChunkPeekerTest {
|
|||||||
final Format.Builder formatBuilder = new Format.Builder();
|
final Format.Builder formatBuilder = new Format.Builder();
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
||||||
.build();
|
.build();
|
||||||
final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput);
|
final Mp4vChunkHandler mp4vChunkPeeker = new Mp4vChunkHandler(0, fakeTrackOutput,
|
||||||
|
new ChunkClock(1_000_000L, 24), formatBuilder);
|
||||||
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
||||||
Assert.assertEquals(16f/9f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
Assert.assertEquals(16f/9f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.audio.MpegAudioUtil;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class MpegAudioChunkHandlerTest {
|
||||||
|
private static final int FPS = 24;
|
||||||
|
private Format MP3_FORMAT = new Format.Builder().setChannelCount(2).
|
||||||
|
setSampleMimeType(MimeTypes.AUDIO_MPEG).setSampleRate(44100).build();
|
||||||
|
private static final long CHUNK_MS = C.MICROS_PER_SECOND / FPS;
|
||||||
|
private final MpegAudioUtil.Header header = new MpegAudioUtil.Header();
|
||||||
|
private FakeTrackOutput fakeTrackOutput;
|
||||||
|
private MpegAudioChunkHandler mpegAudioChunkHandler;
|
||||||
|
private byte[] mp3Frame;
|
||||||
|
private long frameUs;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() throws IOException {
|
||||||
|
fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
|
fakeTrackOutput.format(MP3_FORMAT);
|
||||||
|
mpegAudioChunkHandler = new MpegAudioChunkHandler(0, fakeTrackOutput,
|
||||||
|
new ChunkClock(C.MICROS_PER_SECOND, FPS), MP3_FORMAT.sampleRate);
|
||||||
|
|
||||||
|
if (mp3Frame == null) {
|
||||||
|
final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
mp3Frame = TestUtil.getByteArray(context,"extractordumps/avi/frame.mp3.dump");
|
||||||
|
header.setForHeaderData(ByteBuffer.wrap(mp3Frame).getInt());
|
||||||
|
//About 26ms
|
||||||
|
frameUs = header.samplesPerFrame * C.MICROS_PER_SECOND / header.sampleRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void newChunk_givenNonMpegData() throws IOException {
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[1024]).
|
||||||
|
build();
|
||||||
|
|
||||||
|
mpegAudioChunkHandler.newChunk((int)input.getLength(), input);
|
||||||
|
Assert.assertEquals(1024, fakeTrackOutput.getSampleData(0).length);
|
||||||
|
Assert.assertEquals(CHUNK_MS, mpegAudioChunkHandler.getClock().getUs());
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
public void newChunk_givenEmptyChunk() throws IOException {
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).
|
||||||
|
build();
|
||||||
|
mpegAudioChunkHandler.newChunk((int)input.getLength(), input);
|
||||||
|
Assert.assertEquals(C.MICROS_PER_SECOND / 24, mpegAudioChunkHandler.getClock().getUs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setIndex_given12frames() {
|
||||||
|
mpegAudioChunkHandler.setIndex(12);
|
||||||
|
Assert.assertEquals(500_000L, mpegAudioChunkHandler.getTimeUs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void newChunk_givenSingleFrame() throws IOException {
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(mp3Frame).build();
|
||||||
|
|
||||||
|
mpegAudioChunkHandler.newChunk(mp3Frame.length, input);
|
||||||
|
Assert.assertArrayEquals(mp3Frame, fakeTrackOutput.getSampleData(0));
|
||||||
|
Assert.assertEquals(frameUs, mpegAudioChunkHandler.getTimeUs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void newChunk_givenSeekAndFragmentedFrames() throws IOException {
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(mp3Frame.length * 2);
|
||||||
|
byteBuffer.put(mp3Frame, mp3Frame.length / 2, mp3Frame.length / 2);
|
||||||
|
byteBuffer.put(mp3Frame);
|
||||||
|
final int remainder = byteBuffer.remaining();
|
||||||
|
byteBuffer.put(mp3Frame, 0, remainder);
|
||||||
|
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).
|
||||||
|
build();
|
||||||
|
|
||||||
|
mpegAudioChunkHandler.setIndex(1); //Seek
|
||||||
|
Assert.assertFalse(mpegAudioChunkHandler.newChunk(byteBuffer.capacity(), input));
|
||||||
|
Assert.assertArrayEquals(mp3Frame, fakeTrackOutput.getSampleData(0));
|
||||||
|
Assert.assertEquals(frameUs + CHUNK_MS, mpegAudioChunkHandler.getTimeUs());
|
||||||
|
|
||||||
|
Assert.assertTrue(mpegAudioChunkHandler.resume(input));
|
||||||
|
Assert.assertEquals(header.frameSize - remainder, mpegAudioChunkHandler.getFrameRemaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void newChunk_givenTwoFrames() throws IOException {
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(mp3Frame.length * 2);
|
||||||
|
byteBuffer.put(mp3Frame);
|
||||||
|
byteBuffer.put(mp3Frame);
|
||||||
|
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).
|
||||||
|
build();
|
||||||
|
Assert.assertFalse(mpegAudioChunkHandler.newChunk(byteBuffer.capacity(), input));
|
||||||
|
Assert.assertEquals(1, fakeTrackOutput.getSampleCount());
|
||||||
|
Assert.assertEquals(0L, fakeTrackOutput.getSampleTimeUs(0));
|
||||||
|
|
||||||
|
Assert.assertTrue(mpegAudioChunkHandler.resume(input));
|
||||||
|
Assert.assertEquals(2, fakeTrackOutput.getSampleCount());
|
||||||
|
Assert.assertEquals(frameUs, fakeTrackOutput.getSampleTimeUs(1));
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,6 @@
|
|||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
import java.io.EOFException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
@ -26,7 +25,7 @@ public class NalChunkPeekerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void construct_givenTooSmallPeekSize() {
|
public void construct_givenTooSmallPeekSize() {
|
||||||
try {
|
try {
|
||||||
new MockNalChunkPeeker(4, false);
|
new MockNalChunkHandler(4, false);
|
||||||
Assert.fail();
|
Assert.fail();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
//Intentionally blank
|
//Intentionally blank
|
||||||
@ -36,7 +35,7 @@ public class NalChunkPeekerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void peek_givenNoData() {
|
public void peek_givenNoData() {
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().build();
|
||||||
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false);
|
final MockNalChunkHandler peeker = new MockNalChunkHandler(5, false);
|
||||||
try {
|
try {
|
||||||
peeker.peek(input, 10);
|
peeker.peek(input, 10);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -46,7 +45,7 @@ public class NalChunkPeekerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void peek_givenNoNal() {
|
public void peek_givenNoNal() {
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[10]).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[10]).build();
|
||||||
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false);
|
final MockNalChunkHandler peeker = new MockNalChunkHandler(5, false);
|
||||||
try {
|
try {
|
||||||
peeker.peek(input, 10);
|
peeker.peek(input, 10);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -59,7 +58,7 @@ public class NalChunkPeekerTest {
|
|||||||
DataHelper.appendNal(byteBuffer, (byte)32);
|
DataHelper.appendNal(byteBuffer, (byte)32);
|
||||||
|
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build();
|
||||||
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, true);
|
final MockNalChunkHandler peeker = new MockNalChunkHandler(5, true);
|
||||||
try {
|
try {
|
||||||
peeker.peek(input, 10);
|
peeker.peek(input, 10);
|
||||||
Assert.assertEquals(0, input.getPeekPosition());
|
Assert.assertEquals(0, input.getPeekPosition());
|
||||||
|
@ -134,6 +134,7 @@ public final class NalUnitUtilTest {
|
|||||||
assertThat(data.pixelWidthHeightRatio).isEqualTo(1.0f);
|
assertThat(data.pixelWidthHeightRatio).isEqualTo(1.0f);
|
||||||
assertThat(data.picOrderCountType).isEqualTo(0);
|
assertThat(data.picOrderCountType).isEqualTo(0);
|
||||||
assertThat(data.separateColorPlaneFlag).isFalse();
|
assertThat(data.separateColorPlaneFlag).isFalse();
|
||||||
|
assertThat(data.maxNumRefFrames).isEqualTo(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
BIN
testdata/src/test/assets/extractordumps/avi/frame.mp3.dump
vendored
Normal file
BIN
testdata/src/test/assets/extractordumps/avi/frame.mp3.dump
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user