Refactor Clock logic. Refactor peeking for MP4V and AVC. Moved AVI above MP3.

This commit is contained in:
Dustin 2022-01-24 16:02:37 -07:00
parent 09485cbed1
commit 7ea2d75fcd
21 changed files with 1020 additions and 585 deletions

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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
}
}
}

View File

@ -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);
// }
}

View File

@ -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);
}
}
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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;
// }
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}