Refactor Clock logic. Refactor peeking for MP4V and AVC. Moved AVI above MP3.
This commit is contained in:
parent
09485cbed1
commit
7ea2d75fcd
@ -98,9 +98,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||||||
FileTypes.ADTS,
|
FileTypes.ADTS,
|
||||||
FileTypes.AC3,
|
FileTypes.AC3,
|
||||||
FileTypes.AC4,
|
FileTypes.AC4,
|
||||||
|
FileTypes.AVI,
|
||||||
FileTypes.MP3,
|
FileTypes.MP3,
|
||||||
FileTypes.JPEG,
|
FileTypes.JPEG,
|
||||||
FileTypes.AVI,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final FlacExtensionLoader FLAC_EXTENSION_LOADER = new FlacExtensionLoader();
|
private static final FlacExtensionLoader FLAC_EXTENSION_LOADER = new FlacExtensionLoader();
|
||||||
|
@ -1,173 +0,0 @@
|
|||||||
package com.google.android.exoplayer2.extractor.avi;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
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.NalUnitUtil;
|
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
|
||||||
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class AvcAviTrack extends AviTrack{
|
|
||||||
private static final int NAL_TYPE_IRD = 5;
|
|
||||||
private static final int NAL_TYPE_SEI = 6;
|
|
||||||
private static final int NAL_TYPE_SPS = 7;
|
|
||||||
private static final int NAL_MASK = 0x1f;
|
|
||||||
private Format.Builder formatBuilder;
|
|
||||||
private float pixelWidthHeightRatio = 1f;
|
|
||||||
private NalUnitUtil.SpsData spsData;
|
|
||||||
//The frame as a calculated from the picCount
|
|
||||||
private int picFrame;
|
|
||||||
private int lastPicCount;
|
|
||||||
//Largest picFrame, used when we hit an I frame
|
|
||||||
private int maxPicFrame =-1;
|
|
||||||
private int maxPicCount;
|
|
||||||
private int posHalf;
|
|
||||||
private int negHalf;
|
|
||||||
|
|
||||||
AvcAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput,
|
|
||||||
@NonNull Format.Builder formatBuilder) {
|
|
||||||
super(id, streamHeaderBox, trackOutput);
|
|
||||||
this.formatBuilder = formatBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFormatBuilder(Format.Builder formatBuilder) {
|
|
||||||
this.formatBuilder = formatBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int seekNal(final ParsableByteArray parsableByteArray) {
|
|
||||||
final byte[] buffer = parsableByteArray.getData();
|
|
||||||
for (int i=parsableByteArray.getPosition();i<buffer.length - 5;i++) {
|
|
||||||
if (buffer[i] == 0 && buffer[i+1] == 0) {
|
|
||||||
if (buffer[i+2] == 1) {
|
|
||||||
parsableByteArray.setPosition(i+3);
|
|
||||||
} else if (buffer[i+2] == 0 && buffer[i+3] == 1) {
|
|
||||||
parsableByteArray.setPosition(i+4);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return (parsableByteArray.readUnsignedByte() & NAL_MASK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processIdr() {
|
|
||||||
lastPicCount = 0;
|
|
||||||
picFrame = maxPicFrame + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readSps(int size, ExtractorInput input) throws IOException {
|
|
||||||
final byte[] buffer = new byte[size];
|
|
||||||
input.readFully(buffer, 0, size, false);
|
|
||||||
final ParsableByteArray parsableByteArray = new ParsableByteArray(buffer);
|
|
||||||
int nal;
|
|
||||||
while ((nal = seekNal(parsableByteArray)) >= 0) {
|
|
||||||
if (nal == NAL_TYPE_SPS) {
|
|
||||||
spsData = NalUnitUtil.parseSpsNalUnitPayload(parsableByteArray.getData(), parsableByteArray.getPosition(), parsableByteArray.capacity());
|
|
||||||
maxPicCount = 1 << (spsData.picOrderCntLsbLength);
|
|
||||||
posHalf = maxPicCount / 2; //Not sure why pics are 2x
|
|
||||||
negHalf = -posHalf;
|
|
||||||
if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) {
|
|
||||||
formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio);
|
|
||||||
trackOutput.format(formatBuilder.build());
|
|
||||||
}
|
|
||||||
Log.d(AviExtractor.TAG, "SPS Frame: maxPicCount=" + maxPicCount);
|
|
||||||
} else if (nal == NAL_TYPE_IRD) {
|
|
||||||
processIdr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parsableByteArray.setPosition(0);
|
|
||||||
trackOutput.sampleData(parsableByteArray, parsableByteArray.capacity());
|
|
||||||
int flags = 0;
|
|
||||||
if (isKeyFrame()) {
|
|
||||||
flags |= C.BUFFER_FLAG_KEY_FRAME;
|
|
||||||
}
|
|
||||||
trackOutput.sampleMetadata(getUs(frame), flags, parsableByteArray.capacity(), 0, null);
|
|
||||||
Log.d(AviExtractor.TAG, "SPS Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame());
|
|
||||||
advance();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int getUsFrame() {
|
|
||||||
return picFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void seekFrame(int frame) {
|
|
||||||
super.seekFrame(frame);
|
|
||||||
this.picFrame = frame;
|
|
||||||
lastPicCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getPicOrderCountLsb(byte[] peek) {
|
|
||||||
if (peek[3] != 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, 5, peek.length);
|
|
||||||
//slide_header()
|
|
||||||
in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice
|
|
||||||
in.readUnsignedExpGolombCodedInt(); //slice_type
|
|
||||||
in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id
|
|
||||||
if (spsData.separateColorPlaneFlag) {
|
|
||||||
in.skipBits(2); //colour_plane_id
|
|
||||||
}
|
|
||||||
in.readBits(spsData.frameNumLength); //frame_num
|
|
||||||
if (!spsData.frameMbsOnlyFlag) {
|
|
||||||
boolean field_pic_flag = in.readBit(); // field_pic_flag
|
|
||||||
if (field_pic_flag) {
|
|
||||||
in.readBit(); // bottom_field_flag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//We skip IDR in the switch
|
|
||||||
if (spsData.picOrderCountType == 0) {
|
|
||||||
int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength);
|
|
||||||
//Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb);
|
|
||||||
return picOrderCountLsb;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
|
||||||
final int peekSize = Math.min(size, 16);
|
|
||||||
byte[] peek = new byte[peekSize];
|
|
||||||
input.peekFully(peek, 0, peekSize);
|
|
||||||
final int nalType = peek[4] & NAL_MASK;
|
|
||||||
switch (nalType) {
|
|
||||||
case 1:
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
case 4: {
|
|
||||||
final int picCount = getPicOrderCountLsb(peek);
|
|
||||||
if (picCount < 0) {
|
|
||||||
Log.d(AviExtractor.TAG, "Error getting PicOrder");
|
|
||||||
seekFrame(frame);
|
|
||||||
}
|
|
||||||
int delta = picCount - lastPicCount;
|
|
||||||
if (delta < negHalf) {
|
|
||||||
delta += maxPicCount;
|
|
||||||
} else if (delta > posHalf) {
|
|
||||||
delta -= maxPicCount;
|
|
||||||
}
|
|
||||||
picFrame += delta / 2;
|
|
||||||
lastPicCount = picCount;
|
|
||||||
if (maxPicFrame < picFrame) {
|
|
||||||
maxPicFrame = picFrame;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case NAL_TYPE_IRD:
|
|
||||||
processIdr();
|
|
||||||
break;
|
|
||||||
case NAL_TYPE_SEI:
|
|
||||||
case NAL_TYPE_SPS:
|
|
||||||
readSps(size, input);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.newChunk(tag, size, input);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,112 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||||
|
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class AvcChunkPeeker extends NalChunkPeeker {
|
||||||
|
private static final int NAL_TYPE_MASK = 0x1f;
|
||||||
|
private static final int NAL_TYPE_IRD = 5;
|
||||||
|
private static final int NAL_TYPE_SEI = 6;
|
||||||
|
private static final int NAL_TYPE_SPS = 7;
|
||||||
|
private static final int NAL_TYPE_PPS = 8;
|
||||||
|
|
||||||
|
private final PicCountClock picCountClock;
|
||||||
|
private final Format.Builder formatBuilder;
|
||||||
|
private final TrackOutput trackOutput;
|
||||||
|
|
||||||
|
private float pixelWidthHeightRatio = 1f;
|
||||||
|
private NalUnitUtil.SpsData spsData;
|
||||||
|
|
||||||
|
public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, long usPerChunk) {
|
||||||
|
super(16);
|
||||||
|
this.formatBuilder = formatBuilder;
|
||||||
|
this.trackOutput = trackOutput;
|
||||||
|
picCountClock = new PicCountClock(usPerChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PicCountClock getPicCountClock() {
|
||||||
|
return picCountClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean skip(byte nalType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePicCountClock(final int nalTypeOffset) {
|
||||||
|
final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length);
|
||||||
|
//slide_header()
|
||||||
|
in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice
|
||||||
|
in.readUnsignedExpGolombCodedInt(); //slice_type
|
||||||
|
in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id
|
||||||
|
if (spsData.separateColorPlaneFlag) {
|
||||||
|
in.skipBits(2); //colour_plane_id
|
||||||
|
}
|
||||||
|
in.readBits(spsData.frameNumLength); //frame_num
|
||||||
|
if (!spsData.frameMbsOnlyFlag) {
|
||||||
|
boolean field_pic_flag = in.readBit(); // field_pic_flag
|
||||||
|
if (field_pic_flag) {
|
||||||
|
in.readBit(); // bottom_field_flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//We skip IDR in the switch
|
||||||
|
if (spsData.picOrderCountType == 0) {
|
||||||
|
int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength);
|
||||||
|
//Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb);
|
||||||
|
picCountClock.setPicCount(picOrderCountLsb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
picCountClock.setIndex(picCountClock.getIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readSps(ExtractorInput input, int nalTypeOffset) throws IOException {
|
||||||
|
final int spsStart = nalTypeOffset + 1;
|
||||||
|
nalTypeOffset = seekNextNal(input, spsStart);
|
||||||
|
spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos);
|
||||||
|
picCountClock.setMaxPicCount(1 << (spsData.picOrderCntLsbLength));
|
||||||
|
if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) {
|
||||||
|
pixelWidthHeightRatio = spsData.pixelWidthHeightRatio;
|
||||||
|
formatBuilder.setPixelWidthHeightRatio(pixelWidthHeightRatio);
|
||||||
|
trackOutput.format(formatBuilder.build());
|
||||||
|
}
|
||||||
|
return nalTypeOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException {
|
||||||
|
while (true) {
|
||||||
|
final int nalType = buffer[nalTypeOffset] & NAL_TYPE_MASK;
|
||||||
|
switch (nalType) {
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
updatePicCountClock(nalTypeOffset);
|
||||||
|
return;
|
||||||
|
case NAL_TYPE_IRD:
|
||||||
|
picCountClock.syncIndexes();
|
||||||
|
return;
|
||||||
|
case NAL_TYPE_SEI:
|
||||||
|
case NAL_TYPE_PPS: {
|
||||||
|
nalTypeOffset = seekNextNal(input, nalTypeOffset);
|
||||||
|
//Usually chunks have other NALs after these, so just continue
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NAL_TYPE_SPS:
|
||||||
|
nalTypeOffset = readSps(input, nalTypeOffset);
|
||||||
|
//Sometimes video frames lurk after these
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nalTypeOffset < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
compact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import android.util.SparseArray;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
@ -17,9 +17,6 @@ import java.io.IOException;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on the official MicroSoft spec
|
* Based on the official MicroSoft spec
|
||||||
@ -43,13 +40,19 @@ public class AviExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static final String TAG = "AviExtractor";
|
static final String TAG = "AviExtractor";
|
||||||
private static final int PEEK_BYTES = 28;
|
@VisibleForTesting
|
||||||
|
static final int PEEK_BYTES = 28;
|
||||||
|
|
||||||
private static final int STATE_READ_TRACKS = 0;
|
@VisibleForTesting
|
||||||
private static final int STATE_FIND_MOVI = 1;
|
static final int STATE_READ_TRACKS = 0;
|
||||||
private static final int STATE_READ_IDX1 = 2;
|
@VisibleForTesting
|
||||||
private static final int STATE_READ_SAMPLES = 3;
|
static final int STATE_FIND_MOVI = 1;
|
||||||
private static final int STATE_SEEK_START = 4;
|
@VisibleForTesting
|
||||||
|
static final int STATE_READ_IDX1 = 2;
|
||||||
|
@VisibleForTesting
|
||||||
|
static final int STATE_READ_SAMPLES = 3;
|
||||||
|
@VisibleForTesting
|
||||||
|
static final int STATE_SEEK_START = 4;
|
||||||
|
|
||||||
private static final int AVIIF_KEYFRAME = 16;
|
private static final int AVIIF_KEYFRAME = 16;
|
||||||
|
|
||||||
@ -68,16 +71,17 @@ public class AviExtractor implements Extractor {
|
|||||||
|
|
||||||
static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds
|
static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds
|
||||||
|
|
||||||
private int state;
|
@VisibleForTesting
|
||||||
private ExtractorOutput output;
|
int state;
|
||||||
|
@VisibleForTesting
|
||||||
|
ExtractorOutput output;
|
||||||
private AviHeaderBox aviHeader;
|
private AviHeaderBox aviHeader;
|
||||||
private long durationUs = C.TIME_UNSET;
|
private long durationUs = C.TIME_UNSET;
|
||||||
private SparseArray<AviTrack> idTrackMap = new SparseArray<>();
|
private AviTrack[] aviTracks = new AviTrack[0];
|
||||||
//At the start of the movi tag
|
//At the start of the movi tag
|
||||||
private long moviOffset;
|
private long moviOffset;
|
||||||
private long moviEnd;
|
private long moviEnd;
|
||||||
private AviSeekMap aviSeekMap;
|
private AviSeekMap aviSeekMap;
|
||||||
private int flags;
|
|
||||||
|
|
||||||
// private long indexOffset; //Usually chunkStart
|
// private long indexOffset; //Usually chunkStart
|
||||||
|
|
||||||
@ -99,49 +103,42 @@ public class AviExtractor implements Extractor {
|
|||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AviExtractor() {
|
/**
|
||||||
this(0);
|
*
|
||||||
}
|
* @param input
|
||||||
|
* @param bytes Must be at least 20
|
||||||
public AviExtractor(int flags) {
|
*/
|
||||||
this.flags = flags;
|
@Nullable
|
||||||
}
|
private ByteBuffer getAviBuffer(ExtractorInput input, int bytes) throws IOException {
|
||||||
|
if (input.getLength() < bytes) {
|
||||||
@Override
|
return null;
|
||||||
public boolean sniff(ExtractorInput input) throws IOException {
|
}
|
||||||
return peekHeaderList(input);
|
final ByteBuffer byteBuffer = allocate(bytes);
|
||||||
}
|
input.peekFully(byteBuffer.array(), 0, bytes);
|
||||||
|
|
||||||
static ByteBuffer allocate(int bytes) {
|
|
||||||
final byte[] buffer = new byte[bytes];
|
|
||||||
final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
|
|
||||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
return byteBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setSeekMap(AviSeekMap aviSeekMap) {
|
|
||||||
this.aviSeekMap = aviSeekMap;
|
|
||||||
output.seekMap(aviSeekMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean peekHeaderList(ExtractorInput input) throws IOException {
|
|
||||||
final ByteBuffer byteBuffer = allocate(PEEK_BYTES);
|
|
||||||
input.peekFully(byteBuffer.array(), 0, PEEK_BYTES);
|
|
||||||
final int riff = byteBuffer.getInt();
|
final int riff = byteBuffer.getInt();
|
||||||
if (riff != AviExtractor.RIFF) {
|
if (riff != AviExtractor.RIFF) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
long reportedLen = getUInt(byteBuffer) + byteBuffer.position();
|
long reportedLen = getUInt(byteBuffer) + byteBuffer.position();
|
||||||
final long inputLen = input.getLength();
|
final long inputLen = input.getLength();
|
||||||
if (inputLen != C.LENGTH_UNSET && inputLen != reportedLen) {
|
if (inputLen != C.LENGTH_UNSET && inputLen != reportedLen) {
|
||||||
Log.w(TAG, "Header length doesn't match stream length");
|
w("Header length doesn't match stream length");
|
||||||
}
|
}
|
||||||
int avi = byteBuffer.getInt();
|
int avi = byteBuffer.getInt();
|
||||||
if (avi != AviExtractor.AVI_) {
|
if (avi != AviExtractor.AVI_) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
final int list = byteBuffer.getInt();
|
final int list = byteBuffer.getInt();
|
||||||
if (list != ListBox.LIST) {
|
if (list != ListBox.LIST) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return byteBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sniff(ExtractorInput input) throws IOException {
|
||||||
|
final ByteBuffer byteBuffer = getAviBuffer(input, PEEK_BYTES);
|
||||||
|
if (byteBuffer == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//Len
|
//Len
|
||||||
@ -157,27 +154,25 @@ public class AviExtractor implements Extractor {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ByteBuffer allocate(int bytes) {
|
||||||
|
final byte[] buffer = new byte[bytes];
|
||||||
|
final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
|
||||||
|
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
return byteBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSeekMap(AviSeekMap aviSeekMap) {
|
||||||
|
this.aviSeekMap = aviSeekMap;
|
||||||
|
output.seekMap(aviSeekMap);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
ListBox readHeaderList(ExtractorInput input) throws IOException {
|
ListBox readHeaderList(ExtractorInput input) throws IOException {
|
||||||
final ByteBuffer byteBuffer = allocate(20);
|
final ByteBuffer byteBuffer = getAviBuffer(input, 20);
|
||||||
input.readFully(byteBuffer.array(), 0, byteBuffer.capacity());
|
if (byteBuffer == null) {
|
||||||
final int riff = byteBuffer.getInt();
|
|
||||||
if (riff != AviExtractor.RIFF) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
long reportedLen = getUInt(byteBuffer) + byteBuffer.position();
|
|
||||||
final long inputLen = input.getLength();
|
|
||||||
if (inputLen != C.LENGTH_UNSET && inputLen != reportedLen) {
|
|
||||||
Log.w(TAG, "Header length doesn't match stream length");
|
|
||||||
}
|
|
||||||
final int avi = byteBuffer.getInt();
|
|
||||||
if (avi != AviExtractor.AVI_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final int list = byteBuffer.getInt();
|
|
||||||
if (list != ListBox.LIST) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
input.skipFully(20);
|
||||||
final int listSize = byteBuffer.getInt();
|
final int listSize = byteBuffer.getInt();
|
||||||
final ListBox listBox = ListBox.newInstance(listSize, new BoxFactory(), input);
|
final ListBox listBox = ListBox.newInstance(listSize, new BoxFactory(), input);
|
||||||
if (listBox.getListType() != ListBox.TYPE_HDRL) {
|
if (listBox.getListType() != ListBox.TYPE_HDRL) {
|
||||||
@ -196,11 +191,74 @@ public class AviExtractor implements Extractor {
|
|||||||
this.output = output;
|
this.output = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Box peekNext(final List<Box> streams, int i, int type) {
|
private void parseStream(final ListBox streamList, int streamId) {
|
||||||
if (i + 1 < streams.size() && streams.get(i + 1).getType() == type) {
|
final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class);
|
||||||
return streams.get(i + 1);
|
final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class);
|
||||||
|
if (streamHeader == null) {
|
||||||
|
Log.w(TAG, "Missing Stream Header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (streamFormat == null) {
|
||||||
|
Log.w(TAG, "Missing Stream Format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Format.Builder builder = new Format.Builder();
|
||||||
|
builder.setId(streamId);
|
||||||
|
final int suggestedBufferSize = streamHeader.getSuggestedBufferSize();
|
||||||
|
if (suggestedBufferSize != 0) {
|
||||||
|
builder.setMaxInputSize(suggestedBufferSize);
|
||||||
|
}
|
||||||
|
final StreamNameBox streamName = streamList.getChild(StreamNameBox.class);
|
||||||
|
if (streamName != null) {
|
||||||
|
builder.setLabel(streamName.getName());
|
||||||
|
}
|
||||||
|
if (streamHeader.isVideo()) {
|
||||||
|
final String mimeType = streamHeader.getMimeType();
|
||||||
|
if (mimeType == null) {
|
||||||
|
Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final VideoFormat videoFormat = streamFormat.getVideoFormat();
|
||||||
|
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO);
|
||||||
|
builder.setWidth(videoFormat.getWidth());
|
||||||
|
builder.setHeight(videoFormat.getHeight());
|
||||||
|
builder.setFrameRate(streamHeader.getFrameRate());
|
||||||
|
builder.setSampleMimeType(mimeType);
|
||||||
|
|
||||||
|
final AviTrack aviTrack = new AviTrack(streamId, streamHeader, trackOutput);
|
||||||
|
if (MimeTypes.VIDEO_MP4V.equals(mimeType)) {
|
||||||
|
Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(builder, trackOutput);
|
||||||
|
aviTrack.setChunkPeeker(mp4vChunkPeeker);
|
||||||
|
} else if (MimeTypes.VIDEO_H264.equals(mimeType)) {
|
||||||
|
final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample());
|
||||||
|
aviTrack.setClock(avcChunkPeeker.getPicCountClock());
|
||||||
|
aviTrack.setChunkPeeker(avcChunkPeeker);
|
||||||
|
}
|
||||||
|
trackOutput.format(builder.build());
|
||||||
|
durationUs = streamHeader.getUsPerSample() * streamHeader.getLength();
|
||||||
|
aviTracks[streamId] = aviTrack;
|
||||||
|
} else if (streamHeader.isAudio()) {
|
||||||
|
final AudioFormat audioFormat = streamFormat.getAudioFormat();
|
||||||
|
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO);
|
||||||
|
final String mimeType = audioFormat.getMimeType();
|
||||||
|
builder.setSampleMimeType(mimeType);
|
||||||
|
//builder.setCodecs(audioFormat.getCodec());
|
||||||
|
builder.setChannelCount(audioFormat.getChannels());
|
||||||
|
builder.setSampleRate(audioFormat.getSamplesPerSecond());
|
||||||
|
if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) {
|
||||||
|
final short bps = audioFormat.getBitsPerSample();
|
||||||
|
if (bps == 8) {
|
||||||
|
builder.setPcmEncoding(C.ENCODING_PCM_8BIT);
|
||||||
|
} else if (bps == 16){
|
||||||
|
builder.setPcmEncoding(C.ENCODING_PCM_16BIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (MimeTypes.AUDIO_AAC.equals(mimeType) && audioFormat.getCbSize() > 0) {
|
||||||
|
builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData()));
|
||||||
|
}
|
||||||
|
trackOutput.format(builder.build());
|
||||||
|
aviTracks[streamId] = new AviTrack(streamId, streamHeader, trackOutput);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int readTracks(ExtractorInput input) throws IOException {
|
private int readTracks(ExtractorInput input) throws IOException {
|
||||||
@ -212,83 +270,15 @@ 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()];
|
||||||
//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.getFrames() * (long)aviHeader.getMicroSecPerFrame();
|
durationUs = aviHeader.getTotalFrames() * (long)aviHeader.getMicroSecPerFrame();
|
||||||
|
|
||||||
int streamId = 0;
|
int streamId = 0;
|
||||||
for (Box box : headerList.getChildren()) {
|
for (Box box : headerList.getChildren()) {
|
||||||
if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) {
|
if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) {
|
||||||
final ListBox streamList = (ListBox) box;
|
final ListBox streamList = (ListBox) box;
|
||||||
final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class);
|
parseStream(streamList, streamId);
|
||||||
final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class);
|
|
||||||
if (streamHeader == null) {
|
|
||||||
Log.w(TAG, "Missing Stream Header");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (streamFormat == null) {
|
|
||||||
Log.w(TAG, "Missing Stream Format");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final Format.Builder builder = new Format.Builder();
|
|
||||||
builder.setId(streamId);
|
|
||||||
final int suggestedBufferSize = streamHeader.getSuggestedBufferSize();
|
|
||||||
if (suggestedBufferSize != 0) {
|
|
||||||
builder.setMaxInputSize(suggestedBufferSize);
|
|
||||||
}
|
|
||||||
final StreamNameBox streamName = streamList.getChild(StreamNameBox.class);
|
|
||||||
if (streamName != null) {
|
|
||||||
builder.setLabel(streamName.getName());
|
|
||||||
}
|
|
||||||
if (streamHeader.isVideo()) {
|
|
||||||
final String mimeType = streamHeader.getMimeType();
|
|
||||||
if (mimeType == null) {
|
|
||||||
Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final VideoFormat videoFormat = streamFormat.getVideoFormat();
|
|
||||||
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO);
|
|
||||||
builder.setWidth(videoFormat.getWidth());
|
|
||||||
builder.setHeight(videoFormat.getHeight());
|
|
||||||
builder.setFrameRate(streamHeader.getFrameRate());
|
|
||||||
builder.setSampleMimeType(mimeType);
|
|
||||||
|
|
||||||
final AviTrack aviTrack;
|
|
||||||
switch (mimeType) {
|
|
||||||
case MimeTypes.VIDEO_MP4V:
|
|
||||||
aviTrack = new Mp4vAviTrack(streamId, streamHeader, trackOutput, builder);
|
|
||||||
break;
|
|
||||||
case MimeTypes.VIDEO_H264:
|
|
||||||
aviTrack = new AvcAviTrack(streamId, streamHeader, trackOutput, builder);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
aviTrack = new AviTrack(streamId, streamHeader, trackOutput);
|
|
||||||
}
|
|
||||||
trackOutput.format(builder.build());
|
|
||||||
idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), aviTrack);
|
|
||||||
durationUs = streamHeader.getUsPerSample() * streamHeader.getLength();
|
|
||||||
} else if (streamHeader.isAudio()) {
|
|
||||||
final AudioFormat audioFormat = streamFormat.getAudioFormat();
|
|
||||||
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO);
|
|
||||||
final String mimeType = audioFormat.getMimeType();
|
|
||||||
builder.setSampleMimeType(mimeType);
|
|
||||||
//builder.setCodecs(audioFormat.getCodec());
|
|
||||||
builder.setChannelCount(audioFormat.getChannels());
|
|
||||||
builder.setSampleRate(audioFormat.getSamplesPerSecond());
|
|
||||||
if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) {
|
|
||||||
final short bps = audioFormat.getBitsPerSample();
|
|
||||||
if (bps == 8) {
|
|
||||||
builder.setPcmEncoding(C.ENCODING_PCM_8BIT);
|
|
||||||
} else if (bps == 16){
|
|
||||||
builder.setPcmEncoding(C.ENCODING_PCM_16BIT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (MimeTypes.AUDIO_AAC.equals(mimeType) && audioFormat.getCbSize() > 0) {
|
|
||||||
builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData()));
|
|
||||||
}
|
|
||||||
trackOutput.format(builder.build());
|
|
||||||
idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24),
|
|
||||||
new AviTrack(streamId, streamHeader, trackOutput));
|
|
||||||
}
|
|
||||||
streamId++;
|
streamId++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -323,6 +313,15 @@ public class AviExtractor implements Extractor {
|
|||||||
return RESULT_SEEK;
|
return RESULT_SEEK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AviTrack getVideoTrack() {
|
||||||
|
for (@Nullable AviTrack aviTrack : aviTracks) {
|
||||||
|
if (aviTrack != null && aviTrack.isVideo()) {
|
||||||
|
return aviTrack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the index and sets the keyFrames and creates the SeekMap
|
* Reads the index and sets the keyFrames and creates the SeekMap
|
||||||
* @param input
|
* @param input
|
||||||
@ -330,27 +329,20 @@ public class AviExtractor implements Extractor {
|
|||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
void readIdx1(ExtractorInput input, int remaining) throws IOException {
|
void readIdx1(ExtractorInput input, int remaining) throws IOException {
|
||||||
final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024));
|
final AviTrack videoTrack = getVideoTrack();
|
||||||
final byte[] bytes = indexByteBuffer.array();
|
|
||||||
|
|
||||||
final HashMap<Integer, UnboundedIntArray> audioIdFrameMap = new HashMap<>();
|
|
||||||
AviTrack videoTrack = null;
|
|
||||||
//Video seek offsets
|
|
||||||
UnboundedIntArray videoSeekOffset = new UnboundedIntArray();
|
|
||||||
for (int i=0;i<idTrackMap.size();i++) {
|
|
||||||
final AviTrack aviTrack = idTrackMap.valueAt(i);
|
|
||||||
if (videoTrack == null && aviTrack.isVideo()) {
|
|
||||||
videoTrack = aviTrack;
|
|
||||||
} else {
|
|
||||||
audioIdFrameMap.put(idTrackMap.keyAt(i), new UnboundedIntArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (videoTrack == null) {
|
if (videoTrack == null) {
|
||||||
output.seekMap(new SeekMap.Unseekable(getDuration()));
|
output.seekMap(new SeekMap.Unseekable(getDuration()));
|
||||||
Log.w(TAG, "No video track found");
|
Log.w(TAG, "No video track found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetFrames();
|
final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024));
|
||||||
|
final byte[] bytes = indexByteBuffer.array();
|
||||||
|
|
||||||
|
final int[] chunkCounts = new int[aviTracks.length];
|
||||||
|
final UnboundedIntArray[] seekOffsets = new UnboundedIntArray[aviTracks.length];
|
||||||
|
for (int i=0;i<seekOffsets.length;i++) {
|
||||||
|
seekOffsets[i] = new UnboundedIntArray();
|
||||||
|
}
|
||||||
final int seekFrameRate = (int)(videoTrack.streamHeaderBox.getFrameRate() * 2);
|
final int seekFrameRate = (int)(videoTrack.streamHeaderBox.getFrameRate() * 2);
|
||||||
|
|
||||||
final UnboundedIntArray keyFrameList = new UnboundedIntArray();
|
final UnboundedIntArray keyFrameList = new UnboundedIntArray();
|
||||||
@ -359,11 +351,11 @@ public class AviExtractor implements Extractor {
|
|||||||
input.readFully(bytes, indexByteBuffer.position(), toRead);
|
input.readFully(bytes, indexByteBuffer.position(), toRead);
|
||||||
remaining -= toRead;
|
remaining -= toRead;
|
||||||
while (indexByteBuffer.remaining() >= 16) {
|
while (indexByteBuffer.remaining() >= 16) {
|
||||||
final int id = indexByteBuffer.getInt();
|
final int chunkId = indexByteBuffer.getInt();
|
||||||
final AviTrack aviTrack = idTrackMap.get(id);
|
final AviTrack aviTrack = getAviTrack(chunkId);
|
||||||
if (aviTrack == null) {
|
if (aviTrack == null) {
|
||||||
if (id != AviExtractor.REC_) {
|
if (chunkId != AviExtractor.REC_) {
|
||||||
Log.w(TAG, "Unknown Track Type: " + toString(id));
|
Log.w(TAG, "Unknown Track Type: " + toString(chunkId));
|
||||||
}
|
}
|
||||||
indexByteBuffer.position(indexByteBuffer.position() + 12);
|
indexByteBuffer.position(indexByteBuffer.position() + 12);
|
||||||
continue;
|
continue;
|
||||||
@ -374,56 +366,80 @@ public class AviExtractor implements Extractor {
|
|||||||
//int size = indexByteBuffer.getInt();
|
//int size = indexByteBuffer.getInt();
|
||||||
if (aviTrack.isVideo()) {
|
if (aviTrack.isVideo()) {
|
||||||
if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
|
if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
|
||||||
keyFrameList.add(aviTrack.frame);
|
keyFrameList.add(chunkCounts[aviTrack.id]);
|
||||||
}
|
}
|
||||||
if (aviTrack.frame % seekFrameRate == 0) {
|
if (chunkCounts[aviTrack.id] % seekFrameRate == 0) {
|
||||||
|
seekOffsets[aviTrack.id].add(offset);
|
||||||
videoSeekOffset.add(offset);
|
for (int i=0;i<seekOffsets.length;i++) {
|
||||||
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
|
if (i != aviTrack.id) {
|
||||||
final int audioId = entry.getKey();
|
seekOffsets[i].add(chunkCounts[i]);
|
||||||
final UnboundedIntArray videoFrameMap = entry.getValue();
|
}
|
||||||
final AviTrack audioTrack = idTrackMap.get(audioId);
|
|
||||||
videoFrameMap.add(audioTrack.frame);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aviTrack.advance();
|
chunkCounts[aviTrack.id]++;
|
||||||
}
|
}
|
||||||
indexByteBuffer.compact();
|
indexByteBuffer.compact();
|
||||||
}
|
}
|
||||||
videoSeekOffset.pack();
|
//Set the keys frames
|
||||||
if (!videoTrack.isAllKeyFrames()) {
|
if (!videoTrack.isAllKeyFrames()) {
|
||||||
keyFrameList.pack();
|
|
||||||
final int[] keyFrames = keyFrameList.getArray();
|
final int[] keyFrames = keyFrameList.getArray();
|
||||||
videoTrack.setKeyFrames(keyFrames);
|
videoTrack.setKeyFrames(keyFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Correct the timings
|
//Correct the timings
|
||||||
durationUs = videoTrack.usPerSample * videoTrack.frame;
|
durationUs = chunkCounts[videoTrack.id] * videoTrack.getClock().usPerChunk;
|
||||||
|
|
||||||
final SparseArray<int[]> idFrameArray = new SparseArray<>();
|
for (int i=0;i<chunkCounts.length;i++) {
|
||||||
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
|
final AviTrack aviTrack = aviTracks[i];
|
||||||
entry.getValue().pack();
|
if (aviTrack != null && aviTrack.isAudio()) {
|
||||||
idFrameArray.put(entry.getKey(), entry.getValue().getArray());
|
final long calcUsPerSample = (durationUs/chunkCounts[i]);
|
||||||
final AviTrack aviTrack = idTrackMap.get(entry.getKey());
|
final LinearClock linearClock = aviTrack.getClock();
|
||||||
//Sometimes this value is way off
|
final float deltaPercent = Math.abs(calcUsPerSample - linearClock.usPerChunk) / (float)linearClock.usPerChunk;
|
||||||
long calcUsPerSample = (getDuration()/aviTrack.frame);
|
if (deltaPercent >.01) {
|
||||||
float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample;
|
Log.i(TAG, "Updating stream " + i + " calcUsPerSample=" + calcUsPerSample + " reported=" + linearClock.usPerChunk);
|
||||||
if (deltaPercent >.01) {
|
linearClock.usPerChunk = calcUsPerSample;
|
||||||
aviTrack.usPerSample = getDuration()/aviTrack.frame;
|
}
|
||||||
Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.getArray(),
|
final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekOffsets, seekFrameRate, moviOffset, getDuration());
|
||||||
idFrameArray, moviOffset, getDuration());
|
|
||||||
setSeekMap(seekMap);
|
setSeekMap(seekMap);
|
||||||
resetFrames();
|
}
|
||||||
|
|
||||||
|
private static int getStreamId(int chunkId) {
|
||||||
|
final int upperChar = chunkId & 0xff;
|
||||||
|
if (Character.isDigit(upperChar)) {
|
||||||
|
final int lowerChar = (chunkId >> 8) & 0xff;
|
||||||
|
if (Character.isDigit(upperChar)) {
|
||||||
|
return (lowerChar & 0xf) + ((upperChar & 0xf) * 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private AviTrack getAviTrack(int chunkId) {
|
||||||
|
final int streamId = getStreamId(chunkId);
|
||||||
|
if (streamId >= 0) {
|
||||||
|
return aviTracks[streamId];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int checkAlign(final ExtractorInput input, PositionHolder seekPosition) {
|
||||||
|
final long position = input.getPosition();
|
||||||
|
if ((position & 1) ==1) {
|
||||||
|
seekPosition.position = position +1;
|
||||||
|
return RESULT_SEEK;
|
||||||
|
}
|
||||||
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
||||||
if (chunkHandler != null) {
|
if (chunkHandler != null) {
|
||||||
if (chunkHandler.resume(input)) {
|
if (chunkHandler.resume(input)) {
|
||||||
chunkHandler = null;
|
chunkHandler = null;
|
||||||
|
return checkAlign(input, seekPosition);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ByteBuffer byteBuffer = allocate(8);
|
ByteBuffer byteBuffer = allocate(8);
|
||||||
@ -437,24 +453,27 @@ public class AviExtractor implements Extractor {
|
|||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
input.readFully(bytes, 1, 7);
|
input.readFully(bytes, 1, 7);
|
||||||
final int id = byteBuffer.getInt();
|
final int chunkId = byteBuffer.getInt();
|
||||||
final int size = byteBuffer.getInt();
|
if (chunkId == ListBox.LIST) {
|
||||||
AviTrack sampleTrack = idTrackMap.get(id);
|
seekPosition.position = input.getPosition() + 8;
|
||||||
if (sampleTrack == null) {
|
|
||||||
if (id == ListBox.LIST) {
|
|
||||||
seekPosition.position = input.getPosition() + 4;
|
|
||||||
} else {
|
|
||||||
seekPosition.position = alignPosition(input.getPosition() + size);
|
|
||||||
if (id != JUNK) {
|
|
||||||
Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8)
|
|
||||||
+ " size=" + size + " moviEnd=" + moviEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RESULT_SEEK;
|
return RESULT_SEEK;
|
||||||
|
}
|
||||||
|
final int size = byteBuffer.getInt();
|
||||||
|
if (chunkId == JUNK) {
|
||||||
|
seekPosition.position = alignPosition(input.getPosition() + size);
|
||||||
|
return RESULT_SEEK;
|
||||||
|
}
|
||||||
|
final AviTrack aviTrack = getAviTrack(chunkId);
|
||||||
|
if (aviTrack == null) {
|
||||||
|
seekPosition.position = alignPosition(input.getPosition() + size);
|
||||||
|
Log.w(TAG, "Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8)
|
||||||
|
+ " size=" + size + " moviEnd=" + moviEnd);
|
||||||
|
return RESULT_SEEK;
|
||||||
|
}
|
||||||
|
if (aviTrack.newChunk(chunkId, size, input)) {
|
||||||
|
return checkAlign(input, seekPosition);
|
||||||
} else {
|
} else {
|
||||||
if (!sampleTrack.newChunk(id, size, input)) {
|
chunkHandler = aviTrack;
|
||||||
chunkHandler = sampleTrack;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
@ -489,7 +508,6 @@ public class AviExtractor implements Extractor {
|
|||||||
state = STATE_READ_SAMPLES;
|
state = STATE_READ_SAMPLES;
|
||||||
return RESULT_SEEK;
|
return RESULT_SEEK;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
@ -499,24 +517,34 @@ public class AviExtractor implements Extractor {
|
|||||||
chunkHandler = null;
|
chunkHandler = null;
|
||||||
if (position <= 0) {
|
if (position <= 0) {
|
||||||
if (moviOffset != 0) {
|
if (moviOffset != 0) {
|
||||||
resetFrames();
|
resetClocks();
|
||||||
state = STATE_SEEK_START;
|
state = STATE_SEEK_START;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (aviSeekMap != null) {
|
if (aviSeekMap != null) {
|
||||||
aviSeekMap.setFrames(position, timeUs, idTrackMap);
|
aviSeekMap.setFrames(position, timeUs, aviTracks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetFrames() {
|
void resetClocks() {
|
||||||
for (int i=0;i<idTrackMap.size();i++) {
|
for (@Nullable AviTrack aviTrack : aviTracks) {
|
||||||
final AviTrack aviTrack = idTrackMap.valueAt(i);
|
if (aviTrack != null) {
|
||||||
aviTrack.seekFrame(0);
|
aviTrack.getClock().setIndex(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
|
//Intentionally blank
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void w(String message) {
|
||||||
|
try {
|
||||||
|
Log.w(TAG, message);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
//Catch not mocked for tests
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import java.nio.ByteBuffer;
|
|||||||
|
|
||||||
public class AviHeaderBox extends ResidentBox {
|
public class AviHeaderBox extends ResidentBox {
|
||||||
private static final int AVIF_HASINDEX = 0x10;
|
private static final int AVIF_HASINDEX = 0x10;
|
||||||
private static int AVIF_MUSTUSEINDEX = 0x20;
|
private static final int AVIF_MUSTUSEINDEX = 0x20;
|
||||||
static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24);
|
static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24);
|
||||||
|
|
||||||
//AVIMAINHEADER
|
//AVIMAINHEADER
|
||||||
@ -32,20 +32,29 @@ public class AviHeaderBox extends ResidentBox {
|
|||||||
return byteBuffer.getInt(12);
|
return byteBuffer.getInt(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
int getFrames() {
|
int getTotalFrames() {
|
||||||
return byteBuffer.getInt(16);
|
return byteBuffer.getInt(16);
|
||||||
}
|
}
|
||||||
//20 = dwInitialFrames
|
|
||||||
|
|
||||||
int getSuggestedBufferSize() {
|
// 20 - dwInitialFrames
|
||||||
|
// int getInitialFrames() {
|
||||||
|
// return byteBuffer.getInt(20);
|
||||||
|
// }
|
||||||
|
|
||||||
|
int getStreams() {
|
||||||
return byteBuffer.getInt(24);
|
return byteBuffer.getInt(24);
|
||||||
}
|
}
|
||||||
|
|
||||||
int getWidth() {
|
// 28 - dwSuggestedBufferSize
|
||||||
return byteBuffer.getInt(28);
|
// int getSuggestedBufferSize() {
|
||||||
}
|
// return byteBuffer.getInt(28);
|
||||||
|
// }
|
||||||
int getHeight() {
|
//
|
||||||
return byteBuffer.getInt(32);
|
// int getWidth() {
|
||||||
}
|
// return byteBuffer.getInt(32);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// int getHeight() {
|
||||||
|
// return byteBuffer.getInt(36);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,35 @@
|
|||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import android.util.SparseArray;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
|
||||||
public class AviSeekMap implements SeekMap {
|
public class AviSeekMap implements SeekMap {
|
||||||
final AviTrack videoTrack;
|
final long videoUsPerChunk;
|
||||||
|
final int videoStreamId;
|
||||||
/**
|
/**
|
||||||
* Number of frames per index
|
* Number of frames per index
|
||||||
* i.e. videoFrameOffsetMap[1] is frame 1 * seekIndexFactor
|
* i.e. videoFrameOffsetMap[1] is frame 1 * seekIndexFactor
|
||||||
*/
|
*/
|
||||||
final int seekIndexFactor;
|
final int seekIndexFactor;
|
||||||
//Map from the Video Frame index to the offset
|
//Seek offsets by streamId, for video, this is the actual offset, for audio, this is the chunkId
|
||||||
final int[] videoFrameOffsetMap;
|
final int[][] seekOffsets;
|
||||||
//Holds a map of video frameIds to audioFrameIds for each audioId
|
//Holds a map of video frameIds to audioFrameIds for each audioId
|
||||||
final SparseArray<int[]> audioIdMap;
|
|
||||||
final long moviOffset;
|
final long moviOffset;
|
||||||
final long duration;
|
final long duration;
|
||||||
|
|
||||||
public AviSeekMap(AviTrack videoTrack, int seekIndexFactor, int[] videoFrameOffsetMap,
|
public AviSeekMap(AviTrack videoTrack, UnboundedIntArray[] seekOffsets, int seekIndexFactor, long moviOffset, long duration) {
|
||||||
SparseArray<int[]> audioIdMap, long moviOffset, long duration) {
|
videoUsPerChunk = videoTrack.getClock().usPerChunk;
|
||||||
this.videoTrack = videoTrack;
|
videoStreamId = videoTrack.id;
|
||||||
this.seekIndexFactor = seekIndexFactor;
|
this.seekIndexFactor = seekIndexFactor;
|
||||||
this.videoFrameOffsetMap = videoFrameOffsetMap;
|
|
||||||
this.audioIdMap = audioIdMap;
|
|
||||||
this.moviOffset = moviOffset;
|
this.moviOffset = moviOffset;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
|
this.seekOffsets = new int[seekOffsets.length][];
|
||||||
|
for (int i=0;i<seekOffsets.length;i++) {
|
||||||
|
this.seekOffsets[i] = seekOffsets[i].getArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -41,10 +43,10 @@ public class AviSeekMap implements SeekMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int getSeekFrameIndex(long timeUs) {
|
private int getSeekFrameIndex(long timeUs) {
|
||||||
final int reqFrame = (int)(timeUs / videoTrack.usPerSample);
|
final int reqFrame = (int)(timeUs / videoUsPerChunk);
|
||||||
int reqFrameIndex = reqFrame / seekIndexFactor;
|
int reqFrameIndex = reqFrame / seekIndexFactor;
|
||||||
if (reqFrameIndex >= videoFrameOffsetMap.length) {
|
if (reqFrameIndex >= seekOffsets[videoStreamId].length) {
|
||||||
reqFrameIndex = videoFrameOffsetMap.length - 1;
|
reqFrameIndex = seekOffsets[videoStreamId].length - 1;
|
||||||
}
|
}
|
||||||
return reqFrameIndex;
|
return reqFrameIndex;
|
||||||
}
|
}
|
||||||
@ -53,23 +55,29 @@ public class AviSeekMap implements SeekMap {
|
|||||||
@Override
|
@Override
|
||||||
public SeekPoints getSeekPoints(long timeUs) {
|
public SeekPoints getSeekPoints(long timeUs) {
|
||||||
final int seekFrameIndex = getSeekFrameIndex(timeUs);
|
final int seekFrameIndex = getSeekFrameIndex(timeUs);
|
||||||
int offset = videoFrameOffsetMap[seekFrameIndex];
|
int offset = seekOffsets[videoStreamId][seekFrameIndex];
|
||||||
final long outUs = seekFrameIndex * seekIndexFactor * videoTrack.usPerSample;
|
final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk;
|
||||||
final long position = offset + moviOffset;
|
final long position = offset + moviOffset;
|
||||||
Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
||||||
|
|
||||||
return new SeekPoints(new SeekPoint(outUs, position));
|
return new SeekPoints(new SeekPoint(outUs, position));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFrames(final long position, final long timeUs, final SparseArray<AviTrack> idTrackMap) {
|
public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) {
|
||||||
final int seekFrameIndex = getSeekFrameIndex(timeUs);
|
final int seekFrameIndex = getSeekFrameIndex(timeUs);
|
||||||
videoTrack.seekFrame(seekFrameIndex * seekIndexFactor);
|
for (int i=0;i<aviTracks.length;i++) {
|
||||||
for (int i=0;i<audioIdMap.size();i++) {
|
final AviTrack aviTrack = aviTracks[i];
|
||||||
final int audioId = audioIdMap.keyAt(i);
|
if (aviTrack != null) {
|
||||||
final int[] video2AudioFrameMap = audioIdMap.get(audioId);
|
final LinearClock clock = aviTrack.getClock();
|
||||||
final AviTrack audioTrack = idTrackMap.get(audioId);
|
if (aviTrack.isVideo()) {
|
||||||
audioTrack.frame = video2AudioFrameMap[seekFrameIndex];
|
//TODO: Although this works, it leads to partial frames being painted
|
||||||
|
aviTrack.setForceKeyFrame(true);
|
||||||
|
clock.setIndex(seekFrameIndex * seekIndexFactor);
|
||||||
|
} else {
|
||||||
|
final int offset = seekOffsets[i][seekFrameIndex];
|
||||||
|
clock.setIndex(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,13 +18,19 @@ public class AviTrack {
|
|||||||
@NonNull
|
@NonNull
|
||||||
final StreamHeaderBox streamHeaderBox;
|
final StreamHeaderBox streamHeaderBox;
|
||||||
|
|
||||||
long usPerSample;
|
@NonNull
|
||||||
|
LinearClock clock;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
ChunkPeeker chunkPeeker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True indicates all frames are key frames (e.g. Audio, MJPEG)
|
* True indicates all frames are key frames (e.g. Audio, MJPEG)
|
||||||
*/
|
*/
|
||||||
boolean allKeyFrames;
|
boolean allKeyFrames;
|
||||||
|
|
||||||
|
boolean forceKeyFrame;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
TrackOutput trackOutput;
|
TrackOutput trackOutput;
|
||||||
|
|
||||||
@ -37,19 +43,24 @@ public class AviTrack {
|
|||||||
transient int chunkSize;
|
transient int chunkSize;
|
||||||
transient int chunkRemaining;
|
transient int chunkRemaining;
|
||||||
|
|
||||||
/**
|
|
||||||
* Current frame in the stream
|
|
||||||
* This needs to be updated on seek
|
|
||||||
* TODO: Should be offset from StreamHeaderBox.getStart()
|
|
||||||
*/
|
|
||||||
int frame;
|
|
||||||
|
|
||||||
AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) {
|
AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.trackOutput = trackOutput;
|
this.trackOutput = trackOutput;
|
||||||
this.streamHeaderBox = streamHeaderBox;
|
this.streamHeaderBox = streamHeaderBox;
|
||||||
this.usPerSample = streamHeaderBox.getUsPerSample();
|
clock = new LinearClock(streamHeaderBox.getUsPerSample());
|
||||||
this.allKeyFrames = streamHeaderBox.isAudio() || (MimeTypes.IMAGE_JPEG.equals(streamHeaderBox.getMimeType()));
|
this.allKeyFrames = streamHeaderBox.isAudio() || (MimeTypes.VIDEO_MJPEG.equals(streamHeaderBox.getMimeType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinearClock getClock() {
|
||||||
|
return clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClock(LinearClock clock) {
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChunkPeeker(ChunkPeeker chunkPeeker) {
|
||||||
|
this.chunkPeeker = chunkPeeker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAllKeyFrames() {
|
public boolean isAllKeyFrames() {
|
||||||
@ -60,25 +71,26 @@ public class AviTrack {
|
|||||||
if (allKeyFrames) {
|
if (allKeyFrames) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (forceKeyFrame) {
|
||||||
|
forceKeyFrame = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (keyFrames != null) {
|
if (keyFrames != null) {
|
||||||
return Arrays.binarySearch(keyFrames, frame) >= 0;
|
return Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0;
|
||||||
}
|
}
|
||||||
//Hack: Exo needs at least one frame before it starts playback
|
//Hack: Exo needs at least one frame before it starts playback
|
||||||
return frame == 0;
|
//return clock.getIndex() == 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setForceKeyFrame(boolean v) {
|
||||||
|
forceKeyFrame = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setKeyFrames(int[] keyFrames) {
|
public void setKeyFrames(int[] keyFrames) {
|
||||||
this.keyFrames = keyFrames;
|
this.keyFrames = keyFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUs() {
|
|
||||||
return getUs(getUsFrame());
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getUs(final int myFrame) {
|
|
||||||
return myFrame * usPerSample;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isVideo() {
|
public boolean isVideo() {
|
||||||
return streamHeaderBox.isVideo();
|
return streamHeaderBox.isVideo();
|
||||||
}
|
}
|
||||||
@ -87,23 +99,10 @@ public class AviTrack {
|
|||||||
return streamHeaderBox.isAudio();
|
return streamHeaderBox.isAudio();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void advance() {
|
|
||||||
frame++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the frame number used to calculate the timeUs
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
int getUsFrame() {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
void seekFrame(int frame) {
|
|
||||||
this.frame = frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
||||||
|
if (chunkPeeker != null) {
|
||||||
|
chunkPeeker.peek(input, size);
|
||||||
|
}
|
||||||
final int remaining = size - trackOutput.sampleData(input, size, false);
|
final int remaining = size - trackOutput.sampleData(input, size, false);
|
||||||
if (remaining == 0) {
|
if (remaining == 0) {
|
||||||
done(size);
|
done(size);
|
||||||
@ -127,8 +126,8 @@ public class AviTrack {
|
|||||||
|
|
||||||
void done(final int size) {
|
void done(final int size) {
|
||||||
trackOutput.sampleMetadata(
|
trackOutput.sampleMetadata(
|
||||||
getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null);
|
clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null);
|
||||||
//Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame());
|
//Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame());
|
||||||
advance();
|
clock.advance();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface ChunkPeeker {
|
||||||
|
void peek(ExtractorInput input, final int size) throws IOException;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
public class LinearClock {
|
||||||
|
long usPerChunk;
|
||||||
|
|
||||||
|
int index;
|
||||||
|
|
||||||
|
public LinearClock(long usPerChunk) {
|
||||||
|
this.usPerChunk = usPerChunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIndex(int index) {
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void advance() {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUs() {
|
||||||
|
return index * usPerChunk;
|
||||||
|
}
|
||||||
|
}
|
@ -1,81 +0,0 @@
|
|||||||
package com.google.android.exoplayer2.extractor.avi;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
|
||||||
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class Mp4vAviTrack extends AviTrack {
|
|
||||||
private static final byte SEQUENCE_START_CODE = (byte)0xb0;
|
|
||||||
private static final int LAYER_START_CODE = 0x20;
|
|
||||||
private static final float[] ASPECT_RATIO = {0f, 1f, 12f/11f, 10f/11f, 16f/11f, 40f/33f};
|
|
||||||
private static final int Extended_PAR = 0xf;
|
|
||||||
private final Format.Builder formatBuilder;
|
|
||||||
float pixelWidthHeightRatio = 1f;
|
|
||||||
|
|
||||||
Mp4vAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput,
|
|
||||||
@NonNull Format.Builder formatBuilder) {
|
|
||||||
super(id, streamHeaderBox, trackOutput);
|
|
||||||
this.formatBuilder = formatBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
void processLayerStart(@NonNull final ParsableNalUnitBitArray in) {
|
|
||||||
in.skipBit(); // random_accessible_vol
|
|
||||||
in.skipBits(8); // video_object_type_indication
|
|
||||||
boolean is_object_layer_identifier = in.readBit();
|
|
||||||
if (is_object_layer_identifier) {
|
|
||||||
in.skipBits(7); // video_object_layer_verid, video_object_layer_priority
|
|
||||||
}
|
|
||||||
int aspect_ratio_info = in.readBits(4);
|
|
||||||
final float aspectRatio;
|
|
||||||
if (aspect_ratio_info == Extended_PAR) {
|
|
||||||
float par_width = (float)in.readBits(8);
|
|
||||||
float par_height = (float)in.readBits(8);
|
|
||||||
aspectRatio = par_width / par_height;
|
|
||||||
} else {
|
|
||||||
aspectRatio = ASPECT_RATIO[aspect_ratio_info];
|
|
||||||
}
|
|
||||||
if (aspectRatio != pixelWidthHeightRatio) {
|
|
||||||
trackOutput.format(formatBuilder.setPixelWidthHeightRatio(aspectRatio).build());
|
|
||||||
pixelWidthHeightRatio = aspectRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
@Nullable
|
|
||||||
static ParsableNalUnitBitArray findLayerStart(ExtractorInput input, final int peekSize)
|
|
||||||
throws IOException {
|
|
||||||
byte[] peek = new byte[peekSize];
|
|
||||||
input.peekFully(peek, 0, peekSize);
|
|
||||||
for (int i = 4;i<peek.length - 4;i++) {
|
|
||||||
if (peek[i] == 0 && peek[i+1] == 0 && peek[i+2] == 1 && (peek[i+3] & 0xf0) == LAYER_START_CODE) {
|
|
||||||
return new ParsableNalUnitBitArray(peek, i+4, peek.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static boolean isSequenceStart(ExtractorInput input) throws IOException {
|
|
||||||
final byte[] peek = new byte[4];
|
|
||||||
input.peekFully(peek, 0, peek.length);
|
|
||||||
return peek[0] == 0 && peek[1] == 0 && peek[2] == 1 && peek[3] == SEQUENCE_START_CODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
|
||||||
if (isSequenceStart(input)) {
|
|
||||||
// -4 because isSequenceStart peeks 4
|
|
||||||
final ParsableNalUnitBitArray layerStart = findLayerStart(input, Math.min(size - 4, 128));
|
|
||||||
if (layerStart != null) {
|
|
||||||
processLayerStart(layerStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.newChunk(tag, size, input);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,76 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class Mp4vChunkPeeker extends NalChunkPeeker {
|
||||||
|
@VisibleForTesting
|
||||||
|
static final byte SEQUENCE_START_CODE = (byte)0xb0;
|
||||||
|
@VisibleForTesting
|
||||||
|
static final int LAYER_START_CODE = 0x20;
|
||||||
|
private static final float[] ASPECT_RATIO = {0f, 1f, 12f/11f, 10f/11f, 16f/11f, 40f/33f};
|
||||||
|
@VisibleForTesting
|
||||||
|
static final int Extended_PAR = 0xf;
|
||||||
|
|
||||||
|
private final Format.Builder formatBuilder;
|
||||||
|
private final TrackOutput trackOutput;
|
||||||
|
|
||||||
|
@VisibleForTesting()
|
||||||
|
float pixelWidthHeightRatio = 1f;
|
||||||
|
|
||||||
|
public Mp4vChunkPeeker(@NonNull Format.Builder formatBuilder, @NonNull TrackOutput trackOutput) {
|
||||||
|
super(5);
|
||||||
|
this.formatBuilder = formatBuilder;
|
||||||
|
this.trackOutput = trackOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean skip(byte nalType) {
|
||||||
|
return nalType != SEQUENCE_START_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void processLayerStart(int nalTypeOffset) {
|
||||||
|
@NonNull final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, pos);
|
||||||
|
in.skipBit(); // random_accessible_vol
|
||||||
|
in.skipBits(8); // video_object_type_indication
|
||||||
|
boolean is_object_layer_identifier = in.readBit();
|
||||||
|
if (is_object_layer_identifier) {
|
||||||
|
in.skipBits(7); // video_object_layer_verid, video_object_layer_priority
|
||||||
|
}
|
||||||
|
int aspect_ratio_info = in.readBits(4);
|
||||||
|
final float aspectRatio;
|
||||||
|
if (aspect_ratio_info == Extended_PAR) {
|
||||||
|
float par_width = (float)in.readBits(8);
|
||||||
|
float par_height = (float)in.readBits(8);
|
||||||
|
aspectRatio = par_width / par_height;
|
||||||
|
} else {
|
||||||
|
aspectRatio = ASPECT_RATIO[aspect_ratio_info];
|
||||||
|
}
|
||||||
|
if (aspectRatio != pixelWidthHeightRatio) {
|
||||||
|
trackOutput.format(formatBuilder.setPixelWidthHeightRatio(aspectRatio).build());
|
||||||
|
pixelWidthHeightRatio = aspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException {
|
||||||
|
while (true) {
|
||||||
|
if ((buffer[nalTypeOffset] & 0xf0) == LAYER_START_CODE) {
|
||||||
|
seekNextNal(input, nalTypeOffset);
|
||||||
|
processLayerStart(nalTypeOffset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nalTypeOffset = seekNextNal(input, nalTypeOffset);
|
||||||
|
if (nalTypeOffset < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
compact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public abstract class NalChunkPeeker implements ChunkPeeker {
|
||||||
|
private static final int SEEK_PEEK_SIZE = 256;
|
||||||
|
private final int peekSize;
|
||||||
|
|
||||||
|
private transient int remaining;
|
||||||
|
transient byte[] buffer;
|
||||||
|
transient int pos;
|
||||||
|
|
||||||
|
abstract void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return NAL offset from pos
|
||||||
|
*/
|
||||||
|
private int getNalTypeOffset() {
|
||||||
|
if (buffer[pos] == 0 && buffer[pos+1] == 0) {
|
||||||
|
if (buffer[pos+2] == 1) {
|
||||||
|
return 3;
|
||||||
|
} else if (buffer[pos+2] == 0 && buffer[pos+3] == 1) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for the next NAL in buffer, incrementing pos
|
||||||
|
* @return offset of the nal from the pos
|
||||||
|
*/
|
||||||
|
private int seekNal() {
|
||||||
|
int nalOffset;
|
||||||
|
while ((nalOffset = getNalTypeOffset()) < 0 && pos < buffer.length - 5) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
return nalOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes everything before the pos
|
||||||
|
*/
|
||||||
|
void compact() {
|
||||||
|
//Compress down to the last NAL
|
||||||
|
final byte[] newBuffer = new byte[buffer.length - pos];
|
||||||
|
System.arraycopy(buffer, pos, newBuffer, 0, newBuffer.length);
|
||||||
|
buffer = newBuffer;
|
||||||
|
pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param peekSize number of bytes to append
|
||||||
|
*/
|
||||||
|
void append(final ExtractorInput input, final int peekSize) throws IOException {
|
||||||
|
int oldLength = buffer.length;
|
||||||
|
buffer = Arrays.copyOf(buffer, oldLength + peekSize);
|
||||||
|
input.peekFully(buffer, oldLength, peekSize);
|
||||||
|
remaining -= peekSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return NAL offset from pos, -1 if end of input
|
||||||
|
*/
|
||||||
|
int seekNextNal(final ExtractorInput input, int skip) throws IOException {
|
||||||
|
pos += skip;
|
||||||
|
while (pos + 5 < buffer.length || remaining > 0) {
|
||||||
|
if (buffer.length - pos < SEEK_PEEK_SIZE && remaining > 0) {
|
||||||
|
append(input, Math.min(SEEK_PEEK_SIZE, remaining));
|
||||||
|
}
|
||||||
|
final int nalOffset = seekNal();
|
||||||
|
if (nalOffset > 0) {
|
||||||
|
return nalOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos = buffer.length;
|
||||||
|
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);
|
||||||
|
|
||||||
|
public void peek(ExtractorInput input, final int size) throws IOException {
|
||||||
|
buffer = new byte[peekSize];
|
||||||
|
if (!input.peekFully(buffer, 0, peekSize, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pos = 0;
|
||||||
|
int nalTypeOffset = getNalTypeOffset();
|
||||||
|
if (nalTypeOffset < 0 || skip(buffer[nalTypeOffset])) {
|
||||||
|
input.resetPeekPosition();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
remaining = size - peekSize;
|
||||||
|
processChunk(input, nalTypeOffset);
|
||||||
|
input.resetPeekPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
// void setBuffer(byte[] buffer) {
|
||||||
|
// this.buffer = buffer;
|
||||||
|
// }
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properly calculates the frame time for H264 frames using PicCount
|
||||||
|
*/
|
||||||
|
public class PicCountClock extends LinearClock {
|
||||||
|
//The frame as a calculated from the picCount
|
||||||
|
private int picIndex;
|
||||||
|
private int lastPicCount;
|
||||||
|
//Largest picFrame, used when we hit an I frame
|
||||||
|
private int maxPicIndex =-1;
|
||||||
|
private int maxPicCount;
|
||||||
|
private int posHalf;
|
||||||
|
private int negHalf;
|
||||||
|
|
||||||
|
public PicCountClock(long usPerFrame) {
|
||||||
|
super(usPerFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxPicCount(int maxPicCount) {
|
||||||
|
this.maxPicCount = maxPicCount;
|
||||||
|
posHalf = maxPicCount / 2; //Not sure why pics are 2x
|
||||||
|
negHalf = -posHalf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Done on seek. May cause sync issues if frame picCount != 0 (I frames are always 0)
|
||||||
|
* @param index
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setIndex(int index) {
|
||||||
|
super.setIndex(index);
|
||||||
|
syncIndexes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPicCount(int picCount) {
|
||||||
|
int delta = picCount - lastPicCount;
|
||||||
|
if (delta < negHalf) {
|
||||||
|
delta += maxPicCount;
|
||||||
|
} else if (delta > posHalf) {
|
||||||
|
delta -= maxPicCount;
|
||||||
|
}
|
||||||
|
picIndex += delta / 2;
|
||||||
|
lastPicCount = picCount;
|
||||||
|
if (maxPicIndex < picIndex) {
|
||||||
|
maxPicIndex = picIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle key frame
|
||||||
|
*/
|
||||||
|
public void syncIndexes() {
|
||||||
|
lastPicCount = 0;
|
||||||
|
maxPicIndex = picIndex = getIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getUs() {
|
||||||
|
return picIndex * usPerChunk;
|
||||||
|
}
|
||||||
|
}
|
@ -36,6 +36,7 @@ public class StreamHeaderBox extends ResidentBox {
|
|||||||
STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType);
|
STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType);
|
||||||
STREAM_MAP.put(XVID, mimeType);
|
STREAM_MAP.put(XVID, mimeType);
|
||||||
STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType);
|
STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType);
|
||||||
|
STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType);
|
||||||
|
|
||||||
STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG);
|
STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class AviExtractorTest {
|
||||||
|
@Test
|
||||||
|
public void init_givenFakeExtractorOutput() {
|
||||||
|
AviExtractor aviExtractor = new AviExtractor();
|
||||||
|
FakeExtractorOutput output = new FakeExtractorOutput();
|
||||||
|
aviExtractor.init(output);
|
||||||
|
|
||||||
|
Assert.assertEquals(AviExtractor.STATE_READ_TRACKS, aviExtractor.state);
|
||||||
|
Assert.assertEquals(output, aviExtractor.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean sniff(ByteBuffer byteBuffer) {
|
||||||
|
AviExtractor aviExtractor = new AviExtractor();
|
||||||
|
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||||
|
.setData(byteBuffer.array()).build();
|
||||||
|
try {
|
||||||
|
return aviExtractor.sniff(input);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Assert.fail(e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenTooFewByte() {
|
||||||
|
Assert.assertFalse(sniff(AviExtractor.allocate(AviExtractor.PEEK_BYTES - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenAllZero() {
|
||||||
|
ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES);
|
||||||
|
Assert.assertFalse(sniff(byteBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenOnlyRiff() {
|
||||||
|
ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES);
|
||||||
|
byteBuffer.putInt(AviExtractor.RIFF);
|
||||||
|
Assert.assertFalse(sniff(byteBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenOnlyRiffAvi_() {
|
||||||
|
ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES);
|
||||||
|
byteBuffer.putInt(AviExtractor.RIFF);
|
||||||
|
byteBuffer.putInt(128);
|
||||||
|
byteBuffer.putInt(AviExtractor.AVI_);
|
||||||
|
Assert.assertFalse(sniff(byteBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenOnlyRiffAvi_List() {
|
||||||
|
ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES);
|
||||||
|
byteBuffer.putInt(AviExtractor.RIFF);
|
||||||
|
byteBuffer.putInt(128);
|
||||||
|
byteBuffer.putInt(AviExtractor.AVI_);
|
||||||
|
byteBuffer.putInt(ListBox.LIST);
|
||||||
|
Assert.assertFalse(sniff(byteBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenOnlyRiffAvi_ListHdrl() {
|
||||||
|
ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES);
|
||||||
|
byteBuffer.putInt(AviExtractor.RIFF);
|
||||||
|
byteBuffer.putInt(128);
|
||||||
|
byteBuffer.putInt(AviExtractor.AVI_);
|
||||||
|
byteBuffer.putInt(ListBox.LIST);
|
||||||
|
byteBuffer.putInt(64);
|
||||||
|
byteBuffer.putInt(ListBox.TYPE_HDRL);
|
||||||
|
Assert.assertFalse(sniff(byteBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenOnlyRiffAvi_ListHdrlAvih() {
|
||||||
|
ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES);
|
||||||
|
byteBuffer.putInt(AviExtractor.RIFF);
|
||||||
|
byteBuffer.putInt(128);
|
||||||
|
byteBuffer.putInt(AviExtractor.AVI_);
|
||||||
|
byteBuffer.putInt(ListBox.LIST);
|
||||||
|
byteBuffer.putInt(64);
|
||||||
|
byteBuffer.putInt(ListBox.TYPE_HDRL);
|
||||||
|
byteBuffer.putInt(AviHeaderBox.AVIH);
|
||||||
|
Assert.assertTrue(sniff(byteBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void toString_givenKnownString() {
|
||||||
|
final int riff = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24);
|
||||||
|
Assert.assertEquals("RIFF", AviExtractor.toString(riff));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import java.nio.BufferOverflowException;
|
||||||
|
|
||||||
|
public class BitBuffer {
|
||||||
|
private long work;
|
||||||
|
int bits;
|
||||||
|
|
||||||
|
public void push(boolean b) {
|
||||||
|
grow(1);
|
||||||
|
if (b) {
|
||||||
|
work |= 1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void grow(int bits) {
|
||||||
|
if (this.bits + bits > 64) {
|
||||||
|
throw new BufferOverflowException();
|
||||||
|
}
|
||||||
|
this.bits += bits;
|
||||||
|
work <<= bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void push(int bits, int value) {
|
||||||
|
int mask = (1 << bits) - 1;
|
||||||
|
if ((value & mask) != value) {
|
||||||
|
throw new IllegalArgumentException("Expected only " + bits + " bits, got " + value);
|
||||||
|
}
|
||||||
|
grow(bits);
|
||||||
|
work |= (value & 0xffffffffL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes() {
|
||||||
|
//Byte align
|
||||||
|
grow(8 - bits % 8);
|
||||||
|
final int count = bits / 8;
|
||||||
|
final byte[] bytes = new byte[count];
|
||||||
|
for (int i=count -1; i >= 0;i--) {
|
||||||
|
bytes[i] = (byte)(work & 0xff);
|
||||||
|
work >>=8;
|
||||||
|
}
|
||||||
|
work = 0L;
|
||||||
|
bits = 0;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
@ -50,4 +50,12 @@ public class DataHelper {
|
|||||||
bytes = Arrays.copyOf(bytes, bytes.length + 1);
|
bytes = Arrays.copyOf(bytes, bytes.length + 1);
|
||||||
return new StreamNameBox(StreamNameBox.STRN, bytes.length, ByteBuffer.wrap(bytes));
|
return new StreamNameBox(StreamNameBox.STRN, bytes.length, ByteBuffer.wrap(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ByteBuffer appendNal(final ByteBuffer byteBuffer, byte nalType) {
|
||||||
|
byteBuffer.put((byte)0);
|
||||||
|
byteBuffer.put((byte)0);
|
||||||
|
byteBuffer.put((byte) 1);
|
||||||
|
byteBuffer.put(nalType);
|
||||||
|
return byteBuffer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class MockNalChunkPeeker extends NalChunkPeeker {
|
||||||
|
private boolean skip;
|
||||||
|
public MockNalChunkPeeker(int peakSize, boolean skip) {
|
||||||
|
super(peakSize);
|
||||||
|
this.skip = skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean skip(byte nalType) {
|
||||||
|
return skip;
|
||||||
|
}
|
||||||
|
}
|
@ -1,51 +0,0 @@
|
|||||||
package com.google.android.exoplayer2.extractor.avi;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
|
||||||
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
|
||||||
import java.io.IOException;
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class Mp4vAviTrackTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void isSequenceStart_givenSequence() throws IOException {
|
|
||||||
final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump");
|
|
||||||
Assert.assertTrue(Mp4vAviTrack.isSequenceStart(input));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void findLayerStart_givenSequence() throws IOException {
|
|
||||||
final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump");
|
|
||||||
final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input,
|
|
||||||
(int)input.getLength());
|
|
||||||
//Offset 0x12
|
|
||||||
Assert.assertEquals(8, bitArray.readBits(8));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void findLayerStart_givenAllZeros() throws IOException {
|
|
||||||
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().
|
|
||||||
setData(new byte[128]).build();
|
|
||||||
Assert.assertNull(Mp4vAviTrack.findLayerStart(fakeExtractorInput, 128));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void pixelWidthHeightRatio_givenSequence() throws IOException {
|
|
||||||
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
|
||||||
final Format.Builder formatBuilder = new Format.Builder();
|
|
||||||
final Mp4vAviTrack mp4vAviTrack = new Mp4vAviTrack(0, DataHelper.getVidsStreamHeader(),
|
|
||||||
fakeTrackOutput, formatBuilder);
|
|
||||||
final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump");
|
|
||||||
mp4vAviTrack.newChunk(0, (int)input.getLength(), input);
|
|
||||||
// final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input,
|
|
||||||
// (int)input.getLength());
|
|
||||||
// mp4vAviTrack.processLayerStart(bitArray);
|
|
||||||
Assert.assertEquals(mp4vAviTrack.pixelWidthHeightRatio, 1.2121212, 0.01);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,65 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class Mp4vChunkPeekerTest {
|
||||||
|
|
||||||
|
private ByteBuffer makeSequence() {
|
||||||
|
return DataHelper.appendNal(AviExtractor.allocate(32),Mp4vChunkPeeker.SEQUENCE_START_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenNoSequence() throws IOException {
|
||||||
|
ByteBuffer byteBuffer = makeSequence();
|
||||||
|
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
|
final Format.Builder formatBuilder = new Format.Builder();
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
||||||
|
.build();
|
||||||
|
final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput);
|
||||||
|
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
||||||
|
Assert.assertEquals(1f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenAspectRatio() throws IOException {
|
||||||
|
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
|
final Format.Builder formatBuilder = new Format.Builder();
|
||||||
|
final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput);
|
||||||
|
final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump");
|
||||||
|
|
||||||
|
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
||||||
|
Assert.assertEquals(1.2121212, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenCustomAspectRatio() throws IOException {
|
||||||
|
ByteBuffer byteBuffer = makeSequence();
|
||||||
|
byteBuffer.putInt(0x5555);
|
||||||
|
DataHelper.appendNal(byteBuffer, (byte)Mp4vChunkPeeker.LAYER_START_CODE);
|
||||||
|
|
||||||
|
BitBuffer bitBuffer = new BitBuffer();
|
||||||
|
bitBuffer.push(false); //random_accessible_vol
|
||||||
|
bitBuffer.push(8, 8); //video_object_type_indication
|
||||||
|
bitBuffer.push(true); // is_object_layer_identifier
|
||||||
|
bitBuffer.push(7, 7); // video_object_layer_verid, video_object_layer_priority
|
||||||
|
bitBuffer.push(4, Mp4vChunkPeeker.Extended_PAR);
|
||||||
|
bitBuffer.push(8, 16);
|
||||||
|
bitBuffer.push(8, 9);
|
||||||
|
final byte bytes[] = bitBuffer.getBytes();
|
||||||
|
byteBuffer.put(bytes);
|
||||||
|
|
||||||
|
final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
|
final Format.Builder formatBuilder = new Format.Builder();
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array())
|
||||||
|
.build();
|
||||||
|
final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput);
|
||||||
|
mp4vChunkPeeker.peek(input, (int) input.getLength());
|
||||||
|
Assert.assertEquals(16f/9f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class NalChunkPeekerTest {
|
||||||
|
@Test
|
||||||
|
public void construct_givenTooSmallPeekSize() {
|
||||||
|
try {
|
||||||
|
new MockNalChunkPeeker(4, false);
|
||||||
|
Assert.fail();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
//Intentionally blank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void peek_givenNoData() {
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().build();
|
||||||
|
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false);
|
||||||
|
try {
|
||||||
|
peeker.peek(input, 10);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Assert.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
public void peek_givenNoNal() {
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[10]).build();
|
||||||
|
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false);
|
||||||
|
try {
|
||||||
|
peeker.peek(input, 10);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Assert.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
public void peek_givenAlwaysSkip() {
|
||||||
|
final ByteBuffer byteBuffer = AviExtractor.allocate(10);
|
||||||
|
DataHelper.appendNal(byteBuffer, (byte)32);
|
||||||
|
|
||||||
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build();
|
||||||
|
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, true);
|
||||||
|
try {
|
||||||
|
peeker.peek(input, 10);
|
||||||
|
Assert.assertEquals(0, input.getPeekPosition());
|
||||||
|
} catch (IOException e) {
|
||||||
|
Assert.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user