From 33d22a1268c6c4b165d083cc7a00c0b99d28eb1d Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 15 Jan 2022 16:42:54 -0700 Subject: [PATCH 01/70] Tracks parsing, SeekMap (Index) started --- demos/main/src/main/assets/media.exolist.json | 5 + .../android/exoplayer2/util/FileTypes.java | 9 +- .../android/exoplayer2/util/MimeTypes.java | 1 + .../extractor/DefaultExtractorsFactory.java | 7 +- .../exoplayer2/extractor/avi/AudioFormat.java | 49 +++ .../extractor/avi/AviExtractor.java | 298 ++++++++++++++++++ .../exoplayer2/extractor/avi/AviHeader.java | 51 +++ .../exoplayer2/extractor/avi/AviUtil.java | 81 +++++ .../android/exoplayer2/extractor/avi/Box.java | 28 ++ .../exoplayer2/extractor/avi/IAviList.java | 9 + .../exoplayer2/extractor/avi/ResidentBox.java | 113 +++++++ .../extractor/avi/ResidentList.java | 25 ++ .../extractor/avi/StreamFormat.java | 22 ++ .../extractor/avi/StreamHeader.java | 74 +++++ .../exoplayer2/extractor/avi/VideoFormat.java | 21 ++ 15 files changed, 791 insertions(+), 2 deletions(-) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AudioFormat.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeader.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviUtil.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Box.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IAviList.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentBox.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentList.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormat.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeader.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/VideoFormat.java 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); + } + +} From a9c941859191d3be7396273837b59466b77e0740 Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 17 Jan 2022 22:48:13 -0700 Subject: [PATCH 02/70] Working! --- .../exoplayer2/extractor/avi/AudioFormat.java | 30 +- .../extractor/avi/AviExtractor.java | 313 ++++++++++++++---- .../avi/{AviHeader.java => AviHeaderBox.java} | 4 +- .../exoplayer2/extractor/avi/AviSeekMap.java | 75 +++++ .../exoplayer2/extractor/avi/AviTrack.java | 80 +++++ .../exoplayer2/extractor/avi/AviUtil.java | 13 - .../android/exoplayer2/extractor/avi/Box.java | 3 + .../exoplayer2/extractor/avi/BoxFactory.java | 25 ++ .../exoplayer2/extractor/avi/IAviList.java | 9 - .../avi/{ResidentList.java => ListBox.java} | 9 +- .../exoplayer2/extractor/avi/ResidentBox.java | 43 ++- ...StreamFormat.java => StreamFormatBox.java} | 4 +- ...StreamHeader.java => StreamHeaderBox.java} | 42 ++- .../extractor/avi/UnboundedIntArray.java | 51 +++ 14 files changed, 574 insertions(+), 127 deletions(-) rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{AviHeader.java => AviHeaderBox.java} (88%) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IAviList.java rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{ResidentList.java => ListBox.java} (56%) rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{StreamFormat.java => StreamFormatBox.java} (79%) rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{StreamHeader.java => StreamHeaderBox.java} (59%) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java 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 index 237ac09bd0..6bb5a5e81a 100644 --- 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 @@ -5,14 +5,16 @@ 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 + public static final short WAVE_FORMAT_PCM = 1; + private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; + private static final short WAVE_FORMAT_AAC = 0xff; + private static final short WAVE_FORMAT_DVM = 0x2000; //AC3 + private static final 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_AAC, MimeTypes.AUDIO_AAC); FORMAT_MAP.put(WAVE_FORMAT_DVM, MimeTypes.AUDIO_AC3); FORMAT_MAP.put(WAVE_FORMAT_DTS2, MimeTypes.AUDIO_DTS); } @@ -24,7 +26,7 @@ public class AudioFormat { this.byteBuffer = byteBuffer; } - public String getCodec() { + public String getMimeType() { return FORMAT_MAP.get(getFormatTag() & 0xffff); } @@ -38,12 +40,24 @@ public class AudioFormat { return byteBuffer.getInt(4); } // 8 - nAvgBytesPerSec(uint) - // 12 - nBlockAlign(ushort) + public int getBlockAlign() { + return byteBuffer.getShort(12); + } public short getBitsPerSample() { return byteBuffer.getShort(14); } - public short getCbSize() { - return byteBuffer.getShort(16); + public int getCbSize() { + return byteBuffer.getShort(16) & 0xffff; + } + public byte[] getCodecData() { + final int size = getCbSize(); + final ByteBuffer temp = byteBuffer.duplicate(); + temp.clear(); + temp.position(18); + temp.limit(18 + size); + final byte[] data = new byte[size]; + temp.get(data); + return data; } //TODO: Deal with WAVEFORMATEXTENSIBLE } 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 index 7baf0685d5..c1f39af608 100644 --- 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 @@ -1,5 +1,8 @@ package com.google.android.exoplayer2.extractor.avi; +import android.util.SparseArray; +import android.util.SparseIntArray; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -10,20 +13,34 @@ 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 com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.TreeMap; +/** + * Based on the official MicroSoft spec + * https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference + */ public class AviExtractor implements Extractor { static final String TAG = "AviExtractor"; + static final int KEY_FRAME_MASK = Integer.MIN_VALUE; 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; + private static final int STATE_READ_TRACKS = 0; + private static final int STATE_FIND_MOVI = 1; + private static final int STATE_READ_IDX1 = 2; + private static final int STATE_READ_SAMPLES = 3; + private static final int STATE_SEEK_START = 4; + + private static final int AVIIF_KEYFRAME = 16; + static final int RIFF = AviUtil.toInt(new byte[]{'R','I','F','F'}); static final int AVI_ = AviUtil.toInt(new byte[]{'A','V','I',' '}); @@ -36,13 +53,26 @@ public class AviExtractor implements Extractor { //Index static final int IDX1 = 'i' | ('d' << 8) | ('x' << 16) | ('1' << 24); - private final int flags; + static final int JUNK = 'J' | ('U' << 8) | ('N' << 16) | ('K' << 24); + + static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds private int state; private ExtractorOutput output; - private AviHeader aviHeader; + private AviHeaderBox aviHeader; + private SparseArray idTrackMap = new SparseArray<>(); //After the movi position - private long firstChunkPosition; + private long moviOffset; + private long moviEnd; + private AviSeekMap aviSeekMap; + private int flags; + +// private long indexOffset; //Usually chunkStart + + //If partial read + private transient AviTrack sampleTrack; + private transient int sampleRemaining; + private transient int sampleSize; public AviExtractor() { this(0); @@ -63,6 +93,12 @@ public class AviExtractor implements Extractor { byteBuffer.order(ByteOrder.LITTLE_ENDIAN); return byteBuffer; } + + private void setSeekMap(AviSeekMap aviSeekMap) { + this.aviSeekMap = aviSeekMap; + output.seekMap(aviSeekMap); + } + boolean peakHeaderList(ExtractorInput input) throws IOException { final ByteBuffer byteBuffer = allocate(PEEK_BYTES); input.peekFully(byteBuffer.array(), 0, PEEK_BYTES); @@ -80,23 +116,23 @@ public class AviExtractor implements Extractor { return false; } final int list = byteBuffer.getInt(); - if (list != IAviList.LIST) { + if (list != ListBox.LIST) { return false; } //Len byteBuffer.getInt(); final int hdrl = byteBuffer.getInt(); - if (hdrl != IAviList.TYPE_HDRL) { + if (hdrl != ListBox.TYPE_HDRL) { return false; } final int avih = byteBuffer.getInt(); - if (avih != AviHeader.AVIH) { + if (avih != AviHeaderBox.AVIH) { return false; } return true; } @Nullable - ResidentList readHeaderList(ExtractorInput input) throws IOException { + ListBox readHeaderList(ExtractorInput input) throws IOException { final ByteBuffer byteBuffer = allocate(20); input.readFully(byteBuffer.array(), 0, byteBuffer.capacity()); final int riff = byteBuffer.getInt(); @@ -112,12 +148,12 @@ public class AviExtractor implements Extractor { if (avi != AviExtractor.AVI_) { return null; } - final ResidentList header = ResidentList.getInstance(byteBuffer, input, ResidentList.class); + final ListBox header = ListBox.getInstance(byteBuffer, input, ListBox.class); if (header == null) { return null; } - if (header.getListType() != IAviList.TYPE_HDRL) { - Log.e(TAG, "Expected " +AviUtil.toString(IAviList.TYPE_HDRL) + ", got: " + + if (header.getListType() != ListBox.TYPE_HDRL) { + Log.e(TAG, "Expected " +AviUtil.toString(ListBox.TYPE_HDRL) + ", got: " + AviUtil.toString(header.getType())); return null; } @@ -145,12 +181,13 @@ public class AviExtractor implements Extractor { } private int readTracks(ExtractorInput input) throws IOException { - final ResidentList headerList = readHeaderList(input); + final ListBox 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); + final BoxFactory boxFactory = new BoxFactory(); + final List headerChildren = headerList.getBoxList(boxFactory); + aviHeader = AviUtil.getBox(headerChildren, AviHeaderBox.class); if (aviHeader == null) { throw new IOException("AviHeader not found"); } @@ -159,14 +196,14 @@ public class AviExtractor implements Extractor { 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(); + if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { + final ListBox streamList = (ListBox) box; + final List streamChildren = streamList.getBoxList(boxFactory); for (int i=0;i 0) { + builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); + } trackOutput.format(builder.build()); + idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24), + new AviTrack(streamId, trackOutput, streamHeader)); } } streamId++; @@ -224,16 +278,17 @@ public class AviExtractor implements Extractor { final long position = input.getPosition(); //-4 because we over read for the LIST type long nextBox = position + size - 4; - if (tag == IAviList.LIST) { + if (tag == ListBox.LIST) { final int listType = byteBuffer.getInt(); if (listType == MOVI) { - firstChunkPosition = position; + moviOffset = position - 4; + moviEnd = moviOffset + size; if (aviHeader.hasIndex()) { - state = STATE_FIND_IDX1; + state = STATE_READ_IDX1; } else { output.seekMap(new SeekMap.Unseekable(getDuration())); state = STATE_READ_TRACKS; - nextBox = firstChunkPosition; + nextBox = moviOffset + 4; } } } @@ -241,54 +296,194 @@ public class AviExtractor implements Extractor { 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; - } + /** + * Reads the index and sets the keyFrames and creates the SeekMap + * @param input + * @param remaining + * @throws IOException + */ + void readIdx1(ExtractorInput input, int remaining) throws IOException { + final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); + final byte[] bytes = indexByteBuffer.array(); + final HashMap audioIdFrameMap = new HashMap<>(); + AviTrack videoTrack = null; + //Video seek offsets + UnboundedIntArray videoSeekOffset = new UnboundedIntArray(); + for (int i=0;i 0) { + final int toRead = Math.min(indexByteBuffer.remaining(), remaining); + input.readFully(bytes, indexByteBuffer.position(), toRead); + remaining -= toRead; + while (indexByteBuffer.remaining() >= 16) { + final int id = indexByteBuffer.getInt(); + final AviTrack aviTrack = idTrackMap.get(id); + if (aviTrack == null) { + Log.w(TAG, "Unknown Track Type: " + AviUtil.toString(id)); + indexByteBuffer.position(indexByteBuffer.position() + 12); + continue; + } + final int flags = indexByteBuffer.getInt(); + final int offset = indexByteBuffer.getInt(); + indexByteBuffer.position(indexByteBuffer.position() + 4); + //int size = indexByteBuffer.getInt(); + if (aviTrack.isVideo()) { + if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { + keyFrameList.add(aviTrack.frame); + } + if (aviTrack.frame % seekFrameRate == 0) { + + videoSeekOffset.add(offset); + for (Map.Entry entry : audioIdFrameMap.entrySet()) { + final int audioId = entry.getKey(); + final UnboundedIntArray videoFrameMap = entry.getValue(); + final AviTrack audioTrack = idTrackMap.get(audioId); + videoFrameMap.add(audioTrack.frame); + } + } + } + aviTrack.advance(); + } + indexByteBuffer.compact(); + } + videoSeekOffset.pack(); + keyFrameList.pack(); + final int[] keyFrames = keyFrameList.array; + videoTrack.setKeyFrames(keyFrames); + + final SparseArray idFrameArray = new SparseArray<>(); + for (Map.Entry entry : audioIdFrameMap.entrySet()) { + entry.getValue().pack(); + idFrameArray.put(entry.getKey(), entry.getValue().array); + final AviTrack aviTrack = idTrackMap.get(entry.getKey()); + //Sometimes this value is way off + long calcUsPerSample = (getDuration()/aviTrack.frame); + float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample; + if (deltaPercent >.01) { + aviTrack.usPerSample = getDuration()/aviTrack.frame; + Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); } -//TODO - } else { - seekPosition.position = input.getPosition() + remaining; } - return RESULT_SEEK; + final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.array, + idFrameArray, moviOffset, getDuration()); + setSeekMap(seekMap); + resetFrames(); + } + + int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException { + if (sampleRemaining != 0) { + sampleRemaining -= sampleTrack.trackOutput.sampleData(input, sampleRemaining, false); + } else { + ByteBuffer byteBuffer = allocate(8); + final byte[] bytes = byteBuffer.array(); + // This isn't documented anywhere, but most files are aligned to even bytes + // and can have gaps of zeros + if ((input.getPosition() & 1) == 1) { + input.skipFully(1); + } + input.readFully(bytes, 0, 1); + while (bytes[0] == 0) { + input.readFully(bytes, 0, 1); + } + if (input.getPosition() >= moviEnd) { + return RESULT_END_OF_INPUT; + } + input.readFully(bytes, 1, 7); + int id = byteBuffer.getInt(); + sampleSize = byteBuffer.getInt(); + sampleTrack = idTrackMap.get(id); + if (sampleTrack == null) { + if (id != JUNK) { + Log.w(TAG, "Unknown tag=" + AviUtil.toString(id) + " pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " moviEnd=" + moviEnd); + } + seekPosition.position = input.getPosition() + sampleSize; + return RESULT_SEEK; + } else { + //Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo()); + sampleRemaining = sampleSize - sampleTrack.trackOutput.sampleData(input, sampleSize, false); + } + } + if (sampleRemaining != 0) { + return RESULT_CONTINUE; + } + sampleTrack.trackOutput.sampleMetadata( + sampleTrack.getUs(), sampleTrack.isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0 , sampleSize, 0, null); + //Log.d(TAG, "Frame: " + (sampleTrack.isVideo()? 'V' : 'A') + " us=" + sampleTrack.getUs()); + sampleTrack.advance(); + return RESULT_CONTINUE; } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { switch (state) { + case STATE_READ_SAMPLES: + return readSamples(input, seekPosition); + case STATE_SEEK_START: + state = STATE_READ_SAMPLES; + seekPosition.position = moviOffset + 4; + return RESULT_SEEK; case STATE_READ_TRACKS: return readTracks(input); case STATE_FIND_MOVI: return findMovi(input, seekPosition); - case STATE_FIND_IDX1: - return findIdx1(input, seekPosition); + case STATE_READ_IDX1: { + if (aviHeader.hasIndex()) { + ByteBuffer byteBuffer = allocate(8); + input.readFully(byteBuffer.array(), 0,8); + final int tag = byteBuffer.getInt(); + final int size = byteBuffer.getInt(); + if (tag == IDX1) { + readIdx1(input, size); + } + } else { + output.seekMap(new SeekMap.Unseekable(getDuration())); + } + seekPosition.position = moviOffset + 4; + state = STATE_READ_SAMPLES; + return RESULT_SEEK; + } + } return RESULT_CONTINUE; } @Override public void seek(long position, long timeUs) { + Log.d("Test", "Seek: pos=" + position + " us=" + timeUs); + if (position == 0) { + if (moviOffset != 0) { + resetFrames(); + state = STATE_SEEK_START; + } + } else { + if (aviSeekMap != null) { + aviSeekMap.setFrames(position, timeUs, idTrackMap); + } + } + } + void resetFrames() { + for (int i=0;i audioIdMap; + final long moviOffset; + final long duration; + + public AviSeekMap(AviTrack videoTrack, int seekIndexFactor, int[] videoFrameOffsetMap, + SparseArray audioIdMap, long moviOffset, long duration) { + this.videoTrack = videoTrack; + this.seekIndexFactor = seekIndexFactor; + this.videoFrameOffsetMap = videoFrameOffsetMap; + this.audioIdMap = audioIdMap; + this.moviOffset = moviOffset; + this.duration = duration; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return duration; + } + + private int getSeekFrameIndex(long timeUs) { + final int reqFrame = (int)(timeUs / videoTrack.usPerSample); + int reqFrameIndex = reqFrame / seekIndexFactor; + if (reqFrameIndex >= videoFrameOffsetMap.length) { + reqFrameIndex = videoFrameOffsetMap.length - 1; + } + return reqFrameIndex; + } + + @NonNull + @Override + public SeekPoints getSeekPoints(long timeUs) { + final int seekFrameIndex = getSeekFrameIndex(timeUs); + int offset = videoFrameOffsetMap[seekFrameIndex]; + final long outUs = seekFrameIndex * seekIndexFactor * videoTrack.usPerSample; + final long position = offset + moviOffset; + Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); + + return new SeekPoints(new SeekPoint(outUs, position)); + } + + public void setFrames(final long position, final long timeUs, final SparseArray idTrackMap) { + final int seekFrameIndex = getSeekFrameIndex(timeUs); + videoTrack.frame = seekFrameIndex * seekIndexFactor; + for (int i=0;i= 0; + } + + public void setKeyFrames(int[] keyFrames) { + this.keyFrames = keyFrames; + } + + public long getUs() { + return frame * usPerSample; + } + + public void advance() { + frame++; + } + + public boolean isVideo() { + return streamHeaderBox.isVideo(); + } + + public boolean isAudio() { + return streamHeaderBox.isAudio(); + } +} 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 index 8317213e8b..039a1c8acc 100644 --- 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 @@ -65,17 +65,4 @@ public class AviUtil { } 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 index cbf42840b3..bb00c7043f 100644 --- 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 @@ -1,5 +1,8 @@ package com.google.android.exoplayer2.extractor.avi; +/** + * This is referred to as a Chunk in the MS spec, but that gets confusing with AV chunks + */ public class Box { private final long size; private final int type; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java new file mode 100644 index 0000000000..8bf568d72d --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java @@ -0,0 +1,25 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.annotation.NonNull; +import java.nio.ByteBuffer; + +public class BoxFactory { + @NonNull + public ResidentBox createBox(final int type, final int size, final ByteBuffer byteBuffer) { + final ByteBuffer boxBuffer = AviExtractor.allocate(size); + AviUtil.copy(byteBuffer, boxBuffer, size); + + switch (type) { + case AviHeaderBox.AVIH: + return new AviHeaderBox(type, size, boxBuffer); + case ListBox.LIST: + return new ListBox(type, size, boxBuffer); + case StreamHeaderBox.STRH: + return new StreamHeaderBox(type, size, boxBuffer); + case StreamFormatBox.STRF: + return new StreamFormatBox(type, size, boxBuffer); + default: + return new ResidentBox(type, size, boxBuffer); + } + } +} 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 deleted file mode 100644 index 22aff293b3..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IAviList.java +++ /dev/null @@ -1,9 +0,0 @@ -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/ResidentList.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java similarity index 56% rename from library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ResidentList.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index f4811f8555..e9767bd818 100644 --- 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/ListBox.java @@ -5,15 +5,18 @@ import java.nio.ByteBuffer; /** * An AVI LIST box, memory resident */ -public class ResidentList extends ResidentBox implements IAviList { +public class ListBox extends ResidentBox { + public static final int LIST = 'L' | ('I' << 8) | ('S' << 16) | ('T' << 24); + //Header List + public static final int TYPE_HDRL = 'h' | ('d' << 8) | ('r' << 16) | ('l' << 24); + private final int listType; - ResidentList(int type, int size, ByteBuffer byteBuffer) { + ListBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); listType = byteBuffer.getInt(0); } - @Override public int getListType() { return listType; } 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 index 6ec4286cdc..d4283e314a 100644 --- 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 @@ -13,25 +13,28 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; +/** + * A box that is resident in memory + */ public class ResidentBox extends Box { private static final String TAG = AviExtractor.TAG; - final private static int MAX_RESIDENT = 2*1024; + final private static int MAX_RESIDENT = 64*1024; final ByteBuffer byteBuffer; - 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; - } - } +// private Class getClass(final int type) { +// switch (type) { +// case AviHeaderBox.AVIH: +// return AviHeaderBox.class; +// case ListBox.LIST: +// return ListBox.class; +// case StreamHeaderBox.STRH: +// return StreamHeaderBox.class; +// case StreamFormatBox.STRF: +// return StreamFormatBox.class; +// default: +// return ResidentBox.class; +// } +// } ResidentBox(int type, int size, ByteBuffer byteBuffer) { super(type, size); @@ -92,20 +95,14 @@ public class ResidentBox extends Box { } @NonNull - public List getBoxList() { + public List getBoxList(final BoxFactory boxFactory) { 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; - } + final ResidentBox residentBox = boxFactory.createBox(type, size, temp); list.add(residentBox); } return 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/StreamFormatBox.java similarity index 79% rename from library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormat.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java index 9b0c67a09d..8b727d4c1a 100644 --- 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/StreamFormatBox.java @@ -3,10 +3,10 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; import java.nio.ByteBuffer; -public class StreamFormat extends ResidentBox { +public class StreamFormatBox extends ResidentBox { public static final int STRF = 's' | ('t' << 8) | ('r' << 16) | ('f' << 24); - StreamFormat(int type, int size, ByteBuffer byteBuffer) { + StreamFormatBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, 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/StreamHeaderBox.java similarity index 59% rename from library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 353ec32c32..81f2bef8a5 100644 --- 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/StreamHeaderBox.java @@ -4,7 +4,10 @@ import android.util.SparseArray; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; -public class StreamHeader extends ResidentBox { +/** + * AVISTREAMHEADER + */ +public class StreamHeaderBox extends ResidentBox { public static final int STRH = 's' | ('t' << 8) | ('r' << 16) | ('h' << 24); //Audio Stream @@ -16,15 +19,23 @@ public class StreamHeader extends ResidentBox { 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); + //Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG + final String mimeType = MimeTypes.VIDEO_MP4V; + //final String mimeType = MimeTypes.VIDEO_H263; + + //Doesn't seem to be supported on Android + //STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_MP4); + STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); + STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); + STREAM_MAP.put('X' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.IMAGE_JPEG); } - StreamHeader(int type, int size, ByteBuffer byteBuffer) { + StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); } @@ -40,7 +51,15 @@ public class StreamHeader extends ResidentBox { return getRate() / (float)getScale(); } - public String getCodec() { + /** + * How long each sample covers + * @return + */ + public long getUsPerSample() { + return getScale() * 1_000_000L / getRate(); + } + + public String getMimeType() { return STREAM_MAP.get(getFourCC()); } @@ -67,8 +86,15 @@ public class StreamHeader extends ResidentBox { public int getRate() { return byteBuffer.getInt(24); } - //28 - dwStart + public int getStart() { + return byteBuffer.getInt(28); + } public long getLength() { return byteBuffer.getInt(32) & AviUtil.UINT_MASK; } + //36 - dwSuggestedBufferSize + //40 - dwQuality + public int getSampleSize() { + return byteBuffer.getInt(44); + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java new file mode 100644 index 0000000000..02d8aad034 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java @@ -0,0 +1,51 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.annotation.NonNull; +import java.util.Arrays; + +public class UnboundedIntArray { + @NonNull + int[] array; + //unint + int size =0; + + public UnboundedIntArray() { + this(8); + } + + public UnboundedIntArray(int size) { + if (size < 0) { + throw new IllegalArgumentException("Initial size must be positive: " + size); + } + array = new int[size]; + } + + public void add(int v) { + if (size == array.length) { + grow(); + } + array[size++] = v; + } + + public int getSize() { + return size; + } + + public void pack() { + array = Arrays.copyOf(array, size); + } + + protected void grow() { + int increase = Math.max(array.length /4, 1); + array = Arrays.copyOf(array, increase + array.length + size); + } + + /** + * Only works if values are in sequential order + * @param v + * @return + */ + public int indexOf(int v) { + return Arrays.binarySearch(array, v); + } +} From 1a1dc44b84585288d9769ecb6e1178c43b4bab88 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 18 Jan 2022 15:28:10 -0700 Subject: [PATCH 03/70] More efficient header parsing --- .../extractor/avi/AviExtractor.java | 32 ++++----- .../android/exoplayer2/extractor/avi/Box.java | 8 ++- .../exoplayer2/extractor/avi/BoxFactory.java | 47 +++++++++++-- .../exoplayer2/extractor/avi/ListBox.java | 68 +++++++++++++++++-- .../exoplayer2/extractor/avi/ResidentBox.java | 34 +--------- .../extractor/avi/StreamDataBox.java | 17 +++++ 6 files changed, 142 insertions(+), 64 deletions(-) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java 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 index c1f39af608..c89d92f0d8 100644 --- 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 @@ -1,7 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; import android.util.SparseArray; -import android.util.SparseIntArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -17,12 +16,10 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; /** * Based on the official MicroSoft spec @@ -46,8 +43,6 @@ public class AviExtractor implements Extractor { 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 @@ -144,20 +139,20 @@ public class AviExtractor implements Extractor { if (inputLen != C.LENGTH_UNSET && inputLen != reportedLen) { Log.w(TAG, "Header length doesn't match stream length"); } - int avi = byteBuffer.getInt(); + final int avi = byteBuffer.getInt(); if (avi != AviExtractor.AVI_) { return null; } - final ListBox header = ListBox.getInstance(byteBuffer, input, ListBox.class); - if (header == null) { + final int list = byteBuffer.getInt(); + if (list != ListBox.LIST) { return null; } - if (header.getListType() != ListBox.TYPE_HDRL) { - Log.e(TAG, "Expected " +AviUtil.toString(ListBox.TYPE_HDRL) + ", got: " + - AviUtil.toString(header.getType())); + final int listSize = byteBuffer.getInt(); + final ListBox listBox = ListBox.newInstance(listSize, new BoxFactory(), input); + if (listBox.getListType() != ListBox.TYPE_HDRL) { return null; } - return header; + return listBox; } long getDuration() { @@ -173,7 +168,7 @@ public class AviExtractor implements Extractor { this.output = output; } - private static ResidentBox peekNext(final List streams, int i, int type) { + private static Box peekNext(final List streams, int i, int type) { if (i + 1 < streams.size() && streams.get(i + 1).getType() == type) { return streams.get(i + 1); } @@ -185,8 +180,7 @@ public class AviExtractor implements Extractor { if (headerList == null) { throw new IOException("AVI Header List not found"); } - final BoxFactory boxFactory = new BoxFactory(); - final List headerChildren = headerList.getBoxList(boxFactory); + final List headerChildren = headerList.getChildren(); aviHeader = AviUtil.getBox(headerChildren, AviHeaderBox.class); if (aviHeader == null) { throw new IOException("AviHeader not found"); @@ -198,9 +192,9 @@ public class AviExtractor implements Extractor { for (Box box : headerChildren) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { final ListBox streamList = (ListBox) box; - final List streamChildren = streamList.getBoxList(boxFactory); + final List streamChildren = streamList.getChildren(); for (int i=0;i codecData; if (codecBox != null) { - codecData = Collections.singletonList(codecBox.byteBuffer.array()); + codecData = Collections.singletonList(codecBox.getData()); i++; } else { codecData = 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 index bb00c7043f..3cc891bc29 100644 --- 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 @@ -4,15 +4,19 @@ package com.google.android.exoplayer2.extractor.avi; * This is referred to as a Chunk in the MS spec, but that gets confusing with AV chunks */ public class Box { - private final long size; + private final int size; private final int type; - Box(int type, long size) { + Box(int type, int size) { this.type = type; this.size = size; } public long getSize() { + return size & AviUtil.UINT_MASK; + } + + public int getSizeInt() { return size; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java index 8bf568d72d..5dc0314ee7 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java @@ -1,25 +1,60 @@ 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.Arrays; public class BoxFactory { - @NonNull + static int[] types = {AviHeaderBox.AVIH, StreamHeaderBox.STRH, StreamFormatBox.STRF, StreamDataBox.STRD}; + static { + Arrays.sort(types); + } + + public boolean isUnknown(final int type) { + return Arrays.binarySearch(types, type) < 0; + } + + @Nullable public ResidentBox createBox(final int type, final int size, final ByteBuffer byteBuffer) { final ByteBuffer boxBuffer = AviExtractor.allocate(size); AviUtil.copy(byteBuffer, boxBuffer, size); - + //TODO: Deal with list + switch (type) { + case AviHeaderBox.AVIH: + return new AviHeaderBox(type, size, boxBuffer); + case StreamHeaderBox.STRH: + return new StreamHeaderBox(type, size, boxBuffer); + case StreamFormatBox.STRF: + return new StreamFormatBox(type, size, boxBuffer); + case StreamDataBox.STRD: + return new StreamDataBox(type, size, boxBuffer); + default: + return null; + } + } + + private ResidentBox createBoxImpl(final int type, final int size, final ByteBuffer boxBuffer) { switch (type) { case AviHeaderBox.AVIH: return new AviHeaderBox(type, size, boxBuffer); - case ListBox.LIST: - return new ListBox(type, size, boxBuffer); case StreamHeaderBox.STRH: return new StreamHeaderBox(type, size, boxBuffer); case StreamFormatBox.STRF: return new StreamFormatBox(type, size, boxBuffer); default: - return new ResidentBox(type, size, boxBuffer); + return null; } } + + public ResidentBox createBox(final int type, final int size, ExtractorInput input) throws IOException { + if (isUnknown(type)) { + input.skipFully(size); + return null; + } + final ByteBuffer boxBuffer = AviExtractor.allocate(size); + input.readFully(boxBuffer.array(),0,size); + return createBoxImpl(type, size, boxBuffer); + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index e9767bd818..c2ca4f343e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java @@ -1,20 +1,28 @@ package com.google.android.exoplayer2.extractor.avi; +import androidx.annotation.NonNull; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; /** * An AVI LIST box, memory resident */ -public class ListBox extends ResidentBox { +public class ListBox extends Box { public static final int LIST = 'L' | ('I' << 8) | ('S' << 16) | ('T' << 24); //Header List public static final int TYPE_HDRL = 'h' | ('d' << 8) | ('r' << 16) | ('l' << 24); private final int listType; - ListBox(int type, int size, ByteBuffer byteBuffer) { - super(type, size, byteBuffer); - listType = byteBuffer.getInt(0); + final List children; + + ListBox(int size, int listType, List children) { + super(LIST, size); + this.listType = listType; + this.children = children; } public int getListType() { @@ -25,4 +33,56 @@ public class ListBox extends ResidentBox { boolean assertType() { return simpleAssert(LIST); } + + @NonNull + public List getChildren() { + return new ArrayList<>(children); + } + +// static List realizeChildren(final ByteBuffer byteBuffer, final BoxFactory boxFactory) { +// final List list = new ArrayList<>(); +// while (byteBuffer.hasRemaining()) { +// final int type = byteBuffer.getInt(); +// final int size = byteBuffer.getInt(); +// final ResidentBox residentBox = boxFactory.createBox(type, size, byteBuffer); +// list.add(residentBox); +// } +// return list; +// } + + /** + * Assume the input is pointing to the list type + * @param boxFactory + * @param input + * @return + * @throws IOException + */ + public static ListBox newInstance(final int listSize, BoxFactory boxFactory, + ExtractorInput input) throws IOException { + + final List list = new ArrayList<>(); + final ByteBuffer headerBuffer = AviExtractor.allocate(8); + byte [] bytes = headerBuffer.array(); + input.readFully(bytes, 0, 4); + final int listType = headerBuffer.getInt(); + + long endPos = input.getPosition() + listSize - 4; + while (input.getPosition() + 8 < endPos) { + headerBuffer.clear(); + input.readFully(bytes, 0, 8); + final int type = headerBuffer.getInt(); + final int size = headerBuffer.getInt(); + final Box box; + if (type == LIST) { + box = newInstance(size, boxFactory, input); + } else { + box = boxFactory.createBox(type, size, input); + } + + if (box != null) { + list.add(box); + } + } + return new ListBox(listSize, listType, list); + } } 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 index d4283e314a..843300b8af 100644 --- 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 @@ -10,32 +10,15 @@ import java.nio.BufferOverflowException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.List; /** * A box that is resident in memory */ public class ResidentBox extends Box { private static final String TAG = AviExtractor.TAG; - final private static int MAX_RESIDENT = 64*1024; + final private static int MAX_RESIDENT = 1024; final ByteBuffer byteBuffer; -// private Class getClass(final int type) { -// switch (type) { -// case AviHeaderBox.AVIH: -// return AviHeaderBox.class; -// case ListBox.LIST: -// return ListBox.class; -// case StreamHeaderBox.STRH: -// return StreamHeaderBox.class; -// case StreamFormatBox.STRF: -// return StreamFormatBox.class; -// default: -// return ResidentBox.class; -// } -// } - ResidentBox(int type, int size, ByteBuffer byteBuffer) { super(type, size); this.byteBuffer = byteBuffer; @@ -81,7 +64,6 @@ public class ResidentBox extends Box { } } - /** * Returns shallow copy of this ByteBuffer with the position at 0 * @return @@ -93,18 +75,4 @@ public class ResidentBox extends Box { clone.clear(); return clone; } - - @NonNull - public List getBoxList(final BoxFactory boxFactory) { - 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 ResidentBox residentBox = boxFactory.createBox(type, size, temp); - list.add(residentBox); - } - return list; - } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java new file mode 100644 index 0000000000..ce6b0260ae --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java @@ -0,0 +1,17 @@ +package com.google.android.exoplayer2.extractor.avi; + +import java.nio.ByteBuffer; + +public class StreamDataBox extends ResidentBox { + //Stream CODEC data + static final int STRD = 's' | ('t' << 8) | ('r' << 16) | ('d' << 24); + + StreamDataBox(int type, int size, ByteBuffer byteBuffer) { + super(type, size, byteBuffer); + } + byte[] getData() { + byte[] data = new byte[byteBuffer.capacity()]; + System.arraycopy(byteBuffer.array(), 0, data, 0, data.length); + return data; + } +} From 58a2ca6083f00f848aadb4177d5580ec96c0d702 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 18 Jan 2022 15:46:25 -0700 Subject: [PATCH 04/70] Fix for movi with LIST('rec ') --- .../exoplayer2/extractor/avi/AviExtractor.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 index c89d92f0d8..8d682aca4f 100644 --- 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 @@ -404,10 +404,15 @@ public class AviExtractor implements Extractor { sampleSize = byteBuffer.getInt(); sampleTrack = idTrackMap.get(id); if (sampleTrack == null) { - if (id != JUNK) { - Log.w(TAG, "Unknown tag=" + AviUtil.toString(id) + " pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " moviEnd=" + moviEnd); + if (id == ListBox.LIST) { + seekPosition.position = input.getPosition() + 4; + } else { + seekPosition.position = input.getPosition() + sampleSize; + if (id != JUNK) { + Log.w(TAG, "Unknown tag=" + AviUtil.toString(id) + " pos=" + (input.getPosition() - 8) + + " size=" + sampleSize + " moviEnd=" + moviEnd); + } } - seekPosition.position = input.getPosition() + sampleSize; return RESULT_SEEK; } else { //Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo()); From 8d90498f791940f95374423963997abb2ce40429 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 18 Jan 2022 23:04:14 -0700 Subject: [PATCH 05/70] Minor Tweaks --- .../extractor/avi/AviExtractor.java | 39 +++++++++++-------- .../extractor/avi/AviHeaderBox.java | 5 ++- .../exoplayer2/extractor/avi/AviTrack.java | 6 ++- 3 files changed, 31 insertions(+), 19 deletions(-) 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 index 8d682aca4f..b1b3388260 100644 --- 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 @@ -49,14 +49,16 @@ public class AviExtractor implements Extractor { static final int IDX1 = 'i' | ('d' << 8) | ('x' << 16) | ('1' << 24); static final int JUNK = 'J' | ('U' << 8) | ('N' << 16) | ('K' << 24); + static final int REC_ = 'r' | ('e' << 8) | ('c' << 16) | (' ' << 24); static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds private int state; private ExtractorOutput output; private AviHeaderBox aviHeader; + private long durationUs = C.TIME_UNSET; private SparseArray idTrackMap = new SparseArray<>(); - //After the movi position + //At the start of the movi tag private long moviOffset; private long moviEnd; private AviSeekMap aviSeekMap; @@ -154,14 +156,9 @@ public class AviExtractor implements Extractor { } return listBox; } - long getDuration() { - if (aviHeader == null) { - return C.TIME_UNSET; - } - return aviHeader.getFrames() * (long)aviHeader.getMicroSecPerFrame(); + return durationUs; } - @Override public void init(ExtractorOutput output) { this.state = STATE_READ_TRACKS; @@ -185,6 +182,8 @@ public class AviExtractor implements Extractor { if (aviHeader == null) { throw new IOException("AviHeader not found"); } + //This is usually wrong, so it will be overwritten by video if present + durationUs = aviHeader.getFrames() * (long)aviHeader.getMicroSecPerFrame(); headerChildren.remove(aviHeader); //headerChildren should only be Stream Lists now @@ -228,6 +227,7 @@ public class AviExtractor implements Extractor { idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), new AviTrack(streamId, trackOutput, streamHeader)); + durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); } else if (streamHeader.isAudio()) { final AudioFormat audioFormat = streamFormat.getAudioFormat(); final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); @@ -329,7 +329,9 @@ public class AviExtractor implements Extractor { final int id = indexByteBuffer.getInt(); final AviTrack aviTrack = idTrackMap.get(id); if (aviTrack == null) { - Log.w(TAG, "Unknown Track Type: " + AviUtil.toString(id)); + if (id != AviExtractor.REC_) { + Log.w(TAG, "Unknown Track Type: " + AviUtil.toString(id)); + } indexByteBuffer.position(indexByteBuffer.position() + 12); continue; } @@ -366,14 +368,16 @@ public class AviExtractor implements Extractor { entry.getValue().pack(); idFrameArray.put(entry.getKey(), entry.getValue().array); final AviTrack aviTrack = idTrackMap.get(entry.getKey()); - //Sometimes this value is way off - long calcUsPerSample = (getDuration()/aviTrack.frame); - float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample; - if (deltaPercent >.01) { - aviTrack.usPerSample = getDuration()/aviTrack.frame; - Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); + //If the index isn't sparse, double check the audio length + if (videoTrack.frame == videoTrack.streamHeaderBox.getLength()) { + //Sometimes this value is way off + long calcUsPerSample = (getDuration()/aviTrack.frame); + float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample; + if (deltaPercent >.01) { + aviTrack.usPerSample = getDuration()/aviTrack.frame; + Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); + } } - } final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.array, idFrameArray, moviOffset, getDuration()); @@ -415,8 +419,9 @@ public class AviExtractor implements Extractor { } return RESULT_SEEK; } else { - //Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo()); + //sampleOffset = (int)(input.getPosition() - 8 - moviOffset); sampleRemaining = sampleSize - sampleTrack.trackOutput.sampleData(input, sampleSize, false); + //Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo()); } } if (sampleRemaining != 0) { @@ -424,7 +429,7 @@ public class AviExtractor implements Extractor { } sampleTrack.trackOutput.sampleMetadata( sampleTrack.getUs(), sampleTrack.isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0 , sampleSize, 0, null); - //Log.d(TAG, "Frame: " + (sampleTrack.isVideo()? 'V' : 'A') + " us=" + sampleTrack.getUs()); + //Log.d(TAG, "Frame: " + (sampleTrack.isVideo()? 'V' : 'A') + " us=" + sampleTrack.getUs() + " size=" + sampleSize); sampleTrack.advance(); return RESULT_CONTINUE; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index ad8146ea3e..c541763bba 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -26,7 +26,10 @@ public class AviHeaderBox extends ResidentBox { } //4 = dwMaxBytesPerSec - //8 = dwPaddingGranularity + //Always 0, but should be 2 +// int getPaddingGranularity() { +// return byteBuffer.getInt(8); +// } int getFlags() { return byteBuffer.getInt(12); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index 3114b319d1..9aa2937268 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -55,7 +55,11 @@ public class AviTrack { if (allKeyFrames) { return true; } - return keyFrames != null && Arrays.binarySearch(keyFrames, frame) >= 0; + if (keyFrames != null) { + return Arrays.binarySearch(keyFrames, frame) >= 0; + } + //Hack: Exo needs at least one frame before it starts playback + return frame == 0; } public void setKeyFrames(int[] keyFrames) { From d9afe5105bebb3b63a7beac9e15cfe126c2cb740 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 18 Jan 2022 23:25:42 -0700 Subject: [PATCH 06/70] Refactor, remove dead code --- .../extractor/avi/AviExtractor.java | 40 +++++++---- .../extractor/avi/AviHeaderBox.java | 5 -- .../exoplayer2/extractor/avi/AviUtil.java | 68 ------------------- .../android/exoplayer2/extractor/avi/Box.java | 10 +-- .../exoplayer2/extractor/avi/BoxFactory.java | 20 ------ .../exoplayer2/extractor/avi/ListBox.java | 25 +++---- .../exoplayer2/extractor/avi/ResidentBox.java | 40 ----------- .../extractor/avi/StreamHeaderBox.java | 2 +- 8 files changed, 37 insertions(+), 173 deletions(-) delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviUtil.java 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 index b1b3388260..6c55fff1db 100644 --- 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 @@ -26,8 +26,23 @@ import java.util.Map; * https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference */ public class AviExtractor implements Extractor { + static final long UINT_MASK = 0xffffffffL; + + static long getUInt(ByteBuffer byteBuffer) { + return byteBuffer.getInt() & UINT_MASK; + } + + @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(); + } + static final String TAG = "AviExtractor"; - static final int KEY_FRAME_MASK = Integer.MIN_VALUE; private static final int PEEK_BYTES = 28; private static final int STATE_READ_TRACKS = 0; @@ -39,8 +54,8 @@ public class AviExtractor implements Extractor { private static final int AVIIF_KEYFRAME = 16; - static final int RIFF = AviUtil.toInt(new byte[]{'R','I','F','F'}); - static final int AVI_ = AviUtil.toInt(new byte[]{'A','V','I',' '}); + static final int RIFF = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); + static final int AVI_ = 'A' | ('V' << 8) | ('I' << 16) | (' ' << 24); //Stream List static final int STRL = 's' | ('t' << 8) | ('r' << 16) | ('l' << 24); //movie data box @@ -103,7 +118,7 @@ public class AviExtractor implements Extractor { if (riff != AviExtractor.RIFF) { return false; } - long reportedLen = AviUtil.getUInt(byteBuffer) + byteBuffer.position(); + long reportedLen = 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"); @@ -136,7 +151,7 @@ public class AviExtractor implements Extractor { if (riff != AviExtractor.RIFF) { return null; } - long reportedLen = AviUtil.getUInt(byteBuffer) + byteBuffer.position(); + long reportedLen = 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"); @@ -177,18 +192,15 @@ public class AviExtractor implements Extractor { if (headerList == null) { throw new IOException("AVI Header List not found"); } - final List headerChildren = headerList.getChildren(); - aviHeader = AviUtil.getBox(headerChildren, AviHeaderBox.class); + aviHeader = headerList.getChild(AviHeaderBox.class); if (aviHeader == null) { throw new IOException("AviHeader not found"); } //This is usually wrong, so it will be overwritten by video if present durationUs = aviHeader.getFrames() * (long)aviHeader.getMicroSecPerFrame(); - headerChildren.remove(aviHeader); - //headerChildren should only be Stream Lists now int streamId = 0; - for (Box box : headerChildren) { + for (Box box : headerList.getChildren()) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { final ListBox streamList = (ListBox) box; final List streamChildren = streamList.getChildren(); @@ -238,7 +250,6 @@ public class AviExtractor implements Extractor { 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); @@ -268,7 +279,7 @@ public class AviExtractor implements Extractor { 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 size = getUInt(byteBuffer); final long position = input.getPosition(); //-4 because we over read for the LIST type long nextBox = position + size - 4; @@ -330,7 +341,7 @@ public class AviExtractor implements Extractor { final AviTrack aviTrack = idTrackMap.get(id); if (aviTrack == null) { if (id != AviExtractor.REC_) { - Log.w(TAG, "Unknown Track Type: " + AviUtil.toString(id)); + Log.w(TAG, "Unknown Track Type: " + toString(id)); } indexByteBuffer.position(indexByteBuffer.position() + 12); continue; @@ -413,7 +424,7 @@ public class AviExtractor implements Extractor { } else { seekPosition.position = input.getPosition() + sampleSize; if (id != JUNK) { - Log.w(TAG, "Unknown tag=" + AviUtil.toString(id) + " pos=" + (input.getPosition() - 8) + Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " moviEnd=" + moviEnd); } } @@ -470,7 +481,6 @@ public class AviExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - Log.d("Test", "Seek: pos=" + position + " us=" + timeUs); if (position == 0) { if (moviOffset != 0) { resetFrames(); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index c541763bba..c6c094f903 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -12,11 +12,6 @@ public class AviHeaderBox extends ResidentBox { super(type, size, byteBuffer); } - @Override - boolean assertType() { - return simpleAssert(AVIH); - } - boolean hasIndex() { return (getFlags() & AVIF_HASINDEX) > 0; } 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 deleted file mode 100644 index 039a1c8acc..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviUtil.java +++ /dev/null @@ -1,68 +0,0 @@ -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; - } -} 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 index 3cc891bc29..c5c10731f9 100644 --- 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 @@ -13,11 +13,7 @@ public class Box { } public long getSize() { - return size & AviUtil.UINT_MASK; - } - - public int getSizeInt() { - return size; + return size & AviExtractor.UINT_MASK; } public int getType() { @@ -28,8 +24,4 @@ public class Box { 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/BoxFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java index 5dc0314ee7..d8c882ac5b 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java @@ -1,6 +1,5 @@ package com.google.android.exoplayer2.extractor.avi; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.ExtractorInput; import java.io.IOException; import java.nio.ByteBuffer; @@ -16,25 +15,6 @@ public class BoxFactory { return Arrays.binarySearch(types, type) < 0; } - @Nullable - public ResidentBox createBox(final int type, final int size, final ByteBuffer byteBuffer) { - final ByteBuffer boxBuffer = AviExtractor.allocate(size); - AviUtil.copy(byteBuffer, boxBuffer, size); - //TODO: Deal with list - switch (type) { - case AviHeaderBox.AVIH: - return new AviHeaderBox(type, size, boxBuffer); - case StreamHeaderBox.STRH: - return new StreamHeaderBox(type, size, boxBuffer); - case StreamFormatBox.STRF: - return new StreamFormatBox(type, size, boxBuffer); - case StreamDataBox.STRD: - return new StreamDataBox(type, size, boxBuffer); - default: - return null; - } - } - private ResidentBox createBoxImpl(final int type, final int size, final ByteBuffer boxBuffer) { switch (type) { case AviHeaderBox.AVIH: diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index c2ca4f343e..eedb0322ea 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java @@ -1,6 +1,7 @@ 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; @@ -29,26 +30,20 @@ public class ListBox extends Box { return listType; } - @Override - boolean assertType() { - return simpleAssert(LIST); - } - @NonNull public List getChildren() { return new ArrayList<>(children); } -// static List realizeChildren(final ByteBuffer byteBuffer, final BoxFactory boxFactory) { -// final List list = new ArrayList<>(); -// while (byteBuffer.hasRemaining()) { -// final int type = byteBuffer.getInt(); -// final int size = byteBuffer.getInt(); -// final ResidentBox residentBox = boxFactory.createBox(type, size, byteBuffer); -// list.add(residentBox); -// } -// return list; -// } + @Nullable + public T getChild(Class c) { + for (Box box : children) { + if (box.getClass() == c) { + return (T)box; + } + } + return null; + } /** * Assume the input is pointing to the list type 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 index 843300b8af..5234869389 100644 --- 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 @@ -24,46 +24,6 @@ public class ResidentBox extends Box { 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 diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 81f2bef8a5..e0c6cea720 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -90,7 +90,7 @@ public class StreamHeaderBox extends ResidentBox { return byteBuffer.getInt(28); } public long getLength() { - return byteBuffer.getInt(32) & AviUtil.UINT_MASK; + return byteBuffer.getInt(32) & AviExtractor.UINT_MASK; } //36 - dwSuggestedBufferSize //40 - dwQuality From 4c76bf1a9d305fe3d0008db173b71d4a50c1fa55 Mon Sep 17 00:00:00 2001 From: Dustin Date: Fri, 21 Jan 2022 22:25:32 -0700 Subject: [PATCH 07/70] Add file chooser to UI, Fixed timing issues on H264, Fixed PAR on XVID and H264 --- demos/main/src/main/assets/media.exolist.json | 5 +- .../demo/SampleChooserActivity.java | 27 ++- .../exoplayer2/extractor/avi/AvcAviTrack.java | 177 ++++++++++++++++++ .../extractor/avi/AviExtractor.java | 92 +++++---- .../extractor/avi/AviHeaderBox.java | 20 +- .../exoplayer2/extractor/avi/AviTrack.java | 68 +++++-- .../extractor/avi/Mp4vAviTrack.java | 67 +++++++ 7 files changed, 382 insertions(+), 74 deletions(-) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 2a295a8e26..e6cb246db1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -543,9 +543,8 @@ "name": "Misc", "samples": [ { - "name": "AVI", - "uri": "https://drive.google.com/u/0/uc?id=1K6oLKCS56WFbhz33TgilTJBqfMYFTeUd&?export=download", - "extension": "avi" + "name": "User File", + "uri": "content://user" }, { "name": "Dizzy (MP4)", diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index b79a7a62ca..0111210101 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -40,6 +41,8 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.MediaItem; @@ -73,6 +76,7 @@ public class SampleChooserActivity extends AppCompatActivity private static final String TAG = "SampleChooserActivity"; private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position"; private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position"; + private static final Uri USER_CONTENT = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority("user").build(); private String[] uris; private boolean useExtensionRenderers; @@ -80,6 +84,13 @@ public class SampleChooserActivity extends AppCompatActivity private SampleAdapter sampleAdapter; private MenuItem preferExtensionDecodersMenuItem; private ExpandableListView sampleListView; + private final ActivityResultLauncher openDocumentLauncher = registerForActivityResult( + new ActivityResultContracts.OpenDocument(), uri -> { + if (uri != null) { + final MediaItem mediaItem = new MediaItem.Builder().setUri(uri).build(); + startPlayer(Collections.singletonList(mediaItem)); + } + }); @Override public void onCreate(Bundle savedInstanceState) { @@ -223,13 +234,25 @@ public class SampleChooserActivity extends AppCompatActivity prefEditor.apply(); PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag(); + final List mediaItems = playlistHolder.mediaItems; + if (!mediaItems.isEmpty()) { + final MediaItem mediaItem = mediaItems.get(0); + if (mediaItem.localConfiguration != null && USER_CONTENT.equals(mediaItem.localConfiguration.uri)) { + openDocumentLauncher.launch(new String[]{"video/*"}); + return true; + } + } + startPlayer(playlistHolder.mediaItems); + return true; + } + + private void startPlayer(final List mediaItems) { Intent intent = new Intent(this, PlayerActivity.class); intent.putExtra( IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, isNonNullAndChecked(preferExtensionDecodersMenuItem)); - IntentUtil.addToIntent(playlistHolder.mediaItems, intent); + IntentUtil.addToIntent(mediaItems, intent); startActivity(intent); - return true; } private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java new file mode 100644 index 0000000000..0c9d88f380 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java @@ -0,0 +1,177 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.annotation.NonNull; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.NalUnitUtil; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import java.io.IOException; + +public class AvcAviTrack extends AviTrack{ + private static final int NAL_TYPE_IRD = 5; + private static final int NAL_TYPE_SEI = 6; + private static final int NAL_TYPE_SPS = 7; + private static final int NAL_MASK = 0x1f; + private Format.Builder formatBuilder; + private float pixelWidthHeightRatio = 1f; + private NalUnitUtil.SpsData spsData; + //The frame as a calculated from the picCount + private int picFrame; + private int lastPicFrame; + //Largest picFrame, used when we hit an I frame + private int maxPicFrame =-1; + private int maxPicCount; + private int posHalf; + private int negHalf; + + AvcAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput, + @NonNull Format.Builder formatBuilder) { + super(id, streamHeaderBox, trackOutput); + this.formatBuilder = formatBuilder; + } + + public void setFormatBuilder(Format.Builder formatBuilder) { + this.formatBuilder = formatBuilder; + } + + private int seekNal(final ParsableByteArray parsableByteArray) { + final byte[] buffer = parsableByteArray.getData(); + for (int i=parsableByteArray.getPosition();i= 0) { + if (nal == NAL_TYPE_SPS) { + spsData = NalUnitUtil.parseSpsNalUnitPayload(parsableByteArray.getData(), parsableByteArray.getPosition(), parsableByteArray.capacity()); + maxPicCount = 1 << (spsData.picOrderCntLsbLength); + posHalf = maxPicCount / 2; //Not sure why pics are 2x + negHalf = -posHalf; + //Not sure if this works after the fact + if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { + formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio); + trackOutput.format(formatBuilder.build()); + } + Log.d(AviExtractor.TAG, "SPS Frame: maxPicCount=" + maxPicCount); + } else if (nal == NAL_TYPE_IRD) { + processIdr(); + } + } + parsableByteArray.setPosition(0); + trackOutput.sampleData(parsableByteArray, parsableByteArray.capacity()); + int flags = 0; + if (isKeyFrame()) { + flags |= C.BUFFER_FLAG_KEY_FRAME; + } + trackOutput.sampleMetadata(getUs(frame), flags, parsableByteArray.capacity(), 0, null); + Log.d(AviExtractor.TAG, "SPS Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame()); + advance(); + } + + @Override + int getUsFrame() { + return picFrame; + } + + int getPicOrderCountLsb(byte[] peek) { + if (peek[3] != 1) { + return 0; + } + final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, 5, peek.length); + //slide_header() + in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice + in.readUnsignedExpGolombCodedInt(); //slice_type + in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id + if (spsData.separateColorPlaneFlag) { + in.skipBits(2); //colour_plane_id + } + in.readBits(spsData.frameNumLength); //frame_num + if (!spsData.frameMbsOnlyFlag) { + boolean field_pic_flag = in.readBit(); // field_pic_flag + if (field_pic_flag) { + in.readBit(); // bottom_field_flag + } + } + //We skip IDR in the switch + if (spsData.picOrderCountType == 0) { + int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); + Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); + return picOrderCountLsb; + } + return 0; + } + + @Override + public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { + final int peekSize = Math.min(size, 16); + byte[] peek = new byte[peekSize]; + input.peekFully(peek, 0, peekSize); + final int nalType = peek[4] & NAL_MASK; + switch (nalType) { + case 1: + case 2: + case 3: + case 4: { + final int myPicCount = getPicOrderCountLsb(peek); + int delta = myPicCount - lastPicFrame; + if (delta < negHalf) { + delta += maxPicCount; + } else if (delta > posHalf) { + delta -= maxPicCount; + } + picFrame += delta / 2; + lastPicFrame = myPicCount; + if (maxPicFrame < picFrame) { + maxPicFrame = picFrame; + } + break; + } + case NAL_TYPE_IRD: + processIdr(); + break; + case NAL_TYPE_SEI: + case NAL_TYPE_SPS: + readSps(size, input); + return true; + } + return super.newChunk(tag, size, input); + } + + public static String toString(byte[] buffer, int i, final int len) { + final StringBuilder sb = new StringBuilder((len - i) * 3); + while (i < len) { + String hex = Integer.toHexString(buffer[i] & 0xff); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + sb.append(' '); + i++; + } + return sb.toString(); + } +} 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 index 6c55fff1db..f8797b29b0 100644 --- 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 @@ -82,9 +82,7 @@ public class AviExtractor implements Extractor { // private long indexOffset; //Usually chunkStart //If partial read - private transient AviTrack sampleTrack; - private transient int sampleRemaining; - private transient int sampleSize; + private transient AviTrack chunkHandler; public AviExtractor() { this(0); @@ -213,14 +211,6 @@ public class AviExtractor implements Extractor { i++; if (streamHeader.isVideo()) { final VideoFormat videoFormat = streamFormat.getVideoFormat(); - final StreamDataBox codecBox = (StreamDataBox) peekNext(streamChildren, i, StreamDataBox.STRD); - final List codecData; - if (codecBox != null) { - codecData = Collections.singletonList(codecBox.getData()); - 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()); @@ -228,17 +218,30 @@ public class AviExtractor implements Extractor { builder.setFrameRate(streamHeader.getFrameRate()); final String mimeType = streamHeader.getMimeType(); builder.setSampleMimeType(mimeType); - if (MimeTypes.VIDEO_H263.equals(mimeType)) { - builder.setSelectionFlags(C.SELECTION_FLAG_FORCED); - } - //builder.setCodecs(streamHeader.getCodec()); - if (codecData != null) { - builder.setInitializationData(codecData); +// final StreamDataBox codecBox = (StreamDataBox) peekNext(streamChildren, i, StreamDataBox.STRD); +// final List codecData; +// if (codecBox != null) { +// codecData = Collections.singletonList(codecBox.getData()); +// i++; +// } else { +// codecData = null; +// } +// if (codecData != null) { +// builder.setInitializationData(codecData); +// } + final AviTrack aviTrack; + switch (mimeType) { + case MimeTypes.VIDEO_MP4V: + aviTrack = new Mp4vAviTrack(streamId, streamHeader, trackOutput, builder); + break; + case MimeTypes.VIDEO_H264: + aviTrack = new AvcAviTrack(streamId, streamHeader, trackOutput, builder); + break; + default: + aviTrack = new AviTrack(streamId, streamHeader, trackOutput); } trackOutput.format(builder.build()); - idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), - new AviTrack(streamId, trackOutput, - streamHeader)); + idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), aviTrack); durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); } else if (streamHeader.isAudio()) { final AudioFormat audioFormat = streamFormat.getAudioFormat(); @@ -262,7 +265,7 @@ public class AviExtractor implements Extractor { } trackOutput.format(builder.build()); idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24), - new AviTrack(streamId, trackOutput, streamHeader)); + new AviTrack(streamId, streamHeader, trackOutput)); } } streamId++; @@ -374,20 +377,20 @@ public class AviExtractor implements Extractor { final int[] keyFrames = keyFrameList.array; videoTrack.setKeyFrames(keyFrames); + //Correct the timings + durationUs = videoTrack.usPerSample * videoTrack.frame; + final SparseArray idFrameArray = new SparseArray<>(); for (Map.Entry entry : audioIdFrameMap.entrySet()) { entry.getValue().pack(); idFrameArray.put(entry.getKey(), entry.getValue().array); final AviTrack aviTrack = idTrackMap.get(entry.getKey()); - //If the index isn't sparse, double check the audio length - if (videoTrack.frame == videoTrack.streamHeaderBox.getLength()) { - //Sometimes this value is way off - long calcUsPerSample = (getDuration()/aviTrack.frame); - float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample; - if (deltaPercent >.01) { - aviTrack.usPerSample = getDuration()/aviTrack.frame; - Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); - } + //Sometimes this value is way off + long calcUsPerSample = (getDuration()/aviTrack.frame); + float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample; + if (deltaPercent >.01) { + aviTrack.usPerSample = getDuration()/aviTrack.frame; + Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); } } final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.array, @@ -397,8 +400,10 @@ public class AviExtractor implements Extractor { } int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException { - if (sampleRemaining != 0) { - sampleRemaining -= sampleTrack.trackOutput.sampleData(input, sampleRemaining, false); + if (chunkHandler != null) { + if (chunkHandler.resume(input)) { + chunkHandler = null; + } } else { ByteBuffer byteBuffer = allocate(8); final byte[] bytes = byteBuffer.array(); @@ -415,33 +420,26 @@ public class AviExtractor implements Extractor { return RESULT_END_OF_INPUT; } input.readFully(bytes, 1, 7); - int id = byteBuffer.getInt(); - sampleSize = byteBuffer.getInt(); - sampleTrack = idTrackMap.get(id); + final int id = byteBuffer.getInt(); + final int size = byteBuffer.getInt(); + AviTrack sampleTrack = idTrackMap.get(id); if (sampleTrack == null) { if (id == ListBox.LIST) { seekPosition.position = input.getPosition() + 4; } else { - seekPosition.position = input.getPosition() + sampleSize; + seekPosition.position = input.getPosition() + size; if (id != JUNK) { Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8) - + " size=" + sampleSize + " moviEnd=" + moviEnd); + + " size=" + size + " moviEnd=" + moviEnd); } } return RESULT_SEEK; } else { - //sampleOffset = (int)(input.getPosition() - 8 - moviOffset); - sampleRemaining = sampleSize - sampleTrack.trackOutput.sampleData(input, sampleSize, false); - //Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo()); + if (!sampleTrack.newChunk(id, size, input)) { + chunkHandler = sampleTrack; + } } } - if (sampleRemaining != 0) { - return RESULT_CONTINUE; - } - sampleTrack.trackOutput.sampleMetadata( - sampleTrack.getUs(), sampleTrack.isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0 , sampleSize, 0, null); - //Log.d(TAG, "Frame: " + (sampleTrack.isVideo()? 'V' : 'A') + " us=" + sampleTrack.getUs() + " size=" + sampleSize); - sampleTrack.advance(); return RESULT_CONTINUE; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index c6c094f903..08d5ddf9e3 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -3,7 +3,8 @@ package com.google.android.exoplayer2.extractor.avi; import java.nio.ByteBuffer; public class AviHeaderBox extends ResidentBox { - public static final int AVIF_HASINDEX = 0x10; + private static final int AVIF_HASINDEX = 0x10; + private static int AVIF_MUSTUSEINDEX = 0x20; static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24); //AVIMAINHEADER @@ -12,19 +13,20 @@ public class AviHeaderBox extends ResidentBox { super(type, size, byteBuffer); } - boolean hasIndex() { - return (getFlags() & AVIF_HASINDEX) > 0; - } - int getMicroSecPerFrame() { return byteBuffer.getInt(0); } //4 = dwMaxBytesPerSec - //Always 0, but should be 2 -// int getPaddingGranularity() { -// return byteBuffer.getInt(8); -// } + //8 = dwPaddingGranularity - Always 0, but should be 2 + + public boolean hasIndex() { + return (getFlags() & AVIF_HASINDEX) == AVIF_HASINDEX; + } + + public boolean mustUseIndex() { + return (getFlags() & AVIF_MUSTUSEINDEX) == AVIF_MUSTUSEINDEX; + } int getFlags() { return byteBuffer.getInt(12); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index 9aa2937268..fb47cc37cd 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -1,10 +1,12 @@ package com.google.android.exoplayer2.extractor.avi; -import android.util.SparseIntArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; import java.util.Arrays; /** @@ -13,9 +15,6 @@ import java.util.Arrays; public class AviTrack { final int id; - @NonNull - final TrackOutput trackOutput; - @NonNull final StreamHeaderBox streamHeaderBox; @@ -26,24 +25,26 @@ public class AviTrack { */ boolean allKeyFrames; + @NonNull + TrackOutput trackOutput; + /** * Key is frame number value is offset */ @Nullable int[] keyFrames; + transient int chunkSize; + transient int chunkRemaining; + /** * Current frame in the stream * This needs to be updated on seek * TODO: Should be offset from StreamHeaderBox.getStart() */ - transient int frame; + int frame; - /** - * - * @param trackOutput - */ - AviTrack(int id, @NonNull TrackOutput trackOutput, @NonNull StreamHeaderBox streamHeaderBox) { + AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) { this.id = id; this.trackOutput = trackOutput; this.streamHeaderBox = streamHeaderBox; @@ -67,11 +68,11 @@ public class AviTrack { } public long getUs() { - return frame * usPerSample; + return getUs(getUsFrame()); } - public void advance() { - frame++; + public long getUs(final int myFrame) { + return myFrame * usPerSample; } public boolean isVideo() { @@ -81,4 +82,45 @@ public class AviTrack { public boolean isAudio() { return streamHeaderBox.isAudio(); } + + public void advance() { + frame++; + } + + /** + * Get the frame number used to calculate the timeUs + * @return + */ + int getUsFrame() { + return frame; + } + + public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { + final int remaining = size - trackOutput.sampleData(input, size, false); + if (remaining == 0) { + done(size); + return true; + } else { + chunkSize = size; + chunkRemaining = remaining; + return false; + } + } + + public boolean resume(ExtractorInput input) throws IOException { + chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false); + if (chunkRemaining == 0) { + done(chunkSize); + return true; + } else { + return false; + } + } + + void done(final int size) { + trackOutput.sampleMetadata( + getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); + //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame()); + advance(); + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java new file mode 100644 index 0000000000..ff0962ece0 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java @@ -0,0 +1,67 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.annotation.NonNull; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import java.io.IOException; + +public class Mp4vAviTrack extends AviTrack { + private static final byte SEQUENCE_START_CODE = (byte)0xb0; + private static final int LAYER_START_CODE = 0x20; + private static final float[] ASPECT_RATIO = {0f, 1f, 12f/11f, 10f/11f, 16f/11f, 40f/33f}; + private static final int Extended_PAR = 0xf; + private final Format.Builder formatBuilder; + private float pixelWidthHeightRatio = 1f; + + Mp4vAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput, + @NonNull Format.Builder formatBuilder) { + super(id, streamHeaderBox, trackOutput); + this.formatBuilder = formatBuilder; + } + + private void processLayerStart(byte[] peek, int offset) { + final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, offset, peek.length); + in.skipBit(); // random_accessible_vol + in.skipBits(8); // video_object_type_indication + boolean is_object_layer_identifier = in.readBit(); + if (is_object_layer_identifier) { + in.skipBits(7); // video_object_layer_verid, video_object_layer_priority + } + int aspect_ratio_info = in.readBits(4); + final float aspectRatio; + if (aspect_ratio_info == Extended_PAR) { + float par_width = (float)in.readBits(8); + float par_height = (float)in.readBits(8); + aspectRatio = par_width / par_height; + } else { + aspectRatio = ASPECT_RATIO[aspect_ratio_info]; + } + if (aspectRatio != pixelWidthHeightRatio) { + trackOutput.format(formatBuilder.setPixelWidthHeightRatio(aspectRatio).build()); + pixelWidthHeightRatio = aspectRatio; + } + } + + private void seekLayerStart(ExtractorInput input) throws IOException { + byte[] peek = new byte[128]; + input.peekFully(peek, 0, peek.length); + for (int i = 4;i Date: Sat, 22 Jan 2022 08:03:49 -0700 Subject: [PATCH 08/70] Fix Avc Seek --- .../exoplayer2/extractor/avi/AvcAviTrack.java | 17 ++++++++++++----- .../exoplayer2/extractor/avi/AviSeekMap.java | 2 +- .../exoplayer2/extractor/avi/AviTrack.java | 4 ++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java index 0c9d88f380..86f2b94f88 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java @@ -21,7 +21,7 @@ public class AvcAviTrack extends AviTrack{ private NalUnitUtil.SpsData spsData; //The frame as a calculated from the picCount private int picFrame; - private int lastPicFrame; + private int lastPicCount; //Largest picFrame, used when we hit an I frame private int maxPicFrame =-1; private int maxPicCount; @@ -56,7 +56,7 @@ public class AvcAviTrack extends AviTrack{ } private void processIdr() { - lastPicFrame = 0; + lastPicCount = 0; picFrame = maxPicFrame + 1; } @@ -97,6 +97,13 @@ public class AvcAviTrack extends AviTrack{ return picFrame; } + @Override + void seekFrame(int frame) { + super.seekFrame(frame); + this.picFrame = frame; + lastPicCount = 0; + } + int getPicOrderCountLsb(byte[] peek) { if (peek[3] != 1) { return 0; @@ -136,15 +143,15 @@ public class AvcAviTrack extends AviTrack{ case 2: case 3: case 4: { - final int myPicCount = getPicOrderCountLsb(peek); - int delta = myPicCount - lastPicFrame; + final int picCount = getPicOrderCountLsb(peek); + int delta = picCount - lastPicCount; if (delta < negHalf) { delta += maxPicCount; } else if (delta > posHalf) { delta -= maxPicCount; } picFrame += delta / 2; - lastPicFrame = myPicCount; + lastPicCount = picCount; if (maxPicFrame < picFrame) { maxPicFrame = picFrame; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index 7577755069..33678c0976 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -63,7 +63,7 @@ public class AviSeekMap implements SeekMap { public void setFrames(final long position, final long timeUs, final SparseArray idTrackMap) { final int seekFrameIndex = getSeekFrameIndex(timeUs); - videoTrack.frame = seekFrameIndex * seekIndexFactor; + videoTrack.seekFrame(seekFrameIndex * seekIndexFactor); for (int i=0;i Date: Sat, 22 Jan 2022 09:44:10 -0700 Subject: [PATCH 09/70] AvcAviTrack cleanup --- .../exoplayer2/extractor/avi/AvcAviTrack.java | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java index 86f2b94f88..4b3e6f8de5 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java @@ -71,7 +71,6 @@ public class AvcAviTrack extends AviTrack{ maxPicCount = 1 << (spsData.picOrderCntLsbLength); posHalf = maxPicCount / 2; //Not sure why pics are 2x negHalf = -posHalf; - //Not sure if this works after the fact if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio); trackOutput.format(formatBuilder.build()); @@ -106,7 +105,7 @@ public class AvcAviTrack extends AviTrack{ int getPicOrderCountLsb(byte[] peek) { if (peek[3] != 1) { - return 0; + return -1; } final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, 5, peek.length); //slide_header() @@ -126,10 +125,10 @@ public class AvcAviTrack extends AviTrack{ //We skip IDR in the switch if (spsData.picOrderCountType == 0) { int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); - Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); + //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); return picOrderCountLsb; } - return 0; + return -1; } @Override @@ -144,6 +143,10 @@ public class AvcAviTrack extends AviTrack{ case 3: case 4: { final int picCount = getPicOrderCountLsb(peek); + if (picCount < 0) { + Log.d(AviExtractor.TAG, "Error getting PicOrder"); + seekFrame(frame); + } int delta = picCount - lastPicCount; if (delta < negHalf) { delta += maxPicCount; @@ -167,18 +170,4 @@ public class AvcAviTrack extends AviTrack{ } return super.newChunk(tag, size, input); } - - public static String toString(byte[] buffer, int i, final int len) { - final StringBuilder sb = new StringBuilder((len - i) * 3); - while (i < len) { - String hex = Integer.toHexString(buffer[i] & 0xff); - if (hex.length() == 1) { - sb.append('0'); - } - sb.append(hex); - sb.append(' '); - i++; - } - return sb.toString(); - } } From d2bb0c2cc1805bb829f27c504016d837271114d3 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 22 Jan 2022 14:27:28 -0700 Subject: [PATCH 10/70] Add MJPEG Support --- .../android/exoplayer2/util/MimeTypes.java | 1 + .../exoplayer2/DefaultRenderersFactory.java | 2 + .../video/BitmapFactoryVideoRenderer.java | 230 ++++++++++++++++++ .../extractor/avi/StreamHeaderBox.java | 7 +- 4 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java 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 a73be489d1..c9bc1f58cf 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 @@ -55,6 +55,7 @@ public final class MimeTypes { 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_JPEG = BASE_TYPE_VIDEO + "/JPEG"; //RFC 3555 public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; // audio/ MIME types diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 0d1c126dc5..4b0fff8ffc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; +import com.google.android.exoplayer2.video.BitmapFactoryVideoRenderer; import com.google.android.exoplayer2.mediacodec.DefaultMediaCodecAdapterFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; @@ -395,6 +396,7 @@ public class DefaultRenderersFactory implements RenderersFactory { eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); out.add(videoRenderer); + out.add(new BitmapFactoryVideoRenderer(eventHandler, eventListener)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java new file mode 100644 index 0000000000..36b67c12b7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -0,0 +1,230 @@ +package com.google.android.exoplayer2.video; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.BaseRenderer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.util.MimeTypes; +import java.nio.ByteBuffer; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class BitmapFactoryVideoRenderer extends BaseRenderer { + private static final String TAG = "BitmapFactoryRenderer"; + final VideoRendererEventListener.EventDispatcher eventDispatcher; + @Nullable + Surface surface; + private boolean firstFrameRendered; + private final Rect rect = new Rect(); + private final Point lastSurface = new Point(); + private VideoSize lastVideoSize = VideoSize.UNKNOWN; + @Nullable + private ThreadPoolExecutor renderExecutor; + @Nullable + private Thread thread; + private long currentTimeUs; + private long nextFrameUs; + private long frameUs; + private boolean ended; + private DecoderCounters decoderCounters; + + public BitmapFactoryVideoRenderer(@Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener) { + super(C.TRACK_TYPE_VIDEO); + eventDispatcher = new VideoRendererEventListener.EventDispatcher(eventHandler, eventListener); + } + + @NonNull + @Override + public String getName() { + return TAG; + } + + @Override + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + firstFrameRendered = ended = false; + renderExecutor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); + decoderCounters = new DecoderCounters(); + eventDispatcher.enabled(decoderCounters); + } + + @Override + protected void onDisabled() { + renderExecutor.shutdownNow(); + eventDispatcher.disabled(decoderCounters); + } + + @Override + protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) + throws ExoPlaybackException { + nextFrameUs = startPositionUs; + for (final Format format : formats) { + @NonNull final FormatHolder formatHolder = getFormatHolder(); + @Nullable final Format currentFormat = formatHolder.format; + if (formatHolder.format == null || !currentFormat.equals(format)) { + getFormatHolder().format = format; + eventDispatcher.inputFormatChanged(format, null); + frameUs = (long)(1_000_000L / format.frameRate); + } + } + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + synchronized (eventDispatcher) { + currentTimeUs = positionUs; + eventDispatcher.notify(); + } + if (renderExecutor.getActiveCount() > 0) { + if (positionUs > nextFrameUs) { + long us = (positionUs - nextFrameUs) + frameUs; + long dropped = us / frameUs; + eventDispatcher.droppedFrames((int)dropped, us); + nextFrameUs += frameUs * dropped; + } + return; + } + final FormatHolder formatHolder = getFormatHolder(); + final DecoderInputBuffer decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + int result = readSource(formatHolder, decoderInputBuffer, 0); + if (result == C.RESULT_BUFFER_READ) { + renderExecutor.execute(new RenderRunnable(decoderInputBuffer, nextFrameUs)); + nextFrameUs += frameUs; + } else if (result == C.RESULT_END_OF_INPUT) { + ended = true; + } + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + nextFrameUs = positionUs; + @Nullable + final Thread thread = this.thread; + if (thread != null) { + thread.interrupt(); + } + } + + @Override + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_VIDEO_OUTPUT) { + if (message instanceof Surface) { + surface = (Surface) message; + } else { + surface = null; + } + } + super.handleMessage(messageType, message); + } + + @Override + public boolean isReady() { + return surface != null; + } + + @Override + public boolean isEnded() { + return ended && renderExecutor.getActiveCount() == 0; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + //Technically could support any format BitmapFactory supports + if (MimeTypes.VIDEO_JPEG.equals(format.sampleMimeType)) { + return RendererCapabilities.create(C.FORMAT_HANDLED); + } + return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); + } + + class RenderRunnable implements Runnable { + final DecoderInputBuffer decoderInputBuffer; + final long renderUs; + + RenderRunnable(final DecoderInputBuffer decoderInputBuffer, long renderUs) { + this.decoderInputBuffer = decoderInputBuffer; + this.renderUs = renderUs; + } + + public void run() { + synchronized (eventDispatcher) { + while (currentTimeUs < renderUs) { + try { + thread = Thread.currentThread(); + eventDispatcher.wait(); + } catch (InterruptedException e) { + //If we are interrupted, treat as a cancel + return; + } finally { + thread = null; + } + } + } + @Nullable + final ByteBuffer byteBuffer = decoderInputBuffer.data; + @Nullable + final Surface surface = BitmapFactoryVideoRenderer.this.surface; + if (byteBuffer != null && surface != null) { + final Bitmap bitmap; + try { + bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.arrayOffset() + byteBuffer.position()); + } catch (Exception e) { + eventDispatcher.videoCodecError(e); + return; + } + if (bitmap == null) { + eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); + return; + } + //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + final Canvas canvas = surface.lockCanvas(null); + + final Rect clipBounds = canvas.getClipBounds(); + final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); + final boolean videoSizeChanged; + if (videoSize.equals(lastVideoSize)) { + videoSizeChanged = false; + } else { + lastVideoSize = videoSize; + eventDispatcher.videoSizeChanged(videoSize); + videoSizeChanged = true; + } + if (lastSurface.x != clipBounds.width() || lastSurface.y != clipBounds.height() || + videoSizeChanged) { + lastSurface.x = clipBounds.width(); + lastSurface.y = clipBounds.height(); + final float scaleX = lastSurface.x / (float)videoSize.width; + final float scaleY = lastSurface.y / (float)videoSize.height; + final float scale = Math.min(scaleX, scaleY); + final float width = videoSize.width * scale; + final float height = videoSize.height * scale; + final int x = (int)(lastSurface.x - width) / 2; + final int y = (int)(lastSurface.y - height) / 2; + rect.set(x, y, x + (int)width, y + (int) height); + } + canvas.drawBitmap(bitmap, null, rect, null); + + surface.unlockCanvasAndPost(canvas); + decoderCounters.renderedOutputBufferCount++; + if (!firstFrameRendered) { + firstFrameRendered = true; + eventDispatcher.renderedFirstFrame(surface); + } + } + } + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index e0c6cea720..8eea67db55 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -24,7 +24,7 @@ public class StreamHeaderBox extends ResidentBox { //final String mimeType = MimeTypes.VIDEO_H263; //Doesn't seem to be supported on Android - //STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_MP4); + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_AVI); STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); @@ -32,7 +32,7 @@ public class StreamHeaderBox extends ResidentBox { STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); STREAM_MAP.put('X' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); - STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.IMAGE_JPEG); + STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_JPEG); } StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) { @@ -52,8 +52,7 @@ public class StreamHeaderBox extends ResidentBox { } /** - * How long each sample covers - * @return + * @return sample duration in us */ public long getUsPerSample() { return getScale() * 1_000_000L / getRate(); From 8d9b895f15c8e8745e00aa5a776ea8a5a19e9d32 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 22 Jan 2022 14:38:49 -0700 Subject: [PATCH 11/70] Fix ID10T timing error --- .../video/BitmapFactoryVideoRenderer.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java index 36b67c12b7..fe571b1e9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -152,28 +152,15 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { } class RenderRunnable implements Runnable { - final DecoderInputBuffer decoderInputBuffer; - final long renderUs; + private DecoderInputBuffer decoderInputBuffer; + private final long renderUs; - RenderRunnable(final DecoderInputBuffer decoderInputBuffer, long renderUs) { + RenderRunnable(@NonNull final DecoderInputBuffer decoderInputBuffer, long renderUs) { this.decoderInputBuffer = decoderInputBuffer; this.renderUs = renderUs; } public void run() { - synchronized (eventDispatcher) { - while (currentTimeUs < renderUs) { - try { - thread = Thread.currentThread(); - eventDispatcher.wait(); - } catch (InterruptedException e) { - //If we are interrupted, treat as a cancel - return; - } finally { - thread = null; - } - } - } @Nullable final ByteBuffer byteBuffer = decoderInputBuffer.data; @Nullable @@ -190,6 +177,21 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); return; } + decoderInputBuffer = null; + //Wait for time to advance to display the Bitmap + synchronized (eventDispatcher) { + while (currentTimeUs < renderUs) { + try { + thread = Thread.currentThread(); + eventDispatcher.wait(); + } catch (InterruptedException e) { + //If we are interrupted, treat as a cancel + return; + } finally { + thread = null; + } + } + } //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); final Canvas canvas = surface.lockCanvas(null); From 3ce652ead286f1834927fb0414d5d65f9d28c9f7 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 22 Jan 2022 16:43:04 -0700 Subject: [PATCH 12/70] Added support for DX50 --- .../android/exoplayer2/extractor/avi/AviExtractor.java | 6 +++++- .../android/exoplayer2/extractor/avi/StreamHeaderBox.java | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) 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 index f8797b29b0..2db2d17b0a 100644 --- 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 @@ -210,13 +210,17 @@ public class AviExtractor implements Extractor { if (streamFormat != null) { i++; if (streamHeader.isVideo()) { + final String mimeType = streamHeader.getMimeType(); + if (mimeType == null) { + Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); + continue; + } final VideoFormat videoFormat = streamFormat.getVideoFormat(); 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()); - final String mimeType = streamHeader.getMimeType(); builder.setSampleMimeType(mimeType); // final StreamDataBox codecBox = (StreamDataBox) peekNext(streamChildren, i, StreamDataBox.STRD); // final List codecData; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 8eea67db55..1c3be2ee6b 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -31,6 +31,7 @@ public class StreamHeaderBox extends ResidentBox { STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); STREAM_MAP.put('X' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); + STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_JPEG); } From c4cf876ddbc9b33d59eff3ab86fae5790dd6d5eb Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 22 Jan 2022 20:41:39 -0700 Subject: [PATCH 13/70] Tests for Mp4vAviTrack and StreamHeaderBox --- library/extractor/build.gradle | 6 ++- .../extractor/avi/Mp4vAviTrack.java | 38 ++++++++----- .../extractor/avi/StreamHeaderBox.java | 22 ++++---- .../exoplayer2/extractor/avi/DataHelper.java | 32 +++++++++++ .../extractor/avi/Mp4vAviTrackTest.java | 51 ++++++++++++++++++ .../extractor/avi/StreamHeaderBoxTest.java | 29 ++++++++++ .../extractordumps/avi/mp4v_sequence.dump | Bin 0 -> 47 bytes .../avi/vids_stream_header.dump | Bin 0 -> 64 bytes 8 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java create mode 100644 testdata/src/test/assets/extractordumps/avi/mp4v_sequence.dump create mode 100644 testdata/src/test/assets/extractordumps/avi/vids_stream_header.dump diff --git a/library/extractor/build.gradle b/library/extractor/build.gradle index 839d13c38a..0d4ef9abfd 100644 --- a/library/extractor/build.gradle +++ b/library/extractor/build.gradle @@ -19,7 +19,11 @@ android { testCoverageEnabled = true } } - + testOptions{ + unitTests.all { + jvmArgs '-noverify' + } + } sourceSets.test.assets.srcDir '../../testdata/src/test/assets/' } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java index ff0962ece0..d5a02962c8 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java @@ -1,6 +1,8 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -13,7 +15,7 @@ public class Mp4vAviTrack extends AviTrack { private static final float[] ASPECT_RATIO = {0f, 1f, 12f/11f, 10f/11f, 16f/11f, 40f/33f}; private static final int Extended_PAR = 0xf; private final Format.Builder formatBuilder; - private float pixelWidthHeightRatio = 1f; + float pixelWidthHeightRatio = 1f; Mp4vAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput, @NonNull Format.Builder formatBuilder) { @@ -21,8 +23,8 @@ public class Mp4vAviTrack extends AviTrack { this.formatBuilder = formatBuilder; } - private void processLayerStart(byte[] peek, int offset) { - final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, offset, peek.length); + @VisibleForTesting + void processLayerStart(@NonNull final ParsableNalUnitBitArray in) { in.skipBit(); // random_accessible_vol in.skipBits(8); // video_object_type_indication boolean is_object_layer_identifier = in.readBit(); @@ -44,23 +46,35 @@ public class Mp4vAviTrack extends AviTrack { } } - private void seekLayerStart(ExtractorInput input) throws IOException { - byte[] peek = new byte[128]; - input.peekFully(peek, 0, peek.length); + @VisibleForTesting + @Nullable + static ParsableNalUnitBitArray findLayerStart(ExtractorInput input, final int peekSize) + throws IOException { + byte[] peek = new byte[peekSize]; + input.peekFully(peek, 0, peekSize); for (int i = 4;i STREAM_MAP = new SparseArray<>(); @@ -30,7 +32,7 @@ public class StreamHeaderBox extends ResidentBox { STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); - STREAM_MAP.put('X' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); + STREAM_MAP.put(XVID, mimeType); STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_JPEG); @@ -86,15 +88,17 @@ public class StreamHeaderBox extends ResidentBox { public int getRate() { return byteBuffer.getInt(24); } - public int getStart() { - return byteBuffer.getInt(28); - } + // 28 - dwStart +// public int getStart() { +// return byteBuffer.getInt(28); +// } public long getLength() { return byteBuffer.getInt(32) & AviExtractor.UINT_MASK; } //36 - dwSuggestedBufferSize //40 - dwQuality - public int getSampleSize() { - return byteBuffer.getInt(44); - } + //44 - dwSampleSize +// public int getSampleSize() { +// return byteBuffer.getInt(44); +// } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java new file mode 100644 index 0000000000..b60472bcb6 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -0,0 +1,32 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class DataHelper { + //Base path "\ExoPlayer\library\extractor\." + private static final File RELATIVE_PATH = new File("../../testdata/src/test/assets/extractordumps/avi/"); + public static FakeExtractorInput getInput(final String fileName) throws IOException { + return new FakeExtractorInput.Builder().setData(getBytes(fileName)).build(); + } + + public static byte[] getBytes(final String fileName) throws IOException { + final File file = new File(RELATIVE_PATH, fileName); + try (FileInputStream in = new FileInputStream(file)) { + final byte[] buffer = new byte[in.available()]; + in.read(buffer); + return buffer; + } + } + + public static StreamHeaderBox getVidsStreamHeader() throws IOException { + final byte[] buffer = getBytes("vids_stream_header.dump"); + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java new file mode 100644 index 0000000000..c14352f4aa --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java @@ -0,0 +1,51 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class Mp4vAviTrackTest { + + @Test + public void isSequenceStart_givenSequence() throws IOException { + final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); + Assert.assertTrue(Mp4vAviTrack.isSequenceStart(input)); + } + + @Test + public void findLayerStart_givenSequence() throws IOException { + final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); + final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input, + (int)input.getLength()); + //Offset 0x12 + Assert.assertEquals(8, bitArray.readBits(8)); + } + + @Test + public void findLayerStart_givenAllZeros() throws IOException { + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[128]).build(); + Assert.assertNull(Mp4vAviTrack.findLayerStart(fakeExtractorInput, 128)); + } + + @Test + public void pixelWidthHeightRatio_givenSequence() throws IOException { + final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); + final Format.Builder formatBuilder = new Format.Builder(); + final Mp4vAviTrack mp4vAviTrack = new Mp4vAviTrack(0, DataHelper.getVidsStreamHeader(), + fakeTrackOutput, formatBuilder); + final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); + mp4vAviTrack.newChunk(0, (int)input.getLength(), input); +// final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input, +// (int)input.getLength()); +// mp4vAviTrack.processLayerStart(bitArray); + Assert.assertEquals(mp4vAviTrack.pixelWidthHeightRatio, 1.2121212, 0.01); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java new file mode 100644 index 0000000000..e56ce01dfc --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java @@ -0,0 +1,29 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class StreamHeaderBoxTest { + private static float FPS24 = 24000f/1001f; + private static final long US_SAMPLE24FPS = (long)(1_000_000L / FPS24); + + @Test + public void getters_givenXvidStreamHeader() throws IOException { + final StreamHeaderBox streamHeaderBox = DataHelper.getVidsStreamHeader(); + + Assert.assertTrue(streamHeaderBox.isVideo()); + Assert.assertFalse(streamHeaderBox.isAudio()); + Assert.assertEquals(StreamHeaderBox.VIDS, streamHeaderBox.getSteamType()); + Assert.assertEquals(StreamHeaderBox.XVID, streamHeaderBox.getFourCC()); + Assert.assertEquals(0, streamHeaderBox.getInitialFrames()); + Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1); + Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample()); + Assert.assertEquals(MimeTypes.VIDEO_MP4V, streamHeaderBox.getMimeType()); + Assert.assertEquals(11805L, streamHeaderBox.getLength()); + } +} diff --git a/testdata/src/test/assets/extractordumps/avi/mp4v_sequence.dump b/testdata/src/test/assets/extractordumps/avi/mp4v_sequence.dump new file mode 100644 index 0000000000000000000000000000000000000000..6859c2b7dd10ef5ec6398b4b16343aba9f4b222b GIT binary patch literal 47 xcmZQzVBGM9fq`)=Cy-zOVg-(At>w(@m&IJHM3RC0O%Y|8E(Qi>=0Nr~0RZ0H3CjQg literal 0 HcmV?d00001 diff --git a/testdata/src/test/assets/extractordumps/avi/vids_stream_header.dump b/testdata/src/test/assets/extractordumps/avi/vids_stream_header.dump new file mode 100644 index 0000000000000000000000000000000000000000..d7e95e2df3548a89f42564ff78c958ae8fef93be GIT binary patch literal 64 ocmXTROeu~C^K@ZA0xy^u7*@nW1Z4G)B#@XVm>3u?FfuRz02cHH$N&HU literal 0 HcmV?d00001 From 3daa74dcebc71bbf389480285f7f835d670ce135 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 22 Jan 2022 22:10:12 -0700 Subject: [PATCH 14/70] Tests for AudioFormat, VideoFormat and UnboundedIntArray --- .../exoplayer2/extractor/avi/AudioFormat.java | 9 +-- .../extractor/avi/AudioFormatTest.java | 25 ++++++++ .../exoplayer2/extractor/avi/DataHelper.java | 14 +++++ .../extractor/avi/UnboundedIntArrayTest.java | 54 ++++++++++++++++++ .../extractor/avi/VideoFormatTest.java | 15 +++++ .../extractordumps/avi/aac_stream_format.dump | Bin 0 -> 20 bytes .../avi/h264_stream_format.dump | Bin 0 -> 40 bytes 7 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java create mode 100644 testdata/src/test/assets/extractordumps/avi/aac_stream_format.dump create mode 100644 testdata/src/test/assets/extractordumps/avi/h264_stream_format.dump 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 index 6bb5a5e81a..0a8b03ce1f 100644 --- 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 @@ -6,8 +6,8 @@ import java.nio.ByteBuffer; public class AudioFormat { public static final short WAVE_FORMAT_PCM = 1; + static final short WAVE_FORMAT_AAC = 0xff; private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; - private static final short WAVE_FORMAT_AAC = 0xff; private static final short WAVE_FORMAT_DVM = 0x2000; //AC3 private static final short WAVE_FORMAT_DTS2 = 0x2001; //DTS private static final SparseArray FORMAT_MAP = new SparseArray<>(); @@ -40,9 +40,10 @@ public class AudioFormat { return byteBuffer.getInt(4); } // 8 - nAvgBytesPerSec(uint) - public int getBlockAlign() { - return byteBuffer.getShort(12); - } + // 12 - nBlockAlign +// public int getBlockAlign() { +// return byteBuffer.getShort(12); +// } public short getBitsPerSample() { return byteBuffer.getShort(14); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java new file mode 100644 index 0000000000..63cfd5c108 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java @@ -0,0 +1,25 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class AudioFormatTest { + final byte[] CODEC_PRIVATE = {0x11, (byte) 0x90}; + + @Test + public void getters_givenAacStreamFormat() throws IOException { + final StreamFormatBox streamFormatBox = DataHelper.getAudioStreamFormat(); + final AudioFormat audioFormat = streamFormatBox.getAudioFormat(); + Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType()); + Assert.assertEquals(2, audioFormat.getChannels()); + Assert.assertEquals(AudioFormat.WAVE_FORMAT_AAC, audioFormat.getFormatTag()); + Assert.assertEquals(48000, audioFormat.getSamplesPerSecond()); + Assert.assertEquals(0, audioFormat.getBitsPerSample()); //Not meaningful for AAC + Assert.assertArrayEquals(CODEC_PRIVATE, audioFormat.getCodecData()); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index b60472bcb6..4762a40e1b 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -29,4 +29,18 @@ public class DataHelper { byteBuffer.order(ByteOrder.LITTLE_ENDIAN); return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); } + + public static StreamFormatBox getAudioStreamFormat() throws IOException { + final byte[] buffer = getBytes("aac_stream_format.dump"); + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer); + } + + public static StreamFormatBox getVideoStreamFormat() throws IOException { + final byte[] buffer = getBytes("h264_stream_format.dump"); + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java new file mode 100644 index 0000000000..2f0f9078e4 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java @@ -0,0 +1,54 @@ +package com.google.android.exoplayer2.extractor.avi; + +import org.junit.Assert; +import org.junit.Test; + +public class UnboundedIntArrayTest { + @Test + public void add_givenInt() { + final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(); + unboundedIntArray.add(4); + Assert.assertEquals(1, unboundedIntArray.getSize()); + Assert.assertEquals(unboundedIntArray.array[0], 4); + } + + @Test + public void indexOf_givenOrderSet() { + final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(); + unboundedIntArray.add(2); + unboundedIntArray.add(4); + unboundedIntArray.add(5); + unboundedIntArray.add(8); + Assert.assertEquals(2, unboundedIntArray.indexOf(5)); + Assert.assertTrue(unboundedIntArray.indexOf(6) < 0); + } + + @Test + public void grow_givenSizeOfOne() { + final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(1); + unboundedIntArray.add(0); + Assert.assertEquals(1, unboundedIntArray.getSize()); + unboundedIntArray.add(1); + Assert.assertTrue(unboundedIntArray.getSize() > 1); + } + + @Test + public void pack_givenSizeOfOne() { + final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(8); + unboundedIntArray.add(1); + unboundedIntArray.add(2); + Assert.assertEquals(8, unboundedIntArray.array.length); + unboundedIntArray.pack(); + Assert.assertEquals(2, unboundedIntArray.array.length); + } + + @Test + public void illegalArgument_givenNegativeSize() { + try { + new UnboundedIntArray(-1); + Assert.fail(); + } catch (IllegalArgumentException e) { + //Intentionally blank + } + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java new file mode 100644 index 0000000000..7f4d6d5d08 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java @@ -0,0 +1,15 @@ +package com.google.android.exoplayer2.extractor.avi; + +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; + +public class VideoFormatTest { + @Test + public void getters_givenVideoStreamFormat() throws IOException { + final StreamFormatBox streamFormatBox = DataHelper.getVideoStreamFormat(); + final VideoFormat videoFormat = streamFormatBox.getVideoFormat(); + Assert.assertEquals(712, videoFormat.getWidth()); + Assert.assertEquals(464, videoFormat.getHeight()); + } +} diff --git a/testdata/src/test/assets/extractordumps/avi/aac_stream_format.dump b/testdata/src/test/assets/extractordumps/avi/aac_stream_format.dump new file mode 100644 index 0000000000000000000000000000000000000000..6d7d7e37a950f543dccf48a2adedccaed74b8cd3 GIT binary patch literal 20 Ycmey*z{Jq7n}NZ}kAVTm1R}u+04`GlasU7T literal 0 HcmV?d00001 diff --git a/testdata/src/test/assets/extractordumps/avi/h264_stream_format.dump b/testdata/src/test/assets/extractordumps/avi/h264_stream_format.dump new file mode 100644 index 0000000000000000000000000000000000000000..b1a99e6525f48f797eafbf427ba579e523a18314 GIT binary patch literal 40 hcmdO3U|=}G#K3TYk%57cL4v`<$jqcco*yWR3IJgG18)ET literal 0 HcmV?d00001 From b90333af0252e9c3f84f2ed34948c1cb12c84e9c Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 23 Jan 2022 09:21:11 -0700 Subject: [PATCH 15/70] Clean up UnboundedIntArray --- .../exoplayer2/extractor/avi/UnboundedIntArray.java | 13 +++++++++++-- .../extractor/avi/UnboundedIntArrayTest.java | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java index 02d8aad034..6efe3d74a5 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java @@ -1,13 +1,15 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import java.util.Arrays; public class UnboundedIntArray { @NonNull + @VisibleForTesting int[] array; //unint - int size =0; + private int size =0; public UnboundedIntArray() { this(8); @@ -32,7 +34,9 @@ public class UnboundedIntArray { } public void pack() { - array = Arrays.copyOf(array, size); + if (size != array.length) { + array = Arrays.copyOf(array, size); + } } protected void grow() { @@ -40,6 +44,11 @@ public class UnboundedIntArray { array = Arrays.copyOf(array, increase + array.length + size); } + public int[] getArray() { + pack(); + return array; + } + /** * Only works if values are in sequential order * @param v diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java index 2f0f9078e4..b3db043918 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java @@ -9,7 +9,7 @@ public class UnboundedIntArrayTest { final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(); unboundedIntArray.add(4); Assert.assertEquals(1, unboundedIntArray.getSize()); - Assert.assertEquals(unboundedIntArray.array[0], 4); + Assert.assertEquals(unboundedIntArray.getArray()[0], 4); } @Test From 98b487eb31491a38b891e2001e8e8b5632f100ea Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 23 Jan 2022 09:21:40 -0700 Subject: [PATCH 16/70] Removed unused StreamDataBox --- .../exoplayer2/extractor/avi/BoxFactory.java | 2 +- .../exoplayer2/extractor/avi/StreamDataBox.java | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java index d8c882ac5b..61b05119fc 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java @@ -6,7 +6,7 @@ import java.nio.ByteBuffer; import java.util.Arrays; public class BoxFactory { - static int[] types = {AviHeaderBox.AVIH, StreamHeaderBox.STRH, StreamFormatBox.STRF, StreamDataBox.STRD}; + static int[] types = {AviHeaderBox.AVIH, StreamHeaderBox.STRH, StreamFormatBox.STRF}; static { Arrays.sort(types); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java deleted file mode 100644 index ce6b0260ae..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamDataBox.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.google.android.exoplayer2.extractor.avi; - -import java.nio.ByteBuffer; - -public class StreamDataBox extends ResidentBox { - //Stream CODEC data - static final int STRD = 's' | ('t' << 8) | ('r' << 16) | ('d' << 24); - - StreamDataBox(int type, int size, ByteBuffer byteBuffer) { - super(type, size, byteBuffer); - } - byte[] getData() { - byte[] data = new byte[byteBuffer.capacity()]; - System.arraycopy(byteBuffer.array(), 0, data, 0, data.length); - return data; - } -} From 77a1873930887887d11d78a6bd025c22853c866c Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 23 Jan 2022 09:22:22 -0700 Subject: [PATCH 17/70] Fix bugs around seek --- .../extractor/avi/AviExtractor.java | 39 ++++++++----------- .../exoplayer2/extractor/avi/AviTrack.java | 4 ++ 2 files changed, 21 insertions(+), 22 deletions(-) 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 index 2db2d17b0a..d0bbc0fa3d 100644 --- 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 @@ -94,7 +94,7 @@ public class AviExtractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException { - return peakHeaderList(input); + return peekHeaderList(input); } static ByteBuffer allocate(int bytes) { @@ -109,7 +109,7 @@ public class AviExtractor implements Extractor { output.seekMap(aviSeekMap); } - boolean peakHeaderList(ExtractorInput input) throws IOException { + static boolean peekHeaderList(ExtractorInput input) throws IOException { final ByteBuffer byteBuffer = allocate(PEEK_BYTES); input.peekFully(byteBuffer.array(), 0, PEEK_BYTES); final int riff = byteBuffer.getInt(); @@ -141,6 +141,7 @@ public class AviExtractor implements Extractor { } return true; } + @Nullable ListBox readHeaderList(ExtractorInput input) throws IOException { final ByteBuffer byteBuffer = allocate(20); @@ -169,9 +170,11 @@ public class AviExtractor implements Extractor { } return listBox; } + long getDuration() { return durationUs; } + @Override public void init(ExtractorOutput output) { this.state = STATE_READ_TRACKS; @@ -222,17 +225,7 @@ public class AviExtractor implements Extractor { builder.setHeight(videoFormat.getHeight()); builder.setFrameRate(streamHeader.getFrameRate()); builder.setSampleMimeType(mimeType); -// final StreamDataBox codecBox = (StreamDataBox) peekNext(streamChildren, i, StreamDataBox.STRD); -// final List codecData; -// if (codecBox != null) { -// codecData = Collections.singletonList(codecBox.getData()); -// i++; -// } else { -// codecData = null; -// } -// if (codecData != null) { -// builder.setInitializationData(codecData); -// } + final AviTrack aviTrack; switch (mimeType) { case MimeTypes.VIDEO_MP4V: @@ -358,7 +351,7 @@ public class AviExtractor implements Extractor { indexByteBuffer.position(indexByteBuffer.position() + 4); //int size = indexByteBuffer.getInt(); if (aviTrack.isVideo()) { - if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { + if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { keyFrameList.add(aviTrack.frame); } if (aviTrack.frame % seekFrameRate == 0) { @@ -377,9 +370,11 @@ public class AviExtractor implements Extractor { indexByteBuffer.compact(); } videoSeekOffset.pack(); - keyFrameList.pack(); - final int[] keyFrames = keyFrameList.array; - videoTrack.setKeyFrames(keyFrames); + if (!videoTrack.isAllKeyFrames()) { + keyFrameList.pack(); + final int[] keyFrames = keyFrameList.getArray(); + videoTrack.setKeyFrames(keyFrames); + } //Correct the timings durationUs = videoTrack.usPerSample * videoTrack.frame; @@ -387,7 +382,7 @@ public class AviExtractor implements Extractor { final SparseArray idFrameArray = new SparseArray<>(); for (Map.Entry entry : audioIdFrameMap.entrySet()) { entry.getValue().pack(); - idFrameArray.put(entry.getKey(), entry.getValue().array); + idFrameArray.put(entry.getKey(), entry.getValue().getArray()); final AviTrack aviTrack = idTrackMap.get(entry.getKey()); //Sometimes this value is way off long calcUsPerSample = (getDuration()/aviTrack.frame); @@ -397,7 +392,7 @@ public class AviExtractor implements Extractor { Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); } } - final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.array, + final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.getArray(), idFrameArray, moviOffset, getDuration()); setSeekMap(seekMap); resetFrames(); @@ -483,7 +478,8 @@ public class AviExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - if (position == 0) { + chunkHandler = null; + if (position <= 0) { if (moviOffset != 0) { resetFrames(); state = STATE_SEEK_START; @@ -498,12 +494,11 @@ public class AviExtractor implements Extractor { void resetFrames() { for (int i=0;i Date: Sun, 23 Jan 2022 10:16:38 -0700 Subject: [PATCH 18/70] Remapped MP4x to video/mp4x --- .../android/exoplayer2/extractor/avi/StreamHeaderBox.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index c3a44f0e7c..6b44a3de5e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -25,8 +25,10 @@ public class StreamHeaderBox extends ResidentBox { final String mimeType = MimeTypes.VIDEO_MP4V; //final String mimeType = MimeTypes.VIDEO_H263; - //Doesn't seem to be supported on Android - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_AVI); + //I've never seen an Android devices that actually supports MP42 + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42"); + //Samsung seems to support the rare MP43. + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43"); STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); From ec26539aebff278b5c26e872d58c2e320f97005d Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 23 Jan 2022 11:29:56 -0700 Subject: [PATCH 19/70] BitmapFactoryVideoRenderer improvements --- .../android/exoplayer2/util/MimeTypes.java | 3 +- .../video/BitmapFactoryVideoRenderer.java | 55 ++++++++++++------- .../extractor/avi/StreamHeaderBox.java | 2 +- 3 files changed, 39 insertions(+), 21 deletions(-) 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 c9bc1f58cf..85ed1d3df4 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 @@ -55,7 +55,8 @@ public final class MimeTypes { 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_JPEG = BASE_TYPE_VIDEO + "/JPEG"; //RFC 3555 + //This exists on Nvidia Shield + public static final String VIDEO_MJPEG = BASE_TYPE_VIDEO + "/mjpeg"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; // audio/ MIME types diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java index fe571b1e9b..ea0eb5712a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -17,6 +17,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; import java.util.concurrent.ArrayBlockingQueue; @@ -38,7 +39,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { private Thread thread; private long currentTimeUs; private long nextFrameUs; - private long frameUs; + private long frameUs = Long.MIN_VALUE; private boolean ended; private DecoderCounters decoderCounters; @@ -69,18 +70,11 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { eventDispatcher.disabled(decoderCounters); } - @Override - protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) - throws ExoPlaybackException { - nextFrameUs = startPositionUs; - for (final Format format : formats) { - @NonNull final FormatHolder formatHolder = getFormatHolder(); - @Nullable final Format currentFormat = formatHolder.format; - if (formatHolder.format == null || !currentFormat.equals(format)) { - getFormatHolder().format = format; - eventDispatcher.inputFormatChanged(format, null); - frameUs = (long)(1_000_000L / format.frameRate); - } + private void onFormatChanged(@NonNull FormatHolder formatHolder) { + @Nullable final Format format = formatHolder.format; + if (format != null) { + eventDispatcher.inputFormatChanged(format, null); + frameUs = (long)(1_000_000L / format.frameRate); } } @@ -91,6 +85,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { eventDispatcher.notify(); } if (renderExecutor.getActiveCount() > 0) { + //Handle decoder overrun if (positionUs > nextFrameUs) { long us = (positionUs - nextFrameUs) + frameUs; long dropped = us / frameUs; @@ -100,13 +95,20 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { return; } final FormatHolder formatHolder = getFormatHolder(); - final DecoderInputBuffer decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - int result = readSource(formatHolder, decoderInputBuffer, 0); + final DecoderInputBuffer decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + final int result = readSource(formatHolder, decoderInputBuffer, + frameUs == Long.MIN_VALUE ? SampleStream.FLAG_REQUIRE_FORMAT : 0); + if (result == C.RESULT_BUFFER_READ) { renderExecutor.execute(new RenderRunnable(decoderInputBuffer, nextFrameUs)); - nextFrameUs += frameUs; - } else if (result == C.RESULT_END_OF_INPUT) { - ended = true; + if (decoderInputBuffer.isEndOfStream()) { + ended = true; + } else { + nextFrameUs += frameUs; + } + } else if (result == C.RESULT_FORMAT_READ) { + onFormatChanged(formatHolder); } } @@ -145,13 +147,14 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { @Override public int supportsFormat(Format format) throws ExoPlaybackException { //Technically could support any format BitmapFactory supports - if (MimeTypes.VIDEO_JPEG.equals(format.sampleMimeType)) { + if (MimeTypes.VIDEO_MJPEG.equals(format.sampleMimeType)) { return RendererCapabilities.create(C.FORMAT_HANDLED); } return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); } class RenderRunnable implements Runnable { + @Nullable private DecoderInputBuffer decoderInputBuffer; private final long renderUs; @@ -160,7 +163,18 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { this.renderUs = renderUs; } + private boolean maybeDropFrame(long frameUs) { + if (Math.abs(frameUs - currentTimeUs) > frameUs) { + eventDispatcher.droppedFrames(1, frameUs); + return true; + } + return false; + } + public void run() { + if (maybeDropFrame(renderUs)) { + return; + } @Nullable final ByteBuffer byteBuffer = decoderInputBuffer.data; @Nullable @@ -192,6 +206,9 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { } } } + if (maybeDropFrame(renderUs)) { + return; + } //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); final Canvas canvas = surface.lockCanvas(null); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 6b44a3de5e..5aecb8ea62 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -37,7 +37,7 @@ public class StreamHeaderBox extends ResidentBox { STREAM_MAP.put(XVID, mimeType); STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); - STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_JPEG); + STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); } StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) { From 167c2f3fc04615d9563f44db5dd3e4fae61792c8 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 23 Jan 2022 12:10:48 -0700 Subject: [PATCH 20/70] Fix alignment in track scanner --- .../exoplayer2/extractor/avi/AviExtractor.java | 14 +++++++++----- .../android/exoplayer2/extractor/avi/ListBox.java | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) 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 index d0bbc0fa3d..594800e292 100644 --- 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 @@ -84,6 +84,14 @@ public class AviExtractor implements Extractor { //If partial read private transient AviTrack chunkHandler; + static void alignInput(ExtractorInput input) throws IOException { + // This isn't documented anywhere, but most files are aligned to even bytes + // and can have gaps of zeros + if ((input.getPosition() & 1) == 1) { + input.skipFully(1); + } + } + public AviExtractor() { this(0); } @@ -406,11 +414,7 @@ public class AviExtractor implements Extractor { } else { ByteBuffer byteBuffer = allocate(8); final byte[] bytes = byteBuffer.array(); - // This isn't documented anywhere, but most files are aligned to even bytes - // and can have gaps of zeros - if ((input.getPosition() & 1) == 1) { - input.skipFully(1); - } + alignInput(input); input.readFully(bytes, 0, 1); while (bytes[0] == 0) { input.readFully(bytes, 0, 1); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index eedb0322ea..88af081578 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java @@ -60,7 +60,7 @@ public class ListBox extends Box { byte [] bytes = headerBuffer.array(); input.readFully(bytes, 0, 4); final int listType = headerBuffer.getInt(); - + //String listTypeName = AviExtractor.toString(listType); long endPos = input.getPosition() + listSize - 4; while (input.getPosition() + 8 < endPos) { headerBuffer.clear(); @@ -73,7 +73,7 @@ public class ListBox extends Box { } else { box = boxFactory.createBox(type, size, input); } - + AviExtractor.alignInput(input); if (box != null) { list.add(box); } From 43b8a9b3363de1b481a894edf606ba8bdc3bd4a7 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 23 Jan 2022 13:45:22 -0700 Subject: [PATCH 21/70] Add support for StreamName fixed issues with position alignment --- .../extractor/avi/AviExtractor.java | 140 ++++++++++-------- .../exoplayer2/extractor/avi/BoxFactory.java | 4 +- .../extractor/avi/StreamNameBox.java | 22 +++ .../exoplayer2/extractor/avi/DataHelper.java | 7 + .../exoplayer2/extractor/avi/ListBuilder.java | 32 ++++ .../extractor/avi/StreamNameBoxTest.java | 25 ++++ 6 files changed, 164 insertions(+), 66 deletions(-) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java 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 index 594800e292..5389b57abc 100644 --- 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 @@ -92,6 +92,13 @@ public class AviExtractor implements Extractor { } } + static long alignPosition(long position) { + if ((position & 1) == 1) { + position++; + } + return position; + } + public AviExtractor() { this(0); } @@ -212,70 +219,73 @@ public class AviExtractor implements Extractor { for (Box box : headerList.getChildren()) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { final ListBox streamList = (ListBox) box; - final List streamChildren = streamList.getChildren(); - for (int i=0;i 0) { - builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); - } - trackOutput.format(builder.build()); - idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24), - new AviTrack(streamId, streamHeader, trackOutput)); - } - } - streamId++; - } + final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); + final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); + if (streamHeader == null) { + Log.w(TAG, "Missing Stream Header"); + continue; } + if (streamFormat == null) { + Log.w(TAG, "Missing Stream Format"); + continue; + } + final Format.Builder builder = new Format.Builder(); + builder.setId(streamId); + final StreamNameBox streamName = streamList.getChild(StreamNameBox.class); + if (streamName != null) { + builder.setLabel(streamName.getName()); + } + if (streamHeader.isVideo()) { + final String mimeType = streamHeader.getMimeType(); + if (mimeType == null) { + Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); + continue; + } + final VideoFormat videoFormat = streamFormat.getVideoFormat(); + final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO); + builder.setWidth(videoFormat.getWidth()); + builder.setHeight(videoFormat.getHeight()); + builder.setFrameRate(streamHeader.getFrameRate()); + builder.setSampleMimeType(mimeType); + + final AviTrack aviTrack; + switch (mimeType) { + case MimeTypes.VIDEO_MP4V: + aviTrack = new Mp4vAviTrack(streamId, streamHeader, trackOutput, builder); + break; + case MimeTypes.VIDEO_H264: + aviTrack = new AvcAviTrack(streamId, streamHeader, trackOutput, builder); + break; + default: + aviTrack = new AviTrack(streamId, streamHeader, trackOutput); + } + trackOutput.format(builder.build()); + idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), aviTrack); + durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); + } else if (streamHeader.isAudio()) { + final AudioFormat audioFormat = streamFormat.getAudioFormat(); + final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); + final String mimeType = audioFormat.getMimeType(); + builder.setSampleMimeType(mimeType); + //builder.setCodecs(audioFormat.getCodec()); + builder.setChannelCount(audioFormat.getChannels()); + builder.setSampleRate(audioFormat.getSamplesPerSecond()); + if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) { + final short bps = audioFormat.getBitsPerSample(); + if (bps == 8) { + builder.setPcmEncoding(C.ENCODING_PCM_8BIT); + } else if (bps == 16){ + builder.setPcmEncoding(C.ENCODING_PCM_16BIT); + } + } + if (MimeTypes.AUDIO_AAC.equals(mimeType) && audioFormat.getCbSize() > 0) { + builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); + } + trackOutput.format(builder.build()); + idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24), + new AviTrack(streamId, streamHeader, trackOutput)); + } + streamId++; } } output.endTracks(); @@ -290,7 +300,7 @@ public class AviExtractor implements Extractor { final long size = getUInt(byteBuffer); final long position = input.getPosition(); //-4 because we over read for the LIST type - long nextBox = position + size - 4; + long nextBox = alignPosition(position + size - 4); if (tag == ListBox.LIST) { final int listType = byteBuffer.getInt(); if (listType == MOVI) { @@ -430,7 +440,7 @@ public class AviExtractor implements Extractor { if (id == ListBox.LIST) { seekPosition.position = input.getPosition() + 4; } else { - seekPosition.position = input.getPosition() + size; + seekPosition.position = alignPosition(input.getPosition() + size); if (id != JUNK) { Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8) + " size=" + size + " moviEnd=" + moviEnd); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java index 61b05119fc..8921cde14d 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java @@ -6,7 +6,7 @@ import java.nio.ByteBuffer; import java.util.Arrays; public class BoxFactory { - static int[] types = {AviHeaderBox.AVIH, StreamHeaderBox.STRH, StreamFormatBox.STRF}; + static int[] types = {AviHeaderBox.AVIH, StreamHeaderBox.STRH, StreamFormatBox.STRF, StreamNameBox.STRN}; static { Arrays.sort(types); } @@ -23,6 +23,8 @@ public class BoxFactory { return new StreamHeaderBox(type, size, boxBuffer); case StreamFormatBox.STRF: return new StreamFormatBox(type, size, boxBuffer); + case StreamNameBox.STRN: + return new StreamNameBox(type, size, boxBuffer); default: return null; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java new file mode 100644 index 0000000000..e8f31766c9 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java @@ -0,0 +1,22 @@ +package com.google.android.exoplayer2.extractor.avi; + +import java.nio.ByteBuffer; + +public class StreamNameBox extends ResidentBox { + public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24); + + StreamNameBox(int type, int size, ByteBuffer byteBuffer) { + super(type, size, byteBuffer); + } + + public String getName() { + int len = byteBuffer.capacity(); + if (byteBuffer.get(len - 1) == 0) { + len -= 1; + } + final byte[] bytes = new byte[len]; + byteBuffer.position(0); + byteBuffer.get(bytes); + return new String(bytes); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 4762a40e1b..2731878d90 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -6,6 +6,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.Arrays; public class DataHelper { //Base path "\ExoPlayer\library\extractor\." @@ -43,4 +44,10 @@ public class DataHelper { byteBuffer.order(ByteOrder.LITTLE_ENDIAN); return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer); } + + public static StreamNameBox getStreamNameBox(final String name) { + byte[] bytes = name.getBytes(); + bytes = Arrays.copyOf(bytes, bytes.length + 1); + return new StreamNameBox(StreamNameBox.STRN, bytes.length, ByteBuffer.wrap(bytes)); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java new file mode 100644 index 0000000000..3be00dd152 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java @@ -0,0 +1,32 @@ +package com.google.android.exoplayer2.extractor.avi; + +import java.nio.ByteBuffer; + +public class ListBuilder { + private ByteBuffer byteBuffer; + + public ListBuilder(int listType) { + byteBuffer = AviExtractor.allocate(12); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(12); + byteBuffer.putInt(listType); + } + + public void addBox(final ResidentBox box) { + long boxLen = 4 + 4 + box.getSize(); + if ((boxLen & 1) == 1) { + boxLen++; + } + final ByteBuffer boxBuffer = AviExtractor.allocate(byteBuffer.capacity() + (int)boxLen); + byteBuffer.clear(); + boxBuffer.put(byteBuffer); + boxBuffer.putInt(box.getType()); + boxBuffer.putInt((int)box.getSize()); + boxBuffer.put(box.getByteBuffer()); + byteBuffer = boxBuffer; + } + public ByteBuffer build() { + byteBuffer.putInt(4, byteBuffer.capacity() - 8); + return byteBuffer; + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java new file mode 100644 index 0000000000..5190837b60 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java @@ -0,0 +1,25 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class StreamNameBoxTest { + @Test + public void createStreamName_givenList() throws IOException { + final String name = "Test"; + final ListBuilder listBuilder = new ListBuilder(AviExtractor.STRL); + listBuilder.addBox(DataHelper.getStreamNameBox(name)); + final ByteBuffer listBuffer = listBuilder.build(); + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(listBuffer.array()).build(); + fakeExtractorInput.skipFully(8); + ListBox listBox = ListBox.newInstance(listBuffer.capacity() - 8, new BoxFactory(), fakeExtractorInput); + Assert.assertEquals(1, listBox.getChildren().size()); + final StreamNameBox streamNameBox = (StreamNameBox) listBox.getChildren().get(0); + //Test + nullT = 5 bytes, so verify that the input is properly aligned + Assert.assertEquals(0, fakeExtractorInput.getPosition() & 1); + Assert.assertEquals(name, streamNameBox.getName()); + } +} From 09485cbed173c3f2909496e1a76ca733f179d2ee Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 23 Jan 2022 14:52:53 -0700 Subject: [PATCH 22/70] Passed along suggestedBufferSize to ExoPlayer --- .../android/exoplayer2/extractor/avi/AviExtractor.java | 4 ++++ .../android/exoplayer2/extractor/avi/StreamHeaderBox.java | 5 ++++- .../exoplayer2/extractor/avi/StreamHeaderBoxTest.java | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) 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 index 5389b57abc..8f0dda0d56 100644 --- 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 @@ -231,6 +231,10 @@ public class AviExtractor implements Extractor { } final Format.Builder builder = new Format.Builder(); builder.setId(streamId); + final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); + if (suggestedBufferSize != 0) { + builder.setMaxInputSize(suggestedBufferSize); + } final StreamNameBox streamName = streamList.getChild(StreamNameBox.class); if (streamName != null) { builder.setLabel(streamName.getName()); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 5aecb8ea62..703bb9aa24 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -97,7 +97,10 @@ public class StreamHeaderBox extends ResidentBox { public long getLength() { return byteBuffer.getInt(32) & AviExtractor.UINT_MASK; } - //36 - dwSuggestedBufferSize + + public int getSuggestedBufferSize() { + return byteBuffer.getInt(36); + } //40 - dwQuality //44 - dwSampleSize // public int getSampleSize() { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java index e56ce01dfc..03b732d1f1 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java @@ -25,5 +25,6 @@ public class StreamHeaderBoxTest { Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample()); Assert.assertEquals(MimeTypes.VIDEO_MP4V, streamHeaderBox.getMimeType()); Assert.assertEquals(11805L, streamHeaderBox.getLength()); + Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize()); } } From 7ea2d75fcdc72132b3d0c0d94e1300f291452fb5 Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 24 Jan 2022 16:02:37 -0700 Subject: [PATCH 23/70] Refactor Clock logic. Refactor peeking for MP4V and AVC. Moved AVI above MP3. --- .../extractor/DefaultExtractorsFactory.java | 2 +- .../exoplayer2/extractor/avi/AvcAviTrack.java | 173 ------- .../extractor/avi/AvcChunkPeeker.java | 112 +++++ .../extractor/avi/AviExtractor.java | 442 ++++++++++-------- .../extractor/avi/AviHeaderBox.java | 31 +- .../exoplayer2/extractor/avi/AviSeekMap.java | 54 ++- .../exoplayer2/extractor/avi/AviTrack.java | 75 ++- .../exoplayer2/extractor/avi/ChunkPeeker.java | 8 + .../exoplayer2/extractor/avi/LinearClock.java | 27 ++ .../extractor/avi/Mp4vAviTrack.java | 81 ---- .../extractor/avi/Mp4vChunkPeeker.java | 76 +++ .../extractor/avi/NalChunkPeeker.java | 113 +++++ .../extractor/avi/PicCountClock.java | 62 +++ .../extractor/avi/StreamHeaderBox.java | 1 + .../extractor/avi/AviExtractorTest.java | 101 ++++ .../exoplayer2/extractor/avi/BitBuffer.java | 46 ++ .../exoplayer2/extractor/avi/DataHelper.java | 8 + .../extractor/avi/MockNalChunkPeeker.java | 22 + .../extractor/avi/Mp4vAviTrackTest.java | 51 -- .../extractor/avi/Mp4vChunkPeekerTest.java | 65 +++ .../extractor/avi/NalChunkPeekerTest.java | 55 +++ 21 files changed, 1020 insertions(+), 585 deletions(-) delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java delete mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java 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 793762569c..5c5f89afce 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 @@ -98,9 +98,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { FileTypes.ADTS, FileTypes.AC3, FileTypes.AC4, + FileTypes.AVI, FileTypes.MP3, FileTypes.JPEG, - FileTypes.AVI, }; private static final FlacExtensionLoader FLAC_EXTENSION_LOADER = new FlacExtensionLoader(); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java deleted file mode 100644 index 4b3e6f8de5..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.google.android.exoplayer2.extractor.avi; - -import androidx.annotation.NonNull; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.NalUnitUtil; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; -import java.io.IOException; - -public class AvcAviTrack extends AviTrack{ - private static final int NAL_TYPE_IRD = 5; - private static final int NAL_TYPE_SEI = 6; - private static final int NAL_TYPE_SPS = 7; - private static final int NAL_MASK = 0x1f; - private Format.Builder formatBuilder; - private float pixelWidthHeightRatio = 1f; - private NalUnitUtil.SpsData spsData; - //The frame as a calculated from the picCount - private int picFrame; - private int lastPicCount; - //Largest picFrame, used when we hit an I frame - private int maxPicFrame =-1; - private int maxPicCount; - private int posHalf; - private int negHalf; - - AvcAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput, - @NonNull Format.Builder formatBuilder) { - super(id, streamHeaderBox, trackOutput); - this.formatBuilder = formatBuilder; - } - - public void setFormatBuilder(Format.Builder formatBuilder) { - this.formatBuilder = formatBuilder; - } - - private int seekNal(final ParsableByteArray parsableByteArray) { - final byte[] buffer = parsableByteArray.getData(); - for (int i=parsableByteArray.getPosition();i= 0) { - if (nal == NAL_TYPE_SPS) { - spsData = NalUnitUtil.parseSpsNalUnitPayload(parsableByteArray.getData(), parsableByteArray.getPosition(), parsableByteArray.capacity()); - maxPicCount = 1 << (spsData.picOrderCntLsbLength); - posHalf = maxPicCount / 2; //Not sure why pics are 2x - negHalf = -posHalf; - if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { - formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio); - trackOutput.format(formatBuilder.build()); - } - Log.d(AviExtractor.TAG, "SPS Frame: maxPicCount=" + maxPicCount); - } else if (nal == NAL_TYPE_IRD) { - processIdr(); - } - } - parsableByteArray.setPosition(0); - trackOutput.sampleData(parsableByteArray, parsableByteArray.capacity()); - int flags = 0; - if (isKeyFrame()) { - flags |= C.BUFFER_FLAG_KEY_FRAME; - } - trackOutput.sampleMetadata(getUs(frame), flags, parsableByteArray.capacity(), 0, null); - Log.d(AviExtractor.TAG, "SPS Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame()); - advance(); - } - - @Override - int getUsFrame() { - return picFrame; - } - - @Override - void seekFrame(int frame) { - super.seekFrame(frame); - this.picFrame = frame; - lastPicCount = 0; - } - - int getPicOrderCountLsb(byte[] peek) { - if (peek[3] != 1) { - return -1; - } - final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, 5, peek.length); - //slide_header() - in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice - in.readUnsignedExpGolombCodedInt(); //slice_type - in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id - if (spsData.separateColorPlaneFlag) { - in.skipBits(2); //colour_plane_id - } - in.readBits(spsData.frameNumLength); //frame_num - if (!spsData.frameMbsOnlyFlag) { - boolean field_pic_flag = in.readBit(); // field_pic_flag - if (field_pic_flag) { - in.readBit(); // bottom_field_flag - } - } - //We skip IDR in the switch - if (spsData.picOrderCountType == 0) { - int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); - //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); - return picOrderCountLsb; - } - return -1; - } - - @Override - public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { - final int peekSize = Math.min(size, 16); - byte[] peek = new byte[peekSize]; - input.peekFully(peek, 0, peekSize); - final int nalType = peek[4] & NAL_MASK; - switch (nalType) { - case 1: - case 2: - case 3: - case 4: { - final int picCount = getPicOrderCountLsb(peek); - if (picCount < 0) { - Log.d(AviExtractor.TAG, "Error getting PicOrder"); - seekFrame(frame); - } - int delta = picCount - lastPicCount; - if (delta < negHalf) { - delta += maxPicCount; - } else if (delta > posHalf) { - delta -= maxPicCount; - } - picFrame += delta / 2; - lastPicCount = picCount; - if (maxPicFrame < picFrame) { - maxPicFrame = picFrame; - } - break; - } - case NAL_TYPE_IRD: - processIdr(); - break; - case NAL_TYPE_SEI: - case NAL_TYPE_SPS: - readSps(size, input); - return true; - } - return super.newChunk(tag, size, input); - } -} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java new file mode 100644 index 0000000000..67620785e5 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java @@ -0,0 +1,112 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.NalUnitUtil; +import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import java.io.IOException; + +public class AvcChunkPeeker extends NalChunkPeeker { + private static final int NAL_TYPE_MASK = 0x1f; + private static final int NAL_TYPE_IRD = 5; + private static final int NAL_TYPE_SEI = 6; + private static final int NAL_TYPE_SPS = 7; + private static final int NAL_TYPE_PPS = 8; + + private final PicCountClock picCountClock; + private final Format.Builder formatBuilder; + private final TrackOutput trackOutput; + + private float pixelWidthHeightRatio = 1f; + private NalUnitUtil.SpsData spsData; + + public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, long usPerChunk) { + super(16); + this.formatBuilder = formatBuilder; + this.trackOutput = trackOutput; + picCountClock = new PicCountClock(usPerChunk); + } + + public PicCountClock getPicCountClock() { + return picCountClock; + } + + @Override + boolean skip(byte nalType) { + return false; + } + + void updatePicCountClock(final int nalTypeOffset) { + final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length); + //slide_header() + in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice + in.readUnsignedExpGolombCodedInt(); //slice_type + in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id + if (spsData.separateColorPlaneFlag) { + in.skipBits(2); //colour_plane_id + } + in.readBits(spsData.frameNumLength); //frame_num + if (!spsData.frameMbsOnlyFlag) { + boolean field_pic_flag = in.readBit(); // field_pic_flag + if (field_pic_flag) { + in.readBit(); // bottom_field_flag + } + } + //We skip IDR in the switch + if (spsData.picOrderCountType == 0) { + int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); + //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); + picCountClock.setPicCount(picOrderCountLsb); + return; + } + picCountClock.setIndex(picCountClock.getIndex()); + } + + private int readSps(ExtractorInput input, int nalTypeOffset) throws IOException { + final int spsStart = nalTypeOffset + 1; + nalTypeOffset = seekNextNal(input, spsStart); + spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); + picCountClock.setMaxPicCount(1 << (spsData.picOrderCntLsbLength)); + if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { + pixelWidthHeightRatio = spsData.pixelWidthHeightRatio; + formatBuilder.setPixelWidthHeightRatio(pixelWidthHeightRatio); + trackOutput.format(formatBuilder.build()); + } + return nalTypeOffset; + } + + @Override + void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException { + while (true) { + final int nalType = buffer[nalTypeOffset] & NAL_TYPE_MASK; + switch (nalType) { + case 1: + case 2: + case 3: + case 4: + updatePicCountClock(nalTypeOffset); + return; + case NAL_TYPE_IRD: + picCountClock.syncIndexes(); + return; + case NAL_TYPE_SEI: + case NAL_TYPE_PPS: { + nalTypeOffset = seekNextNal(input, nalTypeOffset); + //Usually chunks have other NALs after these, so just continue + break; + } + case NAL_TYPE_SPS: + nalTypeOffset = readSps(input, nalTypeOffset); + //Sometimes video frames lurk after these + break; + default: + return; + } + if (nalTypeOffset < 0) { + return; + } + compact(); + } + } +} 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 index 8f0dda0d56..18fc767061 100644 --- 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 @@ -1,8 +1,8 @@ package com.google.android.exoplayer2.extractor.avi; -import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; @@ -17,9 +17,6 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; /** * Based on the official MicroSoft spec @@ -43,13 +40,19 @@ public class AviExtractor implements Extractor { } static final String TAG = "AviExtractor"; - private static final int PEEK_BYTES = 28; + @VisibleForTesting + static final int PEEK_BYTES = 28; - private static final int STATE_READ_TRACKS = 0; - private static final int STATE_FIND_MOVI = 1; - private static final int STATE_READ_IDX1 = 2; - private static final int STATE_READ_SAMPLES = 3; - private static final int STATE_SEEK_START = 4; + @VisibleForTesting + static final int STATE_READ_TRACKS = 0; + @VisibleForTesting + static final int STATE_FIND_MOVI = 1; + @VisibleForTesting + static final int STATE_READ_IDX1 = 2; + @VisibleForTesting + static final int STATE_READ_SAMPLES = 3; + @VisibleForTesting + static final int STATE_SEEK_START = 4; private static final int AVIIF_KEYFRAME = 16; @@ -68,16 +71,17 @@ public class AviExtractor implements Extractor { static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds - private int state; - private ExtractorOutput output; + @VisibleForTesting + int state; + @VisibleForTesting + ExtractorOutput output; private AviHeaderBox aviHeader; private long durationUs = C.TIME_UNSET; - private SparseArray idTrackMap = new SparseArray<>(); + private AviTrack[] aviTracks = new AviTrack[0]; //At the start of the movi tag private long moviOffset; private long moviEnd; private AviSeekMap aviSeekMap; - private int flags; // private long indexOffset; //Usually chunkStart @@ -99,49 +103,42 @@ public class AviExtractor implements Extractor { return position; } - public AviExtractor() { - this(0); - } - - public AviExtractor(int flags) { - this.flags = flags; - } - - @Override - public boolean sniff(ExtractorInput input) throws IOException { - return peekHeaderList(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; - } - - private void setSeekMap(AviSeekMap aviSeekMap) { - this.aviSeekMap = aviSeekMap; - output.seekMap(aviSeekMap); - } - - static boolean peekHeaderList(ExtractorInput input) throws IOException { - final ByteBuffer byteBuffer = allocate(PEEK_BYTES); - input.peekFully(byteBuffer.array(), 0, PEEK_BYTES); + /** + * + * @param input + * @param bytes Must be at least 20 + */ + @Nullable + private ByteBuffer getAviBuffer(ExtractorInput input, int bytes) throws IOException { + if (input.getLength() < bytes) { + return null; + } + final ByteBuffer byteBuffer = allocate(bytes); + input.peekFully(byteBuffer.array(), 0, bytes); final int riff = byteBuffer.getInt(); if (riff != AviExtractor.RIFF) { - return false; + return null; } long reportedLen = 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"); + w("Header length doesn't match stream length"); } int avi = byteBuffer.getInt(); if (avi != AviExtractor.AVI_) { - return false; + return null; } final int list = byteBuffer.getInt(); if (list != ListBox.LIST) { + return null; + } + return byteBuffer; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + final ByteBuffer byteBuffer = getAviBuffer(input, PEEK_BYTES); + if (byteBuffer == null) { return false; } //Len @@ -157,27 +154,25 @@ public class AviExtractor implements Extractor { return true; } + static ByteBuffer allocate(int bytes) { + final byte[] buffer = new byte[bytes]; + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return byteBuffer; + } + + private void setSeekMap(AviSeekMap aviSeekMap) { + this.aviSeekMap = aviSeekMap; + output.seekMap(aviSeekMap); + } + @Nullable ListBox 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 = 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"); - } - final int avi = byteBuffer.getInt(); - if (avi != AviExtractor.AVI_) { - return null; - } - final int list = byteBuffer.getInt(); - if (list != ListBox.LIST) { + final ByteBuffer byteBuffer = getAviBuffer(input, 20); + if (byteBuffer == null) { return null; } + input.skipFully(20); final int listSize = byteBuffer.getInt(); final ListBox listBox = ListBox.newInstance(listSize, new BoxFactory(), input); if (listBox.getListType() != ListBox.TYPE_HDRL) { @@ -196,11 +191,74 @@ public class AviExtractor implements Extractor { this.output = output; } - private static Box peekNext(final List streams, int i, int type) { - if (i + 1 < streams.size() && streams.get(i + 1).getType() == type) { - return streams.get(i + 1); + private void parseStream(final ListBox streamList, int streamId) { + final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); + final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); + if (streamHeader == null) { + Log.w(TAG, "Missing Stream Header"); + return; + } + if (streamFormat == null) { + Log.w(TAG, "Missing Stream Format"); + return; + } + final Format.Builder builder = new Format.Builder(); + builder.setId(streamId); + final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); + if (suggestedBufferSize != 0) { + builder.setMaxInputSize(suggestedBufferSize); + } + final StreamNameBox streamName = streamList.getChild(StreamNameBox.class); + if (streamName != null) { + builder.setLabel(streamName.getName()); + } + if (streamHeader.isVideo()) { + final String mimeType = streamHeader.getMimeType(); + if (mimeType == null) { + Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); + return; + } + final VideoFormat videoFormat = streamFormat.getVideoFormat(); + final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO); + builder.setWidth(videoFormat.getWidth()); + builder.setHeight(videoFormat.getHeight()); + builder.setFrameRate(streamHeader.getFrameRate()); + builder.setSampleMimeType(mimeType); + + final AviTrack aviTrack = new AviTrack(streamId, streamHeader, trackOutput); + if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { + Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(builder, trackOutput); + aviTrack.setChunkPeeker(mp4vChunkPeeker); + } else if (MimeTypes.VIDEO_H264.equals(mimeType)) { + final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample()); + aviTrack.setClock(avcChunkPeeker.getPicCountClock()); + aviTrack.setChunkPeeker(avcChunkPeeker); + } + trackOutput.format(builder.build()); + durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); + aviTracks[streamId] = aviTrack; + } else if (streamHeader.isAudio()) { + final AudioFormat audioFormat = streamFormat.getAudioFormat(); + final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); + final String mimeType = audioFormat.getMimeType(); + builder.setSampleMimeType(mimeType); + //builder.setCodecs(audioFormat.getCodec()); + builder.setChannelCount(audioFormat.getChannels()); + builder.setSampleRate(audioFormat.getSamplesPerSecond()); + if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) { + final short bps = audioFormat.getBitsPerSample(); + if (bps == 8) { + builder.setPcmEncoding(C.ENCODING_PCM_8BIT); + } else if (bps == 16){ + builder.setPcmEncoding(C.ENCODING_PCM_16BIT); + } + } + if (MimeTypes.AUDIO_AAC.equals(mimeType) && audioFormat.getCbSize() > 0) { + builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); + } + trackOutput.format(builder.build()); + aviTracks[streamId] = new AviTrack(streamId, streamHeader, trackOutput); } - return null; } private int readTracks(ExtractorInput input) throws IOException { @@ -212,83 +270,15 @@ public class AviExtractor implements Extractor { if (aviHeader == null) { throw new IOException("AviHeader not found"); } + aviTracks = new AviTrack[aviHeader.getStreams()]; //This is usually wrong, so it will be overwritten by video if present - durationUs = aviHeader.getFrames() * (long)aviHeader.getMicroSecPerFrame(); + durationUs = aviHeader.getTotalFrames() * (long)aviHeader.getMicroSecPerFrame(); int streamId = 0; for (Box box : headerList.getChildren()) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { final ListBox streamList = (ListBox) box; - final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); - final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); - if (streamHeader == null) { - Log.w(TAG, "Missing Stream Header"); - continue; - } - if (streamFormat == null) { - Log.w(TAG, "Missing Stream Format"); - continue; - } - final Format.Builder builder = new Format.Builder(); - builder.setId(streamId); - final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); - if (suggestedBufferSize != 0) { - builder.setMaxInputSize(suggestedBufferSize); - } - final StreamNameBox streamName = streamList.getChild(StreamNameBox.class); - if (streamName != null) { - builder.setLabel(streamName.getName()); - } - if (streamHeader.isVideo()) { - final String mimeType = streamHeader.getMimeType(); - if (mimeType == null) { - Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); - continue; - } - final VideoFormat videoFormat = streamFormat.getVideoFormat(); - final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO); - builder.setWidth(videoFormat.getWidth()); - builder.setHeight(videoFormat.getHeight()); - builder.setFrameRate(streamHeader.getFrameRate()); - builder.setSampleMimeType(mimeType); - - final AviTrack aviTrack; - switch (mimeType) { - case MimeTypes.VIDEO_MP4V: - aviTrack = new Mp4vAviTrack(streamId, streamHeader, trackOutput, builder); - break; - case MimeTypes.VIDEO_H264: - aviTrack = new AvcAviTrack(streamId, streamHeader, trackOutput, builder); - break; - default: - aviTrack = new AviTrack(streamId, streamHeader, trackOutput); - } - trackOutput.format(builder.build()); - idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), aviTrack); - durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); - } else if (streamHeader.isAudio()) { - final AudioFormat audioFormat = streamFormat.getAudioFormat(); - final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); - final String mimeType = audioFormat.getMimeType(); - builder.setSampleMimeType(mimeType); - //builder.setCodecs(audioFormat.getCodec()); - builder.setChannelCount(audioFormat.getChannels()); - builder.setSampleRate(audioFormat.getSamplesPerSecond()); - if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) { - final short bps = audioFormat.getBitsPerSample(); - if (bps == 8) { - builder.setPcmEncoding(C.ENCODING_PCM_8BIT); - } else if (bps == 16){ - builder.setPcmEncoding(C.ENCODING_PCM_16BIT); - } - } - if (MimeTypes.AUDIO_AAC.equals(mimeType) && audioFormat.getCbSize() > 0) { - builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); - } - trackOutput.format(builder.build()); - idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24), - new AviTrack(streamId, streamHeader, trackOutput)); - } + parseStream(streamList, streamId); streamId++; } } @@ -323,6 +313,15 @@ public class AviExtractor implements Extractor { return RESULT_SEEK; } + private AviTrack getVideoTrack() { + for (@Nullable AviTrack aviTrack : aviTracks) { + if (aviTrack != null && aviTrack.isVideo()) { + return aviTrack; + } + } + return null; + } + /** * Reads the index and sets the keyFrames and creates the SeekMap * @param input @@ -330,27 +329,20 @@ public class AviExtractor implements Extractor { * @throws IOException */ void readIdx1(ExtractorInput input, int remaining) throws IOException { - final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); - final byte[] bytes = indexByteBuffer.array(); - - final HashMap audioIdFrameMap = new HashMap<>(); - AviTrack videoTrack = null; - //Video seek offsets - UnboundedIntArray videoSeekOffset = new UnboundedIntArray(); - for (int i=0;i= 16) { - final int id = indexByteBuffer.getInt(); - final AviTrack aviTrack = idTrackMap.get(id); + final int chunkId = indexByteBuffer.getInt(); + final AviTrack aviTrack = getAviTrack(chunkId); if (aviTrack == null) { - if (id != AviExtractor.REC_) { - Log.w(TAG, "Unknown Track Type: " + toString(id)); + if (chunkId != AviExtractor.REC_) { + Log.w(TAG, "Unknown Track Type: " + toString(chunkId)); } indexByteBuffer.position(indexByteBuffer.position() + 12); continue; @@ -374,56 +366,80 @@ public class AviExtractor implements Extractor { //int size = indexByteBuffer.getInt(); if (aviTrack.isVideo()) { if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { - keyFrameList.add(aviTrack.frame); + keyFrameList.add(chunkCounts[aviTrack.id]); } - if (aviTrack.frame % seekFrameRate == 0) { - - videoSeekOffset.add(offset); - for (Map.Entry entry : audioIdFrameMap.entrySet()) { - final int audioId = entry.getKey(); - final UnboundedIntArray videoFrameMap = entry.getValue(); - final AviTrack audioTrack = idTrackMap.get(audioId); - videoFrameMap.add(audioTrack.frame); + if (chunkCounts[aviTrack.id] % seekFrameRate == 0) { + seekOffsets[aviTrack.id].add(offset); + for (int i=0;i idFrameArray = new SparseArray<>(); - for (Map.Entry entry : audioIdFrameMap.entrySet()) { - entry.getValue().pack(); - idFrameArray.put(entry.getKey(), entry.getValue().getArray()); - final AviTrack aviTrack = idTrackMap.get(entry.getKey()); - //Sometimes this value is way off - long calcUsPerSample = (getDuration()/aviTrack.frame); - float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample; - if (deltaPercent >.01) { - aviTrack.usPerSample = getDuration()/aviTrack.frame; - Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame)); + for (int i=0;i.01) { + Log.i(TAG, "Updating stream " + i + " calcUsPerSample=" + calcUsPerSample + " reported=" + linearClock.usPerChunk); + linearClock.usPerChunk = calcUsPerSample; + } } } - final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.getArray(), - idFrameArray, moviOffset, getDuration()); + final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekOffsets, seekFrameRate, moviOffset, getDuration()); setSeekMap(seekMap); - resetFrames(); + } + + private static int getStreamId(int chunkId) { + final int upperChar = chunkId & 0xff; + if (Character.isDigit(upperChar)) { + final int lowerChar = (chunkId >> 8) & 0xff; + if (Character.isDigit(upperChar)) { + return (lowerChar & 0xf) + ((upperChar & 0xf) * 10); + } + } + return -1; + } + + @Nullable + private AviTrack getAviTrack(int chunkId) { + final int streamId = getStreamId(chunkId); + if (streamId >= 0) { + return aviTracks[streamId]; + } + return null; + } + + int checkAlign(final ExtractorInput input, PositionHolder seekPosition) { + final long position = input.getPosition(); + if ((position & 1) ==1) { + seekPosition.position = position +1; + return RESULT_SEEK; + } + return RESULT_CONTINUE; } int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { chunkHandler = null; + return checkAlign(input, seekPosition); } } else { ByteBuffer byteBuffer = allocate(8); @@ -437,24 +453,27 @@ public class AviExtractor implements Extractor { return RESULT_END_OF_INPUT; } input.readFully(bytes, 1, 7); - final int id = byteBuffer.getInt(); - final int size = byteBuffer.getInt(); - AviTrack sampleTrack = idTrackMap.get(id); - if (sampleTrack == null) { - if (id == ListBox.LIST) { - seekPosition.position = input.getPosition() + 4; - } else { - seekPosition.position = alignPosition(input.getPosition() + size); - if (id != JUNK) { - Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8) - + " size=" + size + " moviEnd=" + moviEnd); - } - } + final int chunkId = byteBuffer.getInt(); + if (chunkId == ListBox.LIST) { + seekPosition.position = input.getPosition() + 8; return RESULT_SEEK; + } + final int size = byteBuffer.getInt(); + if (chunkId == JUNK) { + seekPosition.position = alignPosition(input.getPosition() + size); + return RESULT_SEEK; + } + final AviTrack aviTrack = getAviTrack(chunkId); + if (aviTrack == null) { + seekPosition.position = alignPosition(input.getPosition() + size); + Log.w(TAG, "Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + + " size=" + size + " moviEnd=" + moviEnd); + return RESULT_SEEK; + } + if (aviTrack.newChunk(chunkId, size, input)) { + return checkAlign(input, seekPosition); } else { - if (!sampleTrack.newChunk(id, size, input)) { - chunkHandler = sampleTrack; - } + chunkHandler = aviTrack; } } return RESULT_CONTINUE; @@ -489,7 +508,6 @@ public class AviExtractor implements Extractor { state = STATE_READ_SAMPLES; return RESULT_SEEK; } - } return RESULT_CONTINUE; } @@ -499,24 +517,34 @@ public class AviExtractor implements Extractor { chunkHandler = null; if (position <= 0) { if (moviOffset != 0) { - resetFrames(); + resetClocks(); state = STATE_SEEK_START; } } else { if (aviSeekMap != null) { - aviSeekMap.setFrames(position, timeUs, idTrackMap); + aviSeekMap.setFrames(position, timeUs, aviTracks); } } } - void resetFrames() { - for (int i=0;i audioIdMap; + final long moviOffset; final long duration; - public AviSeekMap(AviTrack videoTrack, int seekIndexFactor, int[] videoFrameOffsetMap, - SparseArray audioIdMap, long moviOffset, long duration) { - this.videoTrack = videoTrack; + public AviSeekMap(AviTrack videoTrack, UnboundedIntArray[] seekOffsets, int seekIndexFactor, long moviOffset, long duration) { + videoUsPerChunk = videoTrack.getClock().usPerChunk; + videoStreamId = videoTrack.id; this.seekIndexFactor = seekIndexFactor; - this.videoFrameOffsetMap = videoFrameOffsetMap; - this.audioIdMap = audioIdMap; this.moviOffset = moviOffset; this.duration = duration; + this.seekOffsets = new int[seekOffsets.length][]; + for (int i=0;i= videoFrameOffsetMap.length) { - reqFrameIndex = videoFrameOffsetMap.length - 1; + if (reqFrameIndex >= seekOffsets[videoStreamId].length) { + reqFrameIndex = seekOffsets[videoStreamId].length - 1; } return reqFrameIndex; } @@ -53,23 +55,29 @@ public class AviSeekMap implements SeekMap { @Override public SeekPoints getSeekPoints(long timeUs) { final int seekFrameIndex = getSeekFrameIndex(timeUs); - int offset = videoFrameOffsetMap[seekFrameIndex]; - final long outUs = seekFrameIndex * seekIndexFactor * videoTrack.usPerSample; + int offset = seekOffsets[videoStreamId][seekFrameIndex]; + final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk; final long position = offset + moviOffset; Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); return new SeekPoints(new SeekPoint(outUs, position)); } - public void setFrames(final long position, final long timeUs, final SparseArray idTrackMap) { + public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) { final int seekFrameIndex = getSeekFrameIndex(timeUs); - videoTrack.seekFrame(seekFrameIndex * seekIndexFactor); - for (int i=0;i= 0; + return Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0; } //Hack: Exo needs at least one frame before it starts playback - return frame == 0; + //return clock.getIndex() == 0; + return false; + } + + public void setForceKeyFrame(boolean v) { + forceKeyFrame = v; } public void setKeyFrames(int[] keyFrames) { this.keyFrames = keyFrames; } - public long getUs() { - return getUs(getUsFrame()); - } - - public long getUs(final int myFrame) { - return myFrame * usPerSample; - } - public boolean isVideo() { return streamHeaderBox.isVideo(); } @@ -87,23 +99,10 @@ public class AviTrack { return streamHeaderBox.isAudio(); } - public void advance() { - frame++; - } - - /** - * Get the frame number used to calculate the timeUs - * @return - */ - int getUsFrame() { - return frame; - } - - void seekFrame(int frame) { - this.frame = frame; - } - public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { + if (chunkPeeker != null) { + chunkPeeker.peek(input, size); + } final int remaining = size - trackOutput.sampleData(input, size, false); if (remaining == 0) { done(size); @@ -127,8 +126,8 @@ public class AviTrack { void done(final int size) { trackOutput.sampleMetadata( - getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); + clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame()); - advance(); + clock.advance(); } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java new file mode 100644 index 0000000000..84eb49e459 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java @@ -0,0 +1,8 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.IOException; + +public interface ChunkPeeker { + void peek(ExtractorInput input, final int size) throws IOException; +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java new file mode 100644 index 0000000000..b313e501e5 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java @@ -0,0 +1,27 @@ +package com.google.android.exoplayer2.extractor.avi; + +public class LinearClock { + long usPerChunk; + + int index; + + public LinearClock(long usPerChunk) { + this.usPerChunk = usPerChunk; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public void advance() { + index++; + } + + public long getUs() { + return index * usPerChunk; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java deleted file mode 100644 index d5a02962c8..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.google.android.exoplayer2.extractor.avi; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; -import java.io.IOException; - -public class Mp4vAviTrack extends AviTrack { - private static final byte SEQUENCE_START_CODE = (byte)0xb0; - private static final int LAYER_START_CODE = 0x20; - private static final float[] ASPECT_RATIO = {0f, 1f, 12f/11f, 10f/11f, 16f/11f, 40f/33f}; - private static final int Extended_PAR = 0xf; - private final Format.Builder formatBuilder; - float pixelWidthHeightRatio = 1f; - - Mp4vAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput, - @NonNull Format.Builder formatBuilder) { - super(id, streamHeaderBox, trackOutput); - this.formatBuilder = formatBuilder; - } - - @VisibleForTesting - void processLayerStart(@NonNull final ParsableNalUnitBitArray in) { - in.skipBit(); // random_accessible_vol - in.skipBits(8); // video_object_type_indication - boolean is_object_layer_identifier = in.readBit(); - if (is_object_layer_identifier) { - in.skipBits(7); // video_object_layer_verid, video_object_layer_priority - } - int aspect_ratio_info = in.readBits(4); - final float aspectRatio; - if (aspect_ratio_info == Extended_PAR) { - float par_width = (float)in.readBits(8); - float par_height = (float)in.readBits(8); - aspectRatio = par_width / par_height; - } else { - aspectRatio = ASPECT_RATIO[aspect_ratio_info]; - } - if (aspectRatio != pixelWidthHeightRatio) { - trackOutput.format(formatBuilder.setPixelWidthHeightRatio(aspectRatio).build()); - pixelWidthHeightRatio = aspectRatio; - } - } - - @VisibleForTesting - @Nullable - static ParsableNalUnitBitArray findLayerStart(ExtractorInput input, final int peekSize) - throws IOException { - byte[] peek = new byte[peekSize]; - input.peekFully(peek, 0, peekSize); - for (int i = 4;i 0) { + if (buffer.length - pos < SEEK_PEEK_SIZE && remaining > 0) { + append(input, Math.min(SEEK_PEEK_SIZE, remaining)); + } + final int nalOffset = seekNal(); + if (nalOffset > 0) { + return nalOffset; + } + } + pos = buffer.length; + return -1; + } + + public NalChunkPeeker(int peakSize) { + if (peakSize < 5) { + throw new IllegalArgumentException("Peak size must at least be 5"); + } + this.peekSize = peakSize; + } + + abstract boolean skip(byte nalType); + + public void peek(ExtractorInput input, final int size) throws IOException { + buffer = new byte[peekSize]; + if (!input.peekFully(buffer, 0, peekSize, true)) { + return; + } + pos = 0; + int nalTypeOffset = getNalTypeOffset(); + if (nalTypeOffset < 0 || skip(buffer[nalTypeOffset])) { + input.resetPeekPosition(); + return; + } + remaining = size - peekSize; + processChunk(input, nalTypeOffset); + input.resetPeekPosition(); + } + +// @VisibleForTesting(otherwise = VisibleForTesting.NONE) +// void setBuffer(byte[] buffer) { +// this.buffer = buffer; +// } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java new file mode 100644 index 0000000000..cc7017bc85 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -0,0 +1,62 @@ +package com.google.android.exoplayer2.extractor.avi; + +/** + * Properly calculates the frame time for H264 frames using PicCount + */ +public class PicCountClock extends LinearClock { + //The frame as a calculated from the picCount + private int picIndex; + private int lastPicCount; + //Largest picFrame, used when we hit an I frame + private int maxPicIndex =-1; + private int maxPicCount; + private int posHalf; + private int negHalf; + + public PicCountClock(long usPerFrame) { + super(usPerFrame); + } + + public void setMaxPicCount(int maxPicCount) { + this.maxPicCount = maxPicCount; + posHalf = maxPicCount / 2; //Not sure why pics are 2x + negHalf = -posHalf; + } + + /** + * Done on seek. May cause sync issues if frame picCount != 0 (I frames are always 0) + * @param index + */ + @Override + public void setIndex(int index) { + super.setIndex(index); + syncIndexes(); + } + + public void setPicCount(int picCount) { + int delta = picCount - lastPicCount; + if (delta < negHalf) { + delta += maxPicCount; + } else if (delta > posHalf) { + delta -= maxPicCount; + } + picIndex += delta / 2; + lastPicCount = picCount; + if (maxPicIndex < picIndex) { + maxPicIndex = picIndex; + } + } + + /** + * Handle key frame + */ + public void syncIndexes() { + lastPicCount = 0; + maxPicIndex = picIndex = getIndex(); + } + + @Override + public long getUs() { + return picIndex * usPerChunk; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 703bb9aa24..8e4ce29d4a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -36,6 +36,7 @@ public class StreamHeaderBox extends ResidentBox { STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); STREAM_MAP.put(XVID, mimeType); STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); + STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java new file mode 100644 index 0000000000..25afad0039 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -0,0 +1,101 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class AviExtractorTest { + @Test + public void init_givenFakeExtractorOutput() { + AviExtractor aviExtractor = new AviExtractor(); + FakeExtractorOutput output = new FakeExtractorOutput(); + aviExtractor.init(output); + + Assert.assertEquals(AviExtractor.STATE_READ_TRACKS, aviExtractor.state); + Assert.assertEquals(output, aviExtractor.output); + } + + + private boolean sniff(ByteBuffer byteBuffer) { + AviExtractor aviExtractor = new AviExtractor(); + FakeExtractorInput input = new FakeExtractorInput.Builder() + .setData(byteBuffer.array()).build(); + try { + return aviExtractor.sniff(input); + } catch (IOException e) { + Assert.fail(e.getMessage()); + return false; + } + } + + @Test + public void peek_givenTooFewByte() { + Assert.assertFalse(sniff(AviExtractor.allocate(AviExtractor.PEEK_BYTES - 1))); + } + + @Test + public void peek_givenAllZero() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiff() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_List() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + byteBuffer.putInt(ListBox.LIST); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_ListHdrl() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(64); + byteBuffer.putInt(ListBox.TYPE_HDRL); + Assert.assertFalse(sniff(byteBuffer)); + } + + @Test + public void peek_givenOnlyRiffAvi_ListHdrlAvih() { + ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(64); + byteBuffer.putInt(ListBox.TYPE_HDRL); + byteBuffer.putInt(AviHeaderBox.AVIH); + Assert.assertTrue(sniff(byteBuffer)); + } + + @Test + public void toString_givenKnownString() { + final int riff = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); + Assert.assertEquals("RIFF", AviExtractor.toString(riff)); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java new file mode 100644 index 0000000000..4775d93872 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java @@ -0,0 +1,46 @@ +package com.google.android.exoplayer2.extractor.avi; + +import java.nio.BufferOverflowException; + +public class BitBuffer { + private long work; + int bits; + + public void push(boolean b) { + grow(1); + if (b) { + work |= 1L; + } + } + + void grow(int bits) { + if (this.bits + bits > 64) { + throw new BufferOverflowException(); + } + this.bits += bits; + work <<= bits; + } + + public void push(int bits, int value) { + int mask = (1 << bits) - 1; + if ((value & mask) != value) { + throw new IllegalArgumentException("Expected only " + bits + " bits, got " + value); + } + grow(bits); + work |= (value & 0xffffffffL); + } + + public byte[] getBytes() { + //Byte align + grow(8 - bits % 8); + final int count = bits / 8; + final byte[] bytes = new byte[count]; + for (int i=count -1; i >= 0;i--) { + bytes[i] = (byte)(work & 0xff); + work >>=8; + } + work = 0L; + bits = 0; + return bytes; + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 2731878d90..246220ed0d 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -50,4 +50,12 @@ public class DataHelper { bytes = Arrays.copyOf(bytes, bytes.length + 1); return new StreamNameBox(StreamNameBox.STRN, bytes.length, ByteBuffer.wrap(bytes)); } + + public static ByteBuffer appendNal(final ByteBuffer byteBuffer, byte nalType) { + byteBuffer.put((byte)0); + byteBuffer.put((byte)0); + byteBuffer.put((byte) 1); + byteBuffer.put(nalType); + return byteBuffer; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java new file mode 100644 index 0000000000..0be4368b5c --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java @@ -0,0 +1,22 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.IOException; + +public class MockNalChunkPeeker extends NalChunkPeeker { + private boolean skip; + public MockNalChunkPeeker(int peakSize, boolean skip) { + super(peakSize); + this.skip = skip; + } + + @Override + void processChunk(ExtractorInput input, int nalTypeOffset) throws IOException { + + } + + @Override + boolean skip(byte nalType) { + return skip; + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java deleted file mode 100644 index c14352f4aa..0000000000 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrackTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.google.android.exoplayer2.extractor.avi; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.FakeTrackOutput; -import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; -import java.io.IOException; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class Mp4vAviTrackTest { - - @Test - public void isSequenceStart_givenSequence() throws IOException { - final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); - Assert.assertTrue(Mp4vAviTrack.isSequenceStart(input)); - } - - @Test - public void findLayerStart_givenSequence() throws IOException { - final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); - final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input, - (int)input.getLength()); - //Offset 0x12 - Assert.assertEquals(8, bitArray.readBits(8)); - } - - @Test - public void findLayerStart_givenAllZeros() throws IOException { - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). - setData(new byte[128]).build(); - Assert.assertNull(Mp4vAviTrack.findLayerStart(fakeExtractorInput, 128)); - } - - @Test - public void pixelWidthHeightRatio_givenSequence() throws IOException { - final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); - final Format.Builder formatBuilder = new Format.Builder(); - final Mp4vAviTrack mp4vAviTrack = new Mp4vAviTrack(0, DataHelper.getVidsStreamHeader(), - fakeTrackOutput, formatBuilder); - final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); - mp4vAviTrack.newChunk(0, (int)input.getLength(), input); -// final ParsableNalUnitBitArray bitArray = Mp4vAviTrack.findLayerStart(input, -// (int)input.getLength()); -// mp4vAviTrack.processLayerStart(bitArray); - Assert.assertEquals(mp4vAviTrack.pixelWidthHeightRatio, 1.2121212, 0.01); - } -} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java new file mode 100644 index 0000000000..baa6277ad1 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java @@ -0,0 +1,65 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class Mp4vChunkPeekerTest { + + private ByteBuffer makeSequence() { + return DataHelper.appendNal(AviExtractor.allocate(32),Mp4vChunkPeeker.SEQUENCE_START_CODE); + } + + @Test + public void peek_givenNoSequence() throws IOException { + ByteBuffer byteBuffer = makeSequence(); + final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); + final Format.Builder formatBuilder = new Format.Builder(); + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + .build(); + final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput); + mp4vChunkPeeker.peek(input, (int) input.getLength()); + Assert.assertEquals(1f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01); + } + + @Test + public void peek_givenAspectRatio() throws IOException { + final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); + final Format.Builder formatBuilder = new Format.Builder(); + final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput); + final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); + + mp4vChunkPeeker.peek(input, (int) input.getLength()); + Assert.assertEquals(1.2121212, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01); + } + + @Test + public void peek_givenCustomAspectRatio() throws IOException { + ByteBuffer byteBuffer = makeSequence(); + byteBuffer.putInt(0x5555); + DataHelper.appendNal(byteBuffer, (byte)Mp4vChunkPeeker.LAYER_START_CODE); + + BitBuffer bitBuffer = new BitBuffer(); + bitBuffer.push(false); //random_accessible_vol + bitBuffer.push(8, 8); //video_object_type_indication + bitBuffer.push(true); // is_object_layer_identifier + bitBuffer.push(7, 7); // video_object_layer_verid, video_object_layer_priority + bitBuffer.push(4, Mp4vChunkPeeker.Extended_PAR); + bitBuffer.push(8, 16); + bitBuffer.push(8, 9); + final byte bytes[] = bitBuffer.getBytes(); + byteBuffer.put(bytes); + + final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); + final Format.Builder formatBuilder = new Format.Builder(); + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + .build(); + final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput); + mp4vChunkPeeker.peek(input, (int) input.getLength()); + Assert.assertEquals(16f/9f, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01); + } +} \ No newline at end of file diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java new file mode 100644 index 0000000000..2708015f4f --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java @@ -0,0 +1,55 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class NalChunkPeekerTest { + @Test + public void construct_givenTooSmallPeekSize() { + try { + new MockNalChunkPeeker(4, false); + Assert.fail(); + } catch (IllegalArgumentException e) { + //Intentionally blank + } + } + + @Test + public void peek_givenNoData() { + final FakeExtractorInput input = new FakeExtractorInput.Builder().build(); + final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false); + try { + peeker.peek(input, 10); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } + @Test + public void peek_givenNoNal() { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[10]).build(); + final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false); + try { + peeker.peek(input, 10); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } + @Test + public void peek_givenAlwaysSkip() { + final ByteBuffer byteBuffer = AviExtractor.allocate(10); + DataHelper.appendNal(byteBuffer, (byte)32); + + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build(); + final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, true); + try { + peeker.peek(input, 10); + Assert.assertEquals(0, input.getPeekPosition()); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } +} From f1d007e68cd3e18abcb547239d23b65a75381e88 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 25 Jan 2022 15:01:02 -0700 Subject: [PATCH 24/70] Fix issue where reading mime type wrong in video. More tests --- .../exoplayer2/extractor/avi/AudioFormat.java | 14 ++- .../extractor/avi/AvcChunkPeeker.java | 10 +- .../extractor/avi/AviExtractor.java | 99 ++++++++++-------- .../exoplayer2/extractor/avi/AviSeekMap.java | 3 +- .../exoplayer2/extractor/avi/AviTrack.java | 49 +++++---- .../extractor/avi/IStreamFormat.java | 9 ++ .../extractor/avi/PicCountClock.java | 6 +- .../extractor/avi/StreamHeaderBox.java | 32 ------ .../exoplayer2/extractor/avi/VideoFormat.java | 50 ++++++++- .../extractor/avi/AudioFormatTest.java | 3 +- .../extractor/avi/AviExtractorRoboTest.java | 38 +++++++ .../extractor/avi/AviExtractorTest.java | 50 +++++++++ .../exoplayer2/extractor/avi/DataHelper.java | 45 +++++++- .../extractor/avi/LinearClockTest.java | 18 ++++ .../extractor/avi/PicCountClockTest.java | 45 ++++++++ .../extractor/avi/StreamHeaderBoxTest.java | 3 +- .../avi/auds_stream_header.dump | Bin 0 -> 56 bytes 17 files changed, 364 insertions(+), 110 deletions(-) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java create mode 100644 testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump 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 index 0a8b03ce1f..65bc631340 100644 --- 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 @@ -1,10 +1,11 @@ package com.google.android.exoplayer2.extractor.avi; import android.util.SparseArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; -public class AudioFormat { +public class AudioFormat implements IStreamFormat { public static final short WAVE_FORMAT_PCM = 1; static final short WAVE_FORMAT_AAC = 0xff; private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; @@ -60,5 +61,16 @@ public class AudioFormat { temp.get(data); return data; } + + @Override + public boolean isAllKeyFrames() { + return true; + } + + @Override + public @C.TrackType int getTrackType() { + return C.TRACK_TYPE_AUDIO; + } + //TODO: Deal with WAVEFORMATEXTENSIBLE } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java index 67620785e5..bd2544bb97 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java @@ -1,5 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -37,6 +38,12 @@ public class AvcChunkPeeker extends NalChunkPeeker { return false; } + /** + * Greatly simplified way to calculate the picOrder + * Full logic is here + * https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/video/h264_poc.cc + * @param nalTypeOffset + */ void updatePicCountClock(final int nalTypeOffset) { final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length); //slide_header() @@ -63,7 +70,8 @@ public class AvcChunkPeeker extends NalChunkPeeker { picCountClock.setIndex(picCountClock.getIndex()); } - private int readSps(ExtractorInput input, int nalTypeOffset) throws IOException { + @VisibleForTesting + int readSps(ExtractorInput input, int nalTypeOffset) throws IOException { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); 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 index 18fc767061..411b6f8d99 100644 --- 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 @@ -39,6 +39,21 @@ public class AviExtractor implements Extractor { return sb.toString(); } + static long alignPosition(long position) { + if ((position & 1) == 1) { + position++; + } + return position; + } + + static void alignInput(ExtractorInput input) throws IOException { + // This isn't documented anywhere, but most files are aligned to even bytes + // and can have gaps of zeros + if ((input.getPosition() & 1) == 1) { + input.skipFully(1); + } + } + static final String TAG = "AviExtractor"; @VisibleForTesting static final int PEEK_BYTES = 28; @@ -81,28 +96,14 @@ public class AviExtractor implements Extractor { //At the start of the movi tag private long moviOffset; private long moviEnd; - private AviSeekMap aviSeekMap; + @VisibleForTesting + AviSeekMap aviSeekMap; // private long indexOffset; //Usually chunkStart //If partial read private transient AviTrack chunkHandler; - static void alignInput(ExtractorInput input) throws IOException { - // This isn't documented anywhere, but most files are aligned to even bytes - // and can have gaps of zeros - if ((input.getPosition() & 1) == 1) { - input.skipFully(1); - } - } - - static long alignPosition(long position) { - if ((position & 1) == 1) { - position++; - } - return position; - } - /** * * @param input @@ -161,7 +162,20 @@ public class AviExtractor implements Extractor { return byteBuffer; } - private void setSeekMap(AviSeekMap aviSeekMap) { + @VisibleForTesting + static int getStreamId(int chunkId) { + final int upperChar = chunkId & 0xff; + if (Character.isDigit(upperChar)) { + final int lowerChar = (chunkId >> 8) & 0xff; + if (Character.isDigit(upperChar)) { + return (lowerChar & 0xf) + ((upperChar & 0xf) * 10); + } + } + return -1; + } + + @VisibleForTesting + void setSeekMap(AviSeekMap aviSeekMap) { this.aviSeekMap = aviSeekMap; output.seekMap(aviSeekMap); } @@ -191,16 +205,17 @@ public class AviExtractor implements Extractor { this.output = output; } - private void parseStream(final ListBox streamList, int streamId) { + @VisibleForTesting + AviTrack parseStream(final ListBox streamList, int streamId) { final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); if (streamHeader == null) { Log.w(TAG, "Missing Stream Header"); - return; + return null; } if (streamFormat == null) { Log.w(TAG, "Missing Stream Format"); - return; + return null; } final Format.Builder builder = new Format.Builder(); builder.setId(streamId); @@ -212,31 +227,33 @@ public class AviExtractor implements Extractor { if (streamName != null) { builder.setLabel(streamName.getName()); } + final AviTrack aviTrack; if (streamHeader.isVideo()) { - final String mimeType = streamHeader.getMimeType(); + final VideoFormat videoFormat = streamFormat.getVideoFormat(); + final String mimeType = videoFormat.getMimeType(); if (mimeType == null) { Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); - return; + return null; } - final VideoFormat videoFormat = streamFormat.getVideoFormat(); final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO); builder.setWidth(videoFormat.getWidth()); builder.setHeight(videoFormat.getHeight()); builder.setFrameRate(streamHeader.getFrameRate()); builder.setSampleMimeType(mimeType); - final AviTrack aviTrack = new AviTrack(streamId, streamHeader, trackOutput); - if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { - Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(builder, trackOutput); - aviTrack.setChunkPeeker(mp4vChunkPeeker); - } else if (MimeTypes.VIDEO_H264.equals(mimeType)) { + if (MimeTypes.VIDEO_H264.equals(mimeType)) { final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample()); - aviTrack.setClock(avcChunkPeeker.getPicCountClock()); + aviTrack = new AviTrack(streamId, videoFormat, avcChunkPeeker.getPicCountClock(), trackOutput); aviTrack.setChunkPeeker(avcChunkPeeker); + } else { + aviTrack = new AviTrack(streamId, videoFormat, + new LinearClock(streamHeader.getUsPerSample()), trackOutput); + if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { + aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput)); + } } trackOutput.format(builder.build()); durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); - aviTracks[streamId] = aviTrack; } else if (streamHeader.isAudio()) { final AudioFormat audioFormat = streamFormat.getAudioFormat(); final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); @@ -257,8 +274,12 @@ public class AviExtractor implements Extractor { builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); } trackOutput.format(builder.build()); - aviTracks[streamId] = new AviTrack(streamId, streamHeader, trackOutput); + aviTrack = new AviTrack(streamId, audioFormat, new LinearClock(streamHeader.getUsPerSample()), + trackOutput); + }else { + aviTrack = null; } + return aviTrack; } private int readTracks(ExtractorInput input) throws IOException { @@ -278,7 +299,7 @@ public class AviExtractor implements Extractor { for (Box box : headerList.getChildren()) { if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { final ListBox streamList = (ListBox) box; - parseStream(streamList, streamId); + aviTracks[streamId] = parseStream(streamList, streamId); streamId++; } } @@ -343,7 +364,8 @@ public class AviExtractor implements Extractor { for (int i=0;i 0) { @@ -406,17 +428,6 @@ public class AviExtractor implements Extractor { setSeekMap(seekMap); } - private static int getStreamId(int chunkId) { - final int upperChar = chunkId & 0xff; - if (Character.isDigit(upperChar)) { - final int lowerChar = (chunkId >> 8) & 0xff; - if (Character.isDigit(upperChar)) { - return (lowerChar & 0xf) + ((upperChar & 0xf) * 10); - } - } - return -1; - } - @Nullable private AviTrack getAviTrack(int chunkId) { final int streamId = getStreamId(chunkId); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index edb6324a61..e228756848 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -3,7 +3,6 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.Log; public class AviSeekMap implements SeekMap { final long videoUsPerChunk; @@ -58,7 +57,7 @@ public class AviSeekMap implements SeekMap { int offset = seekOffsets[videoStreamId][seekFrameIndex]; final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk; final long position = offset + moviOffset; - Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); + //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); return new SeekPoints(new SeekPoint(outUs, position)); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index 61168935b7..cbad3b1df2 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -5,7 +5,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.Arrays; @@ -16,23 +15,22 @@ public class AviTrack { final int id; @NonNull - final StreamHeaderBox streamHeaderBox; + final LinearClock clock; - @NonNull - LinearClock clock; - - @Nullable - ChunkPeeker chunkPeeker; /** * True indicates all frames are key frames (e.g. Audio, MJPEG) */ - boolean allKeyFrames; + final boolean allKeyFrames; + final @C.TrackType int trackType; + + @NonNull + final TrackOutput trackOutput; boolean forceKeyFrame; - @NonNull - TrackOutput trackOutput; + @Nullable + ChunkPeeker chunkPeeker; /** * Key is frame number value is offset @@ -43,22 +41,19 @@ public class AviTrack { transient int chunkSize; transient int chunkRemaining; - AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) { + AviTrack(int id, @NonNull IStreamFormat streamFormat, @NonNull LinearClock clock, + @NonNull TrackOutput trackOutput) { this.id = id; + this.clock = clock; + this.allKeyFrames = streamFormat.isAllKeyFrames(); + this.trackType = streamFormat.getTrackType(); this.trackOutput = trackOutput; - this.streamHeaderBox = streamHeaderBox; - clock = new LinearClock(streamHeaderBox.getUsPerSample()); - this.allKeyFrames = streamHeaderBox.isAudio() || (MimeTypes.VIDEO_MJPEG.equals(streamHeaderBox.getMimeType())); } public LinearClock getClock() { return clock; } - public void setClock(LinearClock clock) { - this.clock = clock; - } - public void setChunkPeeker(ChunkPeeker chunkPeeker) { this.chunkPeeker = chunkPeeker; } @@ -78,8 +73,6 @@ public class AviTrack { if (keyFrames != null) { return Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0; } - //Hack: Exo needs at least one frame before it starts playback - //return clock.getIndex() == 0; return false; } @@ -92,11 +85,11 @@ public class AviTrack { } public boolean isVideo() { - return streamHeaderBox.isVideo(); + return trackType == C.TRACK_TYPE_VIDEO; } public boolean isAudio() { - return streamHeaderBox.isAudio(); + return trackType == C.TRACK_TYPE_AUDIO; } public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException { @@ -114,7 +107,13 @@ public class AviTrack { } } - public boolean resume(ExtractorInput input) throws IOException { + /** + * Resume a partial read of a chunk + * @param input + * @return + * @throws IOException + */ + boolean resume(ExtractorInput input) throws IOException { chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false); if (chunkRemaining == 0) { done(chunkSize); @@ -124,6 +123,10 @@ public class AviTrack { } } + /** + * Done reading a chunk + * @param size + */ void done(final int size) { trackOutput.sampleMetadata( clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java new file mode 100644 index 0000000000..0211b197dc --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java @@ -0,0 +1,9 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.C; + +public interface IStreamFormat { + String getMimeType(); + boolean isAllKeyFrames(); + @C.TrackType int getTrackType(); +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java index cc7017bc85..7f76f00296 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -4,6 +4,8 @@ package com.google.android.exoplayer2.extractor.avi; * Properly calculates the frame time for H264 frames using PicCount */ public class PicCountClock extends LinearClock { + //I believe this is 2 because there is a bottom pic order and a top pic order + private static final int STEP = 2; //The frame as a calculated from the picCount private int picIndex; private int lastPicCount; @@ -19,7 +21,7 @@ public class PicCountClock extends LinearClock { public void setMaxPicCount(int maxPicCount) { this.maxPicCount = maxPicCount; - posHalf = maxPicCount / 2; //Not sure why pics are 2x + posHalf = maxPicCount / STEP; negHalf = -posHalf; } @@ -40,7 +42,7 @@ public class PicCountClock extends LinearClock { } else if (delta > posHalf) { delta -= maxPicCount; } - picIndex += delta / 2; + picIndex += delta / STEP; lastPicCount = picCount; if (maxPicIndex < picIndex) { maxPicIndex = picIndex; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 8e4ce29d4a..6cfbb61fb1 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -1,7 +1,5 @@ package com.google.android.exoplayer2.extractor.avi; -import android.util.SparseArray; -import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; /** @@ -16,31 +14,6 @@ public class StreamHeaderBox extends ResidentBox { //Videos Stream static final int VIDS = 'v' | ('i' << 8) | ('d' << 16) | ('s' << 24); - static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); - - private static final SparseArray STREAM_MAP = new SparseArray<>(); - - static { - //Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG - final String mimeType = MimeTypes.VIDEO_MP4V; - //final String mimeType = MimeTypes.VIDEO_H263; - - //I've never seen an Android devices that actually supports MP42 - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42"); - //Samsung seems to support the rare MP43. - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43"); - STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); - STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); - STREAM_MAP.put(XVID, mimeType); - STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); - STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); - - STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); - } - StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); } @@ -64,11 +37,6 @@ public class StreamHeaderBox extends ResidentBox { return getScale() * 1_000_000L / getRate(); } - public String getMimeType() { - return STREAM_MAP.get(getFourCC()); - } - - public int getSteamType() { return byteBuffer.getInt(0); } 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 index 55b1e67b0b..f849eed932 100644 --- 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 @@ -1,8 +1,38 @@ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; +import java.util.HashMap; + +public class VideoFormat implements IStreamFormat { + + static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); + + private static final HashMap STREAM_MAP = new HashMap<>(); + + static { + //Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG + final String mimeType = MimeTypes.VIDEO_MP4V; + //final String mimeType = MimeTypes.VIDEO_H263; + + //I've never seen an Android devices that actually supports MP42 + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42"); + //Samsung seems to support the rare MP43. + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43"); + STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); + STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); + STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); + STREAM_MAP.put(XVID, mimeType); + STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); + STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); + + STREAM_MAP.put('M' | ('J' << 8) | ('P' << 16) | ('G' << 24), MimeTypes.VIDEO_MJPEG); + STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); + } -public class VideoFormat { private final ByteBuffer byteBuffer; public VideoFormat(final ByteBuffer byteBuffer) { @@ -17,5 +47,23 @@ public class VideoFormat { public int getHeight() { return byteBuffer.getInt(8); } + // 12 - biPlanes + // 14 - biBitCount + public int getCompression() { + return byteBuffer.getInt(16); + } + public String getMimeType() { + return STREAM_MAP.get(getCompression()); + } + + @Override + public boolean isAllKeyFrames() { + return MimeTypes.VIDEO_MJPEG.equals(getMimeType()); + } + + @Override + public int getTrackType() { + return C.TRACK_TYPE_VIDEO; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java index 63cfd5c108..cc1860749c 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java @@ -13,7 +13,7 @@ public class AudioFormatTest { @Test public void getters_givenAacStreamFormat() throws IOException { - final StreamFormatBox streamFormatBox = DataHelper.getAudioStreamFormat(); + final StreamFormatBox streamFormatBox = DataHelper.getAacStreamFormat(); final AudioFormat audioFormat = streamFormatBox.getAudioFormat(); Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType()); Assert.assertEquals(2, audioFormat.getChannels()); @@ -21,5 +21,6 @@ public class AudioFormatTest { Assert.assertEquals(48000, audioFormat.getSamplesPerSecond()); Assert.assertEquals(0, audioFormat.getBitsPerSample()); //Not meaningful for AAC Assert.assertArrayEquals(CODEC_PRIVATE, audioFormat.getCodecData()); + Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType()); } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java new file mode 100644 index 0000000000..315e504d7d --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -0,0 +1,38 @@ +package com.google.android.exoplayer2.extractor.avi; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class AviExtractorRoboTest { + + @Test + public void parseStream_givenH264StreamList() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = DataHelper.getVideoStreamList(); + aviExtractor.parseStream(streamList, 0); + FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); + Assert.assertEquals(MimeTypes.VIDEO_H264, trackOutput.lastFormat.sampleMimeType); + } + + @Test + public void parseStream_givenAacStreamList() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = DataHelper.getAacStreamList(); + aviExtractor.parseStream(streamList, 0); + FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); + Assert.assertEquals(MimeTypes.AUDIO_AAC, trackOutput.lastFormat.sampleMimeType); + } + +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 25afad0039..f9325cc1de 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -98,4 +98,54 @@ public class AviExtractorTest { final int riff = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); Assert.assertEquals("RIFF", AviExtractor.toString(riff)); } + + @Test + public void alignPosition_givenOddPosition() { + Assert.assertEquals(2, AviExtractor.alignPosition(1)); + } + + @Test + public void alignPosition_givenEvenPosition() { + Assert.assertEquals(2, AviExtractor.alignPosition(2)); + } + + @Test + public void alignInput_givenOddPosition() throws IOException { + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[16]).build(); + fakeExtractorInput.setPosition(1); + AviExtractor.alignInput(fakeExtractorInput); + Assert.assertEquals(2, fakeExtractorInput.getPosition()); + } + @Test + + public void alignInput_givenEvenPosition() throws IOException { + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[16]).build(); + fakeExtractorInput.setPosition(4); + AviExtractor.alignInput(fakeExtractorInput); + Assert.assertEquals(4, fakeExtractorInput.getPosition()); + } + + @Test + public void setSeekMap_givenStubbedSeekMap() throws IOException { + final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + aviExtractor.setSeekMap(aviSeekMap); + Assert.assertEquals(aviSeekMap, fakeExtractorOutput.seekMap); + Assert.assertEquals(aviSeekMap, aviExtractor.aviSeekMap); + } + + @Test + public void getStreamId_givenInvalidStreamId() { + Assert.assertEquals(-1, AviExtractor.getStreamId(AviExtractor.JUNK)); + } + + @Test + public void getStreamId_givenValidStreamId() { + Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24))); + } + } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 246220ed0d..21c087fd29 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -1,11 +1,13 @@ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.Arrays; public class DataHelper { @@ -31,7 +33,14 @@ public class DataHelper { return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); } - public static StreamFormatBox getAudioStreamFormat() throws IOException { + public static StreamHeaderBox getAudioStreamHeader() throws IOException { + final byte[] buffer = getBytes("auds_stream_header.dump"); + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); + } + + public static StreamFormatBox getAacStreamFormat() throws IOException { final byte[] buffer = getBytes("aac_stream_format.dump"); final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); @@ -45,6 +54,26 @@ public class DataHelper { return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer); } + public static ListBox getVideoStreamList() throws IOException { + final StreamHeaderBox streamHeaderBox = getVidsStreamHeader(); + final StreamFormatBox streamFormatBox = getVideoStreamFormat(); + final ArrayList list = new ArrayList<>(2); + list.add(streamHeaderBox); + list.add(streamFormatBox); + return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()), + AviExtractor.STRL, list); + } + + public static ListBox getAacStreamList() throws IOException { + final StreamHeaderBox streamHeaderBox = getAudioStreamHeader(); + final StreamFormatBox streamFormatBox = getAacStreamFormat(); + final ArrayList list = new ArrayList<>(2); + list.add(streamHeaderBox); + list.add(streamFormatBox); + return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()), + AviExtractor.STRL, list); + } + public static StreamNameBox getStreamNameBox(final String name) { byte[] bytes = name.getBytes(); bytes = Arrays.copyOf(bytes, bytes.length + 1); @@ -58,4 +87,18 @@ public class DataHelper { byteBuffer.put(nalType); return byteBuffer; } + public static AviSeekMap getAviSeekMap() throws IOException { + + final FakeTrackOutput output = new FakeTrackOutput(false); + final AviTrack videoTrack = new AviTrack(0, + DataHelper.getVideoStreamFormat().getVideoFormat(), new LinearClock(100), output); + final UnboundedIntArray videoArray = new UnboundedIntArray(); + videoArray.add(0); + videoArray.add(1024); + final UnboundedIntArray audioArray = new UnboundedIntArray(); + audioArray.add(0); + audioArray.add(128); + return new AviSeekMap(videoTrack, + new UnboundedIntArray[]{videoArray, audioArray}, 24, 0L, 0L); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java new file mode 100644 index 0000000000..305f01440c --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java @@ -0,0 +1,18 @@ +package com.google.android.exoplayer2.extractor.avi; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Most of this is covered by the PicOrderClockTest + */ +public class LinearClockTest { + @Test + public void advance() { + final LinearClock linearClock = new LinearClock(100L); + linearClock.setIndex(2); + Assert.assertEquals(200, linearClock.getUs()); + linearClock.advance(); + Assert.assertEquals(300, linearClock.getUs()); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java new file mode 100644 index 0000000000..1c18a4ee5b --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java @@ -0,0 +1,45 @@ +package com.google.android.exoplayer2.extractor.avi; + +import org.junit.Assert; +import org.junit.Test; + +public class PicCountClockTest { + @Test + public void us_givenTwoStepsForward() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setMaxPicCount(16*2); + picCountClock.setPicCount(2*2); + Assert.assertEquals(2*100, picCountClock.getUs()); + } + + @Test + public void us_givenThreeStepsBackwards() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setMaxPicCount(16*2); + picCountClock.setPicCount(4*2); // 400ms + Assert.assertEquals(400, picCountClock.getUs()); + picCountClock.setPicCount(1*2); + Assert.assertEquals(1*100, picCountClock.getUs()); + } + + @Test + public void setIndex_given3Chunks() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setIndex(3); + Assert.assertEquals(3*100, picCountClock.getUs()); + } + + @Test + public void us_giveWrapBackwards() { + final PicCountClock picCountClock = new PicCountClock(100); + picCountClock.setMaxPicCount(16*2); + //Need to walk up no faster than maxPicCount / 2 + picCountClock.setPicCount(7*2); + picCountClock.setPicCount(11*2); + picCountClock.setPicCount(15*2); + picCountClock.setPicCount(1*2); + Assert.assertEquals(17*100, picCountClock.getUs()); + picCountClock.setPicCount(14*2); + Assert.assertEquals(14*100, picCountClock.getUs()); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java index 03b732d1f1..f8e8436288 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java @@ -19,11 +19,10 @@ public class StreamHeaderBoxTest { Assert.assertTrue(streamHeaderBox.isVideo()); Assert.assertFalse(streamHeaderBox.isAudio()); Assert.assertEquals(StreamHeaderBox.VIDS, streamHeaderBox.getSteamType()); - Assert.assertEquals(StreamHeaderBox.XVID, streamHeaderBox.getFourCC()); + Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC()); Assert.assertEquals(0, streamHeaderBox.getInitialFrames()); Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1); Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample()); - Assert.assertEquals(MimeTypes.VIDEO_MP4V, streamHeaderBox.getMimeType()); Assert.assertEquals(11805L, streamHeaderBox.getLength()); Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize()); } diff --git a/testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump b/testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump new file mode 100644 index 0000000000000000000000000000000000000000..4224a479f6a2583261db19a002c6b20147260cab GIT binary patch literal 56 hcmYc+O(|wT0*pX52shY41RDzUSRLdcY>+q%005N^1MvU= literal 0 HcmV?d00001 From c41dc2360f528aaaf3f1fbab17db767aec9ef635 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 25 Jan 2022 15:19:10 -0700 Subject: [PATCH 25/70] Fix crash on streamId out of bounds --- .../google/android/exoplayer2/extractor/avi/AviExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 411b6f8d99..29e262252d 100644 --- 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 @@ -431,7 +431,7 @@ public class AviExtractor implements Extractor { @Nullable private AviTrack getAviTrack(int chunkId) { final int streamId = getStreamId(chunkId); - if (streamId >= 0) { + if (streamId >= 0 && streamId < aviTracks.length) { return aviTracks[streamId]; } return null; From 1d85bf2456b11c68388c45736b0b535363f09b46 Mon Sep 17 00:00:00 2001 From: Dustin Date: Fri, 28 Jan 2022 12:47:43 -0700 Subject: [PATCH 26/70] Updated seek --- .../exoplayer2/extractor/avi/AudioFormat.java | 13 +- .../extractor/avi/AvcChunkPeeker.java | 9 +- .../extractor/avi/AviExtractor.java | 163 ++++++++++++------ .../exoplayer2/extractor/avi/AviSeekMap.java | 97 ++++++----- .../exoplayer2/extractor/avi/AviTrack.java | 86 +++++---- .../extractor/avi/IStreamFormat.java | 9 - .../exoplayer2/extractor/avi/LinearClock.java | 23 ++- .../extractor/avi/PicCountClock.java | 6 +- .../extractor/avi/StreamHeaderBox.java | 17 +- .../extractor/avi/UnboundedIntArray.java | 7 + .../exoplayer2/extractor/avi/VideoFormat.java | 13 +- .../extractor/avi/AviExtractorTest.java | 97 +++++++++++ .../extractor/avi/AviSeekMapTest.java | 54 ++++++ .../exoplayer2/extractor/avi/DataHelper.java | 67 ++++++- .../extractor/avi/LinearClockTest.java | 2 +- .../extractor/avi/PicCountClockTest.java | 8 +- .../extractor/avi/StreamHeaderBoxTest.java | 1 - .../extractor/avi/UnboundedIntArrayTest.java | 19 ++ 18 files changed, 495 insertions(+), 196 deletions(-) delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java 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 index 65bc631340..fb421b76e2 100644 --- 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 @@ -1,11 +1,10 @@ package com.google.android.exoplayer2.extractor.avi; import android.util.SparseArray; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; -public class AudioFormat implements IStreamFormat { +public class AudioFormat { public static final short WAVE_FORMAT_PCM = 1; static final short WAVE_FORMAT_AAC = 0xff; private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; @@ -62,15 +61,5 @@ public class AudioFormat implements IStreamFormat { return data; } - @Override - public boolean isAllKeyFrames() { - return true; - } - - @Override - public @C.TrackType int getTrackType() { - return C.TRACK_TYPE_AUDIO; - } - //TODO: Deal with WAVEFORMATEXTENSIBLE } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java index bd2544bb97..0fa888a266 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java @@ -8,6 +8,10 @@ import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; import java.io.IOException; +/** + * Corrects the time and PAR for H264 streams + * H264 is very rare in AVI due to the rise of mp4 + */ public class AvcChunkPeeker extends NalChunkPeeker { private static final int NAL_TYPE_MASK = 0x1f; private static final int NAL_TYPE_IRD = 5; @@ -22,11 +26,12 @@ public class AvcChunkPeeker extends NalChunkPeeker { private float pixelWidthHeightRatio = 1f; private NalUnitUtil.SpsData spsData; - public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, long usPerChunk) { + public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, long durationUs, + int length) { super(16); this.formatBuilder = formatBuilder; this.trackOutput = trackOutput; - picCountClock = new PicCountClock(usPerChunk); + picCountClock = new PicCountClock(durationUs, length); } public PicCountClock getPicCountClock() { 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 index 29e262252d..614719112e 100644 --- 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 @@ -17,12 +17,15 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collections; +import java.util.HashMap; /** * Based on the official MicroSoft spec * https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference */ public class AviExtractor implements Extractor { + //Minimum time between keyframes in the SeekMap + static final long MIN_KEY_FRAME_RATE_US = 2_000_000L; static final long UINT_MASK = 0xffffffffL; static long getUInt(ByteBuffer byteBuffer) { @@ -69,7 +72,7 @@ public class AviExtractor implements Extractor { @VisibleForTesting static final int STATE_SEEK_START = 4; - private static final int AVIIF_KEYFRAME = 16; + static final int AVIIF_KEYFRAME = 16; static final int RIFF = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); @@ -92,6 +95,9 @@ public class AviExtractor implements Extractor { ExtractorOutput output; private AviHeaderBox aviHeader; private long durationUs = C.TIME_UNSET; + /** + * AviTracks by StreamId + */ private AviTrack[] aviTracks = new AviTrack[0]; //At the start of the movi tag private long moviOffset; @@ -210,13 +216,17 @@ public class AviExtractor implements Extractor { final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class); final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class); if (streamHeader == null) { - Log.w(TAG, "Missing Stream Header"); + w("Missing Stream Header"); return null; } + //i(streamHeader.toString()); if (streamFormat == null) { - Log.w(TAG, "Missing Stream Format"); + w("Missing Stream Format"); return null; } + final long durationUs = streamHeader.getDurationUs(); + //Initial estimate + final int length = streamHeader.getLength(); final Format.Builder builder = new Format.Builder(); builder.setId(streamId); final int suggestedBufferSize = streamHeader.getSuggestedBufferSize(); @@ -242,18 +252,20 @@ public class AviExtractor implements Extractor { builder.setSampleMimeType(mimeType); if (MimeTypes.VIDEO_H264.equals(mimeType)) { - final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample()); - aviTrack = new AviTrack(streamId, videoFormat, avcChunkPeeker.getPicCountClock(), trackOutput); + final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, durationUs, + length); + aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, avcChunkPeeker.getPicCountClock(), + trackOutput); aviTrack.setChunkPeeker(avcChunkPeeker); } else { - aviTrack = new AviTrack(streamId, videoFormat, - new LinearClock(streamHeader.getUsPerSample()), trackOutput); + aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, + new LinearClock(durationUs, length), trackOutput); if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput)); } } trackOutput.format(builder.build()); - durationUs = streamHeader.getUsPerSample() * streamHeader.getLength(); + this.durationUs = durationUs; } else if (streamHeader.isAudio()) { final AudioFormat audioFormat = streamFormat.getAudioFormat(); final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); @@ -274,8 +286,9 @@ public class AviExtractor implements Extractor { builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData())); } trackOutput.format(builder.build()); - aviTrack = new AviTrack(streamId, audioFormat, new LinearClock(streamHeader.getUsPerSample()), - trackOutput); + aviTrack = new AviTrack(streamId, C.TRACK_TYPE_AUDIO, + new LinearClock(durationUs, length), trackOutput); + aviTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); }else { aviTrack = null; } @@ -343,6 +356,27 @@ public class AviExtractor implements Extractor { return null; } + void updateAudioTiming(final int[] keyFrameCounts, final long videoDuration) { + for (final AviTrack aviTrack : aviTracks) { + if (aviTrack != null && aviTrack.isAudio()) { + final long durationUs = aviTrack.getClock().durationUs; + i("Audio #" + aviTrack.id + " chunks: " + aviTrack.chunks + " us=" + durationUs + + " size=" + aviTrack.size); + final LinearClock linearClock = aviTrack.getClock(); + //If the audio track duration is off from the video by >5 % recalc using video + if ((durationUs - videoDuration) / (float)videoDuration > .05f) { + w("Audio #" + aviTrack.id + " duration is off using videoDuration"); + linearClock.setDuration(videoDuration); + } + linearClock.setLength(aviTrack.chunks); + if (aviTrack.chunks != keyFrameCounts[aviTrack.id]) { + w("Audio is not all key frames chunks=" + aviTrack.chunks + " keyFrames=" + + keyFrameCounts[aviTrack.id]); + } + } + } + } + /** * Reads the index and sets the keyFrames and creates the SeekMap * @param input @@ -353,86 +387,93 @@ public class AviExtractor implements Extractor { final AviTrack videoTrack = getVideoTrack(); if (videoTrack == null) { output.seekMap(new SeekMap.Unseekable(getDuration())); - Log.w(TAG, "No video track found"); + w("No video track found"); return; } + final int videoId = videoTrack.id; final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); final byte[] bytes = indexByteBuffer.array(); - final int[] chunkCounts = new int[aviTracks.length]; - final UnboundedIntArray[] seekOffsets = new UnboundedIntArray[aviTracks.length]; - for (int i=0;i tagMap = new HashMap<>(); while (remaining > 0) { final int toRead = Math.min(indexByteBuffer.remaining(), remaining); input.readFully(bytes, indexByteBuffer.position(), toRead); + indexByteBuffer.limit(indexByteBuffer.position() + toRead); remaining -= toRead; while (indexByteBuffer.remaining() >= 16) { final int chunkId = indexByteBuffer.getInt(); + Integer count = tagMap.get(chunkId); + if (count == null) { + count = 1; + } else { + count += 1; + } + tagMap.put(chunkId, count); final AviTrack aviTrack = getAviTrack(chunkId); if (aviTrack == null) { if (chunkId != AviExtractor.REC_) { - Log.w(TAG, "Unknown Track Type: " + toString(chunkId)); + w("Unknown Track Type: " + toString(chunkId)); } indexByteBuffer.position(indexByteBuffer.position() + 12); continue; } final int flags = indexByteBuffer.getInt(); final int offset = indexByteBuffer.getInt(); - indexByteBuffer.position(indexByteBuffer.position() + 4); - //int size = indexByteBuffer.getInt(); - if (aviTrack.isVideo()) { - if (!aviTrack.isAllKeyFrames() && (flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { - keyFrameList.add(chunkCounts[aviTrack.id]); - } - if (chunkCounts[aviTrack.id] % seekFrameRate == 0) { - seekOffsets[aviTrack.id].add(offset); - for (int i=0;i= chunksPerKeyFrame) { + keyFrameOffsetsDiv2.add(offset / 2); + for (AviTrack seekTrack : aviTracks) { + if (seekTrack != null) { + seekIndexes[seekTrack.id].add(seekTrack.chunks); + } } } } + keyFrameCounts[aviTrack.id]++; } - chunkCounts[aviTrack.id]++; + aviTrack.chunks++; + aviTrack.size+=size; } indexByteBuffer.compact(); } - //Set the keys frames - if (!videoTrack.isAllKeyFrames()) { - final int[] keyFrames = keyFrameList.getArray(); - videoTrack.setKeyFrames(keyFrames); + if (videoTrack.chunks == keyFrameCounts[videoTrack.id]) { + videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); + } else { + videoTrack.setKeyFrames(seekIndexes[videoId].getArray()); } - //Correct the timings - durationUs = chunkCounts[videoTrack.id] * videoTrack.getClock().usPerChunk; + final AviSeekMap seekMap = new AviSeekMap(videoId, videoTrack.clock.durationUs, videoTrack.chunks, + keyFrameOffsetsDiv2.getArray(), seekIndexes, moviOffset); + + i("Video chunks=" + videoTrack.chunks + " us=" + seekMap.getDurationUs()); + + //Needs to be called after the duration is updated + updateAudioTiming(keyFrameCounts, durationUs); - for (int i=0;i.01) { - Log.i(TAG, "Updating stream " + i + " calcUsPerSample=" + calcUsPerSample + " reported=" + linearClock.usPerChunk); - linearClock.usPerChunk = calcUsPerSample; - } - } - } - final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekOffsets, seekFrameRate, moviOffset, getDuration()); setSeekMap(seekMap); } @Nullable private AviTrack getAviTrack(int chunkId) { - final int streamId = getStreamId(chunkId); - if (streamId >= 0 && streamId < aviTracks.length) { - return aviTracks[streamId]; + for (AviTrack aviTrack : aviTracks) { + if (aviTrack.handlesChunkId(chunkId)) { + return aviTrack; + } } return null; } @@ -525,6 +566,7 @@ public class AviExtractor implements Extractor { @Override public void seek(long position, long timeUs) { + //i("Seek pos=" + position +", us="+timeUs); chunkHandler = null; if (position <= 0) { if (moviOffset != 0) { @@ -551,6 +593,11 @@ public class AviExtractor implements Extractor { //Intentionally blank } + @VisibleForTesting + void setAviTracks(AviTrack[] aviTracks) { + this.aviTracks = aviTracks; + } + private static void w(String message) { try { Log.w(TAG, message); @@ -558,4 +605,12 @@ public class AviExtractor implements Extractor { //Catch not mocked for tests } } + + private static void i(String message) { + try { + Log.i(TAG, message); + } catch (RuntimeException e) { + //Catch not mocked for tests + } + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index e228756848..e5539039eb 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -1,34 +1,32 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; +import java.util.Arrays; public class AviSeekMap implements SeekMap { + final int videoId; final long videoUsPerChunk; - final int videoStreamId; - /** - * Number of frames per index - * i.e. videoFrameOffsetMap[1] is frame 1 * seekIndexFactor - */ - final int seekIndexFactor; - //Seek offsets by streamId, for video, this is the actual offset, for audio, this is the chunkId - final int[][] seekOffsets; - //Holds a map of video frameIds to audioFrameIds for each audioId - - final long moviOffset; final long duration; + //These are ints / 2 + final int[] keyFrameOffsetsDiv2; + //Seek chunk indexes by streamId + final int[][] seekIndexes; + final long moviOffset; - public AviSeekMap(AviTrack videoTrack, UnboundedIntArray[] seekOffsets, int seekIndexFactor, long moviOffset, long duration) { - videoUsPerChunk = videoTrack.getClock().usPerChunk; - videoStreamId = videoTrack.id; - this.seekIndexFactor = seekIndexFactor; - this.moviOffset = moviOffset; - this.duration = duration; - this.seekOffsets = new int[seekOffsets.length][]; - for (int i=0;i= seekOffsets[videoStreamId].length) { - reqFrameIndex = seekOffsets[videoStreamId].length - 1; + return Arrays.binarySearch(seekIndexes[videoId], reqFrame); + } + + @VisibleForTesting + int getFirstSeekIndex(int index) { + int firstIndex = -index - 2; + if (firstIndex < 0) { + firstIndex = 0; } - return reqFrameIndex; + return firstIndex; + } + + private SeekPoint getSeekPoint(int index) { + long offset = keyFrameOffsetsDiv2[index] * 2L; + final long outUs = seekIndexes[videoId][index] * videoUsPerChunk; + final long position = offset + moviOffset; + return new SeekPoint(outUs, position); } @NonNull @Override public SeekPoints getSeekPoints(long timeUs) { - final int seekFrameIndex = getSeekFrameIndex(timeUs); - int offset = seekOffsets[videoStreamId][seekFrameIndex]; - final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk; - final long position = offset + moviOffset; - //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); + final int index = getSeekIndex(timeUs); + if (index >= 0) { + return new SeekPoints(getSeekPoint(index)); + } + final int firstSeekIndex = getFirstSeekIndex(index); + if (firstSeekIndex + 1 < keyFrameOffsetsDiv2.length) { + return new SeekPoints(getSeekPoint(firstSeekIndex), getSeekPoint(firstSeekIndex+1)); + } else { + return new SeekPoints(getSeekPoint(firstSeekIndex)); + } - return new SeekPoints(new SeekPoint(outUs, position)); + //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); } public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) { - final int seekFrameIndex = getSeekFrameIndex(timeUs); + final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - moviOffset) / 2)); + + if (index < 0) { + throw new IllegalArgumentException("Position: " + position); + } for (int i=0;i= 0; - } - return false; - } - - public void setForceKeyFrame(boolean v) { - forceKeyFrame = v; - } - - public void setKeyFrames(int[] keyFrames) { - this.keyFrames = keyFrames; + return keyFrames == ALL_KEY_FRAMES || Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0; } public boolean isVideo() { @@ -130,7 +143,8 @@ public class AviTrack { void done(final int size) { trackOutput.sampleMetadata( clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); - //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame()); + final LinearClock clock = getClock(); +// Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); clock.advance(); } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java deleted file mode 100644 index 0211b197dc..0000000000 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/IStreamFormat.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.google.android.exoplayer2.extractor.avi; - -import com.google.android.exoplayer2.C; - -public interface IStreamFormat { - String getMimeType(); - boolean isAllKeyFrames(); - @C.TrackType int getTrackType(); -} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java index b313e501e5..03fcdbd795 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java @@ -1,12 +1,22 @@ package com.google.android.exoplayer2.extractor.avi; public class LinearClock { - long usPerChunk; + long durationUs; + int length; int index; - public LinearClock(long usPerChunk) { - this.usPerChunk = usPerChunk; + public LinearClock(long durationUs, int length) { + this.durationUs = durationUs; + this.length = length; + } + + public void setDuration(long durationUs) { + this.durationUs = durationUs; + } + + public void setLength(int length) { + this.length = length; } public int getIndex() { @@ -22,6 +32,11 @@ public class LinearClock { } public long getUs() { - return index * usPerChunk; + return getUs(index); + } + + long getUs(int index) { + //Doing this the hard way lessens round errors + return durationUs * index / length; } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java index 7f76f00296..7e515f786f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -15,8 +15,8 @@ public class PicCountClock extends LinearClock { private int posHalf; private int negHalf; - public PicCountClock(long usPerFrame) { - super(usPerFrame); + public PicCountClock(long durationUs, int length) { + super(durationUs, length); } public void setMaxPicCount(int maxPicCount) { @@ -59,6 +59,6 @@ public class PicCountClock extends LinearClock { @Override public long getUs() { - return picIndex * usPerChunk; + return getUs(picIndex); } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 6cfbb61fb1..fffc43f016 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -30,11 +30,8 @@ public class StreamHeaderBox extends ResidentBox { return getRate() / (float)getScale(); } - /** - * @return sample duration in us - */ - public long getUsPerSample() { - return getScale() * 1_000_000L / getRate(); + public long getDurationUs() { + return getScale() * getLength() * 1_000_000L / getRate(); } public int getSteamType() { @@ -59,12 +56,12 @@ public class StreamHeaderBox extends ResidentBox { public int getRate() { return byteBuffer.getInt(24); } - // 28 - dwStart + //28 - dwStart - doesn't seem to ever be set // public int getStart() { // return byteBuffer.getInt(28); // } - public long getLength() { - return byteBuffer.getInt(32) & AviExtractor.UINT_MASK; + public int getLength() { + return byteBuffer.getInt(32); } public int getSuggestedBufferSize() { @@ -75,4 +72,8 @@ public class StreamHeaderBox extends ResidentBox { // public int getSampleSize() { // return byteBuffer.getInt(44); // } + +// public String toString() { +// return "scale=" + getScale() + " rate=" + getRate() + " length=" + getLength() + " us=" + getDurationUs(); +// } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java index 6efe3d74a5..61455d19b6 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java @@ -29,6 +29,13 @@ public class UnboundedIntArray { array[size++] = v; } + public int get(final int index) { + if (index >= size) { + throw new ArrayIndexOutOfBoundsException(index + ">=" + size); + } + return array[index]; + } + public int getSize() { return size; } 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 index f849eed932..af25396002 100644 --- 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 @@ -1,11 +1,10 @@ package com.google.android.exoplayer2.extractor.avi; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; import java.util.HashMap; -public class VideoFormat implements IStreamFormat { +public class VideoFormat { static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); @@ -56,14 +55,4 @@ public class VideoFormat implements IStreamFormat { public String getMimeType() { return STREAM_MAP.get(getCompression()); } - - @Override - public boolean isAllKeyFrames() { - return MimeTypes.VIDEO_MJPEG.equals(getMimeType()); - } - - @Override - public int getTrackType() { - return C.TRACK_TYPE_VIDEO; - } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index f9325cc1de..f969979b8a 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -1,5 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import java.io.IOException; @@ -148,4 +149,100 @@ public class AviExtractorTest { Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24))); } + private void assertIdx1(AviSeekMap aviSeekMap, AviTrack videoTrack, int keyFrames, int keyFrameRate) { + Assert.assertEquals(keyFrames, videoTrack.keyFrames.length); + + final int framesPerKeyFrame = 24 * 3; + //This indirectly verifies the number of video chunks + Assert.assertEquals(9 * DataHelper.FPS, videoTrack.chunks); + + Assert.assertEquals(2 * framesPerKeyFrame, videoTrack.keyFrames[2]); + + Assert.assertEquals(2 * keyFrameRate * DataHelper.AUDIO_PER_VIDEO, + aviSeekMap.seekIndexes[DataHelper.AUDIO_ID][2]); + Assert.assertEquals(4L + 2 * keyFrameRate * DataHelper.VIDEO_SIZE + + 2 * keyFrameRate * DataHelper.AUDIO_SIZE * DataHelper.AUDIO_PER_VIDEO, + aviSeekMap.keyFrameOffsetsDiv2[2] * 2L); + + } + + @Test + public void readIdx1_given9secsAv() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final int secs = 9; + final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds + final int keyFrames = secs * DataHelper.FPS / keyFrameRate; + final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate); + final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs); + final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); + aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); + + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + final AviSeekMap aviSeekMap = aviExtractor.aviSeekMap; + assertIdx1(aviSeekMap, videoTrack, keyFrames, keyFrameRate); + } + @Test + public void readIdx1_givenNoVideo() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final int secs = 9; + final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds + final ByteBuffer idx1 = DataHelper.getIndex(secs, keyFrameRate); + final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); + aviExtractor.setAviTracks(new AviTrack[]{audioTrack}); + + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + Assert.assertTrue(fakeExtractorOutput.seekMap instanceof SeekMap.Unseekable); + } + + @Test + public void readIdx1_givenJunkInIndex() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final int secs = 9; + final int keyFrameRate = 3 * DataHelper.FPS; // Keyframe every 3 seconds + final int keyFrames = secs * DataHelper.FPS / keyFrameRate; + final ByteBuffer idx1 = DataHelper.getIndex(9, keyFrameRate); + final ByteBuffer junk = AviExtractor.allocate(idx1.capacity() + 16); + junk.putInt(AviExtractor.JUNK); + junk.putInt(0); + junk.putInt(0); + junk.putInt(0); + idx1.flip(); + junk.put(idx1); + final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs); + final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); + aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); + + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(junk.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + + assertIdx1(aviExtractor.aviSeekMap, videoTrack, keyFrames, keyFrameRate); + } + + @Test + public void readIdx1_givenAllKeyFrames() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final int secs = 4; + final ByteBuffer idx1 = DataHelper.getIndex(secs, 1); + final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs); + final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); + aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); + + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + + //We should be throttled to 2 key frame per second + Assert.assertSame(AviTrack.ALL_KEY_FRAMES, videoTrack.keyFrames); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java new file mode 100644 index 0000000000..a20660edd3 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java @@ -0,0 +1,54 @@ +package com.google.android.exoplayer2.extractor.avi; + +import com.google.android.exoplayer2.extractor.SeekMap; +import org.junit.Assert; +import org.junit.Test; + +public class AviSeekMapTest { + + @Test + public void setFrames_givenExactSeekPointMatch() { + final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); + final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.moviOffset; + final int secs = 4; + final AviTrack[] aviTracks = new AviTrack[]{DataHelper.getVideoAviTrack(secs), + DataHelper.getAudioAviTrack(secs)}; + + aviSeekMap.setFrames(position, 1_000_000L, aviTracks); + for (int i=0;i Date: Fri, 28 Jan 2022 17:17:32 -0700 Subject: [PATCH 27/70] Fix BitmapFactoryVideoRenderer sync issues --- .../video/BitmapFactoryVideoRenderer.java | 271 +++++++++--------- 1 file changed, 142 insertions(+), 129 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java index ea0eb5712a..41a17b7b9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -6,6 +6,7 @@ import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.os.Handler; +import android.os.SystemClock; import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -20,27 +21,21 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; public class BitmapFactoryVideoRenderer extends BaseRenderer { private static final String TAG = "BitmapFactoryRenderer"; final VideoRendererEventListener.EventDispatcher eventDispatcher; @Nullable - Surface surface; - private boolean firstFrameRendered; + volatile Surface surface; private final Rect rect = new Rect(); private final Point lastSurface = new Point(); + private final RenderRunnable renderRunnable = new RenderRunnable(); + private final Thread thread = new Thread(renderRunnable, "BitmapFactoryVideoRenderer"); private VideoSize lastVideoSize = VideoSize.UNKNOWN; - @Nullable - private ThreadPoolExecutor renderExecutor; - @Nullable - private Thread thread; private long currentTimeUs; - private long nextFrameUs; - private long frameUs = Long.MIN_VALUE; - private boolean ended; + private long frameUs; + boolean ended; + @Nullable private DecoderCounters decoderCounters; public BitmapFactoryVideoRenderer(@Nullable Handler eventHandler, @@ -58,68 +53,43 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { @Override protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) throws ExoPlaybackException { - firstFrameRendered = ended = false; - renderExecutor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); + thread.start(); } @Override protected void onDisabled() { - renderExecutor.shutdownNow(); - eventDispatcher.disabled(decoderCounters); + renderRunnable.running = false; + thread.interrupt(); + + @Nullable + final DecoderCounters decoderCounters = this.decoderCounters; + if (decoderCounters != null) { + eventDispatcher.disabled(decoderCounters); + } } private void onFormatChanged(@NonNull FormatHolder formatHolder) { @Nullable final Format format = formatHolder.format; if (format != null) { - eventDispatcher.inputFormatChanged(format, null); frameUs = (long)(1_000_000L / format.frameRate); + eventDispatcher.inputFormatChanged(format, null); } } @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + //Log.d(TAG, "Render: us=" + positionUs); synchronized (eventDispatcher) { currentTimeUs = positionUs; eventDispatcher.notify(); } - if (renderExecutor.getActiveCount() > 0) { - //Handle decoder overrun - if (positionUs > nextFrameUs) { - long us = (positionUs - nextFrameUs) + frameUs; - long dropped = us / frameUs; - eventDispatcher.droppedFrames((int)dropped, us); - nextFrameUs += frameUs * dropped; - } - return; - } - final FormatHolder formatHolder = getFormatHolder(); - final DecoderInputBuffer decoderInputBuffer = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - final int result = readSource(formatHolder, decoderInputBuffer, - frameUs == Long.MIN_VALUE ? SampleStream.FLAG_REQUIRE_FORMAT : 0); - - if (result == C.RESULT_BUFFER_READ) { - renderExecutor.execute(new RenderRunnable(decoderInputBuffer, nextFrameUs)); - if (decoderInputBuffer.isEndOfStream()) { - ended = true; - } else { - nextFrameUs += frameUs; - } - } else if (result == C.RESULT_FORMAT_READ) { - onFormatChanged(formatHolder); - } } @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - nextFrameUs = positionUs; - @Nullable - final Thread thread = this.thread; - if (thread != null) { - thread.interrupt(); - } + thread.interrupt(); } @Override @@ -141,7 +111,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { @Override public boolean isEnded() { - return ended && renderExecutor.getActiveCount() == 0; + return renderRunnable.ended; } @Override @@ -154,96 +124,139 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { } class RenderRunnable implements Runnable { - @Nullable - private DecoderInputBuffer decoderInputBuffer; - private final long renderUs; + private volatile boolean ended; + private boolean firstFrameRendered; + private volatile boolean running = true; - RenderRunnable(@NonNull final DecoderInputBuffer decoderInputBuffer, long renderUs) { - this.decoderInputBuffer = decoderInputBuffer; - this.renderUs = renderUs; + @Nullable + private Bitmap decodeInputBuffer(final DecoderInputBuffer decoderInputBuffer) { + @Nullable final ByteBuffer byteBuffer = decoderInputBuffer.data; + if (byteBuffer != null) { + final Bitmap bitmap; + try { + bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), + byteBuffer.arrayOffset() + byteBuffer.position()); + if (bitmap == null) { + eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); + } else { + return bitmap; + } + } catch (Exception e) { + eventDispatcher.videoCodecError(e); + } + } + return null; } - private boolean maybeDropFrame(long frameUs) { - if (Math.abs(frameUs - currentTimeUs) > frameUs) { - eventDispatcher.droppedFrames(1, frameUs); - return true; + private void renderBitmap(final Bitmap bitmap, @NonNull final Surface surface) { + //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + final Canvas canvas = surface.lockCanvas(null); + + final Rect clipBounds = canvas.getClipBounds(); + final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); + final boolean videoSizeChanged; + if (videoSize.equals(lastVideoSize)) { + videoSizeChanged = false; + } else { + lastVideoSize = videoSize; + eventDispatcher.videoSizeChanged(videoSize); + videoSizeChanged = true; + } + if (lastSurface.x != clipBounds.width() || lastSurface.y != clipBounds.height() || + videoSizeChanged) { + lastSurface.x = clipBounds.width(); + lastSurface.y = clipBounds.height(); + final float scaleX = lastSurface.x / (float)videoSize.width; + final float scaleY = lastSurface.y / (float)videoSize.height; + final float scale = Math.min(scaleX, scaleY); + final float width = videoSize.width * scale; + final float height = videoSize.height * scale; + final int x = (int)(lastSurface.x - width) / 2; + final int y = (int)(lastSurface.y - height) / 2; + rect.set(x, y, x + (int)width, y + (int) height); + } + canvas.drawBitmap(bitmap, null, rect, null); + + surface.unlockCanvasAndPost(canvas); + @Nullable + final DecoderCounters decoderCounters = BitmapFactoryVideoRenderer.this.decoderCounters; + if (decoderCounters != null) { + decoderCounters.renderedOutputBufferCount++; + } + if (!firstFrameRendered) { + firstFrameRendered = true; + eventDispatcher.renderedFirstFrame(surface); + } + } + + /** + * + * @return true if interrupted + */ + private boolean sleep() { + synchronized (eventDispatcher) { + try { + eventDispatcher.wait(); + return false; + } catch (InterruptedException e) { + //If we are interrupted, treat as a cancel + return true; + } } - return false; } public void run() { - if (maybeDropFrame(renderUs)) { - return; - } - @Nullable - final ByteBuffer byteBuffer = decoderInputBuffer.data; - @Nullable - final Surface surface = BitmapFactoryVideoRenderer.this.surface; - if (byteBuffer != null && surface != null) { - final Bitmap bitmap; - try { - bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.arrayOffset() + byteBuffer.position()); - } catch (Exception e) { - eventDispatcher.videoCodecError(e); - return; - } - if (bitmap == null) { - eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); - return; - } - decoderInputBuffer = null; - //Wait for time to advance to display the Bitmap - synchronized (eventDispatcher) { - while (currentTimeUs < renderUs) { - try { - thread = Thread.currentThread(); - eventDispatcher.wait(); - } catch (InterruptedException e) { - //If we are interrupted, treat as a cancel - return; - } finally { - thread = null; + final FormatHolder formatHolder = getFormatHolder(); + @NonNull + final DecoderInputBuffer decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + long start = SystemClock.uptimeMillis(); + main: + while (running) { + decoderInputBuffer.clear(); + final int result = readSource(formatHolder, decoderInputBuffer, + formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0); + if (result == C.RESULT_BUFFER_READ) { + if (decoderInputBuffer.isEndOfStream()) { + ended = true; + if (!sleep()) { + ended = false; + } + continue; + } + final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; + //If we are more than 1/2 a frame behind, skip the next frame + if (leadUs < -frameUs / 2) { + eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start); + start = SystemClock.uptimeMillis(); + continue; + } + start = SystemClock.uptimeMillis(); + + @Nullable + final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer); + if (bitmap == null) { + continue; + } + while (currentTimeUs < decoderInputBuffer.timeUs) { + //Log.d(TAG, "Sleep: us=" + currentTimeUs); + if (sleep()) { + continue main; + } + if (!running) { + break main; } } - } - if (maybeDropFrame(renderUs)) { - return; - } - //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); - final Canvas canvas = surface.lockCanvas(null); - - final Rect clipBounds = canvas.getClipBounds(); - final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); - final boolean videoSizeChanged; - if (videoSize.equals(lastVideoSize)) { - videoSizeChanged = false; - } else { - lastVideoSize = videoSize; - eventDispatcher.videoSizeChanged(videoSize); - videoSizeChanged = true; - } - if (lastSurface.x != clipBounds.width() || lastSurface.y != clipBounds.height() || - videoSizeChanged) { - lastSurface.x = clipBounds.width(); - lastSurface.y = clipBounds.height(); - final float scaleX = lastSurface.x / (float)videoSize.width; - final float scaleY = lastSurface.y / (float)videoSize.height; - final float scale = Math.min(scaleX, scaleY); - final float width = videoSize.width * scale; - final float height = videoSize.height * scale; - final int x = (int)(lastSurface.x - width) / 2; - final int y = (int)(lastSurface.y - height) / 2; - rect.set(x, y, x + (int)width, y + (int) height); - } - canvas.drawBitmap(bitmap, null, rect, null); - - surface.unlockCanvasAndPost(canvas); - decoderCounters.renderedOutputBufferCount++; - if (!firstFrameRendered) { - firstFrameRendered = true; - eventDispatcher.renderedFirstFrame(surface); + @Nullable + final Surface surface = BitmapFactoryVideoRenderer.this.surface; + if (surface != null) { + renderBitmap(bitmap, surface); + } + } else if (result == C.RESULT_FORMAT_READ) { + onFormatChanged(formatHolder); } } + ended = true; } } } From a17d36de12b7adb79b1e979803042efe505f876e Mon Sep 17 00:00:00 2001 From: Dustin Date: Fri, 28 Jan 2022 17:42:04 -0700 Subject: [PATCH 28/70] Minor cleanup --- .../video/BitmapFactoryVideoRenderer.java | 12 ++-- .../extractor/avi/AviExtractor.java | 71 ++++++++----------- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java index 41a17b7b9e..e7b15d2fd3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -148,7 +148,10 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { return null; } - private void renderBitmap(final Bitmap bitmap, @NonNull final Surface surface) { + private void renderBitmap(final Bitmap bitmap, @Nullable final Surface surface) { + if (surface == null) { + return; + } //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); final Canvas canvas = surface.lockCanvas(null); @@ -243,13 +246,8 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { if (sleep()) { continue main; } - if (!running) { - break main; - } } - @Nullable - final Surface surface = BitmapFactoryVideoRenderer.this.surface; - if (surface != null) { + if (running) { renderBitmap(bitmap, surface); } } else if (result == C.RESULT_FORMAT_READ) { 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 index 614719112e..a48025afb5 100644 --- 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 @@ -57,6 +57,34 @@ public class AviExtractor implements Extractor { } } + static int checkAlign(final ExtractorInput input, PositionHolder seekPosition) { + final long position = input.getPosition(); + if ((position & 1) ==1) { + seekPosition.position = position + 1; + return RESULT_SEEK; + } + return RESULT_CONTINUE; + } + + static ByteBuffer allocate(int bytes) { + final byte[] buffer = new byte[bytes]; + final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return byteBuffer; + } + + @VisibleForTesting + static int getStreamId(int chunkId) { + final int upperChar = chunkId & 0xff; + if (Character.isDigit(upperChar)) { + final int lowerChar = (chunkId >> 8) & 0xff; + if (Character.isDigit(upperChar)) { + return (lowerChar & 0xf) + ((upperChar & 0xf) * 10); + } + } + return -1; + } + static final String TAG = "AviExtractor"; @VisibleForTesting static final int PEEK_BYTES = 28; @@ -87,8 +115,6 @@ public class AviExtractor implements Extractor { static final int JUNK = 'J' | ('U' << 8) | ('N' << 16) | ('K' << 24); static final int REC_ = 'r' | ('e' << 8) | ('c' << 16) | (' ' << 24); - static final long SEEK_GAP = 2_000_000L; //Time between seek points in micro seconds - @VisibleForTesting int state; @VisibleForTesting @@ -112,7 +138,6 @@ public class AviExtractor implements Extractor { /** * - * @param input * @param bytes Must be at least 20 */ @Nullable @@ -143,7 +168,7 @@ public class AviExtractor implements Extractor { } @Override - public boolean sniff(ExtractorInput input) throws IOException { + public boolean sniff(@NonNull ExtractorInput input) throws IOException { final ByteBuffer byteBuffer = getAviBuffer(input, PEEK_BYTES); if (byteBuffer == null) { return false; @@ -155,29 +180,7 @@ public class AviExtractor implements Extractor { return false; } final int avih = byteBuffer.getInt(); - if (avih != AviHeaderBox.AVIH) { - return false; - } - return true; - } - - static ByteBuffer allocate(int bytes) { - final byte[] buffer = new byte[bytes]; - final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - return byteBuffer; - } - - @VisibleForTesting - static int getStreamId(int chunkId) { - final int upperChar = chunkId & 0xff; - if (Character.isDigit(upperChar)) { - final int lowerChar = (chunkId >> 8) & 0xff; - if (Character.isDigit(upperChar)) { - return (lowerChar & 0xf) + ((upperChar & 0xf) * 10); - } - } - return -1; + return avih == AviHeaderBox.AVIH; } @VisibleForTesting @@ -206,7 +209,7 @@ public class AviExtractor implements Extractor { } @Override - public void init(ExtractorOutput output) { + public void init(@NonNull ExtractorOutput output) { this.state = STATE_READ_TRACKS; this.output = output; } @@ -379,9 +382,6 @@ public class AviExtractor implements Extractor { /** * Reads the index and sets the keyFrames and creates the SeekMap - * @param input - * @param remaining - * @throws IOException */ void readIdx1(ExtractorInput input, int remaining) throws IOException { final AviTrack videoTrack = getVideoTrack(); @@ -478,15 +478,6 @@ public class AviExtractor implements Extractor { return null; } - int checkAlign(final ExtractorInput input, PositionHolder seekPosition) { - final long position = input.getPosition(); - if ((position & 1) ==1) { - seekPosition.position = position +1; - return RESULT_SEEK; - } - return RESULT_CONTINUE; - } - int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { From b520b26f0faba2a4e69dc66a7553568eb675df33 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 29 Jan 2022 10:59:55 -0700 Subject: [PATCH 29/70] More AviExtractor tests --- .../extractor/avi/AviExtractor.java | 21 ++--- .../extractor/avi/AviHeaderBox.java | 16 +--- .../android/exoplayer2/extractor/avi/Box.java | 5 -- .../extractor/avi/StreamHeaderBox.java | 8 +- .../extractor/avi/AviExtractorRoboTest.java | 20 +++++ .../extractor/avi/AviExtractorTest.java | 83 ++++++++++++++---- .../exoplayer2/extractor/avi/DataHelper.java | 52 ++++++++--- .../extractor/avi/StreamHeaderBoxTest.java | 5 +- .../avi/auds_stream_header.dump | Bin 56 -> 0 bytes .../avi/vids_stream_header.dump | Bin 64 -> 0 bytes 10 files changed, 147 insertions(+), 63 deletions(-) delete mode 100644 testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump delete mode 100644 testdata/src/test/assets/extractordumps/avi/vids_stream_header.dump 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 index a48025afb5..72fea3cb6c 100644 --- 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 @@ -28,7 +28,7 @@ public class AviExtractor implements Extractor { static final long MIN_KEY_FRAME_RATE_US = 2_000_000L; static final long UINT_MASK = 0xffffffffL; - static long getUInt(ByteBuffer byteBuffer) { + static long getUInt(@NonNull ByteBuffer byteBuffer) { return byteBuffer.getInt() & UINT_MASK; } @@ -49,7 +49,7 @@ public class AviExtractor implements Extractor { return position; } - static void alignInput(ExtractorInput input) throws IOException { + static void alignInput(@NonNull ExtractorInput input) throws IOException { // This isn't documented anywhere, but most files are aligned to even bytes // and can have gaps of zeros if ((input.getPosition() & 1) == 1) { @@ -57,15 +57,16 @@ public class AviExtractor implements Extractor { } } - static int checkAlign(final ExtractorInput input, PositionHolder seekPosition) { + static int alignPositionHolder(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) { final long position = input.getPosition(); - if ((position & 1) ==1) { + if ((position & 1) == 1) { seekPosition.position = position + 1; return RESULT_SEEK; } return RESULT_CONTINUE; } + @NonNull static ByteBuffer allocate(int bytes) { final byte[] buffer = new byte[bytes]; final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); @@ -141,7 +142,7 @@ public class AviExtractor implements Extractor { * @param bytes Must be at least 20 */ @Nullable - private ByteBuffer getAviBuffer(ExtractorInput input, int bytes) throws IOException { + static private ByteBuffer getAviBuffer(@NonNull ExtractorInput input, int bytes) throws IOException { if (input.getLength() < bytes) { return null; } @@ -190,7 +191,7 @@ public class AviExtractor implements Extractor { } @Nullable - ListBox readHeaderList(ExtractorInput input) throws IOException { + static ListBox readHeaderList(ExtractorInput input) throws IOException { final ByteBuffer byteBuffer = getAviBuffer(input, 20); if (byteBuffer == null) { return null; @@ -245,7 +246,7 @@ public class AviExtractor implements Extractor { final VideoFormat videoFormat = streamFormat.getVideoFormat(); final String mimeType = videoFormat.getMimeType(); if (mimeType == null) { - Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC())); + Log.w(TAG, "Unknown FourCC: " + toString(videoFormat.getCompression())); return null; } final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO); @@ -478,11 +479,11 @@ public class AviExtractor implements Extractor { return null; } - int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException { + int readSamples(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { chunkHandler = null; - return checkAlign(input, seekPosition); + return alignPositionHolder(input, seekPosition); } } else { ByteBuffer byteBuffer = allocate(8); @@ -514,7 +515,7 @@ public class AviExtractor implements Extractor { return RESULT_SEEK; } if (aviTrack.newChunk(chunkId, size, input)) { - return checkAlign(input, seekPosition); + return alignPositionHolder(input, seekPosition); } else { chunkHandler = aviTrack; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index 8f6dfcedfc..84b6557d0b 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -3,7 +3,8 @@ package com.google.android.exoplayer2.extractor.avi; import java.nio.ByteBuffer; public class AviHeaderBox extends ResidentBox { - private static final int AVIF_HASINDEX = 0x10; + static final int LEN = 0x38; + static final int AVIF_HASINDEX = 0x10; private static final int AVIF_MUSTUSEINDEX = 0x20; static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24); @@ -46,15 +47,6 @@ public class AviHeaderBox extends ResidentBox { } // 28 - dwSuggestedBufferSize -// int getSuggestedBufferSize() { -// return byteBuffer.getInt(28); -// } -// -// int getWidth() { -// return byteBuffer.getInt(32); -// } -// -// int getHeight() { -// return byteBuffer.getInt(36); -// } + // 32 - dwWidth + // 36 - dwHeight } 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 index c5c10731f9..94ed3d4028 100644 --- 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 @@ -19,9 +19,4 @@ public class Box { public int getType() { return type; } - - boolean simpleAssert(final int expected) { - return getType() == expected; - } - } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index fffc43f016..8781d32adb 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -37,13 +37,7 @@ public class StreamHeaderBox extends ResidentBox { public int getSteamType() { return byteBuffer.getInt(0); } - /** - * Only meaningful for video - * @return FourCC - */ - public int getFourCC() { - return byteBuffer.getInt(4); - } + //4 - fourCC //8 - dwFlags //12 - wPriority //14 - wLanguage diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index 315e504d7d..97b4932795 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -6,6 +6,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.Collections; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,4 +36,23 @@ public class AviExtractorRoboTest { Assert.assertEquals(MimeTypes.AUDIO_AAC, trackOutput.lastFormat.sampleMimeType); } + @Test + public void parseStream_givenNoStreamHeader() { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = new ListBox(128, AviExtractor.STRL, Collections.EMPTY_LIST); + Assert.assertNull(aviExtractor.parseStream(streamList, 0)); + } + + @Test + public void parseStream_givenNoStreamFormat() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = new ListBox(128, AviExtractor.STRL, + Collections.singletonList(DataHelper.getVidsStreamHeader())); + Assert.assertNull(aviExtractor.parseStream(streamList, 0)); + } + } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index f969979b8a..68f4a6a6f4 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -1,5 +1,7 @@ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; @@ -9,6 +11,7 @@ import org.junit.Assert; import org.junit.Test; public class AviExtractorTest { + @Test public void init_givenFakeExtractorOutput() { AviExtractor aviExtractor = new AviExtractor(); @@ -83,14 +86,7 @@ public class AviExtractorTest { @Test public void peek_givenOnlyRiffAvi_ListHdrlAvih() { - ByteBuffer byteBuffer = AviExtractor.allocate(AviExtractor.PEEK_BYTES); - byteBuffer.putInt(AviExtractor.RIFF); - byteBuffer.putInt(128); - byteBuffer.putInt(AviExtractor.AVI_); - byteBuffer.putInt(ListBox.LIST); - byteBuffer.putInt(64); - byteBuffer.putInt(ListBox.TYPE_HDRL); - byteBuffer.putInt(AviHeaderBox.AVIH); + final ByteBuffer byteBuffer = DataHelper.getAviHeader(AviExtractor.PEEK_BYTES, 128); Assert.assertTrue(sniff(byteBuffer)); } @@ -118,6 +114,7 @@ public class AviExtractorTest { AviExtractor.alignInput(fakeExtractorInput); Assert.assertEquals(2, fakeExtractorInput.getPosition()); } + @Test public void alignInput_givenEvenPosition() throws IOException { @@ -149,7 +146,8 @@ public class AviExtractorTest { Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24))); } - private void assertIdx1(AviSeekMap aviSeekMap, AviTrack videoTrack, int keyFrames, int keyFrameRate) { + private void assertIdx1(AviSeekMap aviSeekMap, AviTrack videoTrack, int keyFrames, + int keyFrameRate) { Assert.assertEquals(keyFrames, videoTrack.keyFrames.length); final int framesPerKeyFrame = 24 * 3; @@ -179,11 +177,13 @@ public class AviExtractorTest { final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(idx1.array()).build(); - aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder() + .setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); final AviSeekMap aviSeekMap = aviExtractor.aviSeekMap; assertIdx1(aviSeekMap, videoTrack, keyFrames, keyFrameRate); } + @Test public void readIdx1_givenNoVideo() throws IOException { final AviExtractor aviExtractor = new AviExtractor(); @@ -195,8 +195,9 @@ public class AviExtractorTest { final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); aviExtractor.setAviTracks(new AviTrack[]{audioTrack}); - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(idx1.array()).build(); - aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder() + .setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); Assert.assertTrue(fakeExtractorOutput.seekMap instanceof SeekMap.Unseekable); } @@ -222,7 +223,7 @@ public class AviExtractorTest { final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). setData(junk.array()).build(); - aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); assertIdx1(aviExtractor.aviSeekMap, videoTrack, keyFrames, keyFrameRate); } @@ -240,9 +241,59 @@ public class AviExtractorTest { final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). setData(idx1.array()).build(); - aviExtractor.readIdx1(fakeExtractorInput, (int)fakeExtractorInput.getLength()); + aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); //We should be throttled to 2 key frame per second Assert.assertSame(AviTrack.ALL_KEY_FRAMES, videoTrack.keyFrames); } -} + + @Test + public void alignPositionHolder_givenOddPosition() { + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[4]).build(); + fakeExtractorInput.setPosition(1); + final PositionHolder positionHolder = new PositionHolder(); + final int result = AviExtractor.alignPositionHolder(fakeExtractorInput, positionHolder); + Assert.assertEquals(Extractor.RESULT_SEEK, result); + Assert.assertEquals(2, positionHolder.position); + } + + @Test + public void alignPositionHolder_givenEvenPosition() { + + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[4]).build(); + fakeExtractorInput.setPosition(2); + final PositionHolder positionHolder = new PositionHolder(); + final int result = AviExtractor.alignPositionHolder(fakeExtractorInput, positionHolder); + Assert.assertEquals(Extractor.RESULT_CONTINUE, result); + } + + @Test + public void readHeaderList_givenBadHeader() throws IOException { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[32]).build(); + Assert.assertNull(AviExtractor.readHeaderList(input)); + } + + @Test + public void readHeaderList_givenNoHeaderList() throws IOException { + final ByteBuffer byteBuffer = DataHelper.getAviHeader(88, 0x44); + byteBuffer.putInt(0x14, AviExtractor.STRL); //Overwrite header list with stream list + final FakeExtractorInput input = new FakeExtractorInput.Builder(). + setData(byteBuffer.array()).build(); + Assert.assertNull(AviExtractor.readHeaderList(input)); + } + + @Test + public void readHeaderList_givenEmptyHeaderList() throws IOException { + final ByteBuffer byteBuffer = DataHelper.getAviHeader(88, 0x44); + byteBuffer.putInt(AviHeaderBox.LEN); + byteBuffer.put(DataHelper.createHeader()); + final FakeExtractorInput input = new FakeExtractorInput.Builder(). + setData(byteBuffer.array()).build(); + final ListBox listBox = AviExtractor.readHeaderList(input); + Assert.assertEquals(1, listBox.getChildren().size()); + + Assert.assertTrue(listBox.getChildren().get(0) instanceof AviHeaderBox); + } +} \ No newline at end of file diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 26c55b49fc..be72b48450 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -35,18 +35,22 @@ public class DataHelper { } } - public static StreamHeaderBox getVidsStreamHeader() throws IOException { - final byte[] buffer = getBytes("vids_stream_header.dump"); - final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); + public static StreamHeaderBox getStreamHeader(int type, int scale, int rate, int length) { + final ByteBuffer byteBuffer = AviExtractor.allocate(0x40); + byteBuffer.putInt(type); + byteBuffer.putInt(20, scale); + byteBuffer.putInt(24, rate); + byteBuffer.putInt(32, length); + byteBuffer.putInt(36, (type == StreamHeaderBox.VIDS ? 128 : 16) * 1024); //Suggested buffer size + return new StreamHeaderBox(StreamHeaderBox.STRH, 0x40, byteBuffer); } - public static StreamHeaderBox getAudioStreamHeader() throws IOException { - final byte[] buffer = getBytes("auds_stream_header.dump"); - final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer); + public static StreamHeaderBox getVidsStreamHeader() { + return getStreamHeader(StreamHeaderBox.VIDS, 1001, 24000, 9 * FPS); + } + + public static StreamHeaderBox getAudioStreamHeader() { + return getStreamHeader(StreamHeaderBox.AUDS, 1, 44100, 9 * FPS); } public static StreamFormatBox getAacStreamFormat() throws IOException { @@ -154,4 +158,32 @@ public class DataHelper { } return byteBuffer; } + + /** + * Get the RIFF header up to AVI Header + * @param bufferSize + * @return + */ + public static ByteBuffer getAviHeader(int bufferSize, int headerListSize) { + ByteBuffer byteBuffer = AviExtractor.allocate(bufferSize); + byteBuffer.putInt(AviExtractor.RIFF); + byteBuffer.putInt(128); + byteBuffer.putInt(AviExtractor.AVI_); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(headerListSize); + byteBuffer.putInt(ListBox.TYPE_HDRL); + byteBuffer.putInt(AviHeaderBox.AVIH); + return byteBuffer; + } + + public static ByteBuffer createHeader() { + final ByteBuffer byteBuffer = ByteBuffer.allocate(AviHeaderBox.LEN); + byteBuffer.putInt((int)VIDEO_US); + byteBuffer.putLong(0); //skip 4+4 + byteBuffer.putInt(AviHeaderBox.AVIF_HASINDEX); + byteBuffer.putInt(FPS * 5); //5 seconds + byteBuffer.putInt(24, 2); // Number of streams + byteBuffer.clear(); + return byteBuffer; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java index 7bc7edcb72..e36ac29cb3 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java @@ -19,10 +19,9 @@ public class StreamHeaderBoxTest { Assert.assertTrue(streamHeaderBox.isVideo()); Assert.assertFalse(streamHeaderBox.isAudio()); Assert.assertEquals(StreamHeaderBox.VIDS, streamHeaderBox.getSteamType()); - Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC()); Assert.assertEquals(0, streamHeaderBox.getInitialFrames()); Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1); - Assert.assertEquals(11805L, streamHeaderBox.getLength()); - Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize()); + Assert.assertEquals(9 * DataHelper.FPS, streamHeaderBox.getLength()); + Assert.assertEquals(128 * 1024, streamHeaderBox.getSuggestedBufferSize()); } } diff --git a/testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump b/testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump deleted file mode 100644 index 4224a479f6a2583261db19a002c6b20147260cab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56 hcmYc+O(|wT0*pX52shY41RDzUSRLdcY>+q%005N^1MvU= diff --git a/testdata/src/test/assets/extractordumps/avi/vids_stream_header.dump b/testdata/src/test/assets/extractordumps/avi/vids_stream_header.dump deleted file mode 100644 index d7e95e2df3548a89f42564ff78c958ae8fef93be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 ocmXTROeu~C^K@ZA0xy^u7*@nW1Z4G)B#@XVm>3u?FfuRz02cHH$N&HU From 9d88db7119873c17dd807b0c8feb636ea362f4e7 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 29 Jan 2022 11:00:23 -0700 Subject: [PATCH 30/70] Clean up BitmapFactoryVideoRenderer --- .../video/BitmapFactoryVideoRenderer.java | 137 ++++++++++-------- 1 file changed, 74 insertions(+), 63 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java index e7b15d2fd3..08c2e51e49 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -24,17 +24,22 @@ import java.nio.ByteBuffer; public class BitmapFactoryVideoRenderer extends BaseRenderer { private static final String TAG = "BitmapFactoryRenderer"; - final VideoRendererEventListener.EventDispatcher eventDispatcher; - @Nullable - volatile Surface surface; + private static int threadId; + private final Rect rect = new Rect(); private final Point lastSurface = new Point(); private final RenderRunnable renderRunnable = new RenderRunnable(); - private final Thread thread = new Thread(renderRunnable, "BitmapFactoryVideoRenderer"); + + final VideoRendererEventListener.EventDispatcher eventDispatcher; + final Thread thread = new Thread(renderRunnable, getClass().getSimpleName() + threadId++); + + @Nullable + volatile Surface surface; + private VideoSize lastVideoSize = VideoSize.UNKNOWN; private long currentTimeUs; private long frameUs; - boolean ended; + private boolean firstFrameRendered; @Nullable private DecoderCounters decoderCounters; @@ -60,8 +65,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { @Override protected void onDisabled() { - renderRunnable.running = false; - thread.interrupt(); + renderRunnable.stop(); @Nullable final DecoderCounters decoderCounters = this.decoderCounters; @@ -111,7 +115,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { @Override public boolean isEnded() { - return renderRunnable.ended; + return renderRunnable.isEnded(); } @Override @@ -123,11 +127,67 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); } + void renderBitmap(final Bitmap bitmap) { + @Nullable + final Surface surface = this.surface; + if (surface == null) { + return; + } + //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + final Canvas canvas = surface.lockCanvas(null); + + final Rect clipBounds = canvas.getClipBounds(); + final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); + final boolean videoSizeChanged; + if (videoSize.equals(lastVideoSize)) { + videoSizeChanged = false; + } else { + lastVideoSize = videoSize; + eventDispatcher.videoSizeChanged(videoSize); + videoSizeChanged = true; + } + if (lastSurface.x != clipBounds.width() || lastSurface.y != clipBounds.height() || + videoSizeChanged) { + lastSurface.x = clipBounds.width(); + lastSurface.y = clipBounds.height(); + final float scaleX = lastSurface.x / (float)videoSize.width; + final float scaleY = lastSurface.y / (float)videoSize.height; + final float scale = Math.min(scaleX, scaleY); + final float width = videoSize.width * scale; + final float height = videoSize.height * scale; + final int x = (int)(lastSurface.x - width) / 2; + final int y = (int)(lastSurface.y - height) / 2; + rect.set(x, y, x + (int)width, y + (int) height); + } + canvas.drawBitmap(bitmap, null, rect, null); + + surface.unlockCanvasAndPost(canvas); + @Nullable + final DecoderCounters decoderCounters = BitmapFactoryVideoRenderer.this.decoderCounters; + if (decoderCounters != null) { + decoderCounters.renderedOutputBufferCount++; + } + if (!firstFrameRendered) { + firstFrameRendered = true; + eventDispatcher.renderedFirstFrame(surface); + } + } + class RenderRunnable implements Runnable { - private volatile boolean ended; - private boolean firstFrameRendered; + final DecoderInputBuffer decoderInputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + private volatile boolean running = true; + void stop() { + running = false; + thread.interrupt(); + } + + boolean isEnded() { + return !running || decoderInputBuffer.isEndOfStream(); + } + @Nullable private Bitmap decodeInputBuffer(final DecoderInputBuffer decoderInputBuffer) { @Nullable final ByteBuffer byteBuffer = decoderInputBuffer.data; @@ -148,50 +208,6 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { return null; } - private void renderBitmap(final Bitmap bitmap, @Nullable final Surface surface) { - if (surface == null) { - return; - } - //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); - final Canvas canvas = surface.lockCanvas(null); - - final Rect clipBounds = canvas.getClipBounds(); - final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); - final boolean videoSizeChanged; - if (videoSize.equals(lastVideoSize)) { - videoSizeChanged = false; - } else { - lastVideoSize = videoSize; - eventDispatcher.videoSizeChanged(videoSize); - videoSizeChanged = true; - } - if (lastSurface.x != clipBounds.width() || lastSurface.y != clipBounds.height() || - videoSizeChanged) { - lastSurface.x = clipBounds.width(); - lastSurface.y = clipBounds.height(); - final float scaleX = lastSurface.x / (float)videoSize.width; - final float scaleY = lastSurface.y / (float)videoSize.height; - final float scale = Math.min(scaleX, scaleY); - final float width = videoSize.width * scale; - final float height = videoSize.height * scale; - final int x = (int)(lastSurface.x - width) / 2; - final int y = (int)(lastSurface.y - height) / 2; - rect.set(x, y, x + (int)width, y + (int) height); - } - canvas.drawBitmap(bitmap, null, rect, null); - - surface.unlockCanvasAndPost(canvas); - @Nullable - final DecoderCounters decoderCounters = BitmapFactoryVideoRenderer.this.decoderCounters; - if (decoderCounters != null) { - decoderCounters.renderedOutputBufferCount++; - } - if (!firstFrameRendered) { - firstFrameRendered = true; - eventDispatcher.renderedFirstFrame(surface); - } - } - /** * * @return true if interrupted @@ -210,9 +226,6 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { public void run() { final FormatHolder formatHolder = getFormatHolder(); - @NonNull - final DecoderInputBuffer decoderInputBuffer = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); long start = SystemClock.uptimeMillis(); main: while (running) { @@ -221,10 +234,8 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0); if (result == C.RESULT_BUFFER_READ) { if (decoderInputBuffer.isEndOfStream()) { - ended = true; - if (!sleep()) { - ended = false; - } + //Wait for shutdown or stream to be changed + sleep(); continue; } final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; @@ -244,17 +255,17 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { while (currentTimeUs < decoderInputBuffer.timeUs) { //Log.d(TAG, "Sleep: us=" + currentTimeUs); if (sleep()) { + //Sleep was interrupted, discard Bitmap continue main; } } if (running) { - renderBitmap(bitmap, surface); + renderBitmap(bitmap); } } else if (result == C.RESULT_FORMAT_READ) { onFormatChanged(formatHolder); } } - ended = true; } } } From 66c240f1bdb51c2181e47392877118052a00ff9e Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 29 Jan 2022 11:35:35 -0700 Subject: [PATCH 31/70] AviHeaderBox Tests --- .../extractor/avi/AviExtractorRoboTest.java | 2 +- .../extractor/avi/AviExtractorTest.java | 8 +++---- .../extractor/avi/AviHeaderBoxTest.java | 21 +++++++++++++++++++ .../exoplayer2/extractor/avi/DataHelper.java | 4 ++-- 4 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index 97b4932795..8a19cc0289 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -46,7 +46,7 @@ public class AviExtractorRoboTest { } @Test - public void parseStream_givenNoStreamFormat() throws IOException { + public void parseStream_givenNoStreamFormat() { final AviExtractor aviExtractor = new AviExtractor(); final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); aviExtractor.init(fakeExtractorOutput); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 68f4a6a6f4..5c731e88f3 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -86,7 +86,7 @@ public class AviExtractorTest { @Test public void peek_givenOnlyRiffAvi_ListHdrlAvih() { - final ByteBuffer byteBuffer = DataHelper.getAviHeader(AviExtractor.PEEK_BYTES, 128); + final ByteBuffer byteBuffer = DataHelper.getRiffHeader(AviExtractor.PEEK_BYTES, 128); Assert.assertTrue(sniff(byteBuffer)); } @@ -277,7 +277,7 @@ public class AviExtractorTest { @Test public void readHeaderList_givenNoHeaderList() throws IOException { - final ByteBuffer byteBuffer = DataHelper.getAviHeader(88, 0x44); + final ByteBuffer byteBuffer = DataHelper.getRiffHeader(88, 0x44); byteBuffer.putInt(0x14, AviExtractor.STRL); //Overwrite header list with stream list final FakeExtractorInput input = new FakeExtractorInput.Builder(). setData(byteBuffer.array()).build(); @@ -286,9 +286,9 @@ public class AviExtractorTest { @Test public void readHeaderList_givenEmptyHeaderList() throws IOException { - final ByteBuffer byteBuffer = DataHelper.getAviHeader(88, 0x44); + final ByteBuffer byteBuffer = DataHelper.getRiffHeader(88, 0x44); byteBuffer.putInt(AviHeaderBox.LEN); - byteBuffer.put(DataHelper.createHeader()); + byteBuffer.put(DataHelper.createAviHeader()); final FakeExtractorInput input = new FakeExtractorInput.Builder(). setData(byteBuffer.array()).build(); final ListBox listBox = AviExtractor.readHeaderList(input); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java new file mode 100644 index 0000000000..13eee629ef --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java @@ -0,0 +1,21 @@ +package com.google.android.exoplayer2.extractor.avi; + +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; + +public class AviHeaderBoxTest { + + @Test + public void getters() { + final ByteBuffer byteBuffer = DataHelper.createAviHeader(); + final AviHeaderBox aviHeaderBox = new AviHeaderBox(AviHeaderBox.AVIH, + byteBuffer.capacity(), byteBuffer); + Assert.assertEquals(DataHelper.VIDEO_US, aviHeaderBox.getMicroSecPerFrame()); + Assert.assertTrue(aviHeaderBox.hasIndex()); + Assert.assertFalse(aviHeaderBox.mustUseIndex()); + Assert.assertEquals(5 * DataHelper.FPS, aviHeaderBox.getTotalFrames()); + Assert.assertEquals(2, aviHeaderBox.getStreams()); + } + +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index be72b48450..c50b04f8e5 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -164,7 +164,7 @@ public class DataHelper { * @param bufferSize * @return */ - public static ByteBuffer getAviHeader(int bufferSize, int headerListSize) { + public static ByteBuffer getRiffHeader(int bufferSize, int headerListSize) { ByteBuffer byteBuffer = AviExtractor.allocate(bufferSize); byteBuffer.putInt(AviExtractor.RIFF); byteBuffer.putInt(128); @@ -176,7 +176,7 @@ public class DataHelper { return byteBuffer; } - public static ByteBuffer createHeader() { + public static ByteBuffer createAviHeader() { final ByteBuffer byteBuffer = ByteBuffer.allocate(AviHeaderBox.LEN); byteBuffer.putInt((int)VIDEO_US); byteBuffer.putLong(0); //skip 4+4 From 432ff5ea7079e6573b42e27dac02e19188e24810 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 29 Jan 2022 14:51:03 -0700 Subject: [PATCH 32/70] More AviExtractor tests --- .../extractor/avi/AviExtractor.java | 26 ++- .../extractor/avi/AviHeaderBox.java | 6 + .../extractor/avi/AviExtractorTest.java | 153 ++++++++++++++++++ .../extractor/avi/AviHeaderBoxTest.java | 4 +- .../exoplayer2/extractor/avi/DataHelper.java | 8 +- 5 files changed, 191 insertions(+), 6 deletions(-) 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 index 72fea3cb6c..d45914796c 100644 --- 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 @@ -351,7 +351,8 @@ public class AviExtractor implements Extractor { return RESULT_SEEK; } - private AviTrack getVideoTrack() { + @VisibleForTesting + AviTrack getVideoTrack() { for (@Nullable AviTrack aviTrack : aviTracks) { if (aviTrack != null && aviTrack.isVideo()) { return aviTrack; @@ -510,7 +511,7 @@ public class AviExtractor implements Extractor { final AviTrack aviTrack = getAviTrack(chunkId); if (aviTrack == null) { seekPosition.position = alignPosition(input.getPosition() + size); - Log.w(TAG, "Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + w("Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + " size=" + size + " moviEnd=" + moviEnd); return RESULT_SEEK; } @@ -590,6 +591,27 @@ public class AviExtractor implements Extractor { this.aviTracks = aviTracks; } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + void setAviHeader(final AviHeaderBox aviHeaderBox) { + aviHeader = aviHeaderBox; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + void setMovi(final int offset, final int end) { + moviOffset = offset; + moviEnd = end; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + AviTrack getChunkHandler() { + return chunkHandler; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + void setChunkHandler(final AviTrack aviTrack) { + chunkHandler = aviTrack; + } + private static void w(String message) { try { Log.w(TAG, message); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index 84b6557d0b..d2a21dd727 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -1,5 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; +import androidx.annotation.VisibleForTesting; import java.nio.ByteBuffer; public class AviHeaderBox extends ResidentBox { @@ -49,4 +50,9 @@ public class AviHeaderBox extends ResidentBox { // 28 - dwSuggestedBufferSize // 32 - dwWidth // 36 - dwHeight + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + void setFlags(int flags) { + byteBuffer.putInt(12, flags); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 5c731e88f3..6f1e26bd79 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -1,10 +1,14 @@ package com.google.android.exoplayer2.extractor.avi; +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.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.nio.ByteBuffer; import org.junit.Assert; @@ -296,4 +300,153 @@ public class AviExtractorTest { Assert.assertTrue(listBox.getChildren().get(0) instanceof AviHeaderBox); } + + @Test + public void findMovi_givenMoviListAndIndex() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + + ByteBuffer byteBuffer = AviExtractor.allocate(12); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(64*1024); + byteBuffer.putInt(AviExtractor.MOVI); + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build(); + aviExtractor.findMovi(input, new PositionHolder()); + Assert.assertEquals(aviExtractor.state, AviExtractor.STATE_READ_IDX1); + } + + @Test + public void findMovi_givenMoviListAndNoIndex() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final AviHeaderBox aviHeaderBox = DataHelper.createAviHeaderBox(); + aviHeaderBox.setFlags(0); + aviExtractor.setAviHeader(aviHeaderBox); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + + ByteBuffer byteBuffer = AviExtractor.allocate(12); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(64*1024); + byteBuffer.putInt(AviExtractor.MOVI); + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build(); + aviExtractor.state = AviExtractor.STATE_FIND_MOVI; + aviExtractor.read(input, new PositionHolder()); + Assert.assertEquals(aviExtractor.state, AviExtractor.STATE_READ_TRACKS); + Assert.assertTrue(fakeExtractorOutput.seekMap instanceof SeekMap.Unseekable); + } + + @Test + public void findMovi_givenJunk() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + + ByteBuffer byteBuffer = AviExtractor.allocate(12); + byteBuffer.putInt(AviExtractor.JUNK); + byteBuffer.putInt(64*1024); + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build(); + final PositionHolder positionHolder = new PositionHolder(); + aviExtractor.findMovi(input, positionHolder); + Assert.assertEquals(64 * 1024 + 8, positionHolder.position); + } + + private AviExtractor setupReadSamples() { + final AviExtractor aviExtractor = new AviExtractor(); + aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + + final AviTrack aviTrack = DataHelper.getVideoAviTrack(9); + aviExtractor.setAviTracks(new AviTrack[]{aviTrack}); + final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build(); + aviTrack.trackOutput.format(format); + + aviExtractor.state = AviExtractor.STATE_READ_SAMPLES; + aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024); + return aviExtractor; + } + + @Test + public void readSamples_givenAtEndOfInput() throws IOException { + AviExtractor aviExtractor = setupReadSamples(); + aviExtractor.setMovi(0, 0); + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final ByteBuffer byteBuffer = AviExtractor.allocate(32); + byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(24); + + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build(); + Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder())); + } + + @Test + public void readSamples_completeChunk() throws IOException { + AviExtractor aviExtractor = setupReadSamples(); + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final ByteBuffer byteBuffer = AviExtractor.allocate(32); + byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(24); + + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + .build(); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(input, new PositionHolder())); + + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; + Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length); + } + + @Test + public void readSamples_fragmentedChunk() throws IOException { + AviExtractor aviExtractor = setupReadSamples(); + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final int size = 24 + 16; + final ByteBuffer byteBuffer = AviExtractor.allocate(32); + byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(size); + + final ExtractorInput chunk0 = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + .build(); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk0, new PositionHolder())); + + final ExtractorInput chunk1 = new FakeExtractorInput.Builder().setData(new byte[16]) + .build(); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk1, new PositionHolder())); + + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; + Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); + } + + @Test + public void seek_givenPosition0() throws IOException { + final AviExtractor aviExtractor = setupReadSamples(); + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + aviExtractor.setChunkHandler(aviTrack); + aviTrack.getClock().setIndex(10); + + aviExtractor.seek(0L, 0L); + + Assert.assertNull(aviExtractor.getChunkHandler()); + Assert.assertEquals(0, aviTrack.getClock().getIndex()); + Assert.assertEquals(aviExtractor.state, AviExtractor.STATE_SEEK_START); + + + final ExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build(); + final PositionHolder positionHolder = new PositionHolder(); + Assert.assertEquals(Extractor.RESULT_SEEK, aviExtractor.read(input, positionHolder)); + Assert.assertEquals(DataHelper.MOVI_OFFSET + 4, positionHolder.position); + } + + @Test + public void seek_givenKeyFrame() throws IOException { + final AviExtractor aviExtractor = setupReadSamples(); + final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); + aviExtractor.aviSeekMap = aviSeekMap; + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final long position = DataHelper.MOVI_OFFSET + aviSeekMap.keyFrameOffsetsDiv2[1] * 2L; + aviExtractor.seek(position, 0L); + Assert.assertEquals(aviSeekMap.seekIndexes[aviTrack.id][1], aviTrack.getClock().getIndex()); + } } \ No newline at end of file diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java index 13eee629ef..e7b3a93e7b 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java @@ -8,9 +8,7 @@ public class AviHeaderBoxTest { @Test public void getters() { - final ByteBuffer byteBuffer = DataHelper.createAviHeader(); - final AviHeaderBox aviHeaderBox = new AviHeaderBox(AviHeaderBox.AVIH, - byteBuffer.capacity(), byteBuffer); + final AviHeaderBox aviHeaderBox = DataHelper.createAviHeaderBox(); Assert.assertEquals(DataHelper.VIDEO_US, aviHeaderBox.getMicroSecPerFrame()); Assert.assertTrue(aviHeaderBox.hasIndex()); Assert.assertFalse(aviHeaderBox.mustUseIndex()); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index c50b04f8e5..805655317e 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -18,6 +18,7 @@ public class DataHelper { static final int VIDEO_SIZE = 4096; static final int AUDIO_SIZE = 256; static final int AUDIO_ID = 1; + static final int MOVI_OFFSET = 4096; private static final long AUDIO_US = VIDEO_US / AUDIO_PER_VIDEO; //Base path "\ExoPlayer\library\extractor\." @@ -124,7 +125,7 @@ public class DataHelper { audioArray.add(0); audioArray.add(128); return new AviSeekMap(0, 100L, 8, keyFrameOffsetsDiv2, - new UnboundedIntArray[]{videoArray, audioArray}, 4096); + new UnboundedIntArray[]{videoArray, audioArray}, MOVI_OFFSET); } private static void putIndex(final ByteBuffer byteBuffer, int chunkId, int flags, int offset, @@ -186,4 +187,9 @@ public class DataHelper { byteBuffer.clear(); return byteBuffer; } + + public static AviHeaderBox createAviHeaderBox() { + final ByteBuffer byteBuffer = createAviHeader(); + return new AviHeaderBox(AviHeaderBox.AVIH, byteBuffer.capacity(), byteBuffer); + } } From df9e51de9d364a0a023ca1e346567faf6199dea4 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 29 Jan 2022 19:19:13 -0700 Subject: [PATCH 33/70] More AviExtractor tests --- .../android/exoplayer2/extractor/avi/Box.java | 4 +- .../extractor/avi/AviExtractorRoboTest.java | 42 +++++++++++++++++++ .../extractor/avi/AviExtractorTest.java | 33 +++++++++++---- 3 files changed, 68 insertions(+), 11 deletions(-) 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 index 94ed3d4028..2f6ea39092 100644 --- 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 @@ -12,8 +12,8 @@ public class Box { this.size = size; } - public long getSize() { - return size & AviExtractor.UINT_MASK; + public int getSize() { + return size; } public int getType() { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index 8a19cc0289..9307cdff3a 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -2,10 +2,15 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.Collections; import org.junit.Assert; import org.junit.Test; @@ -55,4 +60,41 @@ public class AviExtractorRoboTest { Assert.assertNull(aviExtractor.parseStream(streamList, 0)); } + @Test + public void readTracks_givenVideoTrack() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + + final ByteBuffer byteBuffer = DataHelper.getRiffHeader(0xdc, 0xc8); + final ByteBuffer aviHeader = DataHelper.createAviHeader(); + byteBuffer.putInt(aviHeader.capacity()); + byteBuffer.put(aviHeader); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(byteBuffer.remaining() - 4); + byteBuffer.putInt(AviExtractor.STRL); + + final StreamHeaderBox streamHeaderBox = DataHelper.getVidsStreamHeader(); + byteBuffer.putInt(StreamHeaderBox.STRH); + byteBuffer.putInt(streamHeaderBox.getSize()); + byteBuffer.put(streamHeaderBox.getByteBuffer()); + + final StreamFormatBox streamFormatBox = DataHelper.getVideoStreamFormat(); + byteBuffer.putInt(StreamFormatBox.STRF); + byteBuffer.putInt(streamFormatBox.getSize()); + byteBuffer.put(streamFormatBox.getByteBuffer()); + + aviExtractor.state = AviExtractor.STATE_READ_TRACKS; + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + build(); + final PositionHolder positionHolder = new PositionHolder(); + aviExtractor.read(input, positionHolder); + + Assert.assertEquals(AviExtractor.STATE_FIND_MOVI, aviExtractor.state); + + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + Assert.assertEquals(aviTrack.getClock().durationUs, streamHeaderBox.getDurationUs()); + } + } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 6f1e26bd79..7c8c692e2a 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -180,12 +180,26 @@ public class AviExtractorTest { final AviTrack videoTrack = DataHelper.getVideoAviTrack(secs); final AviTrack audioTrack = DataHelper.getAudioAviTrack(secs); aviExtractor.setAviTracks(new AviTrack[]{videoTrack, audioTrack}); + aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); + aviExtractor.state = AviExtractor.STATE_READ_IDX1; + aviExtractor.setMovi(DataHelper.MOVI_OFFSET, 128*1024); + final ByteBuffer idx1Box = AviExtractor.allocate(idx1.capacity() + 8); + idx1Box.putInt(AviExtractor.IDX1); + idx1Box.putInt(idx1.capacity()); + idx1.clear(); + idx1Box.put(idx1); final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder() - .setData(idx1.array()).build(); - aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); + .setData(idx1Box.array()).build(); + //aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); + final PositionHolder positionHolder = new PositionHolder(); + aviExtractor.read(fakeExtractorInput, positionHolder); + final AviSeekMap aviSeekMap = aviExtractor.aviSeekMap; assertIdx1(aviSeekMap, videoTrack, keyFrames, keyFrameRate); + + Assert.assertEquals(AviExtractor.STATE_READ_SAMPLES, aviExtractor.state); + Assert.assertEquals(DataHelper.MOVI_OFFSET + 4, positionHolder.position); } @Test @@ -353,7 +367,7 @@ public class AviExtractorTest { Assert.assertEquals(64 * 1024 + 8, positionHolder.position); } - private AviExtractor setupReadSamples() { + private AviExtractor setupVideoAviExtractor() { final AviExtractor aviExtractor = new AviExtractor(); aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); @@ -371,7 +385,7 @@ public class AviExtractorTest { @Test public void readSamples_givenAtEndOfInput() throws IOException { - AviExtractor aviExtractor = setupReadSamples(); + AviExtractor aviExtractor = setupVideoAviExtractor(); aviExtractor.setMovi(0, 0); final AviTrack aviTrack = aviExtractor.getVideoTrack(); final ByteBuffer byteBuffer = AviExtractor.allocate(32); @@ -384,7 +398,7 @@ public class AviExtractorTest { @Test public void readSamples_completeChunk() throws IOException { - AviExtractor aviExtractor = setupReadSamples(); + AviExtractor aviExtractor = setupVideoAviExtractor(); final AviTrack aviTrack = aviExtractor.getVideoTrack(); final ByteBuffer byteBuffer = AviExtractor.allocate(32); byteBuffer.putInt(aviTrack.chunkId); @@ -400,7 +414,7 @@ public class AviExtractorTest { @Test public void readSamples_fragmentedChunk() throws IOException { - AviExtractor aviExtractor = setupReadSamples(); + AviExtractor aviExtractor = setupVideoAviExtractor(); final AviTrack aviTrack = aviExtractor.getVideoTrack(); final int size = 24 + 16; final ByteBuffer byteBuffer = AviExtractor.allocate(32); @@ -421,7 +435,7 @@ public class AviExtractorTest { @Test public void seek_givenPosition0() throws IOException { - final AviExtractor aviExtractor = setupReadSamples(); + final AviExtractor aviExtractor = setupVideoAviExtractor(); final AviTrack aviTrack = aviExtractor.getVideoTrack(); aviExtractor.setChunkHandler(aviTrack); aviTrack.getClock().setIndex(10); @@ -440,8 +454,8 @@ public class AviExtractorTest { } @Test - public void seek_givenKeyFrame() throws IOException { - final AviExtractor aviExtractor = setupReadSamples(); + public void seek_givenKeyFrame() { + final AviExtractor aviExtractor = setupVideoAviExtractor(); final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); aviExtractor.aviSeekMap = aviSeekMap; final AviTrack aviTrack = aviExtractor.getVideoTrack(); @@ -449,4 +463,5 @@ public class AviExtractorTest { aviExtractor.seek(position, 0L); Assert.assertEquals(aviSeekMap.seekIndexes[aviTrack.id][1], aviTrack.getClock().getIndex()); } + } \ No newline at end of file From bec8b44ba55b922f5b650f65b99992b8697e32aa Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 30 Jan 2022 13:29:07 -0700 Subject: [PATCH 34/70] BitmapFactoryVideoRenderer Tests --- library/core/build.gradle | 6 +- .../video/BitmapFactoryVideoRenderer.java | 200 +++++++++------ .../video/BitmapFactoryVideoRendererTest.java | 237 ++++++++++++++++++ .../exoplayer2/video/FakeEventListener.java | 64 +++++ .../video/ShadowSurfaceExtended.java | 43 ++++ .../exoplayer2/ui/CanvasSubtitleOutput.java | 3 +- .../test/assets/media/jpeg/image-320-240.jpg | Bin 0 -> 36039 bytes 7 files changed, 474 insertions(+), 79 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java create mode 100644 testdata/src/test/assets/media/jpeg/image-320-240.jpg diff --git a/library/core/build.gradle b/library/core/build.gradle index 5bf70ad151..3196735378 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -21,7 +21,11 @@ android { testInstrumentationRunnerArguments clearPackageData: 'true' multiDexEnabled true } - + testOptions{ + unitTests.all { + jvmArgs '-noverify' + } + } buildTypes { debug { testCoverageEnabled = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java index 08c2e51e49..31dba8ef3e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -3,13 +3,15 @@ package com.google.android.exoplayer2.video; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; -import android.graphics.Point; import android.graphics.Rect; import android.os.Handler; import android.os.SystemClock; import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.arch.core.util.Function; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -23,11 +25,16 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; public class BitmapFactoryVideoRenderer extends BaseRenderer { - private static final String TAG = "BitmapFactoryRenderer"; + static final String TAG = "BitmapFactoryRenderer"; + + //Sleep Reasons + static final String STREAM_END = "Stream End"; + static final String STREAM_EMPTY = "Stream Empty"; + static final String RENDER_WAIT = "Render Wait"; + private static int threadId; private final Rect rect = new Rect(); - private final Point lastSurface = new Point(); private final RenderRunnable renderRunnable = new RenderRunnable(); final VideoRendererEventListener.EventDispatcher eventDispatcher; @@ -60,7 +67,16 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { throws ExoPlaybackException { decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); - thread.start(); + if (mayRenderStartOfStream) { + thread.start(); + } + } + + @Override + protected void onStarted() throws ExoPlaybackException { + if (thread.getState() == Thread.State.NEW) { + thread.start(); + } } @Override @@ -74,20 +90,12 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { } } - private void onFormatChanged(@NonNull FormatHolder formatHolder) { - @Nullable final Format format = formatHolder.format; - if (format != null) { - frameUs = (long)(1_000_000L / format.frameRate); - eventDispatcher.inputFormatChanged(format, null); - } - } - @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { //Log.d(TAG, "Render: us=" + positionUs); - synchronized (eventDispatcher) { + synchronized (renderRunnable) { currentTimeUs = positionUs; - eventDispatcher.notify(); + renderRunnable.notify(); } } @@ -127,7 +135,17 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); } - void renderBitmap(final Bitmap bitmap) { + @WorkerThread + private void onFormatChanged(@NonNull FormatHolder formatHolder) { + @Nullable final Format format = formatHolder.format; + if (format != null) { + frameUs = (long)(1_000_000L / format.frameRate); + eventDispatcher.inputFormatChanged(format, null); + } + } + + @WorkerThread + void renderBitmap(@NonNull final Bitmap bitmap) { @Nullable final Surface surface = this.surface; if (surface == null) { @@ -136,30 +154,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); final Canvas canvas = surface.lockCanvas(null); - final Rect clipBounds = canvas.getClipBounds(); - final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); - final boolean videoSizeChanged; - if (videoSize.equals(lastVideoSize)) { - videoSizeChanged = false; - } else { - lastVideoSize = videoSize; - eventDispatcher.videoSizeChanged(videoSize); - videoSizeChanged = true; - } - if (lastSurface.x != clipBounds.width() || lastSurface.y != clipBounds.height() || - videoSizeChanged) { - lastSurface.x = clipBounds.width(); - lastSurface.y = clipBounds.height(); - final float scaleX = lastSurface.x / (float)videoSize.width; - final float scaleY = lastSurface.y / (float)videoSize.height; - final float scale = Math.min(scaleX, scaleY); - final float width = videoSize.width * scale; - final float height = videoSize.height * scale; - final int x = (int)(lastSurface.x - width) / 2; - final int y = (int)(lastSurface.y - height) / 2; - rect.set(x, y, x + (int)width, y + (int) height); - } - canvas.drawBitmap(bitmap, null, rect, null); + renderBitmap(bitmap, canvas); surface.unlockCanvasAndPost(canvas); @Nullable @@ -173,12 +168,27 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { } } - class RenderRunnable implements Runnable { + @WorkerThread + @VisibleForTesting + void renderBitmap(Bitmap bitmap, Canvas canvas) { + final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); + if (!videoSize.equals(lastVideoSize)) { + lastVideoSize = videoSize; + eventDispatcher.videoSizeChanged(videoSize); + } + rect.set(0,0,canvas.getWidth(), canvas.getHeight()); + canvas.drawBitmap(bitmap, null, rect, null); + } + + class RenderRunnable implements Runnable, Function { final DecoderInputBuffer decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); private volatile boolean running = true; + @VisibleForTesting + Function sleepFunction = this; + void stop() { running = false; thread.interrupt(); @@ -197,7 +207,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.arrayOffset() + byteBuffer.position()); if (bitmap == null) { - eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); + throw new NullPointerException("Decode bytes failed"); } else { return bitmap; } @@ -212,18 +222,21 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { * * @return true if interrupted */ - private boolean sleep() { - synchronized (eventDispatcher) { - try { - eventDispatcher.wait(); - return false; - } catch (InterruptedException e) { - //If we are interrupted, treat as a cancel - return true; - } + public synchronized Boolean apply(String why) { + try { + wait(); + return false; + } catch (InterruptedException e) { + //If we are interrupted, treat as a cancel + return true; } } + private boolean sleep(String why) { + return sleepFunction.apply(why); + } + + @WorkerThread public void run() { final FormatHolder formatHolder = getFormatHolder(); long start = SystemClock.uptimeMillis(); @@ -232,40 +245,73 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { decoderInputBuffer.clear(); final int result = readSource(formatHolder, decoderInputBuffer, formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0); - if (result == C.RESULT_BUFFER_READ) { - if (decoderInputBuffer.isEndOfStream()) { - //Wait for shutdown or stream to be changed - sleep(); - continue; - } - final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; - //If we are more than 1/2 a frame behind, skip the next frame - if (leadUs < -frameUs / 2) { - eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start); + switch (result) { + case C.RESULT_BUFFER_READ: { + if (decoderInputBuffer.isEndOfStream()) { + //Wait for shutdown or stream to be changed + sleep(STREAM_END); + continue; + } + final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; + //If we are more than 1/2 a frame behind, skip the next frame + if (leadUs < -frameUs / 2) { + eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start); + start = SystemClock.uptimeMillis(); + continue; + } start = SystemClock.uptimeMillis(); - continue; - } - start = SystemClock.uptimeMillis(); - @Nullable - final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer); - if (bitmap == null) { - continue; - } - while (currentTimeUs < decoderInputBuffer.timeUs) { - //Log.d(TAG, "Sleep: us=" + currentTimeUs); - if (sleep()) { - //Sleep was interrupted, discard Bitmap - continue main; + @Nullable + final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer); + if (bitmap == null) { + continue; + } + while (currentTimeUs < decoderInputBuffer.timeUs) { + //Log.d(TAG, "Sleep: us=" + currentTimeUs); + if (sleep(RENDER_WAIT)) { + //Sleep was interrupted, discard Bitmap + continue main; + } + } + if (running) { + renderBitmap(bitmap); } } - if (running) { - renderBitmap(bitmap); - } - } else if (result == C.RESULT_FORMAT_READ) { + break; + case C.RESULT_FORMAT_READ: onFormatChanged(formatHolder); + break; + case C.RESULT_NOTHING_READ: + sleep(STREAM_EMPTY); + break; } } } } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + Rect getRect() { + return rect; + } + + @Nullable + @VisibleForTesting + DecoderCounters getDecoderCounters() { + return decoderCounters; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + Thread getThread() { + return thread; + } + + @Nullable + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + Surface getSurface() { + return surface; + } + + RenderRunnable getRenderRunnable() { + return renderRunnable; + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java new file mode 100644 index 0000000000..914f59786b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java @@ -0,0 +1,237 @@ +package com.google.android.exoplayer2.video; + +import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; +import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; +import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.sample; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; +import androidx.arch.core.util.Function; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowBitmapFactory; +import org.robolectric.shadows.ShadowLooper; + +@RunWith(AndroidJUnit4.class) +@Config(shadows = {ShadowSurfaceExtended.class}) +public class BitmapFactoryVideoRendererTest { + private final static Format FORMAT_MJPEG = new Format.Builder(). + setSampleMimeType(MimeTypes.VIDEO_MJPEG). + setWidth(320).setHeight(240). + setFrameRate(15f).build(); + + FakeEventListener fakeEventListener = new FakeEventListener(); + BitmapFactoryVideoRenderer bitmapFactoryVideoRenderer; + + @Before + public void before() { + fakeEventListener = new FakeEventListener(); + final Handler handler = new Handler(Looper.getMainLooper()); + bitmapFactoryVideoRenderer = new BitmapFactoryVideoRenderer(handler, fakeEventListener); + } + + @After + public void after() { + //Kill the Thread + bitmapFactoryVideoRenderer.onDisabled(); + } + + @Test + public void getName() { + Assert.assertEquals(BitmapFactoryVideoRenderer.TAG, bitmapFactoryVideoRenderer.getName()); + } + + @Test + public void onEnabled_givenMayRenderStartOfStream() throws PlaybackException { + bitmapFactoryVideoRenderer.onEnabled(false, true); + ShadowLooper.idleMainLooper(); + Assert.assertNotNull(bitmapFactoryVideoRenderer.getDecoderCounters()); + Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState()); + Assert.assertTrue(fakeEventListener.isVideoEnabled()); + } + + @Test + public void onStarted_givenThreadNotStarted() throws PlaybackException { + bitmapFactoryVideoRenderer.onStarted(); + ShadowLooper.idleMainLooper(); + Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState()); + } + + @Test + public void onDisabled_givenOnEnabled() throws PlaybackException, InterruptedException { + onEnabled_givenMayRenderStartOfStream(); + bitmapFactoryVideoRenderer.onDisabled(); + ShadowLooper.idleMainLooper(); + Assert.assertFalse(fakeEventListener.isVideoEnabled()); + //Ensure Thread is shutdown + bitmapFactoryVideoRenderer.getThread().join(500L); + Assert.assertTrue(bitmapFactoryVideoRenderer.isEnded()); + } + + private FakeSampleStream getSampleStream() throws IOException { + final Context context = ApplicationProvider.getApplicationContext(); + final byte[] bytes = TestUtil.getByteArray(context, "media/jpeg/image-320-240.jpg"); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ FORMAT_MJPEG, + ImmutableList.of( + sample(0L, C.BUFFER_FLAG_KEY_FRAME, bytes), + END_OF_STREAM_ITEM)); + return fakeSampleStream; + } + + private Surface setSurface() throws ExoPlaybackException { + final Surface surface = ShadowSurfaceExtended.newInstance(); + final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface); + shadowSurfaceExtended.setSize(1080, 1920); + bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + return surface; + } + + @Test + public void handleMessage_givenSurface() throws ExoPlaybackException { + final Surface surface = setSurface(); + Assert.assertSame(surface, bitmapFactoryVideoRenderer.getSurface()); + bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, null); + Assert.assertNull(bitmapFactoryVideoRenderer.getSurface()); + } + + @Test + public void isReady_givenSurface() throws ExoPlaybackException { + Assert.assertFalse(bitmapFactoryVideoRenderer.isReady()); + setSurface(); + Assert.assertTrue(bitmapFactoryVideoRenderer.isReady()); + } + + @Test + public void render_givenJpegAndSurface() throws IOException, ExoPlaybackException { + final Surface surface = setSurface(); + final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface); + + FakeSampleStream fakeSampleStream = getSampleStream(); + fakeSampleStream.writeData(0L); + bitmapFactoryVideoRenderer.enable(RendererConfiguration.DEFAULT, new Format[]{FORMAT_MJPEG}, + fakeSampleStream, 0L, false, true, 0L, 0L); + bitmapFactoryVideoRenderer.render(0L, 0L); + // This test actually decodes the JPEG (very cool!), + // May need to bump up timers for slow machines + Assert.assertTrue(shadowSurfaceExtended.waitForPost(500L)); + } + + @Test + public void supportsFormat_givenMjpegFormat() throws ExoPlaybackException{ + Assert.assertEquals(C.FORMAT_HANDLED, + bitmapFactoryVideoRenderer.supportsFormat(FORMAT_MJPEG) & C.FORMAT_HANDLED); + } + + @Test + public void supportsFormat_givenMp4vFormat() throws ExoPlaybackException{ + final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build(); + Assert.assertEquals(0, + bitmapFactoryVideoRenderer.supportsFormat(format) & C.FORMAT_HANDLED); + } + + @Test + public void renderBitmap_given4by3BitmapAnd16by9Canvas() { + final Bitmap bitmap = Bitmap.createBitmap(FORMAT_MJPEG.width, FORMAT_MJPEG.height, Bitmap.Config.ARGB_8888); + final Bitmap canvasBitmap = Bitmap.createBitmap(1080, 1920, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(canvasBitmap); + bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas); + ShadowLooper.idleMainLooper(); + + final Rect rect = bitmapFactoryVideoRenderer.getRect(); + Assert.assertEquals(canvas.getWidth(), rect.width()); + Assert.assertEquals(canvas.getHeight(), rect.height()); + final VideoSize videoSize = fakeEventListener.videoSize; + Assert.assertEquals(bitmap.getWidth(), videoSize.width); + + bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas); + ShadowLooper.idleMainLooper(); + Assert.assertSame(videoSize, fakeEventListener.videoSize); + } + + @Test + public void RenderRunnable_run_givenLateFrame() throws IOException, ExoPlaybackException { + final Function sleep = why -> {throw new RuntimeException(why);}; + + FakeSampleStream fakeSampleStream = getSampleStream(); + fakeSampleStream.writeData(0L); + //Don't enable so the Thread is not running + bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L); + BitmapFactoryVideoRenderer.RenderRunnable renderRunnable = + bitmapFactoryVideoRenderer.getRenderRunnable(); + renderRunnable.sleepFunction = sleep; + bitmapFactoryVideoRenderer.render(1_000_000L, 0L); + try { + renderRunnable.run(); + } catch (RuntimeException e) { + Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage()); + } + ShadowLooper.idleMainLooper(); + Assert.assertEquals(1, fakeEventListener.getDroppedFrames()); + } + + @Test + public void RenderRunnable_run_givenBadJpeg() throws IOException, ExoPlaybackException { + final Function sleep = why -> {throw new RuntimeException(why);}; + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ FORMAT_MJPEG, + ImmutableList.of( + oneByteSample(0L, C.BUFFER_FLAG_KEY_FRAME), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(0L); + + //Don't enable so the Thread is not running + bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L); + BitmapFactoryVideoRenderer.RenderRunnable renderRunnable = + bitmapFactoryVideoRenderer.getRenderRunnable(); + renderRunnable.sleepFunction = sleep; + bitmapFactoryVideoRenderer.render(0L, 0L); + // There is a bug in Robolectric where it doesn't handle null images, + // so we won't get our Exception + ShadowBitmapFactory.setAllowInvalidImageData(false); + try { + renderRunnable.run(); + } catch (RuntimeException e) { + Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage()); + } + ShadowLooper.idleMainLooper(); + Assert.assertTrue(fakeEventListener.getVideoCodecError() instanceof NullPointerException); + + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java b/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java new file mode 100644 index 0000000000..ebc70e3f94 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java @@ -0,0 +1,64 @@ +package com.google.android.exoplayer2.video; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.decoder.DecoderCounters; + +public class FakeEventListener implements VideoRendererEventListener { + @Nullable + VideoSize videoSize; + @Nullable + DecoderCounters decoderCounters; + + private long firstFrameRenderMs = Long.MIN_VALUE; + + private int droppedFrames; + + private Exception videoCodecError; + + @Override + public void onVideoSizeChanged(VideoSize videoSize) { + this.videoSize = videoSize; + } + + public boolean isVideoEnabled() { + return decoderCounters != null; + } + + @Override + public void onVideoEnabled(DecoderCounters counters) { + decoderCounters = counters; + } + + @Override + public void onVideoDisabled(DecoderCounters counters) { + decoderCounters = null; + } + + public long getFirstFrameRenderMs() { + return firstFrameRenderMs; + } + + @Override + public void onRenderedFirstFrame(Object output, long renderTimeMs) { + firstFrameRenderMs = renderTimeMs; + } + + public int getDroppedFrames() { + return droppedFrames; + } + + @Override + public void onDroppedFrames(int count, long elapsedMs) { + droppedFrames+=count; + } + + public Exception getVideoCodecError() { + return videoCodecError; + } + + @Override + public void onVideoCodecError(Exception videoCodecError) { + this.videoCodecError = videoCodecError; + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java b/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java new file mode 100644 index 0000000000..c107832310 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java @@ -0,0 +1,43 @@ +package com.google.android.exoplayer2.video; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.view.Surface; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import org.robolectric.annotation.Implements; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowSurface; + +@Implements(Surface.class) +public class ShadowSurfaceExtended extends ShadowSurface { + private final Semaphore postSemaphore = new Semaphore(0); + private int width; + private int height; + + public static Surface newInstance() { + return Shadow.newInstanceOf(Surface.class); + } + + public void setSize(final int width, final int height) { + this.width = width; + this.height = height; + } + + public Canvas lockCanvas(Rect canvas) { + return new Canvas(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)); + } + + public void unlockCanvasAndPost(Canvas canvas) { + postSemaphore.release(); + } + + public boolean waitForPost(long millis) { + try { + return postSemaphore.tryAcquire(millis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java index f0826ce35b..3d92566d3c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java @@ -32,7 +32,8 @@ import java.util.List; * A {@link SubtitleView.Output} that uses Android's native layout framework via {@link * SubtitlePainter}. */ -/* package */ final class CanvasSubtitleOutput extends View implements SubtitleView.Output { +/* package */ final class +CanvasSubtitleOutput extends View implements SubtitleView.Output { private final List painters; diff --git a/testdata/src/test/assets/media/jpeg/image-320-240.jpg b/testdata/src/test/assets/media/jpeg/image-320-240.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d1796c66fdb1869e074b43d4b9964d8257709452 GIT binary patch literal 36039 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�oMa*=h2irbTvr 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 Date: Sun, 30 Jan 2022 13:53:20 -0700 Subject: [PATCH 35/70] Clean up tests --- .../exoplayer2/extractor/avi/VideoFormat.java | 17 ++++++++ .../extractor/avi/AviExtractorRoboTest.java | 4 +- .../exoplayer2/extractor/avi/DataHelper.java | 41 ++++++------------ .../extractor/avi/Mp4vChunkPeekerTest.java | 10 ++++- .../extractor/avi/VideoFormatTest.java | 6 ++- .../avi/h264_stream_format.dump | Bin 40 -> 0 bytes 6 files changed, 45 insertions(+), 33 deletions(-) delete mode 100644 testdata/src/test/assets/extractordumps/avi/h264_stream_format.dump 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 index af25396002..28d0cbeb11 100644 --- 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 @@ -1,5 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; import java.util.HashMap; @@ -55,4 +56,20 @@ public class VideoFormat { public String getMimeType() { return STREAM_MAP.get(getCompression()); } + + @VisibleForTesting + public void setWidth(final int width) { + byteBuffer.putInt(4, width); + } + + @VisibleForTesting + public void setHeight(final int height) { + byteBuffer.putInt(8, height); + } + + @VisibleForTesting + public void setCompression(final int compression) { + byteBuffer.putInt(16, compression); + } + } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index 9307cdff3a..694e254530 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -20,14 +20,14 @@ import org.junit.runner.RunWith; public class AviExtractorRoboTest { @Test - public void parseStream_givenH264StreamList() throws IOException { + public void parseStream_givenXvidStreamList() throws IOException { final AviExtractor aviExtractor = new AviExtractor(); final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); aviExtractor.init(fakeExtractorOutput); final ListBox streamList = DataHelper.getVideoStreamList(); aviExtractor.parseStream(streamList, 0); FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); - Assert.assertEquals(MimeTypes.VIDEO_H264, trackOutput.lastFormat.sampleMimeType); + Assert.assertEquals(MimeTypes.VIDEO_MP4V, trackOutput.lastFormat.sampleMimeType); } @Test diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 805655317e..c1da51da3b 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -1,10 +1,10 @@ package com.google.android.exoplayer2.extractor.avi; +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; -import java.io.File; -import java.io.FileInputStream; +import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -19,22 +19,6 @@ public class DataHelper { static final int AUDIO_SIZE = 256; static final int AUDIO_ID = 1; static final int MOVI_OFFSET = 4096; - private static final long AUDIO_US = VIDEO_US / AUDIO_PER_VIDEO; - - //Base path "\ExoPlayer\library\extractor\." - private static final File RELATIVE_PATH = new File("../../testdata/src/test/assets/extractordumps/avi/"); - public static FakeExtractorInput getInput(final String fileName) throws IOException { - return new FakeExtractorInput.Builder().setData(getBytes(fileName)).build(); - } - - public static byte[] getBytes(final String fileName) throws IOException { - final File file = new File(RELATIVE_PATH, fileName); - try (FileInputStream in = new FileInputStream(file)) { - final byte[] buffer = new byte[in.available()]; - in.read(buffer); - return buffer; - } - } public static StreamHeaderBox getStreamHeader(int type, int scale, int rate, int length) { final ByteBuffer byteBuffer = AviExtractor.allocate(0x40); @@ -55,20 +39,23 @@ public class DataHelper { } public static StreamFormatBox getAacStreamFormat() throws IOException { - final byte[] buffer = getBytes("aac_stream_format.dump"); + final Context context = ApplicationProvider.getApplicationContext(); + final byte[] buffer = TestUtil.getByteArray(context,"extractordumps/avi/aac_stream_format.dump"); final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer); } - public static StreamFormatBox getVideoStreamFormat() throws IOException { - final byte[] buffer = getBytes("h264_stream_format.dump"); - final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer); + public static StreamFormatBox getVideoStreamFormat() { + final ByteBuffer byteBuffer = AviExtractor.allocate(40); + final VideoFormat videoFormat = new VideoFormat(byteBuffer); + videoFormat.setWidth(720); + videoFormat.setHeight(480); + videoFormat.setCompression(VideoFormat.XVID); + return new StreamFormatBox(StreamFormatBox.STRF, byteBuffer.capacity(), byteBuffer); } - public static ListBox getVideoStreamList() throws IOException { + public static ListBox getVideoStreamList() { final StreamHeaderBox streamHeaderBox = getVidsStreamHeader(); final StreamFormatBox streamFormatBox = getVideoStreamFormat(); final ArrayList list = new ArrayList<>(2); @@ -162,8 +149,6 @@ public class DataHelper { /** * Get the RIFF header up to AVI Header - * @param bufferSize - * @return */ public static ByteBuffer getRiffHeader(int bufferSize, int headerListSize) { ByteBuffer byteBuffer = AviExtractor.allocate(bufferSize); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java index baa6277ad1..9a3205b90f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java @@ -1,13 +1,19 @@ package com.google.android.exoplayer2.extractor.avi; +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import java.nio.ByteBuffer; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +@RunWith(AndroidJUnit4.class) public class Mp4vChunkPeekerTest { private ByteBuffer makeSequence() { @@ -30,8 +36,10 @@ public class Mp4vChunkPeekerTest { public void peek_givenAspectRatio() throws IOException { final FakeTrackOutput fakeTrackOutput = new FakeTrackOutput(false); final Format.Builder formatBuilder = new Format.Builder(); + final Context context = ApplicationProvider.getApplicationContext(); + final byte[] bytes = TestUtil.getByteArray(context, "extractordumps/avi/mp4v_sequence.dump"); + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build(); final Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(formatBuilder, fakeTrackOutput); - final FakeExtractorInput input = DataHelper.getInput("mp4v_sequence.dump"); mp4vChunkPeeker.peek(input, (int) input.getLength()); Assert.assertEquals(1.2121212, mp4vChunkPeeker.pixelWidthHeightRatio, 0.01); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java index 7f4d6d5d08..53b9b4c6ad 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java @@ -1,5 +1,6 @@ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import org.junit.Assert; import org.junit.Test; @@ -9,7 +10,8 @@ public class VideoFormatTest { public void getters_givenVideoStreamFormat() throws IOException { final StreamFormatBox streamFormatBox = DataHelper.getVideoStreamFormat(); final VideoFormat videoFormat = streamFormatBox.getVideoFormat(); - Assert.assertEquals(712, videoFormat.getWidth()); - Assert.assertEquals(464, videoFormat.getHeight()); + Assert.assertEquals(720, videoFormat.getWidth()); + Assert.assertEquals(480, videoFormat.getHeight()); + Assert.assertEquals(MimeTypes.VIDEO_MP4V, videoFormat.getMimeType()); } } diff --git a/testdata/src/test/assets/extractordumps/avi/h264_stream_format.dump b/testdata/src/test/assets/extractordumps/avi/h264_stream_format.dump deleted file mode 100644 index b1a99e6525f48f797eafbf427ba579e523a18314..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40 hcmdO3U|=}G#K3TYk%57cL4v`<$jqcco*yWR3IJgG18)ET From 5e7679df534841e80ae655f0ac4311044474e132 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 30 Jan 2022 14:12:08 -0700 Subject: [PATCH 36/70] Fix bug with null in aviTracks, add FourCC FMP4 --- .../android/exoplayer2/extractor/avi/AviExtractor.java | 5 +++-- .../android/exoplayer2/extractor/avi/VideoFormat.java | 2 +- .../android/exoplayer2/extractor/avi/AviExtractorTest.java | 7 +++++++ 3 files changed, 11 insertions(+), 3 deletions(-) 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 index d45914796c..ec7ec42864 100644 --- 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 @@ -471,9 +471,10 @@ public class AviExtractor implements Extractor { } @Nullable - private AviTrack getAviTrack(int chunkId) { + @VisibleForTesting + AviTrack getAviTrack(int chunkId) { for (AviTrack aviTrack : aviTracks) { - if (aviTrack.handlesChunkId(chunkId)) { + if (aviTrack != null && aviTrack.handlesChunkId(chunkId)) { return aviTrack; } } 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 index 28d0cbeb11..99acc62f57 100644 --- 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 @@ -28,7 +28,7 @@ public class VideoFormat { STREAM_MAP.put(XVID, mimeType); STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); - + STREAM_MAP.put('F' | ('M' << 8) | ('P' << 16) | ('4' << 24), mimeType); STREAM_MAP.put('M' | ('J' << 8) | ('P' << 16) | ('G' << 24), MimeTypes.VIDEO_MJPEG); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 7c8c692e2a..0a2fac3603 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -464,4 +464,11 @@ public class AviExtractorTest { Assert.assertEquals(aviSeekMap.seekIndexes[aviTrack.id][1], aviTrack.getClock().getIndex()); } + @Test + public void getAviTrack_givenListWithNull() { + final AviExtractor aviExtractor = new AviExtractor(); + final AviTrack aviTrack = DataHelper.getAudioAviTrack(9); + aviExtractor.setAviTracks(new AviTrack[]{null, aviTrack}); + Assert.assertSame(aviTrack, aviExtractor.getAviTrack(aviTrack.chunkId)); + } } \ No newline at end of file From bf1a15652dc1cc5df2fc237381b9ac24bf4ff039 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 30 Jan 2022 21:05:33 -0700 Subject: [PATCH 37/70] Added support for AVC picOrderCountType = 2 --- .../extractor/avi/AvcChunkPeeker.java | 21 +++++--- .../extractor/avi/AviExtractor.java | 54 ++++++++++--------- .../exoplayer2/extractor/avi/AviTrack.java | 11 ++-- .../extractor/avi/PicCountClock.java | 13 +++-- .../extractor/avi/PicCountClockTest.java | 6 +-- 5 files changed, 62 insertions(+), 43 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java index 0fa888a266..950823da2a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java @@ -18,6 +18,7 @@ public class AvcChunkPeeker extends NalChunkPeeker { private static final int NAL_TYPE_SEI = 6; private static final int NAL_TYPE_SPS = 7; private static final int NAL_TYPE_PPS = 8; + private static final int NAL_TYPE_AUD = 9; private final PicCountClock picCountClock; private final Format.Builder formatBuilder; @@ -26,15 +27,14 @@ public class AvcChunkPeeker extends NalChunkPeeker { private float pixelWidthHeightRatio = 1f; private NalUnitUtil.SpsData spsData; - public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, long durationUs, - int length) { + public AvcChunkPeeker(Format.Builder formatBuilder, TrackOutput trackOutput, LinearClock clock) { super(16); this.formatBuilder = formatBuilder; this.trackOutput = trackOutput; - picCountClock = new PicCountClock(durationUs, length); + picCountClock = new PicCountClock(clock.durationUs, clock.length); } - public PicCountClock getPicCountClock() { + public LinearClock getClock() { return picCountClock; } @@ -58,7 +58,7 @@ public class AvcChunkPeeker extends NalChunkPeeker { if (spsData.separateColorPlaneFlag) { in.skipBits(2); //colour_plane_id } - in.readBits(spsData.frameNumLength); //frame_num + final int frameNum = in.readBits(spsData.frameNumLength); //frame_num if (!spsData.frameMbsOnlyFlag) { boolean field_pic_flag = in.readBit(); // field_pic_flag if (field_pic_flag) { @@ -71,6 +71,9 @@ public class AvcChunkPeeker extends NalChunkPeeker { //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); picCountClock.setPicCount(picOrderCountLsb); return; + } else if (spsData.picOrderCountType == 2) { + picCountClock.setPicCount(frameNum); + return; } picCountClock.setIndex(picCountClock.getIndex()); } @@ -80,7 +83,12 @@ public class AvcChunkPeeker extends NalChunkPeeker { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); - picCountClock.setMaxPicCount(1 << (spsData.picOrderCntLsbLength)); + if (spsData.picOrderCountType == 0) { + picCountClock.setMaxPicCount(1 << spsData.picOrderCntLsbLength, 2); + } else if (spsData.picOrderCountType == 2) { + //Plus one because we double the frame number + picCountClock.setMaxPicCount(1 << spsData.frameNumLength, 1); + } if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { pixelWidthHeightRatio = spsData.pixelWidthHeightRatio; formatBuilder.setPixelWidthHeightRatio(pixelWidthHeightRatio); @@ -103,6 +111,7 @@ public class AvcChunkPeeker extends NalChunkPeeker { case NAL_TYPE_IRD: picCountClock.syncIndexes(); return; + case NAL_TYPE_AUD: case NAL_TYPE_SEI: case NAL_TYPE_PPS: { nalTypeOffset = seekNextNal(input, nalTypeOffset); 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 index ec7ec42864..a19a4178df 100644 --- 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 @@ -255,15 +255,14 @@ public class AviExtractor implements Extractor { builder.setFrameRate(streamHeader.getFrameRate()); builder.setSampleMimeType(mimeType); + final LinearClock clock = new LinearClock(durationUs, length); + aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, clock, trackOutput); + if (MimeTypes.VIDEO_H264.equals(mimeType)) { - final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, durationUs, - length); - aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, avcChunkPeeker.getPicCountClock(), - trackOutput); + final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, clock); + aviTrack.setClock(avcChunkPeeker.getClock()); aviTrack.setChunkPeeker(avcChunkPeeker); } else { - aviTrack = new AviTrack(streamId, C.TRACK_TYPE_VIDEO, - new LinearClock(durationUs, length), trackOutput); if (MimeTypes.VIDEO_MP4V.equals(mimeType)) { aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput)); } @@ -361,23 +360,31 @@ public class AviExtractor implements Extractor { return null; } - void updateAudioTiming(final int[] keyFrameCounts, final long videoDuration) { + void fixTimings(final int[] keyFrameCounts, final long videoDuration) { for (final AviTrack aviTrack : aviTracks) { - if (aviTrack != null && aviTrack.isAudio()) { - final long durationUs = aviTrack.getClock().durationUs; - i("Audio #" + aviTrack.id + " chunks: " + aviTrack.chunks + " us=" + durationUs + - " size=" + aviTrack.size); - final LinearClock linearClock = aviTrack.getClock(); - //If the audio track duration is off from the video by >5 % recalc using video - if ((durationUs - videoDuration) / (float)videoDuration > .05f) { - w("Audio #" + aviTrack.id + " duration is off using videoDuration"); - linearClock.setDuration(videoDuration); - } - linearClock.setLength(aviTrack.chunks); - if (aviTrack.chunks != keyFrameCounts[aviTrack.id]) { - w("Audio is not all key frames chunks=" + aviTrack.chunks + " keyFrames=" + - keyFrameCounts[aviTrack.id]); - } + if (aviTrack != null) { + if (aviTrack.isAudio()) { + final long durationUs = aviTrack.getClock().durationUs; + i("Audio #" + aviTrack.id + " chunks: " + aviTrack.chunks + " us=" + durationUs + + " size=" + aviTrack.size); + final LinearClock linearClock = aviTrack.getClock(); + //If the audio track duration is off from the video by >5 % recalc using video + if ((durationUs - videoDuration) / (float)videoDuration > .05f) { + w("Audio #" + aviTrack.id + " duration is off using videoDuration"); + linearClock.setDuration(videoDuration); + } + linearClock.setLength(aviTrack.chunks); + if (aviTrack.chunks != keyFrameCounts[aviTrack.id]) { + w("Audio is not all key frames chunks=" + aviTrack.chunks + " keyFrames=" + + keyFrameCounts[aviTrack.id]); + } + } /* else if (aviTrack.isVideo()) { + final LinearClock clock = aviTrack.getClock(); + if (clock.length != aviTrack.chunks) { + w("Video #" + aviTrack.id + " chunks != length changing FPS"); + clock.setLength(aviTrack.chunks); + } + }*/ } } } @@ -464,8 +471,7 @@ public class AviExtractor implements Extractor { i("Video chunks=" + videoTrack.chunks + " us=" + seekMap.getDurationUs()); - //Needs to be called after the duration is updated - updateAudioTiming(keyFrameCounts, durationUs); + fixTimings(keyFrameCounts, durationUs); setSeekMap(seekMap); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index 2a321f8289..c507604164 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -20,7 +20,7 @@ public class AviTrack { final @C.TrackType int trackType; @NonNull - final LinearClock clock; + LinearClock clock; @NonNull final TrackOutput trackOutput; @@ -56,7 +56,7 @@ public class AviTrack { return getChunkIdLower(id) | ('w' << 16) | ('b' << 24); } - AviTrack(int id, final @C.TrackType int trackType, @NonNull LinearClock clock, + AviTrack(int id, @C.TrackType int trackType, @NonNull LinearClock clock, @NonNull TrackOutput trackOutput) { this.id = id; this.clock = clock; @@ -77,10 +77,15 @@ public class AviTrack { return this.chunkId == chunkId || chunkIdAlt == chunkId; } + @NonNull public LinearClock getClock() { return clock; } + public void setClock(@NonNull LinearClock clock) { + this.clock = clock; + } + public void setChunkPeeker(ChunkPeeker chunkPeeker) { this.chunkPeeker = chunkPeeker; } @@ -144,7 +149,7 @@ public class AviTrack { trackOutput.sampleMetadata( clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); final LinearClock clock = getClock(); -// Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); + Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); clock.advance(); } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java index 7e515f786f..050bdf59f9 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -4,14 +4,13 @@ package com.google.android.exoplayer2.extractor.avi; * Properly calculates the frame time for H264 frames using PicCount */ public class PicCountClock extends LinearClock { - //I believe this is 2 because there is a bottom pic order and a top pic order - private static final int STEP = 2; //The frame as a calculated from the picCount private int picIndex; private int lastPicCount; //Largest picFrame, used when we hit an I frame private int maxPicIndex =-1; private int maxPicCount; + private int step = 2; private int posHalf; private int negHalf; @@ -19,15 +18,15 @@ public class PicCountClock extends LinearClock { super(durationUs, length); } - public void setMaxPicCount(int maxPicCount) { + public void setMaxPicCount(int maxPicCount, int step) { this.maxPicCount = maxPicCount; - posHalf = maxPicCount / STEP; + this.step = step; + posHalf = maxPicCount / step; negHalf = -posHalf; } /** - * Done on seek. May cause sync issues if frame picCount != 0 (I frames are always 0) - * @param index + * Used primarily on seek. May cause issues if not a key frame */ @Override public void setIndex(int index) { @@ -42,7 +41,7 @@ public class PicCountClock extends LinearClock { } else if (delta > posHalf) { delta -= maxPicCount; } - picIndex += delta / STEP; + picIndex += delta / step; lastPicCount = picCount; if (maxPicIndex < picIndex) { maxPicIndex = picIndex; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java index c0bc1fad09..2139f13c1a 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java @@ -7,7 +7,7 @@ public class PicCountClockTest { @Test public void us_givenTwoStepsForward() { final PicCountClock picCountClock = new PicCountClock(10_000L, 100); - picCountClock.setMaxPicCount(16*2); + picCountClock.setMaxPicCount(16*2, 2); picCountClock.setPicCount(2*2); Assert.assertEquals(2*100, picCountClock.getUs()); } @@ -15,7 +15,7 @@ public class PicCountClockTest { @Test public void us_givenThreeStepsBackwards() { final PicCountClock picCountClock = new PicCountClock(10_000L, 100); - picCountClock.setMaxPicCount(16*2); + picCountClock.setMaxPicCount(16*2, 2); picCountClock.setPicCount(4*2); // 400ms Assert.assertEquals(400, picCountClock.getUs()); picCountClock.setPicCount(1*2); @@ -32,7 +32,7 @@ public class PicCountClockTest { @Test public void us_giveWrapBackwards() { final PicCountClock picCountClock = new PicCountClock(10_000L, 100); - picCountClock.setMaxPicCount(16*2); + picCountClock.setMaxPicCount(16*2, 2); //Need to walk up no faster than maxPicCount / 2 picCountClock.setPicCount(7*2); picCountClock.setPicCount(11*2); From 7c1cf36a9a1400c93a91ed41f2946a1eb136716a Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 30 Jan 2022 21:28:04 -0700 Subject: [PATCH 38/70] Commented verbose log --- .../com/google/android/exoplayer2/extractor/avi/AviTrack.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index c507604164..d901ceb023 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -149,7 +149,7 @@ public class AviTrack { trackOutput.sampleMetadata( clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); final LinearClock clock = getClock(); - Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); + //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); clock.advance(); } } From 565db92ae265d0686a46fb3250cb668aeb180764 Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 31 Jan 2022 03:33:12 -0700 Subject: [PATCH 39/70] Don't send meta on 0 length packets --- .../google/android/exoplayer2/extractor/avi/AviTrack.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index d901ceb023..8d4131013d 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -146,8 +146,10 @@ public class AviTrack { * @param size */ void done(final int size) { - trackOutput.sampleMetadata( - clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); + if (size > 0) { + trackOutput.sampleMetadata( + clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); + } final LinearClock clock = getClock(); //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); clock.advance(); From 0ff238df9957391933c8370cc8164c0017bb7b3e Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 31 Jan 2022 04:14:43 -0700 Subject: [PATCH 40/70] Fix int overrun on files with large scale --- .../android/exoplayer2/extractor/avi/StreamHeaderBox.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 8781d32adb..ff37d7d7c3 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -31,7 +31,7 @@ public class StreamHeaderBox extends ResidentBox { } public long getDurationUs() { - return getScale() * getLength() * 1_000_000L / getRate(); + return 1_000_000L * getScale() * getLength() / getRate(); } public int getSteamType() { From 9bd93ad98e307a1d8db22e7b0868692059f396ef Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 31 Jan 2022 04:48:57 -0700 Subject: [PATCH 41/70] Add work-around for muxer bug where idx1 offset is from 0, not "movi" location --- .../exoplayer2/extractor/avi/AviExtractor.java | 12 +++++++++++- .../exoplayer2/extractor/avi/AviSeekMap.java | 13 ++++++++----- .../exoplayer2/extractor/avi/AviSeekMapTest.java | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) 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 index a19a4178df..33380cadd9 100644 --- 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 @@ -399,6 +399,14 @@ public class AviExtractor implements Extractor { w("No video track found"); return; } + if (remaining < 16) { + output.seekMap(new SeekMap.Unseekable(getDuration())); + w("Index too short"); + return; + } + final ByteBuffer firstEntry = AviExtractor.allocate(16); + input.peekFully(firstEntry.array(), 0, 16); + final int videoId = videoTrack.id; final ByteBuffer indexByteBuffer = allocate(Math.min(remaining, 64 * 1024)); final byte[] bytes = indexByteBuffer.array(); @@ -466,8 +474,10 @@ public class AviExtractor implements Extractor { videoTrack.setKeyFrames(seekIndexes[videoId].getArray()); } + //Work-around a bug where the offset is from the start of the file, not "movi" + final long seekOffset = firstEntry.getInt(8) > moviOffset ? 0L : moviOffset; final AviSeekMap seekMap = new AviSeekMap(videoId, videoTrack.clock.durationUs, videoTrack.chunks, - keyFrameOffsetsDiv2.getArray(), seekIndexes, moviOffset); + keyFrameOffsetsDiv2.getArray(), seekIndexes, seekOffset); i("Video chunks=" + videoTrack.chunks + " us=" + seekMap.getDurationUs()); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index e5539039eb..717fd7f167 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -14,10 +14,13 @@ public class AviSeekMap implements SeekMap { final int[] keyFrameOffsetsDiv2; //Seek chunk indexes by streamId final int[][] seekIndexes; - final long moviOffset; + /** + * Usually the same as moviOffset, but sometimes 0 (muxer bug) + */ + final long seekOffset; public AviSeekMap(int videoId, long usDuration, int videoChunks, int[] keyFrameOffsetsDiv2, - UnboundedIntArray[] seekIndexes, long moviOffset) { + UnboundedIntArray[] seekIndexes, long seekOffset) { this.videoId = videoId; this.videoUsPerChunk = usDuration / videoChunks; this.duration = usDuration; @@ -26,7 +29,7 @@ public class AviSeekMap implements SeekMap { for (int i=0;i Date: Mon, 31 Jan 2022 10:51:52 -0700 Subject: [PATCH 42/70] Beef up readIdx1 tests --- .../extractor/avi/AviExtractor.java | 5 +++ .../extractor/avi/AviExtractorTest.java | 31 +++++++++++++++++++ .../exoplayer2/extractor/avi/DataHelper.java | 9 +++++- 3 files changed, 44 insertions(+), 1 deletion(-) 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 index 33380cadd9..4163d1d2f8 100644 --- 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 @@ -629,6 +629,11 @@ public class AviExtractor implements Extractor { chunkHandler = aviTrack; } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + long getMoviOffset() { + return moviOffset; + } + private static void w(String message) { try { Log.w(TAG, message); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 0a2fac3603..a93354ee7f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -265,6 +265,29 @@ public class AviExtractorTest { Assert.assertSame(AviTrack.ALL_KEY_FRAMES, videoTrack.keyFrames); } + @Test + public void readIdx1_givenBufferToShort() throws IOException { + final AviExtractor aviExtractor = setupVideoAviExtractor(); + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(new byte[12]).build(); + + aviExtractor.readIdx1(fakeExtractorInput, 12); + final FakeExtractorOutput fakeExtractorOutput = (FakeExtractorOutput) aviExtractor.output; + Assert.assertTrue(fakeExtractorOutput.seekMap instanceof SeekMap.Unseekable); + } + + @Test + public void readIdx1_givenBadOffset() throws IOException { + final AviExtractor aviExtractor = setupVideoAviExtractor(); + final int secs = 4; + final ByteBuffer idx1 = DataHelper.getIndex(secs, 1, (int)aviExtractor.getMoviOffset() + 4); + + final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). + setData(idx1.array()).build(); + aviExtractor.readIdx1(fakeExtractorInput, (int) fakeExtractorInput.getLength()); + Assert.assertEquals(0, aviExtractor.aviSeekMap.seekOffset); + } + @Test public void alignPositionHolder_givenOddPosition() { final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). @@ -471,4 +494,12 @@ public class AviExtractorTest { aviExtractor.setAviTracks(new AviTrack[]{null, aviTrack}); Assert.assertSame(aviTrack, aviExtractor.getAviTrack(aviTrack.chunkId)); } + + @Test + public void release() { + //Shameless way to get 100% method coverage + final AviExtractor aviExtractor = new AviExtractor(); + aviExtractor.release(); + //Nothing to assert on a method that does nothing + } } \ No newline at end of file diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index c1da51da3b..268716a36b 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -129,10 +129,17 @@ public class DataHelper { * @param keyFrameRate Key frame rate 1= every frame, 2=every other, ... */ public static ByteBuffer getIndex(final int secs, final int keyFrameRate) { + return getIndex(secs, keyFrameRate, 4); + } + /** + * + * @param secs Number of seconds + * @param keyFrameRate Key frame rate 1= every frame, 2=every other, ... + */ + public static ByteBuffer getIndex(final int secs, final int keyFrameRate, int offset) { final int videoFrames = secs * FPS; final int videoChunkId = AviTrack.getVideoChunkId(0); final int audioChunkId = AviTrack.getAudioChunkId(1); - int offset = 4; final ByteBuffer byteBuffer = AviExtractor.allocate((videoFrames + videoFrames*AUDIO_PER_VIDEO) * 16); for (int v=0;v Date: Mon, 31 Jan 2022 10:52:12 -0700 Subject: [PATCH 43/70] AvcChunkPeeker tests --- .../extractor/avi/AvcChunkPeeker.java | 7 +- .../extractor/avi/PicCountClock.java | 12 ++++ .../extractor/avi/AvcChunkPeekerTest.java | 65 +++++++++++++++++++ .../extractor/avi/AviTrackTest.java | 15 +++++ .../exoplayer2/extractor/avi/BitBuffer.java | 13 ++++ 5 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java index 950823da2a..9b26c2d1da 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java @@ -34,7 +34,7 @@ public class AvcChunkPeeker extends NalChunkPeeker { picCountClock = new PicCountClock(clock.durationUs, clock.length); } - public LinearClock getClock() { + public PicCountClock getClock() { return picCountClock; } @@ -131,4 +131,9 @@ public class AvcChunkPeeker extends NalChunkPeeker { compact(); } } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public NalUnitUtil.SpsData getSpsData() { + return spsData; + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java index 050bdf59f9..55743a83db 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -1,5 +1,7 @@ package com.google.android.exoplayer2.extractor.avi; +import androidx.annotation.VisibleForTesting; + /** * Properly calculates the frame time for H264 frames using PicCount */ @@ -60,4 +62,14 @@ public class PicCountClock extends LinearClock { public long getUs() { return getUs(picIndex); } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + int getMaxPicCount() { + return maxPicCount; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + int getLastPicCount() { + return lastPicCount; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java new file mode 100644 index 0000000000..0e28dbc375 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java @@ -0,0 +1,65 @@ +package com.google.android.exoplayer2.extractor.avi; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class AvcChunkPeekerTest { + private static final Format.Builder FORMAT_BUILDER_AVC = new Format.Builder(). + setSampleMimeType(MimeTypes.VIDEO_H264). + setWidth(1280).setHeight(720).setFrameRate(24000f/1001f); + + private static final byte[] P_SLICE = {00,00,00,01,0x41,(byte)0x9A,0x13,0x36,0x21,0x3A,0x5F, + (byte)0xFE,(byte)0x9E,0x10,00,00}; + + private FakeTrackOutput fakeTrackOutput; + private AvcChunkPeeker avcChunkPeeker; + + @Before + public void before() { + fakeTrackOutput = new FakeTrackOutput(false); + avcChunkPeeker = new AvcChunkPeeker(FORMAT_BUILDER_AVC, fakeTrackOutput, + new LinearClock(10_000_000L, 24 * 10)); + } + + private void peekStreamHeader() throws IOException { + final Context context = ApplicationProvider.getApplicationContext(); + final byte[] bytes = + TestUtil.getByteArray(context,"extractordumps/avi/avc_sei_sps_pps_ird.dump"); + + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build(); + + avcChunkPeeker.peek(input, bytes.length); + } + + @Test + public void peek_givenStreamHeader() throws IOException { + peekStreamHeader(); + final PicCountClock picCountClock = avcChunkPeeker.getClock(); + Assert.assertEquals(64, picCountClock.getMaxPicCount()); + Assert.assertEquals(0, avcChunkPeeker.getSpsData().picOrderCountType); + Assert.assertEquals(1.18f, fakeTrackOutput.lastFormat.pixelWidthHeightRatio, 0.01f); + } + + @Test + public void peek_givenStreamHeaderAndPSlice() throws IOException { + peekStreamHeader(); + final PicCountClock picCountClock = avcChunkPeeker.getClock(); + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build(); + + avcChunkPeeker.peek(input, P_SLICE.length); + + Assert.assertEquals(12, picCountClock.getLastPicCount()); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java new file mode 100644 index 0000000000..5b9afcfac9 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java @@ -0,0 +1,15 @@ +package com.google.android.exoplayer2.extractor.avi; + +import org.junit.Assert; +import org.junit.Test; + +public class AviTrackTest { + @Test + public void setClock_givenLinearClock() { + final LinearClock linearClock = new LinearClock(1_000_000L, 30); + final AviTrack aviTrack = DataHelper.getVideoAviTrack(1); + aviTrack.setClock(linearClock); + + Assert.assertSame(linearClock, aviTrack.getClock()); + } +} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java index 4775d93872..66b24ada1b 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java @@ -30,6 +30,19 @@ public class BitBuffer { work |= (value & 0xffffffffL); } + public void pushExpGolomb(final int i) { + if (i == 0) { + push(true); + } + int v = i + 1; + int zeroBits = 0; + while ((v >>>=1) > 1) { + zeroBits++; + } + final int bits = zeroBits * 2 + 1; + push(bits, i + 1); + } + public byte[] getBytes() { //Byte align grow(8 - bits % 8); From e67d47192bb5c3012cbe2da20e6ab62330890d0e Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 1 Feb 2022 12:58:40 -0700 Subject: [PATCH 44/70] Added missing sample file. --- .../extractordumps/avi/avc_sei_sps_pps_ird.dump | Bin 0 -> 166 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 testdata/src/test/assets/extractordumps/avi/avc_sei_sps_pps_ird.dump diff --git a/testdata/src/test/assets/extractordumps/avi/avc_sei_sps_pps_ird.dump b/testdata/src/test/assets/extractordumps/avi/avc_sei_sps_pps_ird.dump new file mode 100644 index 0000000000000000000000000000000000000000..e4b4894d811e987abdbaadf3b186c4b943b0ffcc GIT binary patch literal 166 zcmZQzU|?ipWMpYz05Z~37&O;}>2N#mi~rA{z#yH)!^G3T!1$$sA@+cKkHpafAVnE3 z)=BGh0~x6uO$@?I|9yM2kJU`%M8I@GoujwB3hcC&G&Gc(0F^RBk(RsVmuHV3ed~4r l3NYPbSkJ`9zC+bvAFF{0<30h0eVhzM4G7h6Z3sq$BLL~@DQ*A& literal 0 HcmV?d00001 From 89d10451ceba15527f09617157b69a6d91e67b61 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 1 Feb 2022 12:59:45 -0700 Subject: [PATCH 45/70] Move list types to the same class. Expose readHeaderList for external use. --- .../android/exoplayer2/extractor/avi/AviExtractor.java | 6 ++---- .../google/android/exoplayer2/extractor/avi/ListBox.java | 5 ++--- .../exoplayer2/extractor/avi/AviExtractorRoboTest.java | 6 +++--- .../android/exoplayer2/extractor/avi/AviExtractorTest.java | 2 +- .../google/android/exoplayer2/extractor/avi/DataHelper.java | 4 ++-- .../android/exoplayer2/extractor/avi/StreamNameBoxTest.java | 2 +- 6 files changed, 11 insertions(+), 14 deletions(-) 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 index 4163d1d2f8..8a3beaa27c 100644 --- 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 @@ -106,8 +106,6 @@ public class AviExtractor implements Extractor { static final int RIFF = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); static final int AVI_ = 'A' | ('V' << 8) | ('I' << 16) | (' ' << 24); - //Stream List - static final int STRL = 's' | ('t' << 8) | ('r' << 16) | ('l' << 24); //movie data box static final int MOVI = 'm' | ('o' << 8) | ('v' << 16) | ('i' << 24); //Index @@ -191,7 +189,7 @@ public class AviExtractor implements Extractor { } @Nullable - static ListBox readHeaderList(ExtractorInput input) throws IOException { + public static ListBox readHeaderList(ExtractorInput input) throws IOException { final ByteBuffer byteBuffer = getAviBuffer(input, 20); if (byteBuffer == null) { return null; @@ -313,7 +311,7 @@ public class AviExtractor implements Extractor { int streamId = 0; for (Box box : headerList.getChildren()) { - if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) { + if (box instanceof ListBox && ((ListBox) box).getListType() == ListBox.TYPE_STRL) { final ListBox streamList = (ListBox) box; aviTracks[streamId] = parseStream(streamList, streamId); streamId++; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index 88af081578..952736ab39 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java @@ -15,6 +15,8 @@ public class ListBox extends Box { public static final int LIST = 'L' | ('I' << 8) | ('S' << 16) | ('T' << 24); //Header List public static final int TYPE_HDRL = 'h' | ('d' << 8) | ('r' << 16) | ('l' << 24); + //Stream List + public static final int TYPE_STRL = 's' | ('t' << 8) | ('r' << 16) | ('l' << 24); private final int listType; @@ -47,9 +49,6 @@ public class ListBox extends Box { /** * Assume the input is pointing to the list type - * @param boxFactory - * @param input - * @return * @throws IOException */ public static ListBox newInstance(final int listSize, BoxFactory boxFactory, diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index 694e254530..39126dfcaf 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -46,7 +46,7 @@ public class AviExtractorRoboTest { final AviExtractor aviExtractor = new AviExtractor(); final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); aviExtractor.init(fakeExtractorOutput); - final ListBox streamList = new ListBox(128, AviExtractor.STRL, Collections.EMPTY_LIST); + final ListBox streamList = new ListBox(128, ListBox.TYPE_STRL, Collections.EMPTY_LIST); Assert.assertNull(aviExtractor.parseStream(streamList, 0)); } @@ -55,7 +55,7 @@ public class AviExtractorRoboTest { final AviExtractor aviExtractor = new AviExtractor(); final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); aviExtractor.init(fakeExtractorOutput); - final ListBox streamList = new ListBox(128, AviExtractor.STRL, + final ListBox streamList = new ListBox(128, ListBox.TYPE_STRL, Collections.singletonList(DataHelper.getVidsStreamHeader())); Assert.assertNull(aviExtractor.parseStream(streamList, 0)); } @@ -73,7 +73,7 @@ public class AviExtractorRoboTest { byteBuffer.put(aviHeader); byteBuffer.putInt(ListBox.LIST); byteBuffer.putInt(byteBuffer.remaining() - 4); - byteBuffer.putInt(AviExtractor.STRL); + byteBuffer.putInt(ListBox.TYPE_STRL); final StreamHeaderBox streamHeaderBox = DataHelper.getVidsStreamHeader(); byteBuffer.putInt(StreamHeaderBox.STRH); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index a93354ee7f..a6ff1487b3 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -319,7 +319,7 @@ public class AviExtractorTest { @Test public void readHeaderList_givenNoHeaderList() throws IOException { final ByteBuffer byteBuffer = DataHelper.getRiffHeader(88, 0x44); - byteBuffer.putInt(0x14, AviExtractor.STRL); //Overwrite header list with stream list + byteBuffer.putInt(0x14, ListBox.TYPE_STRL); //Overwrite header list with stream list final FakeExtractorInput input = new FakeExtractorInput.Builder(). setData(byteBuffer.array()).build(); Assert.assertNull(AviExtractor.readHeaderList(input)); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 268716a36b..1e944b085f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -62,7 +62,7 @@ public class DataHelper { list.add(streamHeaderBox); list.add(streamFormatBox); return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()), - AviExtractor.STRL, list); + ListBox.TYPE_STRL, list); } public static ListBox getAacStreamList() throws IOException { @@ -72,7 +72,7 @@ public class DataHelper { list.add(streamHeaderBox); list.add(streamFormatBox); return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()), - AviExtractor.STRL, list); + ListBox.TYPE_STRL, list); } public static StreamNameBox getStreamNameBox(final String name) { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java index 5190837b60..c721df59b6 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java @@ -10,7 +10,7 @@ public class StreamNameBoxTest { @Test public void createStreamName_givenList() throws IOException { final String name = "Test"; - final ListBuilder listBuilder = new ListBuilder(AviExtractor.STRL); + final ListBuilder listBuilder = new ListBuilder(ListBox.TYPE_STRL); listBuilder.addBox(DataHelper.getStreamNameBox(name)); final ByteBuffer listBuffer = listBuilder.build(); final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().setData(listBuffer.array()).build(); From d1fffb477f70ce712b977cca6bd6ad980b6045ef Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 1 Feb 2022 13:00:49 -0700 Subject: [PATCH 46/70] Add audio to picker. --- .../google/android/exoplayer2/demo/SampleChooserActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 0111210101..74464e914a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -238,7 +238,7 @@ public class SampleChooserActivity extends AppCompatActivity if (!mediaItems.isEmpty()) { final MediaItem mediaItem = mediaItems.get(0); if (mediaItem.localConfiguration != null && USER_CONTENT.equals(mediaItem.localConfiguration.uri)) { - openDocumentLauncher.launch(new String[]{"video/*"}); + openDocumentLauncher.launch(new String[]{"video/*","audio/*"}); return true; } } From 0896a04d02c24c8c0bcc46fc4400524405696f66 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 1 Feb 2022 15:08:13 -0700 Subject: [PATCH 47/70] Add bits per second for audio. --- .../android/exoplayer2/extractor/avi/AudioFormat.java | 4 +++- .../android/exoplayer2/extractor/avi/AviExtractor.java | 7 +++++-- .../android/exoplayer2/extractor/avi/AudioFormatTest.java | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) 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 index fb421b76e2..94d5ef6d43 100644 --- 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 @@ -39,7 +39,9 @@ public class AudioFormat { public int getSamplesPerSecond() { return byteBuffer.getInt(4); } - // 8 - nAvgBytesPerSec(uint) + public int getAvgBytesPerSec() { + return byteBuffer.getInt(8); + } // 12 - nBlockAlign // public int getBlockAlign() { // return byteBuffer.getShort(12); 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 index 8a3beaa27c..dee13c84ed 100644 --- 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 @@ -272,10 +272,13 @@ public class AviExtractor implements Extractor { final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO); final String mimeType = audioFormat.getMimeType(); builder.setSampleMimeType(mimeType); - //builder.setCodecs(audioFormat.getCodec()); builder.setChannelCount(audioFormat.getChannels()); builder.setSampleRate(audioFormat.getSamplesPerSecond()); - if (audioFormat.getFormatTag() == AudioFormat.WAVE_FORMAT_PCM) { + final int bytesPerSecond = audioFormat.getAvgBytesPerSec(); + if (bytesPerSecond != 0) { + builder.setAverageBitrate(bytesPerSecond * 8); + } + if (MimeTypes.AUDIO_RAW.equals(mimeType)) { final short bps = audioFormat.getBitsPerSample(); if (bps == 8) { builder.setPcmEncoding(C.ENCODING_PCM_8BIT); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java index cc1860749c..d01e5e3fd4 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java @@ -22,5 +22,6 @@ public class AudioFormatTest { Assert.assertEquals(0, audioFormat.getBitsPerSample()); //Not meaningful for AAC Assert.assertArrayEquals(CODEC_PRIVATE, audioFormat.getCodecData()); Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType()); + Assert.assertEquals(20034, audioFormat.getAvgBytesPerSec()); } } From e9fcc967a34210813d6ca0f7384d40255a738975 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 1 Feb 2022 17:33:10 -0700 Subject: [PATCH 48/70] Added copyright, better comments, removed dead code. --- .../android/exoplayer2/util/MimeTypes.java | 1 - .../exoplayer2/extractor/avi/AudioFormat.java | 26 +++++++++++---- .../extractor/avi/AvcChunkPeeker.java | 21 ++++++++++-- .../extractor/avi/AviExtractor.java | 33 +++++++++++-------- .../extractor/avi/AviHeaderBox.java | 23 ++++++++++--- .../exoplayer2/extractor/avi/AviSeekMap.java | 19 +++++++++++ .../exoplayer2/extractor/avi/AviTrack.java | 19 +++++++++-- .../android/exoplayer2/extractor/avi/Box.java | 18 +++++++++- .../exoplayer2/extractor/avi/BoxFactory.java | 18 ++++++++++ .../exoplayer2/extractor/avi/ChunkPeeker.java | 18 ++++++++++ .../exoplayer2/extractor/avi/LinearClock.java | 18 ++++++++++ .../exoplayer2/extractor/avi/ListBox.java | 17 +++++++++- .../extractor/avi/Mp4vChunkPeeker.java | 18 ++++++++++ .../extractor/avi/NalChunkPeeker.java | 24 +++++++++++--- .../extractor/avi/PicCountClock.java | 15 +++++++++ .../exoplayer2/extractor/avi/ResidentBox.java | 17 ++++++++-- .../extractor/avi/StreamFormatBox.java | 18 ++++++++++ .../extractor/avi/StreamHeaderBox.java | 29 ++++++++++------ .../extractor/avi/StreamNameBox.java | 18 ++++++++++ .../extractor/avi/UnboundedIntArray.java | 25 +++++++++++--- .../exoplayer2/extractor/avi/VideoFormat.java | 21 ++++++++++-- .../extractor/avi/AudioFormatTest.java | 15 +++++++++ .../extractor/avi/AvcChunkPeekerTest.java | 15 +++++++++ .../extractor/avi/AviExtractorRoboTest.java | 15 +++++++++ .../extractor/avi/AviExtractorTest.java | 15 +++++++++ .../extractor/avi/AviHeaderBoxTest.java | 16 ++++++++- .../extractor/avi/AviSeekMapTest.java | 15 +++++++++ .../extractor/avi/AviTrackTest.java | 15 +++++++++ .../exoplayer2/extractor/avi/BitBuffer.java | 15 +++++++++ .../exoplayer2/extractor/avi/DataHelper.java | 15 +++++++++ .../extractor/avi/LinearClockTest.java | 15 +++++++++ .../exoplayer2/extractor/avi/ListBuilder.java | 15 +++++++++ .../extractor/avi/MockNalChunkPeeker.java | 15 +++++++++ .../extractor/avi/Mp4vChunkPeekerTest.java | 15 +++++++++ .../extractor/avi/NalChunkPeekerTest.java | 15 +++++++++ .../extractor/avi/PicCountClockTest.java | 15 +++++++++ .../extractor/avi/StreamHeaderBoxTest.java | 15 +++++++++ .../extractor/avi/StreamNameBoxTest.java | 15 +++++++++ .../extractor/avi/UnboundedIntArrayTest.java | 15 +++++++++ .../extractor/avi/VideoFormatTest.java | 15 +++++++++ 40 files changed, 644 insertions(+), 58 deletions(-) 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 85ed1d3df4..e033ec31fc 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 @@ -55,7 +55,6 @@ public final class MimeTypes { 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"; - //This exists on Nvidia Shield public static final String VIDEO_MJPEG = BASE_TYPE_VIDEO + "/mjpeg"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; 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 index 94d5ef6d43..479da0b7dc 100644 --- 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 @@ -1,9 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import android.util.SparseArray; import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; +/** + * Wrapper for the WAVEFORMATEX structure + */ public class AudioFormat { public static final short WAVE_FORMAT_PCM = 1; static final short WAVE_FORMAT_AAC = 0xff; @@ -19,9 +37,8 @@ public class AudioFormat { FORMAT_MAP.put(WAVE_FORMAT_DTS2, MimeTypes.AUDIO_DTS); } - private ByteBuffer byteBuffer; + private final ByteBuffer byteBuffer; - //WAVEFORMATEX public AudioFormat(ByteBuffer byteBuffer) { this.byteBuffer = byteBuffer; } @@ -43,9 +60,6 @@ public class AudioFormat { return byteBuffer.getInt(8); } // 12 - nBlockAlign -// public int getBlockAlign() { -// return byteBuffer.getShort(12); -// } public short getBitsPerSample() { return byteBuffer.getShort(14); } @@ -62,6 +76,4 @@ public class AudioFormat { temp.get(data); return data; } - - //TODO: Deal with WAVEFORMATEXTENSIBLE } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java index 9b26c2d1da..b3ecf270d2 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeeker.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.VisibleForTesting; @@ -10,11 +25,11 @@ import java.io.IOException; /** * Corrects the time and PAR for H264 streams - * H264 is very rare in AVI due to the rise of mp4 + * AVC is very rare in AVI due to the rise of the mp4 container */ public class AvcChunkPeeker extends NalChunkPeeker { private static final int NAL_TYPE_MASK = 0x1f; - private static final int NAL_TYPE_IRD = 5; + private static final int NAL_TYPE_IDR = 5; //I Frame private static final int NAL_TYPE_SEI = 6; private static final int NAL_TYPE_SPS = 7; private static final int NAL_TYPE_PPS = 8; @@ -108,7 +123,7 @@ public class AvcChunkPeeker extends NalChunkPeeker { case 4: updatePicCountClock(nalTypeOffset); return; - case NAL_TYPE_IRD: + case NAL_TYPE_IDR: picCountClock.syncIndexes(); return; case NAL_TYPE_AUD: 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 index dee13c84ed..dd06a90c26 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; @@ -20,11 +35,11 @@ import java.util.Collections; import java.util.HashMap; /** - * Based on the official MicroSoft spec + * Extractor based on the official MicroSoft spec * https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference */ public class AviExtractor implements Extractor { - //Minimum time between keyframes in the SeekMap + //Minimum time between keyframes in the AviSeekMap static final long MIN_KEY_FRAME_RATE_US = 2_000_000L; static final long UINT_MASK = 0xffffffffL; @@ -130,9 +145,7 @@ public class AviExtractor implements Extractor { @VisibleForTesting AviSeekMap aviSeekMap; -// private long indexOffset; //Usually chunkStart - - //If partial read + //Set if a chunk is only partially read private transient AviTrack chunkHandler; /** @@ -379,13 +392,7 @@ public class AviExtractor implements Extractor { w("Audio is not all key frames chunks=" + aviTrack.chunks + " keyFrames=" + keyFrameCounts[aviTrack.id]); } - } /* else if (aviTrack.isVideo()) { - final LinearClock clock = aviTrack.getClock(); - if (clock.length != aviTrack.chunks) { - w("Video #" + aviTrack.id + " chunks != length changing FPS"); - clock.setLength(aviTrack.chunks); - } - }*/ + } } } } @@ -447,8 +454,6 @@ public class AviExtractor implements Extractor { } final int flags = indexByteBuffer.getInt(); final int offset = indexByteBuffer.getInt(); - //Skip size - //indexByteBuffer.position(indexByteBuffer.position() + 4); final int size = indexByteBuffer.getInt(); if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { if (aviTrack.isVideo()) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index d2a21dd727..0a40b85218 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -1,16 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.VisibleForTesting; import java.nio.ByteBuffer; +/** + * Wrapper around the AVIMAINHEADER structure + */ public class AviHeaderBox extends ResidentBox { static final int LEN = 0x38; static final int AVIF_HASINDEX = 0x10; private static final int AVIF_MUSTUSEINDEX = 0x20; static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24); - //AVIMAINHEADER - AviHeaderBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); } @@ -39,9 +55,6 @@ public class AviHeaderBox extends ResidentBox { } // 20 - dwInitialFrames -// int getInitialFrames() { -// return byteBuffer.getInt(20); -// } int getStreams() { return byteBuffer.getInt(24); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index 717fd7f167..fe662ca615 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; @@ -6,6 +21,10 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import java.util.Arrays; +/** + * Seek map for AVI. + * Consists of Video chunk offsets and indexes for all streams + */ public class AviSeekMap implements SeekMap { final int videoId; final long videoUsPerChunk; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index 8d4131013d..ac1cd66501 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; @@ -5,12 +20,12 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.util.Log; import java.io.IOException; import java.util.Arrays; /** - * Collection of info about a track + * Collection of info about a track. + * This acts a bridge between AVI and ExoPlayer structures */ public class AviTrack { public static final int[] ALL_KEY_FRAMES = new int[0]; 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 index 2f6ea39092..e90bcd67e4 100644 --- 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 @@ -1,7 +1,23 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; /** - * This is referred to as a Chunk in the MS spec, but that gets confusing with AV chunks + * This is referred to as a Chunk in the MS spec, but that gets confusing with AV chunks. + * Borrowed the term from mp4 as these are similar to boxes or atoms. */ public class Box { private final int size; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java index 8921cde14d..075e60d0f0 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/BoxFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -5,6 +20,9 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; +/** + * Factory for Boxes. These usually exist inside a ListBox + */ public class BoxFactory { static int[] types = {AviHeaderBox.AVIH, StreamHeaderBox.STRH, StreamFormatBox.STRF, StreamNameBox.STRN}; static { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java index 84eb49e459..0cfd2a97e6 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkPeeker.java @@ -1,8 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.extractor.ExtractorInput; import java.io.IOException; +/** + * Peeks for import data in the chunk stream. + */ public interface ChunkPeeker { void peek(ExtractorInput input, final int size) throws IOException; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java index 03fcdbd795..63059bc919 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/LinearClock.java @@ -1,5 +1,23 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; +/** + * A clock that is linearly derived from the current chunk index of a given stream + */ public class LinearClock { long durationUs; int length; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index 952736ab39..daf5b4fc7a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; @@ -9,7 +24,7 @@ import java.util.ArrayList; import java.util.List; /** - * An AVI LIST box, memory resident + * An AVI LIST box. Similar to a Java List */ public class ListBox extends Box { public static final int LIST = 'L' | ('I' << 8) | ('S' << 16) | ('T' << 24); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java index 48e270b265..518172cc77 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeeker.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; @@ -8,6 +23,9 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; import java.io.IOException; +/** + * Peeks an MP4V stream looking for pixelWidthHeightRatio data + */ public class Mp4vChunkPeeker extends NalChunkPeeker { @VisibleForTesting static final byte SEQUENCE_START_CODE = (byte)0xb0; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java index ca8c3d7b78..e5be793ae4 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeeker.java @@ -1,9 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.extractor.ExtractorInput; import java.io.IOException; import java.util.Arrays; +/** + * Generic base class for NAL (0x00 0x00 0x01) chunk headers + * Theses are used by AVC and MP4V (XVID) + */ public abstract class NalChunkPeeker implements ChunkPeeker { private static final int SEEK_PEEK_SIZE = 256; private final int peekSize; @@ -105,9 +124,4 @@ public abstract class NalChunkPeeker implements ChunkPeeker { processChunk(input, nalTypeOffset); input.resetPeekPosition(); } - -// @VisibleForTesting(otherwise = VisibleForTesting.NONE) -// void setBuffer(byte[] buffer) { -// this.buffer = buffer; -// } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java index 55743a83db..ccec181b4c 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/PicCountClock.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.VisibleForTesting; 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 index 5234869389..e450d14848 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; @@ -15,8 +30,6 @@ import java.nio.ByteOrder; * A box that is resident in memory */ public class ResidentBox extends Box { - private static final String TAG = AviExtractor.TAG; - final private static int MAX_RESIDENT = 1024; final ByteBuffer byteBuffer; ResidentBox(int type, int size, ByteBuffer byteBuffer) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java index 8b727d4c1a..9cbabdfeba 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java @@ -1,8 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; import java.nio.ByteBuffer; +/** + * Wrapper around the various StreamFormats + */ public class StreamFormatBox extends ResidentBox { public static final int STRF = 's' | ('t' << 8) | ('r' << 16) | ('f' << 24); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index ff37d7d7c3..5486d43d7f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -1,9 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import java.nio.ByteBuffer; /** - * AVISTREAMHEADER + * Wrapper around the AVISTREAMHEADER structure */ public class StreamHeaderBox extends ResidentBox { public static final int STRH = 's' | ('t' << 8) | ('r' << 16) | ('h' << 24); @@ -51,9 +66,6 @@ public class StreamHeaderBox extends ResidentBox { return byteBuffer.getInt(24); } //28 - dwStart - doesn't seem to ever be set -// public int getStart() { -// return byteBuffer.getInt(28); -// } public int getLength() { return byteBuffer.getInt(32); } @@ -63,11 +75,8 @@ public class StreamHeaderBox extends ResidentBox { } //40 - dwQuality //44 - dwSampleSize -// public int getSampleSize() { -// return byteBuffer.getInt(44); -// } -// public String toString() { -// return "scale=" + getScale() + " rate=" + getRate() + " length=" + getLength() + " us=" + getDurationUs(); -// } + public String toString() { + return "scale=" + getScale() + " rate=" + getRate() + " length=" + getLength() + " us=" + getDurationUs(); + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java index e8f31766c9..357bc9ecfe 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java @@ -1,7 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import java.nio.ByteBuffer; +/** + * Human readable stream name + */ public class StreamNameBox extends ResidentBox { public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java index 61455d19b6..3db43cf730 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java @@ -1,15 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import java.util.Arrays; +/** + * Optimized unbounded array of ints. + * Used primarily to create Index (SeekMap) data. + */ public class UnboundedIntArray { @NonNull @VisibleForTesting int[] array; - //unint - private int size =0; + //uint + private int size = 0; public UnboundedIntArray() { this(8); @@ -58,8 +77,6 @@ public class UnboundedIntArray { /** * Only works if values are in sequential order - * @param v - * @return */ public int indexOf(int v) { return Arrays.binarySearch(array, v); 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 index 99acc62f57..0021647a98 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.VisibleForTesting; @@ -5,6 +20,9 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; import java.util.HashMap; +/** + * Wrapper around the BITMAPINFOHEADER structure + */ public class VideoFormat { static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); @@ -39,7 +57,7 @@ public class VideoFormat { this.byteBuffer = byteBuffer; } - //biSize - (uint) + // 0 - biSize - (uint) public int getWidth() { return byteBuffer.getInt(4); @@ -71,5 +89,4 @@ public class VideoFormat { public void setCompression(final int compression) { byteBuffer.putInt(16, compression); } - } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java index d01e5e3fd4..cfb084b9b5 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.test.ext.junit.runners.AndroidJUnit4; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java index 0e28dbc375..da9ddec141 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import android.content.Context; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index 39126dfcaf..acfd3ae6b1 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.test.ext.junit.runners.AndroidJUnit4; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index a6ff1487b3..c6a4d7842d 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.Format; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java index e7b3a93e7b..e18e71ac1f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBoxTest.java @@ -1,6 +1,20 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; -import java.nio.ByteBuffer; import org.junit.Assert; import org.junit.Test; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java index 4dfbc6e01c..8328d9a3f5 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.extractor.SeekMap; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java index 5b9afcfac9..6c848893fb 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviTrackTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import org.junit.Assert; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java index 66b24ada1b..78027534ad 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/BitBuffer.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import java.nio.BufferOverflowException; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 1e944b085f..5e74367243 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import android.content.Context; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java index ef88ac2dfa..c50fc76e7f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/LinearClockTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import org.junit.Assert; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java index 3be00dd152..86283e31b8 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ListBuilder.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import java.nio.ByteBuffer; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java index 0be4368b5c..1c5e303712 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MockNalChunkPeeker.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.extractor.ExtractorInput; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java index 9a3205b90f..5f8f8936eb 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/Mp4vChunkPeekerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import android.content.Context; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java index 2708015f4f..fe1d62f297 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/NalChunkPeekerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.testutil.FakeExtractorInput; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java index 2139f13c1a..f9fc5109cd 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/PicCountClockTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import org.junit.Assert; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java index e36ac29cb3..115e1ab792 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBoxTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import androidx.test.ext.junit.runners.AndroidJUnit4; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java index c721df59b6..70f4780ae5 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/StreamNameBoxTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.testutil.FakeExtractorInput; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java index 20a2df51a6..7f4251e31f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import org.junit.Assert; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java index 53b9b4c6ad..521fa36278 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/VideoFormatTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer2.extractor.avi; import com.google.android.exoplayer2.util.MimeTypes; From 43cfc4a61168d6d698e1209ad0b683a5d277cd05 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Oct 2021 23:55:06 +0100 Subject: [PATCH 49/70] Revert Demo and remove BitmapFactoryVideoRenderer classes --- demos/main/src/main/assets/media.exolist.json | 4 - .../demo/SampleChooserActivity.java | 27 +- library/core/build.gradle | 6 +- .../exoplayer2/DefaultRenderersFactory.java | 2 - .../video/BitmapFactoryVideoRenderer.java | 317 ------------------ .../video/BitmapFactoryVideoRendererTest.java | 237 ------------- .../exoplayer2/video/FakeEventListener.java | 64 ---- .../video/ShadowSurfaceExtended.java | 43 --- .../test/assets/media/jpeg/image-320-240.jpg | Bin 36039 -> 0 bytes 9 files changed, 3 insertions(+), 697 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java delete mode 100644 testdata/src/test/assets/media/jpeg/image-320-240.jpg diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index e6cb246db1..0b479ff6d5 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -542,10 +542,6 @@ { "name": "Misc", "samples": [ - { - "name": "User File", - "uri": "content://user" - }, { "name": "Dizzy (MP4)", "uri": "https://html5demos.com/assets/dizzy.mp4" diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 74464e914a..b79a7a62ca 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -19,7 +19,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -41,8 +40,6 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.MediaItem; @@ -76,7 +73,6 @@ public class SampleChooserActivity extends AppCompatActivity private static final String TAG = "SampleChooserActivity"; private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position"; private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position"; - private static final Uri USER_CONTENT = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority("user").build(); private String[] uris; private boolean useExtensionRenderers; @@ -84,13 +80,6 @@ public class SampleChooserActivity extends AppCompatActivity private SampleAdapter sampleAdapter; private MenuItem preferExtensionDecodersMenuItem; private ExpandableListView sampleListView; - private final ActivityResultLauncher openDocumentLauncher = registerForActivityResult( - new ActivityResultContracts.OpenDocument(), uri -> { - if (uri != null) { - final MediaItem mediaItem = new MediaItem.Builder().setUri(uri).build(); - startPlayer(Collections.singletonList(mediaItem)); - } - }); @Override public void onCreate(Bundle savedInstanceState) { @@ -234,25 +223,13 @@ public class SampleChooserActivity extends AppCompatActivity prefEditor.apply(); PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag(); - final List mediaItems = playlistHolder.mediaItems; - if (!mediaItems.isEmpty()) { - final MediaItem mediaItem = mediaItems.get(0); - if (mediaItem.localConfiguration != null && USER_CONTENT.equals(mediaItem.localConfiguration.uri)) { - openDocumentLauncher.launch(new String[]{"video/*","audio/*"}); - return true; - } - } - startPlayer(playlistHolder.mediaItems); - return true; - } - - private void startPlayer(final List mediaItems) { Intent intent = new Intent(this, PlayerActivity.class); intent.putExtra( IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, isNonNullAndChecked(preferExtensionDecodersMenuItem)); - IntentUtil.addToIntent(mediaItems, intent); + IntentUtil.addToIntent(playlistHolder.mediaItems, intent); startActivity(intent); + return true; } private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) { diff --git a/library/core/build.gradle b/library/core/build.gradle index 3196735378..5bf70ad151 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -21,11 +21,7 @@ android { testInstrumentationRunnerArguments clearPackageData: 'true' multiDexEnabled true } - testOptions{ - unitTests.all { - jvmArgs '-noverify' - } - } + buildTypes { debug { testCoverageEnabled = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 4b0fff8ffc..0d1c126dc5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; -import com.google.android.exoplayer2.video.BitmapFactoryVideoRenderer; import com.google.android.exoplayer2.mediacodec.DefaultMediaCodecAdapterFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; @@ -396,7 +395,6 @@ public class DefaultRenderersFactory implements RenderersFactory { eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); out.add(videoRenderer); - out.add(new BitmapFactoryVideoRenderer(eventHandler, eventListener)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java deleted file mode 100644 index 31dba8ef3e..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ /dev/null @@ -1,317 +0,0 @@ -package com.google.android.exoplayer2.video; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.os.Handler; -import android.os.SystemClock; -import android.view.Surface; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; -import androidx.arch.core.util.Function; -import com.google.android.exoplayer2.BaseRenderer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.util.MimeTypes; -import java.nio.ByteBuffer; - -public class BitmapFactoryVideoRenderer extends BaseRenderer { - static final String TAG = "BitmapFactoryRenderer"; - - //Sleep Reasons - static final String STREAM_END = "Stream End"; - static final String STREAM_EMPTY = "Stream Empty"; - static final String RENDER_WAIT = "Render Wait"; - - private static int threadId; - - private final Rect rect = new Rect(); - private final RenderRunnable renderRunnable = new RenderRunnable(); - - final VideoRendererEventListener.EventDispatcher eventDispatcher; - final Thread thread = new Thread(renderRunnable, getClass().getSimpleName() + threadId++); - - @Nullable - volatile Surface surface; - - private VideoSize lastVideoSize = VideoSize.UNKNOWN; - private long currentTimeUs; - private long frameUs; - private boolean firstFrameRendered; - @Nullable - private DecoderCounters decoderCounters; - - public BitmapFactoryVideoRenderer(@Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener) { - super(C.TRACK_TYPE_VIDEO); - eventDispatcher = new VideoRendererEventListener.EventDispatcher(eventHandler, eventListener); - } - - @NonNull - @Override - public String getName() { - return TAG; - } - - @Override - protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) - throws ExoPlaybackException { - decoderCounters = new DecoderCounters(); - eventDispatcher.enabled(decoderCounters); - if (mayRenderStartOfStream) { - thread.start(); - } - } - - @Override - protected void onStarted() throws ExoPlaybackException { - if (thread.getState() == Thread.State.NEW) { - thread.start(); - } - } - - @Override - protected void onDisabled() { - renderRunnable.stop(); - - @Nullable - final DecoderCounters decoderCounters = this.decoderCounters; - if (decoderCounters != null) { - eventDispatcher.disabled(decoderCounters); - } - } - - @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - //Log.d(TAG, "Render: us=" + positionUs); - synchronized (renderRunnable) { - currentTimeUs = positionUs; - renderRunnable.notify(); - } - } - - @Override - protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - thread.interrupt(); - } - - @Override - public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_VIDEO_OUTPUT) { - if (message instanceof Surface) { - surface = (Surface) message; - } else { - surface = null; - } - } - super.handleMessage(messageType, message); - } - - @Override - public boolean isReady() { - return surface != null; - } - - @Override - public boolean isEnded() { - return renderRunnable.isEnded(); - } - - @Override - public int supportsFormat(Format format) throws ExoPlaybackException { - //Technically could support any format BitmapFactory supports - if (MimeTypes.VIDEO_MJPEG.equals(format.sampleMimeType)) { - return RendererCapabilities.create(C.FORMAT_HANDLED); - } - return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); - } - - @WorkerThread - private void onFormatChanged(@NonNull FormatHolder formatHolder) { - @Nullable final Format format = formatHolder.format; - if (format != null) { - frameUs = (long)(1_000_000L / format.frameRate); - eventDispatcher.inputFormatChanged(format, null); - } - } - - @WorkerThread - void renderBitmap(@NonNull final Bitmap bitmap) { - @Nullable - final Surface surface = this.surface; - if (surface == null) { - return; - } - //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); - final Canvas canvas = surface.lockCanvas(null); - - renderBitmap(bitmap, canvas); - - surface.unlockCanvasAndPost(canvas); - @Nullable - final DecoderCounters decoderCounters = BitmapFactoryVideoRenderer.this.decoderCounters; - if (decoderCounters != null) { - decoderCounters.renderedOutputBufferCount++; - } - if (!firstFrameRendered) { - firstFrameRendered = true; - eventDispatcher.renderedFirstFrame(surface); - } - } - - @WorkerThread - @VisibleForTesting - void renderBitmap(Bitmap bitmap, Canvas canvas) { - final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); - if (!videoSize.equals(lastVideoSize)) { - lastVideoSize = videoSize; - eventDispatcher.videoSizeChanged(videoSize); - } - rect.set(0,0,canvas.getWidth(), canvas.getHeight()); - canvas.drawBitmap(bitmap, null, rect, null); - } - - class RenderRunnable implements Runnable, Function { - final DecoderInputBuffer decoderInputBuffer = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - - private volatile boolean running = true; - - @VisibleForTesting - Function sleepFunction = this; - - void stop() { - running = false; - thread.interrupt(); - } - - boolean isEnded() { - return !running || decoderInputBuffer.isEndOfStream(); - } - - @Nullable - private Bitmap decodeInputBuffer(final DecoderInputBuffer decoderInputBuffer) { - @Nullable final ByteBuffer byteBuffer = decoderInputBuffer.data; - if (byteBuffer != null) { - final Bitmap bitmap; - try { - bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), - byteBuffer.arrayOffset() + byteBuffer.position()); - if (bitmap == null) { - throw new NullPointerException("Decode bytes failed"); - } else { - return bitmap; - } - } catch (Exception e) { - eventDispatcher.videoCodecError(e); - } - } - return null; - } - - /** - * - * @return true if interrupted - */ - public synchronized Boolean apply(String why) { - try { - wait(); - return false; - } catch (InterruptedException e) { - //If we are interrupted, treat as a cancel - return true; - } - } - - private boolean sleep(String why) { - return sleepFunction.apply(why); - } - - @WorkerThread - public void run() { - final FormatHolder formatHolder = getFormatHolder(); - long start = SystemClock.uptimeMillis(); - main: - while (running) { - decoderInputBuffer.clear(); - final int result = readSource(formatHolder, decoderInputBuffer, - formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0); - switch (result) { - case C.RESULT_BUFFER_READ: { - if (decoderInputBuffer.isEndOfStream()) { - //Wait for shutdown or stream to be changed - sleep(STREAM_END); - continue; - } - final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; - //If we are more than 1/2 a frame behind, skip the next frame - if (leadUs < -frameUs / 2) { - eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start); - start = SystemClock.uptimeMillis(); - continue; - } - start = SystemClock.uptimeMillis(); - - @Nullable - final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer); - if (bitmap == null) { - continue; - } - while (currentTimeUs < decoderInputBuffer.timeUs) { - //Log.d(TAG, "Sleep: us=" + currentTimeUs); - if (sleep(RENDER_WAIT)) { - //Sleep was interrupted, discard Bitmap - continue main; - } - } - if (running) { - renderBitmap(bitmap); - } - } - break; - case C.RESULT_FORMAT_READ: - onFormatChanged(formatHolder); - break; - case C.RESULT_NOTHING_READ: - sleep(STREAM_EMPTY); - break; - } - } - } - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - Rect getRect() { - return rect; - } - - @Nullable - @VisibleForTesting - DecoderCounters getDecoderCounters() { - return decoderCounters; - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - Thread getThread() { - return thread; - } - - @Nullable - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - Surface getSurface() { - return surface; - } - - RenderRunnable getRenderRunnable() { - return renderRunnable; - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java deleted file mode 100644 index 914f59786b..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java +++ /dev/null @@ -1,237 +0,0 @@ -package com.google.android.exoplayer2.video; - -import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; -import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; -import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.sample; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; -import android.view.Surface; -import androidx.arch.core.util.Function; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.RendererConfiguration; -import com.google.android.exoplayer2.drm.DrmSessionEventListener; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.testutil.FakeSampleStream; -import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowBitmapFactory; -import org.robolectric.shadows.ShadowLooper; - -@RunWith(AndroidJUnit4.class) -@Config(shadows = {ShadowSurfaceExtended.class}) -public class BitmapFactoryVideoRendererTest { - private final static Format FORMAT_MJPEG = new Format.Builder(). - setSampleMimeType(MimeTypes.VIDEO_MJPEG). - setWidth(320).setHeight(240). - setFrameRate(15f).build(); - - FakeEventListener fakeEventListener = new FakeEventListener(); - BitmapFactoryVideoRenderer bitmapFactoryVideoRenderer; - - @Before - public void before() { - fakeEventListener = new FakeEventListener(); - final Handler handler = new Handler(Looper.getMainLooper()); - bitmapFactoryVideoRenderer = new BitmapFactoryVideoRenderer(handler, fakeEventListener); - } - - @After - public void after() { - //Kill the Thread - bitmapFactoryVideoRenderer.onDisabled(); - } - - @Test - public void getName() { - Assert.assertEquals(BitmapFactoryVideoRenderer.TAG, bitmapFactoryVideoRenderer.getName()); - } - - @Test - public void onEnabled_givenMayRenderStartOfStream() throws PlaybackException { - bitmapFactoryVideoRenderer.onEnabled(false, true); - ShadowLooper.idleMainLooper(); - Assert.assertNotNull(bitmapFactoryVideoRenderer.getDecoderCounters()); - Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState()); - Assert.assertTrue(fakeEventListener.isVideoEnabled()); - } - - @Test - public void onStarted_givenThreadNotStarted() throws PlaybackException { - bitmapFactoryVideoRenderer.onStarted(); - ShadowLooper.idleMainLooper(); - Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState()); - } - - @Test - public void onDisabled_givenOnEnabled() throws PlaybackException, InterruptedException { - onEnabled_givenMayRenderStartOfStream(); - bitmapFactoryVideoRenderer.onDisabled(); - ShadowLooper.idleMainLooper(); - Assert.assertFalse(fakeEventListener.isVideoEnabled()); - //Ensure Thread is shutdown - bitmapFactoryVideoRenderer.getThread().join(500L); - Assert.assertTrue(bitmapFactoryVideoRenderer.isEnded()); - } - - private FakeSampleStream getSampleStream() throws IOException { - final Context context = ApplicationProvider.getApplicationContext(); - final byte[] bytes = TestUtil.getByteArray(context, "media/jpeg/image-320-240.jpg"); - FakeSampleStream fakeSampleStream = - new FakeSampleStream( - new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), - /* mediaSourceEventDispatcher= */ null, - DrmSessionManager.DRM_UNSUPPORTED, - new DrmSessionEventListener.EventDispatcher(), - /* initialFormat= */ FORMAT_MJPEG, - ImmutableList.of( - sample(0L, C.BUFFER_FLAG_KEY_FRAME, bytes), - END_OF_STREAM_ITEM)); - return fakeSampleStream; - } - - private Surface setSurface() throws ExoPlaybackException { - final Surface surface = ShadowSurfaceExtended.newInstance(); - final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface); - shadowSurfaceExtended.setSize(1080, 1920); - bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); - return surface; - } - - @Test - public void handleMessage_givenSurface() throws ExoPlaybackException { - final Surface surface = setSurface(); - Assert.assertSame(surface, bitmapFactoryVideoRenderer.getSurface()); - bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, null); - Assert.assertNull(bitmapFactoryVideoRenderer.getSurface()); - } - - @Test - public void isReady_givenSurface() throws ExoPlaybackException { - Assert.assertFalse(bitmapFactoryVideoRenderer.isReady()); - setSurface(); - Assert.assertTrue(bitmapFactoryVideoRenderer.isReady()); - } - - @Test - public void render_givenJpegAndSurface() throws IOException, ExoPlaybackException { - final Surface surface = setSurface(); - final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface); - - FakeSampleStream fakeSampleStream = getSampleStream(); - fakeSampleStream.writeData(0L); - bitmapFactoryVideoRenderer.enable(RendererConfiguration.DEFAULT, new Format[]{FORMAT_MJPEG}, - fakeSampleStream, 0L, false, true, 0L, 0L); - bitmapFactoryVideoRenderer.render(0L, 0L); - // This test actually decodes the JPEG (very cool!), - // May need to bump up timers for slow machines - Assert.assertTrue(shadowSurfaceExtended.waitForPost(500L)); - } - - @Test - public void supportsFormat_givenMjpegFormat() throws ExoPlaybackException{ - Assert.assertEquals(C.FORMAT_HANDLED, - bitmapFactoryVideoRenderer.supportsFormat(FORMAT_MJPEG) & C.FORMAT_HANDLED); - } - - @Test - public void supportsFormat_givenMp4vFormat() throws ExoPlaybackException{ - final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build(); - Assert.assertEquals(0, - bitmapFactoryVideoRenderer.supportsFormat(format) & C.FORMAT_HANDLED); - } - - @Test - public void renderBitmap_given4by3BitmapAnd16by9Canvas() { - final Bitmap bitmap = Bitmap.createBitmap(FORMAT_MJPEG.width, FORMAT_MJPEG.height, Bitmap.Config.ARGB_8888); - final Bitmap canvasBitmap = Bitmap.createBitmap(1080, 1920, Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(canvasBitmap); - bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas); - ShadowLooper.idleMainLooper(); - - final Rect rect = bitmapFactoryVideoRenderer.getRect(); - Assert.assertEquals(canvas.getWidth(), rect.width()); - Assert.assertEquals(canvas.getHeight(), rect.height()); - final VideoSize videoSize = fakeEventListener.videoSize; - Assert.assertEquals(bitmap.getWidth(), videoSize.width); - - bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas); - ShadowLooper.idleMainLooper(); - Assert.assertSame(videoSize, fakeEventListener.videoSize); - } - - @Test - public void RenderRunnable_run_givenLateFrame() throws IOException, ExoPlaybackException { - final Function sleep = why -> {throw new RuntimeException(why);}; - - FakeSampleStream fakeSampleStream = getSampleStream(); - fakeSampleStream.writeData(0L); - //Don't enable so the Thread is not running - bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L); - BitmapFactoryVideoRenderer.RenderRunnable renderRunnable = - bitmapFactoryVideoRenderer.getRenderRunnable(); - renderRunnable.sleepFunction = sleep; - bitmapFactoryVideoRenderer.render(1_000_000L, 0L); - try { - renderRunnable.run(); - } catch (RuntimeException e) { - Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage()); - } - ShadowLooper.idleMainLooper(); - Assert.assertEquals(1, fakeEventListener.getDroppedFrames()); - } - - @Test - public void RenderRunnable_run_givenBadJpeg() throws IOException, ExoPlaybackException { - final Function sleep = why -> {throw new RuntimeException(why);}; - FakeSampleStream fakeSampleStream = - new FakeSampleStream( - new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), - /* mediaSourceEventDispatcher= */ null, - DrmSessionManager.DRM_UNSUPPORTED, - new DrmSessionEventListener.EventDispatcher(), - /* initialFormat= */ FORMAT_MJPEG, - ImmutableList.of( - oneByteSample(0L, C.BUFFER_FLAG_KEY_FRAME), - END_OF_STREAM_ITEM)); - fakeSampleStream.writeData(0L); - - //Don't enable so the Thread is not running - bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L); - BitmapFactoryVideoRenderer.RenderRunnable renderRunnable = - bitmapFactoryVideoRenderer.getRenderRunnable(); - renderRunnable.sleepFunction = sleep; - bitmapFactoryVideoRenderer.render(0L, 0L); - // There is a bug in Robolectric where it doesn't handle null images, - // so we won't get our Exception - ShadowBitmapFactory.setAllowInvalidImageData(false); - try { - renderRunnable.run(); - } catch (RuntimeException e) { - Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage()); - } - ShadowLooper.idleMainLooper(); - Assert.assertTrue(fakeEventListener.getVideoCodecError() instanceof NullPointerException); - - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java b/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java deleted file mode 100644 index ebc70e3f94..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.google.android.exoplayer2.video; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.decoder.DecoderCounters; - -public class FakeEventListener implements VideoRendererEventListener { - @Nullable - VideoSize videoSize; - @Nullable - DecoderCounters decoderCounters; - - private long firstFrameRenderMs = Long.MIN_VALUE; - - private int droppedFrames; - - private Exception videoCodecError; - - @Override - public void onVideoSizeChanged(VideoSize videoSize) { - this.videoSize = videoSize; - } - - public boolean isVideoEnabled() { - return decoderCounters != null; - } - - @Override - public void onVideoEnabled(DecoderCounters counters) { - decoderCounters = counters; - } - - @Override - public void onVideoDisabled(DecoderCounters counters) { - decoderCounters = null; - } - - public long getFirstFrameRenderMs() { - return firstFrameRenderMs; - } - - @Override - public void onRenderedFirstFrame(Object output, long renderTimeMs) { - firstFrameRenderMs = renderTimeMs; - } - - public int getDroppedFrames() { - return droppedFrames; - } - - @Override - public void onDroppedFrames(int count, long elapsedMs) { - droppedFrames+=count; - } - - public Exception getVideoCodecError() { - return videoCodecError; - } - - @Override - public void onVideoCodecError(Exception videoCodecError) { - this.videoCodecError = videoCodecError; - } - -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java b/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java deleted file mode 100644 index c107832310..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.google.android.exoplayer2.video; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.view.Surface; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import org.robolectric.annotation.Implements; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowSurface; - -@Implements(Surface.class) -public class ShadowSurfaceExtended extends ShadowSurface { - private final Semaphore postSemaphore = new Semaphore(0); - private int width; - private int height; - - public static Surface newInstance() { - return Shadow.newInstanceOf(Surface.class); - } - - public void setSize(final int width, final int height) { - this.width = width; - this.height = height; - } - - public Canvas lockCanvas(Rect canvas) { - return new Canvas(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)); - } - - public void unlockCanvasAndPost(Canvas canvas) { - postSemaphore.release(); - } - - public boolean waitForPost(long millis) { - try { - return postSemaphore.tryAcquire(millis, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return false; - } - } -} diff --git a/testdata/src/test/assets/media/jpeg/image-320-240.jpg b/testdata/src/test/assets/media/jpeg/image-320-240.jpg deleted file mode 100644 index d1796c66fdb1869e074b43d4b9964d8257709452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36039 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�oMa*=h2irbTvr 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 Date: Tue, 1 Feb 2022 20:06:15 -0700 Subject: [PATCH 50/70] Revert random change --- .../com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java index 3d92566d3c..f0826ce35b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java @@ -32,8 +32,7 @@ import java.util.List; * A {@link SubtitleView.Output} that uses Android's native layout framework via {@link * SubtitlePainter}. */ -/* package */ final class -CanvasSubtitleOutput extends View implements SubtitleView.Output { +/* package */ final class CanvasSubtitleOutput extends View implements SubtitleView.Output { private final List painters; From 1528b8b5ee4c43036253d1d2ab674ee65fe44ba8 Mon Sep 17 00:00:00 2001 From: Dustin Date: Wed, 2 Feb 2022 14:35:52 -0700 Subject: [PATCH 51/70] Found out RESULT_SEEK is a bad thing. Greatly improved Extractor efficiency. --- .../extractor/avi/AviExtractor.java | 52 +++++++++---------- .../extractor/avi/StreamHeaderBox.java | 3 +- .../extractor/avi/AviExtractorRoboTest.java | 18 +++++++ .../extractor/avi/AviExtractorTest.java | 44 ++++------------ .../extractor/avi/AviSeekMapTest.java | 5 +- .../extractor/avi/StreamHeaderBoxTest.java | 1 + 6 files changed, 59 insertions(+), 64 deletions(-) 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 index dd06a90c26..ded38ab0a6 100644 --- 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 @@ -72,15 +72,6 @@ public class AviExtractor implements Extractor { } } - static int alignPositionHolder(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) { - final long position = input.getPosition(); - if ((position & 1) == 1) { - seekPosition.position = position + 1; - return RESULT_SEEK; - } - return RESULT_CONTINUE; - } - @NonNull static ByteBuffer allocate(int bytes) { final byte[] buffer = new byte[bytes]; @@ -503,47 +494,56 @@ public class AviExtractor implements Extractor { return null; } - int readSamples(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { + int readSamples(@NonNull ExtractorInput input) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { chunkHandler = null; - return alignPositionHolder(input, seekPosition); + alignInput(input); } } else { - ByteBuffer byteBuffer = allocate(8); + final int toRead = 8; + ByteBuffer byteBuffer = allocate(toRead); final byte[] bytes = byteBuffer.array(); alignInput(input); - input.readFully(bytes, 0, 1); + input.readFully(bytes, 0, toRead); + //This is super inefficient, but should be rare while (bytes[0] == 0) { - input.readFully(bytes, 0, 1); + for (int i=1;i= moviEnd) { - return RESULT_END_OF_INPUT; - } - input.readFully(bytes, 1, 7); final int chunkId = byteBuffer.getInt(); if (chunkId == ListBox.LIST) { - seekPosition.position = input.getPosition() + 8; - return RESULT_SEEK; + input.skipFully(8); + return RESULT_CONTINUE; } final int size = byteBuffer.getInt(); if (chunkId == JUNK) { - seekPosition.position = alignPosition(input.getPosition() + size); - return RESULT_SEEK; + input.skipFully(size); + alignInput(input); + return RESULT_CONTINUE; } final AviTrack aviTrack = getAviTrack(chunkId); if (aviTrack == null) { - seekPosition.position = alignPosition(input.getPosition() + size); + input.skipFully(size); + alignInput(input); w("Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + " size=" + size + " moviEnd=" + moviEnd); - return RESULT_SEEK; + return RESULT_CONTINUE; } if (aviTrack.newChunk(chunkId, size, input)) { - return alignPositionHolder(input, seekPosition); + alignInput(input); } else { chunkHandler = aviTrack; } } + if (input.getPosition() == input.getLength()) { + return C.RESULT_END_OF_INPUT; + } return RESULT_CONTINUE; } @@ -551,7 +551,7 @@ public class AviExtractor implements Extractor { public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { switch (state) { case STATE_READ_SAMPLES: - return readSamples(input, seekPosition); + return readSamples(input); case STATE_SEEK_START: state = STATE_READ_SAMPLES; seekPosition.position = moviOffset + 4; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 5486d43d7f..58a8859efa 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; /** @@ -46,7 +47,7 @@ public class StreamHeaderBox extends ResidentBox { } public long getDurationUs() { - return 1_000_000L * getScale() * getLength() / getRate(); + return C.MICROS_PER_SECOND * getScale() * getLength() / getRate(); } public int getSteamType() { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java index acfd3ae6b1..6e85eeaa5d 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java @@ -112,4 +112,22 @@ public class AviExtractorRoboTest { Assert.assertEquals(aviTrack.getClock().durationUs, streamHeaderBox.getDurationUs()); } + @Test + public void readSamples_fragmentedChunk() throws IOException { + AviExtractor aviExtractor = AviExtractorTest.setupVideoAviExtractor(); + final AviTrack aviTrack = aviExtractor.getVideoTrack(); + final int size = 24 + 16; + final ByteBuffer byteBuffer = AviExtractor.allocate(size + 8); + byteBuffer.putInt(aviTrack.chunkId); + byteBuffer.putInt(size); + + final ExtractorInput chunk = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + setSimulatePartialReads(true).build(); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder())); + + Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(chunk, new PositionHolder())); + + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; + Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index c6a4d7842d..ead5808eaa 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -303,28 +303,6 @@ public class AviExtractorTest { Assert.assertEquals(0, aviExtractor.aviSeekMap.seekOffset); } - @Test - public void alignPositionHolder_givenOddPosition() { - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). - setData(new byte[4]).build(); - fakeExtractorInput.setPosition(1); - final PositionHolder positionHolder = new PositionHolder(); - final int result = AviExtractor.alignPositionHolder(fakeExtractorInput, positionHolder); - Assert.assertEquals(Extractor.RESULT_SEEK, result); - Assert.assertEquals(2, positionHolder.position); - } - - @Test - public void alignPositionHolder_givenEvenPosition() { - - final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder(). - setData(new byte[4]).build(); - fakeExtractorInput.setPosition(2); - final PositionHolder positionHolder = new PositionHolder(); - final int result = AviExtractor.alignPositionHolder(fakeExtractorInput, positionHolder); - Assert.assertEquals(Extractor.RESULT_CONTINUE, result); - } - @Test public void readHeaderList_givenBadHeader() throws IOException { final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[32]).build(); @@ -405,7 +383,7 @@ public class AviExtractorTest { Assert.assertEquals(64 * 1024 + 8, positionHolder.position); } - private AviExtractor setupVideoAviExtractor() { + static AviExtractor setupVideoAviExtractor() { final AviExtractor aviExtractor = new AviExtractor(); aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); @@ -444,31 +422,27 @@ public class AviExtractorTest { final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) .build(); - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(input, new PositionHolder())); + Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder())); final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length); } @Test - public void readSamples_fragmentedChunk() throws IOException { + public void readSamples_givenLeadingZeros() throws IOException { AviExtractor aviExtractor = setupVideoAviExtractor(); final AviTrack aviTrack = aviExtractor.getVideoTrack(); - final int size = 24 + 16; - final ByteBuffer byteBuffer = AviExtractor.allocate(32); + final ByteBuffer byteBuffer = AviExtractor.allocate(48); + byteBuffer.position(16); byteBuffer.putInt(aviTrack.chunkId); - byteBuffer.putInt(size); + byteBuffer.putInt(24); - final ExtractorInput chunk0 = new FakeExtractorInput.Builder().setData(byteBuffer.array()) + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()) .build(); - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk0, new PositionHolder())); - - final ExtractorInput chunk1 = new FakeExtractorInput.Builder().setData(new byte[16]) - .build(); - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk1, new PositionHolder())); + Assert.assertEquals(Extractor.RESULT_END_OF_INPUT, aviExtractor.read(input, new PositionHolder())); final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) aviTrack.trackOutput; - Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); + Assert.assertEquals(24, fakeTrackOutput.getSampleData(0).length); } @Test diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java index 8328d9a3f5..91aded8755 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.avi; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; import org.junit.Assert; import org.junit.Test; @@ -29,7 +30,7 @@ public class AviSeekMapTest { final AviTrack[] aviTracks = new AviTrack[]{DataHelper.getVideoAviTrack(secs), DataHelper.getAudioAviTrack(secs)}; - aviSeekMap.setFrames(position, 1_000_000L, aviTracks); + aviSeekMap.setFrames(position, C.MICROS_PER_SECOND, aviTracks); for (int i=0;i= chunksPerKeyFrame) { + if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) { keyFrameOffsetsDiv2.add(offset / 2); - for (AviTrack seekTrack : aviTracks) { + for (ChunkHandler seekTrack : chunkHandlers) { if (seekTrack != null) { seekIndexes[seekTrack.getId()].add(seekTrack.chunks); } } } } - keyFrameCounts[aviTrack.getId()]++; + keyFrameCounts[chunkHandler.getId()]++; } - aviTrack.chunks++; - aviTrack.size+=size; + chunkHandler.chunks++; + chunkHandler.size+=size; } indexByteBuffer.compact(); } if (videoTrack.chunks == keyFrameCounts[videoTrack.getId()]) { - videoTrack.setKeyFrames(AviTrack.ALL_KEY_FRAMES); + videoTrack.setKeyFrames(ChunkHandler.ALL_KEY_FRAMES); } else { videoTrack.setKeyFrames(seekIndexes[videoId].getArray()); } @@ -485,16 +486,16 @@ public class AviExtractor implements Extractor { @Nullable @VisibleForTesting - AviTrack getAviTrack(int chunkId) { - for (AviTrack aviTrack : aviTracks) { - if (aviTrack != null && aviTrack.handlesChunkId(chunkId)) { - return aviTrack; + ChunkHandler getChunkHandler(int chunkId) { + for (ChunkHandler chunkHandler : chunkHandlers) { + if (chunkHandler != null && chunkHandler.handlesChunkId(chunkId)) { + return chunkHandler; } } return null; } - int readSamples(@NonNull ExtractorInput input) throws IOException { + int readChunks(@NonNull ExtractorInput input) throws IOException { if (chunkHandler != null) { if (chunkHandler.resume(input)) { chunkHandler = null; @@ -527,21 +528,21 @@ public class AviExtractor implements Extractor { alignInput(input); return RESULT_CONTINUE; } - final AviTrack aviTrack = getAviTrack(chunkId); - if (aviTrack == null) { + final ChunkHandler chunkHandler = getChunkHandler(chunkId); + if (chunkHandler == null) { input.skipFully(size); alignInput(input); w("Unknown tag=" + toString(chunkId) + " pos=" + (input.getPosition() - 8) + " size=" + size + " moviEnd=" + moviEnd); return RESULT_CONTINUE; } - if (aviTrack.newChunk(chunkId, size, input)) { + if (chunkHandler.newChunk(chunkId, size, input)) { alignInput(input); } else { - chunkHandler = aviTrack; + this.chunkHandler = chunkHandler; } } - if (input.getPosition() == input.getLength()) { + if (input.getPosition() >= moviEnd) { return C.RESULT_END_OF_INPUT; } return RESULT_CONTINUE; @@ -550,10 +551,10 @@ public class AviExtractor implements Extractor { @Override public int read(@NonNull ExtractorInput input, @NonNull PositionHolder seekPosition) throws IOException { switch (state) { - case STATE_READ_SAMPLES: - return readSamples(input); + case STATE_READ_CHUNKS: + return readChunks(input); case STATE_SEEK_START: - state = STATE_READ_SAMPLES; + state = STATE_READ_CHUNKS; seekPosition.position = moviOffset + 4; return RESULT_SEEK; case STATE_READ_TRACKS: @@ -573,7 +574,7 @@ public class AviExtractor implements Extractor { output.seekMap(new SeekMap.Unseekable(getDuration())); } seekPosition.position = moviOffset + 4; - state = STATE_READ_SAMPLES; + state = STATE_READ_CHUNKS; return RESULT_SEEK; } } @@ -591,15 +592,15 @@ public class AviExtractor implements Extractor { } } else { if (aviSeekMap != null) { - aviSeekMap.setFrames(position, timeUs, aviTracks); + aviSeekMap.setFrames(position, timeUs, chunkHandlers); } } } void resetClocks() { - for (@Nullable AviTrack aviTrack : aviTracks) { - if (aviTrack != null) { - aviTrack.getClock().setIndex(0); + for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { + if (chunkHandler != null) { + chunkHandler.getClock().setIndex(0); } } } @@ -610,8 +611,8 @@ public class AviExtractor implements Extractor { } @VisibleForTesting - void setAviTracks(AviTrack[] aviTracks) { - this.aviTracks = aviTracks; + void setChunkHandlers(ChunkHandler[] chunkHandlers) { + this.chunkHandlers = chunkHandlers; } @VisibleForTesting(otherwise = VisibleForTesting.NONE) @@ -626,13 +627,13 @@ public class AviExtractor implements Extractor { } @VisibleForTesting(otherwise = VisibleForTesting.NONE) - AviTrack getChunkHandler() { + ChunkHandler getChunkHandler() { return chunkHandler; } @VisibleForTesting(otherwise = VisibleForTesting.NONE) - void setChunkHandler(final AviTrack aviTrack) { - chunkHandler = aviTrack; + void setChunkHandler(final ChunkHandler chunkHandler) { + this.chunkHandler = chunkHandler; } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index fe662ca615..edd8f87683 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -99,17 +99,18 @@ public class AviSeekMap implements SeekMap { //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); } - public void setFrames(final long position, final long timeUs, final AviTrack[] aviTracks) { + public void setFrames(final long position, final long timeUs, final ChunkHandler[] chunkHandlers) { final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2)); if (index < 0) { throw new IllegalArgumentException("Position: " + position); } - for (int i=0;i Date: Fri, 4 Feb 2022 19:52:10 -0700 Subject: [PATCH 55/70] Optimize AvcChunkHandler to use normal ChunkClock if no B Frames. --- .../extractor/avi/AvcChunkHandler.java | 60 +++++++++++++------ .../android/exoplayer2/util/NalUnitUtil.java | 6 +- .../extractor/avi/AvcChunkPeekerTest.java | 13 ++-- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java index 90bc6bb351..368a11fcbf 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.avi; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -43,28 +44,36 @@ public class AvcChunkHandler extends NalChunkPeeker { public AvcChunkHandler(int id, @NonNull TrackOutput trackOutput, @NonNull ChunkClock clock, Format.Builder formatBuilder) { - super(id, trackOutput, new PicCountClock(clock.durationUs, clock.chunks), 16); + super(id, trackOutput, clock, 16); this.formatBuilder = formatBuilder; } - @NonNull - @Override - public PicCountClock getClock() { - return (PicCountClock) clock; + @Nullable + @VisibleForTesting + PicCountClock getPicCountClock() { + if (clock instanceof PicCountClock) { + return (PicCountClock)clock; + } else { + return null; + } } @Override boolean skip(byte nalType) { - return false; + if (clock instanceof PicCountClock) { + return false; + } else { + //If the clock is regular clock, skip "normal" frames + return nalType >= 0 && nalType <= NAL_TYPE_IDR; + } } /** * Greatly simplified way to calculate the picOrder * Full logic is here * https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/video/h264_poc.cc - * @param nalTypeOffset */ - void updatePicCountClock(final int nalTypeOffset) { + void updatePicCountClock(final int nalTypeOffset, final PicCountClock picCountClock) { final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length); //slide_header() in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice @@ -84,10 +93,10 @@ public class AvcChunkHandler extends NalChunkPeeker { if (spsData.picOrderCountType == 0) { int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); - getClock().setPicCount(picOrderCountLsb); + picCountClock.setPicCount(picOrderCountLsb); return; } else if (spsData.picOrderCountType == 2) { - getClock().setPicCount(frameNum); + picCountClock.setPicCount(frameNum); return; } clock.setIndex(clock.getIndex()); @@ -98,11 +107,20 @@ public class AvcChunkHandler extends NalChunkPeeker { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); - if (spsData.picOrderCountType == 0) { - getClock().setMaxPicCount(1 << spsData.picOrderCntLsbLength, 2); - } else if (spsData.picOrderCountType == 2) { - //Plus one because we double the frame number - getClock().setMaxPicCount(1 << spsData.frameNumLength, 1); + //If we have B Frames, upgrade to PicCountClock + final PicCountClock picCountClock; + if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) { + clock = picCountClock = new PicCountClock(clock.durationUs, clock.chunks); + } else { + picCountClock = getPicCountClock(); + } + if (picCountClock != null) { + if (spsData.picOrderCountType == 0) { + picCountClock.setMaxPicCount(1 << spsData.picOrderCntLsbLength, 2); + } else if (spsData.picOrderCountType == 2) { + //Plus one because we double the frame number + picCountClock.setMaxPicCount(1 << spsData.frameNumLength, 1); + } } if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) { pixelWidthHeightRatio = spsData.pixelWidthHeightRatio; @@ -121,11 +139,17 @@ public class AvcChunkHandler extends NalChunkPeeker { case 2: case 3: case 4: - updatePicCountClock(nalTypeOffset); + if (clock instanceof PicCountClock) { + updatePicCountClock(nalTypeOffset, (PicCountClock)clock); + } return; - case NAL_TYPE_IDR: - getClock().syncIndexes(); + case NAL_TYPE_IDR: { + final PicCountClock picCountClock = getPicCountClock(); + if (picCountClock != null) { + picCountClock.syncIndexes(); + } return; + } case NAL_TYPE_AUD: case NAL_TYPE_SEI: case NAL_TYPE_PPS: { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index 44014bda8e..a1d290b5e5 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -33,6 +33,7 @@ public final class NalUnitUtil { public final int constraintsFlagsAndReservedZero2Bits; public final int levelIdc; public final int seqParameterSetId; + public final int maxNumRefFrames; public final int width; public final int height; public final float pixelWidthHeightRatio; @@ -48,6 +49,7 @@ public final class NalUnitUtil { int constraintsFlagsAndReservedZero2Bits, int levelIdc, int seqParameterSetId, + int maxNumRefFrames, int width, int height, float pixelWidthHeightRatio, @@ -61,6 +63,7 @@ public final class NalUnitUtil { this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits; this.levelIdc = levelIdc; this.seqParameterSetId = seqParameterSetId; + this.maxNumRefFrames = maxNumRefFrames; this.width = width; this.height = height; this.pixelWidthHeightRatio = pixelWidthHeightRatio; @@ -367,7 +370,7 @@ public final class NalUnitUtil { data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] } } - data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames + int maxNumRefFrames = data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames data.skipBit(); // gaps_in_frame_num_value_allowed_flag int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1; @@ -427,6 +430,7 @@ public final class NalUnitUtil { constraintsFlagsAndReservedZero2Bits, levelIdc, seqParameterSetId, + maxNumRefFrames, frameWidth, frameHeight, pixelWidthHeightRatio, diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java index bb0ab70263..82716243ba 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AvcChunkPeekerTest.java @@ -35,8 +35,8 @@ public class AvcChunkPeekerTest { setSampleMimeType(MimeTypes.VIDEO_H264). setWidth(1280).setHeight(720).setFrameRate(24000f/1001f); - private static final byte[] P_SLICE = {00,00,00,01,0x41,(byte)0x9A,0x13,0x36,0x21,0x3A,0x5F, - (byte)0xFE,(byte)0x9E,0x10,00,00}; + private static final byte[] P_SLICE = {0,0,0,1,0x41,(byte)0x9A,0x13,0x36,0x21,0x3A,0x5F, + (byte)0xFE,(byte)0x9E,0x10,0,0}; private FakeTrackOutput fakeTrackOutput; private AvcChunkHandler avcChunkPeeker; @@ -61,19 +61,20 @@ public class AvcChunkPeekerTest { @Test public void peek_givenStreamHeader() throws IOException { peekStreamHeader(); - final PicCountClock picCountClock = avcChunkPeeker.getClock(); + final PicCountClock picCountClock = avcChunkPeeker.getPicCountClock(); + Assert.assertNotNull(picCountClock); Assert.assertEquals(64, picCountClock.getMaxPicCount()); Assert.assertEquals(0, avcChunkPeeker.getSpsData().picOrderCountType); Assert.assertEquals(1.18f, fakeTrackOutput.lastFormat.pixelWidthHeightRatio, 0.01f); } @Test - public void peek_givenStreamHeaderAndPSlice() throws IOException { + public void newChunk_givenStreamHeaderAndPSlice() throws IOException { peekStreamHeader(); - final PicCountClock picCountClock = avcChunkPeeker.getClock(); + final PicCountClock picCountClock = avcChunkPeeker.getPicCountClock(); final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build(); - avcChunkPeeker.peek(input, P_SLICE.length); + avcChunkPeeker.newChunk(0, P_SLICE.length, input); Assert.assertEquals(12, picCountClock.getLastPicCount()); } From 84d3f62e8806b7eee64aa88da2be38e74533c4dc Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 5 Feb 2022 07:06:25 -0700 Subject: [PATCH 56/70] Fix DefaultExtractorsFactoryTest --- .../exoplayer2/extractor/DefaultExtractorsFactoryTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java index 1c6ce7b70c..b7dbb58f7f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; 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; @@ -69,6 +70,7 @@ public final class DefaultExtractorsFactoryTest { AdtsExtractor.class, Ac3Extractor.class, Ac4Extractor.class, + AviExtractor.class, Mp3Extractor.class, JpegExtractor.class) .inOrder(); @@ -112,6 +114,7 @@ public final class DefaultExtractorsFactoryTest { AdtsExtractor.class, Ac3Extractor.class, Ac4Extractor.class, + AviExtractor.class, JpegExtractor.class) .inOrder(); } From 14c842e5030e0958df74498b38eaac030204d88a Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 5 Feb 2022 07:06:58 -0700 Subject: [PATCH 57/70] Add coverage for max ref frames --- .../java/com/google/android/exoplayer2/util/NalUnitUtilTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java index 483f70fd96..ed5a62f7c5 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java @@ -134,6 +134,7 @@ public final class NalUnitUtilTest { assertThat(data.pixelWidthHeightRatio).isEqualTo(1.0f); assertThat(data.picOrderCountType).isEqualTo(0); assertThat(data.separateColorPlaneFlag).isFalse(); + assertThat(data.maxNumRefFrames).isEqualTo(4); } @Test From 3a9f1f9a34190748315cb1a49d6f517a59ef822a Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 5 Feb 2022 07:09:29 -0700 Subject: [PATCH 58/70] Improved comments, improved naming consistency. --- .../extractor/avi/AvcChunkHandler.java | 10 ++- .../extractor/avi/AviExtractor.java | 18 ++--- .../exoplayer2/extractor/avi/AviSeekMap.java | 17 +++-- .../extractor/avi/ChunkHandler.java | 74 ++++++++++++++----- .../extractor/avi/Mp4vChunkHandler.java | 2 +- ...andler.java => MpegAudioChunkHandler.java} | 65 ++++++++++------ ...lChunkPeeker.java => NalChunkHandler.java} | 8 +- .../exoplayer2/extractor/avi/ResidentBox.java | 8 -- .../extractor/avi/StreamNameBox.java | 2 +- .../extractor/avi/AvcChunkPeekerTest.java | 14 ++-- .../extractor/avi/AviSeekMapTest.java | 10 +-- .../extractor/avi/ChunkHandlerTest.java | 30 -------- ...nkPeeker.java => MockNalChunkHandler.java} | 4 +- .../extractor/avi/NalChunkPeekerTest.java | 9 +-- 14 files changed, 147 insertions(+), 124 deletions(-) rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{Mp3ChunkHandler.java => MpegAudioChunkHandler.java} (65%) rename library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/{NalChunkPeeker.java => NalChunkHandler.java} (93%) delete mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/ChunkHandlerTest.java rename library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/{MockNalChunkPeeker.java => MockNalChunkHandler.java} (90%) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java index 368a11fcbf..0d67032d53 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java @@ -29,7 +29,7 @@ import java.io.IOException; * Corrects the time and PAR for H264 streams * AVC is very rare in AVI due to the rise of the mp4 container */ -public class AvcChunkHandler extends NalChunkPeeker { +public class AvcChunkHandler extends NalChunkHandler { private static final int NAL_TYPE_MASK = 0x1f; private static final int NAL_TYPE_IDR = 5; //I Frame private static final int NAL_TYPE_SEI = 6; @@ -63,7 +63,7 @@ public class AvcChunkHandler extends NalChunkPeeker { if (clock instanceof PicCountClock) { return false; } else { - //If the clock is regular clock, skip "normal" frames + //If the clock is ChunkClock, skip "normal" frames return nalType >= 0 && nalType <= NAL_TYPE_IDR; } } @@ -107,10 +107,12 @@ public class AvcChunkHandler extends NalChunkPeeker { final int spsStart = nalTypeOffset + 1; nalTypeOffset = seekNextNal(input, spsStart); spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos); - //If we have B Frames, upgrade to PicCountClock + //If we can have B Frames, upgrade to PicCountClock final PicCountClock picCountClock; if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) { - clock = picCountClock = new PicCountClock(clock.durationUs, clock.chunks); + picCountClock = new PicCountClock(clock.durationUs, clock.chunks); + picCountClock.setIndex(clock.getIndex()); + clock = picCountClock; } else { picCountClock = getPicCountClock(); } 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 index 3831210a15..a854e3b86f 100644 --- 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 @@ -291,7 +291,7 @@ public class AviExtractor implements Extractor { } trackOutput.format(builder.build()); if (MimeTypes.AUDIO_MPEG.equals(mimeType)) { - chunkHandler = new Mp3ChunkHandler(streamId, trackOutput, clock, + chunkHandler = new MpegAudioChunkHandler(streamId, trackOutput, clock, audioFormat.getSamplesPerSecond()); } else { chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_AUDIO, @@ -367,7 +367,7 @@ public class AviExtractor implements Extractor { } void fixTimings(final int[] keyFrameCounts, final long videoDuration) { - for (final ChunkHandler chunkHandler : chunkHandlers) { + for (@Nullable final ChunkHandler chunkHandler : chunkHandlers) { if (chunkHandler != null) { if (chunkHandler.isAudio()) { final long durationUs = chunkHandler.getClock().durationUs; @@ -452,7 +452,7 @@ public class AviExtractor implements Extractor { int indexSize = seekIndexes[videoId].getSize(); if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) { keyFrameOffsetsDiv2.add(offset / 2); - for (ChunkHandler seekTrack : chunkHandlers) { + for (@Nullable ChunkHandler seekTrack : chunkHandlers) { if (seekTrack != null) { seekIndexes[seekTrack.getId()].add(seekTrack.chunks); } @@ -487,7 +487,7 @@ public class AviExtractor implements Extractor { @Nullable @VisibleForTesting ChunkHandler getChunkHandler(int chunkId) { - for (ChunkHandler chunkHandler : chunkHandlers) { + for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { if (chunkHandler != null && chunkHandler.handlesChunkId(chunkId)) { return chunkHandler; } @@ -536,7 +536,7 @@ public class AviExtractor implements Extractor { + " size=" + size + " moviEnd=" + moviEnd); return RESULT_CONTINUE; } - if (chunkHandler.newChunk(chunkId, size, input)) { + if (chunkHandler.newChunk(size, input)) { alignInput(input); } else { this.chunkHandler = chunkHandler; @@ -587,20 +587,20 @@ public class AviExtractor implements Extractor { chunkHandler = null; if (position <= 0) { if (moviOffset != 0) { - resetClocks(); + setIndexes(new int[chunkHandlers.length]); state = STATE_SEEK_START; } } else { if (aviSeekMap != null) { - aviSeekMap.setFrames(position, timeUs, chunkHandlers); + setIndexes(aviSeekMap.getIndexes(position)); } } } - void resetClocks() { + private void setIndexes(@NonNull int[] indexes) { for (@Nullable ChunkHandler chunkHandler : chunkHandlers) { if (chunkHandler != null) { - chunkHandler.getClock().setIndex(0); + chunkHandler.setIndex(indexes[chunkHandler.getId()]); } } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index edd8f87683..f709a68e82 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -99,18 +99,23 @@ public class AviSeekMap implements SeekMap { //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); } - public void setFrames(final long position, final long timeUs, final ChunkHandler[] chunkHandlers) { + /** + * Get the ChunkClock indexes by stream id + * @param position seek position in the file + */ + @NonNull + public int[] getIndexes(final long position) { final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2)); if (index < 0) { throw new IllegalArgumentException("Position: " + position); } - for (int i=0;i Date: Sat, 5 Feb 2022 09:32:19 -0700 Subject: [PATCH 59/70] MpegAudioChunkHandler cleanup and Tests --- .../extractor/avi/MpegAudioChunkHandler.java | 31 +++-- .../avi/MpegAudioChunkHandlerTest.java | 116 ++++++++++++++++++ .../assets/extractordumps/avi/frame.mp3.dump | Bin 0 -> 417 bytes 3 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java create mode 100644 testdata/src/test/assets/extractordumps/avi/frame.mp3.dump diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java index 59a7aff775..ac10b3dcb7 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java @@ -51,7 +51,12 @@ public class MpegAudioChunkHandler extends ChunkHandler { syncTime(); return true; } - chunkRemaining = size; + this.size = chunkRemaining = size; + return resume(input); + } + + @Override + boolean resume(@NonNull ExtractorInput input) throws IOException { if (process(input)) { // Fail Over: If the scratch is the entire chunk, we didn't find a MP3 header. // Dump the chunk as is and hope the decoder can handle it. @@ -59,17 +64,8 @@ public class MpegAudioChunkHandler extends ChunkHandler { scratch.setPosition(0); trackOutput.sampleData(scratch, size); scratch.reset(0); + done(size); } - clock.advance(); - return true; - } - return false; - } - - @Override - boolean resume(@NonNull ExtractorInput input) throws IOException { - if (process(input)) { - clock.advance(); return true; } return false; @@ -101,7 +97,6 @@ public class MpegAudioChunkHandler extends ChunkHandler { scratch.ensureCapacity(scratch.limit() + chunkRemaining); int toRead = 4; while (chunkRemaining > 0 && readScratch(input, toRead) != C.RESULT_END_OF_INPUT) { - readScratch(input, toRead); while (scratch.bytesLeft() >= 4) { if (header.setForHeaderData(scratch.readInt())) { scratch.skipBytes(-4); @@ -127,7 +122,7 @@ public class MpegAudioChunkHandler extends ChunkHandler { trackOutput.sampleData(scratch, scratchBytes); frameRemaining = header.frameSize - scratchBytes; } else { - return chunkRemaining == 0; + return true; } } final int bytes = trackOutput.sampleData(input, Math.min(frameRemaining, chunkRemaining), false); @@ -151,4 +146,14 @@ public class MpegAudioChunkHandler extends ChunkHandler { timeUs = clock.getUs(); frameRemaining = 0; } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + long getTimeUs() { + return timeUs; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + int getFrameRemaining() { + return frameRemaining; + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java new file mode 100644 index 0000000000..c37f370331 --- /dev/null +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandlerTest.java @@ -0,0 +1,116 @@ +package com.google.android.exoplayer2.extractor.avi; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.MpegAudioUtil; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MpegAudioChunkHandlerTest { + private static final int FPS = 24; + private Format MP3_FORMAT = new Format.Builder().setChannelCount(2). + setSampleMimeType(MimeTypes.AUDIO_MPEG).setSampleRate(44100).build(); + private static final long CHUNK_MS = C.MICROS_PER_SECOND / FPS; + private final MpegAudioUtil.Header header = new MpegAudioUtil.Header(); + private FakeTrackOutput fakeTrackOutput; + private MpegAudioChunkHandler mpegAudioChunkHandler; + private byte[] mp3Frame; + private long frameUs; + + @Before + public void before() throws IOException { + fakeTrackOutput = new FakeTrackOutput(false); + fakeTrackOutput.format(MP3_FORMAT); + mpegAudioChunkHandler = new MpegAudioChunkHandler(0, fakeTrackOutput, + new ChunkClock(C.MICROS_PER_SECOND, FPS), MP3_FORMAT.sampleRate); + + if (mp3Frame == null) { + final Context context = ApplicationProvider.getApplicationContext(); + mp3Frame = TestUtil.getByteArray(context,"extractordumps/avi/frame.mp3.dump"); + header.setForHeaderData(ByteBuffer.wrap(mp3Frame).getInt()); + //About 26ms + frameUs = header.samplesPerFrame * C.MICROS_PER_SECOND / header.sampleRate; + } + } + + @Test + public void newChunk_givenNonMpegData() throws IOException { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[1024]). + build(); + + mpegAudioChunkHandler.newChunk((int)input.getLength(), input); + Assert.assertEquals(1024, fakeTrackOutput.getSampleData(0).length); + Assert.assertEquals(CHUNK_MS, mpegAudioChunkHandler.getClock().getUs()); + } + @Test + public void newChunk_givenEmptyChunk() throws IOException { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]). + build(); + mpegAudioChunkHandler.newChunk((int)input.getLength(), input); + Assert.assertEquals(C.MICROS_PER_SECOND / 24, mpegAudioChunkHandler.getClock().getUs()); + } + + @Test + public void setIndex_given12frames() { + mpegAudioChunkHandler.setIndex(12); + Assert.assertEquals(500_000L, mpegAudioChunkHandler.getTimeUs()); + } + + @Test + public void newChunk_givenSingleFrame() throws IOException { + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(mp3Frame).build(); + + mpegAudioChunkHandler.newChunk(mp3Frame.length, input); + Assert.assertArrayEquals(mp3Frame, fakeTrackOutput.getSampleData(0)); + Assert.assertEquals(frameUs, mpegAudioChunkHandler.getTimeUs()); + } + + @Test + public void newChunk_givenSeekAndFragmentedFrames() throws IOException { + ByteBuffer byteBuffer = ByteBuffer.allocate(mp3Frame.length * 2); + byteBuffer.put(mp3Frame, mp3Frame.length / 2, mp3Frame.length / 2); + byteBuffer.put(mp3Frame); + final int remainder = byteBuffer.remaining(); + byteBuffer.put(mp3Frame, 0, remainder); + + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + build(); + + mpegAudioChunkHandler.setIndex(1); //Seek + Assert.assertFalse(mpegAudioChunkHandler.newChunk(byteBuffer.capacity(), input)); + Assert.assertArrayEquals(mp3Frame, fakeTrackOutput.getSampleData(0)); + Assert.assertEquals(frameUs + CHUNK_MS, mpegAudioChunkHandler.getTimeUs()); + + Assert.assertTrue(mpegAudioChunkHandler.resume(input)); + Assert.assertEquals(header.frameSize - remainder, mpegAudioChunkHandler.getFrameRemaining()); + } + + @Test + public void newChunk_givenTwoFrames() throws IOException { + ByteBuffer byteBuffer = ByteBuffer.allocate(mp3Frame.length * 2); + byteBuffer.put(mp3Frame); + byteBuffer.put(mp3Frame); + + final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + build(); + Assert.assertFalse(mpegAudioChunkHandler.newChunk(byteBuffer.capacity(), input)); + Assert.assertEquals(1, fakeTrackOutput.getSampleCount()); + Assert.assertEquals(0L, fakeTrackOutput.getSampleTimeUs(0)); + + Assert.assertTrue(mpegAudioChunkHandler.resume(input)); + Assert.assertEquals(2, fakeTrackOutput.getSampleCount()); + Assert.assertEquals(frameUs, fakeTrackOutput.getSampleTimeUs(1)); + } +} diff --git a/testdata/src/test/assets/extractordumps/avi/frame.mp3.dump b/testdata/src/test/assets/extractordumps/avi/frame.mp3.dump new file mode 100644 index 0000000000000000000000000000000000000000..2f6ff74b38f808199693c0368f0ea9183c63c317 GIT binary patch literal 417 tcmezWdqN5W{|5$!Oa=x94h9BZ1qKF2AX) Date: Sat, 5 Feb 2022 10:19:26 -0700 Subject: [PATCH 60/70] MpegAudioChunkHandler seek fixes --- .../extractor/avi/MpegAudioChunkHandler.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java index ac10b3dcb7..a66d6d105f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -104,7 +105,9 @@ public class MpegAudioChunkHandler extends ChunkHandler { } scratch.skipBytes(-3); } - toRead = Math.min(chunkRemaining, 128); + // 16 is small, but if we end up reading multiple frames into scratch, things get complicated. + // We should only loop on seek, so this is the lesser of the evils. + toRead = Math.min(chunkRemaining, 16); } return false; } @@ -140,11 +143,17 @@ public class MpegAudioChunkHandler extends ChunkHandler { public void setIndex(int index) { super.setIndex(index); syncTime(); + if (frameRemaining != 0) { + // We have a partial frame in the output, no way to clear it, so just send it as is. + // Next frame should be key frame, so the codec should recover. + trackOutput.sampleMetadata(timeUs, 0, header.frameSize - frameRemaining, + 0, null); + frameRemaining = 0; + } } private void syncTime() { timeUs = clock.getUs(); - frameRemaining = 0; } @VisibleForTesting(otherwise = VisibleForTesting.NONE) From 35c9788695f8140ec1967458cbe69dd80b0e2d1f Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 5 Feb 2022 10:53:32 -0700 Subject: [PATCH 61/70] Remove method I didn't end up needing --- .../exoplayer2/util/ParsableByteArray.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 6c989c5639..79913d2aa9 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -565,22 +565,4 @@ public final class ParsableByteArray { position += length; return value; } - - /** - * The data from the end of the buffer is copied to the front - * The limit() because the bytesLeft() and position is zero - */ - public void compact() { - if (bytesLeft() == 0) { - limit = 0; - } else { - final ByteBuffer byteBuffer = ByteBuffer.wrap(data); - byteBuffer.limit(limit); - byteBuffer.position(position); - byteBuffer.compact(); - byteBuffer.flip(); - limit = byteBuffer.limit(); - } - position = 0; - } } From e7cbb3d50a68ac42c6d2ac6461c897de4b82f094 Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 6 Feb 2022 06:22:15 -0700 Subject: [PATCH 62/70] Add yet another Divx FourCC variant. --- .../android/exoplayer2/extractor/avi/VideoFormat.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 0021647a98..8bda2df4e6 100644 --- 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 @@ -42,11 +42,12 @@ public class VideoFormat { STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); + STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); + STREAM_MAP.put('D' | ('I' << 8) | ('V' << 16) | ('X' << 24), mimeType); + STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); + STREAM_MAP.put('F' | ('M' << 8) | ('P' << 16) | ('4' << 24), mimeType); STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); STREAM_MAP.put(XVID, mimeType); - STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); - STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); - STREAM_MAP.put('F' | ('M' << 8) | ('P' << 16) | ('4' << 24), mimeType); STREAM_MAP.put('M' | ('J' << 8) | ('P' << 16) | ('G' << 24), MimeTypes.VIDEO_MJPEG); STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); } From 05db1717c0034f0a9f4db5b2989ee369e3899a9a Mon Sep 17 00:00:00 2001 From: Dustin Date: Sun, 6 Feb 2022 06:22:41 -0700 Subject: [PATCH 63/70] Remove method I didn't end up needing --- .../exoplayer2/util/ParsableByteArray.java | 18 ------------------ .../extractor/avi/MpegAudioChunkHandler.java | 8 +------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 6c989c5639..79913d2aa9 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -565,22 +565,4 @@ public final class ParsableByteArray { position += length; return value; } - - /** - * The data from the end of the buffer is copied to the front - * The limit() because the bytesLeft() and position is zero - */ - public void compact() { - if (bytesLeft() == 0) { - limit = 0; - } else { - final ByteBuffer byteBuffer = ByteBuffer.wrap(data); - byteBuffer.limit(limit); - byteBuffer.position(position); - byteBuffer.compact(); - byteBuffer.flip(); - limit = byteBuffer.limit(); - } - position = 0; - } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java index a66d6d105f..fed06bd755 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java @@ -143,13 +143,7 @@ public class MpegAudioChunkHandler extends ChunkHandler { public void setIndex(int index) { super.setIndex(index); syncTime(); - if (frameRemaining != 0) { - // We have a partial frame in the output, no way to clear it, so just send it as is. - // Next frame should be key frame, so the codec should recover. - trackOutput.sampleMetadata(timeUs, 0, header.frameSize - frameRemaining, - 0, null); - frameRemaining = 0; - } + frameRemaining = 0; } private void syncTime() { From 3886f5f0b675a3b0b969f051f473689837243fb8 Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 14 Feb 2022 14:35:38 -0700 Subject: [PATCH 64/70] Code Review Changes (cherry picked from commit 135e103faa1bd829df0542a7062e67ebcfc8638f) --- .../android/exoplayer2/util/MimeTypes.java | 2 ++ library/extractor/build.gradle | 5 ---- .../exoplayer2/extractor/avi/AviSeekMap.java | 25 +++++++++++++++---- .../extractor/avi/ChunkHandler.java | 4 +-- .../exoplayer2/extractor/avi/ListBox.java | 2 +- .../exoplayer2/extractor/avi/VideoFormat.java | 5 ++-- .../extractor/avi/AviExtractorTest.java | 9 ++++--- .../extractor/avi/AviSeekMapTest.java | 11 ++++---- .../exoplayer2/extractor/avi/DataHelper.java | 1 + 9 files changed, 39 insertions(+), 25 deletions(-) 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 e033ec31fc..f7c26bbd27 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 @@ -56,6 +56,8 @@ public final class MimeTypes { 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_MJPEG = BASE_TYPE_VIDEO + "/mjpeg"; + public static final String VIDEO_MP42 = BASE_TYPE_VIDEO + "/mp42"; + public static final String VIDEO_MP43 = BASE_TYPE_VIDEO + "/mp43"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; // audio/ MIME types diff --git a/library/extractor/build.gradle b/library/extractor/build.gradle index 0d4ef9abfd..f05134129b 100644 --- a/library/extractor/build.gradle +++ b/library/extractor/build.gradle @@ -19,11 +19,6 @@ android { testCoverageEnabled = true } } - testOptions{ - unitTests.all { - jvmArgs '-noverify' - } - } sourceSets.test.assets.srcDir '../../testdata/src/test/assets/' } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index f709a68e82..5efd155396 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -26,13 +26,13 @@ import java.util.Arrays; * Consists of Video chunk offsets and indexes for all streams */ public class AviSeekMap implements SeekMap { - final int videoId; - final long videoUsPerChunk; - final long duration; + private final int videoId; + private final long videoUsPerChunk; + private final long duration; //These are ints / 2 - final int[] keyFrameOffsetsDiv2; + private final int[] keyFrameOffsetsDiv2; //Seek chunk indexes by streamId - final int[][] seekIndexes; + private final int[][] seekIndexes; /** * Usually the same as moviOffset, but sometimes 0 (muxer bug) */ @@ -118,4 +118,19 @@ public class AviSeekMap implements SeekMap { } return indexes; } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public long getKeyFrameOffsets(int streamId) { + return keyFrameOffsetsDiv2[streamId] * 2L; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public int[] getSeekIndexes(int streamId) { + return seekIndexes[streamId]; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public long getVideoUsPerChunk() { + return videoUsPerChunk; + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java index e764515343..c18c76e496 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java @@ -35,8 +35,8 @@ public class ChunkHandler { */ public static final int[] ALL_KEY_FRAMES = new int[0]; - public static int TYPE_VIDEO = ('d' << 16) | ('c' << 24); - public static int TYPE_AUDIO = ('w' << 16) | ('b' << 24); + public static final int TYPE_VIDEO = ('d' << 16) | ('c' << 24); + public static final int TYPE_AUDIO = ('w' << 16) | ('b' << 24); @NonNull ChunkClock clock; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index daf5b4fc7a..761328c4e1 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java @@ -37,7 +37,7 @@ public class ListBox extends Box { final List children; - ListBox(int size, int listType, List children) { + public ListBox(int size, int listType, List children) { super(LIST, size); this.listType = listType; this.children = children; 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 index 8bda2df4e6..9f12ff813f 100644 --- 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 @@ -32,12 +32,11 @@ public class VideoFormat { static { //Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG final String mimeType = MimeTypes.VIDEO_MP4V; - //final String mimeType = MimeTypes.VIDEO_H263; //I've never seen an Android devices that actually supports MP42 - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42"); + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_MP42); //Samsung seems to support the rare MP43. - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43"); + STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.VIDEO_MP43); STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 205ddab5c6..29500f4edd 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -176,10 +176,10 @@ public class AviExtractorTest { Assert.assertEquals(2 * framesPerKeyFrame, videoTrack.keyFrames[2]); Assert.assertEquals(2 * keyFrameRate * DataHelper.AUDIO_PER_VIDEO, - aviSeekMap.seekIndexes[DataHelper.AUDIO_ID][2]); + aviSeekMap.getSeekIndexes(DataHelper.AUDIO_ID)[2]); Assert.assertEquals(4L + 2 * keyFrameRate * DataHelper.VIDEO_SIZE + 2 * keyFrameRate * DataHelper.AUDIO_SIZE * DataHelper.AUDIO_PER_VIDEO, - aviSeekMap.keyFrameOffsetsDiv2[2] * 2L); + aviSeekMap.getKeyFrameOffsets(2)); } @@ -471,9 +471,10 @@ public class AviExtractorTest { final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); aviExtractor.aviSeekMap = aviSeekMap; final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); - final long position = DataHelper.MOVI_OFFSET + aviSeekMap.keyFrameOffsetsDiv2[1] * 2L; + final long position = DataHelper.MOVI_OFFSET + aviSeekMap.getKeyFrameOffsets(DataHelper.AUDIO_ID); aviExtractor.seek(position, 0L); - Assert.assertEquals(aviSeekMap.seekIndexes[chunkHandler.getId()][1], chunkHandler.getClock().getIndex()); + Assert.assertEquals(aviSeekMap.getSeekIndexes(chunkHandler.getId())[1], + chunkHandler.getClock().getIndex()); } @Test diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java index 60e12289c9..1f32a93490 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviSeekMapTest.java @@ -24,14 +24,14 @@ public class AviSeekMapTest { @Test public void getFrames_givenExactSeekPointMatch() { final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap(); - final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.seekOffset; + final long position = aviSeekMap.getKeyFrameOffsets(DataHelper.AUDIO_ID) + aviSeekMap.seekOffset; final int secs = 4; final ChunkHandler[] chunkHandlers = new ChunkHandler[]{DataHelper.getVideoChunkHandler(secs), DataHelper.getAudioChunkHandler(secs)}; int[] indexes = aviSeekMap.getIndexes(position); for (int i=0;i Date: Tue, 15 Feb 2022 10:30:53 -0700 Subject: [PATCH 65/70] Removed commented code. --- .../android/exoplayer2/extractor/avi/AvcChunkHandler.java | 1 - .../com/google/android/exoplayer2/extractor/avi/AviSeekMap.java | 2 -- .../google/android/exoplayer2/extractor/avi/ChunkHandler.java | 1 - .../android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java | 1 - 4 files changed, 5 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java index 0d67032d53..e1f8dc3cef 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcChunkHandler.java @@ -92,7 +92,6 @@ public class AvcChunkHandler extends NalChunkHandler { //We skip IDR in the switch if (spsData.picOrderCountType == 0) { int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength); - //Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb); picCountClock.setPicCount(picOrderCountLsb); return; } else if (spsData.picOrderCountType == 2) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java index 5efd155396..4e16c805b7 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviSeekMap.java @@ -95,8 +95,6 @@ public class AviSeekMap implements SeekMap { } else { return new SeekPoints(getSeekPoint(firstSeekIndex)); } - - //Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position); } /** diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java index c18c76e496..b303e25e77 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ChunkHandler.java @@ -173,7 +173,6 @@ public class ChunkHandler { trackOutput.sampleMetadata( clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null); } - //Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame()); clock.advance(); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java index fed06bd755..3734f097b5 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/MpegAudioChunkHandler.java @@ -132,7 +132,6 @@ public class MpegAudioChunkHandler extends ChunkHandler { frameRemaining -= bytes; if (frameRemaining == 0) { trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, header.frameSize, 0, null); - //Log.d(AviExtractor.TAG, "MP3: us=" + us); timeUs += header.samplesPerFrame * C.MICROS_PER_SECOND / samplesPerSecond; } chunkRemaining -= bytes; From f604ee59d1e3c3ba7c90f0bb98a606a62ec8951a Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 15 Feb 2022 10:31:37 -0700 Subject: [PATCH 66/70] Changed constants to literals. --- .../exoplayer2/extractor/avi/AudioFormat.java | 15 +++++---------- .../exoplayer2/extractor/avi/AudioFormatTest.java | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) 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 index 479da0b7dc..98e1fed5ba 100644 --- 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 @@ -23,18 +23,13 @@ import java.nio.ByteBuffer; * Wrapper for the WAVEFORMATEX structure */ public class AudioFormat { - public static final short WAVE_FORMAT_PCM = 1; - static final short WAVE_FORMAT_AAC = 0xff; - private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55; - private static final short WAVE_FORMAT_DVM = 0x2000; //AC3 - private static final 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_AAC, MimeTypes.AUDIO_AAC); - FORMAT_MAP.put(WAVE_FORMAT_DVM, MimeTypes.AUDIO_AC3); - FORMAT_MAP.put(WAVE_FORMAT_DTS2, MimeTypes.AUDIO_DTS); + FORMAT_MAP.put(0x1, MimeTypes.AUDIO_RAW); // WAVE_FORMAT_PCM + FORMAT_MAP.put(0x55, MimeTypes.AUDIO_MPEG); // WAVE_FORMAT_MPEGLAYER3 + FORMAT_MAP.put(0xff, MimeTypes.AUDIO_AAC); // WAVE_FORMAT_AAC + FORMAT_MAP.put(0x2000, MimeTypes.AUDIO_AC3); // WAVE_FORMAT_DVM - AC3 + FORMAT_MAP.put(0x2001, MimeTypes.AUDIO_DTS); // WAVE_FORMAT_DTS2 } private final ByteBuffer byteBuffer; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java index cfb084b9b5..8207e14b6e 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AudioFormatTest.java @@ -32,7 +32,7 @@ public class AudioFormatTest { final AudioFormat audioFormat = streamFormatBox.getAudioFormat(); Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType()); Assert.assertEquals(2, audioFormat.getChannels()); - Assert.assertEquals(AudioFormat.WAVE_FORMAT_AAC, audioFormat.getFormatTag()); + Assert.assertEquals(0xff, audioFormat.getFormatTag()); // AAC Assert.assertEquals(48000, audioFormat.getSamplesPerSecond()); Assert.assertEquals(0, audioFormat.getBitsPerSample()); //Not meaningful for AAC Assert.assertArrayEquals(CODEC_PRIVATE, audioFormat.getCodecData()); From 1a616b4e0083273c6ef18c87a4124db25b012657 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 15 Feb 2022 10:32:43 -0700 Subject: [PATCH 67/70] Added /* package */ to package level variables. --- .../exoplayer2/extractor/avi/DataHelper.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index 9e95425edd..cfd86f24d6 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -26,14 +26,14 @@ import java.util.ArrayList; import java.util.Arrays; public class DataHelper { - static final int FPS = 24; - static final long VIDEO_US = 1_000_000L / FPS; - static final int AUDIO_PER_VIDEO = 4; - static final int VIDEO_SIZE = 4096; - static final int AUDIO_SIZE = 256; - static final int VIDEO_ID = 0; - static final int AUDIO_ID = 1; - static final int MOVI_OFFSET = 4096; + /* package */ static final int FPS = 24; + /* package */ static final long VIDEO_US = 1_000_000L / FPS; + /* package */ static final int AUDIO_PER_VIDEO = 4; + /* package */ static final int VIDEO_SIZE = 4096; + /* package */ static final int AUDIO_SIZE = 256; + /* package */ static final int VIDEO_ID = 0; + /* package */ static final int AUDIO_ID = 1; + /* package */ static final int MOVI_OFFSET = 4096; public static StreamHeaderBox getStreamHeader(int type, int scale, int rate, int length) { final ByteBuffer byteBuffer = AviExtractor.allocate(0x40); From 4f09fc36f99fa32bcfa0bacad40d3aa1e1a45947 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 15 Feb 2022 10:34:24 -0700 Subject: [PATCH 68/70] Changed readable values to hex to follow Exo standard --- .../extractor/avi/AviExtractor.java | 12 ++++---- .../extractor/avi/AviHeaderBox.java | 2 +- .../exoplayer2/extractor/avi/ListBox.java | 8 ++--- .../extractor/avi/StreamFormatBox.java | 2 +- .../extractor/avi/StreamHeaderBox.java | 6 ++-- .../extractor/avi/StreamNameBox.java | 2 +- .../exoplayer2/extractor/avi/VideoFormat.java | 30 +++++++++---------- 7 files changed, 30 insertions(+), 32 deletions(-) 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 index a854e3b86f..d4a97d065e 100644 --- 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 @@ -110,15 +110,15 @@ public class AviExtractor implements Extractor { static final int AVIIF_KEYFRAME = 16; - static final int RIFF = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24); - static final int AVI_ = 'A' | ('V' << 8) | ('I' << 16) | (' ' << 24); + static final int RIFF = 0x46464952; // RIFF + static final int AVI_ = 0x20495641; // AVI //movie data box - static final int MOVI = 'm' | ('o' << 8) | ('v' << 16) | ('i' << 24); + static final int MOVI = 0x69766f6d; // movi //Index - static final int IDX1 = 'i' | ('d' << 8) | ('x' << 16) | ('1' << 24); + static final int IDX1 = 0x31786469; // idx1 - static final int JUNK = 'J' | ('U' << 8) | ('N' << 16) | ('K' << 24); - static final int REC_ = 'r' | ('e' << 8) | ('c' << 16) | (' ' << 24); + static final int JUNK = 0x4b4e554a; // JUNK + static final int REC_ = 0x20636572; // rec @VisibleForTesting int state; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index 0a40b85218..4feb65c70e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -25,7 +25,7 @@ public class AviHeaderBox extends ResidentBox { static final int LEN = 0x38; static final int AVIF_HASINDEX = 0x10; private static final int AVIF_MUSTUSEINDEX = 0x20; - static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24); + static final int AVIH = 0x68697661; // avih AviHeaderBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java index 761328c4e1..4e0a8ebd7f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/ListBox.java @@ -27,11 +27,9 @@ import java.util.List; * An AVI LIST box. Similar to a Java List */ public class ListBox extends Box { - public static final int LIST = 'L' | ('I' << 8) | ('S' << 16) | ('T' << 24); - //Header List - public static final int TYPE_HDRL = 'h' | ('d' << 8) | ('r' << 16) | ('l' << 24); - //Stream List - public static final int TYPE_STRL = 's' | ('t' << 8) | ('r' << 16) | ('l' << 24); + public static final int LIST = 0x5453494c; // LIST + public static final int TYPE_HDRL = 0x6c726468; // hdrl - Header List + public static final int TYPE_STRL = 0x6c727473; // strl - Stream List private final int listType; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java index 9cbabdfeba..e7f551c1ef 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamFormatBox.java @@ -22,7 +22,7 @@ import java.nio.ByteBuffer; * Wrapper around the various StreamFormats */ public class StreamFormatBox extends ResidentBox { - public static final int STRF = 's' | ('t' << 8) | ('r' << 16) | ('f' << 24); + public static final int STRF = 0x66727473; // strf StreamFormatBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java index 58a8859efa..da4353f1fc 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamHeaderBox.java @@ -22,13 +22,13 @@ import java.nio.ByteBuffer; * Wrapper around the AVISTREAMHEADER structure */ public class StreamHeaderBox extends ResidentBox { - public static final int STRH = 's' | ('t' << 8) | ('r' << 16) | ('h' << 24); + public static final int STRH = 0x68727473; // strh //Audio Stream - static final int AUDS = 'a' | ('u' << 8) | ('d' << 16) | ('s' << 24); + static final int AUDS = 0x73647561; // auds //Videos Stream - static final int VIDS = 'v' | ('i' << 8) | ('d' << 16) | ('s' << 24); + static final int VIDS = 0x73646976; // vids StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java index 03d1757047..f8e6a71bbe 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/StreamNameBox.java @@ -21,7 +21,7 @@ import java.nio.ByteBuffer; * Box containing a human readable stream name */ public class StreamNameBox extends ResidentBox { - public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24); + public static final int STRN = 0x6e727473; // strn StreamNameBox(int type, int size, ByteBuffer byteBuffer) { super(type, size, byteBuffer); 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 index 9f12ff813f..ff10c3f99d 100644 --- 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 @@ -25,7 +25,7 @@ import java.util.HashMap; */ public class VideoFormat { - static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24); + static final int XVID = 0x44495658; // XVID private static final HashMap STREAM_MAP = new HashMap<>(); @@ -34,21 +34,21 @@ public class VideoFormat { final String mimeType = MimeTypes.VIDEO_MP4V; //I've never seen an Android devices that actually supports MP42 - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.VIDEO_MP42); + STREAM_MAP.put(0x3234504d, MimeTypes.VIDEO_MP42); // MP42 //Samsung seems to support the rare MP43. - STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.VIDEO_MP43); - STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264); - STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType); - STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType); - STREAM_MAP.put('D' | ('I' << 8) | ('V' << 16) | ('X' << 24), mimeType); - STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType); - STREAM_MAP.put('F' | ('M' << 8) | ('P' << 16) | ('4' << 24), mimeType); - STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType); - STREAM_MAP.put(XVID, mimeType); - STREAM_MAP.put('M' | ('J' << 8) | ('P' << 16) | ('G' << 24), MimeTypes.VIDEO_MJPEG); - STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG); + STREAM_MAP.put(0x3334504d, MimeTypes.VIDEO_MP43); // MP43 + STREAM_MAP.put(0x34363248, MimeTypes.VIDEO_H264); // H264 + STREAM_MAP.put(0x31637661, MimeTypes.VIDEO_H264); // avc1 + STREAM_MAP.put(0x31435641, MimeTypes.VIDEO_H264); // AVC1 + STREAM_MAP.put(0x44495633, mimeType); // 3VID + STREAM_MAP.put(0x78766964, mimeType); // divx + STREAM_MAP.put(0x58564944, mimeType); // DIVX + STREAM_MAP.put(0x30355844, mimeType); // DX50 + STREAM_MAP.put(0x34504d46, mimeType); // FMP4 + STREAM_MAP.put(0x64697678, mimeType); // xvid + STREAM_MAP.put(XVID, mimeType); // XVID + STREAM_MAP.put(0x47504a4d, MimeTypes.VIDEO_MJPEG); // MJPG + STREAM_MAP.put(0x67706a6d, MimeTypes.VIDEO_MJPEG); // mjpg } private final ByteBuffer byteBuffer; From e14617fc3201a065c050230a98ea9dacc670bd02 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 15 Feb 2022 10:39:20 -0700 Subject: [PATCH 69/70] Merged Robo and non-Robo tests into one file --- .../extractor/avi/AviExtractorRoboTest.java | 133 ------------------ .../extractor/avi/AviExtractorTest.java | 102 ++++++++++++++ 2 files changed, 102 insertions(+), 133 deletions(-) delete mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java deleted file mode 100644 index a8d5aa4274..0000000000 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorRoboTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.avi; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.FakeExtractorOutput; -import com.google.android.exoplayer2.testutil.FakeTrackOutput; -import com.google.android.exoplayer2.util.MimeTypes; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Collections; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class AviExtractorRoboTest { - - @Test - public void parseStream_givenXvidStreamList() throws IOException { - final AviExtractor aviExtractor = new AviExtractor(); - final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - aviExtractor.init(fakeExtractorOutput); - final ListBox streamList = DataHelper.getVideoStreamList(); - aviExtractor.parseStream(streamList, 0); - FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); - Assert.assertEquals(MimeTypes.VIDEO_MP4V, trackOutput.lastFormat.sampleMimeType); - } - - @Test - public void parseStream_givenAacStreamList() throws IOException { - final AviExtractor aviExtractor = new AviExtractor(); - final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - aviExtractor.init(fakeExtractorOutput); - final ListBox streamList = DataHelper.getAacStreamList(); - aviExtractor.parseStream(streamList, 0); - FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); - Assert.assertEquals(MimeTypes.AUDIO_AAC, trackOutput.lastFormat.sampleMimeType); - } - - @Test - public void parseStream_givenNoStreamHeader() { - final AviExtractor aviExtractor = new AviExtractor(); - final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - aviExtractor.init(fakeExtractorOutput); - final ListBox streamList = new ListBox(128, ListBox.TYPE_STRL, Collections.EMPTY_LIST); - Assert.assertNull(aviExtractor.parseStream(streamList, 0)); - } - - @Test - public void parseStream_givenNoStreamFormat() { - final AviExtractor aviExtractor = new AviExtractor(); - final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - aviExtractor.init(fakeExtractorOutput); - final ListBox streamList = new ListBox(128, ListBox.TYPE_STRL, - Collections.singletonList(DataHelper.getVidsStreamHeader())); - Assert.assertNull(aviExtractor.parseStream(streamList, 0)); - } - - @Test - public void readTracks_givenVideoTrack() throws IOException { - final AviExtractor aviExtractor = new AviExtractor(); - aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); - final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - aviExtractor.init(fakeExtractorOutput); - - final ByteBuffer byteBuffer = DataHelper.getRiffHeader(0xdc, 0xc8); - final ByteBuffer aviHeader = DataHelper.createAviHeader(); - byteBuffer.putInt(aviHeader.capacity()); - byteBuffer.put(aviHeader); - byteBuffer.putInt(ListBox.LIST); - byteBuffer.putInt(byteBuffer.remaining() - 4); - byteBuffer.putInt(ListBox.TYPE_STRL); - - final StreamHeaderBox streamHeaderBox = DataHelper.getVidsStreamHeader(); - byteBuffer.putInt(StreamHeaderBox.STRH); - byteBuffer.putInt(streamHeaderBox.getSize()); - byteBuffer.put(streamHeaderBox.getByteBuffer()); - - final StreamFormatBox streamFormatBox = DataHelper.getVideoStreamFormat(); - byteBuffer.putInt(StreamFormatBox.STRF); - byteBuffer.putInt(streamFormatBox.getSize()); - byteBuffer.put(streamFormatBox.getByteBuffer()); - - aviExtractor.state = AviExtractor.STATE_READ_TRACKS; - final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()). - build(); - final PositionHolder positionHolder = new PositionHolder(); - aviExtractor.read(input, positionHolder); - - Assert.assertEquals(AviExtractor.STATE_FIND_MOVI, aviExtractor.state); - - final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); - Assert.assertEquals(chunkHandler.getClock().durationUs, streamHeaderBox.getDurationUs()); - } - - @Test - public void readSamples_fragmentedChunk() throws IOException { - AviExtractor aviExtractor = AviExtractorTest.setupVideoAviExtractor(); - final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); - final int size = 24 + 16; - final ByteBuffer byteBuffer = AviExtractor.allocate(size + 8); - byteBuffer.putInt(chunkHandler.chunkId); - byteBuffer.putInt(size); - - final ExtractorInput chunk = new FakeExtractorInput.Builder().setData(byteBuffer.array()). - setSimulatePartialReads(true).build(); - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder())); - - Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder())); - - final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput; - Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); - } -} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 29500f4edd..9605e208c2 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.avi; +import androidx.test.ext.junit.runners.AndroidJUnit4; +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; @@ -26,9 +28,12 @@ import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Collections; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +@RunWith(AndroidJUnit4.class) public class AviExtractorTest { @Test @@ -492,4 +497,101 @@ public class AviExtractorTest { aviExtractor.release(); //Nothing to assert on a method that does nothing } + + @Test + public void parseStream_givenXvidStreamList() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = DataHelper.getVideoStreamList(); + aviExtractor.parseStream(streamList, 0); + FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); + Assert.assertEquals(MimeTypes.VIDEO_MP4V, trackOutput.lastFormat.sampleMimeType); + } + + @Test + public void parseStream_givenAacStreamList() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = DataHelper.getAacStreamList(); + aviExtractor.parseStream(streamList, 0); + FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO); + Assert.assertEquals(MimeTypes.AUDIO_AAC, trackOutput.lastFormat.sampleMimeType); + } + + @Test + public void parseStream_givenNoStreamHeader() { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = new ListBox(128, ListBox.TYPE_STRL, Collections.EMPTY_LIST); + Assert.assertNull(aviExtractor.parseStream(streamList, 0)); + } + + @Test + public void parseStream_givenNoStreamFormat() { + final AviExtractor aviExtractor = new AviExtractor(); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + final ListBox streamList = new ListBox(128, ListBox.TYPE_STRL, + Collections.singletonList(DataHelper.getVidsStreamHeader())); + Assert.assertNull(aviExtractor.parseStream(streamList, 0)); + } + + @Test + public void readTracks_givenVideoTrack() throws IOException { + final AviExtractor aviExtractor = new AviExtractor(); + aviExtractor.setAviHeader(DataHelper.createAviHeaderBox()); + final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + aviExtractor.init(fakeExtractorOutput); + + final ByteBuffer byteBuffer = DataHelper.getRiffHeader(0xdc, 0xc8); + final ByteBuffer aviHeader = DataHelper.createAviHeader(); + byteBuffer.putInt(aviHeader.capacity()); + byteBuffer.put(aviHeader); + byteBuffer.putInt(ListBox.LIST); + byteBuffer.putInt(byteBuffer.remaining() - 4); + byteBuffer.putInt(ListBox.TYPE_STRL); + + final StreamHeaderBox streamHeaderBox = DataHelper.getVidsStreamHeader(); + byteBuffer.putInt(StreamHeaderBox.STRH); + byteBuffer.putInt(streamHeaderBox.getSize()); + byteBuffer.put(streamHeaderBox.getByteBuffer()); + + final StreamFormatBox streamFormatBox = DataHelper.getVideoStreamFormat(); + byteBuffer.putInt(StreamFormatBox.STRF); + byteBuffer.putInt(streamFormatBox.getSize()); + byteBuffer.put(streamFormatBox.getByteBuffer()); + + aviExtractor.state = AviExtractor.STATE_READ_TRACKS; + final ExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + build(); + final PositionHolder positionHolder = new PositionHolder(); + aviExtractor.read(input, positionHolder); + + Assert.assertEquals(AviExtractor.STATE_FIND_MOVI, aviExtractor.state); + + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); + Assert.assertEquals(chunkHandler.getClock().durationUs, streamHeaderBox.getDurationUs()); + } + + @Test + public void readSamples_fragmentedChunk() throws IOException { + AviExtractor aviExtractor = AviExtractorTest.setupVideoAviExtractor(); + final ChunkHandler chunkHandler = aviExtractor.getVideoTrack(); + final int size = 24 + 16; + final ByteBuffer byteBuffer = AviExtractor.allocate(size + 8); + byteBuffer.putInt(chunkHandler.chunkId); + byteBuffer.putInt(size); + + final ExtractorInput chunk = new FakeExtractorInput.Builder().setData(byteBuffer.array()). + setSimulatePartialReads(true).build(); + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder())); + + Assert.assertEquals(Extractor.RESULT_CONTINUE, aviExtractor.read(chunk, new PositionHolder())); + + final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput; + Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); + } } \ No newline at end of file From 0b629e4be80468f43892b2989cf27f759979c2b5 Mon Sep 17 00:00:00 2001 From: Dustin Date: Thu, 17 Feb 2022 07:31:54 -0700 Subject: [PATCH 70/70] Simplified UnboundedIntArray (cherry picked from commit b4cb20cd31e44fe641aac94fee7e69456e48356c) --- .../extractor/avi/AviExtractor.java | 57 +++++++++--- .../exoplayer2/extractor/avi/AviSeekMap.java | 7 +- .../extractor/avi/UnboundedIntArray.java | 84 ------------------ .../extractor/avi/AviExtractorTest.java | 14 +++ .../exoplayer2/extractor/avi/DataHelper.java | 12 ++- .../extractor/avi/UnboundedIntArrayTest.java | 88 ------------------- 6 files changed, 64 insertions(+), 198 deletions(-) delete mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArray.java delete mode 100644 library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java 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 index d4a97d065e..f2c88fe5f7 100644 --- 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -449,8 +450,8 @@ public class AviExtractor implements Extractor { final int size = indexByteBuffer.getInt(); if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { if (chunkHandler.isVideo()) { - int indexSize = seekIndexes[videoId].getSize(); - if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) { + int indexSize = seekIndexes[videoId].size; + if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].array[indexSize - 1] >= chunksPerKeyFrame) { keyFrameOffsetsDiv2.add(offset / 2); for (@Nullable ChunkHandler seekTrack : chunkHandlers) { if (seekTrack != null) { @@ -469,13 +470,17 @@ public class AviExtractor implements Extractor { if (videoTrack.chunks == keyFrameCounts[videoTrack.getId()]) { videoTrack.setKeyFrames(ChunkHandler.ALL_KEY_FRAMES); } else { - videoTrack.setKeyFrames(seekIndexes[videoId].getArray()); + videoTrack.setKeyFrames(seekIndexes[videoId].pack()); } //Work-around a bug where the offset is from the start of the file, not "movi" final long seekOffset = firstEntry.getInt(8) > moviOffset ? 0L : moviOffset; + int[][] seekIndexArrays = new int[seekIndexes.length][]; + for (int i=0;i= size) { - throw new ArrayIndexOutOfBoundsException(index + ">=" + size); - } - return array[index]; - } - - public int getSize() { - return size; - } - - public void pack() { - if (size != array.length) { - array = Arrays.copyOf(array, size); - } - } - - protected void grow() { - int increase = Math.max(array.length /4, 1); - array = Arrays.copyOf(array, increase + array.length + size); - } - - public int[] getArray() { - pack(); - return array; - } - - /** - * Only works if values are in sequential order - */ - public int indexOf(int v) { - return Arrays.binarySearch(array, v); - } -} diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java index 9605e208c2..013fc3afe3 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/AviExtractorTest.java @@ -594,4 +594,18 @@ public class AviExtractorTest { final FakeTrackOutput fakeTrackOutput = (FakeTrackOutput) chunkHandler.trackOutput; Assert.assertEquals(size, fakeTrackOutput.getSampleData(0).length); } + + @Test + public void unboundIntArray_add_givenExceedsCapacity() { + final AviExtractor.UnboundedIntArray unboundedIntArray = new AviExtractor.UnboundedIntArray(); + final int testLen = unboundedIntArray.array.length + 1; + for (int i=0; i < testLen; i++) { + unboundedIntArray.add(i); + } + + Assert.assertEquals(testLen, unboundedIntArray.size); + for (int i=0; i < testLen; i++) { + Assert.assertEquals(i, unboundedIntArray.array[i]); + } + } } \ No newline at end of file diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java index cfd86f24d6..c78abae9b8 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/DataHelper.java @@ -118,14 +118,12 @@ public class DataHelper { public static AviSeekMap getAviSeekMap() { final int[] keyFrameOffsetsDiv2= {4, 1024}; - final UnboundedIntArray videoArray = new UnboundedIntArray(); - videoArray.add(0); - videoArray.add(4); - final UnboundedIntArray audioArray = new UnboundedIntArray(); - audioArray.add(0); - audioArray.add(128); + final int[] videoArray = new int[2]; + videoArray[1] = 4; + final int[] audioArray = new int[2]; + audioArray[1] = 128; return new AviSeekMap(0, 100L, 8, keyFrameOffsetsDiv2, - new UnboundedIntArray[]{videoArray, audioArray}, MOVI_OFFSET); + new int[][]{videoArray, audioArray}, MOVI_OFFSET); } private static void putIndex(final ByteBuffer byteBuffer, int chunkId, int flags, int offset, diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java deleted file mode 100644 index 7f4251e31f..0000000000 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/avi/UnboundedIntArrayTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.avi; - -import org.junit.Assert; -import org.junit.Test; - -public class UnboundedIntArrayTest { - @Test - public void add_givenInt() { - final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(); - unboundedIntArray.add(4); - Assert.assertEquals(1, unboundedIntArray.getSize()); - Assert.assertEquals(unboundedIntArray.getArray()[0], 4); - } - - @Test - public void indexOf_givenOrderSet() { - final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(); - unboundedIntArray.add(2); - unboundedIntArray.add(4); - unboundedIntArray.add(5); - unboundedIntArray.add(8); - Assert.assertEquals(2, unboundedIntArray.indexOf(5)); - Assert.assertTrue(unboundedIntArray.indexOf(6) < 0); - } - - @Test - public void grow_givenSizeOfOne() { - final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(1); - unboundedIntArray.add(0); - Assert.assertEquals(1, unboundedIntArray.getSize()); - unboundedIntArray.add(1); - Assert.assertTrue(unboundedIntArray.getSize() > 1); - } - - @Test - public void pack_givenSizeOfOne() { - final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(8); - unboundedIntArray.add(1); - unboundedIntArray.add(2); - Assert.assertEquals(8, unboundedIntArray.array.length); - unboundedIntArray.pack(); - Assert.assertEquals(2, unboundedIntArray.array.length); - } - - @Test - public void illegalArgument_givenNegativeSize() { - try { - new UnboundedIntArray(-1); - Assert.fail(); - } catch (IllegalArgumentException e) { - //Intentionally blank - } - } - - @Test - public void get_givenValidIndex() { - final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(4); - unboundedIntArray.add(1); - unboundedIntArray.add(2); - Assert.assertEquals(1, unboundedIntArray.get(0)); - } - - @Test - public void get_givenOutOfBounds() { - final UnboundedIntArray unboundedIntArray = new UnboundedIntArray(4); - try { - unboundedIntArray.get(0); - Assert.fail(); - } catch (ArrayIndexOutOfBoundsException e) { - //Intentionally blank - } - } -}