Tracks parsing, SeekMap (Index) started

This commit is contained in:
Dustin 2022-01-15 16:42:54 -07:00
parent f216fa2042
commit 33d22a1268
15 changed files with 791 additions and 2 deletions

View File

@ -542,6 +542,11 @@
{
"name": "Misc",
"samples": [
{
"name": "AVI",
"uri": "https://drive.google.com/u/0/uc?id=1K6oLKCS56WFbhz33TgilTJBqfMYFTeUd&?export=download",
"extension": "avi"
},
{
"name": "Dizzy (MP4)",
"uri": "https://html5demos.com/assets/dizzy.mp4"

View File

@ -38,7 +38,7 @@ public final class FileTypes {
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG, AVI
})
public @interface Type {}
/** Unknown file type. */
@ -73,6 +73,8 @@ public final class FileTypes {
public static final int WEBVTT = 13;
/** File type for the JPEG format. */
public static final int JPEG = 14;
/** File type for the AVI format. */
public static final int AVI = 15;
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
@ -105,6 +107,7 @@ public final class FileTypes {
private static final String EXTENSION_WEBVTT = ".webvtt";
private static final String EXTENSION_JPG = ".jpg";
private static final String EXTENSION_JPEG = ".jpeg";
private static final String EXTENSION_AVI = ".avi";
private FileTypes() {}
@ -167,6 +170,8 @@ public final class FileTypes {
return FileTypes.WEBVTT;
case MimeTypes.IMAGE_JPEG:
return FileTypes.JPEG;
case MimeTypes.VIDEO_AVI:
return FileTypes.AVI;
default:
return FileTypes.UNKNOWN;
}
@ -229,6 +234,8 @@ public final class FileTypes {
return FileTypes.WEBVTT;
} else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
return FileTypes.JPEG;
} else if (filename.endsWith(EXTENSION_AVI)) {
return FileTypes.AVI;
} else {
return FileTypes.UNKNOWN;
}

View File

@ -54,6 +54,7 @@ public final class MimeTypes {
public static final String VIDEO_FLV = BASE_TYPE_VIDEO + "/x-flv";
public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision";
public static final String VIDEO_OGG = BASE_TYPE_VIDEO + "/ogg";
public static final String VIDEO_AVI = BASE_TYPE_VIDEO + "/x-msvideo";
public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown";
// audio/ MIME types

View File

@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.avi.AviExtractor;
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor;
@ -99,6 +100,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
FileTypes.AC4,
FileTypes.MP3,
FileTypes.JPEG,
FileTypes.AVI,
};
private static final FlacExtensionLoader FLAC_EXTENSION_LOADER = new FlacExtensionLoader();
@ -300,7 +302,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Override
public synchronized Extractor[] createExtractors(
Uri uri, Map<String, List<String>> responseHeaders) {
List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 14);
List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 15);
@FileTypes.Type
int responseHeadersInferredFileType = inferFileTypeFromResponseHeaders(responseHeaders);
@ -397,6 +399,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
case FileTypes.JPEG:
extractors.add(new JpegExtractor());
break;
case FileTypes.AVI:
extractors.add(new AviExtractor());
break;
case FileTypes.WEBVTT:
case FileTypes.UNKNOWN:
default:

View File

@ -0,0 +1,49 @@
package com.google.android.exoplayer2.extractor.avi;
import android.util.SparseArray;
import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer;
public class AudioFormat {
public static short WAVE_FORMAT_PCM = 1;
public static short WAVE_FORMAT_MPEGLAYER3 = 0x55;
public static short WAVE_FORMAT_DVM = 0x2000; //AC3
public static short WAVE_FORMAT_DTS2 = 0x2001; //DTS
private static final SparseArray<String> FORMAT_MAP = new SparseArray<>();
static {
FORMAT_MAP.put(WAVE_FORMAT_PCM, MimeTypes.AUDIO_RAW);
FORMAT_MAP.put(WAVE_FORMAT_MPEGLAYER3, MimeTypes.AUDIO_MPEG);
FORMAT_MAP.put(WAVE_FORMAT_DVM, MimeTypes.AUDIO_AC3);
FORMAT_MAP.put(WAVE_FORMAT_DTS2, MimeTypes.AUDIO_DTS);
}
private ByteBuffer byteBuffer;
//WAVEFORMATEX
public AudioFormat(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
}
public String getCodec() {
return FORMAT_MAP.get(getFormatTag() & 0xffff);
}
public short getFormatTag() {
return byteBuffer.getShort(0);
}
public short getChannels() {
return byteBuffer.getShort(2);
}
public int getSamplesPerSecond() {
return byteBuffer.getInt(4);
}
// 8 - nAvgBytesPerSec(uint)
// 12 - nBlockAlign(ushort)
public short getBitsPerSample() {
return byteBuffer.getShort(14);
}
public short getCbSize() {
return byteBuffer.getShort(16);
}
//TODO: Deal with WAVEFORMATEXTENSIBLE
}

View File

@ -0,0 +1,298 @@
package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Log;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collections;
import java.util.List;
public class AviExtractor implements Extractor {
static final String TAG = "AviExtractor";
private static final int PEEK_BYTES = 28;
private final int STATE_READ_TRACKS = 0;
private final int STATE_FIND_MOVI = 1;
private final int STATE_FIND_IDX1 = 2;
private final int STATE_READ_SAMPLES = 3;
static final int RIFF = AviUtil.toInt(new byte[]{'R','I','F','F'});
static final int AVI_ = AviUtil.toInt(new byte[]{'A','V','I',' '});
//Stream List
static final int STRL = 's' | ('t' << 8) | ('r' << 16) | ('l' << 24);
//Stream CODEC data
static final int STRD = 's' | ('t' << 8) | ('r' << 16) | ('d' << 24);
//movie data box
static final int MOVI = 'm' | ('o' << 8) | ('v' << 16) | ('i' << 24);
//Index
static final int IDX1 = 'i' | ('d' << 8) | ('x' << 16) | ('1' << 24);
private final int flags;
private int state;
private ExtractorOutput output;
private AviHeader aviHeader;
//After the movi position
private long firstChunkPosition;
public AviExtractor() {
this(0);
}
public AviExtractor(int flags) {
this.flags = flags;
}
@Override
public boolean sniff(ExtractorInput input) throws IOException {
return peakHeaderList(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;
}
boolean peakHeaderList(ExtractorInput input) throws IOException {
final ByteBuffer byteBuffer = allocate(PEEK_BYTES);
input.peekFully(byteBuffer.array(), 0, PEEK_BYTES);
final int riff = byteBuffer.getInt();
if (riff != AviExtractor.RIFF) {
return false;
}
long reportedLen = AviUtil.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");
}
int avi = byteBuffer.getInt();
if (avi != AviExtractor.AVI_) {
return false;
}
final int list = byteBuffer.getInt();
if (list != IAviList.LIST) {
return false;
}
//Len
byteBuffer.getInt();
final int hdrl = byteBuffer.getInt();
if (hdrl != IAviList.TYPE_HDRL) {
return false;
}
final int avih = byteBuffer.getInt();
if (avih != AviHeader.AVIH) {
return false;
}
return true;
}
@Nullable
ResidentList 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 = AviUtil.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");
}
int avi = byteBuffer.getInt();
if (avi != AviExtractor.AVI_) {
return null;
}
final ResidentList header = ResidentList.getInstance(byteBuffer, input, ResidentList.class);
if (header == null) {
return null;
}
if (header.getListType() != IAviList.TYPE_HDRL) {
Log.e(TAG, "Expected " +AviUtil.toString(IAviList.TYPE_HDRL) + ", got: " +
AviUtil.toString(header.getType()));
return null;
}
return header;
}
long getDuration() {
if (aviHeader == null) {
return C.TIME_UNSET;
}
return aviHeader.getFrames() * (long)aviHeader.getMicroSecPerFrame();
}
@Override
public void init(ExtractorOutput output) {
this.state = STATE_READ_TRACKS;
this.output = output;
}
private static ResidentBox peekNext(final List<ResidentBox> streams, int i, int type) {
if (i + 1 < streams.size() && streams.get(i + 1).getType() == type) {
return streams.get(i + 1);
}
return null;
}
private int readTracks(ExtractorInput input) throws IOException {
final ResidentList headerList = readHeaderList(input);
if (headerList == null) {
throw new IOException("AVI Header List not found");
}
final List<ResidentBox> headerChildren = headerList.getBoxList();
aviHeader = AviUtil.getBox(headerChildren, AviHeader.class);
if (aviHeader == null) {
throw new IOException("AviHeader not found");
}
headerChildren.remove(aviHeader);
//headerChildren should only be Stream Lists now
int streamId = 0;
for (Box box : headerChildren) {
if (box instanceof ResidentList && ((ResidentList) box).getListType() == STRL) {
final ResidentList streamList = (ResidentList) box;
final List<ResidentBox> streamChildren = streamList.getBoxList();
for (int i=0;i<streamChildren.size();i++) {
final ResidentBox residentBox = streamChildren.get(i);
if (residentBox instanceof StreamHeader) {
final StreamHeader streamHeader = (StreamHeader) residentBox;
final StreamFormat streamFormat = (StreamFormat) peekNext(streamChildren, i, StreamFormat.STRF);
if (streamFormat != null) {
i++;
if (streamHeader.isVideo()) {
final VideoFormat videoFormat = streamFormat.getVideoFormat();
final ResidentBox codecBox = (ResidentBox) peekNext(streamChildren, i, STRD);
final List<byte[]> codecData;
if (codecBox != null) {
codecData = Collections.singletonList(codecBox.byteBuffer.array());
i++;
} else {
codecData = null;
}
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO);
final Format.Builder builder = new Format.Builder();
builder.setWidth(videoFormat.getWidth());
builder.setHeight(videoFormat.getHeight());
builder.setFrameRate(streamHeader.getFrameRate());
builder.setCodecs(streamHeader.getCodec());
builder.setInitializationData(codecData);
trackOutput.format(builder.build());
} else if (streamHeader.isAudio()) {
final AudioFormat audioFormat = streamFormat.getAudioFormat();
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO);
final Format.Builder builder = new Format.Builder();
builder.setCodecs(audioFormat.getCodec());
builder.setChannelCount(audioFormat.getChannels());
builder.setSampleRate(audioFormat.getSamplesPerSecond());
if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) {
//TODO: Determine if this is LE or BE - Most likely LE
final short bps = audioFormat.getBitsPerSample();
if (bps == 8) {
builder.setPcmEncoding(C.ENCODING_PCM_8BIT);
} else if (bps == 16){
builder.setPcmEncoding(C.ENCODING_PCM_16BIT);
}
}
trackOutput.format(builder.build());
}
}
streamId++;
}
}
}
}
output.endTracks();
state = STATE_FIND_MOVI;
return RESULT_CONTINUE;
}
int findMovi(ExtractorInput input, PositionHolder seekPosition) throws IOException {
ByteBuffer byteBuffer = allocate(12);
input.readFully(byteBuffer.array(), 0,12);
final int tag = byteBuffer.getInt();
final long size = byteBuffer.getInt() & AviUtil.UINT_MASK;
final long position = input.getPosition();
//-4 because we over read for the LIST type
long nextBox = position + size - 4;
if (tag == IAviList.LIST) {
final int listType = byteBuffer.getInt();
if (listType == MOVI) {
firstChunkPosition = position;
if (aviHeader.hasIndex()) {
state = STATE_FIND_IDX1;
} else {
output.seekMap(new SeekMap.Unseekable(getDuration()));
state = STATE_READ_TRACKS;
nextBox = firstChunkPosition;
}
}
}
seekPosition.position = nextBox;
return RESULT_SEEK;
}
int findIdx1(ExtractorInput input, PositionHolder seekPosition) throws IOException {
ByteBuffer byteBuffer = allocate(8);
input.readFully(byteBuffer.array(), 0,8);
final int tag = byteBuffer.getInt();
long remaining = byteBuffer.getInt() & AviUtil.UINT_MASK;
//TODO: Sanity check on file length
if (tag == IDX1) {
final ByteBuffer index = allocate(4096);
final byte[] bytes = index.array();
index.position(index.capacity());
while (remaining > 0) {
if (!index.hasRemaining()) {
index.clear();
final int toRead = (int)Math.min(4096, remaining);
if (!input.readFully(bytes, 0, toRead, true)) {
seekPosition.position = firstChunkPosition;
output.seekMap(new SeekMap.Unseekable(getDuration()));
break;
}
index.limit(toRead);
remaining -=toRead;
}
}
//TODO
} else {
seekPosition.position = input.getPosition() + remaining;
}
return RESULT_SEEK;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
switch (state) {
case STATE_READ_TRACKS:
return readTracks(input);
case STATE_FIND_MOVI:
return findMovi(input, seekPosition);
case STATE_FIND_IDX1:
return findIdx1(input, seekPosition);
}
return RESULT_CONTINUE;
}
@Override
public void seek(long position, long timeUs) {
}
@Override
public void release() {
}
}

View File

@ -0,0 +1,51 @@
package com.google.android.exoplayer2.extractor.avi;
import java.nio.ByteBuffer;
public class AviHeader extends ResidentBox {
public static final int AVIF_HASINDEX = 0x10;
static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24);
//AVIMAINHEADER
AviHeader(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer);
}
@Override
boolean assertType() {
return simpleAssert(AVIH);
}
boolean hasIndex() {
return (getFlags() & AVIF_HASINDEX) > 0;
}
int getMicroSecPerFrame() {
return byteBuffer.getInt(0);
}
//4 = dwMaxBytesPerSec
//8 = dwPaddingGranularity
int getFlags() {
return byteBuffer.getInt(12);
}
int getFrames() {
return byteBuffer.getInt(16);
}
//20 = dwInitialFrames
int getSuggestedBufferSize() {
return byteBuffer.getInt(24);
}
int getWidth() {
return byteBuffer.getInt(28);
}
int getHeight() {
return byteBuffer.getInt(32);
}
}

View File

@ -0,0 +1,81 @@
package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
public class AviUtil {
static final long UINT_MASK = 0xffffffffL;
static int toInt(byte[] bytes) {
int i = 0;
for (int b=bytes.length - 1;b>=0;b--) {
i <<=8;
i |= bytes[b];
}
return i;
}
static long getUInt(ByteBuffer byteBuffer) {
return byteBuffer.getInt() & UINT_MASK;
}
static void copy(ByteBuffer source, ByteBuffer dest, int bytes) {
final int inLimit = source.limit();
source.limit(source.position() + bytes);
dest.put(source);
source.limit(inLimit);
}
static ByteBuffer getByteBuffer(final ByteBuffer source, final int size,
final ExtractorInput input) throws IOException {
final ByteBuffer byteBuffer = AviExtractor.allocate(size);
if (size < source.remaining()) {
copy(source, byteBuffer, size);
} else {
final int copy = source.remaining();
copy(source, byteBuffer, copy);
int remaining = size - copy;
final int offset = byteBuffer.position() + byteBuffer.arrayOffset();
input.readFully(byteBuffer.array(), offset, remaining, false);
}
return byteBuffer;
}
@NonNull
static String toString(int tag) {
final StringBuilder sb = new StringBuilder(4);
for (int i=0;i<4;i++) {
sb.append((char)(tag & 0xff));
tag >>=8;
}
return sb.toString();
}
@Nullable
static <T extends Box> T getBox(List<? extends Box> list, Class<T> clazz) {
for (Box box : list) {
if (box.getClass() == clazz) {
return (T)box;
}
}
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;
}
}

View File

@ -0,0 +1,28 @@
package com.google.android.exoplayer2.extractor.avi;
public class Box {
private final long size;
private final int type;
Box(int type, long size) {
this.type = type;
this.size = size;
}
public long getSize() {
return size;
}
public int getType() {
return type;
}
boolean simpleAssert(final int expected) {
return getType() == expected;
}
boolean assertType() {
//Generic box, nothing to assert
return true;
}
}

View File

@ -0,0 +1,9 @@
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();
}

View File

@ -0,0 +1,113 @@
package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Log;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
public class ResidentBox extends Box {
private static final String TAG = AviExtractor.TAG;
final private static int MAX_RESIDENT = 2*1024;
final ByteBuffer byteBuffer;
private Class<? extends ResidentBox> getClass(final int type) {
switch (type) {
case AviHeader.AVIH:
return AviHeader.class;
case IAviList.LIST:
return ResidentList.class;
case StreamHeader.STRH:
return StreamHeader.class;
case StreamFormat.STRF:
return StreamFormat.class;
default:
return ResidentBox.class;
}
}
ResidentBox(int type, int size, ByteBuffer byteBuffer) {
super(type, size);
this.byteBuffer = byteBuffer;
}
/**
* List is not yet populated
* @param byteBuffer
* @return
* @throws IOException
*/
@Nullable
public static <T extends ResidentBox> T getInstance(final ByteBuffer byteBuffer,
ExtractorInput input, Class<T> boxClass) throws IOException {
if (byteBuffer.remaining() < 8) {
//Should not happen
throw new BufferUnderflowException();
}
final int type = byteBuffer.getInt();
final long size = AviUtil.getUInt(byteBuffer);
if (size > MAX_RESIDENT) {
throw new BufferOverflowException();
}
final ByteBuffer boxBuffer = AviUtil.getByteBuffer(byteBuffer, (int)size, input);
return newInstance(type, (int)size, boxBuffer, boxClass);
}
@Nullable
private static <T extends ResidentBox> T newInstance(int type, int size, ByteBuffer boxBuffer,
Class<T> boxClass) {
try {
final Constructor<T> constructor =
boxClass.getDeclaredConstructor(int.class, int.class, ByteBuffer.class);
T box = constructor.newInstance(type, size, boxBuffer);
if (!box.assertType()) {
Log.e(TAG, "Expected " + AviUtil.toString(type) + " got " + AviUtil.toString(box.getType()));
return null;
}
return box;
} catch (Exception e) {
Log.e(TAG, "Create box failed " + AviUtil.toString(type));
return null;
}
}
/**
* Returns shallow copy of this ByteBuffer with the position at 0
* @return
*/
@NonNull
public ByteBuffer getByteBuffer() {
final ByteBuffer clone = byteBuffer.duplicate();
clone.order(ByteOrder.LITTLE_ENDIAN);
clone.clear();
return clone;
}
@NonNull
public List<ResidentBox> getBoxList() {
final ByteBuffer temp = getByteBuffer();
temp.position(4);
final List<ResidentBox> list = new ArrayList<>();
while (temp.hasRemaining()) {
final int type = temp.getInt();
final int size = temp.getInt();
final Class<? extends ResidentBox> clazz = getClass(type);
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);
}
return list;
}
}

View File

@ -0,0 +1,25 @@
package com.google.android.exoplayer2.extractor.avi;
import java.nio.ByteBuffer;
/**
* An AVI LIST box, memory resident
*/
public class ResidentList extends ResidentBox implements IAviList {
private final int listType;
ResidentList(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer);
listType = byteBuffer.getInt(0);
}
@Override
public int getListType() {
return listType;
}
@Override
boolean assertType() {
return simpleAssert(LIST);
}
}

View File

@ -0,0 +1,22 @@
package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
public class StreamFormat extends ResidentBox {
public static final int STRF = 's' | ('t' << 8) | ('r' << 16) | ('f' << 24);
StreamFormat(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer);
}
@NonNull
public VideoFormat getVideoFormat() {
return new VideoFormat(byteBuffer);
}
@NonNull
public AudioFormat getAudioFormat() {
return new AudioFormat(byteBuffer);
}
}

View File

@ -0,0 +1,74 @@
package com.google.android.exoplayer2.extractor.avi;
import android.util.SparseArray;
import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer;
public class StreamHeader extends ResidentBox {
public static final int STRH = 's' | ('t' << 8) | ('r' << 16) | ('h' << 24);
//Audio Stream
private static final int AUDS = 'a' | ('u' << 8) | ('d' << 16) | ('s' << 24);
//Videos Stream
private static final int VIDS = 'v' | ('i' << 8) | ('d' << 16) | ('s' << 24);
private static final SparseArray<String> STREAM_MAP = new SparseArray<>();
static {
STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_MP4V);
STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), MimeTypes.VIDEO_MP4V);
STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), MimeTypes.VIDEO_MP4V);
STREAM_MAP.put('X' | ('V' << 8) | ('I' << 16) | ('D' << 24), MimeTypes.VIDEO_MP4V);
STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.IMAGE_JPEG);
}
StreamHeader(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer);
}
public boolean isAudio() {
return getSteamType() == AUDS;
}
public boolean isVideo() {
return getSteamType() == VIDS;
}
public float getFrameRate() {
return getRate() / (float)getScale();
}
public String getCodec() {
return STREAM_MAP.get(getFourCC());
}
public int getSteamType() {
return byteBuffer.getInt(0);
}
/**
* Only meaningful for video
* @return FourCC
*/
public int getFourCC() {
return byteBuffer.getInt(4);
}
//8 - dwFlags
//12 - wPriority
//14 - wLanguage
public int getInitialFrames() {
return byteBuffer.getInt(16);
}
public int getScale() {
return byteBuffer.getInt(20);
}
public int getRate() {
return byteBuffer.getInt(24);
}
//28 - dwStart
public long getLength() {
return byteBuffer.getInt(32) & AviUtil.UINT_MASK;
}
}

View File

@ -0,0 +1,21 @@
package com.google.android.exoplayer2.extractor.avi;
import java.nio.ByteBuffer;
public class VideoFormat {
private final ByteBuffer byteBuffer;
public VideoFormat(final ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
}
//biSize - (uint)
public int getWidth() {
return byteBuffer.getInt(4);
}
public int getHeight() {
return byteBuffer.getInt(8);
}
}