mirror of
https://github.com/androidx/media.git
synced 2025-05-07 07:30:22 +08:00
Tracks parsing, SeekMap (Index) started
This commit is contained in:
parent
f216fa2042
commit
33d22a1268
@ -542,6 +542,11 @@
|
|||||||
{
|
{
|
||||||
"name": "Misc",
|
"name": "Misc",
|
||||||
"samples": [
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "AVI",
|
||||||
|
"uri": "https://drive.google.com/u/0/uc?id=1K6oLKCS56WFbhz33TgilTJBqfMYFTeUd&?export=download",
|
||||||
|
"extension": "avi"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Dizzy (MP4)",
|
"name": "Dizzy (MP4)",
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
|
@ -38,7 +38,7 @@ public final class FileTypes {
|
|||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({
|
@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 {}
|
public @interface Type {}
|
||||||
/** Unknown file type. */
|
/** Unknown file type. */
|
||||||
@ -73,6 +73,8 @@ public final class FileTypes {
|
|||||||
public static final int WEBVTT = 13;
|
public static final int WEBVTT = 13;
|
||||||
/** File type for the JPEG format. */
|
/** File type for the JPEG format. */
|
||||||
public static final int JPEG = 14;
|
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";
|
@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_WEBVTT = ".webvtt";
|
||||||
private static final String EXTENSION_JPG = ".jpg";
|
private static final String EXTENSION_JPG = ".jpg";
|
||||||
private static final String EXTENSION_JPEG = ".jpeg";
|
private static final String EXTENSION_JPEG = ".jpeg";
|
||||||
|
private static final String EXTENSION_AVI = ".avi";
|
||||||
|
|
||||||
private FileTypes() {}
|
private FileTypes() {}
|
||||||
|
|
||||||
@ -167,6 +170,8 @@ public final class FileTypes {
|
|||||||
return FileTypes.WEBVTT;
|
return FileTypes.WEBVTT;
|
||||||
case MimeTypes.IMAGE_JPEG:
|
case MimeTypes.IMAGE_JPEG:
|
||||||
return FileTypes.JPEG;
|
return FileTypes.JPEG;
|
||||||
|
case MimeTypes.VIDEO_AVI:
|
||||||
|
return FileTypes.AVI;
|
||||||
default:
|
default:
|
||||||
return FileTypes.UNKNOWN;
|
return FileTypes.UNKNOWN;
|
||||||
}
|
}
|
||||||
@ -229,6 +234,8 @@ public final class FileTypes {
|
|||||||
return FileTypes.WEBVTT;
|
return FileTypes.WEBVTT;
|
||||||
} else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
|
} else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
|
||||||
return FileTypes.JPEG;
|
return FileTypes.JPEG;
|
||||||
|
} else if (filename.endsWith(EXTENSION_AVI)) {
|
||||||
|
return FileTypes.AVI;
|
||||||
} else {
|
} else {
|
||||||
return FileTypes.UNKNOWN;
|
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_FLV = BASE_TYPE_VIDEO + "/x-flv";
|
||||||
public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision";
|
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_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";
|
public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown";
|
||||||
|
|
||||||
// audio/ MIME types
|
// audio/ MIME types
|
||||||
|
@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
|
|||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
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.flac.FlacExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor;
|
import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor;
|
||||||
@ -99,6 +100,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||||||
FileTypes.AC4,
|
FileTypes.AC4,
|
||||||
FileTypes.MP3,
|
FileTypes.MP3,
|
||||||
FileTypes.JPEG,
|
FileTypes.JPEG,
|
||||||
|
FileTypes.AVI,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final FlacExtensionLoader FLAC_EXTENSION_LOADER = new FlacExtensionLoader();
|
private static final FlacExtensionLoader FLAC_EXTENSION_LOADER = new FlacExtensionLoader();
|
||||||
@ -300,7 +302,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||||||
@Override
|
@Override
|
||||||
public synchronized Extractor[] createExtractors(
|
public synchronized Extractor[] createExtractors(
|
||||||
Uri uri, Map<String, List<String>> responseHeaders) {
|
Uri uri, Map<String, List<String>> responseHeaders) {
|
||||||
List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 14);
|
List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 15);
|
||||||
|
|
||||||
@FileTypes.Type
|
@FileTypes.Type
|
||||||
int responseHeadersInferredFileType = inferFileTypeFromResponseHeaders(responseHeaders);
|
int responseHeadersInferredFileType = inferFileTypeFromResponseHeaders(responseHeaders);
|
||||||
@ -397,6 +399,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||||||
case FileTypes.JPEG:
|
case FileTypes.JPEG:
|
||||||
extractors.add(new JpegExtractor());
|
extractors.add(new JpegExtractor());
|
||||||
break;
|
break;
|
||||||
|
case FileTypes.AVI:
|
||||||
|
extractors.add(new AviExtractor());
|
||||||
|
break;
|
||||||
case FileTypes.WEBVTT:
|
case FileTypes.WEBVTT:
|
||||||
case FileTypes.UNKNOWN:
|
case FileTypes.UNKNOWN:
|
||||||
default:
|
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