diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 0b479ff6d5..2a295a8e26 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -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" diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java index 53396e135b..8e66d628d1 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java @@ -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; } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 75c1b94566..a73be489d1 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -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 diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index a71796cbb8..793762569c 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -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> responseHeaders) { - List extractors = new ArrayList<>(/* initialCapacity= */ 14); + List 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: diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java new file mode 100644 index 0000000000..237ac09bd0 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java @@ -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 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 +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java new file mode 100644 index 0000000000..7baf0685d5 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java @@ -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 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 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 streamChildren = streamList.getBoxList(); + for (int i=0;i 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() { + + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeader.java new file mode 100644 index 0000000000..b63bd6b844 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeader.java @@ -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); + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviUtil.java new file mode 100644 index 0000000000..8317213e8b --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviUtil.java @@ -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 getBox(List list, Class clazz) { + for (Box box : list) { + if (box.getClass() == clazz) { + return (T)box; + } + } + return null; + } + + @Nullable + static IAviList getSubList(List 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; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Box.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Box.java new file mode 100644 index 0000000000..cbf42840b3 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Box.java @@ -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; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IAviList.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IAviList.java new file mode 100644 index 0000000000..22aff293b3 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IAviList.java @@ -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(); +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java new file mode 100644 index 0000000000..6ec4286cdc --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java @@ -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 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 getInstance(final ByteBuffer byteBuffer, + ExtractorInput input, Class 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 newInstance(int type, int size, ByteBuffer boxBuffer, + Class boxClass) { + try { + final Constructor 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 getBoxList() { + final ByteBuffer temp = getByteBuffer(); + temp.position(4); + final List list = new ArrayList<>(); + while (temp.hasRemaining()) { + final int type = temp.getInt(); + final int size = temp.getInt(); + final Class 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; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentList.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentList.java new file mode 100644 index 0000000000..f4811f8555 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentList.java @@ -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); + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormat.java new file mode 100644 index 0000000000..9b0c67a09d --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormat.java @@ -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); + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeader.java new file mode 100644 index 0000000000..353ec32c32 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeader.java @@ -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 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; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java new file mode 100644 index 0000000000..55b1e67b0b --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java @@ -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); + } + +}