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.AC3,
|
||||
FileTypes.AC4,
|
||||
FileTypes.AVI,
|
||||
FileTypes.MP3,
|
||||
FileTypes.JPEG,
|
||||
FileTypes.AVI,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
@ -17,9 +17,6 @@ import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Based on the official MicroSoft spec
|
||||
@ -43,13 +40,19 @@ public class AviExtractor implements Extractor {
|
||||
}
|
||||
|
||||
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;
|
||||
private static final int STATE_FIND_MOVI = 1;
|
||||
private static final int STATE_READ_IDX1 = 2;
|
||||
private static final int STATE_READ_SAMPLES = 3;
|
||||
private static final int STATE_SEEK_START = 4;
|
||||
@VisibleForTesting
|
||||
static final int STATE_READ_TRACKS = 0;
|
||||
@VisibleForTesting
|
||||
static final int STATE_FIND_MOVI = 1;
|
||||
@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;
|
||||
|
||||
@ -68,16 +71,17 @@ public class AviExtractor implements Extractor {
|
||||
|
||||
static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds
|
||||
|
||||
private int state;
|
||||
private ExtractorOutput output;
|
||||
@VisibleForTesting
|
||||
int state;
|
||||
@VisibleForTesting
|
||||
ExtractorOutput output;
|
||||
private AviHeaderBox aviHeader;
|
||||
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
|
||||
private long moviOffset;
|
||||
private long moviEnd;
|
||||
private AviSeekMap aviSeekMap;
|
||||
private int flags;
|
||||
|
||||
// private long indexOffset; //Usually chunkStart
|
||||
|
||||
@ -99,49 +103,42 @@ public class AviExtractor implements Extractor {
|
||||
return position;
|
||||
}
|
||||
|
||||
public AviExtractor() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public AviExtractor(int flags) {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sniff(ExtractorInput input) throws IOException {
|
||||
return peekHeaderList(input);
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
*
|
||||
* @param input
|
||||
* @param bytes Must be at least 20
|
||||
*/
|
||||
@Nullable
|
||||
private ByteBuffer getAviBuffer(ExtractorInput input, int bytes) throws IOException {
|
||||
if (input.getLength() < bytes) {
|
||||
return null;
|
||||
}
|
||||
final ByteBuffer byteBuffer = allocate(bytes);
|
||||
input.peekFully(byteBuffer.array(), 0, bytes);
|
||||
final int riff = byteBuffer.getInt();
|
||||
if (riff != AviExtractor.RIFF) {
|
||||
return false;
|
||||
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");
|
||||
w("Header length doesn't match stream length");
|
||||
}
|
||||
int avi = byteBuffer.getInt();
|
||||
if (avi != AviExtractor.AVI_) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
final int list = byteBuffer.getInt();
|
||||
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;
|
||||
}
|
||||
//Len
|
||||
@ -157,27 +154,25 @@ public class AviExtractor implements Extractor {
|
||||
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
|
||||
ListBox readHeaderList(ExtractorInput input) throws IOException {
|
||||
final ByteBuffer byteBuffer = allocate(20);
|
||||
input.readFully(byteBuffer.array(), 0, byteBuffer.capacity());
|
||||
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) {
|
||||
final ByteBuffer byteBuffer = getAviBuffer(input, 20);
|
||||
if (byteBuffer == null) {
|
||||
return null;
|
||||
}
|
||||
input.skipFully(20);
|
||||
final int listSize = byteBuffer.getInt();
|
||||
final ListBox listBox = ListBox.newInstance(listSize, new BoxFactory(), input);
|
||||
if (listBox.getListType() != ListBox.TYPE_HDRL) {
|
||||
@ -196,11 +191,74 @@ public class AviExtractor implements Extractor {
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
private static Box peekNext(final List<Box> streams, int i, int type) {
|
||||
if (i + 1 < streams.size() && streams.get(i + 1).getType() == type) {
|
||||
return streams.get(i + 1);
|
||||
private void parseStream(final ListBox streamList, int streamId) {
|
||||
final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class);
|
||||
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 {
|
||||
@ -212,83 +270,15 @@ public class AviExtractor implements Extractor {
|
||||
if (aviHeader == null) {
|
||||
throw new IOException("AviHeader not found");
|
||||
}
|
||||
aviTracks = new AviTrack[aviHeader.getStreams()];
|
||||
//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;
|
||||
for (Box box : headerList.getChildren()) {
|
||||
if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) {
|
||||
final ListBox streamList = (ListBox) box;
|
||||
final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class);
|
||||
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));
|
||||
}
|
||||
parseStream(streamList, streamId);
|
||||
streamId++;
|
||||
}
|
||||
}
|
||||
@ -323,6 +313,15 @@ public class AviExtractor implements Extractor {
|
||||
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
|
||||
* @param input
|
||||
@ -330,27 +329,20 @@ public class AviExtractor implements Extractor {
|
||||
* @throws IOException
|
||||
*/
|
||||
void readIdx1(ExtractorInput input, int remaining) throws IOException {
|
||||
final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024));
|
||||
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());
|
||||
}
|
||||
}
|
||||
final AviTrack videoTrack = getVideoTrack();
|
||||
if (videoTrack == null) {
|
||||
output.seekMap(new SeekMap.Unseekable(getDuration()));
|
||||
Log.w(TAG, "No video track found");
|
||||
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 UnboundedIntArray keyFrameList = new UnboundedIntArray();
|
||||
@ -359,11 +351,11 @@ public class AviExtractor implements Extractor {
|
||||
input.readFully(bytes, indexByteBuffer.position(), toRead);
|
||||
remaining -= toRead;
|
||||
while (indexByteBuffer.remaining() >= 16) {
|
||||
final int id = indexByteBuffer.getInt();
|
||||
final AviTrack aviTrack = idTrackMap.get(id);
|
||||
final int chunkId = indexByteBuffer.getInt();
|
||||
final AviTrack aviTrack = getAviTrack(chunkId);
|
||||
if (aviTrack == null) {
|
||||
if (id != AviExtractor.REC_) {
|
||||
Log.w(TAG, "Unknown Track Type: " + toString(id));
|
||||
if (chunkId != AviExtractor.REC_) {
|
||||
Log.w(TAG, "Unknown Track Type: " + toString(chunkId));
|
||||
}
|
||||
indexByteBuffer.position(indexByteBuffer.position() + 12);
|
||||
continue;
|
||||
@ -374,56 +366,80 @@ public class AviExtractor implements Extractor {
|
||||
//int size = indexByteBuffer.getInt();
|
||||
if (aviTrack.isVideo()) {
|
||||
if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
|
||||
keyFrameList.add(aviTrack.frame);
|
||||
keyFrameList.add(chunkCounts[aviTrack.id]);
|
||||
}
|
||||
if (aviTrack.frame % seekFrameRate == 0) {
|
||||
|
||||
videoSeekOffset.add(offset);
|
||||
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
|
||||
final int audioId = entry.getKey();
|
||||
final UnboundedIntArray videoFrameMap = entry.getValue();
|
||||
final AviTrack audioTrack = idTrackMap.get(audioId);
|
||||
videoFrameMap.add(audioTrack.frame);
|
||||
if (chunkCounts[aviTrack.id] % seekFrameRate == 0) {
|
||||
seekOffsets[aviTrack.id].add(offset);
|
||||
for (int i=0;i<seekOffsets.length;i++) {
|
||||
if (i != aviTrack.id) {
|
||||
seekOffsets[i].add(chunkCounts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
aviTrack.advance();
|
||||
chunkCounts[aviTrack.id]++;
|
||||
}
|
||||
indexByteBuffer.compact();
|
||||
}
|
||||
videoSeekOffset.pack();
|
||||
//Set the keys frames
|
||||
if (!videoTrack.isAllKeyFrames()) {
|
||||
keyFrameList.pack();
|
||||
final int[] keyFrames = keyFrameList.getArray();
|
||||
videoTrack.setKeyFrames(keyFrames);
|
||||
}
|
||||
|
||||
//Correct the timings
|
||||
durationUs = videoTrack.usPerSample * videoTrack.frame;
|
||||
durationUs = chunkCounts[videoTrack.id] * videoTrack.getClock().usPerChunk;
|
||||
|
||||
final SparseArray<int[]> idFrameArray = new SparseArray<>();
|
||||
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
|
||||
entry.getValue().pack();
|
||||
idFrameArray.put(entry.getKey(), entry.getValue().getArray());
|
||||
final AviTrack aviTrack = idTrackMap.get(entry.getKey());
|
||||
//Sometimes this value is way off
|
||||
long calcUsPerSample = (getDuration()/aviTrack.frame);
|
||||
float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample;
|
||||
if (deltaPercent >.01) {
|
||||
aviTrack.usPerSample = getDuration()/aviTrack.frame;
|
||||
Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame));
|
||||
for (int i=0;i<chunkCounts.length;i++) {
|
||||
final AviTrack aviTrack = aviTracks[i];
|
||||
if (aviTrack != null && aviTrack.isAudio()) {
|
||||
final long calcUsPerSample = (durationUs/chunkCounts[i]);
|
||||
final LinearClock linearClock = aviTrack.getClock();
|
||||
final float deltaPercent = Math.abs(calcUsPerSample - linearClock.usPerChunk) / (float)linearClock.usPerChunk;
|
||||
if (deltaPercent >.01) {
|
||||
Log.i(TAG, "Updating stream " + i + " calcUsPerSample=" + calcUsPerSample + " reported=" + linearClock.usPerChunk);
|
||||
linearClock.usPerChunk = calcUsPerSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.getArray(),
|
||||
idFrameArray, moviOffset, getDuration());
|
||||
final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekOffsets, seekFrameRate, moviOffset, getDuration());
|
||||
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 {
|
||||
if (chunkHandler != null) {
|
||||
if (chunkHandler.resume(input)) {
|
||||
chunkHandler = null;
|
||||
return checkAlign(input, seekPosition);
|
||||
}
|
||||
} else {
|
||||
ByteBuffer byteBuffer = allocate(8);
|
||||
@ -437,24 +453,27 @@ public class AviExtractor implements Extractor {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
input.readFully(bytes, 1, 7);
|
||||
final int id = byteBuffer.getInt();
|
||||
final int size = byteBuffer.getInt();
|
||||
AviTrack sampleTrack = idTrackMap.get(id);
|
||||
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);
|
||||
}
|
||||
}
|
||||
final int chunkId = byteBuffer.getInt();
|
||||
if (chunkId == ListBox.LIST) {
|
||||
seekPosition.position = input.getPosition() + 8;
|
||||
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 {
|
||||
if (!sampleTrack.newChunk(id, size, input)) {
|
||||
chunkHandler = sampleTrack;
|
||||
}
|
||||
chunkHandler = aviTrack;
|
||||
}
|
||||
}
|
||||
return RESULT_CONTINUE;
|
||||
@ -489,7 +508,6 @@ public class AviExtractor implements Extractor {
|
||||
state = STATE_READ_SAMPLES;
|
||||
return RESULT_SEEK;
|
||||
}
|
||||
|
||||
}
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
@ -499,24 +517,34 @@ public class AviExtractor implements Extractor {
|
||||
chunkHandler = null;
|
||||
if (position <= 0) {
|
||||
if (moviOffset != 0) {
|
||||
resetFrames();
|
||||
resetClocks();
|
||||
state = STATE_SEEK_START;
|
||||
}
|
||||
} else {
|
||||
if (aviSeekMap != null) {
|
||||
aviSeekMap.setFrames(position, timeUs, idTrackMap);
|
||||
aviSeekMap.setFrames(position, timeUs, aviTracks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void resetFrames() {
|
||||
for (int i=0;i<idTrackMap.size();i++) {
|
||||
final AviTrack aviTrack = idTrackMap.valueAt(i);
|
||||
aviTrack.seekFrame(0);
|
||||
void resetClocks() {
|
||||
for (@Nullable AviTrack aviTrack : aviTracks) {
|
||||
if (aviTrack != null) {
|
||||
aviTrack.getClock().setIndex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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 {
|
||||
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);
|
||||
|
||||
//AVIMAINHEADER
|
||||
@ -32,20 +32,29 @@ public class AviHeaderBox extends ResidentBox {
|
||||
return byteBuffer.getInt(12);
|
||||
}
|
||||
|
||||
int getFrames() {
|
||||
int getTotalFrames() {
|
||||
return byteBuffer.getInt(16);
|
||||
}
|
||||
//20 = dwInitialFrames
|
||||
|
||||
int getSuggestedBufferSize() {
|
||||
// 20 - dwInitialFrames
|
||||
// int getInitialFrames() {
|
||||
// return byteBuffer.getInt(20);
|
||||
// }
|
||||
|
||||
int getStreams() {
|
||||
return byteBuffer.getInt(24);
|
||||
}
|
||||
|
||||
int getWidth() {
|
||||
return byteBuffer.getInt(28);
|
||||
}
|
||||
|
||||
int getHeight() {
|
||||
return byteBuffer.getInt(32);
|
||||
}
|
||||
// 28 - dwSuggestedBufferSize
|
||||
// int getSuggestedBufferSize() {
|
||||
// return byteBuffer.getInt(28);
|
||||
// }
|
||||
//
|
||||
// int getWidth() {
|
||||
// return byteBuffer.getInt(32);
|
||||
// }
|
||||
//
|
||||
// int getHeight() {
|
||||
// return byteBuffer.getInt(36);
|
||||
// }
|
||||
}
|
||||
|
@ -1,33 +1,35 @@
|
||||
package com.google.android.exoplayer2.extractor.avi;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
|
||||
public class AviSeekMap implements SeekMap {
|
||||
final AviTrack videoTrack;
|
||||
final long videoUsPerChunk;
|
||||
final int videoStreamId;
|
||||
/**
|
||||
* Number of frames per index
|
||||
* i.e. videoFrameOffsetMap[1] is frame 1 * seekIndexFactor
|
||||
*/
|
||||
final int seekIndexFactor;
|
||||
//Map from the Video Frame index to the offset
|
||||
final int[] videoFrameOffsetMap;
|
||||
//Seek offsets by streamId, for video, this is the actual offset, for audio, this is the chunkId
|
||||
final int[][] seekOffsets;
|
||||
//Holds a map of video frameIds to audioFrameIds for each audioId
|
||||
final SparseArray<int[]> audioIdMap;
|
||||
|
||||
final long moviOffset;
|
||||
final long duration;
|
||||
|
||||
public AviSeekMap(AviTrack videoTrack, int seekIndexFactor, int[] videoFrameOffsetMap,
|
||||
SparseArray<int[]> audioIdMap, long moviOffset, long duration) {
|
||||
this.videoTrack = videoTrack;
|
||||
public AviSeekMap(AviTrack videoTrack, UnboundedIntArray[] seekOffsets, int seekIndexFactor, long moviOffset, long duration) {
|
||||
videoUsPerChunk = videoTrack.getClock().usPerChunk;
|
||||
videoStreamId = videoTrack.id;
|
||||
this.seekIndexFactor = seekIndexFactor;
|
||||
this.videoFrameOffsetMap = videoFrameOffsetMap;
|
||||
this.audioIdMap = audioIdMap;
|
||||
this.moviOffset = moviOffset;
|
||||
this.duration = duration;
|
||||
this.seekOffsets = new int[seekOffsets.length][];
|
||||
for (int i=0;i<seekOffsets.length;i++) {
|
||||
this.seekOffsets[i] = seekOffsets[i].getArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -41,10 +43,10 @@ public class AviSeekMap implements SeekMap {
|
||||
}
|
||||
|
||||
private int getSeekFrameIndex(long timeUs) {
|
||||
final int reqFrame = (int)(timeUs / videoTrack.usPerSample);
|
||||
final int reqFrame = (int)(timeUs / videoUsPerChunk);
|
||||
int reqFrameIndex = reqFrame / seekIndexFactor;
|
||||
if (reqFrameIndex >= videoFrameOffsetMap.length) {
|
||||
reqFrameIndex = videoFrameOffsetMap.length - 1;
|
||||
if (reqFrameIndex >= seekOffsets[videoStreamId].length) {
|
||||
reqFrameIndex = seekOffsets[videoStreamId].length - 1;
|
||||
}
|
||||
return reqFrameIndex;
|
||||
}
|
||||
@ -53,23 +55,29 @@ public class AviSeekMap implements SeekMap {
|
||||
@Override
|
||||
public SeekPoints getSeekPoints(long timeUs) {
|
||||
final int seekFrameIndex = getSeekFrameIndex(timeUs);
|
||||
int offset = videoFrameOffsetMap[seekFrameIndex];
|
||||
final long outUs = seekFrameIndex * seekIndexFactor * videoTrack.usPerSample;
|
||||
int offset = seekOffsets[videoStreamId][seekFrameIndex];
|
||||
final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk;
|
||||
final long position = offset + moviOffset;
|
||||
Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
||||
|
||||
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);
|
||||
videoTrack.seekFrame(seekFrameIndex * seekIndexFactor);
|
||||
for (int i=0;i<audioIdMap.size();i++) {
|
||||
final int audioId = audioIdMap.keyAt(i);
|
||||
final int[] video2AudioFrameMap = audioIdMap.get(audioId);
|
||||
final AviTrack audioTrack = idTrackMap.get(audioId);
|
||||
audioTrack.frame = video2AudioFrameMap[seekFrameIndex];
|
||||
for (int i=0;i<aviTracks.length;i++) {
|
||||
final AviTrack aviTrack = aviTracks[i];
|
||||
if (aviTrack != null) {
|
||||
final LinearClock clock = aviTrack.getClock();
|
||||
if (aviTrack.isVideo()) {
|
||||
//TODO: Although this works, it leads to partial frames being painted
|
||||
aviTrack.setForceKeyFrame(true);
|
||||
clock.setIndex(seekFrameIndex * seekIndexFactor);
|
||||
} else {
|
||||
final int offset = seekOffsets[i][seekFrameIndex];
|
||||
clock.setIndex(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,13 +18,19 @@ public class AviTrack {
|
||||
@NonNull
|
||||
final StreamHeaderBox streamHeaderBox;
|
||||
|
||||
long usPerSample;
|
||||
@NonNull
|
||||
LinearClock clock;
|
||||
|
||||
@Nullable
|
||||
ChunkPeeker chunkPeeker;
|
||||
|
||||
/**
|
||||
* True indicates all frames are key frames (e.g. Audio, MJPEG)
|
||||
*/
|
||||
boolean allKeyFrames;
|
||||
|
||||
boolean forceKeyFrame;
|
||||
|
||||
@NonNull
|
||||
TrackOutput trackOutput;
|
||||
|
||||
@ -37,19 +43,24 @@ public class AviTrack {
|
||||
transient int chunkSize;
|
||||
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) {
|
||||
this.id = id;
|
||||
this.trackOutput = trackOutput;
|
||||
this.streamHeaderBox = streamHeaderBox;
|
||||
this.usPerSample = streamHeaderBox.getUsPerSample();
|
||||
this.allKeyFrames = streamHeaderBox.isAudio() || (MimeTypes.IMAGE_JPEG.equals(streamHeaderBox.getMimeType()));
|
||||
clock = new LinearClock(streamHeaderBox.getUsPerSample());
|
||||
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() {
|
||||
@ -60,25 +71,26 @@ public class AviTrack {
|
||||
if (allKeyFrames) {
|
||||
return true;
|
||||
}
|
||||
if (forceKeyFrame) {
|
||||
forceKeyFrame = false;
|
||||
return true;
|
||||
}
|
||||
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
|
||||
return frame == 0;
|
||||
//return clock.getIndex() == 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setForceKeyFrame(boolean v) {
|
||||
forceKeyFrame = v;
|
||||
}
|
||||
|
||||
public void setKeyFrames(int[] keyFrames) {
|
||||
this.keyFrames = keyFrames;
|
||||
}
|
||||
|
||||
public long getUs() {
|
||||
return getUs(getUsFrame());
|
||||
}
|
||||
|
||||
public long getUs(final int myFrame) {
|
||||
return myFrame * usPerSample;
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return streamHeaderBox.isVideo();
|
||||
}
|
||||
@ -87,23 +99,10 @@ public class AviTrack {
|
||||
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 {
|
||||
if (chunkPeeker != null) {
|
||||
chunkPeeker.peek(input, size);
|
||||
}
|
||||
final int remaining = size - trackOutput.sampleData(input, size, false);
|
||||
if (remaining == 0) {
|
||||
done(size);
|
||||
@ -127,8 +126,8 @@ public class AviTrack {
|
||||
|
||||
void done(final int size) {
|
||||
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());
|
||||
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(XVID, 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);
|
||||
}
|
||||
|
@ -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);
|
||||
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