Commit a9c94185 by Dustin

Working!

parent 33d22a12
Showing with 571 additions and 124 deletions
...@@ -5,14 +5,16 @@ import com.google.android.exoplayer2.util.MimeTypes; ...@@ -5,14 +5,16 @@ import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class AudioFormat { public class AudioFormat {
public static short WAVE_FORMAT_PCM = 1; public static final short WAVE_FORMAT_PCM = 1;
public static short WAVE_FORMAT_MPEGLAYER3 = 0x55; private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55;
public static short WAVE_FORMAT_DVM = 0x2000; //AC3 private static final short WAVE_FORMAT_AAC = 0xff;
public static short WAVE_FORMAT_DTS2 = 0x2001; //DTS private static final short WAVE_FORMAT_DVM = 0x2000; //AC3
private static final short WAVE_FORMAT_DTS2 = 0x2001; //DTS
private static final SparseArray<String> FORMAT_MAP = new SparseArray<>(); private static final SparseArray<String> FORMAT_MAP = new SparseArray<>();
static { static {
FORMAT_MAP.put(WAVE_FORMAT_PCM, MimeTypes.AUDIO_RAW); FORMAT_MAP.put(WAVE_FORMAT_PCM, MimeTypes.AUDIO_RAW);
FORMAT_MAP.put(WAVE_FORMAT_MPEGLAYER3, MimeTypes.AUDIO_MPEG); FORMAT_MAP.put(WAVE_FORMAT_MPEGLAYER3, MimeTypes.AUDIO_MPEG);
FORMAT_MAP.put(WAVE_FORMAT_AAC, MimeTypes.AUDIO_AAC);
FORMAT_MAP.put(WAVE_FORMAT_DVM, MimeTypes.AUDIO_AC3); FORMAT_MAP.put(WAVE_FORMAT_DVM, MimeTypes.AUDIO_AC3);
FORMAT_MAP.put(WAVE_FORMAT_DTS2, MimeTypes.AUDIO_DTS); FORMAT_MAP.put(WAVE_FORMAT_DTS2, MimeTypes.AUDIO_DTS);
} }
...@@ -24,7 +26,7 @@ public class AudioFormat { ...@@ -24,7 +26,7 @@ public class AudioFormat {
this.byteBuffer = byteBuffer; this.byteBuffer = byteBuffer;
} }
public String getCodec() { public String getMimeType() {
return FORMAT_MAP.get(getFormatTag() & 0xffff); return FORMAT_MAP.get(getFormatTag() & 0xffff);
} }
...@@ -38,12 +40,24 @@ public class AudioFormat { ...@@ -38,12 +40,24 @@ public class AudioFormat {
return byteBuffer.getInt(4); return byteBuffer.getInt(4);
} }
// 8 - nAvgBytesPerSec(uint) // 8 - nAvgBytesPerSec(uint)
// 12 - nBlockAlign(ushort) public int getBlockAlign() {
return byteBuffer.getShort(12);
}
public short getBitsPerSample() { public short getBitsPerSample() {
return byteBuffer.getShort(14); return byteBuffer.getShort(14);
} }
public short getCbSize() { public int getCbSize() {
return byteBuffer.getShort(16); return byteBuffer.getShort(16) & 0xffff;
}
public byte[] getCodecData() {
final int size = getCbSize();
final ByteBuffer temp = byteBuffer.duplicate();
temp.clear();
temp.position(18);
temp.limit(18 + size);
final byte[] data = new byte[size];
temp.get(data);
return data;
} }
//TODO: Deal with WAVEFORMATEXTENSIBLE //TODO: Deal with WAVEFORMATEXTENSIBLE
} }
package com.google.android.exoplayer2.extractor.avi; package com.google.android.exoplayer2.extractor.avi;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -10,20 +13,34 @@ import com.google.android.exoplayer2.extractor.PositionHolder; ...@@ -10,20 +13,34 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Based on the official MicroSoft spec
* https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
*/
public class AviExtractor implements Extractor { public class AviExtractor implements Extractor {
static final String TAG = "AviExtractor"; static final String TAG = "AviExtractor";
static final int KEY_FRAME_MASK = Integer.MIN_VALUE;
private static final int PEEK_BYTES = 28; private static final int PEEK_BYTES = 28;
private final int STATE_READ_TRACKS = 0; private static final int STATE_READ_TRACKS = 0;
private final int STATE_FIND_MOVI = 1; private static final int STATE_FIND_MOVI = 1;
private final int STATE_FIND_IDX1 = 2; private static final int STATE_READ_IDX1 = 2;
private final int STATE_READ_SAMPLES = 3; private static final int STATE_READ_SAMPLES = 3;
private static final int STATE_SEEK_START = 4;
private static final int AVIIF_KEYFRAME = 16;
static final int RIFF = AviUtil.toInt(new byte[]{'R','I','F','F'}); static final int RIFF = AviUtil.toInt(new byte[]{'R','I','F','F'});
static final int AVI_ = AviUtil.toInt(new byte[]{'A','V','I',' '}); static final int AVI_ = AviUtil.toInt(new byte[]{'A','V','I',' '});
...@@ -36,13 +53,26 @@ public class AviExtractor implements Extractor { ...@@ -36,13 +53,26 @@ public class AviExtractor implements Extractor {
//Index //Index
static final int IDX1 = 'i' | ('d' << 8) | ('x' << 16) | ('1' << 24); static final int IDX1 = 'i' | ('d' << 8) | ('x' << 16) | ('1' << 24);
private final int flags; static final int JUNK = 'J' | ('U' << 8) | ('N' << 16) | ('K' << 24);
static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds
private int state; private int state;
private ExtractorOutput output; private ExtractorOutput output;
private AviHeader aviHeader; private AviHeaderBox aviHeader;
private SparseArray<AviTrack> idTrackMap = new SparseArray<>();
//After the movi position //After the movi position
private long firstChunkPosition; private long moviOffset;
private long moviEnd;
private AviSeekMap aviSeekMap;
private int flags;
// private long indexOffset; //Usually chunkStart
//If partial read
private transient AviTrack sampleTrack;
private transient int sampleRemaining;
private transient int sampleSize;
public AviExtractor() { public AviExtractor() {
this(0); this(0);
...@@ -63,6 +93,12 @@ public class AviExtractor implements Extractor { ...@@ -63,6 +93,12 @@ public class AviExtractor implements Extractor {
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
return byteBuffer; return byteBuffer;
} }
private void setSeekMap(AviSeekMap aviSeekMap) {
this.aviSeekMap = aviSeekMap;
output.seekMap(aviSeekMap);
}
boolean peakHeaderList(ExtractorInput input) throws IOException { boolean peakHeaderList(ExtractorInput input) throws IOException {
final ByteBuffer byteBuffer = allocate(PEEK_BYTES); final ByteBuffer byteBuffer = allocate(PEEK_BYTES);
input.peekFully(byteBuffer.array(), 0, PEEK_BYTES); input.peekFully(byteBuffer.array(), 0, PEEK_BYTES);
...@@ -80,23 +116,23 @@ public class AviExtractor implements Extractor { ...@@ -80,23 +116,23 @@ public class AviExtractor implements Extractor {
return false; return false;
} }
final int list = byteBuffer.getInt(); final int list = byteBuffer.getInt();
if (list != IAviList.LIST) { if (list != ListBox.LIST) {
return false; return false;
} }
//Len //Len
byteBuffer.getInt(); byteBuffer.getInt();
final int hdrl = byteBuffer.getInt(); final int hdrl = byteBuffer.getInt();
if (hdrl != IAviList.TYPE_HDRL) { if (hdrl != ListBox.TYPE_HDRL) {
return false; return false;
} }
final int avih = byteBuffer.getInt(); final int avih = byteBuffer.getInt();
if (avih != AviHeader.AVIH) { if (avih != AviHeaderBox.AVIH) {
return false; return false;
} }
return true; return true;
} }
@Nullable @Nullable
ResidentList readHeaderList(ExtractorInput input) throws IOException { ListBox readHeaderList(ExtractorInput input) throws IOException {
final ByteBuffer byteBuffer = allocate(20); final ByteBuffer byteBuffer = allocate(20);
input.readFully(byteBuffer.array(), 0, byteBuffer.capacity()); input.readFully(byteBuffer.array(), 0, byteBuffer.capacity());
final int riff = byteBuffer.getInt(); final int riff = byteBuffer.getInt();
...@@ -112,12 +148,12 @@ public class AviExtractor implements Extractor { ...@@ -112,12 +148,12 @@ public class AviExtractor implements Extractor {
if (avi != AviExtractor.AVI_) { if (avi != AviExtractor.AVI_) {
return null; return null;
} }
final ResidentList header = ResidentList.getInstance(byteBuffer, input, ResidentList.class); final ListBox header = ListBox.getInstance(byteBuffer, input, ListBox.class);
if (header == null) { if (header == null) {
return null; return null;
} }
if (header.getListType() != IAviList.TYPE_HDRL) { if (header.getListType() != ListBox.TYPE_HDRL) {
Log.e(TAG, "Expected " +AviUtil.toString(IAviList.TYPE_HDRL) + ", got: " + Log.e(TAG, "Expected " +AviUtil.toString(ListBox.TYPE_HDRL) + ", got: " +
AviUtil.toString(header.getType())); AviUtil.toString(header.getType()));
return null; return null;
} }
...@@ -145,12 +181,13 @@ public class AviExtractor implements Extractor { ...@@ -145,12 +181,13 @@ public class AviExtractor implements Extractor {
} }
private int readTracks(ExtractorInput input) throws IOException { private int readTracks(ExtractorInput input) throws IOException {
final ResidentList headerList = readHeaderList(input); final ListBox headerList = readHeaderList(input);
if (headerList == null) { if (headerList == null) {
throw new IOException("AVI Header List not found"); throw new IOException("AVI Header List not found");
} }
final List<ResidentBox> headerChildren = headerList.getBoxList(); final BoxFactory boxFactory = new BoxFactory();
aviHeader = AviUtil.getBox(headerChildren, AviHeader.class); final List<ResidentBox> headerChildren = headerList.getBoxList(boxFactory);
aviHeader = AviUtil.getBox(headerChildren, AviHeaderBox.class);
if (aviHeader == null) { if (aviHeader == null) {
throw new IOException("AviHeader not found"); throw new IOException("AviHeader not found");
} }
...@@ -159,14 +196,14 @@ public class AviExtractor implements Extractor { ...@@ -159,14 +196,14 @@ public class AviExtractor implements Extractor {
int streamId = 0; int streamId = 0;
for (Box box : headerChildren) { for (Box box : headerChildren) {
if (box instanceof ResidentList && ((ResidentList) box).getListType() == STRL) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) {
final ResidentList streamList = (ResidentList) box; final ListBox streamList = (ListBox) box;
final List<ResidentBox> streamChildren = streamList.getBoxList(); final List<ResidentBox> streamChildren = streamList.getBoxList(boxFactory);
for (int i=0;i<streamChildren.size();i++) { for (int i=0;i<streamChildren.size();i++) {
final ResidentBox residentBox = streamChildren.get(i); final ResidentBox residentBox = streamChildren.get(i);
if (residentBox instanceof StreamHeader) { if (residentBox instanceof StreamHeaderBox) {
final StreamHeader streamHeader = (StreamHeader) residentBox; final StreamHeaderBox streamHeader = (StreamHeaderBox) residentBox;
final StreamFormat streamFormat = (StreamFormat) peekNext(streamChildren, i, StreamFormat.STRF); final StreamFormatBox streamFormat = (StreamFormatBox) peekNext(streamChildren, i, StreamFormatBox.STRF);
if (streamFormat != null) { if (streamFormat != null) {
i++; i++;
if (streamHeader.isVideo()) { if (streamHeader.isVideo()) {
...@@ -184,14 +221,26 @@ public class AviExtractor implements Extractor { ...@@ -184,14 +221,26 @@ public class AviExtractor implements Extractor {
builder.setWidth(videoFormat.getWidth()); builder.setWidth(videoFormat.getWidth());
builder.setHeight(videoFormat.getHeight()); builder.setHeight(videoFormat.getHeight());
builder.setFrameRate(streamHeader.getFrameRate()); builder.setFrameRate(streamHeader.getFrameRate());
builder.setCodecs(streamHeader.getCodec()); final String mimeType = streamHeader.getMimeType();
builder.setInitializationData(codecData); builder.setSampleMimeType(mimeType);
if (MimeTypes.VIDEO_H263.equals(mimeType)) {
builder.setSelectionFlags(C.SELECTION_FLAG_FORCED);
}
//builder.setCodecs(streamHeader.getCodec());
if (codecData != null) {
builder.setInitializationData(codecData);
}
trackOutput.format(builder.build()); trackOutput.format(builder.build());
idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24),
new AviTrack(streamId, trackOutput,
streamHeader));
} else if (streamHeader.isAudio()) { } else if (streamHeader.isAudio()) {
final AudioFormat audioFormat = streamFormat.getAudioFormat(); final AudioFormat audioFormat = streamFormat.getAudioFormat();
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO);
final Format.Builder builder = new Format.Builder(); final Format.Builder builder = new Format.Builder();
builder.setCodecs(audioFormat.getCodec()); final String mimeType = audioFormat.getMimeType();
builder.setSampleMimeType(mimeType);
//builder.setCodecs(audioFormat.getCodec());
builder.setChannelCount(audioFormat.getChannels()); builder.setChannelCount(audioFormat.getChannels());
builder.setSampleRate(audioFormat.getSamplesPerSecond()); builder.setSampleRate(audioFormat.getSamplesPerSecond());
if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) { if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) {
...@@ -203,7 +252,12 @@ public class AviExtractor implements Extractor { ...@@ -203,7 +252,12 @@ public class AviExtractor implements Extractor {
builder.setPcmEncoding(C.ENCODING_PCM_16BIT); 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()); trackOutput.format(builder.build());
idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24),
new AviTrack(streamId, trackOutput, streamHeader));
} }
} }
streamId++; streamId++;
...@@ -224,16 +278,17 @@ public class AviExtractor implements Extractor { ...@@ -224,16 +278,17 @@ public class AviExtractor implements Extractor {
final long position = input.getPosition(); final long position = input.getPosition();
//-4 because we over read for the LIST type //-4 because we over read for the LIST type
long nextBox = position + size - 4; long nextBox = position + size - 4;
if (tag == IAviList.LIST) { if (tag == ListBox.LIST) {
final int listType = byteBuffer.getInt(); final int listType = byteBuffer.getInt();
if (listType == MOVI) { if (listType == MOVI) {
firstChunkPosition = position; moviOffset = position - 4;
moviEnd = moviOffset + size;
if (aviHeader.hasIndex()) { if (aviHeader.hasIndex()) {
state = STATE_FIND_IDX1; state = STATE_READ_IDX1;
} else { } else {
output.seekMap(new SeekMap.Unseekable(getDuration())); output.seekMap(new SeekMap.Unseekable(getDuration()));
state = STATE_READ_TRACKS; state = STATE_READ_TRACKS;
nextBox = firstChunkPosition; nextBox = moviOffset + 4;
} }
} }
} }
...@@ -241,54 +296,194 @@ public class AviExtractor implements Extractor { ...@@ -241,54 +296,194 @@ public class AviExtractor implements Extractor {
return RESULT_SEEK; return RESULT_SEEK;
} }
int findIdx1(ExtractorInput input, PositionHolder seekPosition) throws IOException { /**
ByteBuffer byteBuffer = allocate(8); * Reads the index and sets the keyFrames and creates the SeekMap
input.readFully(byteBuffer.array(), 0,8); * @param input
final int tag = byteBuffer.getInt(); * @param remaining
long remaining = byteBuffer.getInt() & AviUtil.UINT_MASK; * @throws IOException
//TODO: Sanity check on file length */
if (tag == IDX1) { void readIdx1(ExtractorInput input, int remaining) throws IOException {
final ByteBuffer index = allocate(4096); final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024));
final byte[] bytes = index.array(); final byte[] bytes = indexByteBuffer.array();
index.position(index.capacity());
while (remaining > 0) { final HashMap<Integer, UnboundedIntArray> audioIdFrameMap = new HashMap<>();
if (!index.hasRemaining()) { AviTrack videoTrack = null;
index.clear(); //Video seek offsets
final int toRead = (int)Math.min(4096, remaining); UnboundedIntArray videoSeekOffset = new UnboundedIntArray();
if (!input.readFully(bytes, 0, toRead, true)) { for (int i=0;i<idTrackMap.size();i++) {
seekPosition.position = firstChunkPosition; final AviTrack aviTrack = idTrackMap.valueAt(i);
output.seekMap(new SeekMap.Unseekable(getDuration())); if (videoTrack == null && aviTrack.isVideo()) {
break; videoTrack = aviTrack;
} else {
audioIdFrameMap.put(idTrackMap.keyAt(i), new UnboundedIntArray());
}
}
if (videoTrack == null) {
output.seekMap(new SeekMap.Unseekable(getDuration()));
Log.w(TAG, "No video track found");
return;
}
resetFrames();
final int seekFrameRate = (int)(videoTrack.streamHeaderBox.getFrameRate() * 2);
final UnboundedIntArray keyFrameList = new UnboundedIntArray();
while (remaining > 0) {
final int toRead = Math.min(indexByteBuffer.remaining(), remaining);
input.readFully(bytes, indexByteBuffer.position(), toRead);
remaining -= toRead;
while (indexByteBuffer.remaining() >= 16) {
final int id = indexByteBuffer.getInt();
final AviTrack aviTrack = idTrackMap.get(id);
if (aviTrack == null) {
Log.w(TAG, "Unknown Track Type: " + AviUtil.toString(id));
indexByteBuffer.position(indexByteBuffer.position() + 12);
continue;
}
final int flags = indexByteBuffer.getInt();
final int offset = indexByteBuffer.getInt();
indexByteBuffer.position(indexByteBuffer.position() + 4);
//int size = indexByteBuffer.getInt();
if (aviTrack.isVideo()) {
if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
keyFrameList.add(aviTrack.frame);
}
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);
}
} }
index.limit(toRead);
remaining -=toRead;
} }
aviTrack.advance();
}
indexByteBuffer.compact();
}
videoSeekOffset.pack();
keyFrameList.pack();
final int[] keyFrames = keyFrameList.array;
videoTrack.setKeyFrames(keyFrames);
final SparseArray<int[]> idFrameArray = new SparseArray<>();
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
entry.getValue().pack();
idFrameArray.put(entry.getKey(), entry.getValue().array);
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));
} }
//TODO }
final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.array,
idFrameArray, moviOffset, getDuration());
setSeekMap(seekMap);
resetFrames();
}
int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException {
if (sampleRemaining != 0) {
sampleRemaining -= sampleTrack.trackOutput.sampleData(input, sampleRemaining, false);
} else { } else {
seekPosition.position = input.getPosition() + remaining; ByteBuffer byteBuffer = allocate(8);
final byte[] bytes = byteBuffer.array();
// This isn't documented anywhere, but most files are aligned to even bytes
// and can have gaps of zeros
if ((input.getPosition() & 1) == 1) {
input.skipFully(1);
}
input.readFully(bytes, 0, 1);
while (bytes[0] == 0) {
input.readFully(bytes, 0, 1);
}
if (input.getPosition() >= moviEnd) {
return RESULT_END_OF_INPUT;
}
input.readFully(bytes, 1, 7);
int id = byteBuffer.getInt();
sampleSize = byteBuffer.getInt();
sampleTrack = idTrackMap.get(id);
if (sampleTrack == null) {
if (id != JUNK) {
Log.w(TAG, "Unknown tag=" + AviUtil.toString(id) + " pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " moviEnd=" + moviEnd);
}
seekPosition.position = input.getPosition() + sampleSize;
return RESULT_SEEK;
} else {
//Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo());
sampleRemaining = sampleSize - sampleTrack.trackOutput.sampleData(input, sampleSize, false);
}
} }
return RESULT_SEEK; if (sampleRemaining != 0) {
return RESULT_CONTINUE;
}
sampleTrack.trackOutput.sampleMetadata(
sampleTrack.getUs(), sampleTrack.isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0 , sampleSize, 0, null);
//Log.d(TAG, "Frame: " + (sampleTrack.isVideo()? 'V' : 'A') + " us=" + sampleTrack.getUs());
sampleTrack.advance();
return RESULT_CONTINUE;
} }
@Override @Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException {
switch (state) { switch (state) {
case STATE_READ_SAMPLES:
return readSamples(input, seekPosition);
case STATE_SEEK_START:
state = STATE_READ_SAMPLES;
seekPosition.position = moviOffset + 4;
return RESULT_SEEK;
case STATE_READ_TRACKS: case STATE_READ_TRACKS:
return readTracks(input); return readTracks(input);
case STATE_FIND_MOVI: case STATE_FIND_MOVI:
return findMovi(input, seekPosition); return findMovi(input, seekPosition);
case STATE_FIND_IDX1: case STATE_READ_IDX1: {
return findIdx1(input, seekPosition); if (aviHeader.hasIndex()) {
ByteBuffer byteBuffer = allocate(8);
input.readFully(byteBuffer.array(), 0,8);
final int tag = byteBuffer.getInt();
final int size = byteBuffer.getInt();
if (tag == IDX1) {
readIdx1(input, size);
}
} else {
output.seekMap(new SeekMap.Unseekable(getDuration()));
}
seekPosition.position = moviOffset + 4;
state = STATE_READ_SAMPLES;
return RESULT_SEEK;
}
} }
return RESULT_CONTINUE; return RESULT_CONTINUE;
} }
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
Log.d("Test", "Seek: pos=" + position + " us=" + timeUs);
if (position == 0) {
if (moviOffset != 0) {
resetFrames();
state = STATE_SEEK_START;
}
} else {
if (aviSeekMap != null) {
aviSeekMap.setFrames(position, timeUs, idTrackMap);
}
}
}
void resetFrames() {
for (int i=0;i<idTrackMap.size();i++) {
final AviTrack aviTrack = idTrackMap.valueAt(i);
aviTrack.frame = 0;
}
} }
@Override @Override
......
...@@ -2,13 +2,13 @@ package com.google.android.exoplayer2.extractor.avi; ...@@ -2,13 +2,13 @@ package com.google.android.exoplayer2.extractor.avi;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class AviHeader extends ResidentBox { public class AviHeaderBox extends ResidentBox {
public static final int AVIF_HASINDEX = 0x10; public static final int AVIF_HASINDEX = 0x10;
static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24); static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24);
//AVIMAINHEADER //AVIMAINHEADER
AviHeader(int type, int size, ByteBuffer byteBuffer) { AviHeaderBox(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer); super(type, size, byteBuffer);
} }
......
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;
/**
* 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;
//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;
this.seekIndexFactor = seekIndexFactor;
this.videoFrameOffsetMap = videoFrameOffsetMap;
this.audioIdMap = audioIdMap;
this.moviOffset = moviOffset;
this.duration = duration;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getDurationUs() {
return duration;
}
private int getSeekFrameIndex(long timeUs) {
final int reqFrame = (int)(timeUs / videoTrack.usPerSample);
int reqFrameIndex = reqFrame / seekIndexFactor;
if (reqFrameIndex >= videoFrameOffsetMap.length) {
reqFrameIndex = videoFrameOffsetMap.length - 1;
}
return reqFrameIndex;
}
@NonNull
@Override
public SeekPoints getSeekPoints(long timeUs) {
final int seekFrameIndex = getSeekFrameIndex(timeUs);
int offset = videoFrameOffsetMap[seekFrameIndex];
final long outUs = seekFrameIndex * seekIndexFactor * videoTrack.usPerSample;
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) {
final int seekFrameIndex = getSeekFrameIndex(timeUs);
videoTrack.frame = 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];
}
}
}
package com.google.android.exoplayer2.extractor.avi;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
/**
* Collection of info about a track
*/
public class AviTrack {
final int id;
@NonNull
final TrackOutput trackOutput;
@NonNull
final StreamHeaderBox streamHeaderBox;
long usPerSample;
/**
* True indicates all frames are key frames (e.g. Audio, MJPEG)
*/
boolean allKeyFrames;
/**
* Key is frame number value is offset
*/
@Nullable
int[] keyFrames;
/**
* Current frame in the stream
* This needs to be updated on seek
* TODO: Should be offset from StreamHeaderBox.getStart()
*/
transient int frame;
/**
*
* @param trackOutput
*/
AviTrack(int id, @NonNull TrackOutput trackOutput, @NonNull StreamHeaderBox streamHeaderBox) {
this.id = id;
this.trackOutput = trackOutput;
this.streamHeaderBox = streamHeaderBox;
this.usPerSample = streamHeaderBox.getUsPerSample();
this.allKeyFrames = streamHeaderBox.isAudio() || (MimeTypes.IMAGE_JPEG.equals(streamHeaderBox.getMimeType()));
}
public boolean isKeyFrame() {
if (allKeyFrames) {
return true;
}
return keyFrames != null && Arrays.binarySearch(keyFrames, frame) >= 0;
}
public void setKeyFrames(int[] keyFrames) {
this.keyFrames = keyFrames;
}
public long getUs() {
return frame * usPerSample;
}
public void advance() {
frame++;
}
public boolean isVideo() {
return streamHeaderBox.isVideo();
}
public boolean isAudio() {
return streamHeaderBox.isAudio();
}
}
...@@ -65,17 +65,4 @@ public class AviUtil { ...@@ -65,17 +65,4 @@ public class AviUtil {
} }
return null; return null;
} }
@Nullable
static IAviList getSubList(List<? extends Box> list, int listType) {
for (Box box : list) {
if (IAviList.class.isInstance(box)) {
final IAviList aviList = (IAviList) box;
if (aviList.getListType() == listType) {
return aviList;
}
}
}
return null;
}
} }
package com.google.android.exoplayer2.extractor.avi; package com.google.android.exoplayer2.extractor.avi;
/**
* This is referred to as a Chunk in the MS spec, but that gets confusing with AV chunks
*/
public class Box { public class Box {
private final long size; private final long size;
private final int type; private final int type;
......
package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
public class BoxFactory {
@NonNull
public ResidentBox createBox(final int type, final int size, final ByteBuffer byteBuffer) {
final ByteBuffer boxBuffer = AviExtractor.allocate(size);
AviUtil.copy(byteBuffer, boxBuffer, size);
switch (type) {
case AviHeaderBox.AVIH:
return new AviHeaderBox(type, size, boxBuffer);
case ListBox.LIST:
return new ListBox(type, size, boxBuffer);
case StreamHeaderBox.STRH:
return new StreamHeaderBox(type, size, boxBuffer);
case StreamFormatBox.STRF:
return new StreamFormatBox(type, size, boxBuffer);
default:
return new ResidentBox(type, size, boxBuffer);
}
}
}
package com.google.android.exoplayer2.extractor.avi;
public interface IAviList {
int LIST = 'L' | ('I' << 8) | ('S' << 16) | ('T' << 24);
//Header List
int TYPE_HDRL = 'h' | ('d' << 8) | ('r' << 16) | ('l' << 24);
int getListType();
}
...@@ -5,15 +5,18 @@ import java.nio.ByteBuffer; ...@@ -5,15 +5,18 @@ import java.nio.ByteBuffer;
/** /**
* An AVI LIST box, memory resident * An AVI LIST box, memory resident
*/ */
public class ResidentList extends ResidentBox implements IAviList { public class ListBox extends ResidentBox {
public static final int LIST = 'L' | ('I' << 8) | ('S' << 16) | ('T' << 24);
//Header List
public static final int TYPE_HDRL = 'h' | ('d' << 8) | ('r' << 16) | ('l' << 24);
private final int listType; private final int listType;
ResidentList(int type, int size, ByteBuffer byteBuffer) { ListBox(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer); super(type, size, byteBuffer);
listType = byteBuffer.getInt(0); listType = byteBuffer.getInt(0);
} }
@Override
public int getListType() { public int getListType() {
return listType; return listType;
} }
......
...@@ -13,25 +13,28 @@ import java.nio.ByteOrder; ...@@ -13,25 +13,28 @@ import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* A box that is resident in memory
*/
public class ResidentBox extends Box { public class ResidentBox extends Box {
private static final String TAG = AviExtractor.TAG; private static final String TAG = AviExtractor.TAG;
final private static int MAX_RESIDENT = 2*1024; final private static int MAX_RESIDENT = 64*1024;
final ByteBuffer byteBuffer; final ByteBuffer byteBuffer;
private Class<? extends ResidentBox> getClass(final int type) { // private Class<? extends ResidentBox> getClass(final int type) {
switch (type) { // switch (type) {
case AviHeader.AVIH: // case AviHeaderBox.AVIH:
return AviHeader.class; // return AviHeaderBox.class;
case IAviList.LIST: // case ListBox.LIST:
return ResidentList.class; // return ListBox.class;
case StreamHeader.STRH: // case StreamHeaderBox.STRH:
return StreamHeader.class; // return StreamHeaderBox.class;
case StreamFormat.STRF: // case StreamFormatBox.STRF:
return StreamFormat.class; // return StreamFormatBox.class;
default: // default:
return ResidentBox.class; // return ResidentBox.class;
} // }
} // }
ResidentBox(int type, int size, ByteBuffer byteBuffer) { ResidentBox(int type, int size, ByteBuffer byteBuffer) {
super(type, size); super(type, size);
...@@ -92,20 +95,14 @@ public class ResidentBox extends Box { ...@@ -92,20 +95,14 @@ public class ResidentBox extends Box {
} }
@NonNull @NonNull
public List<ResidentBox> getBoxList() { public List<ResidentBox> getBoxList(final BoxFactory boxFactory) {
final ByteBuffer temp = getByteBuffer(); final ByteBuffer temp = getByteBuffer();
temp.position(4); temp.position(4);
final List<ResidentBox> list = new ArrayList<>(); final List<ResidentBox> list = new ArrayList<>();
while (temp.hasRemaining()) { while (temp.hasRemaining()) {
final int type = temp.getInt(); final int type = temp.getInt();
final int size = temp.getInt(); final int size = temp.getInt();
final Class<? extends ResidentBox> clazz = getClass(type); final ResidentBox residentBox = boxFactory.createBox(type, size, temp);
final ByteBuffer boxBuffer = AviExtractor.allocate(size);
AviUtil.copy(temp, boxBuffer, size);
final ResidentBox residentBox = newInstance(type, size, boxBuffer, clazz);
if (residentBox == null) {
break;
}
list.add(residentBox); list.add(residentBox);
} }
return list; return list;
......
...@@ -3,10 +3,10 @@ package com.google.android.exoplayer2.extractor.avi; ...@@ -3,10 +3,10 @@ package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class StreamFormat extends ResidentBox { public class StreamFormatBox extends ResidentBox {
public static final int STRF = 's' | ('t' << 8) | ('r' << 16) | ('f' << 24); public static final int STRF = 's' | ('t' << 8) | ('r' << 16) | ('f' << 24);
StreamFormat(int type, int size, ByteBuffer byteBuffer) { StreamFormatBox(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer); super(type, size, byteBuffer);
} }
......
...@@ -4,7 +4,10 @@ import android.util.SparseArray; ...@@ -4,7 +4,10 @@ import android.util.SparseArray;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class StreamHeader extends ResidentBox { /**
* AVISTREAMHEADER
*/
public class StreamHeaderBox extends ResidentBox {
public static final int STRH = 's' | ('t' << 8) | ('r' << 16) | ('h' << 24); public static final int STRH = 's' | ('t' << 8) | ('r' << 16) | ('h' << 24);
//Audio Stream //Audio Stream
...@@ -16,15 +19,23 @@ public class StreamHeader extends ResidentBox { ...@@ -16,15 +19,23 @@ public class StreamHeader extends ResidentBox {
private static final SparseArray<String> STREAM_MAP = new SparseArray<>(); private static final SparseArray<String> STREAM_MAP = new SparseArray<>();
static { static {
STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_MP4V); //Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG
STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), MimeTypes.VIDEO_MP4V); final String mimeType = MimeTypes.VIDEO_MP4V;
STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), MimeTypes.VIDEO_MP4V); //final String mimeType = MimeTypes.VIDEO_H263;
STREAM_MAP.put('X' | ('V' << 8) | ('I' << 16) | ('D' << 24), MimeTypes.VIDEO_MP4V);
//Doesn't seem to be supported on Android
//STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_MP4);
STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType);
STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType);
STREAM_MAP.put('X' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType);
STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.IMAGE_JPEG); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.IMAGE_JPEG);
} }
StreamHeader(int type, int size, ByteBuffer byteBuffer) { StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer); super(type, size, byteBuffer);
} }
...@@ -40,7 +51,15 @@ public class StreamHeader extends ResidentBox { ...@@ -40,7 +51,15 @@ public class StreamHeader extends ResidentBox {
return getRate() / (float)getScale(); return getRate() / (float)getScale();
} }
public String getCodec() { /**
* How long each sample covers
* @return
*/
public long getUsPerSample() {
return getScale() * 1_000_000L / getRate();
}
public String getMimeType() {
return STREAM_MAP.get(getFourCC()); return STREAM_MAP.get(getFourCC());
} }
...@@ -67,8 +86,15 @@ public class StreamHeader extends ResidentBox { ...@@ -67,8 +86,15 @@ public class StreamHeader extends ResidentBox {
public int getRate() { public int getRate() {
return byteBuffer.getInt(24); return byteBuffer.getInt(24);
} }
//28 - dwStart public int getStart() {
return byteBuffer.getInt(28);
}
public long getLength() { public long getLength() {
return byteBuffer.getInt(32) & AviUtil.UINT_MASK; return byteBuffer.getInt(32) & AviUtil.UINT_MASK;
} }
//36 - dwSuggestedBufferSize
//40 - dwQuality
public int getSampleSize() {
return byteBuffer.getInt(44);
}
} }
package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull;
import java.util.Arrays;
public class UnboundedIntArray {
@NonNull
int[] array;
//unint
int size =0;
public UnboundedIntArray() {
this(8);
}
public UnboundedIntArray(int size) {
if (size < 0) {
throw new IllegalArgumentException("Initial size must be positive: " + size);
}
array = new int[size];
}
public void add(int v) {
if (size == array.length) {
grow();
}
array[size++] = v;
}
public int getSize() {
return size;
}
public void pack() {
array = Arrays.copyOf(array, size);
}
protected void grow() {
int increase = Math.max(array.length /4, 1);
array = Arrays.copyOf(array, increase + array.length + size);
}
/**
* Only works if values are in sequential order
* @param v
* @return
*/
public int indexOf(int v) {
return Arrays.binarySearch(array, v);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment