Merge pull request #9915 from dburckh:avi

PiperOrigin-RevId: 455094147
This commit is contained in:
Marc Baechinger 2022-06-15 15:28:22 +00:00
commit ad3348cc69
21 changed files with 4819 additions and 3 deletions

View File

@ -95,6 +95,8 @@
* MP4: Parse bitrates from `esds` boxes. * MP4: Parse bitrates from `esds` boxes.
* Ogg: Allow duplicate Opus ID and comment headers * Ogg: Allow duplicate Opus ID and comment headers
([#10038](https://github.com/google/ExoPlayer/issues/10038)). ([#10038](https://github.com/google/ExoPlayer/issues/10038)).
* Add support for AVI
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
* UI: * UI:
* Fix delivery of events to `OnClickListener`s set on `PlayerView` and * Fix delivery of events to `OnClickListener`s set on `PlayerView` and
`LegacyPlayerView`, in the case that `useController=false` `LegacyPlayerView`, in the case that `useController=false`

View File

@ -44,7 +44,7 @@ public final class FileTypes {
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({ @IntDef({
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG, UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG,
MIDI MIDI, AVI
}) })
public @interface Type {} public @interface Type {}
/** Unknown file type. */ /** Unknown file type. */
@ -81,6 +81,8 @@ public final class FileTypes {
public static final int JPEG = 14; public static final int JPEG = 14;
/** File type for the MIDI format. */ /** File type for the MIDI format. */
public static final int MIDI = 15; public static final int MIDI = 15;
/** File type for the AVI format. */
public static final int AVI = 16;
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type"; @VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
@ -116,6 +118,7 @@ public final class FileTypes {
private static final String EXTENSION_WEBVTT = ".webvtt"; private static final String EXTENSION_WEBVTT = ".webvtt";
private static final String EXTENSION_JPG = ".jpg"; private static final String EXTENSION_JPG = ".jpg";
private static final String EXTENSION_JPEG = ".jpeg"; private static final String EXTENSION_JPEG = ".jpeg";
private static final String EXTENSION_AVI = ".avi";
private FileTypes() {} private FileTypes() {}
@ -179,6 +182,8 @@ public final class FileTypes {
return FileTypes.WEBVTT; return FileTypes.WEBVTT;
case MimeTypes.IMAGE_JPEG: case MimeTypes.IMAGE_JPEG:
return FileTypes.JPEG; return FileTypes.JPEG;
case MimeTypes.VIDEO_AVI:
return FileTypes.AVI;
default: default:
return FileTypes.UNKNOWN; return FileTypes.UNKNOWN;
} }
@ -244,6 +249,8 @@ public final class FileTypes {
return FileTypes.WEBVTT; return FileTypes.WEBVTT;
} else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) { } else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
return FileTypes.JPEG; return FileTypes.JPEG;
} else if (filename.endsWith(EXTENSION_AVI)) {
return FileTypes.AVI;
} else { } else {
return FileTypes.UNKNOWN; return FileTypes.UNKNOWN;
} }

View File

@ -56,6 +56,10 @@ public final class MimeTypes {
@UnstableApi public static final String VIDEO_FLV = BASE_TYPE_VIDEO + "/x-flv"; @UnstableApi public static final String VIDEO_FLV = BASE_TYPE_VIDEO + "/x-flv";
public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision"; public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision";
public static final String VIDEO_OGG = BASE_TYPE_VIDEO + "/ogg"; public static final String VIDEO_OGG = BASE_TYPE_VIDEO + "/ogg";
public static final String VIDEO_AVI = BASE_TYPE_VIDEO + "/x-msvideo";
public static final String VIDEO_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";
@UnstableApi public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @UnstableApi public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown";
// audio/ MIME types // audio/ MIME types

View File

@ -27,6 +27,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.util.TimestampAdjuster; import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.amr.AmrExtractor; import androidx.media3.extractor.amr.AmrExtractor;
import androidx.media3.extractor.avi.AviExtractor;
import androidx.media3.extractor.flac.FlacExtractor; import androidx.media3.extractor.flac.FlacExtractor;
import androidx.media3.extractor.flv.FlvExtractor; import androidx.media3.extractor.flv.FlvExtractor;
import androidx.media3.extractor.jpeg.JpegExtractor; import androidx.media3.extractor.jpeg.JpegExtractor;
@ -103,8 +104,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
FileTypes.AC3, FileTypes.AC3,
FileTypes.AC4, FileTypes.AC4,
FileTypes.MP3, FileTypes.MP3,
FileTypes.JPEG, // The following extractors are not part of the optimized ordering, and were appended
// without further analysis.
FileTypes.AVI,
FileTypes.MIDI, FileTypes.MIDI,
FileTypes.JPEG,
}; };
private static final ExtensionLoader FLAC_EXTENSION_LOADER = private static final ExtensionLoader FLAC_EXTENSION_LOADER =
@ -309,7 +313,8 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Override @Override
public synchronized Extractor[] createExtractors( public synchronized Extractor[] createExtractors(
Uri uri, Map<String, List<String>> responseHeaders) { Uri uri, Map<String, List<String>> responseHeaders) {
List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 14); List<Extractor> extractors =
new ArrayList<>(/* initialCapacity= */ DEFAULT_EXTRACTOR_ORDER.length);
@FileTypes.Type @FileTypes.Type
int responseHeadersInferredFileType = inferFileTypeFromResponseHeaders(responseHeaders); int responseHeadersInferredFileType = inferFileTypeFromResponseHeaders(responseHeaders);
@ -412,6 +417,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors.add(midiExtractor); extractors.add(midiExtractor);
} }
break; break;
case FileTypes.AVI:
extractors.add(new AviExtractor());
break;
case FileTypes.WEBVTT: case FileTypes.WEBVTT:
case FileTypes.UNKNOWN: case FileTypes.UNKNOWN:
default: default:

View File

@ -0,0 +1,27 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
/**
* A chunk, as defined in the AVI spec.
*
* <p>See https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference.
*/
/* package */ interface AviChunk {
/** Returns the chunk type fourcc. */
int getType();
}

View File

@ -0,0 +1,557 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.DummyExtractorOutput;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.TrackOutput;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Extracts data from the AVI container format.
*
* <p>Spec: https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference.
*/
@UnstableApi
public final class AviExtractor implements Extractor {
private static final String TAG = "AviExtractor";
public static final int FOURCC_RIFF = 0x46464952;
public static final int FOURCC_AVI_ = 0x20495641; // AVI<space>
public static final int FOURCC_LIST = 0x5453494c;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_avih = 0x68697661;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_hdrl = 0x6c726468;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_strl = 0x6c727473;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_movi = 0x69766f6d;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_idx1 = 0x31786469;
public static final int FOURCC_JUNK = 0x4b4e554a;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_strf = 0x66727473;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_strn = 0x6e727473;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_strh = 0x68727473;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_auds = 0x73647561;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_txts = 0x73747874;
@SuppressWarnings("ConstantCaseForConstants")
public static final int FOURCC_vids = 0x73646976;
/** Parser states. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
STATE_SKIPPING_TO_HDRL,
STATE_READING_HDRL_HEADER,
STATE_READING_HDRL_BODY,
STATE_FINDING_MOVI_HEADER,
STATE_FINDING_IDX1_HEADER,
STATE_READING_IDX1_BODY,
STATE_READING_SAMPLES,
})
private @interface State {}
private static final int STATE_SKIPPING_TO_HDRL = 0;
private static final int STATE_READING_HDRL_HEADER = 1;
private static final int STATE_READING_HDRL_BODY = 2;
private static final int STATE_FINDING_MOVI_HEADER = 3;
private static final int STATE_FINDING_IDX1_HEADER = 4;
private static final int STATE_READING_IDX1_BODY = 5;
private static final int STATE_READING_SAMPLES = 6;
private static final int AVIIF_KEYFRAME = 16;
/**
* Maximum size to skip using {@link ExtractorInput#skip}. Boxes larger than this size are skipped
* using {@link #RESULT_SEEK}.
*/
private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
private final ParsableByteArray scratch;
private final ChunkHeaderHolder chunkHeaderHolder;
private @State int state;
private ExtractorOutput extractorOutput;
private @MonotonicNonNull AviMainHeaderChunk aviHeader;
private long durationUs;
private ChunkReader[] chunkReaders;
private long pendingReposition;
@Nullable private ChunkReader currentChunkReader;
private int hdrlSize;
private long moviStart;
private long moviEnd;
private int idx1BodySize;
private boolean seekMapHasBeenOutput;
public AviExtractor() {
scratch = new ParsableByteArray(/* limit= */ 12);
chunkHeaderHolder = new ChunkHeaderHolder();
extractorOutput = new DummyExtractorOutput();
chunkReaders = new ChunkReader[0];
moviStart = C.POSITION_UNSET;
moviEnd = C.POSITION_UNSET;
hdrlSize = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
}
// Extractor implementation.
@Override
public void init(ExtractorOutput output) {
this.state = STATE_SKIPPING_TO_HDRL;
this.extractorOutput = output;
pendingReposition = C.POSITION_UNSET;
}
@Override
public boolean sniff(ExtractorInput input) throws IOException {
input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 12);
scratch.setPosition(0);
if (scratch.readLittleEndianInt() != FOURCC_RIFF) {
return false;
}
scratch.skipBytes(4); // Skip the RIFF chunk length.
return scratch.readLittleEndianInt() == FOURCC_AVI_;
}
@Override
public int read(ExtractorInput input, PositionHolder positionHolder) throws IOException {
if (resolvePendingReposition(input, positionHolder)) {
return RESULT_SEEK;
}
switch (state) {
case STATE_SKIPPING_TO_HDRL:
// Check for RIFF and AVI fourcc's just in case the caller did not sniff, in order to
// provide a meaningful error if the input is not an AVI file.
if (sniff(input)) {
input.skipFully(/* length= */ 12);
} else {
throw ParserException.createForMalformedContainer(
/* message= */ "AVI Header List not found", /* cause= */ null);
}
state = STATE_READING_HDRL_HEADER;
return RESULT_CONTINUE;
case STATE_READING_HDRL_HEADER:
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 12);
scratch.setPosition(0);
chunkHeaderHolder.populateWithListHeaderFrom(scratch);
if (chunkHeaderHolder.listType != FOURCC_hdrl) {
throw ParserException.createForMalformedContainer(
/* message= */ "hdrl expected, found: " + chunkHeaderHolder.listType,
/* cause= */ null);
}
hdrlSize = chunkHeaderHolder.size;
state = STATE_READING_HDRL_BODY;
return RESULT_CONTINUE;
case STATE_READING_HDRL_BODY:
// hdrlSize includes the LIST type (hdrl), so we subtract 4 to the size.
int bytesToRead = hdrlSize - 4;
ParsableByteArray hdrlBody = new ParsableByteArray(bytesToRead);
input.readFully(hdrlBody.getData(), /* offset= */ 0, bytesToRead);
parseHdrlBody(hdrlBody);
state = STATE_FINDING_MOVI_HEADER;
return RESULT_CONTINUE;
case STATE_FINDING_MOVI_HEADER:
if (moviStart != C.POSITION_UNSET && input.getPosition() != moviStart) {
pendingReposition = moviStart;
return RESULT_CONTINUE;
}
input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 12);
input.resetPeekPosition();
scratch.setPosition(0);
chunkHeaderHolder.populateFrom(scratch);
int listType = scratch.readLittleEndianInt();
if (chunkHeaderHolder.chunkType == FOURCC_RIFF) {
// We are at the start of the file. The movi chunk is in the RIFF chunk, so we skip the
// header, so as to read the RIFF chunk's body.
input.skipFully(12);
return RESULT_CONTINUE;
}
if (chunkHeaderHolder.chunkType != FOURCC_LIST || listType != FOURCC_movi) {
// The chunk header (8 bytes) plus the whole body.
pendingReposition = input.getPosition() + chunkHeaderHolder.size + 8;
return RESULT_CONTINUE;
}
moviStart = input.getPosition();
// Size includes the list type, but not the LIST or size fields, so we add 8.
moviEnd = moviStart + chunkHeaderHolder.size + 8;
if (!seekMapHasBeenOutput) {
if (Assertions.checkNotNull(aviHeader).hasIndex()) {
state = STATE_FINDING_IDX1_HEADER;
pendingReposition = moviEnd;
return RESULT_CONTINUE;
} else {
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
seekMapHasBeenOutput = true;
}
}
// No need to parse the idx1, so we start reading the samples from the movi chunk straight
// away. We skip 12 bytes to move to the start of the movi's body.
pendingReposition = input.getPosition() + 12;
state = STATE_READING_SAMPLES;
return RESULT_CONTINUE;
case STATE_FINDING_IDX1_HEADER:
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 8);
scratch.setPosition(0);
int idx1Fourcc = scratch.readLittleEndianInt();
int boxSize = scratch.readLittleEndianInt();
if (idx1Fourcc == FOURCC_idx1) {
state = STATE_READING_IDX1_BODY;
idx1BodySize = boxSize;
} else {
// This one is not idx1, skip to the next box.
pendingReposition = input.getPosition() + boxSize;
}
return RESULT_CONTINUE;
case STATE_READING_IDX1_BODY:
ParsableByteArray idx1Body = new ParsableByteArray(idx1BodySize);
input.readFully(idx1Body.getData(), /* offset= */ 0, /* length= */ idx1BodySize);
parseIdx1Body(idx1Body);
state = STATE_READING_SAMPLES;
pendingReposition = moviStart;
return RESULT_CONTINUE;
case STATE_READING_SAMPLES:
return readMoviChunks(input);
default:
throw new AssertionError(); // Should never happen.
}
}
@Override
public void seek(long position, long timeUs) {
pendingReposition = C.POSITION_UNSET;
currentChunkReader = null;
for (ChunkReader chunkReader : chunkReaders) {
chunkReader.seekToPosition(position);
}
if (position == 0) {
if (chunkReaders.length == 0) {
// Still unprepared.
state = STATE_SKIPPING_TO_HDRL;
} else {
state = STATE_FINDING_MOVI_HEADER;
}
return;
}
state = STATE_READING_SAMPLES;
}
@Override
public void release() {
// Nothing to release.
}
// Internal methods.
/**
* Returns whether a {@link #RESULT_SEEK} is required for the pending reposition. A seek may not
* be necessary when the desired position (as held by {@link #pendingReposition}) is after the
* {@link ExtractorInput#getPosition() current position}, but not further than {@link
* #RELOAD_MINIMUM_SEEK_DISTANCE}.
*/
private boolean resolvePendingReposition(ExtractorInput input, PositionHolder positionHolder)
throws IOException {
boolean needSeek = false;
if (pendingReposition != C.POSITION_UNSET) {
long currentPosition = input.getPosition();
if (pendingReposition < currentPosition
|| pendingReposition > currentPosition + RELOAD_MINIMUM_SEEK_DISTANCE) {
positionHolder.position = pendingReposition;
needSeek = true;
} else {
// The distance to the target position is short enough that it makes sense to just skip the
// bytes, instead of doing a seek which might re-create an HTTP connection.
input.skipFully((int) (pendingReposition - currentPosition));
}
}
pendingReposition = C.POSITION_UNSET;
return needSeek;
}
private void parseHdrlBody(ParsableByteArray hrdlBody) throws IOException {
ListChunk headerList = ListChunk.parseFrom(FOURCC_hdrl, hrdlBody);
if (headerList.getType() != FOURCC_hdrl) {
throw ParserException.createForMalformedContainer(
/* message= */ "Unexpected header list type " + headerList.getType(), /* cause= */ null);
}
@Nullable AviMainHeaderChunk aviHeader = headerList.getChild(AviMainHeaderChunk.class);
if (aviHeader == null) {
throw ParserException.createForMalformedContainer(
/* message= */ "AviHeader not found", /* cause= */ null);
}
this.aviHeader = aviHeader;
// This is usually wrong, so it will be overwritten by video if present
durationUs = aviHeader.totalFrames * (long) aviHeader.frameDurationUs;
ArrayList<ChunkReader> chunkReaderList = new ArrayList<>();
int streamId = 0;
for (AviChunk aviChunk : headerList.children) {
if (aviChunk.getType() == FOURCC_strl) {
ListChunk streamList = (ListChunk) aviChunk;
// Note the streamId needs to increment even if the corresponding `strl` is discarded.
// See
// https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference#avi-stream-headers.
@Nullable ChunkReader chunkReader = processStreamList(streamList, streamId++);
if (chunkReader != null) {
chunkReaderList.add(chunkReader);
}
}
}
chunkReaders = chunkReaderList.toArray(new ChunkReader[0]);
extractorOutput.endTracks();
}
/** Builds and outputs the {@link SeekMap} from the idx1 chunk. */
private void parseIdx1Body(ParsableByteArray body) {
long seekOffset = peekSeekOffset(body);
while (body.bytesLeft() >= 16) {
int chunkId = body.readLittleEndianInt();
int flags = body.readLittleEndianInt();
long offset = body.readLittleEndianInt() + seekOffset;
body.readLittleEndianInt(); // We ignore the size.
ChunkReader chunkReader = getChunkReader(chunkId);
if (chunkReader == null) {
// We ignore unknown chunk IDs.
continue;
}
if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) {
chunkReader.appendKeyFrameToIndex(offset);
}
chunkReader.incrementIndexChunkCount();
}
for (ChunkReader chunkReader : chunkReaders) {
chunkReader.compactIndex();
}
seekMapHasBeenOutput = true;
extractorOutput.seekMap(new AviSeekMap(durationUs));
}
private long peekSeekOffset(ParsableByteArray idx1Body) {
// The spec states the offset is based on the start of the movi list type fourcc, but it also
// says some files base the offset on the start of the file. We use a best effort approach to
// figure out which is the case. See:
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/Aviriff/ns-aviriff-avioldindex#dwoffset.
if (idx1Body.bytesLeft() < 16) {
// There are no full entries in the index, meaning we don't need to apply an offset.
return 0;
}
int startingPosition = idx1Body.getPosition();
idx1Body.skipBytes(8); // Skip chunkId (4 bytes) and flags (4 bytes).
int offset = idx1Body.readLittleEndianInt();
// moviStart poitns at the start of the LIST, while the seek offset is based at the start of the
// movi fourCC, so we add 8 to reconcile the difference.
long seekOffset = offset > moviStart ? 0L : moviStart + 8;
idx1Body.setPosition(startingPosition);
return seekOffset;
}
@Nullable
private ChunkReader getChunkReader(int chunkId) {
for (ChunkReader chunkReader : chunkReaders) {
if (chunkReader.handlesChunkId(chunkId)) {
return chunkReader;
}
}
return null;
}
private int readMoviChunks(ExtractorInput input) throws IOException {
if (input.getPosition() >= moviEnd) {
return C.RESULT_END_OF_INPUT;
} else if (currentChunkReader != null) {
if (currentChunkReader.onChunkData(input)) {
currentChunkReader = null;
}
} else {
alignInputToEvenPosition(input);
input.peekFully(scratch.getData(), /* offset= */ 0, 12);
scratch.setPosition(0);
int chunkType = scratch.readLittleEndianInt();
if (chunkType == FOURCC_LIST) {
scratch.setPosition(8);
int listType = scratch.readLittleEndianInt();
input.skipFully(listType == FOURCC_movi ? 12 : 8);
input.resetPeekPosition();
return RESULT_CONTINUE;
}
int size = scratch.readLittleEndianInt();
if (chunkType == FOURCC_JUNK) {
pendingReposition = input.getPosition() + size + 8;
return RESULT_CONTINUE;
}
input.skipFully(8);
input.resetPeekPosition();
ChunkReader chunkReader = getChunkReader(chunkType);
if (chunkReader == null) {
// No handler for this chunk. We skip it.
pendingReposition = input.getPosition() + size;
return RESULT_CONTINUE;
} else {
chunkReader.onChunkStart(size);
this.currentChunkReader = chunkReader;
}
}
return RESULT_CONTINUE;
}
@Nullable
private ChunkReader processStreamList(ListChunk streamList, int streamId) {
AviStreamHeaderChunk aviStreamHeaderChunk = streamList.getChild(AviStreamHeaderChunk.class);
StreamFormatChunk streamFormatChunk = streamList.getChild(StreamFormatChunk.class);
if (aviStreamHeaderChunk == null) {
Log.w(TAG, "Missing Stream Header");
return null;
}
if (streamFormatChunk == null) {
Log.w(TAG, "Missing Stream Format");
return null;
}
long durationUs = aviStreamHeaderChunk.getDurationUs();
Format streamFormat = streamFormatChunk.format;
Format.Builder builder = streamFormat.buildUpon();
builder.setId(streamId);
int suggestedBufferSize = aviStreamHeaderChunk.suggestedBufferSize;
if (suggestedBufferSize != 0) {
builder.setMaxInputSize(suggestedBufferSize);
}
StreamNameChunk streamName = streamList.getChild(StreamNameChunk.class);
if (streamName != null) {
builder.setLabel(streamName.name);
}
int trackType = MimeTypes.getTrackType(streamFormat.sampleMimeType);
if (trackType == C.TRACK_TYPE_AUDIO || trackType == C.TRACK_TYPE_VIDEO) {
TrackOutput trackOutput = extractorOutput.track(streamId, trackType);
trackOutput.format(builder.build());
ChunkReader chunkReader =
new ChunkReader(
streamId, trackType, durationUs, aviStreamHeaderChunk.length, trackOutput);
this.durationUs = durationUs;
return chunkReader;
} else {
// We don't currently support tracks other than video and audio.
return null;
}
}
/**
* Skips one byte from the given {@code input} if the current position is odd.
*
* <p>This isn't documented anywhere, but AVI files are aligned to even bytes and fill gaps with
* zeros.
*/
private static void alignInputToEvenPosition(ExtractorInput input) throws IOException {
if ((input.getPosition() & 1) == 1) {
input.skipFully(1);
}
}
// Internal classes.
private class AviSeekMap implements SeekMap {
private final long durationUs;
public AviSeekMap(long durationUs) {
this.durationUs = durationUs;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getDurationUs() {
return durationUs;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
SeekPoints result = chunkReaders[0].getSeekPoints(timeUs);
for (int i = 1; i < chunkReaders.length; i++) {
SeekPoints seekPoints = chunkReaders[i].getSeekPoints(timeUs);
if (seekPoints.first.position < result.first.position) {
result = seekPoints;
}
}
return result;
}
}
private static class ChunkHeaderHolder {
public int chunkType;
public int size;
public int listType;
public void populateWithListHeaderFrom(ParsableByteArray headerBytes) throws ParserException {
populateFrom(headerBytes);
if (chunkType != AviExtractor.FOURCC_LIST) {
throw ParserException.createForMalformedContainer(
/* message= */ "LIST expected, found: " + chunkType, /* cause= */ null);
}
listType = headerBytes.readLittleEndianInt();
}
public void populateFrom(ParsableByteArray headerBytes) {
chunkType = headerBytes.readLittleEndianInt();
size = headerBytes.readLittleEndianInt();
listType = 0;
}
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import androidx.media3.common.util.ParsableByteArray;
/** Wrapper around the AVIMAINHEADER structure */
/* package */ final class AviMainHeaderChunk implements AviChunk {
private static final int AVIF_HAS_INDEX = 0x10;
public static AviMainHeaderChunk parseFrom(ParsableByteArray body) {
int microSecPerFrame = body.readLittleEndianInt();
body.skipBytes(8); // Skip dwMaxBytesPerSec (4 bytes), dwPaddingGranularity (4 bytes).
int flags = body.readLittleEndianInt();
int totalFrames = body.readLittleEndianInt();
body.skipBytes(4); // dwInitialFrames (4 bytes).
int streams = body.readLittleEndianInt();
body.skipBytes(12); // dwSuggestedBufferSize (4 bytes), dwWidth (4 bytes), dwHeight (4 bytes).
return new AviMainHeaderChunk(microSecPerFrame, flags, totalFrames, streams);
}
public final int frameDurationUs;
public final int flags;
public final int totalFrames;
public final int streams;
private AviMainHeaderChunk(int frameDurationUs, int flags, int totalFrames, int streams) {
this.frameDurationUs = frameDurationUs;
this.flags = flags;
this.totalFrames = totalFrames;
this.streams = streams;
}
@Override
public int getType() {
return AviExtractor.FOURCC_avih;
}
public boolean hasIndex() {
return (flags & AVIF_HAS_INDEX) == AVIF_HAS_INDEX;
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import androidx.media3.common.C;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
/** Parses and holds information from the AVISTREAMHEADER structure. */
/* package */ final class AviStreamHeaderChunk implements AviChunk {
private static final String TAG = "AviStreamHeaderChunk";
public static AviStreamHeaderChunk parseFrom(ParsableByteArray body) {
int streamType = body.readLittleEndianInt();
body.skipBytes(12); // fccHandler (4 bytes), dwFlags (4 bytes), wPriority (2 bytes),
// wLanguage (2 bytes).
int initialFrames = body.readLittleEndianInt();
int scale = body.readLittleEndianInt();
int rate = body.readLittleEndianInt();
body.skipBytes(4); // dwStart (4 bytes).
int length = body.readLittleEndianInt();
int suggestedBufferSize = body.readLittleEndianInt();
body.skipBytes(8); // dwQuality (4 bytes), dwSampleSize (4 bytes).
return new AviStreamHeaderChunk(
streamType, initialFrames, scale, rate, length, suggestedBufferSize);
}
public final int streamType;
public final int initialFrames;
public final int scale;
public final int rate;
public final int length;
public final int suggestedBufferSize;
private AviStreamHeaderChunk(
int streamType, int initialFrames, int scale, int rate, int length, int suggestedBufferSize) {
this.streamType = streamType;
this.initialFrames = initialFrames;
this.scale = scale;
this.rate = rate;
this.length = length;
this.suggestedBufferSize = suggestedBufferSize;
}
@Override
public int getType() {
return AviExtractor.FOURCC_strh;
}
public @C.TrackType int getTrackType() {
switch (streamType) {
case AviExtractor.FOURCC_auds:
return C.TRACK_TYPE_AUDIO;
case AviExtractor.FOURCC_vids:
return C.TRACK_TYPE_VIDEO;
case AviExtractor.FOURCC_txts:
return C.TRACK_TYPE_TEXT;
default:
Log.w(TAG, "Found unsupported streamType fourCC: " + Integer.toHexString(streamType));
return C.TRACK_TYPE_UNKNOWN;
}
}
public float getFrameRate() {
return rate / (float) scale;
}
public long getDurationUs() {
return Util.scaleLargeTimestamp(
/* timestamp= */ length,
/* multiplier= */ C.MICROS_PER_SECOND * scale,
/* divisor= */ rate);
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.SeekPoint;
import androidx.media3.extractor.TrackOutput;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
/** Reads chunks holding sample data. */
/* package */ final class ChunkReader {
/** Parser states. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
CHUNK_TYPE_VIDEO_COMPRESSED,
CHUNK_TYPE_VIDEO_UNCOMPRESSED,
CHUNK_TYPE_AUDIO,
})
private @interface ChunkType {}
private static final int INITIAL_INDEX_SIZE = 512;
private static final int CHUNK_TYPE_VIDEO_COMPRESSED = ('d' << 16) | ('c' << 24);
private static final int CHUNK_TYPE_VIDEO_UNCOMPRESSED = ('d' << 16) | ('b' << 24);
private static final int CHUNK_TYPE_AUDIO = ('w' << 16) | ('b' << 24);
protected final TrackOutput trackOutput;
/** The chunk id fourCC (example: `01wb`), as defined in the index and the movi. */
private final int chunkId;
/** Secondary chunk id. Bad muxers sometimes use an uncompressed video id (db) for key frames */
private final int alternativeChunkId;
private final long durationUs;
private final int streamHeaderChunkCount;
private int currentChunkSize;
private int bytesRemainingInCurrentChunk;
/** Number of chunks as calculated by the index */
private int currentChunkIndex;
private int indexChunkCount;
private int indexSize;
private long[] keyFrameOffsets;
private int[] keyFrameIndices;
public ChunkReader(
int id,
@C.TrackType int trackType,
long durationnUs,
int streamHeaderChunkCount,
TrackOutput trackOutput) {
Assertions.checkArgument(trackType == C.TRACK_TYPE_AUDIO || trackType == C.TRACK_TYPE_VIDEO);
this.durationUs = durationnUs;
this.streamHeaderChunkCount = streamHeaderChunkCount;
this.trackOutput = trackOutput;
@ChunkType
int chunkType =
trackType == C.TRACK_TYPE_VIDEO ? CHUNK_TYPE_VIDEO_COMPRESSED : CHUNK_TYPE_AUDIO;
chunkId = getChunkIdFourCc(id, chunkType);
alternativeChunkId =
trackType == C.TRACK_TYPE_VIDEO ? getChunkIdFourCc(id, CHUNK_TYPE_VIDEO_UNCOMPRESSED) : -1;
keyFrameOffsets = new long[INITIAL_INDEX_SIZE];
keyFrameIndices = new int[INITIAL_INDEX_SIZE];
}
public void appendKeyFrameToIndex(long offset) {
if (indexSize == keyFrameIndices.length) {
keyFrameOffsets = Arrays.copyOf(keyFrameOffsets, keyFrameOffsets.length * 3 / 2);
keyFrameIndices = Arrays.copyOf(keyFrameIndices, keyFrameIndices.length * 3 / 2);
}
keyFrameOffsets[indexSize] = offset;
keyFrameIndices[indexSize] = indexChunkCount;
indexSize++;
}
public void advanceCurrentChunk() {
currentChunkIndex++;
}
public long getCurrentChunkTimestampUs() {
return getChunkTimestampUs(currentChunkIndex);
}
public long getFrameDurationUs() {
return getChunkTimestampUs(/* chunkIndex= */ 1);
}
public void incrementIndexChunkCount() {
indexChunkCount++;
}
public void compactIndex() {
keyFrameOffsets = Arrays.copyOf(keyFrameOffsets, indexSize);
keyFrameIndices = Arrays.copyOf(keyFrameIndices, indexSize);
}
public boolean handlesChunkId(int chunkId) {
return this.chunkId == chunkId || alternativeChunkId == chunkId;
}
public boolean isCurrentFrameAKeyFrame() {
return Arrays.binarySearch(keyFrameIndices, currentChunkIndex) >= 0;
}
public boolean isVideo() {
return (chunkId & CHUNK_TYPE_VIDEO_COMPRESSED) == CHUNK_TYPE_VIDEO_COMPRESSED;
}
public boolean isAudio() {
return (chunkId & CHUNK_TYPE_AUDIO) == CHUNK_TYPE_AUDIO;
}
/** Prepares for parsing a chunk with the given {@code size}. */
public void onChunkStart(int size) {
currentChunkSize = size;
bytesRemainingInCurrentChunk = size;
}
/**
* Provides data associated to the current chunk and returns whether the full chunk has been
* parsed.
*/
public boolean onChunkData(ExtractorInput input) throws IOException {
bytesRemainingInCurrentChunk -=
trackOutput.sampleData(input, bytesRemainingInCurrentChunk, false);
boolean done = bytesRemainingInCurrentChunk == 0;
if (done) {
if (currentChunkSize > 0) {
trackOutput.sampleMetadata(
getCurrentChunkTimestampUs(),
(isCurrentFrameAKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0),
currentChunkSize,
0,
null);
}
advanceCurrentChunk();
}
return done;
}
public void seekToPosition(long position) {
if (indexSize == 0) {
currentChunkIndex = 0;
} else {
int index =
Util.binarySearchFloor(
keyFrameOffsets, position, /* inclusive= */ true, /* stayInBounds= */ true);
currentChunkIndex = keyFrameIndices[index];
}
}
public SeekMap.SeekPoints getSeekPoints(long timeUs) {
int targetFrameIndex = (int) (timeUs / getFrameDurationUs());
int keyFrameIndex =
Util.binarySearchFloor(
keyFrameIndices, targetFrameIndex, /* inclusive= */ true, /* stayInBounds= */ true);
if (keyFrameIndices[keyFrameIndex] == targetFrameIndex) {
return new SeekMap.SeekPoints(getSeekPoint(keyFrameIndex));
}
// The target frame is not a key frame, we look for the two closest ones.
SeekPoint precedingKeyFrameSeekPoint = getSeekPoint(keyFrameIndex);
if (keyFrameIndex + 1 < keyFrameOffsets.length) {
return new SeekMap.SeekPoints(precedingKeyFrameSeekPoint, getSeekPoint(keyFrameIndex + 1));
} else {
return new SeekMap.SeekPoints(precedingKeyFrameSeekPoint);
}
}
private long getChunkTimestampUs(int chunkIndex) {
return durationUs * chunkIndex / streamHeaderChunkCount;
}
private SeekPoint getSeekPoint(int keyFrameIndex) {
return new SeekPoint(
keyFrameIndices[keyFrameIndex] * getFrameDurationUs(), keyFrameOffsets[keyFrameIndex]);
}
private static int getChunkIdFourCc(int streamId, @ChunkType int chunkType) {
int tens = streamId / 10;
int ones = streamId % 10;
return (('0' + ones) << 8) | ('0' + tens) | chunkType;
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.ParsableByteArray;
import com.google.common.collect.ImmutableList;
/** Represents an AVI LIST. */
/* package */ final class ListChunk implements AviChunk {
public static ListChunk parseFrom(int listType, ParsableByteArray body) {
ImmutableList.Builder<AviChunk> builder = new ImmutableList.Builder<>();
int listBodyEndPosition = body.limit();
@C.TrackType int currentTrackType = C.TRACK_TYPE_NONE;
while (body.bytesLeft() > 8) {
int type = body.readLittleEndianInt();
int size = body.readLittleEndianInt();
int innerBoxBodyEndPosition = body.getPosition() + size;
body.setLimit(innerBoxBodyEndPosition);
@Nullable AviChunk aviChunk;
if (type == AviExtractor.FOURCC_LIST) {
int innerListType = body.readLittleEndianInt();
aviChunk = parseFrom(innerListType, body);
} else {
aviChunk = createBox(type, currentTrackType, body);
}
if (aviChunk != null) {
if (aviChunk.getType() == AviExtractor.FOURCC_strh) {
currentTrackType = ((AviStreamHeaderChunk) aviChunk).getTrackType();
}
builder.add(aviChunk);
}
body.setPosition(innerBoxBodyEndPosition);
body.setLimit(listBodyEndPosition);
}
return new ListChunk(listType, builder.build());
}
public final ImmutableList<AviChunk> children;
private final int type;
private ListChunk(int type, ImmutableList<AviChunk> children) {
this.type = type;
this.children = children;
}
@Override
public int getType() {
return type;
}
@Nullable
@SuppressWarnings("unchecked")
public <T extends AviChunk> T getChild(Class<T> c) {
for (AviChunk aviChunk : children) {
if (aviChunk.getClass() == c) {
return (T) aviChunk;
}
}
return null;
}
@Nullable
private static AviChunk createBox(
int chunkType, @C.TrackType int trackType, ParsableByteArray body) {
switch (chunkType) {
case AviExtractor.FOURCC_avih:
return AviMainHeaderChunk.parseFrom(body);
case AviExtractor.FOURCC_strh:
return AviStreamHeaderChunk.parseFrom(body);
case AviExtractor.FOURCC_strf:
return StreamFormatChunk.parseFrom(trackType, body);
case AviExtractor.FOURCC_strn:
return StreamNameChunk.parseFrom(body);
default:
return null;
}
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
/** Holds the {@link Format} information contained in an STRF chunk. */
/* package */ final class StreamFormatChunk implements AviChunk {
private static final String TAG = "StreamFormatChunk";
@Nullable
public static AviChunk parseFrom(int trackType, ParsableByteArray body) {
if (trackType == C.TRACK_TYPE_VIDEO) {
return parseBitmapInfoHeader(body);
} else if (trackType == C.TRACK_TYPE_AUDIO) {
return parseWaveFormatEx(body);
} else {
Log.w(
TAG,
"Ignoring strf box for unsupported track type: " + Util.getTrackTypeString(trackType));
return null;
}
}
public final Format format;
public StreamFormatChunk(Format format) {
this.format = format;
}
@Override
public int getType() {
return AviExtractor.FOURCC_strf;
}
@Nullable
private static AviChunk parseBitmapInfoHeader(ParsableByteArray body) {
body.skipBytes(4); // biSize.
int width = body.readLittleEndianInt();
int height = body.readLittleEndianInt();
body.skipBytes(4); // biPlanes (2 bytes), biBitCount (2 bytes).
int compression = body.readLittleEndianInt();
String mimeType = getMimeTypeFromCompression(compression);
if (mimeType == null) {
Log.w(TAG, "Ignoring track with unsupported compression " + compression);
return null;
}
Format.Builder formatBuilder = new Format.Builder();
formatBuilder.setWidth(width).setHeight(height).setSampleMimeType(mimeType);
return new StreamFormatChunk(formatBuilder.build());
}
// Syntax defined by the WAVEFORMATEX structure. See
// https://docs.microsoft.com/en-us/previous-versions/dd757713(v=vs.85).
@Nullable
private static AviChunk parseWaveFormatEx(ParsableByteArray body) {
int formatTag = body.readLittleEndianUnsignedShort();
@Nullable String mimeType = getMimeTypeFromTag(formatTag);
if (mimeType == null) {
Log.w(TAG, "Ignoring track with unsupported format tag " + formatTag);
return null;
}
int channelCount = body.readLittleEndianUnsignedShort();
int samplesPerSecond = body.readLittleEndianInt();
body.skipBytes(6); // averageBytesPerSecond (4 bytes), nBlockAlign (2 bytes).
int bitsPerSample = body.readUnsignedShort();
int pcmEncoding = Util.getPcmEncoding(bitsPerSample);
int cbSize = body.readLittleEndianUnsignedShort();
byte[] codecData = new byte[cbSize];
body.readBytes(codecData, /* offset= */ 0, codecData.length);
Format.Builder formatBuilder = new Format.Builder();
formatBuilder
.setSampleMimeType(mimeType)
.setChannelCount(channelCount)
.setSampleRate(samplesPerSecond);
if (MimeTypes.AUDIO_RAW.equals(mimeType) && pcmEncoding != C.ENCODING_INVALID) {
formatBuilder.setPcmEncoding(pcmEncoding);
}
if (MimeTypes.AUDIO_AAC.equals(mimeType) && codecData.length > 0) {
formatBuilder.setInitializationData(ImmutableList.of(codecData));
}
return new StreamFormatChunk(formatBuilder.build());
}
@Nullable
private static String getMimeTypeFromTag(int tag) {
switch (tag) {
case 0x1: // WAVE_FORMAT_PCM
return MimeTypes.AUDIO_RAW;
case 0x55: // WAVE_FORMAT_MPEGLAYER3
return MimeTypes.AUDIO_MPEG;
case 0xff: // WAVE_FORMAT_AAC
return MimeTypes.AUDIO_AAC;
case 0x2000: // WAVE_FORMAT_DVM - AC3
return MimeTypes.AUDIO_AC3;
case 0x2001: // WAVE_FORMAT_DTS2
return MimeTypes.AUDIO_DTS;
default:
return null;
}
}
@Nullable
private static String getMimeTypeFromCompression(int compression) {
switch (compression) {
case 0x3234504d: // MP42
return MimeTypes.VIDEO_MP42;
case 0x3334504d: // MP43
return MimeTypes.VIDEO_MP43;
case 0x34363248: // H264
case 0x31637661: // avc1
case 0x31435641: // AVC1
return MimeTypes.VIDEO_H264;
case 0x44495633: // 3VID
case 0x78766964: // divx
case 0x58564944: // DIVX
case 0x30355844: // DX50
case 0x34504d46: // FMP4
case 0x64697678: // xvid
case 0x44495658: // XVID
return MimeTypes.VIDEO_MP4V;
case 0x47504a4d: // MJPG
case 0x67706a6d: // mjpg
return MimeTypes.VIDEO_MJPEG;
default:
return null;
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import androidx.media3.common.util.ParsableByteArray;
/** Parses and contains the name from the STRN chunk. */
/* package */ final class StreamNameChunk implements AviChunk {
public static StreamNameChunk parseFrom(ParsableByteArray body) {
return new StreamNameChunk(body.readString(body.bytesLeft()));
}
public final String name;
private StreamNameChunk(String name) {
this.name = name;
}
@Override
public int getType() {
return AviExtractor.FOURCC_strn;
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 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.
*/
@NonNullApi
package androidx.media3.extractor.avi;
import androidx.media3.common.util.NonNullApi;

View File

@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.extractor.amr.AmrExtractor; import androidx.media3.extractor.amr.AmrExtractor;
import androidx.media3.extractor.avi.AviExtractor;
import androidx.media3.extractor.flac.FlacExtractor; import androidx.media3.extractor.flac.FlacExtractor;
import androidx.media3.extractor.flv.FlvExtractor; import androidx.media3.extractor.flv.FlvExtractor;
import androidx.media3.extractor.jpeg.JpegExtractor; import androidx.media3.extractor.jpeg.JpegExtractor;
@ -70,6 +71,7 @@ public final class DefaultExtractorsFactoryTest {
Ac3Extractor.class, Ac3Extractor.class,
Ac4Extractor.class, Ac4Extractor.class,
Mp3Extractor.class, Mp3Extractor.class,
AviExtractor.class,
JpegExtractor.class) JpegExtractor.class)
.inOrder(); .inOrder();
} }
@ -112,6 +114,7 @@ public final class DefaultExtractorsFactoryTest {
AdtsExtractor.class, AdtsExtractor.class,
Ac3Extractor.class, Ac3Extractor.class,
Ac4Extractor.class, Ac4Extractor.class,
AviExtractor.class,
JpegExtractor.class) JpegExtractor.class)
.inOrder(); .inOrder();
} }

View File

@ -0,0 +1,41 @@
/*
* Copyright 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 androidx.media3.extractor.avi;
import androidx.media3.test.utils.ExtractorAsserts;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Tests for {@link AviExtractor}. */
@RunWith(ParameterizedRobolectricTestRunner.class)
public final class AviExtractorTest {
@Parameters(name = "{0}")
public static ImmutableList<ExtractorAsserts.SimulationConfig> params() {
return ExtractorAsserts.configs();
}
@Parameter public ExtractorAsserts.SimulationConfig simulationConfig;
@Test
public void aviSample() throws Exception {
ExtractorAsserts.assertBehavior(AviExtractor::new, "media/avi/sample.avi", simulationConfig);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,751 @@
seekMap:
isSeekable = true
duration = 4080000
getPosition(0) = [[timeUs=0, position=9996]]
getPosition(1) = [[timeUs=0, position=9996]]
getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]]
getPosition(4080000) = [[timeUs=3837136, position=308434]]
numberOfTracks = 2
track 0:
total output bytes = 165531
sample count = 66
format 0:
id = 0
sampleMimeType = video/mp4v-es
maxInputSize = 6761
width = 960
height = 400
sample 0:
time = 1334666
flags = 1
data = length 5891, hash 4627CBC3
sample 1:
time = 1376374
flags = 0
data = length 3154, hash B7484F2C
sample 2:
time = 1418083
flags = 0
data = length 2409, hash 93E50DB6
sample 3:
time = 1459791
flags = 0
data = length 2296, hash 73A46768
sample 4:
time = 1501499
flags = 0
data = length 2514, hash F71DCA93
sample 5:
time = 1543208
flags = 0
data = length 2614, hash BDD6744E
sample 6:
time = 1584916
flags = 0
data = length 2797, hash 81BED431
sample 7:
time = 1626624
flags = 0
data = length 1549, hash D892E824
sample 8:
time = 1668333
flags = 0
data = length 2714, hash B3EE7E2A
sample 9:
time = 1710041
flags = 0
data = length 2002, hash BC9E16ED
sample 10:
time = 1751749
flags = 0
data = length 2726, hash C31D5A82
sample 11:
time = 1793458
flags = 0
data = length 2639, hash AE67DC59
sample 12:
time = 1835166
flags = 1
data = length 5011, hash 630ADA59
sample 13:
time = 1876874
flags = 0
data = length 4356, hash 76CE0D21
sample 14:
time = 1918583
flags = 0
data = length 1986, hash AC41A7FC
sample 15:
time = 1960291
flags = 0
data = length 2792, hash 497D3A2D
sample 16:
time = 2001999
flags = 0
data = length 2176, hash FADAC8ED
sample 17:
time = 2043708
flags = 0
data = length 2463, hash 379DE4C8
sample 18:
time = 2085416
flags = 0
data = length 2472, hash 9E68BAC5
sample 19:
time = 2127124
flags = 0
data = length 1960, hash 38BC3EFC
sample 20:
time = 2168832
flags = 0
data = length 1833, hash 139C885B
sample 21:
time = 2210541
flags = 0
data = length 1865, hash A14BE838
sample 22:
time = 2252249
flags = 0
data = length 1491, hash 8EC33935
sample 23:
time = 2293957
flags = 0
data = length 1403, hash 78D87F2C
sample 24:
time = 2335666
flags = 1
data = length 4936, hash C34CC2D0
sample 25:
time = 2377374
flags = 0
data = length 2539, hash D0EDEC2B
sample 26:
time = 2419082
flags = 0
data = length 3052, hash 3F68900F
sample 27:
time = 2460791
flags = 0
data = length 2998, hash B531AC4
sample 28:
time = 2502499
flags = 0
data = length 1670, hash 734A2739
sample 29:
time = 2544207
flags = 0
data = length 1634, hash 60A39EA5
sample 30:
time = 2585916
flags = 0
data = length 1623, hash B18B39FE
sample 31:
time = 2627624
flags = 0
data = length 806, hash DA70C12B
sample 32:
time = 2669332
flags = 0
data = length 990, hash A1642D2C
sample 33:
time = 2711041
flags = 0
data = length 903, hash 411ECEA3
sample 34:
time = 2752749
flags = 0
data = length 713, hash A4DAFA22
sample 35:
time = 2794457
flags = 0
data = length 749, hash F39941EF
sample 36:
time = 2836166
flags = 1
data = length 5258, hash 19670F6D
sample 37:
time = 2877874
flags = 0
data = length 1932, hash 3F7F6D21
sample 38:
time = 2919582
flags = 0
data = length 731, hash 45EF5D68
sample 39:
time = 2961291
flags = 0
data = length 1076, hash 8C23B3FF
sample 40:
time = 3002999
flags = 0
data = length 1560, hash D6133304
sample 41:
time = 3044707
flags = 0
data = length 2564, hash B7B256B
sample 42:
time = 3086416
flags = 0
data = length 2789, hash 97736B63
sample 43:
time = 3128124
flags = 0
data = length 2469, hash C65A89B6
sample 44:
time = 3169832
flags = 0
data = length 2203, hash D89686B4
sample 45:
time = 3211541
flags = 0
data = length 2097, hash 91358D88
sample 46:
time = 3253249
flags = 0
data = length 2043, hash 50547CF1
sample 47:
time = 3294957
flags = 0
data = length 2198, hash F93F1606
sample 48:
time = 3336666
flags = 1
data = length 5084, hash BEC89380
sample 49:
time = 3378374
flags = 0
data = length 3043, hash F3C50E5A
sample 50:
time = 3420082
flags = 0
data = length 2786, hash 49C8C67C
sample 51:
time = 3461791
flags = 0
data = length 2652, hash D0A93BE7
sample 52:
time = 3503499
flags = 0
data = length 2675, hash 81F7F5BD
sample 53:
time = 3545207
flags = 0
data = length 2916, hash E2A38AE1
sample 54:
time = 3586916
flags = 0
data = length 2574, hash 50EC13BD
sample 55:
time = 3628624
flags = 0
data = length 2644, hash 3DF461F4
sample 56:
time = 3670332
flags = 0
data = length 2932, hash E2F2DAB0
sample 57:
time = 3712041
flags = 0
data = length 2625, hash 100D69E1
sample 58:
time = 3753749
flags = 0
data = length 2773, hash 347DCC1F
sample 59:
time = 3795457
flags = 0
data = length 2348, hash 51FC01A3
sample 60:
time = 3837166
flags = 1
data = length 5356, hash 190A3CAE
sample 61:
time = 3878874
flags = 0
data = length 3172, hash 538FA2AE
sample 62:
time = 3920582
flags = 0
data = length 2393, hash 525B26D6
sample 63:
time = 3962291
flags = 0
data = length 2307, hash C894745F
sample 64:
time = 4003999
flags = 0
data = length 2490, hash 800FED70
sample 65:
time = 4045707
flags = 0
data = length 2115, hash A2512D3
track 1:
total output bytes = 44160
sample count = 115
format 0:
id = 1
sampleMimeType = audio/mpeg
maxInputSize = 384
channelCount = 2
sampleRate = 48000
sample 0:
time = 1296000
flags = 1
data = length 384, hash 4338D4A1
sample 1:
time = 1320000
flags = 1
data = length 384, hash C65E6D68
sample 2:
time = 1344000
flags = 1
data = length 384, hash AE2762E8
sample 3:
time = 1368000
flags = 1
data = length 384, hash 8CFEAA7F
sample 4:
time = 1392000
flags = 1
data = length 384, hash A96A80B4
sample 5:
time = 1416000
flags = 1
data = length 384, hash 69A84538
sample 6:
time = 1440000
flags = 1
data = length 384, hash 9131F77E
sample 7:
time = 1464000
flags = 1
data = length 384, hash 818091B1
sample 8:
time = 1488000
flags = 1
data = length 384, hash 6DBECC10
sample 9:
time = 1512000
flags = 1
data = length 384, hash A9912967
sample 10:
time = 1536000
flags = 1
data = length 384, hash 4DA97472
sample 11:
time = 1560000
flags = 1
data = length 384, hash 31B363DC
sample 12:
time = 1584000
flags = 1
data = length 384, hash E15BB36C
sample 13:
time = 1608000
flags = 1
data = length 384, hash 159C963C
sample 14:
time = 1632000
flags = 1
data = length 384, hash 50B874D
sample 15:
time = 1656000
flags = 1
data = length 384, hash 8727F339
sample 16:
time = 1680000
flags = 1
data = length 384, hash C0853B6
sample 17:
time = 1704000
flags = 1
data = length 384, hash 9E026376
sample 18:
time = 1728000
flags = 1
data = length 384, hash C190380F
sample 19:
time = 1752000
flags = 1
data = length 384, hash 20925F33
sample 20:
time = 1776000
flags = 1
data = length 384, hash 9F740DAF
sample 21:
time = 1800000
flags = 1
data = length 384, hash F75757D6
sample 22:
time = 1824000
flags = 1
data = length 384, hash 46D76ED0
sample 23:
time = 1848000
flags = 1
data = length 384, hash 11DC480F
sample 24:
time = 1872000
flags = 1
data = length 384, hash 3428D6D8
sample 25:
time = 1896000
flags = 1
data = length 384, hash 16A11668
sample 26:
time = 1920000
flags = 1
data = length 384, hash 4CFBA63C
sample 27:
time = 1944000
flags = 1
data = length 384, hash 2B6702A9
sample 28:
time = 1968000
flags = 1
data = length 384, hash D047CEF9
sample 29:
time = 1992000
flags = 1
data = length 384, hash 25F05663
sample 30:
time = 2016000
flags = 1
data = length 384, hash 947441C7
sample 31:
time = 2040000
flags = 1
data = length 384, hash E82145F7
sample 32:
time = 2064000
flags = 1
data = length 384, hash 6C40F859
sample 33:
time = 2088000
flags = 1
data = length 384, hash 273FBEF8
sample 34:
time = 2112000
flags = 1
data = length 384, hash 2FF062B6
sample 35:
time = 2136000
flags = 1
data = length 384, hash 73FF8D58
sample 36:
time = 2160000
flags = 1
data = length 384, hash F2BAB943
sample 37:
time = 2184000
flags = 1
data = length 384, hash 507DEF9F
sample 38:
time = 2208000
flags = 1
data = length 384, hash 913E927A
sample 39:
time = 2232000
flags = 1
data = length 384, hash AFFD0AED
sample 40:
time = 2256000
flags = 1
data = length 384, hash EE0C6F4C
sample 41:
time = 2280000
flags = 1
data = length 384, hash 70726632
sample 42:
time = 2304000
flags = 1
data = length 384, hash B5D49F8
sample 43:
time = 2328000
flags = 1
data = length 384, hash B341AF3F
sample 44:
time = 2352000
flags = 1
data = length 384, hash 6AC1D8C4
sample 45:
time = 2376000
flags = 1
data = length 384, hash BC666685
sample 46:
time = 2400000
flags = 1
data = length 384, hash E58054E8
sample 47:
time = 2424000
flags = 1
data = length 384, hash 404AB403
sample 48:
time = 2448000
flags = 1
data = length 384, hash 265A86B8
sample 49:
time = 2472000
flags = 1
data = length 384, hash 306316F6
sample 50:
time = 2496000
flags = 1
data = length 384, hash 7BFDEA60
sample 51:
time = 2520000
flags = 1
data = length 384, hash 2EFF8E5B
sample 52:
time = 2544000
flags = 1
data = length 384, hash C06CE84C
sample 53:
time = 2568000
flags = 1
data = length 384, hash 9069A01E
sample 54:
time = 2592000
flags = 1
data = length 384, hash 4A78F181
sample 55:
time = 2616000
flags = 1
data = length 384, hash 57FD4BE7
sample 56:
time = 2640000
flags = 1
data = length 384, hash B09DB688
sample 57:
time = 2664000
flags = 1
data = length 384, hash 5602C52F
sample 58:
time = 2688000
flags = 1
data = length 384, hash 77762F5D
sample 59:
time = 2712000
flags = 1
data = length 384, hash 6A0BDB6
sample 60:
time = 2736000
flags = 1
data = length 384, hash 2428C91
sample 61:
time = 2760000
flags = 1
data = length 384, hash DEB54354
sample 62:
time = 2784000
flags = 1
data = length 384, hash FB0B7BEE
sample 63:
time = 2808000
flags = 1
data = length 384, hash BDD82F68
sample 64:
time = 2832000
flags = 1
data = length 384, hash BAB3B808
sample 65:
time = 2856000
flags = 1
data = length 384, hash E9183572
sample 66:
time = 2880000
flags = 1
data = length 384, hash 9E36BC40
sample 67:
time = 2904000
flags = 1
data = length 384, hash 937ED026
sample 68:
time = 2928000
flags = 1
data = length 384, hash BF337AD1
sample 69:
time = 2952000
flags = 1
data = length 384, hash E381C534
sample 70:
time = 2976000
flags = 1
data = length 384, hash 6C9E1D71
sample 71:
time = 3000000
flags = 1
data = length 384, hash 1C359B93
sample 72:
time = 3024000
flags = 1
data = length 384, hash 3D137C16
sample 73:
time = 3048000
flags = 1
data = length 384, hash 90D23677
sample 74:
time = 3072000
flags = 1
data = length 384, hash 438F4839
sample 75:
time = 3096000
flags = 1
data = length 384, hash EBAF44EF
sample 76:
time = 3120000
flags = 1
data = length 384, hash D8F64C54
sample 77:
time = 3144000
flags = 1
data = length 384, hash 994F2EA6
sample 78:
time = 3168000
flags = 1
data = length 384, hash 7E9DF6E4
sample 79:
time = 3192000
flags = 1
data = length 384, hash 577F18B8
sample 80:
time = 3216000
flags = 1
data = length 384, hash A47FCEE
sample 81:
time = 3240000
flags = 1
data = length 384, hash 5A1C435E
sample 82:
time = 3264000
flags = 1
data = length 384, hash 1D9EDC36
sample 83:
time = 3288000
flags = 1
data = length 384, hash 3355333
sample 84:
time = 3312000
flags = 1
data = length 384, hash 27CA9735
sample 85:
time = 3336000
flags = 1
data = length 384, hash EB74B3F7
sample 86:
time = 3360000
flags = 1
data = length 384, hash 22AC46CB
sample 87:
time = 3384000
flags = 1
data = length 384, hash 6002643D
sample 88:
time = 3408000
flags = 1
data = length 384, hash 487449E
sample 89:
time = 3432000
flags = 1
data = length 384, hash C5B10A14
sample 90:
time = 3456000
flags = 1
data = length 384, hash 6050635D
sample 91:
time = 3480000
flags = 1
data = length 384, hash 437EAD63
sample 92:
time = 3504000
flags = 1
data = length 384, hash 55A02C25
sample 93:
time = 3528000
flags = 1
data = length 384, hash 171CCC00
sample 94:
time = 3552000
flags = 1
data = length 384, hash 911127C8
sample 95:
time = 3576000
flags = 1
data = length 384, hash AA157B50
sample 96:
time = 3600000
flags = 1
data = length 384, hash 26F2D866
sample 97:
time = 3624000
flags = 1
data = length 384, hash 67ADB3A9
sample 98:
time = 3648000
flags = 1
data = length 384, hash F118D82D
sample 99:
time = 3672000
flags = 1
data = length 384, hash F51C252B
sample 100:
time = 3696000
flags = 1
data = length 384, hash BD13B97C
sample 101:
time = 3720000
flags = 1
data = length 384, hash 24BCF0AB
sample 102:
time = 3744000
flags = 1
data = length 384, hash 18DE9193
sample 103:
time = 3768000
flags = 1
data = length 384, hash 234D8C99
sample 104:
time = 3792000
flags = 1
data = length 384, hash EDFD2511
sample 105:
time = 3816000
flags = 1
data = length 384, hash 69E3E157
sample 106:
time = 3840000
flags = 1
data = length 384, hash AC90ADEC
sample 107:
time = 3864000
flags = 1
data = length 384, hash 6A333A56
sample 108:
time = 3888000
flags = 1
data = length 384, hash 493D75A3
sample 109:
time = 3912000
flags = 1
data = length 384, hash 53FE2A9E
sample 110:
time = 3936000
flags = 1
data = length 384, hash 65D6147C
sample 111:
time = 3960000
flags = 1
data = length 384, hash 5E744FB2
sample 112:
time = 3984000
flags = 1
data = length 384, hash 68AEB7CA
sample 113:
time = 4008000
flags = 1
data = length 384, hash AC2972C
sample 114:
time = 4032000
flags = 1
data = length 384, hash E2A06CB9
tracksEnded = true

View File

@ -0,0 +1,487 @@
seekMap:
isSeekable = true
duration = 4080000
getPosition(0) = [[timeUs=0, position=9996]]
getPosition(1) = [[timeUs=0, position=9996]]
getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]]
getPosition(4080000) = [[timeUs=3837136, position=308434]]
numberOfTracks = 2
track 0:
total output bytes = 102418
sample count = 42
format 0:
id = 0
sampleMimeType = video/mp4v-es
maxInputSize = 6761
width = 960
height = 400
sample 0:
time = 2335666
flags = 1
data = length 4936, hash C34CC2D0
sample 1:
time = 2377374
flags = 0
data = length 2539, hash D0EDEC2B
sample 2:
time = 2419082
flags = 0
data = length 3052, hash 3F68900F
sample 3:
time = 2460791
flags = 0
data = length 2998, hash B531AC4
sample 4:
time = 2502499
flags = 0
data = length 1670, hash 734A2739
sample 5:
time = 2544207
flags = 0
data = length 1634, hash 60A39EA5
sample 6:
time = 2585916
flags = 0
data = length 1623, hash B18B39FE
sample 7:
time = 2627624
flags = 0
data = length 806, hash DA70C12B
sample 8:
time = 2669332
flags = 0
data = length 990, hash A1642D2C
sample 9:
time = 2711041
flags = 0
data = length 903, hash 411ECEA3
sample 10:
time = 2752749
flags = 0
data = length 713, hash A4DAFA22
sample 11:
time = 2794457
flags = 0
data = length 749, hash F39941EF
sample 12:
time = 2836166
flags = 1
data = length 5258, hash 19670F6D
sample 13:
time = 2877874
flags = 0
data = length 1932, hash 3F7F6D21
sample 14:
time = 2919582
flags = 0
data = length 731, hash 45EF5D68
sample 15:
time = 2961291
flags = 0
data = length 1076, hash 8C23B3FF
sample 16:
time = 3002999
flags = 0
data = length 1560, hash D6133304
sample 17:
time = 3044707
flags = 0
data = length 2564, hash B7B256B
sample 18:
time = 3086416
flags = 0
data = length 2789, hash 97736B63
sample 19:
time = 3128124
flags = 0
data = length 2469, hash C65A89B6
sample 20:
time = 3169832
flags = 0
data = length 2203, hash D89686B4
sample 21:
time = 3211541
flags = 0
data = length 2097, hash 91358D88
sample 22:
time = 3253249
flags = 0
data = length 2043, hash 50547CF1
sample 23:
time = 3294957
flags = 0
data = length 2198, hash F93F1606
sample 24:
time = 3336666
flags = 1
data = length 5084, hash BEC89380
sample 25:
time = 3378374
flags = 0
data = length 3043, hash F3C50E5A
sample 26:
time = 3420082
flags = 0
data = length 2786, hash 49C8C67C
sample 27:
time = 3461791
flags = 0
data = length 2652, hash D0A93BE7
sample 28:
time = 3503499
flags = 0
data = length 2675, hash 81F7F5BD
sample 29:
time = 3545207
flags = 0
data = length 2916, hash E2A38AE1
sample 30:
time = 3586916
flags = 0
data = length 2574, hash 50EC13BD
sample 31:
time = 3628624
flags = 0
data = length 2644, hash 3DF461F4
sample 32:
time = 3670332
flags = 0
data = length 2932, hash E2F2DAB0
sample 33:
time = 3712041
flags = 0
data = length 2625, hash 100D69E1
sample 34:
time = 3753749
flags = 0
data = length 2773, hash 347DCC1F
sample 35:
time = 3795457
flags = 0
data = length 2348, hash 51FC01A3
sample 36:
time = 3837166
flags = 1
data = length 5356, hash 190A3CAE
sample 37:
time = 3878874
flags = 0
data = length 3172, hash 538FA2AE
sample 38:
time = 3920582
flags = 0
data = length 2393, hash 525B26D6
sample 39:
time = 3962291
flags = 0
data = length 2307, hash C894745F
sample 40:
time = 4003999
flags = 0
data = length 2490, hash 800FED70
sample 41:
time = 4045707
flags = 0
data = length 2115, hash A2512D3
track 1:
total output bytes = 28032
sample count = 73
format 0:
id = 1
sampleMimeType = audio/mpeg
maxInputSize = 384
channelCount = 2
sampleRate = 48000
sample 0:
time = 2304000
flags = 1
data = length 384, hash B5D49F8
sample 1:
time = 2328000
flags = 1
data = length 384, hash B341AF3F
sample 2:
time = 2352000
flags = 1
data = length 384, hash 6AC1D8C4
sample 3:
time = 2376000
flags = 1
data = length 384, hash BC666685
sample 4:
time = 2400000
flags = 1
data = length 384, hash E58054E8
sample 5:
time = 2424000
flags = 1
data = length 384, hash 404AB403
sample 6:
time = 2448000
flags = 1
data = length 384, hash 265A86B8
sample 7:
time = 2472000
flags = 1
data = length 384, hash 306316F6
sample 8:
time = 2496000
flags = 1
data = length 384, hash 7BFDEA60
sample 9:
time = 2520000
flags = 1
data = length 384, hash 2EFF8E5B
sample 10:
time = 2544000
flags = 1
data = length 384, hash C06CE84C
sample 11:
time = 2568000
flags = 1
data = length 384, hash 9069A01E
sample 12:
time = 2592000
flags = 1
data = length 384, hash 4A78F181
sample 13:
time = 2616000
flags = 1
data = length 384, hash 57FD4BE7
sample 14:
time = 2640000
flags = 1
data = length 384, hash B09DB688
sample 15:
time = 2664000
flags = 1
data = length 384, hash 5602C52F
sample 16:
time = 2688000
flags = 1
data = length 384, hash 77762F5D
sample 17:
time = 2712000
flags = 1
data = length 384, hash 6A0BDB6
sample 18:
time = 2736000
flags = 1
data = length 384, hash 2428C91
sample 19:
time = 2760000
flags = 1
data = length 384, hash DEB54354
sample 20:
time = 2784000
flags = 1
data = length 384, hash FB0B7BEE
sample 21:
time = 2808000
flags = 1
data = length 384, hash BDD82F68
sample 22:
time = 2832000
flags = 1
data = length 384, hash BAB3B808
sample 23:
time = 2856000
flags = 1
data = length 384, hash E9183572
sample 24:
time = 2880000
flags = 1
data = length 384, hash 9E36BC40
sample 25:
time = 2904000
flags = 1
data = length 384, hash 937ED026
sample 26:
time = 2928000
flags = 1
data = length 384, hash BF337AD1
sample 27:
time = 2952000
flags = 1
data = length 384, hash E381C534
sample 28:
time = 2976000
flags = 1
data = length 384, hash 6C9E1D71
sample 29:
time = 3000000
flags = 1
data = length 384, hash 1C359B93
sample 30:
time = 3024000
flags = 1
data = length 384, hash 3D137C16
sample 31:
time = 3048000
flags = 1
data = length 384, hash 90D23677
sample 32:
time = 3072000
flags = 1
data = length 384, hash 438F4839
sample 33:
time = 3096000
flags = 1
data = length 384, hash EBAF44EF
sample 34:
time = 3120000
flags = 1
data = length 384, hash D8F64C54
sample 35:
time = 3144000
flags = 1
data = length 384, hash 994F2EA6
sample 36:
time = 3168000
flags = 1
data = length 384, hash 7E9DF6E4
sample 37:
time = 3192000
flags = 1
data = length 384, hash 577F18B8
sample 38:
time = 3216000
flags = 1
data = length 384, hash A47FCEE
sample 39:
time = 3240000
flags = 1
data = length 384, hash 5A1C435E
sample 40:
time = 3264000
flags = 1
data = length 384, hash 1D9EDC36
sample 41:
time = 3288000
flags = 1
data = length 384, hash 3355333
sample 42:
time = 3312000
flags = 1
data = length 384, hash 27CA9735
sample 43:
time = 3336000
flags = 1
data = length 384, hash EB74B3F7
sample 44:
time = 3360000
flags = 1
data = length 384, hash 22AC46CB
sample 45:
time = 3384000
flags = 1
data = length 384, hash 6002643D
sample 46:
time = 3408000
flags = 1
data = length 384, hash 487449E
sample 47:
time = 3432000
flags = 1
data = length 384, hash C5B10A14
sample 48:
time = 3456000
flags = 1
data = length 384, hash 6050635D
sample 49:
time = 3480000
flags = 1
data = length 384, hash 437EAD63
sample 50:
time = 3504000
flags = 1
data = length 384, hash 55A02C25
sample 51:
time = 3528000
flags = 1
data = length 384, hash 171CCC00
sample 52:
time = 3552000
flags = 1
data = length 384, hash 911127C8
sample 53:
time = 3576000
flags = 1
data = length 384, hash AA157B50
sample 54:
time = 3600000
flags = 1
data = length 384, hash 26F2D866
sample 55:
time = 3624000
flags = 1
data = length 384, hash 67ADB3A9
sample 56:
time = 3648000
flags = 1
data = length 384, hash F118D82D
sample 57:
time = 3672000
flags = 1
data = length 384, hash F51C252B
sample 58:
time = 3696000
flags = 1
data = length 384, hash BD13B97C
sample 59:
time = 3720000
flags = 1
data = length 384, hash 24BCF0AB
sample 60:
time = 3744000
flags = 1
data = length 384, hash 18DE9193
sample 61:
time = 3768000
flags = 1
data = length 384, hash 234D8C99
sample 62:
time = 3792000
flags = 1
data = length 384, hash EDFD2511
sample 63:
time = 3816000
flags = 1
data = length 384, hash 69E3E157
sample 64:
time = 3840000
flags = 1
data = length 384, hash AC90ADEC
sample 65:
time = 3864000
flags = 1
data = length 384, hash 6A333A56
sample 66:
time = 3888000
flags = 1
data = length 384, hash 493D75A3
sample 67:
time = 3912000
flags = 1
data = length 384, hash 53FE2A9E
sample 68:
time = 3936000
flags = 1
data = length 384, hash 65D6147C
sample 69:
time = 3960000
flags = 1
data = length 384, hash 5E744FB2
sample 70:
time = 3984000
flags = 1
data = length 384, hash 68AEB7CA
sample 71:
time = 4008000
flags = 1
data = length 384, hash AC2972C
sample 72:
time = 4032000
flags = 1
data = length 384, hash E2A06CB9
tracksEnded = true

View File

@ -0,0 +1,91 @@
seekMap:
isSeekable = true
duration = 4080000
getPosition(0) = [[timeUs=0, position=9996]]
getPosition(1) = [[timeUs=0, position=9996]]
getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]]
getPosition(4080000) = [[timeUs=3837136, position=308434]]
numberOfTracks = 2
track 0:
total output bytes = 17833
sample count = 6
format 0:
id = 0
sampleMimeType = video/mp4v-es
maxInputSize = 6761
width = 960
height = 400
sample 0:
time = 3837166
flags = 1
data = length 5356, hash 190A3CAE
sample 1:
time = 3878874
flags = 0
data = length 3172, hash 538FA2AE
sample 2:
time = 3920582
flags = 0
data = length 2393, hash 525B26D6
sample 3:
time = 3962291
flags = 0
data = length 2307, hash C894745F
sample 4:
time = 4003999
flags = 0
data = length 2490, hash 800FED70
sample 5:
time = 4045707
flags = 0
data = length 2115, hash A2512D3
track 1:
total output bytes = 3840
sample count = 10
format 0:
id = 1
sampleMimeType = audio/mpeg
maxInputSize = 384
channelCount = 2
sampleRate = 48000
sample 0:
time = 3816000
flags = 1
data = length 384, hash 69E3E157
sample 1:
time = 3840000
flags = 1
data = length 384, hash AC90ADEC
sample 2:
time = 3864000
flags = 1
data = length 384, hash 6A333A56
sample 3:
time = 3888000
flags = 1
data = length 384, hash 493D75A3
sample 4:
time = 3912000
flags = 1
data = length 384, hash 53FE2A9E
sample 5:
time = 3936000
flags = 1
data = length 384, hash 65D6147C
sample 6:
time = 3960000
flags = 1
data = length 384, hash 5E744FB2
sample 7:
time = 3984000
flags = 1
data = length 384, hash 68AEB7CA
sample 8:
time = 4008000
flags = 1
data = length 384, hash AC2972C
sample 9:
time = 4032000
flags = 1
data = length 384, hash E2A06CB9
tracksEnded = true