mirror of
https://github.com/androidx/media.git
synced 2025-05-06 23:20:42 +08:00
Tracks parsing, SeekMap (Index) started
This commit is contained in:
parent
f216fa2042
commit
33d22a1268
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user