diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor')
103 files changed, 26463 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java new file mode 100644 index 0000000000..b0b7c7da13 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A seeker that supports seeking within a stream by searching for the target frame using binary + * search. + * + * <p>This seeker operates on a stream that contains multiple frames (or samples). Each frame is + * associated with some kind of timestamps, such as stream time, or frame indices. Given a target + * seek time, the seeker will find the corresponding target timestamp, and perform a search + * operation within the stream to identify the target frame and return the byte position in the + * stream of the target frame. + */ +public abstract class BinarySearchSeeker { + + /** A seeker that looks for a given timestamp from an input. */ + protected interface TimestampSeeker { + + /** + * Searches a limited window of the provided input for a target timestamp. The size of the + * window is implementation specific, but should be small enough such that it's reasonable for + * multiple such reads to occur during a seek operation. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @param targetTimestamp The target timestamp. + * @return A {@link TimestampSearchResult} that describes the result of the search. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) + throws IOException, InterruptedException; + + /** Called when a seek operation finishes. */ + default void onSeekFinished() {} + } + + /** + * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the + * timestamp for a seek time position. + */ + public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter { + + @Override + public long timeUsToTargetTime(long timeUs) { + return timeUs; + } + } + + /** + * A converter that converts seek time in stream time into target timestamp for the {@link + * BinarySearchSeeker}. + */ + protected interface SeekTimestampConverter { + /** + * Converts a seek time in microseconds into target timestamp for the {@link + * BinarySearchSeeker}. + */ + long timeUsToTargetTime(long timeUs); + } + + /** + * When seeking within the source, if the offset is smaller than or equal to this value, the seek + * operation will be performed using a skip operation. Otherwise, the source will be reloaded at + * the new seek position. + */ + private static final long MAX_SKIP_BYTES = 256 * 1024; + + protected final BinarySearchSeekMap seekMap; + protected final TimestampSeeker timestampSeeker; + protected @Nullable SeekOperationParams seekOperationParams; + + private final int minimumSearchRange; + + /** + * Constructs an instance. + * + * @param seekTimestampConverter The {@link SeekTimestampConverter} that converts seek time in + * stream time into target timestamp. + * @param timestampSeeker A {@link TimestampSeeker} that will be used to search for timestamps + * within the stream. + * @param durationUs The duration of the stream in microseconds. + * @param floorTimePosition The minimum timestamp value (inclusive) in the stream. + * @param ceilingTimePosition The minimum timestamp value (exclusive) in the stream. + * @param floorBytePosition The starting position of the frame with minimum timestamp value + * (inclusive) in the stream. + * @param ceilingBytePosition The position after the frame with maximum timestamp value in the + * stream. + * @param approxBytesPerFrame Approximated bytes per frame. + * @param minimumSearchRange The minimum byte range that this binary seeker will operate on. If + * the remaining search range is smaller than this value, the search will stop, and the seeker + * will return the position at the floor of the range as the result. + */ + @SuppressWarnings("initialization") + protected BinarySearchSeeker( + SeekTimestampConverter seekTimestampConverter, + TimestampSeeker timestampSeeker, + long durationUs, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame, + int minimumSearchRange) { + this.timestampSeeker = timestampSeeker; + this.minimumSearchRange = minimumSearchRange; + this.seekMap = + new BinarySearchSeekMap( + seekTimestampConverter, + durationUs, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + + /** Returns the seek map for the stream. */ + public final SeekMap getSeekMap() { + return seekMap; + } + + /** + * Sets the target time in microseconds within the stream to seek to. + * + * @param timeUs The target time in microseconds within the stream. + */ + public final void setSeekTargetUs(long timeUs) { + if (seekOperationParams != null && seekOperationParams.getSeekTimeUs() == timeUs) { + return; + } + seekOperationParams = createSeekParamsForTargetTimeUs(timeUs); + } + + /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */ + public final boolean isSeeking() { + return seekOperationParams != null; + } + + /** + * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from + * {@link Extractor}. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder) + throws InterruptedException, IOException { + TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker); + while (true) { + SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams); + long floorPosition = seekOperationParams.getFloorBytePosition(); + long ceilingPosition = seekOperationParams.getCeilingBytePosition(); + long searchPosition = seekOperationParams.getNextSearchBytePosition(); + + if (ceilingPosition - floorPosition <= minimumSearchRange) { + // The seeking range is too small, so we can just continue from the floor position. + markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition); + return seekToPosition(input, floorPosition, seekPositionHolder); + } + if (!skipInputUntilPosition(input, searchPosition)) { + return seekToPosition(input, searchPosition, seekPositionHolder); + } + + input.resetPeekPosition(); + TimestampSearchResult timestampSearchResult = + timestampSeeker.searchForTimestamp(input, seekOperationParams.getTargetTimePosition()); + + switch (timestampSearchResult.type) { + case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED: + seekOperationParams.updateSeekCeiling( + timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); + break; + case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED: + seekOperationParams.updateSeekFloor( + timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); + break; + case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND: + markSeekOperationFinished( + /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate); + skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate); + return seekToPosition( + input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder); + case TimestampSearchResult.TYPE_NO_TIMESTAMP: + // We can't find any timestamp in the search range from the search position. + // Give up, and just continue reading from the last search position in this case. + markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition); + return seekToPosition(input, searchPosition, seekPositionHolder); + default: + throw new IllegalStateException("Invalid case"); + } + } + } + + protected SeekOperationParams createSeekParamsForTargetTimeUs(long timeUs) { + return new SeekOperationParams( + timeUs, + seekMap.timeUsToTargetTime(timeUs), + seekMap.floorTimePosition, + seekMap.ceilingTimePosition, + seekMap.floorBytePosition, + seekMap.ceilingBytePosition, + seekMap.approxBytesPerFrame); + } + + protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { + seekOperationParams = null; + timestampSeeker.onSeekFinished(); + onSeekOperationFinished(foundTargetFrame, resultPosition); + } + + protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { + // Do nothing. + } + + protected final boolean skipInputUntilPosition(ExtractorInput input, long position) + throws IOException, InterruptedException { + long bytesToSkip = position - input.getPosition(); + if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) { + input.skipFully((int) bytesToSkip); + return true; + } + return false; + } + + protected final int seekToPosition( + ExtractorInput input, long position, PositionHolder seekPositionHolder) { + if (position == input.getPosition()) { + return Extractor.RESULT_CONTINUE; + } else { + seekPositionHolder.position = position; + return Extractor.RESULT_SEEK; + } + } + + /** + * Contains parameters for a pending seek operation by {@link BinarySearchSeeker}. + * + * <p>This class holds parameters for a binary-search for the {@code targetTimePosition} in the + * range [floorPosition, ceilingPosition). + */ + protected static class SeekOperationParams { + private final long seekTimeUs; + private final long targetTimePosition; + private final long approxBytesPerFrame; + + private long floorTimePosition; + private long ceilingTimePosition; + private long floorBytePosition; + private long ceilingBytePosition; + private long nextSearchBytePosition; + + /** + * Returns the next position in the stream to search for target frame, given [floorBytePosition, + * ceilingBytePosition), with corresponding [floorTimePosition, ceilingTimePosition). + */ + protected static long calculateNextSearchBytePosition( + long targetTimePosition, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + if (floorBytePosition + 1 >= ceilingBytePosition + || floorTimePosition + 1 >= ceilingTimePosition) { + return floorBytePosition; + } + long seekTimeDuration = targetTimePosition - floorTimePosition; + float estimatedBytesPerTimeUnit = + (float) (ceilingBytePosition - floorBytePosition) + / (ceilingTimePosition - floorTimePosition); + // It's better to under-estimate rather than over-estimate, because the extractor + // input can skip forward easily, but cannot rewind easily (it may require a new connection + // to be made). + // Therefore, we should reduce the estimated position by some amount, so it will converge to + // the correct frame earlier. + long bytesToSkip = (long) (seekTimeDuration * estimatedBytesPerTimeUnit); + long confidenceInterval = bytesToSkip / 20; + long estimatedFramePosition = floorBytePosition + bytesToSkip - approxBytesPerFrame; + long estimatedPosition = estimatedFramePosition - confidenceInterval; + return Util.constrainValue(estimatedPosition, floorBytePosition, ceilingBytePosition - 1); + } + + protected SeekOperationParams( + long seekTimeUs, + long targetTimePosition, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + this.seekTimeUs = seekTimeUs; + this.targetTimePosition = targetTimePosition; + this.floorTimePosition = floorTimePosition; + this.ceilingTimePosition = ceilingTimePosition; + this.floorBytePosition = floorBytePosition; + this.ceilingBytePosition = ceilingBytePosition; + this.approxBytesPerFrame = approxBytesPerFrame; + this.nextSearchBytePosition = + calculateNextSearchBytePosition( + targetTimePosition, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + + /** + * Returns the floor byte position of the range [floorPosition, ceilingPosition) for this seek + * operation. + */ + private long getFloorBytePosition() { + return floorBytePosition; + } + + /** + * Returns the ceiling byte position of the range [floorPosition, ceilingPosition) for this seek + * operation. + */ + private long getCeilingBytePosition() { + return ceilingBytePosition; + } + + /** Returns the target timestamp as translated from the seek time. */ + private long getTargetTimePosition() { + return targetTimePosition; + } + + /** Returns the target seek time in microseconds. */ + private long getSeekTimeUs() { + return seekTimeUs; + } + + /** Updates the floor constraints (inclusive) of the seek operation. */ + private void updateSeekFloor(long floorTimePosition, long floorBytePosition) { + this.floorTimePosition = floorTimePosition; + this.floorBytePosition = floorBytePosition; + updateNextSearchBytePosition(); + } + + /** Updates the ceiling constraints (exclusive) of the seek operation. */ + private void updateSeekCeiling(long ceilingTimePosition, long ceilingBytePosition) { + this.ceilingTimePosition = ceilingTimePosition; + this.ceilingBytePosition = ceilingBytePosition; + updateNextSearchBytePosition(); + } + + /** Returns the next position in the stream to search. */ + private long getNextSearchBytePosition() { + return nextSearchBytePosition; + } + + private void updateNextSearchBytePosition() { + this.nextSearchBytePosition = + calculateNextSearchBytePosition( + targetTimePosition, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + } + + /** + * Represents possible search results for {@link + * TimestampSeeker#searchForTimestamp(ExtractorInput, long)}. + */ + public static final class TimestampSearchResult { + + /** The search found a timestamp that it deems close enough to the given target. */ + public static final int TYPE_TARGET_TIMESTAMP_FOUND = 0; + /** The search found only timestamps larger than the target timestamp. */ + public static final int TYPE_POSITION_OVERESTIMATED = -1; + /** The search found only timestamps smaller than the target timestamp. */ + public static final int TYPE_POSITION_UNDERESTIMATED = -2; + /** The search didn't find any timestamps. */ + public static final int TYPE_NO_TIMESTAMP = -3; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_TARGET_TIMESTAMP_FOUND, + TYPE_POSITION_OVERESTIMATED, + TYPE_POSITION_UNDERESTIMATED, + TYPE_NO_TIMESTAMP + }) + @interface Type {} + + public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT = + new TimestampSearchResult(TYPE_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET); + + /** The type of the result. */ + @Type private final int type; + + /** + * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link + * SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@link + * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link + * SeekOperationParams#floorTimePosition} should be updated with this value. + */ + private final long timestampToUpdate; + /** + * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link + * SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@link + * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link + * SeekOperationParams#floorBytePosition} should be updated with this value. + */ + private final long bytePositionToUpdate; + + private TimestampSearchResult( + @Type int type, long timestampToUpdate, long bytePositionToUpdate) { + this.type = type; + this.timestampToUpdate = timestampToUpdate; + this.bytePositionToUpdate = bytePositionToUpdate; + } + + /** + * Returns a result to signal that the current position in the input stream overestimates the + * true position of the target frame, and the {@link BinarySearchSeeker} should modify its + * {@link SeekOperationParams}'s ceiling timestamp and byte position using the given values. + */ + public static TimestampSearchResult overestimatedResult( + long newCeilingTimestamp, long newCeilingBytePosition) { + return new TimestampSearchResult( + TYPE_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition); + } + + /** + * Returns a result to signal that the current position in the input stream underestimates the + * true position of the target frame, and the {@link BinarySearchSeeker} should modify its + * {@link SeekOperationParams}'s floor timestamp and byte position using the given values. + */ + public static TimestampSearchResult underestimatedResult( + long newFloorTimestamp, long newCeilingBytePosition) { + return new TimestampSearchResult( + TYPE_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition); + } + + /** + * Returns a result to signal that the target timestamp has been found at {@code + * resultBytePosition}, and the seek operation can stop. + */ + public static TimestampSearchResult targetFoundResult(long resultBytePosition) { + return new TimestampSearchResult( + TYPE_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition); + } + } + + /** + * A {@link SeekMap} implementation that returns the estimated byte location from {@link + * SeekOperationParams#calculateNextSearchBytePosition(long, long, long, long, long, long)} for + * each {@link #getSeekPoints(long)} query. + */ + public static class BinarySearchSeekMap implements SeekMap { + private final SeekTimestampConverter seekTimestampConverter; + private final long durationUs; + private final long floorTimePosition; + private final long ceilingTimePosition; + private final long floorBytePosition; + private final long ceilingBytePosition; + private final long approxBytesPerFrame; + + /** Constructs a new instance of this seek map. */ + public BinarySearchSeekMap( + SeekTimestampConverter seekTimestampConverter, + long durationUs, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + this.seekTimestampConverter = seekTimestampConverter; + this.durationUs = durationUs; + this.floorTimePosition = floorTimePosition; + this.ceilingTimePosition = ceilingTimePosition; + this.floorBytePosition = floorBytePosition; + this.ceilingBytePosition = ceilingBytePosition; + this.approxBytesPerFrame = approxBytesPerFrame; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long nextSearchPosition = + SeekOperationParams.calculateNextSearchBytePosition( + /* targetTimePosition= */ seekTimestampConverter.timeUsToTargetTime(timeUs), + /* floorTimePosition= */ floorTimePosition, + /* ceilingTimePosition= */ ceilingTimePosition, + /* floorBytePosition= */ floorBytePosition, + /* ceilingBytePosition= */ ceilingBytePosition, + /* approxBytesPerFrame= */ approxBytesPerFrame); + return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition)); + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** @see SeekTimestampConverter#timeUsToTargetTime(long) */ + public long timeUsToTargetTime(long timeUs) { + return seekTimestampConverter.timeUsToTargetTime(timeUs); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java new file mode 100644 index 0000000000..4fdf9f3c55 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Defines chunks of samples within a media stream. + */ +public final class ChunkIndex implements SeekMap { + + /** + * The number of chunks. + */ + public final int length; + + /** + * The chunk sizes, in bytes. + */ + public final int[] sizes; + + /** + * The chunk byte offsets. + */ + public final long[] offsets; + + /** + * The chunk durations, in microseconds. + */ + public final long[] durationsUs; + + /** + * The start time of each chunk, in microseconds. + */ + public final long[] timesUs; + + private final long durationUs; + + /** + * @param sizes The chunk sizes, in bytes. + * @param offsets The chunk byte offsets. + * @param durationsUs The chunk durations, in microseconds. + * @param timesUs The start time of each chunk, in microseconds. + */ + public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) { + this.sizes = sizes; + this.offsets = offsets; + this.durationsUs = durationsUs; + this.timesUs = timesUs; + length = sizes.length; + if (length > 0) { + durationUs = durationsUs[length - 1] + timesUs[length - 1]; + } else { + durationUs = 0; + } + } + + /** + * Obtains the index of the chunk corresponding to a given time. + * + * @param timeUs The time, in microseconds. + * @return The index of the corresponding chunk. + */ + public int getChunkIndex(long timeUs) { + return Util.binarySearchFloor(timesUs, timeUs, true, true); + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + int chunkIndex = getChunkIndex(timeUs); + SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]); + if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } + } + + @Override + public String toString() { + return "ChunkIndex(" + + "length=" + + length + + ", sizes=" + + Arrays.toString(sizes) + + ", offsets=" + + Arrays.toString(offsets) + + ", timeUs=" + + Arrays.toString(timesUs) + + ", durationsUs=" + + Arrays.toString(durationsUs) + + ")"; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java new file mode 100644 index 0000000000..215aac0e6d --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of + * multiple independent frames of the same size. Seek points are calculated to be at frame + * boundaries. + */ +public class ConstantBitrateSeekMap implements SeekMap { + + private final long inputLength; + private final long firstFrameBytePosition; + private final int frameSize; + private final long dataSize; + private final int bitrate; + private final long durationUs; + + /** + * Constructs a new instance from a stream. + * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFrameBytePosition The byte-position of the first frame in the stream. + * @param bitrate The bitrate (which is assumed to be constant in the stream). + * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET} + * if unknown. + */ + public ConstantBitrateSeekMap( + long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) { + this.inputLength = inputLength; + this.firstFrameBytePosition = firstFrameBytePosition; + this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize; + this.bitrate = bitrate; + + if (inputLength == C.LENGTH_UNSET) { + dataSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } else { + dataSize = inputLength - firstFrameBytePosition; + durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate); + } + } + + @Override + public boolean isSeekable() { + return dataSize != C.LENGTH_UNSET; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + if (dataSize == C.LENGTH_UNSET) { + return new SeekPoints(new SeekPoint(0, firstFrameBytePosition)); + } + long seekFramePosition = getFramePositionForTimeUs(timeUs); + long seekTimeUs = getTimeUsAtPosition(seekFramePosition); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition); + if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) { + return new SeekPoints(seekPoint); + } else { + long secondSeekPosition = seekFramePosition + frameSize; + long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the stream time in microseconds for a given position. + * + * @param position The stream byte-position. + * @return The stream time in microseconds for the given position. + */ + public long getTimeUsAtPosition(long position) { + return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate); + } + + // Internal methods + + /** + * Returns the stream time in microseconds for a given stream position. + * + * @param position The stream byte-position. + * @param firstFrameBytePosition The position of the first frame in the stream. + * @param bitrate The bitrate (which is assumed to be constant in the stream). + * @return The stream time in microseconds for the given stream position. + */ + private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) { + return Math.max(0, position - firstFrameBytePosition) + * C.BITS_PER_BYTE + * C.MICROS_PER_SECOND + / bitrate; + } + + private long getFramePositionForTimeUs(long timeUs) { + long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE); + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / frameSize) * frameSize; + positionOffset = + Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize); + return firstFrameBytePosition + positionOffset; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java new file mode 100644 index 0000000000..93009f2d5c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; + +/** + * An {@link ExtractorInput} that wraps a {@link DataSource}. + */ +public final class DefaultExtractorInput implements ExtractorInput { + + private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024; + private static final int PEEK_MAX_FREE_SPACE = 512 * 1024; + private static final int SCRATCH_SPACE_SIZE = 4096; + + private final byte[] scratchSpace; + private final DataSource dataSource; + private final long streamLength; + + private long position; + private byte[] peekBuffer; + private int peekBufferPosition; + private int peekBufferLength; + + /** + * @param dataSource The wrapped {@link DataSource}. + * @param position The initial position in the stream. + * @param length The length of the stream, or {@link C#LENGTH_UNSET} if it is unknown. + */ + public DefaultExtractorInput(DataSource dataSource, long position, long length) { + this.dataSource = dataSource; + this.position = position; + this.streamLength = length; + peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; + scratchSpace = new byte[SCRATCH_SPACE_SIZE]; + } + + @Override + public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { + int bytesRead = readFromPeekBuffer(target, offset, length); + if (bytesRead == 0) { + bytesRead = + readFromDataSource( + target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true); + } + commitBytesRead(bytesRead); + return bytesRead; + } + + @Override + public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + int bytesRead = readFromPeekBuffer(target, offset, length); + while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) { + bytesRead = readFromDataSource(target, offset, length, bytesRead, allowEndOfInput); + } + commitBytesRead(bytesRead); + return bytesRead != C.RESULT_END_OF_INPUT; + } + + @Override + public void readFully(byte[] target, int offset, int length) + throws IOException, InterruptedException { + readFully(target, offset, length, false); + } + + @Override + public int skip(int length) throws IOException, InterruptedException { + int bytesSkipped = skipFromPeekBuffer(length); + if (bytesSkipped == 0) { + bytesSkipped = + readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true); + } + commitBytesRead(bytesSkipped); + return bytesSkipped; + } + + @Override + public boolean skipFully(int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + int bytesSkipped = skipFromPeekBuffer(length); + while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) { + int minLength = Math.min(length, bytesSkipped + scratchSpace.length); + bytesSkipped = + readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput); + } + commitBytesRead(bytesSkipped); + return bytesSkipped != C.RESULT_END_OF_INPUT; + } + + @Override + public void skipFully(int length) throws IOException, InterruptedException { + skipFully(length, false); + } + + @Override + public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition; + int bytesPeeked; + if (peekBufferRemainingBytes == 0) { + bytesPeeked = + readFromDataSource( + peekBuffer, + peekBufferPosition, + length, + /* bytesAlreadyRead= */ 0, + /* allowEndOfInput= */ true); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + peekBufferLength += bytesPeeked; + } else { + bytesPeeked = Math.min(length, peekBufferRemainingBytes); + } + System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked); + peekBufferPosition += bytesPeeked; + return bytesPeeked; + } + + @Override + public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + if (!advancePeekPosition(length, allowEndOfInput)) { + return false; + } + System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length); + return true; + } + + @Override + public void peekFully(byte[] target, int offset, int length) + throws IOException, InterruptedException { + peekFully(target, offset, length, false); + } + + @Override + public boolean advancePeekPosition(int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int bytesPeeked = peekBufferLength - peekBufferPosition; + while (bytesPeeked < length) { + bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked, + allowEndOfInput); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return false; + } + peekBufferLength = peekBufferPosition + bytesPeeked; + } + peekBufferPosition += length; + return true; + } + + @Override + public void advancePeekPosition(int length) throws IOException, InterruptedException { + advancePeekPosition(length, false); + } + + @Override + public void resetPeekPosition() { + peekBufferPosition = 0; + } + + @Override + public long getPeekPosition() { + return position + peekBufferPosition; + } + + @Override + public long getPosition() { + return position; + } + + @Override + public long getLength() { + return streamLength; + } + + @Override + public <E extends Throwable> void setRetryPosition(long position, E e) throws E { + Assertions.checkArgument(position >= 0); + this.position = position; + throw e; + } + + /** + * Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the + * current peek position. + */ + private void ensureSpaceForPeek(int length) { + int requiredLength = peekBufferPosition + length; + if (requiredLength > peekBuffer.length) { + int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2, + requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE); + peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity); + } + } + + /** + * Skips from the peek buffer. + * + * @param length The maximum number of bytes to skip from the peek buffer. + * @return The number of bytes skipped. + */ + private int skipFromPeekBuffer(int length) { + int bytesSkipped = Math.min(peekBufferLength, length); + updatePeekBuffer(bytesSkipped); + return bytesSkipped; + } + + /** + * Reads from the peek buffer. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the peek buffer. + * @return The number of bytes read. + */ + private int readFromPeekBuffer(byte[] target, int offset, int length) { + if (peekBufferLength == 0) { + return 0; + } + int peekBytes = Math.min(peekBufferLength, length); + System.arraycopy(peekBuffer, 0, target, offset, peekBytes); + updatePeekBuffer(peekBytes); + return peekBytes; + } + + /** + * Updates the peek buffer's length, position and contents after consuming data. + * + * @param bytesConsumed The number of bytes consumed from the peek buffer. + */ + private void updatePeekBuffer(int bytesConsumed) { + peekBufferLength -= bytesConsumed; + peekBufferPosition = 0; + byte[] newPeekBuffer = peekBuffer; + if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) { + newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; + } + System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength); + peekBuffer = newPeekBuffer; + } + + /** + * Starts or continues a read from the data source. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the input. + * @param bytesAlreadyRead The number of bytes already read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if + * {@code allowEndOfInput} is true and the input has ended having read no bytes. + * @throws EOFException If the end of input was encountered having partially satisfied the read + * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were + * read and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int readFromDataSource(byte[] target, int offset, int length, int bytesAlreadyRead, + boolean allowEndOfInput) throws InterruptedException, IOException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int bytesRead = dataSource.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead); + if (bytesRead == C.RESULT_END_OF_INPUT) { + if (bytesAlreadyRead == 0 && allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + return bytesAlreadyRead + bytesRead; + } + + /** + * Advances the position by the specified number of bytes read. + * + * @param bytesRead The number of bytes read. + */ + private void commitBytesRead(int bytesRead) { + if (bytesRead != C.RESULT_END_OF_INPUT) { + position += bytesRead; + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java new file mode 100644 index 0000000000..8425f89860 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac.FlacExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv.FlvExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg.OggExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac4Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.PsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav.WavExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import java.lang.reflect.Constructor; + +/** + * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: + * + * <ul> + * <li>MP4, including M4A ({@link Mp4Extractor}) + * <li>fMP4 ({@link FragmentedMp4Extractor}) + * <li>Matroska and WebM ({@link MatroskaExtractor}) + * <li>Ogg Vorbis/FLAC ({@link OggExtractor} + * <li>MP3 ({@link Mp3Extractor}) + * <li>AAC ({@link AdtsExtractor}) + * <li>MPEG TS ({@link TsExtractor}) + * <li>MPEG PS ({@link PsExtractor}) + * <li>FLV ({@link FlvExtractor}) + * <li>WAV ({@link WavExtractor}) + * <li>AC3 ({@link Ac3Extractor}) + * <li>AC4 ({@link Ac4Extractor}) + * <li>AMR ({@link AmrExtractor}) + * <li>FLAC + * <ul> + * <li>If available, the FLAC extension extractor is used. + * <li>Otherwise, the core {@link FlacExtractor} is used. Note that Android devices do not + * generally include a FLAC decoder before API 27. This can be worked around by using + * the FLAC extension or the FFmpeg extension. + * </ul> + * </ul> + */ +public final class DefaultExtractorsFactory implements ExtractorsFactory { + + private static final Constructor<? extends Extractor> FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; + + static { + Constructor<? extends Extractor> flacExtensionExtractorConstructor = null; + try { + // LINT.IfChange + @SuppressWarnings("nullness:argument.type.incompatible") + boolean isFlacNativeLibraryAvailable = + Boolean.TRUE.equals( + Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary") + .getMethod("isAvailable") + .invoke(/* obj= */ null)); + if (isFlacNativeLibraryAvailable) { + flacExtensionExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class) + .getConstructor(); + } + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + } catch (ClassNotFoundException e) { + // Expected if the app was built without the FLAC extension. + } catch (Exception e) { + // The FLAC extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating FLAC extension", e); + } + FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor; + } + + private boolean constantBitrateSeekingEnabled; + private @AdtsExtractor.Flags int adtsFlags; + private @AmrExtractor.Flags int amrFlags; + private @MatroskaExtractor.Flags int matroskaFlags; + private @Mp4Extractor.Flags int mp4Flags; + private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; + private @Mp3Extractor.Flags int mp3Flags; + private @TsExtractor.Mode int tsMode; + private @DefaultTsPayloadReaderFactory.Flags int tsFlags; + + public DefaultExtractorsFactory() { + tsMode = TsExtractor.MODE_SINGLE_PMT; + } + + /** + * Convenience method to set whether approximate seeking using constant bitrate assumptions should + * be enabled for all extractors that support it. If set to true, the flags required to enable + * this functionality will be OR'd with those passed to the setters when creating extractor + * instances. If set to false then the flags passed to the setters will be used without + * modification. + * + * @param constantBitrateSeekingEnabled Whether approximate seeking using a constant bitrate + * assumption should be enabled for all extractors that support it. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setConstantBitrateSeekingEnabled( + boolean constantBitrateSeekingEnabled) { + this.constantBitrateSeekingEnabled = constantBitrateSeekingEnabled; + return this; + } + + /** + * Sets flags for {@link AdtsExtractor} instances created by the factory. + * + * @see AdtsExtractor#AdtsExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setAdtsExtractorFlags( + @AdtsExtractor.Flags int flags) { + this.adtsFlags = flags; + return this; + } + + /** + * Sets flags for {@link AmrExtractor} instances created by the factory. + * + * @see AmrExtractor#AmrExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setAmrExtractorFlags(@AmrExtractor.Flags int flags) { + this.amrFlags = flags; + return this; + } + + /** + * Sets flags for {@link MatroskaExtractor} instances created by the factory. + * + * @see MatroskaExtractor#MatroskaExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMatroskaExtractorFlags( + @MatroskaExtractor.Flags int flags) { + this.matroskaFlags = flags; + return this; + } + + /** + * Sets flags for {@link Mp4Extractor} instances created by the factory. + * + * @see Mp4Extractor#Mp4Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMp4ExtractorFlags(@Mp4Extractor.Flags int flags) { + this.mp4Flags = flags; + return this; + } + + /** + * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory. + * + * @see FragmentedMp4Extractor#FragmentedMp4Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setFragmentedMp4ExtractorFlags( + @FragmentedMp4Extractor.Flags int flags) { + this.fragmentedMp4Flags = flags; + return this; + } + + /** + * Sets flags for {@link Mp3Extractor} instances created by the factory. + * + * @see Mp3Extractor#Mp3Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.Flags int flags) { + mp3Flags = flags; + return this; + } + + /** + * Sets the mode for {@link TsExtractor} instances created by the factory. + * + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory) + * @param mode The mode to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) { + tsMode = mode; + return this; + } + + /** + * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances + * created by the factory. + * + * @see TsExtractor#TsExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorFlags( + @DefaultTsPayloadReaderFactory.Flags int flags) { + tsFlags = flags; + return this; + } + + @Override + public synchronized Extractor[] createExtractors() { + Extractor[] extractors = new Extractor[14]; + extractors[0] = new MatroskaExtractor(matroskaFlags); + extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); + extractors[2] = new Mp4Extractor(mp4Flags); + extractors[3] = + new Mp3Extractor( + mp3Flags + | (constantBitrateSeekingEnabled + ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[4] = + new AdtsExtractor( + adtsFlags + | (constantBitrateSeekingEnabled + ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[5] = new Ac3Extractor(); + extractors[6] = new TsExtractor(tsMode, tsFlags); + extractors[7] = new FlvExtractor(); + extractors[8] = new OggExtractor(); + extractors[9] = new PsExtractor(); + extractors[10] = new WavExtractor(); + extractors[11] = + new AmrExtractor( + amrFlags + | (constantBitrateSeekingEnabled + ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[12] = new Ac4Extractor(); + if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) { + try { + extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance(); + } catch (Exception e) { + // Should never happen. + throw new IllegalStateException("Unexpected error creating FLAC extractor", e); + } + } else { + extractors[13] = new FlacExtractor(); + } + return extractors; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java new file mode 100644 index 0000000000..06c90ae874 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +/** A dummy {@link ExtractorOutput} implementation. */ +public final class DummyExtractorOutput implements ExtractorOutput { + + @Override + public TrackOutput track(int id, int type) { + return new DummyTrackOutput(); + } + + @Override + public void endTracks() { + // Do nothing. + } + + @Override + public void seekMap(SeekMap seekMap) { + // Do nothing. + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java new file mode 100644 index 0000000000..6df947731d --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; + +/** + * A dummy {@link TrackOutput} implementation. + */ +public final class DummyTrackOutput implements TrackOutput { + + @Override + public void format(Format format) { + // Do nothing. + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + int bytesSkipped = input.skip(length); + if (bytesSkipped == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + return bytesSkipped; + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + data.skipBytes(length); + } + + @Override + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { + // Do nothing. + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java new file mode 100644 index 0000000000..aeb7028c3f --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Extracts media data from a container format. + */ +public interface Extractor { + + /** + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed + * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data + * continuing from the position in the stream reached by the returning call. + */ + int RESULT_CONTINUE = 0; + /** + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed + * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting + * from a specified position in the stream. + */ + int RESULT_SEEK = 1; + /** + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the end of the + * {@link ExtractorInput} was reached. Equal to {@link C#RESULT_END_OF_INPUT}. + */ + int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT; + + /** + * Result values that can be returned by {@link #read(ExtractorInput, PositionHolder)}. One of + * {@link #RESULT_CONTINUE}, {@link #RESULT_SEEK} or {@link #RESULT_END_OF_INPUT}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {RESULT_CONTINUE, RESULT_SEEK, RESULT_END_OF_INPUT}) + @interface ReadResult {} + + /** + * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must + * provide data from the start of the stream. + * <p> + * If {@code true} is returned, the {@code input}'s reading position may have been modified. + * Otherwise, only its peek position may have been modified. + * + * @param input The {@link ExtractorInput} from which data should be peeked/read. + * @return Whether this extractor can read the provided input. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + boolean sniff(ExtractorInput input) throws IOException, InterruptedException; + + /** + * Initializes the extractor with an {@link ExtractorOutput}. Called at most once. + * + * @param output An {@link ExtractorOutput} to receive extracted data. + */ + void init(ExtractorOutput output); + + /** + * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link + * #init(ExtractorOutput)}. + * + * <p>A single call to this method will block until some progress has been made, but will not + * block for longer than this. Hence each call will consume only a small amount of input data. + * + * <p>In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the {@link + * ExtractorInput} passed to the next read is required to provide data continuing from the + * position in the stream reached by the returning call. If the extractor requires data to be + * provided from a different position, then that position is set in {@code seekPosition} and + * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the + * {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned. + * + * <p>When this method throws an {@link IOException} or an {@link InterruptedException}, + * extraction may continue by providing an {@link ExtractorInput} with an unchanged {@link + * ExtractorInput#getPosition() read position} to a subsequent call to this method. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the + * position of the required data. + * @return One of the {@code RESULT_} values defined in this interface. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + @ReadResult + int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException; + + /** + * Notifies the extractor that a seek has occurred. + * <p> + * Following a call to this method, the {@link ExtractorInput} passed to the next invocation of + * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from {@code + * position} in the stream. Valid random access positions are the start of the stream and + * positions that can be obtained from any {@link SeekMap} passed to the {@link ExtractorOutput}. + * + * @param position The byte offset in the stream from which data will be provided. + * @param timeUs The seek time in microseconds. + */ + void seek(long position, long timeUs); + + /** + * Releases all kept resources. + */ + void release(); + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java new file mode 100644 index 0000000000..351df1e79e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Provides data to be consumed by an {@link Extractor}. + * + * <p>This interface provides two modes of accessing the underlying input. See the subheadings below + * for more info about each mode. + * + * <ul> + * <li>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. + * <li>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + * </ul> + * + * <h3>{@link InputStream}-like methods</h3> + * + * <p>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. The {@code length} parameter is a maximum, and each method returns + * the number of bytes actually processed. This may be less than {@code length} because the end of + * the input was reached, or the method was interrupted, or the operation was aborted early for + * another reason. + * + * <h3>Block-based methods</h3> + * + * <p>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + * + * <p>These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This + * parameter is intended to be set to true when the caller believes the input might be fully + * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final + * block/frame/header). It's <b>not</b> intended to allow a partial read (i.e. greater than 0 bytes, + * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from + * these methods (a partial read is assumed to indicate a malformed block/frame/header - and + * therefore a malformed file). + * + * <p>The expected behaviour of the block-based methods is therefore: + * + * <ul> + * <li>Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}. + * <li>Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}. + * <li>Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException} + * (regardless of {@code allowEndOfInput}). + * </ul> + */ +public interface ExtractorInput { + + /** + * Reads up to {@code length} bytes from the input and resets the peek position. + * <p> + * This method blocks until at least one byte of data can be read, the end of the input is + * detected, or an exception is thrown. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the input. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int read(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@code false} being returned. False if it should be + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the read was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having read no data. + * @throws EOFException If the end of input was encountered having partially satisfied the read + * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were + * read and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length, + * false)}. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to read from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read. + * + * @param length The maximum number of bytes to skip from the input. + * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int skip(int length) throws IOException, InterruptedException; + + /** + * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read. + * + * @param length The number of bytes to skip from the input. + * @param allowEndOfInput True if encountering the end of the input having skipped no data is + * allowed, and should result in {@code false} being returned. False if it should be + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the skip was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having skipped no data. + * @throws EOFException If the end of input was encountered having partially satisfied the skip + * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were + * skipped and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException; + + /** + * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read. + * <p> + * Encountering the end of input is always considered an error, and will result in an + * {@link EOFException} being thrown. + * + * @param length The number of bytes to skip from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void skipFully(int length) throws IOException, InterruptedException; + + /** + * Peeks up to {@code length} bytes from the peek position. The current read position is left + * unchanged. + * + * <p>This method blocks until at least one byte of data can be peeked, the end of the input is + * detected, or an exception is thrown. + * + * <p>Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * position, so the caller can peek the same data again. Reading or skipping also resets the peek + * position. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to peek from the input. + * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int peek(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to peek from the input. + * @param allowEndOfInput True if encountering the end of the input having peeked no data is + * allowed, and should result in {@code false} being returned. False if it should be + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the peek was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having peeked no data. + * @throws EOFException If the end of input was encountered having partially satisfied the peek + * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were + * peeked and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length, + * false)}. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to peek from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int, + * boolean)} except the data is skipped instead of read. + * + * @param length The number of bytes by which to advance the peek position. + * @param allowEndOfInput True if encountering the end of the input before advancing is allowed, + * and should result in {@code false} being returned. False if it should be considered an + * error, causing an {@link EOFException} to be thrown. See note in class Javadoc. + * @return True if advancing the peek position was successful. False if {@code + * allowEndOfInput=true} and the end of the input was encountered before advancing over any + * data. + * @throws EOFException If the end of input was encountered having partially advanced (i.e. having + * advanced by at least one byte, but fewer than {@code length}), or if the end of input was + * encountered before advancing and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs advancing the peek position. + * @throws InterruptedException If the thread is interrupted. + */ + boolean advancePeekPosition(int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)} + * except the data is skipped instead of read. + * + * @param length The number of bytes to peek from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void advancePeekPosition(int length) throws IOException, InterruptedException; + + /** + * Resets the peek position to equal the current read position. + */ + void resetPeekPosition(); + + /** + * Returns the current peek position (byte offset) in the stream. + * + * @return The peek position (byte offset) in the stream. + */ + long getPeekPosition(); + + /** + * Returns the current read position (byte offset) in the stream. + * + * @return The read position (byte offset) in the stream. + */ + long getPosition(); + + /** + * Returns the length of the source stream, or {@link C#LENGTH_UNSET} if it is unknown. + * + * @return The length of the source stream, or {@link C#LENGTH_UNSET}. + */ + long getLength(); + + /** + * Called when reading fails and the required retry position is different from the last position. + * After setting the retry position it throws the given {@link Throwable}. + * + * @param <E> Type of {@link Throwable} to be thrown. + * @param position The required retry position. + * @param e {@link Throwable} to be thrown. + * @throws E The given {@link Throwable} object. + */ + <E extends Throwable> void setRetryPosition(long position, E e) throws E; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java new file mode 100644 index 0000000000..8708758265 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +/** + * Receives stream level data extracted by an {@link Extractor}. + */ +public interface ExtractorOutput { + + /** + * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. + * <p> + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. + * + * @param id A track identifier. + * @param type The type of the track. Typically one of the {@link org.mozilla.thirdparty.com.google.android.exoplayer2C} + * {@code TRACK_TYPE_*} constants. + * @return The {@link TrackOutput} for the given track identifier. + */ + TrackOutput track(int id, int type); + + /** + * Called when all tracks have been identified, meaning no new {@code trackId} values will be + * passed to {@link #track(int, int)}. + */ + void endTracks(); + + /** + * Called when a {@link SeekMap} has been extracted from the stream. + * + * @param seekMap The extracted {@link SeekMap}. + */ + void seekMap(SeekMap seekMap); + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java new file mode 100644 index 0000000000..6951f7e311 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.io.IOException; + +/** Extractor related utility methods. */ +/* package */ final class ExtractorUtil { + + /** + * Peeks {@code length} bytes from the input peek position, or all the bytes to the end of the + * input if there was less than {@code length} bytes left. + * + * <p>If an exception is thrown, there is no guarantee on the peek position. + * + * @param input The stream input to peek the data from. + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to peek from the input. + * @return The number of bytes peeked. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static int peekToLength(ExtractorInput input, byte[] target, int offset, int length) + throws IOException, InterruptedException { + int totalBytesPeeked = 0; + while (totalBytesPeeked < length) { + int bytesPeeked = input.peek(target, offset + totalBytesPeeked, length - totalBytesPeeked); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + break; + } + totalBytesPeeked += bytesPeeked; + } + return totalBytesPeeked; + } + + private ExtractorUtil() {} +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java new file mode 100644 index 0000000000..64b803f65e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +/** Factory for arrays of {@link Extractor} instances. */ +public interface ExtractorsFactory { + + /** Returns an array of new {@link Extractor} instances. */ + Extractor[] createExtractors(); +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java new file mode 100644 index 0000000000..e8d2b4928b --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * Reads and peeks FLAC frame elements according to the <a + * href="https://xiph.org/flac/format.html">FLAC format specification</a>. + */ +public final class FlacFrameReader { + + /** Holds a sample number. */ + public static final class SampleNumberHolder { + /** The sample number. */ + public long sampleNumber; + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame + * first sample number in {@code sampleNumberHolder}. + * + * <p>If the header is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkAndReadFrameHeader( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) { + int frameStartPosition = data.getPosition(); + + long frameHeaderBytes = data.readUnsignedInt(); + if (frameHeaderBytes >>> 16 != frameStartMarker) { + return false; + } + + boolean isBlockSizeVariable = (frameHeaderBytes >>> 16 & 1) == 1; + int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF); + int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF); + int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF); + int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7); + boolean reservedBit = (frameHeaderBytes & 1) == 1; + return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata) + && checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata) + && !reservedBit + && checkAndReadFirstSampleNumber( + data, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder) + && checkAndReadBlockSizeSamples(data, flacStreamMetadata, blockSizeKey) + && checkAndReadSampleRate(data, flacStreamMetadata, sampleRateKey) + && checkAndReadCrc(data, frameStartPosition); + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, writes the frame first sample + * number in {@code sampleNumberHolder}. + * + * <p>The {@code input} peek position is left unchanged. + * + * @param input The input to get the data from, whose peek position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkFrameHeaderFromPeek( + ExtractorInput input, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) + throws IOException, InterruptedException { + long originalPeekPosition = input.getPeekPosition(); + + byte[] frameStartBytes = new byte[2]; + input.peekFully(frameStartBytes, 0, 2); + int frameStart = (frameStartBytes[0] & 0xFF) << 8 | (frameStartBytes[1] & 0xFF); + if (frameStart != frameStartMarker) { + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + return false; + } + + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + System.arraycopy( + frameStartBytes, /* srcPos= */ 0, scratch.data, /* destPos= */ 0, /* length= */ 2); + + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 2, FlacConstants.MAX_FRAME_HEADER_SIZE - 2); + scratch.setLimit(totalBytesPeeked); + + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + + return checkAndReadFrameHeader( + scratch, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } + + /** + * Returns the number of the first sample in the given frame. + * + * <p>The read position of {@code input} is left unchanged. + * + * <p>If no exception is thrown, the peek position is aligned with the read position. Otherwise, + * there is no guarantee on the peek position. + * + * @param input Input stream to get the sample number from (starting from the read position). + * @return The frame first sample number. + * @throws ParserException If an error occurs parsing the sample number. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + */ + public static long getFirstSampleNumber( + ExtractorInput input, FlacStreamMetadata flacStreamMetadata) + throws IOException, InterruptedException { + input.resetPeekPosition(); + input.advancePeekPosition(1); + byte[] blockingStrategyByte = new byte[1]; + input.peekFully(blockingStrategyByte, 0, 1); + boolean isBlockSizeVariable = (blockingStrategyByte[0] & 1) == 1; + input.advancePeekPosition(2); + + int maxUtf8SampleNumberSize = isBlockSizeVariable ? 7 : 6; + ParsableByteArray scratch = new ParsableByteArray(maxUtf8SampleNumberSize); + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 0, maxUtf8SampleNumberSize); + scratch.setLimit(totalBytesPeeked); + input.resetPeekPosition(); + + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + if (!checkAndReadFirstSampleNumber( + scratch, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder)) { + throw new ParserException(); + } + + return sampleNumberHolder.sampleNumber; + } + + /** + * Reads the given block size. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param blockSizeKey The key in the block size lookup table. + * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid. + */ + public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) { + switch (blockSizeKey) { + case 1: + return 192; + case 2: + case 3: + case 4: + case 5: + return 576 << (blockSizeKey - 2); + case 6: + return data.readUnsignedByte() + 1; + case 7: + return data.readUnsignedShort() + 1; + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return 256 << (blockSizeKey - 8); + default: + return -1; + } + } + + /** + * Checks whether the given channel assignment is valid. + * + * @param channelAssignmentKey The channel assignment lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the channel assignment is valid. + */ + private static boolean checkChannelAssignment( + int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) { + if (channelAssignmentKey <= 7) { + return channelAssignmentKey == flacStreamMetadata.channels - 1; + } else if (channelAssignmentKey <= 10) { + return flacStreamMetadata.channels == 2; + } else { + return false; + } + } + + /** + * Checks whether the given number of bits per sample is valid. + * + * @param bitsPerSampleKey The bits per sample lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the number of bits per sample is valid. + */ + private static boolean checkBitsPerSample( + int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) { + if (bitsPerSampleKey == 0) { + return true; + } + return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey; + } + + /** + * Checks whether the given sample number is valid and, if so, reads it and writes it in {@code + * sampleNumberHolder}. + * + * <p>If the sample number is valid, the position of {@code data} is moved to the byte following + * it. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the sample + * number data. + * @param flacStreamMetadata The stream metadata. + * @param isBlockSizeVariable Whether the stream blocking strategy is variable block size or fixed + * block size. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the sample number is valid. + */ + private static boolean checkAndReadFirstSampleNumber( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + boolean isBlockSizeVariable, + SampleNumberHolder sampleNumberHolder) { + long utf8Value; + try { + utf8Value = data.readUtf8EncodedLong(); + } catch (NumberFormatException e) { + return false; + } + + sampleNumberHolder.sampleNumber = + isBlockSizeVariable ? utf8Value : utf8Value * flacStreamMetadata.maxBlockSizeSamples; + return true; + } + + /** + * Checks whether the given frame block size key and block size bits are valid and, if so, reads + * the block size bits. + * + * <p>If the block size is valid, the position of {@code data} is moved to the byte following the + * block size bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param flacStreamMetadata The stream metadata. + * @param blockSizeKey The key in the block size lookup table. + * @return Whether the block size is valid. + */ + private static boolean checkAndReadBlockSizeSamples( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int blockSizeKey) { + int blockSizeSamples = readFrameBlockSizeSamplesFromKey(data, blockSizeKey); + return blockSizeSamples != -1 && blockSizeSamples <= flacStreamMetadata.maxBlockSizeSamples; + } + + /** + * Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the + * sample rate bits. + * + * <p>If the sample rate is valid, the position of {@code data} is moved to the byte following the + * sample rate bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must indicate the sample rate bits. + * @param flacStreamMetadata The stream metadata. + * @param sampleRateKey The key in the sample rate lookup table. + * @return Whether the sample rate is valid. + */ + private static boolean checkAndReadSampleRate( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) { + int expectedSampleRate = flacStreamMetadata.sampleRate; + if (sampleRateKey == 0) { + return true; + } else if (sampleRateKey <= 11) { + return sampleRateKey == flacStreamMetadata.sampleRateLookupKey; + } else if (sampleRateKey == 12) { + return data.readUnsignedByte() * 1000 == expectedSampleRate; + } else if (sampleRateKey <= 14) { + int sampleRate = data.readUnsignedShort(); + if (sampleRateKey == 14) { + sampleRate *= 10; + } + return sampleRate == expectedSampleRate; + } else { + return false; + } + } + + /** + * Checks whether the given CRC is valid and, if so, reads it. + * + * <p>If the CRC is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + * <p>The {@code data} array must contain the whole frame header. + * + * @param data The array to read the data from, whose position must indicate the CRC. + * @param frameStartPosition The frame start offset in {@code data}. + * @return Whether the CRC is valid. + */ + private static boolean checkAndReadCrc(ParsableByteArray data, int frameStartPosition) { + int crc = data.readUnsignedByte(); + int frameEndPosition = data.getPosition(); + int expectedCrc = + Util.crc8(data.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0); + return crc == expectedCrc; + } + + private FlacFrameReader() {} +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java new file mode 100644 index 0000000000..c5413cf459 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Reads and peeks FLAC stream metadata elements according to the <a + * href="https://xiph.org/flac/format.html">FLAC format specification</a>. + */ +public final class FlacMetadataReader { + + /** Holds a {@link FlacStreamMetadata}. */ + public static final class FlacStreamMetadataHolder { + /** The FLAC stream metadata. */ + @Nullable public FlacStreamMetadata flacStreamMetadata; + + public FlacStreamMetadataHolder(@Nullable FlacStreamMetadata flacStreamMetadata) { + this.flacStreamMetadata = flacStreamMetadata; + } + } + + private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC" + private static final int SYNC_CODE = 0x3FFE; + private static final int SEEK_POINT_SIZE = 18; + + /** + * Peeks ID3 Data. + * + * @param input Input stream to peek the ID3 data from. + * @param parseData Whether to parse the ID3 frames. + * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData} + * is {@code false}. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the + * peek position. + * @throws InterruptedException If interrupted while peeking from input. In this case, there is no + * guarantee on the peek position. + */ + @Nullable + public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData) + throws IOException, InterruptedException { + @Nullable + Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE; + @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate); + return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata; + } + + /** + * Peeks the FLAC stream marker. + * + * @param input Input stream to peek the stream marker from. + * @return Whether the data peeked is the FLAC stream marker. + * @throws IOException If peeking from the input fails. In this case, the peek position is left + * unchanged. + * @throws InterruptedException If interrupted while peeking from input. In this case, the peek + * position is left unchanged. + */ + public static boolean checkAndPeekStreamMarker(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); + input.peekFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); + return scratch.readUnsignedInt() == STREAM_MARKER; + } + + /** + * Reads ID3 Data. + * + * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the ID3 data from. + * @param parseData Whether to parse the ID3 frames. + * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData} + * is {@code false}. + * @throws IOException If reading from the input fails. In this case, the read position is left + * unchanged and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position is left unchanged and there is no guarantee on the peek position. + */ + @Nullable + public static Metadata readId3Metadata(ExtractorInput input, boolean parseData) + throws IOException, InterruptedException { + input.resetPeekPosition(); + long startingPeekPosition = input.getPeekPosition(); + @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData); + int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition); + input.skipFully(peekedId3Bytes); + return id3Metadata; + } + + /** + * Reads the FLAC stream marker. + * + * @param input Input stream to read the stream marker from. + * @throws ParserException If an error occurs parsing the stream marker. In this case, the + * position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes. + * @throws IOException If reading from the input fails. In this case, the position is left + * unchanged. + * @throws InterruptedException If interrupted while reading from input. In this case, the + * position is left unchanged. + */ + public static void readStreamMarker(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); + input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); + if (scratch.readUnsignedInt() != STREAM_MARKER) { + throw new ParserException("Failed to read FLAC stream marker."); + } + } + + /** + * Reads one FLAC metadata block. + * + * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the metadata block from (header included). + * @param metadataHolder A holder for the metadata read. If the stream info block (which must be + * the first metadata block) is read, the holder contains a new instance representing the + * stream info data. If the block read is a Vorbis comment block or a picture block, the + * holder contains a copy of the existing stream metadata with the corresponding metadata + * added. Otherwise, the metadata in the holder is unchanged. + * @return Whether the block read is the last metadata block. + * @throws IllegalArgumentException If the block read is not a stream info block and the metadata + * in {@code metadataHolder} is {@code null}. In this case, the read position will be at the + * start of a metadata block and there is no guarantee on the peek position. + * @throws IOException If reading from the input fails. In this case, the read position will be at + * the start of a metadata block and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position will be at the start of a metadata block and there is no guarantee on the peek + * position. + */ + public static boolean readMetadataBlock( + ExtractorInput input, FlacStreamMetadataHolder metadataHolder) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableBitArray scratch = new ParsableBitArray(new byte[4]); + input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + boolean isLastMetadataBlock = scratch.readBit(); + int type = scratch.readBits(7); + int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24); + if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) { + metadataHolder.flacStreamMetadata = readStreamInfoBlock(input); + } else { + FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata; + if (flacStreamMetadata == null) { + throw new IllegalArgumentException(); + } + if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) { + FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable); + } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) { + List<String> vorbisComments = readVorbisCommentMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = + flacStreamMetadata.copyWithVorbisComments(vorbisComments); + } else if (type == FlacConstants.METADATA_TYPE_PICTURE) { + PictureFrame pictureFrame = readPictureMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = + flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame)); + } else { + input.skipFully(length); + } + } + + return isLastMetadataBlock; + } + + /** + * Reads a FLAC seek table metadata block. + * + * <p>The position of {@code data} is moved to the byte following the seek table metadata block + * (placeholder points included). + * + * @param data The array to read the data from, whose position must correspond to the seek table + * metadata block (header included). + * @return The seek table, without the placeholder points. + */ + public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) { + data.skipBytes(1); + int length = data.readUnsignedInt24(); + + long seekTableEndPosition = data.getPosition() + length; + int seekPointCount = length / SEEK_POINT_SIZE; + long[] pointSampleNumbers = new long[seekPointCount]; + long[] pointOffsets = new long[seekPointCount]; + for (int i = 0; i < seekPointCount; i++) { + // The sample number is expected to fit in a signed long, except if it is a placeholder, in + // which case its value is -1. + long sampleNumber = data.readLong(); + if (sampleNumber == -1) { + pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i); + pointOffsets = Arrays.copyOf(pointOffsets, i); + break; + } + pointSampleNumbers[i] = sampleNumber; + pointOffsets[i] = data.readLong(); + data.skipBytes(2); + } + + data.skipBytes((int) (seekTableEndPosition - data.getPosition())); + return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets); + } + + /** + * Returns the frame start marker, consisting of the 2 first bytes of the first frame. + * + * <p>The read position of {@code input} is left unchanged and the peek position is aligned with + * the read position. + * + * @param input Input stream to get the start marker from (starting from the read position). + * @return The frame start marker (which must be the same for all the frames in the stream). + * @throws ParserException If an error occurs parsing the frame start marker. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + */ + public static int getFrameStartMarker(ExtractorInput input) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableByteArray scratch = new ParsableByteArray(2); + input.peekFully(scratch.data, 0, 2); + + int frameStartMarker = scratch.readUnsignedShort(); + int syncCode = frameStartMarker >> 2; + if (syncCode != SYNC_CODE) { + input.resetPeekPosition(); + throw new ParserException("First frame does not start with sync code."); + } + + input.resetPeekPosition(); + return frameStartMarker; + } + + private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input) + throws IOException, InterruptedException { + byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE]; + input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE); + return new FlacStreamMetadata( + scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE); + } + + private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock( + ExtractorInput input, int length) throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + return readSeekTableMetadataBlock(scratch); + } + + private static List<String> readVorbisCommentMetadataBlock(ExtractorInput input, int length) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); + CommentHeader commentHeader = + VorbisUtil.readVorbisCommentHeader( + scratch, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false); + return Arrays.asList(commentHeader.comments); + } + + private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + int pictureType = scratch.readInt(); + int mimeTypeLength = scratch.readInt(); + String mimeType = scratch.readString(mimeTypeLength, Charset.forName(C.ASCII_NAME)); + int descriptionLength = scratch.readInt(); + String description = scratch.readString(descriptionLength); + int width = scratch.readInt(); + int height = scratch.readInt(); + int depth = scratch.readInt(); + int colors = scratch.readInt(); + int pictureDataLength = scratch.readInt(); + byte[] pictureData = new byte[pictureDataLength]; + scratch.readBytes(pictureData, 0, pictureDataLength); + + return new PictureFrame( + pictureType, mimeType, description, width, height, depth, colors, pictureData); + } + + private FlacMetadataReader() {} +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java new file mode 100644 index 0000000000..56d54596ac --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * A {@link SeekMap} implementation for FLAC streams that contain a <a + * href="https://xiph.org/flac/format.html#metadata_block_seektable">seek table</a>. + */ +public final class FlacSeekTableSeekMap implements SeekMap { + + private final FlacStreamMetadata flacStreamMetadata; + private final long firstFrameOffset; + + /** + * Creates a seek map from the FLAC stream seek table. + * + * @param flacStreamMetadata The stream metadata. + * @param firstFrameOffset The byte offset of the first frame in the stream. + */ + public FlacSeekTableSeekMap(FlacStreamMetadata flacStreamMetadata, long firstFrameOffset) { + this.flacStreamMetadata = flacStreamMetadata; + this.firstFrameOffset = firstFrameOffset; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return flacStreamMetadata.getDurationUs(); + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + Assertions.checkNotNull(flacStreamMetadata.seekTable); + long[] pointSampleNumbers = flacStreamMetadata.seekTable.pointSampleNumbers; + long[] pointOffsets = flacStreamMetadata.seekTable.pointOffsets; + + long targetSampleNumber = flacStreamMetadata.getSampleNumber(timeUs); + int index = + Util.binarySearchFloor( + pointSampleNumbers, + targetSampleNumber, + /* inclusive= */ true, + /* stayInBounds= */ false); + + long seekPointSampleNumber = index == -1 ? 0 : pointSampleNumbers[index]; + long seekPointOffsetFromFirstFrame = index == -1 ? 0 : pointOffsets[index]; + SeekPoint seekPoint = getSeekPoint(seekPointSampleNumber, seekPointOffsetFromFirstFrame); + if (seekPoint.timeUs == timeUs || index == pointSampleNumbers.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint secondSeekPoint = + getSeekPoint(pointSampleNumbers[index + 1], pointOffsets[index + 1]); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private SeekPoint getSeekPoint(long sampleNumber, long offsetFromFirstFrame) { + long seekTimeUs = sampleNumber * C.MICROS_PER_SECOND / flacStreamMetadata.sampleRate; + long seekPosition = firstFrameOffset + offsetFromFirstFrame; + return new SeekPoint(seekTimeUs, seekPosition); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java new file mode 100644 index 0000000000..11893d6136 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.CommentFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.InternalFrame; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Holder for gapless playback information. + */ +public final class GaplessInfoHolder { + + private static final String GAPLESS_DOMAIN = "com.apple.iTunes"; + private static final String GAPLESS_DESCRIPTION = "iTunSMPB"; + private static final Pattern GAPLESS_COMMENT_PATTERN = + Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); + + /** + * The number of samples to trim from the start of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderDelay; + + /** + * The number of samples to trim from the end of the decoded audio stream, or + * {@link Format#NO_VALUE} if not set. + */ + public int encoderPadding; + + /** + * Creates a new holder for gapless playback information. + */ + public GaplessInfoHolder() { + encoderDelay = Format.NO_VALUE; + encoderPadding = Format.NO_VALUE; + } + + /** + * Populates the holder with data from an MP3 Xing header, if valid and non-zero. + * + * @param value The 24-bit value to decode. + * @return Whether the holder was populated. + */ + public boolean setFromXingHeaderValue(int value) { + int encoderDelay = value >> 12; + int encoderPadding = value & 0x0FFF; + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + return false; + } + + /** + * Populates the holder with data parsed from ID3 {@link Metadata}. + * + * @param metadata The metadata from which to parse the gapless information. + * @return Whether the holder was populated. + */ + public boolean setFromMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof CommentFrame) { + CommentFrame commentFrame = (CommentFrame) entry; + if (GAPLESS_DESCRIPTION.equals(commentFrame.description) + && setFromComment(commentFrame.text)) { + return true; + } + } else if (entry instanceof InternalFrame) { + InternalFrame internalFrame = (InternalFrame) entry; + if (GAPLESS_DOMAIN.equals(internalFrame.domain) + && GAPLESS_DESCRIPTION.equals(internalFrame.description) + && setFromComment(internalFrame.text)) { + return true; + } + } + } + return false; + } + + /** + * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header + * or MPEG 4 user data), if valid and non-zero. + * + * @param data The comment's payload data. + * @return Whether the holder was populated. + */ + private boolean setFromComment(String data) { + Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); + if (matcher.find()) { + try { + int encoderDelay = Integer.parseInt(matcher.group(1), 16); + int encoderPadding = Integer.parseInt(matcher.group(2), 16); + if (encoderDelay > 0 || encoderPadding > 0) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + return true; + } + } catch (NumberFormatException e) { + // Ignore incorrectly formatted comments. + } + } + return false; + } + + /** + * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set. + */ + public boolean hasGaplessInfo() { + return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java new file mode 100644 index 0000000000..a0a26c76d8 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; + +/** + * Peeks data from the beginning of an {@link ExtractorInput} to determine if there is any ID3 tag. + */ +public final class Id3Peeker { + + private final ParsableByteArray scratch; + + public Id3Peeker() { + scratch = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + } + + /** + * Peeks ID3 data from the input and parses the first ID3 tag. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @param id3FramePredicate Determines which ID3 frames are decoded. May be null to decode all + * frames. + * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not + * present in the input. + * @throws IOException If an error occurred peeking from the input. + * @throws InterruptedException If the thread was interrupted. + */ + @Nullable + public Metadata peekId3Data( + ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate) + throws IOException, InterruptedException { + int peekedId3Bytes = 0; + Metadata metadata = null; + while (true) { + try { + input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH); + } catch (EOFException e) { + // If input has less than ID3_HEADER_LENGTH, ignore the rest. + break; + } + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { + // Not an ID3 tag. + break; + } + scratch.skipBytes(3); // Skip major version, minor version and flags. + int framesLength = scratch.readSynchSafeInt(); + int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; + + if (metadata == null) { + byte[] id3Data = new byte[tagLength]; + System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); + input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); + + metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); + } else { + input.advancePeekPosition(framesLength); + } + + peekedId3Bytes += tagLength; + } + + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes); + return metadata; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java new file mode 100644 index 0000000000..66c3411094 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; + +/** + * An MPEG audio frame header. + */ +public final class MpegAudioHeader { + + /** + * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2 + * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame * + * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame. + * The next power of two size is 4 KiB. + */ + public static final int MAX_FRAME_SIZE_BYTES = 4096; + + private static final String[] MIME_TYPE_BY_LAYER = + new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG}; + private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000}; + private static final int[] BITRATE_V1_L1 = { + 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, + 416000, 448000 + }; + private static final int[] BITRATE_V2_L1 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, + 224000, 256000 + }; + private static final int[] BITRATE_V1_L2 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000, 384000 + }; + private static final int[] BITRATE_V1_L3 = { + 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000 + }; + private static final int[] BITRATE_V2 = { + 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, + 160000 + }; + + private static final int SAMPLES_PER_FRAME_L1 = 384; + private static final int SAMPLES_PER_FRAME_L2 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V1 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V2 = 576; + + /** + * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it + * is invalid. + */ + public static int getFrameSize(int header) { + if (!isMagicPresent(header)) { + return C.LENGTH_UNSET; + } + + int version = (header >>> 19) & 3; + if (version == 1) { + return C.LENGTH_UNSET; + } + + int layer = (header >>> 17) & 3; + if (layer == 0) { + return C.LENGTH_UNSET; + } + + int bitrateIndex = (header >>> 12) & 15; + if (bitrateIndex == 0 || bitrateIndex == 0xF) { + // Disallow "free" bitrate. + return C.LENGTH_UNSET; + } + + int samplingRateIndex = (header >>> 10) & 3; + if (samplingRateIndex == 3) { + return C.LENGTH_UNSET; + } + + int samplingRate = SAMPLING_RATE_V1[samplingRateIndex]; + if (version == 2) { + // Version 2 + samplingRate /= 2; + } else if (version == 0) { + // Version 2.5 + samplingRate /= 4; + } + + int bitrate; + int padding = (header >>> 9) & 1; + if (layer == 3) { + // Layer I (layer == 3) + bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; + return (12 * bitrate / samplingRate + padding) * 4; + } else { + // Layer II (layer == 2) or III (layer == 1) + if (version == 3) { + bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; + } else { + // Version 2 or 2.5. + bitrate = BITRATE_V2[bitrateIndex - 1]; + } + } + + if (version == 3) { + // Version 1 + return 144 * bitrate / samplingRate + padding; + } else { + // Version 2 or 2.5 + return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding; + } + } + + /** + * Returns the number of samples per frame associated with {@code header}, or {@link + * C#LENGTH_UNSET} if it is invalid. + */ + public static int getFrameSampleCount(int header) { + + if (!isMagicPresent(header)) { + return C.LENGTH_UNSET; + } + + int version = (header >>> 19) & 3; + if (version == 1) { + return C.LENGTH_UNSET; + } + + int layer = (header >>> 17) & 3; + if (layer == 0) { + return C.LENGTH_UNSET; + } + + // Those header values are not used but are checked for consistency with the other methods + int bitrateIndex = (header >>> 12) & 15; + int samplingRateIndex = (header >>> 10) & 3; + if (bitrateIndex == 0 || bitrateIndex == 0xF || samplingRateIndex == 3) { + return C.LENGTH_UNSET; + } + + return getFrameSizeInSamples(version, layer); + } + + /** + * Parses {@code headerData}, populating {@code header} with the parsed data. + * + * @param headerData Header data to parse. + * @param header Header to populate with data from {@code headerData}. + * @return True if the header was populated. False otherwise, indicating that {@code headerData} + * is not a valid MPEG audio header. + */ + public static boolean populateHeader(int headerData, MpegAudioHeader header) { + if (!isMagicPresent(headerData)) { + return false; + } + + int version = (headerData >>> 19) & 3; + if (version == 1) { + return false; + } + + int layer = (headerData >>> 17) & 3; + if (layer == 0) { + return false; + } + + int bitrateIndex = (headerData >>> 12) & 15; + if (bitrateIndex == 0 || bitrateIndex == 0xF) { + // Disallow "free" bitrate. + return false; + } + + int samplingRateIndex = (headerData >>> 10) & 3; + if (samplingRateIndex == 3) { + return false; + } + + int sampleRate = SAMPLING_RATE_V1[samplingRateIndex]; + if (version == 2) { + // Version 2 + sampleRate /= 2; + } else if (version == 0) { + // Version 2.5 + sampleRate /= 4; + } + + int padding = (headerData >>> 9) & 1; + int bitrate; + int frameSize; + int samplesPerFrame = getFrameSizeInSamples(version, layer); + if (layer == 3) { + // Layer I (layer == 3) + bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; + frameSize = (12 * bitrate / sampleRate + padding) * 4; + } else { + // Layer II (layer == 2) or III (layer == 1) + if (version == 3) { + // Version 1 + bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; + frameSize = 144 * bitrate / sampleRate + padding; + } else { + // Version 2 or 2.5. + bitrate = BITRATE_V2[bitrateIndex - 1]; + frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; + } + } + + String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; + int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; + header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); + return true; + } + + private static boolean isMagicPresent(int header) { + return (header & 0xFFE00000) == 0xFFE00000; + } + + private static int getFrameSizeInSamples(int version, int layer) { + switch (layer) { + case 1: + return version == 3 ? SAMPLES_PER_FRAME_L3_V1 : SAMPLES_PER_FRAME_L3_V2; // Layer III + case 2: + return SAMPLES_PER_FRAME_L2; // Layer II + case 3: + return SAMPLES_PER_FRAME_L1; // Layer I + } + throw new IllegalArgumentException(); + } + + /** MPEG audio header version. */ + public int version; + /** The mime type. */ + @Nullable public String mimeType; + /** Size of the frame associated with this header, in bytes. */ + public int frameSize; + /** Sample rate in samples per second. */ + public int sampleRate; + /** Number of audio channels in the frame. */ + public int channels; + /** Bitrate of the frame in bit/s. */ + public int bitrate; + /** Number of samples stored in the frame. */ + public int samplesPerFrame; + + private void setValues( + int version, + String mimeType, + int frameSize, + int sampleRate, + int channels, + int bitrate, + int samplesPerFrame) { + this.version = version; + this.mimeType = mimeType; + this.frameSize = frameSize; + this.sampleRate = sampleRate; + this.channels = channels; + this.bitrate = bitrate; + this.samplesPerFrame = samplesPerFrame; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java new file mode 100644 index 0000000000..feae7f0bc7 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +/** + * Holds a position in the stream. + */ +public final class PositionHolder { + + /** + * The held position. + */ + public long position; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java new file mode 100644 index 0000000000..b3ccad214d --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** + * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream. + */ +public interface SeekMap { + + /** A {@link SeekMap} that does not support seeking. */ + class Unseekable implements SeekMap { + + private final long durationUs; + private final SeekPoints startSeekPoints; + + /** + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. + */ + public Unseekable(long durationUs) { + this(durationUs, 0); + } + + /** + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. + * @param startPosition The position (byte offset) of the start of the media. + */ + public Unseekable(long durationUs, long startPosition) { + this.durationUs = durationUs; + startSeekPoints = + new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition)); + } + + @Override + public boolean isSeekable() { + return false; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + return startSeekPoints; + } + } + + /** Contains one or two {@link SeekPoint}s. */ + final class SeekPoints { + + /** The first seek point. */ + public final SeekPoint first; + /** The second seek point, or {@link #first} if there's only one seek point. */ + public final SeekPoint second; + + /** @param point The single seek point. */ + public SeekPoints(SeekPoint point) { + this(point, point); + } + + /** + * @param first The first seek point. + * @param second The second seek point. + */ + public SeekPoints(SeekPoint first, SeekPoint second) { + this.first = Assertions.checkNotNull(first); + this.second = Assertions.checkNotNull(second); + } + + @Override + public String toString() { + return "[" + first + (first.equals(second) ? "" : (", " + second)) + "]"; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoints other = (SeekPoints) obj; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return (31 * first.hashCode()) + second.hashCode(); + } + } + + /** + * Returns whether seeking is supported. + * + * @return Whether seeking is supported. + */ + boolean isSeekable(); + + /** + * Returns the duration of the stream in microseconds. + * + * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is + * unknown. + */ + long getDurationUs(); + + /** + * Obtains seek points for the specified seek time in microseconds. The returned {@link + * SeekPoints} will contain one or two distinct seek points. + * + * <p>Two seek points [A, B] are returned in the case that seeking can only be performed to + * discrete points in time, there does not exist a seek point at exactly the requested time, and + * there exist seek points on both sides of it. In this case A and B are the closest seek points + * before and after the requested time. A single seek point is returned in all other cases. + * + * @param timeUs A seek time in microseconds. + * @return The corresponding seek points. + */ + SeekPoints getSeekPoints(long timeUs); +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java new file mode 100644 index 0000000000..1c4db35203 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; + +/** Defines a seek point in a media stream. */ +public final class SeekPoint { + + /** A {@link SeekPoint} whose time and byte offset are both set to 0. */ + public static final SeekPoint START = new SeekPoint(0, 0); + + /** The time of the seek point, in microseconds. */ + public final long timeUs; + + /** The byte offset of the seek point. */ + public final long position; + + /** + * @param timeUs The time of the seek point, in microseconds. + * @param position The byte offset of the seek point. + */ + public SeekPoint(long timeUs, long position) { + this.timeUs = timeUs; + this.position = position; + } + + @Override + public String toString() { + return "[timeUs=" + timeUs + ", position=" + position + "]"; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoint other = (SeekPoint) obj; + return timeUs == other.timeUs && position == other.position; + } + + @Override + public int hashCode() { + int result = (int) timeUs; + result = 31 * result + (int) position; + return result; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java new file mode 100644 index 0000000000..fd33bd6027 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; + +/** + * Receives track level data extracted by an {@link Extractor}. + */ +public interface TrackOutput { + + /** + * Holds data required to decrypt a sample. + */ + final class CryptoData { + + /** + * The encryption mode used for the sample. + */ + @C.CryptoMode public final int cryptoMode; + + /** + * The encryption key associated with the sample. Its contents must not be modified. + */ + public final byte[] encryptionKey; + + /** + * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int encryptedBlocks; + + /** + * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int clearBlocks; + + /** + * @param cryptoMode See {@link #cryptoMode}. + * @param encryptionKey See {@link #encryptionKey}. + * @param encryptedBlocks See {@link #encryptedBlocks}. + * @param clearBlocks See {@link #clearBlocks}. + */ + public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks, + int clearBlocks) { + this.cryptoMode = cryptoMode; + this.encryptionKey = encryptionKey; + this.encryptedBlocks = encryptedBlocks; + this.clearBlocks = clearBlocks; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CryptoData other = (CryptoData) obj; + return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks + && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey); + } + + @Override + public int hashCode() { + int result = cryptoMode; + result = 31 * result + Arrays.hashCode(encryptionKey); + result = 31 * result + encryptedBlocks; + result = 31 * result + clearBlocks; + return result; + } + + } + + /** + * Called when the {@link Format} of the track has been extracted from the stream. + * + * @param format The extracted {@link Format}. + */ + void format(Format format); + + /** + * Called to write sample data to the output. + * + * @param input An {@link ExtractorInput} from which to read the sample data. + * @param length The maximum length to read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The number of bytes appended. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Called to write sample data to the output. + * + * @param data A {@link ParsableByteArray} from which to read the sample data. + * @param length The number of bytes to read, starting from {@code data.getPosition()}. + */ + void sampleData(ParsableByteArray data, int length); + + /** + * Called when metadata associated with a sample has been extracted from the stream. + * + * <p>The corresponding sample data will have already been passed to the output via calls to + * {@link #sampleData(ExtractorInput, int, boolean)} or {@link #sampleData(ParsableByteArray, + * int)}. + * + * @param timeUs The media timestamp associated with the sample, in microseconds. + * @param flags Flags associated with the sample. See {@code C.BUFFER_FLAG_*}. + * @param size The size of the sample data, in bytes. + * @param offset The number of bytes that have been passed to {@link #sampleData(ExtractorInput, + * int, boolean)} or {@link #sampleData(ParsableByteArray, int)} since the last byte belonging + * to the sample whose metadata is being passed. + * @param encryptionData The encryption data required to decrypt the sample. May be null. + */ + void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData encryptionData); +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java new file mode 100644 index 0000000000..4ea27c0149 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** + * Wraps a byte array, providing methods that allow it to be read as a Vorbis bitstream. + * + * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking + * specification</a> + */ +public final class VorbisBitArray { + + private final byte[] data; + private final int byteLimit; + + private int byteOffset; + private int bitOffset; + + /** + * Creates a new instance that wraps an existing array. + * + * @param data the array to wrap. + */ + public VorbisBitArray(byte[] data) { + this.data = data; + byteLimit = data.length; + } + + /** + * Resets the reading position to zero. + */ + public void reset() { + byteOffset = 0; + bitOffset = 0; + } + + /** + * Reads a single bit. + * + * @return {@code true} if the bit is set, {@code false} otherwise. + */ + public boolean readBit() { + boolean returnValue = (((data[byteOffset] & 0xFF) >> bitOffset) & 0x01) == 1; + skipBits(1); + return returnValue; + } + + /** + * Reads up to 32 bits. + * + * @param numBits The number of bits to read. + * @return An integer whose bottom {@code numBits} bits hold the read data. + */ + public int readBits(int numBits) { + int tempByteOffset = byteOffset; + int bitsRead = Math.min(numBits, 8 - bitOffset); + int returnValue = ((data[tempByteOffset++] & 0xFF) >> bitOffset) & (0xFF >> (8 - bitsRead)); + while (bitsRead < numBits) { + returnValue |= (data[tempByteOffset++] & 0xFF) << bitsRead; + bitsRead += 8; + } + returnValue &= 0xFFFFFFFF >>> (32 - numBits); + skipBits(numBits); + return returnValue; + } + + /** + * Skips {@code numberOfBits} bits. + * + * @param numBits The number of bits to skip. + */ + public void skipBits(int numBits) { + int numBytes = numBits / 8; + byteOffset += numBytes; + bitOffset += numBits - (numBytes * 8); + if (bitOffset > 7) { + byteOffset++; + bitOffset -= 8; + } + assertValidOffset(); + } + + /** + * Returns the reading position in bits. + */ + public int getPosition() { + return byteOffset * 8 + bitOffset; + } + + /** + * Sets the reading position in bits. + * + * @param position The new reading position in bits. + */ + public void setPosition(int position) { + byteOffset = position / 8; + bitOffset = position - (byteOffset * 8); + assertValidOffset(); + } + + /** + * Returns the number of remaining bits. + */ + public int bitsLeft() { + return (byteLimit - byteOffset) * 8 - bitOffset; + } + + private void assertValidOffset() { + // It is fine for position to be at the end of the array, but no further. + Assertions.checkState(byteOffset >= 0 + && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java new file mode 100644 index 0000000000..bdd3e13b99 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Arrays; + +/** Utility methods for parsing Vorbis streams. */ +public final class VorbisUtil { + + /** Vorbis comment header. */ + public static final class CommentHeader { + + public final String vendor; + public final String[] comments; + public final int length; + + public CommentHeader(String vendor, String[] comments, int length) { + this.vendor = vendor; + this.comments = comments; + this.length = length; + } + } + + /** Vorbis identification header. */ + public static final class VorbisIdHeader { + + public final long version; + public final int channels; + public final long sampleRate; + public final int bitrateMax; + public final int bitrateNominal; + public final int bitrateMin; + public final int blockSize0; + public final int blockSize1; + public final boolean framingFlag; + public final byte[] data; + + public VorbisIdHeader( + long version, + int channels, + long sampleRate, + int bitrateMax, + int bitrateNominal, + int bitrateMin, + int blockSize0, + int blockSize1, + boolean framingFlag, + byte[] data) { + this.version = version; + this.channels = channels; + this.sampleRate = sampleRate; + this.bitrateMax = bitrateMax; + this.bitrateNominal = bitrateNominal; + this.bitrateMin = bitrateMin; + this.blockSize0 = blockSize0; + this.blockSize1 = blockSize1; + this.framingFlag = framingFlag; + this.data = data; + } + + public int getApproximateBitrate() { + return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal; + } + } + + /** Vorbis setup header modes. */ + public static final class Mode { + + public final boolean blockFlag; + public final int windowType; + public final int transformType; + public final int mapping; + + public Mode(boolean blockFlag, int windowType, int transformType, int mapping) { + this.blockFlag = blockFlag; + this.windowType = windowType; + this.transformType = transformType; + this.mapping = mapping; + } + } + + private static final String TAG = "VorbisUtil"; + + /** + * Returns ilog(x), which is the index of the highest set bit in {@code x}. + * + * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1190009.2.1"> + * Vorbis spec</a> + * @param x the value of which the ilog should be calculated. + * @return ilog(x) + */ + public static int iLog(int x) { + int val = 0; + while (x > 0) { + val++; + x >>>= 1; + } + return val; + } + + /** + * Reads a Vorbis identification header from {@code headerData}. + * + * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis + * spec/Identification header</a> + * @param headerData a {@link ParsableByteArray} wrapping the header data. + * @return a {@link VorbisUtil.VorbisIdHeader} with meta data. + * @throws ParserException thrown if invalid capture pattern is detected. + */ + public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData) + throws ParserException { + + verifyVorbisHeaderCapturePattern(0x01, headerData, false); + + long version = headerData.readLittleEndianUnsignedInt(); + int channels = headerData.readUnsignedByte(); + long sampleRate = headerData.readLittleEndianUnsignedInt(); + int bitrateMax = headerData.readLittleEndianInt(); + int bitrateNominal = headerData.readLittleEndianInt(); + int bitrateMin = headerData.readLittleEndianInt(); + + int blockSize = headerData.readUnsignedByte(); + int blockSize0 = (int) Math.pow(2, blockSize & 0x0F); + int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4); + + boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0; + // raw data of Vorbis setup header has to be passed to decoder as CSD buffer #1 + byte[] data = Arrays.copyOf(headerData.data, headerData.limit()); + + return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin, + blockSize0, blockSize1, framingFlag, data); + } + + /** + * Reads a Vorbis comment header. + * + * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis + * spec/Comment header</a> + * @param headerData A {@link ParsableByteArray} wrapping the header data. + * @return A {@link VorbisUtil.CommentHeader} with all the comments. + * @throws ParserException If an error occurs parsing the comment header. + */ + public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) + throws ParserException { + return readVorbisCommentHeader( + headerData, /* hasMetadataHeader= */ true, /* hasFramingBit= */ true); + } + + /** + * Reads a Vorbis comment header. + * + * <p>The data provided may not contain the Vorbis metadata common header and the framing bit. + * + * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis + * spec/Comment header</a> + * @param headerData A {@link ParsableByteArray} wrapping the header data. + * @param hasMetadataHeader Whether the {@code headerData} contains a Vorbis metadata common + * header preceding the comment header. + * @param hasFramingBit Whether the {@code headerData} contains a framing bit. + * @return A {@link VorbisUtil.CommentHeader} with all the comments. + * @throws ParserException If an error occurs parsing the comment header. + */ + public static CommentHeader readVorbisCommentHeader( + ParsableByteArray headerData, boolean hasMetadataHeader, boolean hasFramingBit) + throws ParserException { + + if (hasMetadataHeader) { + verifyVorbisHeaderCapturePattern(/* headerType= */ 0x03, headerData, /* quiet= */ false); + } + int length = 7; + + int len = (int) headerData.readLittleEndianUnsignedInt(); + length += 4; + String vendor = headerData.readString(len); + length += vendor.length(); + + long commentListLen = headerData.readLittleEndianUnsignedInt(); + String[] comments = new String[(int) commentListLen]; + length += 4; + for (int i = 0; i < commentListLen; i++) { + len = (int) headerData.readLittleEndianUnsignedInt(); + length += 4; + comments[i] = headerData.readString(len); + length += comments[i].length(); + } + if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) { + throw new ParserException("framing bit expected to be set"); + } + length += 1; + return new CommentHeader(vendor, comments, length); + } + + /** + * Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code + * headerType}. + * + * @param headerType the type of the header expected. + * @param header the alleged header bytes. + * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned. + * @return the number of bytes read. + * @throws ParserException thrown if header type or capture pattern is not as expected. + */ + public static boolean verifyVorbisHeaderCapturePattern( + int headerType, ParsableByteArray header, boolean quiet) throws ParserException { + if (header.bytesLeft() < 7) { + if (quiet) { + return false; + } else { + throw new ParserException("too short header: " + header.bytesLeft()); + } + } + + if (header.readUnsignedByte() != headerType) { + if (quiet) { + return false; + } else { + throw new ParserException("expected header type " + Integer.toHexString(headerType)); + } + } + + if (!(header.readUnsignedByte() == 'v' + && header.readUnsignedByte() == 'o' + && header.readUnsignedByte() == 'r' + && header.readUnsignedByte() == 'b' + && header.readUnsignedByte() == 'i' + && header.readUnsignedByte() == 's')) { + if (quiet) { + return false; + } else { + throw new ParserException("expected characters 'vorbis'"); + } + } + return true; + } + + /** + * This method reads the modes which are located at the very end of the Vorbis setup header. + * That's why we need to partially decode or at least read the entire setup header to know where + * to start reading the modes. + * + * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4">Vorbis + * spec/Setup header</a> + * @param headerData a {@link ParsableByteArray} containing setup header data. + * @param channels the number of channels. + * @return an array of {@link Mode}s. + * @throws ParserException thrown if bit stream is invalid. + */ + public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels) + throws ParserException { + + verifyVorbisHeaderCapturePattern(0x05, headerData, false); + + int numberOfBooks = headerData.readUnsignedByte() + 1; + + VorbisBitArray bitArray = new VorbisBitArray(headerData.data); + bitArray.skipBits(headerData.getPosition() * 8); + + for (int i = 0; i < numberOfBooks; i++) { + readBook(bitArray); + } + + int timeCount = bitArray.readBits(6) + 1; + for (int i = 0; i < timeCount; i++) { + if (bitArray.readBits(16) != 0x00) { + throw new ParserException("placeholder of time domain transforms not zeroed out"); + } + } + readFloors(bitArray); + readResidues(bitArray); + readMappings(channels, bitArray); + + Mode[] modes = readModes(bitArray); + if (!bitArray.readBit()) { + throw new ParserException("framing bit after modes not set as expected"); + } + return modes; + } + + private static Mode[] readModes(VorbisBitArray bitArray) { + int modeCount = bitArray.readBits(6) + 1; + Mode[] modes = new Mode[modeCount]; + for (int i = 0; i < modeCount; i++) { + boolean blockFlag = bitArray.readBit(); + int windowType = bitArray.readBits(16); + int transformType = bitArray.readBits(16); + int mapping = bitArray.readBits(8); + modes[i] = new Mode(blockFlag, windowType, transformType, mapping); + } + return modes; + } + + private static void readMappings(int channels, VorbisBitArray bitArray) + throws ParserException { + int mappingsCount = bitArray.readBits(6) + 1; + for (int i = 0; i < mappingsCount; i++) { + int mappingType = bitArray.readBits(16); + if (mappingType != 0) { + Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); + continue; + } + int submaps; + if (bitArray.readBit()) { + submaps = bitArray.readBits(4) + 1; + } else { + submaps = 1; + } + int couplingSteps; + if (bitArray.readBit()) { + couplingSteps = bitArray.readBits(8) + 1; + for (int j = 0; j < couplingSteps; j++) { + bitArray.skipBits(iLog(channels - 1)); // magnitude + bitArray.skipBits(iLog(channels - 1)); // angle + } + } /*else { + couplingSteps = 0; + }*/ + if (bitArray.readBits(2) != 0x00) { + throw new ParserException("to reserved bits must be zero after mapping coupling steps"); + } + if (submaps > 1) { + for (int j = 0; j < channels; j++) { + bitArray.skipBits(4); // mappingMux + } + } + for (int j = 0; j < submaps; j++) { + bitArray.skipBits(8); // discard + bitArray.skipBits(8); // submapFloor + bitArray.skipBits(8); // submapResidue + } + } + } + + private static void readResidues(VorbisBitArray bitArray) throws ParserException { + int residueCount = bitArray.readBits(6) + 1; + for (int i = 0; i < residueCount; i++) { + int residueType = bitArray.readBits(16); + if (residueType > 2) { + throw new ParserException("residueType greater than 2 is not decodable"); + } else { + bitArray.skipBits(24); // begin + bitArray.skipBits(24); // end + bitArray.skipBits(24); // partitionSize (add one) + int classifications = bitArray.readBits(6) + 1; + bitArray.skipBits(8); // classbook + int[] cascade = new int[classifications]; + for (int j = 0; j < classifications; j++) { + int highBits = 0; + int lowBits = bitArray.readBits(3); + if (bitArray.readBit()) { + highBits = bitArray.readBits(5); + } + cascade[j] = highBits * 8 + lowBits; + } + for (int j = 0; j < classifications; j++) { + for (int k = 0; k < 8; k++) { + if ((cascade[j] & (0x01 << k)) != 0) { + bitArray.skipBits(8); // discard + } + } + } + } + } + } + + private static void readFloors(VorbisBitArray bitArray) throws ParserException { + int floorCount = bitArray.readBits(6) + 1; + for (int i = 0; i < floorCount; i++) { + int floorType = bitArray.readBits(16); + switch (floorType) { + case 0: + bitArray.skipBits(8); //order + bitArray.skipBits(16); // rate + bitArray.skipBits(16); // barkMapSize + bitArray.skipBits(6); // amplitudeBits + bitArray.skipBits(8); // amplitudeOffset + int floorNumberOfBooks = bitArray.readBits(4) + 1; + for (int j = 0; j < floorNumberOfBooks; j++) { + bitArray.skipBits(8); + } + break; + case 1: + int partitions = bitArray.readBits(5); + int maximumClass = -1; + int[] partitionClassList = new int[partitions]; + for (int j = 0; j < partitions; j++) { + partitionClassList[j] = bitArray.readBits(4); + if (partitionClassList[j] > maximumClass) { + maximumClass = partitionClassList[j]; + } + } + int[] classDimensions = new int[maximumClass + 1]; + for (int j = 0; j < classDimensions.length; j++) { + classDimensions[j] = bitArray.readBits(3) + 1; + int classSubclasses = bitArray.readBits(2); + if (classSubclasses > 0) { + bitArray.skipBits(8); // classMasterbooks + } + for (int k = 0; k < (1 << classSubclasses); k++) { + bitArray.skipBits(8); // subclassBook (subtract 1) + } + } + bitArray.skipBits(2); // multiplier (add one) + int rangeBits = bitArray.readBits(4); + int count = 0; + for (int j = 0, k = 0; j < partitions; j++) { + int idx = partitionClassList[j]; + count += classDimensions[idx]; + for (; k < count; k++) { + bitArray.skipBits(rangeBits); // floorValue + } + } + break; + default: + throw new ParserException("floor type greater than 1 not decodable: " + floorType); + } + } + } + + private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException { + if (bitArray.readBits(24) != 0x564342) { + throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at " + + bitArray.getPosition()); + } + int dimensions = bitArray.readBits(16); + int entries = bitArray.readBits(24); + long[] lengthMap = new long[entries]; + + boolean isOrdered = bitArray.readBit(); + if (!isOrdered) { + boolean isSparse = bitArray.readBit(); + for (int i = 0; i < lengthMap.length; i++) { + if (isSparse) { + if (bitArray.readBit()) { + lengthMap[i] = (long) (bitArray.readBits(5) + 1); + } else { // entry unused + lengthMap[i] = 0; + } + } else { // not sparse + lengthMap[i] = (long) (bitArray.readBits(5) + 1); + } + } + } else { + int length = bitArray.readBits(5) + 1; + for (int i = 0; i < lengthMap.length;) { + int num = bitArray.readBits(iLog(entries - i)); + for (int j = 0; j < num && i < lengthMap.length; i++, j++) { + lengthMap[i] = length; + } + length++; + } + } + + int lookupType = bitArray.readBits(4); + if (lookupType > 2) { + throw new ParserException("lookup type greater than 2 not decodable: " + lookupType); + } else if (lookupType == 1 || lookupType == 2) { + bitArray.skipBits(32); // minimumValue + bitArray.skipBits(32); // deltaValue + int valueBits = bitArray.readBits(4) + 1; + bitArray.skipBits(1); // sequenceP + long lookupValuesCount; + if (lookupType == 1) { + if (dimensions != 0) { + lookupValuesCount = mapType1QuantValues(entries, dimensions); + } else { + lookupValuesCount = 0; + } + } else { + lookupValuesCount = (long) entries * dimensions; + } + // discard (no decoding required yet) + bitArray.skipBits((int) (lookupValuesCount * valueBits)); + } + return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered); + } + + /** + * @see <a href="http://svn.xiph.org/trunk/vorbis/lib/sharedbook.c">_book_maptype1_quantvals</a> + */ + private static long mapType1QuantValues(long entries, long dimension) { + return (long) Math.floor(Math.pow(entries, 1.d / dimension)); + } + + private VorbisUtil() { + // Prevent instantiation. + } + + private static final class CodeBook { + + public final int dimensions; + public final int entries; + public final long[] lengthMap; + public final int lookupType; + public final boolean isOrdered; + + public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType, + boolean isOrdered) { + this.dimensions = dimensions; + this.entries = entries; + this.lengthMap = lengthMap; + this.lookupType = lookupType; + this.isOrdered = isOrdered; + } + + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java new file mode 100644 index 0000000000..35f539a394 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.amr; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; + +/** + * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867, + * section 5. + * + * <p>This extractor only supports single-channel AMR container formats. + */ +public final class AmrExtractor implements Extractor { + + /** Factory for {@link AmrExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR + * narrow band. + */ + private static final int[] frameSizeBytesByTypeNb = { + 13, + 14, + 16, + 18, + 20, + 21, + 27, + 32, + 6, // AMR SID + 7, // GSM-EFR SID + 6, // TDMA-EFR SID + 6, // PDC-EFR SID + 1, // Future use + 1, // Future use + 1, // Future use + 1 // No data + }; + + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR wide + * band. + */ + private static final int[] frameSizeBytesByTypeWb = { + 18, + 24, + 33, + 37, + 41, + 47, + 51, + 59, + 61, + 6, // AMR-WB SID + 1, // Future use + 1, // Future use + 1, // Future use + 1, // Future use + 1, // speech lost + 1 // No data + }; + + private static final byte[] amrSignatureNb = Util.getUtf8Bytes("#!AMR\n"); + private static final byte[] amrSignatureWb = Util.getUtf8Bytes("#!AMR-WB\n"); + + /** Theoretical maximum frame size for a AMR frame. */ + private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8]; + /** + * The required number of samples in the stream with same sample size to classify the stream as a + * constant-bitrate-stream. + */ + private static final int NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD = 20; + + private static final int SAMPLE_RATE_WB = 16_000; + private static final int SAMPLE_RATE_NB = 8_000; + private static final int SAMPLE_TIME_PER_FRAME_US = 20_000; + + private final byte[] scratch; + private final @Flags int flags; + + private boolean isWideBand; + private long currentSampleTimeUs; + private int currentSampleSize; + private int currentSampleBytesRemaining; + private boolean hasOutputSeekMap; + private long firstSamplePosition; + private int firstSampleSize; + private int numSamplesWithSameSize; + private long timeOffsetUs; + + private ExtractorOutput extractorOutput; + private TrackOutput trackOutput; + @Nullable private SeekMap seekMap; + private boolean hasOutputFormat; + + public AmrExtractor() { + this(/* flags= */ 0); + } + + /** @param flags Flags that control the extractor's behavior. */ + public AmrExtractor(@Flags int flags) { + this.flags = flags; + scratch = new byte[1]; + firstSampleSize = C.LENGTH_UNSET; + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return readAmrHeader(input); + } + + @Override + public void init(ExtractorOutput extractorOutput) { + this.extractorOutput = extractorOutput; + trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + extractorOutput.endTracks(); + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (input.getPosition() == 0) { + if (!readAmrHeader(input)) { + throw new ParserException("Could not find AMR header."); + } + } + maybeOutputFormat(); + int sampleReadResult = readSample(input); + maybeOutputSeekMap(input.getLength(), sampleReadResult); + return sampleReadResult; + } + + @Override + public void seek(long position, long timeUs) { + currentSampleTimeUs = 0; + currentSampleSize = 0; + currentSampleBytesRemaining = 0; + if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) { + timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position); + } else { + timeOffsetUs = 0; + } + } + + @Override + public void release() { + // Do nothing + } + + /* package */ static int frameSizeBytesByTypeNb(int frameType) { + return frameSizeBytesByTypeNb[frameType]; + } + + /* package */ static int frameSizeBytesByTypeWb(int frameType) { + return frameSizeBytesByTypeWb[frameType]; + } + + /* package */ static byte[] amrSignatureNb() { + return Arrays.copyOf(amrSignatureNb, amrSignatureNb.length); + } + + /* package */ static byte[] amrSignatureWb() { + return Arrays.copyOf(amrSignatureWb, amrSignatureWb.length); + } + + // Internal methods. + + /** + * Peeks the AMR header from the beginning of the input, and consumes it if it exists. + * + * @param input The {@link ExtractorInput} from which data should be peeked/read. + * @return Whether the AMR header has been read. + */ + private boolean readAmrHeader(ExtractorInput input) throws IOException, InterruptedException { + if (peekAmrSignature(input, amrSignatureNb)) { + isWideBand = false; + input.skipFully(amrSignatureNb.length); + return true; + } else if (peekAmrSignature(input, amrSignatureWb)) { + isWideBand = true; + input.skipFully(amrSignatureWb.length); + return true; + } + return false; + } + + /** Peeks from the beginning of the input to see if the given AMR signature exists. */ + private boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature) + throws IOException, InterruptedException { + input.resetPeekPosition(); + byte[] header = new byte[amrSignature.length]; + input.peekFully(header, 0, amrSignature.length); + return Arrays.equals(header, amrSignature); + } + + private void maybeOutputFormat() { + if (!hasOutputFormat) { + hasOutputFormat = true; + String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB; + int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB; + trackOutput.format( + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + MAX_FRAME_SIZE_BYTES, + /* channelCount= */ 1, + sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null)); + } + } + + private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { + if (currentSampleBytesRemaining == 0) { + try { + currentSampleSize = peekNextSampleSize(extractorInput); + } catch (EOFException e) { + return RESULT_END_OF_INPUT; + } + currentSampleBytesRemaining = currentSampleSize; + if (firstSampleSize == C.LENGTH_UNSET) { + firstSamplePosition = extractorInput.getPosition(); + firstSampleSize = currentSampleSize; + } + if (firstSampleSize == currentSampleSize) { + numSamplesWithSameSize++; + } + } + + int bytesAppended = + trackOutput.sampleData( + extractorInput, currentSampleBytesRemaining, /* allowEndOfInput= */ true); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + currentSampleBytesRemaining -= bytesAppended; + if (currentSampleBytesRemaining > 0) { + return RESULT_CONTINUE; + } + + trackOutput.sampleMetadata( + timeOffsetUs + currentSampleTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentSampleSize, + /* offset= */ 0, + /* encryptionData= */ null); + currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US; + return RESULT_CONTINUE; + } + + private int peekNextSampleSize(ExtractorInput extractorInput) + throws IOException, InterruptedException { + extractorInput.resetPeekPosition(); + extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1); + + byte frameHeader = scratch[0]; + if ((frameHeader & 0x83) > 0) { + // The padding bits are at bit-1 positions in the following pattern: 1000 0011 + // Padding bits must be 0. + throw new ParserException("Invalid padding bits for frame header " + frameHeader); + } + + int frameType = (frameHeader >> 3) & 0x0f; + return getFrameSizeInBytes(frameType); + } + + private int getFrameSizeInBytes(int frameType) throws ParserException { + if (!isValidFrameType(frameType)) { + throw new ParserException( + "Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType); + } + + return isWideBand ? frameSizeBytesByTypeWb[frameType] : frameSizeBytesByTypeNb[frameType]; + } + + private boolean isValidFrameType(int frameType) { + return frameType >= 0 + && frameType <= 15 + && (isWideBandValidFrameType(frameType) || isNarrowBandValidFrameType(frameType)); + } + + private boolean isWideBandValidFrameType(int frameType) { + // For wide band, type 10-13 are for future use. + return isWideBand && (frameType < 10 || frameType > 13); + } + + private boolean isNarrowBandValidFrameType(int frameType) { + // For narrow band, type 12-14 are for future use. + return !isWideBand && (frameType < 12 || frameType > 14); + } + + private void maybeOutputSeekMap(long inputLength, int sampleReadResult) { + if (hasOutputSeekMap) { + return; + } + + if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) == 0 + || inputLength == C.LENGTH_UNSET + || (firstSampleSize != C.LENGTH_UNSET && firstSampleSize != currentSampleSize)) { + seekMap = new SeekMap.Unseekable(C.TIME_UNSET); + extractorOutput.seekMap(seekMap); + hasOutputSeekMap = true; + } else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD + || sampleReadResult == RESULT_END_OF_INPUT) { + seekMap = getConstantBitrateSeekMap(inputLength); + extractorOutput.seekMap(seekMap); + hasOutputSeekMap = true; + } + } + + private SeekMap getConstantBitrateSeekMap(long inputLength) { + int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US); + return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize); + } + + /** + * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. + * + * @param frameSize The size of each frame in the stream. + * @param durationUsPerFrame The duration of the given frame in microseconds. + * @return The stream bitrate. + */ + private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) { + return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java new file mode 100644 index 0000000000..d13b1f394d --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import java.io.IOException; + +/** + * A {@link SeekMap} implementation for FLAC stream using binary search. + * + * <p>This seeker performs seeking by using binary search within the stream, until it finds the + * frame that contains the target sample. + */ +/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker { + + /** + * Creates a {@link FlacBinarySearchSeeker}. + * + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker, consisting of the 2 bytes by which every frame + * in the stream must start. + * @param firstFramePosition The byte offset of the first frame in the stream. + * @param inputLength The length of the stream in bytes. + */ + public FlacBinarySearchSeeker( + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + long firstFramePosition, + long inputLength) { + super( + /* seekTimestampConverter= */ flacStreamMetadata::getSampleNumber, + new FlacTimestampSeeker(flacStreamMetadata, frameStartMarker), + flacStreamMetadata.getDurationUs(), + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ flacStreamMetadata.totalSamples, + /* floorBytePosition= */ firstFramePosition, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ flacStreamMetadata.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max( + FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + } + + private static final class FlacTimestampSeeker implements TimestampSeeker { + + private final FlacStreamMetadata flacStreamMetadata; + private final int frameStartMarker; + private final SampleNumberHolder sampleNumberHolder; + + private FlacTimestampSeeker(FlacStreamMetadata flacStreamMetadata, int frameStartMarker) { + this.flacStreamMetadata = flacStreamMetadata; + this.frameStartMarker = frameStartMarker; + sampleNumberHolder = new SampleNumberHolder(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleNumber) + throws IOException, InterruptedException { + long searchPosition = input.getPosition(); + + // Find left frame. + long leftFrameFirstSampleNumber = findNextFrame(input); + long leftFramePosition = input.getPeekPosition(); + + input.advancePeekPosition( + Math.max(FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + + // Find right frame. + long rightFrameFirstSampleNumber = findNextFrame(input); + long rightFramePosition = input.getPeekPosition(); + + if (leftFrameFirstSampleNumber <= targetSampleNumber + && rightFrameFirstSampleNumber > targetSampleNumber) { + return TimestampSearchResult.targetFoundResult(leftFramePosition); + } else if (rightFrameFirstSampleNumber <= targetSampleNumber) { + return TimestampSearchResult.underestimatedResult( + rightFrameFirstSampleNumber, rightFramePosition); + } else { + return TimestampSearchResult.overestimatedResult( + leftFrameFirstSampleNumber, searchPosition); + } + } + + /** + * Searches for the next frame in {@code input}. + * + * <p>The peek position is advanced to the start of the found frame, or at the end of the stream + * if no frame was found. + * + * @param input The input from which to search (starting from the peek position). + * @return The number of the first sample in the found frame, or the total number of samples in + * the stream if no frame was found. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on + * the peek position. + * @throws InterruptedException If interrupted while peeking from input. In this case, there is + * no guarantee on the peek position. + */ + private long findNextFrame(ExtractorInput input) throws IOException, InterruptedException { + while (input.getPeekPosition() < input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE + && !FlacFrameReader.checkFrameHeaderFromPeek( + input, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + input.advancePeekPosition(1); + } + + if (input.getPeekPosition() >= input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE) { + input.advancePeekPosition((int) (input.getLength() - input.getPeekPosition())); + return flacStreamMetadata.totalSamples; + } + + return sampleNumberHolder.sampleNumber; + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java new file mode 100644 index 0000000000..fa997001e8 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Extracts data from FLAC container format. + * + * <p>The format specification can be found at https://xiph.org/flac/format.html. + */ +public final class FlacExtractor implements Extractor { + + /** Factory for {@link FlacExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_ID3_METADATA}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_DISABLE_ID3_METADATA}) + public @interface Flags {} + + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 1; + + /** Parser state. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_READ_ID3_METADATA, + STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES, + STATE_READ_STREAM_MARKER, + STATE_READ_METADATA_BLOCKS, + STATE_GET_FRAME_START_MARKER, + STATE_READ_FRAMES + }) + private @interface State {} + + private static final int STATE_READ_ID3_METADATA = 0; + private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1; + private static final int STATE_READ_STREAM_MARKER = 2; + private static final int STATE_READ_METADATA_BLOCKS = 3; + private static final int STATE_GET_FRAME_START_MARKER = 4; + private static final int STATE_READ_FRAMES = 5; + + /** Arbitrary buffer length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ + private static final int BUFFER_LENGTH = 32 * 1024; + + /** Value of an unknown sample number. */ + private static final int SAMPLE_NUMBER_UNKNOWN = -1; + + private final byte[] streamMarkerAndInfoBlock; + private final ParsableByteArray buffer; + private final boolean id3MetadataDisabled; + + private final SampleNumberHolder sampleNumberHolder; + + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + + private @State int state; + @Nullable private Metadata id3Metadata; + @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata; + private int minFrameSize; + private int frameStartMarker; + @MonotonicNonNull private FlacBinarySearchSeeker binarySearchSeeker; + private int currentFrameBytesWritten; + private long currentFrameFirstSampleNumber; + + /** Constructs an instance with {@code flags = 0}. */ + public FlacExtractor() { + this(/* flags= */ 0); + } + + /** + * Constructs an instance. + * + * @param flags Flags that control the extractor's behavior. Possible flags are described by + * {@link Flags}. + */ + public FlacExtractor(int flags) { + streamMarkerAndInfoBlock = + new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE]; + buffer = new ParsableByteArray(new byte[BUFFER_LENGTH], /* limit= */ 0); + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + sampleNumberHolder = new SampleNumberHolder(); + state = STATE_READ_ID3_METADATA; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + return FlacMetadataReader.checkAndPeekStreamMarker(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + } + + @Override + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + switch (state) { + case STATE_READ_ID3_METADATA: + readId3Metadata(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES: + getStreamMarkerAndInfoBlockBytes(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_STREAM_MARKER: + readStreamMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_METADATA_BLOCKS: + readMetadataBlocks(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_FRAME_START_MARKER: + getFrameStartMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_FRAMES: + return readFrames(input, seekPosition); + default: + throw new IllegalStateException(); + } + } + + @Override + public void seek(long position, long timeUs) { + if (position == 0) { + state = STATE_READ_ID3_METADATA; + } else if (binarySearchSeeker != null) { + binarySearchSeeker.setSeekTargetUs(timeUs); + } + currentFrameFirstSampleNumber = timeUs == 0 ? 0 : SAMPLE_NUMBER_UNKNOWN; + currentFrameBytesWritten = 0; + buffer.reset(); + } + + @Override + public void release() { + // Do nothing. + } + + // Private methods. + + private void readId3Metadata(ExtractorInput input) throws IOException, InterruptedException { + id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled); + state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES; + } + + private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length); + input.resetPeekPosition(); + state = STATE_READ_STREAM_MARKER; + } + + private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.readStreamMarker(input); + state = STATE_READ_METADATA_BLOCKS; + } + + private void readMetadataBlocks(ExtractorInput input) throws IOException, InterruptedException { + boolean isLastMetadataBlock = false; + FlacMetadataReader.FlacStreamMetadataHolder metadataHolder = + new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata); + while (!isLastMetadataBlock) { + isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder); + // Save the current metadata in case an exception occurs. + flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata); + } + + Assertions.checkNotNull(flacStreamMetadata); + minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE); + castNonNull(trackOutput) + .format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata)); + + state = STATE_GET_FRAME_START_MARKER; + } + + private void getFrameStartMarker(ExtractorInput input) throws IOException, InterruptedException { + frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + castNonNull(extractorOutput) + .seekMap( + getSeekMap( + /* firstFramePosition= */ input.getPosition(), + /* streamLength= */ input.getLength())); + + state = STATE_READ_FRAMES; + } + + private @ReadResult int readFrames(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + Assertions.checkNotNull(trackOutput); + Assertions.checkNotNull(flacStreamMetadata); + + // Handle pending binary search seek if necessary. + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return binarySearchSeeker.handlePendingSeek(input, seekPosition); + } + + // Set current frame first sample number if it became unknown after seeking. + if (currentFrameFirstSampleNumber == SAMPLE_NUMBER_UNKNOWN) { + currentFrameFirstSampleNumber = + FlacFrameReader.getFirstSampleNumber(input, flacStreamMetadata); + return Extractor.RESULT_CONTINUE; + } + + // Copy more bytes into the buffer. + int currentLimit = buffer.limit(); + boolean foundEndOfInput = false; + if (currentLimit < BUFFER_LENGTH) { + int bytesRead = + input.read( + buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); + foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + buffer.setLimit(currentLimit + bytesRead); + } else if (buffer.bytesLeft() == 0) { + outputSampleMetadata(); + return Extractor.RESULT_END_OF_INPUT; + } + } + + // Search for a frame. + int positionBeforeFindingAFrame = buffer.getPosition(); + + // Skip frame search on the bytes within the minimum frame size. + if (currentFrameBytesWritten < minFrameSize) { + buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft())); + } + + long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); + int numberOfFrameBytes = buffer.getPosition() - positionBeforeFindingAFrame; + buffer.setPosition(positionBeforeFindingAFrame); + trackOutput.sampleData(buffer, numberOfFrameBytes); + currentFrameBytesWritten += numberOfFrameBytes; + + // Frame found. + if (nextFrameFirstSampleNumber != SAMPLE_NUMBER_UNKNOWN) { + outputSampleMetadata(); + currentFrameBytesWritten = 0; + currentFrameFirstSampleNumber = nextFrameFirstSampleNumber; + } + + if (buffer.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) { + // The next frame header may not fit in the rest of the buffer, so put the trailing bytes at + // the start of the buffer, and reset the position and limit. + System.arraycopy( + buffer.data, buffer.getPosition(), buffer.data, /* destPos= */ 0, buffer.bytesLeft()); + buffer.reset(buffer.bytesLeft()); + } + + return Extractor.RESULT_CONTINUE; + } + + private SeekMap getSeekMap(long firstFramePosition, long streamLength) { + Assertions.checkNotNull(flacStreamMetadata); + if (flacStreamMetadata.seekTable != null) { + return new FlacSeekTableSeekMap(flacStreamMetadata, firstFramePosition); + } else if (streamLength != C.LENGTH_UNSET && flacStreamMetadata.totalSamples > 0) { + binarySearchSeeker = + new FlacBinarySearchSeeker( + flacStreamMetadata, frameStartMarker, firstFramePosition, streamLength); + return binarySearchSeeker.getSeekMap(); + } else { + return new SeekMap.Unseekable(flacStreamMetadata.getDurationUs()); + } + } + + /** + * Searches for the start of a frame in {@code data}. + * + * <ul> + * <li>If the search is successful, the position is set to the start of the found frame. + * <li>Otherwise, the position is set to the first unsearched byte. + * </ul> + * + * @param data The array to be searched. + * @param foundEndOfInput If the end of input was met when filling in the {@code data}. + * @return The number of the first sample in the frame found, or {@code SAMPLE_NUMBER_UNKNOWN} if + * the search was not successful. + */ + private long findFrame(ParsableByteArray data, boolean foundEndOfInput) { + Assertions.checkNotNull(flacStreamMetadata); + + int frameOffset = data.getPosition(); + while (frameOffset <= data.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) { + data.setPosition(frameOffset); + if (FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + + if (foundEndOfInput) { + // Verify whether there is a frame of size < MAX_FRAME_HEADER_SIZE at the end of the stream by + // checking at every position at a distance between MAX_FRAME_HEADER_SIZE and minFrameSize + // from the buffer limit if it corresponds to a valid frame header. + // At every offset, the different possibilities are: + // 1. The current offset indicates the start of a valid frame header. In this case, consider + // that a frame has been found and stop searching. + // 2. A frame starting at the current offset would be invalid. In this case, keep looking for + // a valid frame header. + // 3. The current offset could be the start of a valid frame header, but there is not enough + // bytes remaining to complete the header. As the end of the file has been reached, this + // means that the current offset does not correspond to a new frame and that the last bytes + // of the last frame happen to be a valid partial frame header. This case can occur in two + // ways: + // 3.1. An attempt to read past the buffer is made when reading the potential frame header. + // 3.2. Reading the potential frame header does not exceed the buffer size, but exceeds the + // buffer limit. + // Note that the third case is very unlikely. It never happens if the end of the input has not + // been reached as it is always made sure that the buffer has at least MAX_FRAME_HEADER_SIZE + // bytes available when reading a potential frame header. + while (frameOffset <= data.limit() - minFrameSize) { + data.setPosition(frameOffset); + boolean frameFound; + try { + frameFound = + FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } catch (IndexOutOfBoundsException e) { + // Case 3.1. + frameFound = false; + } + if (data.getPosition() > data.limit()) { + // TODO: Remove (and update above comments) once [Internal ref: b/147657250] is fixed. + // Case 3.2. + frameFound = false; + } + if (frameFound) { + // Case 1. + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + // The end of the frame is the end of the file. + data.setPosition(data.limit()); + } else { + data.setPosition(frameOffset); + } + + return SAMPLE_NUMBER_UNKNOWN; + } + + private void outputSampleMetadata() { + long timeUs = + currentFrameFirstSampleNumber + * C.MICROS_PER_SECOND + / castNonNull(flacStreamMetadata).sampleRate; + castNonNull(trackOutput) + .sampleMetadata( + timeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentFrameBytesWritten, + /* offset= */ 0, + /* encryptionData= */ null); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java new file mode 100644 index 0000000000..54dbaec003 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; + +import android.util.Pair; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; + +/** + * Parses audio tags from an FLV stream and extracts AAC frames. + */ +/* package */ final class AudioTagPayloadReader extends TagPayloadReader { + + private static final int AUDIO_FORMAT_MP3 = 2; + private static final int AUDIO_FORMAT_ALAW = 7; + private static final int AUDIO_FORMAT_ULAW = 8; + private static final int AUDIO_FORMAT_AAC = 10; + + private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0; + private static final int AAC_PACKET_TYPE_AAC_RAW = 1; + + private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {5512, 11025, 22050, 44100}; + + // State variables + private boolean hasParsedAudioDataHeader; + private boolean hasOutputFormat; + private int audioFormat; + + public AudioTagPayloadReader(TrackOutput output) { + super(output); + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { + if (!hasParsedAudioDataHeader) { + int header = data.readUnsignedByte(); + audioFormat = (header >> 4) & 0x0F; + if (audioFormat == AUDIO_FORMAT_MP3) { + int sampleRateIndex = (header >> 2) & 0x03; + int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex]; + Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null, + Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null); + output.format(format); + hasOutputFormat = true; + } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { + String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW + : MimeTypes.AUDIO_MLAW; + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + /* sampleMimeType= */ type, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 1, + /* sampleRate= */ 8000, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + output.format(format); + hasOutputFormat = true; + } else if (audioFormat != AUDIO_FORMAT_AAC) { + throw new UnsupportedFormatException("Audio format not supported: " + audioFormat); + } + hasParsedAudioDataHeader = true; + } else { + // Skip header if it was parsed previously. + data.skipBytes(1); + } + return true; + } + + @Override + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + if (audioFormat == AUDIO_FORMAT_MP3) { + int sampleSize = data.bytesLeft(); + output.sampleData(data, sampleSize); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; + } else { + int packetType = data.readUnsignedByte(); + if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + // Parse the sequence header. + byte[] audioSpecificConfig = new byte[data.bytesLeft()]; + data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length); + Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( + audioSpecificConfig); + Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, + Collections.singletonList(audioSpecificConfig), null, 0, null); + output.format(format); + hasOutputFormat = true; + return false; + } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { + int sampleSize = data.bytesLeft(); + output.sampleData(data, sampleSize); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; + } else { + return false; + } + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java new file mode 100644 index 0000000000..a7438b190f --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Extracts data from the FLV container format. + */ +public final class FlvExtractor implements Extractor { + + /** Factory for {@link FlvExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlvExtractor()}; + + /** Extractor states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_READING_FLV_HEADER, + STATE_SKIPPING_TO_TAG_HEADER, + STATE_READING_TAG_HEADER, + STATE_READING_TAG_DATA + }) + private @interface States {} + + private static final int STATE_READING_FLV_HEADER = 1; + private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; + private static final int STATE_READING_TAG_HEADER = 3; + private static final int STATE_READING_TAG_DATA = 4; + + // Header sizes. + private static final int FLV_HEADER_SIZE = 9; + private static final int FLV_TAG_HEADER_SIZE = 11; + + // Tag types. + private static final int TAG_TYPE_AUDIO = 8; + private static final int TAG_TYPE_VIDEO = 9; + private static final int TAG_TYPE_SCRIPT_DATA = 18; + + // FLV container identifier. + private static final int FLV_TAG = 0x00464c56; + + private final ParsableByteArray scratch; + private final ParsableByteArray headerBuffer; + private final ParsableByteArray tagHeaderBuffer; + private final ParsableByteArray tagData; + private final ScriptTagPayloadReader metadataReader; + + private ExtractorOutput extractorOutput; + private @States int state; + private boolean outputFirstSample; + private long mediaTagTimestampOffsetUs; + private int bytesToNextTagHeader; + private int tagType; + private int tagDataSize; + private long tagTimestampUs; + private boolean outputSeekMap; + private AudioTagPayloadReader audioReader; + private VideoTagPayloadReader videoReader; + + public FlvExtractor() { + scratch = new ParsableByteArray(4); + headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); + tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); + tagData = new ParsableByteArray(); + metadataReader = new ScriptTagPayloadReader(); + state = STATE_READING_FLV_HEADER; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Check if file starts with "FLV" tag + input.peekFully(scratch.data, 0, 3); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != FLV_TAG) { + return false; + } + + // Checking reserved flags are set to 0 + input.peekFully(scratch.data, 0, 2); + scratch.setPosition(0); + if ((scratch.readUnsignedShort() & 0xFA) != 0) { + return false; + } + + // Read data offset + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + int dataOffset = scratch.readInt(); + + input.resetPeekPosition(); + input.advancePeekPosition(dataOffset); + + // Checking first "previous tag size" is set to 0 + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + + return scratch.readInt() == 0; + } + + @Override + public void init(ExtractorOutput output) { + this.extractorOutput = output; + } + + @Override + public void seek(long position, long timeUs) { + state = STATE_READING_FLV_HEADER; + outputFirstSample = false; + bytesToNextTagHeader = 0; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { + while (true) { + switch (state) { + case STATE_READING_FLV_HEADER: + if (!readFlvHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + case STATE_SKIPPING_TO_TAG_HEADER: + skipToTagHeader(input); + break; + case STATE_READING_TAG_HEADER: + if (!readTagHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + case STATE_READING_TAG_DATA: + if (readTagData(input)) { + return RESULT_CONTINUE; + } + break; + default: + // Never happens. + throw new IllegalStateException(); + } + } + } + + /** + * Reads an FLV container header from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return True if header was read successfully. False if the end of stream was reached. + * @throws IOException If an error occurred reading or parsing data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException { + if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) { + // We've reached the end of the stream. + return false; + } + + headerBuffer.setPosition(0); + headerBuffer.skipBytes(4); + int flags = headerBuffer.readUnsignedByte(); + boolean hasAudio = (flags & 0x04) != 0; + boolean hasVideo = (flags & 0x01) != 0; + if (hasAudio && audioReader == null) { + audioReader = new AudioTagPayloadReader( + extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO)); + } + if (hasVideo && videoReader == null) { + videoReader = new VideoTagPayloadReader( + extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); + } + extractorOutput.endTracks(); + + // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. + bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; + state = STATE_SKIPPING_TO_TAG_HEADER; + return true; + } + + /** + * Skips over data to reach the next tag header. + * + * @param input The {@link ExtractorInput} from which to read. + * @throws IOException If an error occurred skipping data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { + input.skipFully(bytesToNextTagHeader); + bytesToNextTagHeader = 0; + state = STATE_READING_TAG_HEADER; + } + + /** + * Reads a tag header from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return True if tag header was read successfully. Otherwise, false. + * @throws IOException If an error occurred reading or parsing data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException { + if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) { + // We've reached the end of the stream. + return false; + } + + tagHeaderBuffer.setPosition(0); + tagType = tagHeaderBuffer.readUnsignedByte(); + tagDataSize = tagHeaderBuffer.readUnsignedInt24(); + tagTimestampUs = tagHeaderBuffer.readUnsignedInt24(); + tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; + tagHeaderBuffer.skipBytes(3); // streamId + state = STATE_READING_TAG_DATA; + return true; + } + + /** + * Reads the body of a tag from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return True if the data was consumed by a reader. False if it was skipped. + * @throws IOException If an error occurred reading or parsing data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { + boolean wasConsumed = true; + boolean wasSampleOutput = false; + long timestampUs = getCurrentTimestampUs(); + if (tagType == TAG_TYPE_AUDIO && audioReader != null) { + ensureReadyForMediaOutput(); + wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs); + } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { + ensureReadyForMediaOutput(); + wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs); + } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { + wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs); + long durationUs = metadataReader.getDurationUs(); + if (durationUs != C.TIME_UNSET) { + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + outputSeekMap = true; + } + } else { + input.skipFully(tagDataSize); + wasConsumed = false; + } + if (!outputFirstSample && wasSampleOutput) { + outputFirstSample = true; + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } + bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. + state = STATE_SKIPPING_TO_TAG_HEADER; + return wasConsumed; + } + + private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException, + InterruptedException { + if (tagDataSize > tagData.capacity()) { + tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0); + } else { + tagData.setPosition(0); + } + tagData.setLimit(tagDataSize); + input.readFully(tagData.data, 0, tagDataSize); + return tagData; + } + + private void ensureReadyForMediaOutput() { + if (!outputSeekMap) { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + outputSeekMap = true; + } + } + + private long getCurrentTimestampUs() { + return outputFirstSample + ? (mediaTagTimestampOffsetUs + tagTimestampUs) + : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java new file mode 100644 index 0000000000..1494bf1c2e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Parses Script Data tags from an FLV stream and extracts metadata information. + */ +/* package */ final class ScriptTagPayloadReader extends TagPayloadReader { + + private static final String NAME_METADATA = "onMetaData"; + private static final String KEY_DURATION = "duration"; + + // AMF object types + private static final int AMF_TYPE_NUMBER = 0; + private static final int AMF_TYPE_BOOLEAN = 1; + private static final int AMF_TYPE_STRING = 2; + private static final int AMF_TYPE_OBJECT = 3; + private static final int AMF_TYPE_ECMA_ARRAY = 8; + private static final int AMF_TYPE_END_MARKER = 9; + private static final int AMF_TYPE_STRICT_ARRAY = 10; + private static final int AMF_TYPE_DATE = 11; + + private long durationUs; + + public ScriptTagPayloadReader() { + super(new DummyTrackOutput()); + durationUs = C.TIME_UNSET; + } + + public long getDurationUs() { + return durationUs; + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + protected boolean parseHeader(ParsableByteArray data) { + return true; + } + + @Override + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + int nameType = readAmfType(data); + if (nameType != AMF_TYPE_STRING) { + // Should never happen. + throw new ParserException(); + } + String name = readAmfString(data); + if (!NAME_METADATA.equals(name)) { + // We're only interested in metadata. + return false; + } + int type = readAmfType(data); + if (type != AMF_TYPE_ECMA_ARRAY) { + // We're not interested in this metadata. + return false; + } + // Set the duration to the value contained in the metadata, if present. + Map<String, Object> metadata = readAmfEcmaArray(data); + if (metadata.containsKey(KEY_DURATION)) { + double durationSeconds = (double) metadata.get(KEY_DURATION); + if (durationSeconds > 0.0) { + durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); + } + } + return false; + } + + private static int readAmfType(ParsableByteArray data) { + return data.readUnsignedByte(); + } + + /** + * Read a boolean from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static Boolean readAmfBoolean(ParsableByteArray data) { + return data.readUnsignedByte() == 1; + } + + /** + * Read a double number from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static Double readAmfDouble(ParsableByteArray data) { + return Double.longBitsToDouble(data.readLong()); + } + + /** + * Read a string from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static String readAmfString(ParsableByteArray data) { + int size = data.readUnsignedShort(); + int position = data.getPosition(); + data.skipBytes(size); + return new String(data.data, position, size); + } + + /** + * Read an array from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) { + int count = data.readUnsignedIntToInt(); + ArrayList<Object> list = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + int type = readAmfType(data); + Object value = readAmfData(data, type); + if (value != null) { + list.add(value); + } + } + return list; + } + + /** + * Read an object from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static HashMap<String, Object> readAmfObject(ParsableByteArray data) { + HashMap<String, Object> array = new HashMap<>(); + while (true) { + String key = readAmfString(data); + int type = readAmfType(data); + if (type == AMF_TYPE_END_MARKER) { + break; + } + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } + } + return array; + } + + /** + * Read an ECMA array from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) { + int count = data.readUnsignedIntToInt(); + HashMap<String, Object> array = new HashMap<>(count); + for (int i = 0; i < count; i++) { + String key = readAmfString(data); + int type = readAmfType(data); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } + } + return array; + } + + /** + * Read a date from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static Date readAmfDate(ParsableByteArray data) { + Date date = new Date((long) readAmfDouble(data).doubleValue()); + data.skipBytes(2); // Skip reserved bytes. + return date; + } + + @Nullable + private static Object readAmfData(ParsableByteArray data, int type) { + switch (type) { + case AMF_TYPE_NUMBER: + return readAmfDouble(data); + case AMF_TYPE_BOOLEAN: + return readAmfBoolean(data); + case AMF_TYPE_STRING: + return readAmfString(data); + case AMF_TYPE_OBJECT: + return readAmfObject(data); + case AMF_TYPE_ECMA_ARRAY: + return readAmfEcmaArray(data); + case AMF_TYPE_STRICT_ARRAY: + return readAmfStrictArray(data); + case AMF_TYPE_DATE: + return readAmfDate(data); + default: + // We don't log a warning because there are types that we knowingly don't support. + return null; + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java new file mode 100644 index 0000000000..3f8b51244a --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Extracts individual samples from FLV tags, preserving original order. + */ +/* package */ abstract class TagPayloadReader { + + /** + * Thrown when the format is not supported. + */ + public static final class UnsupportedFormatException extends ParserException { + + public UnsupportedFormatException(String msg) { + super(msg); + } + + } + + protected final TrackOutput output; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + protected TagPayloadReader(TrackOutput output) { + this.output = output; + } + + /** + * Notifies the reader that a seek has occurred. + * <p> + * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, long)} will not be a continuation of the data that + * was previously passed. Hence the reader should reset any internal state. + */ + public abstract void seek(); + + /** + * Consumes payload data. + * + * @param data The payload data to consume. + * @param timeUs The timestamp associated with the payload. + * @return Whether a sample was output. + * @throws ParserException If an error occurs parsing the data. + */ + public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException { + return parseHeader(data) && parsePayload(data, timeUs); + } + + /** + * Parses tag header. + * + * @param data Buffer where the tag header is stored. + * @return Whether the header was parsed successfully. + * @throws ParserException If an error occurs parsing the header. + */ + protected abstract boolean parseHeader(ParsableByteArray data) throws ParserException; + + /** + * Parses tag payload. + * + * @param data Buffer where tag payload is stored. + * @param timeUs Time position of the frame. + * @return Whether a sample was output. + * @throws ParserException If an error occurs parsing the payload. + */ + protected abstract boolean parsePayload(ParsableByteArray data, long timeUs) + throws ParserException; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java new file mode 100644 index 0000000000..6ed5206144 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; + +/** + * Parses video tags from an FLV stream and extracts H.264 nal units. + */ +/* package */ final class VideoTagPayloadReader extends TagPayloadReader { + + // Video codec. + private static final int VIDEO_CODEC_AVC = 7; + + // Frame types. + private static final int VIDEO_FRAME_KEYFRAME = 1; + private static final int VIDEO_FRAME_VIDEO_INFO = 5; + + // Packet types. + private static final int AVC_PACKET_TYPE_SEQUENCE_HEADER = 0; + private static final int AVC_PACKET_TYPE_AVC_NALU = 1; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + private int nalUnitLengthFieldLength; + + // State variables. + private boolean hasOutputFormat; + private boolean hasOutputKeyframe; + private int frameType; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public VideoTagPayloadReader(TrackOutput output) { + super(output); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + } + + @Override + public void seek() { + hasOutputKeyframe = false; + } + + @Override + protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { + int header = data.readUnsignedByte(); + int frameType = (header >> 4) & 0x0F; + int videoCodec = (header & 0x0F); + // Support just H.264 encoded content. + if (videoCodec != VIDEO_CODEC_AVC) { + throw new UnsupportedFormatException("Video format not supported: " + videoCodec); + } + this.frameType = frameType; + return (frameType != VIDEO_FRAME_VIDEO_INFO); + } + + @Override + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + int packetType = data.readUnsignedByte(); + int compositionTimeMs = data.readInt24(); + + timeUs += compositionTimeMs * 1000L; + // Parse avc sequence header in case this was not done before. + if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]); + data.readBytes(videoSequence.data, 0, data.bytesLeft()); + AvcConfig avcConfig = AvcConfig.parse(videoSequence); + nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength; + // Construct and output the format. + Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, + Format.NO_VALUE, Format.NO_VALUE, avcConfig.width, avcConfig.height, Format.NO_VALUE, + avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); + output.format(format); + hasOutputFormat = true; + return false; + } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { + boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME; + if (!hasOutputKeyframe && !isKeyframe) { + return false; + } + // TODO: Deduplicate with Mp4Extractor. + // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLengthDiff = 4 - nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + int bytesWritten = 0; + int bytesToWrite; + while (data.bytesLeft() > 0) { + // Read the NAL length so that we know where we find the next one. + data.readBytes(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + nalLength.setPosition(0); + bytesToWrite = nalLength.readUnsignedIntToInt(); + + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + bytesWritten += 4; + + // Write the payload of the NAL unit. + output.sampleData(data, bytesToWrite); + bytesWritten += bytesToWrite; + } + output.sampleMetadata( + timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null); + hasOutputKeyframe = true; + return true; + } else { + return false; + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java new file mode 100644 index 0000000000..b4e160fa74 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.io.EOFException; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; + +/** + * Default implementation of {@link EbmlReader}. + */ +/* package */ final class DefaultEbmlReader implements EbmlReader { + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ELEMENT_STATE_READ_ID, ELEMENT_STATE_READ_CONTENT_SIZE, ELEMENT_STATE_READ_CONTENT}) + private @interface ElementState {} + + private static final int ELEMENT_STATE_READ_ID = 0; + private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1; + private static final int ELEMENT_STATE_READ_CONTENT = 2; + + private static final int MAX_ID_BYTES = 4; + private static final int MAX_LENGTH_BYTES = 8; + + private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8; + private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4; + private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8; + + private final byte[] scratch; + private final ArrayDeque<MasterElement> masterElementsStack; + private final VarintReader varintReader; + + private EbmlProcessor processor; + private @ElementState int elementState; + private int elementId; + private long elementContentSize; + + public DefaultEbmlReader() { + scratch = new byte[8]; + masterElementsStack = new ArrayDeque<>(); + varintReader = new VarintReader(); + } + + @Override + public void init(EbmlProcessor processor) { + this.processor = processor; + } + + @Override + public void reset() { + elementState = ELEMENT_STATE_READ_ID; + masterElementsStack.clear(); + varintReader.reset(); + } + + @Override + public boolean read(ExtractorInput input) throws IOException, InterruptedException { + Assertions.checkNotNull(processor); + while (true) { + if (!masterElementsStack.isEmpty() + && input.getPosition() >= masterElementsStack.peek().elementEndPosition) { + processor.endMasterElement(masterElementsStack.pop().elementId); + return true; + } + + if (elementState == ELEMENT_STATE_READ_ID) { + long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES); + if (result == C.RESULT_MAX_LENGTH_EXCEEDED) { + result = maybeResyncToNextLevel1Element(input); + } + if (result == C.RESULT_END_OF_INPUT) { + return false; + } + // Element IDs are at most 4 bytes, so we can cast to integers. + elementId = (int) result; + elementState = ELEMENT_STATE_READ_CONTENT_SIZE; + } + + if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) { + elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES); + elementState = ELEMENT_STATE_READ_CONTENT; + } + + @EbmlProcessor.ElementType int type = processor.getElementType(elementId); + switch (type) { + case EbmlProcessor.ELEMENT_TYPE_MASTER: + long elementContentPosition = input.getPosition(); + long elementEndPosition = elementContentPosition + elementContentSize; + masterElementsStack.push(new MasterElement(elementId, elementEndPosition)); + processor.startMasterElement(elementId, elementContentPosition, elementContentSize); + elementState = ELEMENT_STATE_READ_ID; + return true; + case EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT: + if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { + throw new ParserException("Invalid integer size: " + elementContentSize); + } + processor.integerElement(elementId, readInteger(input, (int) elementContentSize)); + elementState = ELEMENT_STATE_READ_ID; + return true; + case EbmlProcessor.ELEMENT_TYPE_FLOAT: + if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES + && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { + throw new ParserException("Invalid float size: " + elementContentSize); + } + processor.floatElement(elementId, readFloat(input, (int) elementContentSize)); + elementState = ELEMENT_STATE_READ_ID; + return true; + case EbmlProcessor.ELEMENT_TYPE_STRING: + if (elementContentSize > Integer.MAX_VALUE) { + throw new ParserException("String element size: " + elementContentSize); + } + processor.stringElement(elementId, readString(input, (int) elementContentSize)); + elementState = ELEMENT_STATE_READ_ID; + return true; + case EbmlProcessor.ELEMENT_TYPE_BINARY: + processor.binaryElement(elementId, (int) elementContentSize, input); + elementState = ELEMENT_STATE_READ_ID; + return true; + case EbmlProcessor.ELEMENT_TYPE_UNKNOWN: + input.skipFully((int) elementContentSize); + elementState = ELEMENT_STATE_READ_ID; + break; + default: + throw new ParserException("Invalid element type " + type); + } + } + } + + /** + * Does a byte by byte search to try and find the next level 1 element. This method is called if + * some invalid data is encountered in the parser. + * + * @param input The {@link ExtractorInput} from which data has to be read. + * @return id of the next level 1 element that has been found. + * @throws EOFException If the end of input was encountered when searching for the next level 1 + * element. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException, + InterruptedException { + input.resetPeekPosition(); + while (true) { + input.peekFully(scratch, 0, MAX_ID_BYTES); + int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]); + if (varintLength != C.LENGTH_UNSET && varintLength <= MAX_ID_BYTES) { + int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false); + if (processor.isLevel1Element(potentialId)) { + input.skipFully(varintLength); + return potentialId; + } + } + input.skipFully(1); + } + } + + /** + * Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @param byteLength The length of the integer being read. + * @return The read integer value. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private long readInteger(ExtractorInput input, int byteLength) + throws IOException, InterruptedException { + input.readFully(scratch, 0, byteLength); + long value = 0; + for (int i = 0; i < byteLength; i++) { + value = (value << 8) | (scratch[i] & 0xFF); + } + return value; + } + + /** + * Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @param byteLength The length of the float being read. + * @return The read float value. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private double readFloat(ExtractorInput input, int byteLength) + throws IOException, InterruptedException { + long integerValue = readInteger(input, byteLength); + double floatValue; + if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) { + floatValue = Float.intBitsToFloat((int) integerValue); + } else { + floatValue = Double.longBitsToDouble(integerValue); + } + return floatValue; + } + + /** + * Reads a string of length {@code byteLength} from the {@link ExtractorInput}. Zero padding is + * removed, so the returned string may be shorter than {@code byteLength}. + * + * @param input The {@link ExtractorInput} from which to read. + * @param byteLength The length of the string being read, including zero padding. + * @return The read string value. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private String readString(ExtractorInput input, int byteLength) + throws IOException, InterruptedException { + if (byteLength == 0) { + return ""; + } + byte[] stringBytes = new byte[byteLength]; + input.readFully(stringBytes, 0, byteLength); + // Remove zero padding. + int trimmedLength = byteLength; + while (trimmedLength > 0 && stringBytes[trimmedLength - 1] == 0) { + trimmedLength--; + } + return new String(stringBytes, 0, trimmedLength); + } + + /** + * Used in {@link #masterElementsStack} to track when the current master element ends, so that + * {@link EbmlProcessor#endMasterElement(int)} can be called. + */ + private static final class MasterElement { + + private final int elementId; + private final long elementEndPosition; + + private MasterElement(int elementId, long elementEndPosition) { + this.elementId = elementId; + this.elementEndPosition = elementEndPosition; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java new file mode 100644 index 0000000000..188ced0554 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Defines EBML element IDs/types and processes events. */ +public interface EbmlProcessor { + + /** + * EBML element types. One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link + * #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} or + * {@link #ELEMENT_TYPE_FLOAT}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ELEMENT_TYPE_UNKNOWN, + ELEMENT_TYPE_MASTER, + ELEMENT_TYPE_UNSIGNED_INT, + ELEMENT_TYPE_STRING, + ELEMENT_TYPE_BINARY, + ELEMENT_TYPE_FLOAT + }) + @interface ElementType {} + /** Type for unknown elements. */ + int ELEMENT_TYPE_UNKNOWN = 0; + /** Type for elements that contain child elements. */ + int ELEMENT_TYPE_MASTER = 1; + /** Type for integer value elements of up to 8 bytes. */ + int ELEMENT_TYPE_UNSIGNED_INT = 2; + /** Type for string elements. */ + int ELEMENT_TYPE_STRING = 3; + /** Type for binary elements. */ + int ELEMENT_TYPE_BINARY = 4; + /** Type for IEEE floating point value elements of either 4 or 8 bytes. */ + int ELEMENT_TYPE_FLOAT = 5; + + /** + * Maps an element ID to a corresponding type. + * + * <p>If {@link #ELEMENT_TYPE_UNKNOWN} is returned then the element is skipped. Note that all + * children of a skipped element are also skipped. + * + * @param id The element ID to map. + * @return One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link + * #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} and + * {@link #ELEMENT_TYPE_FLOAT}. + */ + @ElementType + int getElementType(int id); + + /** + * Checks if the given id is that of a level 1 element. + * + * @param id The element ID. + * @return Whether the given id is that of a level 1 element. + */ + boolean isLevel1Element(int id); + + /** + * Called when the start of a master element is encountered. + * <p> + * Following events should be considered as taking place within this element until a matching call + * to {@link #endMasterElement(int)} is made. + * <p> + * Note that it is possible for another master element of the same element ID to be nested within + * itself. + * + * @param id The element ID. + * @param contentPosition The position of the start of the element's content in the stream. + * @param contentSize The size of the element's content in bytes. + * @throws ParserException If a parsing error occurs. + */ + void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException; + + /** + * Called when the end of a master element is encountered. + * + * @param id The element ID. + * @throws ParserException If a parsing error occurs. + */ + void endMasterElement(int id) throws ParserException; + + /** + * Called when an integer element is encountered. + * + * @param id The element ID. + * @param value The integer value that the element contains. + * @throws ParserException If a parsing error occurs. + */ + void integerElement(int id, long value) throws ParserException; + + /** + * Called when a float element is encountered. + * + * @param id The element ID. + * @param value The float value that the element contains + * @throws ParserException If a parsing error occurs. + */ + void floatElement(int id, double value) throws ParserException; + + /** + * Called when a string element is encountered. + * + * @param id The element ID. + * @param value The string value that the element contains. + * @throws ParserException If a parsing error occurs. + */ + void stringElement(int id, String value) throws ParserException; + + /** + * Called when a binary element is encountered. + * <p> + * The element header (containing the element ID and content size) will already have been read. + * Implementations are required to consume the whole remainder of the element, which is + * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail + * (by throwing an exception) having partially consumed the data, however if they do this, they + * must consume the remainder of the content when called again. + * + * @param id The element ID. + * @param contentsSize The element's content size. + * @param input The {@link ExtractorInput} from which data should be read. + * @throws ParserException If a parsing error occurs. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void binaryElement(int id, int contentsSize, ExtractorInput input) + throws IOException, InterruptedException; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java new file mode 100644 index 0000000000..1416a9087e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.IOException; + +/** + * Event-driven EBML reader that delivers events to an {@link EbmlProcessor}. + * + * <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was + * originally designed for the Matroska container format. More information about EBML and Matroska + * is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>. + */ +/* package */ interface EbmlReader { + + /** + * Initializes the extractor with an {@link EbmlProcessor}. + * + * @param processor An {@link EbmlProcessor} to process events. + */ + void init(EbmlProcessor processor); + + /** + * Resets the state of the reader. + * <p> + * Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure + * from scratch. + */ + void reset(); + + /** + * Reads from an {@link ExtractorInput}, invoking an event callback if possible. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @return True if data can continue to be read. False if the end of the input was encountered. + * @throws ParserException If parsing fails. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + boolean read(ExtractorInput input) throws IOException, InterruptedException; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java new file mode 100644 index 0000000000..d9587cd27e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -0,0 +1,2331 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; + +import android.util.Pair; +import android.util.SparseArray; +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.LongArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.HevcConfig; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +/** Extracts data from the Matroska and WebM container formats. */ +public class MatroskaExtractor implements Extractor { + + /** Factory for {@link MatroskaExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new MatroskaExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_SEEK_FOR_CUES}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_DISABLE_SEEK_FOR_CUES}) + public @interface Flags {} + /** + * Flag to disable seeking for cues. + * <p> + * Normally (i.e. when this flag is not set) the extractor will seek to the cues element if its + * position is specified in the seek head and if it's after the first cluster. Setting this flag + * disables seeking to the cues element. If the cues element is after the first cluster then the + * media is treated as being unseekable. + */ + public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1; + + private static final String TAG = "MatroskaExtractor"; + + private static final int UNSET_ENTRY_ID = -1; + + private static final int BLOCK_STATE_START = 0; + private static final int BLOCK_STATE_HEADER = 1; + private static final int BLOCK_STATE_DATA = 2; + + private static final String DOC_TYPE_MATROSKA = "matroska"; + private static final String DOC_TYPE_WEBM = "webm"; + private static final String CODEC_ID_VP8 = "V_VP8"; + private static final String CODEC_ID_VP9 = "V_VP9"; + private static final String CODEC_ID_AV1 = "V_AV1"; + private static final String CODEC_ID_MPEG2 = "V_MPEG2"; + private static final String CODEC_ID_MPEG4_SP = "V_MPEG4/ISO/SP"; + private static final String CODEC_ID_MPEG4_ASP = "V_MPEG4/ISO/ASP"; + private static final String CODEC_ID_MPEG4_AP = "V_MPEG4/ISO/AP"; + private static final String CODEC_ID_H264 = "V_MPEG4/ISO/AVC"; + private static final String CODEC_ID_H265 = "V_MPEGH/ISO/HEVC"; + private static final String CODEC_ID_FOURCC = "V_MS/VFW/FOURCC"; + private static final String CODEC_ID_THEORA = "V_THEORA"; + private static final String CODEC_ID_VORBIS = "A_VORBIS"; + private static final String CODEC_ID_OPUS = "A_OPUS"; + private static final String CODEC_ID_AAC = "A_AAC"; + private static final String CODEC_ID_MP2 = "A_MPEG/L2"; + private static final String CODEC_ID_MP3 = "A_MPEG/L3"; + private static final String CODEC_ID_AC3 = "A_AC3"; + private static final String CODEC_ID_E_AC3 = "A_EAC3"; + private static final String CODEC_ID_TRUEHD = "A_TRUEHD"; + private static final String CODEC_ID_DTS = "A_DTS"; + private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS"; + private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS"; + private static final String CODEC_ID_FLAC = "A_FLAC"; + private static final String CODEC_ID_ACM = "A_MS/ACM"; + private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT"; + private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; + private static final String CODEC_ID_ASS = "S_TEXT/ASS"; + private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; + private static final String CODEC_ID_PGS = "S_HDMV/PGS"; + private static final String CODEC_ID_DVBSUB = "S_DVBSUB"; + + private static final int VORBIS_MAX_INPUT_SIZE = 8192; + private static final int OPUS_MAX_INPUT_SIZE = 5760; + private static final int ENCRYPTION_IV_SIZE = 8; + private static final int TRACK_TYPE_AUDIO = 2; + + private static final int ID_EBML = 0x1A45DFA3; + private static final int ID_EBML_READ_VERSION = 0x42F7; + private static final int ID_DOC_TYPE = 0x4282; + private static final int ID_DOC_TYPE_READ_VERSION = 0x4285; + private static final int ID_SEGMENT = 0x18538067; + private static final int ID_SEGMENT_INFO = 0x1549A966; + private static final int ID_SEEK_HEAD = 0x114D9B74; + private static final int ID_SEEK = 0x4DBB; + private static final int ID_SEEK_ID = 0x53AB; + private static final int ID_SEEK_POSITION = 0x53AC; + private static final int ID_INFO = 0x1549A966; + private static final int ID_TIMECODE_SCALE = 0x2AD7B1; + private static final int ID_DURATION = 0x4489; + private static final int ID_CLUSTER = 0x1F43B675; + private static final int ID_TIME_CODE = 0xE7; + private static final int ID_SIMPLE_BLOCK = 0xA3; + private static final int ID_BLOCK_GROUP = 0xA0; + private static final int ID_BLOCK = 0xA1; + private static final int ID_BLOCK_DURATION = 0x9B; + private static final int ID_BLOCK_ADDITIONS = 0x75A1; + private static final int ID_BLOCK_MORE = 0xA6; + private static final int ID_BLOCK_ADD_ID = 0xEE; + private static final int ID_BLOCK_ADDITIONAL = 0xA5; + private static final int ID_REFERENCE_BLOCK = 0xFB; + private static final int ID_TRACKS = 0x1654AE6B; + private static final int ID_TRACK_ENTRY = 0xAE; + private static final int ID_TRACK_NUMBER = 0xD7; + private static final int ID_TRACK_TYPE = 0x83; + private static final int ID_FLAG_DEFAULT = 0x88; + private static final int ID_FLAG_FORCED = 0x55AA; + private static final int ID_DEFAULT_DURATION = 0x23E383; + private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE; + private static final int ID_NAME = 0x536E; + private static final int ID_CODEC_ID = 0x86; + private static final int ID_CODEC_PRIVATE = 0x63A2; + private static final int ID_CODEC_DELAY = 0x56AA; + private static final int ID_SEEK_PRE_ROLL = 0x56BB; + private static final int ID_VIDEO = 0xE0; + private static final int ID_PIXEL_WIDTH = 0xB0; + private static final int ID_PIXEL_HEIGHT = 0xBA; + private static final int ID_DISPLAY_WIDTH = 0x54B0; + private static final int ID_DISPLAY_HEIGHT = 0x54BA; + private static final int ID_DISPLAY_UNIT = 0x54B2; + private static final int ID_AUDIO = 0xE1; + private static final int ID_CHANNELS = 0x9F; + private static final int ID_AUDIO_BIT_DEPTH = 0x6264; + private static final int ID_SAMPLING_FREQUENCY = 0xB5; + private static final int ID_CONTENT_ENCODINGS = 0x6D80; + private static final int ID_CONTENT_ENCODING = 0x6240; + private static final int ID_CONTENT_ENCODING_ORDER = 0x5031; + private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032; + private static final int ID_CONTENT_COMPRESSION = 0x5034; + private static final int ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254; + private static final int ID_CONTENT_COMPRESSION_SETTINGS = 0x4255; + private static final int ID_CONTENT_ENCRYPTION = 0x5035; + private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1; + private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2; + private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7; + private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8; + private static final int ID_CUES = 0x1C53BB6B; + private static final int ID_CUE_POINT = 0xBB; + private static final int ID_CUE_TIME = 0xB3; + private static final int ID_CUE_TRACK_POSITIONS = 0xB7; + private static final int ID_CUE_CLUSTER_POSITION = 0xF1; + private static final int ID_LANGUAGE = 0x22B59C; + private static final int ID_PROJECTION = 0x7670; + private static final int ID_PROJECTION_TYPE = 0x7671; + private static final int ID_PROJECTION_PRIVATE = 0x7672; + private static final int ID_PROJECTION_POSE_YAW = 0x7673; + private static final int ID_PROJECTION_POSE_PITCH = 0x7674; + private static final int ID_PROJECTION_POSE_ROLL = 0x7675; + private static final int ID_STEREO_MODE = 0x53B8; + private static final int ID_COLOUR = 0x55B0; + private static final int ID_COLOUR_RANGE = 0x55B9; + private static final int ID_COLOUR_TRANSFER = 0x55BA; + private static final int ID_COLOUR_PRIMARIES = 0x55BB; + private static final int ID_MAX_CLL = 0x55BC; + private static final int ID_MAX_FALL = 0x55BD; + private static final int ID_MASTERING_METADATA = 0x55D0; + private static final int ID_PRIMARY_R_CHROMATICITY_X = 0x55D1; + private static final int ID_PRIMARY_R_CHROMATICITY_Y = 0x55D2; + private static final int ID_PRIMARY_G_CHROMATICITY_X = 0x55D3; + private static final int ID_PRIMARY_G_CHROMATICITY_Y = 0x55D4; + private static final int ID_PRIMARY_B_CHROMATICITY_X = 0x55D5; + private static final int ID_PRIMARY_B_CHROMATICITY_Y = 0x55D6; + private static final int ID_WHITE_POINT_CHROMATICITY_X = 0x55D7; + private static final int ID_WHITE_POINT_CHROMATICITY_Y = 0x55D8; + private static final int ID_LUMNINANCE_MAX = 0x55D9; + private static final int ID_LUMNINANCE_MIN = 0x55DA; + + /** + * BlockAddID value for ITU T.35 metadata in a VP9 track. See also + * https://www.webmproject.org/docs/container/. + */ + private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4; + + private static final int LACING_NONE = 0; + private static final int LACING_XIPH = 1; + private static final int LACING_FIXED_SIZE = 2; + private static final int LACING_EBML = 3; + + private static final int FOURCC_COMPRESSION_DIVX = 0x58564944; + private static final int FOURCC_COMPRESSION_H263 = 0x33363248; + private static final int FOURCC_COMPRESSION_VC1 = 0x31435657; + + /** + * A template for the prefix that must be added to each subrip sample. + * + * <p>The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at + * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with + * the duration of the subtitle. + * + * <p>Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n". + */ + private static final byte[] SUBRIP_PREFIX = + new byte[] { + 49, 10, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, + 48, 58, 48, 48, 44, 48, 48, 48, 10 + }; + /** + * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. + */ + private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19; + /** + * The value by which to divide a time in microseconds to convert it to the unit of the last value + * in a subrip timecode (milliseconds). + */ + private static final long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000; + /** + * The format of a subrip timecode. + */ + private static final String SUBRIP_TIMECODE_FORMAT = "%02d:%02d:%02d,%03d"; + + /** + * Matroska specific format line for SSA subtitles. + */ + private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, " + + "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); + /** + * A template for the prefix that must be added to each SSA sample. + * + * <p>The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at + * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with + * the duration of the subtitle. + * + * <p>Equivalent to the UTF-8 string: "Dialogue: 0:00:00:00,0:00:00:00,". + */ + private static final byte[] SSA_PREFIX = + new byte[] { + 68, 105, 97, 108, 111, 103, 117, 101, 58, 32, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, + 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44 + }; + /** + * The byte offset of the end timecode in {@link #SSA_PREFIX}. + */ + private static final int SSA_PREFIX_END_TIMECODE_OFFSET = 21; + /** + * The value by which to divide a time in microseconds to convert it to the unit of the last value + * in an SSA timecode (1/100ths of a second). + */ + private static final long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000; + /** + * The format of an SSA timecode. + */ + private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d"; + + /** + * The length in bytes of a WAVEFORMATEX structure. + */ + private static final int WAVE_FORMAT_SIZE = 18; + /** + * Format tag indicating a WAVEFORMATEXTENSIBLE structure. + */ + private static final int WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + /** + * Format tag for PCM. + */ + private static final int WAVE_FORMAT_PCM = 1; + /** + * Sub format for PCM. + */ + private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L); + + private final EbmlReader reader; + private final VarintReader varintReader; + private final SparseArray<Track> tracks; + private final boolean seekForCuesEnabled; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + private final ParsableByteArray scratch; + private final ParsableByteArray vorbisNumPageSamples; + private final ParsableByteArray seekEntryIdBytes; + private final ParsableByteArray sampleStrippedBytes; + private final ParsableByteArray subtitleSample; + private final ParsableByteArray encryptionInitializationVector; + private final ParsableByteArray encryptionSubsampleData; + private final ParsableByteArray blockAdditionalData; + private ByteBuffer encryptionSubsampleDataBuffer; + + private long segmentContentSize; + private long segmentContentPosition = C.POSITION_UNSET; + private long timecodeScale = C.TIME_UNSET; + private long durationTimecode = C.TIME_UNSET; + private long durationUs = C.TIME_UNSET; + + // The track corresponding to the current TrackEntry element, or null. + private Track currentTrack; + + // Whether a seek map has been sent to the output. + private boolean sentSeekMap; + + // Master seek entry related elements. + private int seekEntryId; + private long seekEntryPosition; + + // Cue related elements. + private boolean seekForCues; + private long cuesContentPosition = C.POSITION_UNSET; + private long seekPositionAfterBuildingCues = C.POSITION_UNSET; + private long clusterTimecodeUs = C.TIME_UNSET; + private LongArray cueTimesUs; + private LongArray cueClusterPositions; + private boolean seenClusterPositionForCurrentCuePoint; + + // Reading state. + private boolean haveOutputSample; + + // Block reading state. + private int blockState; + private long blockTimeUs; + private long blockDurationUs; + private int blockSampleIndex; + private int blockSampleCount; + private int[] blockSampleSizes; + private int blockTrackNumber; + private int blockTrackNumberLength; + @C.BufferFlags + private int blockFlags; + private int blockAdditionalId; + private boolean blockHasReferenceBlock; + + // Sample writing state. + private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; + private boolean sampleEncodingHandled; + private boolean sampleSignalByteRead; + private boolean samplePartitionCountRead; + private int samplePartitionCount; + private byte sampleSignalByte; + private boolean sampleInitializationVectorRead; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + + public MatroskaExtractor() { + this(0); + } + + public MatroskaExtractor(@Flags int flags) { + this(new DefaultEbmlReader(), flags); + } + + /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) { + this.reader = reader; + this.reader.init(new InnerEbmlProcessor()); + seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0; + varintReader = new VarintReader(); + tracks = new SparseArray<>(); + scratch = new ParsableByteArray(4); + vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array()); + seekEntryIdBytes = new ParsableByteArray(4); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + sampleStrippedBytes = new ParsableByteArray(); + subtitleSample = new ParsableByteArray(); + encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); + encryptionSubsampleData = new ParsableByteArray(); + blockAdditionalData = new ParsableByteArray(); + } + + @Override + public final boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return new Sniffer().sniff(input); + } + + @Override + public final void init(ExtractorOutput output) { + extractorOutput = output; + } + + @CallSuper + @Override + public void seek(long position, long timeUs) { + clusterTimecodeUs = C.TIME_UNSET; + blockState = BLOCK_STATE_START; + reader.reset(); + varintReader.reset(); + resetWriteSampleData(); + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).reset(); + } + } + + @Override + public final void release() { + // Do nothing + } + + @Override + public final int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + haveOutputSample = false; + boolean continueReading = true; + while (continueReading && !haveOutputSample) { + continueReading = reader.read(input); + if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) { + return Extractor.RESULT_SEEK; + } + } + if (!continueReading) { + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).outputPendingSampleMetadata(); + } + return Extractor.RESULT_END_OF_INPUT; + } + return Extractor.RESULT_CONTINUE; + } + + /** + * Maps an element ID to a corresponding type. + * + * @see EbmlProcessor#getElementType(int) + */ + @CallSuper + @EbmlProcessor.ElementType + protected int getElementType(int id) { + switch (id) { + case ID_EBML: + case ID_SEGMENT: + case ID_SEEK_HEAD: + case ID_SEEK: + case ID_INFO: + case ID_CLUSTER: + case ID_TRACKS: + case ID_TRACK_ENTRY: + case ID_AUDIO: + case ID_VIDEO: + case ID_CONTENT_ENCODINGS: + case ID_CONTENT_ENCODING: + case ID_CONTENT_COMPRESSION: + case ID_CONTENT_ENCRYPTION: + case ID_CONTENT_ENCRYPTION_AES_SETTINGS: + case ID_CUES: + case ID_CUE_POINT: + case ID_CUE_TRACK_POSITIONS: + case ID_BLOCK_GROUP: + case ID_BLOCK_ADDITIONS: + case ID_BLOCK_MORE: + case ID_PROJECTION: + case ID_COLOUR: + case ID_MASTERING_METADATA: + return EbmlProcessor.ELEMENT_TYPE_MASTER; + case ID_EBML_READ_VERSION: + case ID_DOC_TYPE_READ_VERSION: + case ID_SEEK_POSITION: + case ID_TIMECODE_SCALE: + case ID_TIME_CODE: + case ID_BLOCK_DURATION: + case ID_PIXEL_WIDTH: + case ID_PIXEL_HEIGHT: + case ID_DISPLAY_WIDTH: + case ID_DISPLAY_HEIGHT: + case ID_DISPLAY_UNIT: + case ID_TRACK_NUMBER: + case ID_TRACK_TYPE: + case ID_FLAG_DEFAULT: + case ID_FLAG_FORCED: + case ID_DEFAULT_DURATION: + case ID_MAX_BLOCK_ADDITION_ID: + case ID_CODEC_DELAY: + case ID_SEEK_PRE_ROLL: + case ID_CHANNELS: + case ID_AUDIO_BIT_DEPTH: + case ID_CONTENT_ENCODING_ORDER: + case ID_CONTENT_ENCODING_SCOPE: + case ID_CONTENT_COMPRESSION_ALGORITHM: + case ID_CONTENT_ENCRYPTION_ALGORITHM: + case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE: + case ID_CUE_TIME: + case ID_CUE_CLUSTER_POSITION: + case ID_REFERENCE_BLOCK: + case ID_STEREO_MODE: + case ID_COLOUR_RANGE: + case ID_COLOUR_TRANSFER: + case ID_COLOUR_PRIMARIES: + case ID_MAX_CLL: + case ID_MAX_FALL: + case ID_PROJECTION_TYPE: + case ID_BLOCK_ADD_ID: + return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT; + case ID_DOC_TYPE: + case ID_NAME: + case ID_CODEC_ID: + case ID_LANGUAGE: + return EbmlProcessor.ELEMENT_TYPE_STRING; + case ID_SEEK_ID: + case ID_CONTENT_COMPRESSION_SETTINGS: + case ID_CONTENT_ENCRYPTION_KEY_ID: + case ID_SIMPLE_BLOCK: + case ID_BLOCK: + case ID_CODEC_PRIVATE: + case ID_PROJECTION_PRIVATE: + case ID_BLOCK_ADDITIONAL: + return EbmlProcessor.ELEMENT_TYPE_BINARY; + case ID_DURATION: + case ID_SAMPLING_FREQUENCY: + case ID_PRIMARY_R_CHROMATICITY_X: + case ID_PRIMARY_R_CHROMATICITY_Y: + case ID_PRIMARY_G_CHROMATICITY_X: + case ID_PRIMARY_G_CHROMATICITY_Y: + case ID_PRIMARY_B_CHROMATICITY_X: + case ID_PRIMARY_B_CHROMATICITY_Y: + case ID_WHITE_POINT_CHROMATICITY_X: + case ID_WHITE_POINT_CHROMATICITY_Y: + case ID_LUMNINANCE_MAX: + case ID_LUMNINANCE_MIN: + case ID_PROJECTION_POSE_YAW: + case ID_PROJECTION_POSE_PITCH: + case ID_PROJECTION_POSE_ROLL: + return EbmlProcessor.ELEMENT_TYPE_FLOAT; + default: + return EbmlProcessor.ELEMENT_TYPE_UNKNOWN; + } + } + + /** + * Checks if the given id is that of a level 1 element. + * + * @see EbmlProcessor#isLevel1Element(int) + */ + @CallSuper + protected boolean isLevel1Element(int id) { + return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS; + } + + /** + * Called when the start of a master element is encountered. + * + * @see EbmlProcessor#startMasterElement(int, long, long) + */ + @CallSuper + protected void startMasterElement(int id, long contentPosition, long contentSize) + throws ParserException { + switch (id) { + case ID_SEGMENT: + if (segmentContentPosition != C.POSITION_UNSET + && segmentContentPosition != contentPosition) { + throw new ParserException("Multiple Segment elements not supported"); + } + segmentContentPosition = contentPosition; + segmentContentSize = contentSize; + break; + case ID_SEEK: + seekEntryId = UNSET_ENTRY_ID; + seekEntryPosition = C.POSITION_UNSET; + break; + case ID_CUES: + cueTimesUs = new LongArray(); + cueClusterPositions = new LongArray(); + break; + case ID_CUE_POINT: + seenClusterPositionForCurrentCuePoint = false; + break; + case ID_CLUSTER: + if (!sentSeekMap) { + // We need to build cues before parsing the cluster. + if (seekForCuesEnabled && cuesContentPosition != C.POSITION_UNSET) { + // We know where the Cues element is located. Seek to request it. + seekForCues = true; + } else { + // We don't know where the Cues element is located. It's most likely omitted. Allow + // playback, but disable seeking. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + sentSeekMap = true; + } + } + break; + case ID_BLOCK_GROUP: + blockHasReferenceBlock = false; + break; + case ID_CONTENT_ENCODING: + // TODO: check and fail if more than one content encoding is present. + break; + case ID_CONTENT_ENCRYPTION: + currentTrack.hasContentEncryption = true; + break; + case ID_TRACK_ENTRY: + currentTrack = new Track(); + break; + case ID_MASTERING_METADATA: + currentTrack.hasColorInfo = true; + break; + default: + break; + } + } + + /** + * Called when the end of a master element is encountered. + * + * @see EbmlProcessor#endMasterElement(int) + */ + @CallSuper + protected void endMasterElement(int id) throws ParserException { + switch (id) { + case ID_SEGMENT_INFO: + if (timecodeScale == C.TIME_UNSET) { + // timecodeScale was omitted. Use the default value. + timecodeScale = 1000000; + } + if (durationTimecode != C.TIME_UNSET) { + durationUs = scaleTimecodeToUs(durationTimecode); + } + break; + case ID_SEEK: + if (seekEntryId == UNSET_ENTRY_ID || seekEntryPosition == C.POSITION_UNSET) { + throw new ParserException("Mandatory element SeekID or SeekPosition not found"); + } + if (seekEntryId == ID_CUES) { + cuesContentPosition = seekEntryPosition; + } + break; + case ID_CUES: + if (!sentSeekMap) { + extractorOutput.seekMap(buildSeekMap()); + sentSeekMap = true; + } else { + // We have already built the cues. Ignore. + } + break; + case ID_BLOCK_GROUP: + if (blockState != BLOCK_STATE_DATA) { + // We've skipped this block (due to incompatible track number). + return; + } + // Commit sample metadata. + int sampleOffset = 0; + for (int i = 0; i < blockSampleCount; i++) { + sampleOffset += blockSampleSizes[i]; + } + Track track = tracks.get(blockTrackNumber); + for (int i = 0; i < blockSampleCount; i++) { + long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000; + int sampleFlags = blockFlags; + if (i == 0 && !blockHasReferenceBlock) { + // If the ReferenceBlock element was not found in this block, then the first frame is a + // keyframe. + sampleFlags |= C.BUFFER_FLAG_KEY_FRAME; + } + int sampleSize = blockSampleSizes[i]; + sampleOffset -= sampleSize; // The offset is to the end of the sample. + commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset); + } + blockState = BLOCK_STATE_START; + break; + case ID_CONTENT_ENCODING: + if (currentTrack.hasContentEncryption) { + if (currentTrack.cryptoData == null) { + throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); + } + currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, + MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey)); + } + break; + case ID_CONTENT_ENCODINGS: + if (currentTrack.hasContentEncryption && currentTrack.sampleStrippedBytes != null) { + throw new ParserException("Combining encryption and compression is not supported"); + } + break; + case ID_TRACK_ENTRY: + if (isCodecSupported(currentTrack.codecId)) { + currentTrack.initializeOutput(extractorOutput, currentTrack.number); + tracks.put(currentTrack.number, currentTrack); + } + currentTrack = null; + break; + case ID_TRACKS: + if (tracks.size() == 0) { + throw new ParserException("No valid tracks were found"); + } + extractorOutput.endTracks(); + break; + default: + break; + } + } + + /** + * Called when an integer element is encountered. + * + * @see EbmlProcessor#integerElement(int, long) + */ + @CallSuper + protected void integerElement(int id, long value) throws ParserException { + switch (id) { + case ID_EBML_READ_VERSION: + // Validate that EBMLReadVersion is supported. This extractor only supports v1. + if (value != 1) { + throw new ParserException("EBMLReadVersion " + value + " not supported"); + } + break; + case ID_DOC_TYPE_READ_VERSION: + // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2. + if (value < 1 || value > 2) { + throw new ParserException("DocTypeReadVersion " + value + " not supported"); + } + break; + case ID_SEEK_POSITION: + // Seek Position is the relative offset beginning from the Segment. So to get absolute + // offset from the beginning of the file, we need to add segmentContentPosition to it. + seekEntryPosition = value + segmentContentPosition; + break; + case ID_TIMECODE_SCALE: + timecodeScale = value; + break; + case ID_PIXEL_WIDTH: + currentTrack.width = (int) value; + break; + case ID_PIXEL_HEIGHT: + currentTrack.height = (int) value; + break; + case ID_DISPLAY_WIDTH: + currentTrack.displayWidth = (int) value; + break; + case ID_DISPLAY_HEIGHT: + currentTrack.displayHeight = (int) value; + break; + case ID_DISPLAY_UNIT: + currentTrack.displayUnit = (int) value; + break; + case ID_TRACK_NUMBER: + currentTrack.number = (int) value; + break; + case ID_FLAG_DEFAULT: + currentTrack.flagDefault = value == 1; + break; + case ID_FLAG_FORCED: + currentTrack.flagForced = value == 1; + break; + case ID_TRACK_TYPE: + currentTrack.type = (int) value; + break; + case ID_DEFAULT_DURATION: + currentTrack.defaultSampleDurationNs = (int) value; + break; + case ID_MAX_BLOCK_ADDITION_ID: + currentTrack.maxBlockAdditionId = (int) value; + break; + case ID_CODEC_DELAY: + currentTrack.codecDelayNs = value; + break; + case ID_SEEK_PRE_ROLL: + currentTrack.seekPreRollNs = value; + break; + case ID_CHANNELS: + currentTrack.channelCount = (int) value; + break; + case ID_AUDIO_BIT_DEPTH: + currentTrack.audioBitDepth = (int) value; + break; + case ID_REFERENCE_BLOCK: + blockHasReferenceBlock = true; + break; + case ID_CONTENT_ENCODING_ORDER: + // This extractor only supports one ContentEncoding element and hence the order has to be 0. + if (value != 0) { + throw new ParserException("ContentEncodingOrder " + value + " not supported"); + } + break; + case ID_CONTENT_ENCODING_SCOPE: + // This extractor only supports the scope of all frames. + if (value != 1) { + throw new ParserException("ContentEncodingScope " + value + " not supported"); + } + break; + case ID_CONTENT_COMPRESSION_ALGORITHM: + // This extractor only supports header stripping. + if (value != 3) { + throw new ParserException("ContentCompAlgo " + value + " not supported"); + } + break; + case ID_CONTENT_ENCRYPTION_ALGORITHM: + // Only the value 5 (AES) is allowed according to the WebM specification. + if (value != 5) { + throw new ParserException("ContentEncAlgo " + value + " not supported"); + } + break; + case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE: + // Only the value 1 is allowed according to the WebM specification. + if (value != 1) { + throw new ParserException("AESSettingsCipherMode " + value + " not supported"); + } + break; + case ID_CUE_TIME: + cueTimesUs.add(scaleTimecodeToUs(value)); + break; + case ID_CUE_CLUSTER_POSITION: + if (!seenClusterPositionForCurrentCuePoint) { + // If there's more than one video/audio track, then there could be more than one + // CueTrackPositions within a single CuePoint. In such a case, ignore all but the first + // one (since the cluster position will be quite close for all the tracks). + cueClusterPositions.add(value); + seenClusterPositionForCurrentCuePoint = true; + } + break; + case ID_TIME_CODE: + clusterTimecodeUs = scaleTimecodeToUs(value); + break; + case ID_BLOCK_DURATION: + blockDurationUs = scaleTimecodeToUs(value); + break; + case ID_STEREO_MODE: + int layout = (int) value; + switch (layout) { + case 0: + currentTrack.stereoMode = C.STEREO_MODE_MONO; + break; + case 1: + currentTrack.stereoMode = C.STEREO_MODE_LEFT_RIGHT; + break; + case 3: + currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM; + break; + case 15: + currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH; + break; + default: + break; + } + break; + case ID_COLOUR_PRIMARIES: + currentTrack.hasColorInfo = true; + switch ((int) value) { + case 1: + currentTrack.colorSpace = C.COLOR_SPACE_BT709; + break; + case 4: // BT.470M. + case 5: // BT.470BG. + case 6: // SMPTE 170M. + case 7: // SMPTE 240M. + currentTrack.colorSpace = C.COLOR_SPACE_BT601; + break; + case 9: + currentTrack.colorSpace = C.COLOR_SPACE_BT2020; + break; + default: + break; + } + break; + case ID_COLOUR_TRANSFER: + switch ((int) value) { + case 1: // BT.709. + case 6: // SMPTE 170M. + case 7: // SMPTE 240M. + currentTrack.colorTransfer = C.COLOR_TRANSFER_SDR; + break; + case 16: + currentTrack.colorTransfer = C.COLOR_TRANSFER_ST2084; + break; + case 18: + currentTrack.colorTransfer = C.COLOR_TRANSFER_HLG; + break; + default: + break; + } + break; + case ID_COLOUR_RANGE: + switch((int) value) { + case 1: // Broadcast range. + currentTrack.colorRange = C.COLOR_RANGE_LIMITED; + break; + case 2: + currentTrack.colorRange = C.COLOR_RANGE_FULL; + break; + default: + break; + } + break; + case ID_MAX_CLL: + currentTrack.maxContentLuminance = (int) value; + break; + case ID_MAX_FALL: + currentTrack.maxFrameAverageLuminance = (int) value; + break; + case ID_PROJECTION_TYPE: + switch ((int) value) { + case 0: + currentTrack.projectionType = C.PROJECTION_RECTANGULAR; + break; + case 1: + currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR; + break; + case 2: + currentTrack.projectionType = C.PROJECTION_CUBEMAP; + break; + case 3: + currentTrack.projectionType = C.PROJECTION_MESH; + break; + default: + break; + } + break; + case ID_BLOCK_ADD_ID: + blockAdditionalId = (int) value; + break; + default: + break; + } + } + + /** + * Called when a float element is encountered. + * + * @see EbmlProcessor#floatElement(int, double) + */ + @CallSuper + protected void floatElement(int id, double value) throws ParserException { + switch (id) { + case ID_DURATION: + durationTimecode = (long) value; + break; + case ID_SAMPLING_FREQUENCY: + currentTrack.sampleRate = (int) value; + break; + case ID_PRIMARY_R_CHROMATICITY_X: + currentTrack.primaryRChromaticityX = (float) value; + break; + case ID_PRIMARY_R_CHROMATICITY_Y: + currentTrack.primaryRChromaticityY = (float) value; + break; + case ID_PRIMARY_G_CHROMATICITY_X: + currentTrack.primaryGChromaticityX = (float) value; + break; + case ID_PRIMARY_G_CHROMATICITY_Y: + currentTrack.primaryGChromaticityY = (float) value; + break; + case ID_PRIMARY_B_CHROMATICITY_X: + currentTrack.primaryBChromaticityX = (float) value; + break; + case ID_PRIMARY_B_CHROMATICITY_Y: + currentTrack.primaryBChromaticityY = (float) value; + break; + case ID_WHITE_POINT_CHROMATICITY_X: + currentTrack.whitePointChromaticityX = (float) value; + break; + case ID_WHITE_POINT_CHROMATICITY_Y: + currentTrack.whitePointChromaticityY = (float) value; + break; + case ID_LUMNINANCE_MAX: + currentTrack.maxMasteringLuminance = (float) value; + break; + case ID_LUMNINANCE_MIN: + currentTrack.minMasteringLuminance = (float) value; + break; + case ID_PROJECTION_POSE_YAW: + currentTrack.projectionPoseYaw = (float) value; + break; + case ID_PROJECTION_POSE_PITCH: + currentTrack.projectionPosePitch = (float) value; + break; + case ID_PROJECTION_POSE_ROLL: + currentTrack.projectionPoseRoll = (float) value; + break; + default: + break; + } + } + + /** + * Called when a string element is encountered. + * + * @see EbmlProcessor#stringElement(int, String) + */ + @CallSuper + protected void stringElement(int id, String value) throws ParserException { + switch (id) { + case ID_DOC_TYPE: + // Validate that DocType is supported. + if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) { + throw new ParserException("DocType " + value + " not supported"); + } + break; + case ID_NAME: + currentTrack.name = value; + break; + case ID_CODEC_ID: + currentTrack.codecId = value; + break; + case ID_LANGUAGE: + currentTrack.language = value; + break; + default: + break; + } + } + + /** + * Called when a binary element is encountered. + * + * @see EbmlProcessor#binaryElement(int, int, ExtractorInput) + */ + @CallSuper + protected void binaryElement(int id, int contentSize, ExtractorInput input) + throws IOException, InterruptedException { + switch (id) { + case ID_SEEK_ID: + Arrays.fill(seekEntryIdBytes.data, (byte) 0); + input.readFully(seekEntryIdBytes.data, 4 - contentSize, contentSize); + seekEntryIdBytes.setPosition(0); + seekEntryId = (int) seekEntryIdBytes.readUnsignedInt(); + break; + case ID_CODEC_PRIVATE: + currentTrack.codecPrivate = new byte[contentSize]; + input.readFully(currentTrack.codecPrivate, 0, contentSize); + break; + case ID_PROJECTION_PRIVATE: + currentTrack.projectionData = new byte[contentSize]; + input.readFully(currentTrack.projectionData, 0, contentSize); + break; + case ID_CONTENT_COMPRESSION_SETTINGS: + // This extractor only supports header stripping, so the payload is the stripped bytes. + currentTrack.sampleStrippedBytes = new byte[contentSize]; + input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize); + break; + case ID_CONTENT_ENCRYPTION_KEY_ID: + byte[] encryptionKey = new byte[contentSize]; + input.readFully(encryptionKey, 0, contentSize); + currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey, + 0, 0); // We assume patternless AES-CTR. + break; + case ID_SIMPLE_BLOCK: + case ID_BLOCK: + // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure + // and http://matroska.org/technical/specs/index.html#block_structure + // for info about how data is organized in SimpleBlock and Block elements respectively. They + // differ only in the way flags are specified. + + if (blockState == BLOCK_STATE_START) { + blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true, 8); + blockTrackNumberLength = varintReader.getLastLength(); + blockDurationUs = C.TIME_UNSET; + blockState = BLOCK_STATE_HEADER; + scratch.reset(); + } + + Track track = tracks.get(blockTrackNumber); + + // Ignore the block if we don't know about the track to which it belongs. + if (track == null) { + input.skipFully(contentSize - blockTrackNumberLength); + blockState = BLOCK_STATE_START; + return; + } + + if (blockState == BLOCK_STATE_HEADER) { + // Read the relative timecode (2 bytes) and flags (1 byte). + readScratch(input, 3); + int lacing = (scratch.data[2] & 0x06) >> 1; + if (lacing == LACING_NONE) { + blockSampleCount = 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); + blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3; + } else { + // Read the sample count (1 byte). + readScratch(input, 4); + blockSampleCount = (scratch.data[3] & 0xFF) + 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount); + if (lacing == LACING_FIXED_SIZE) { + int blockLacingSampleSize = + (contentSize - blockTrackNumberLength - 4) / blockSampleCount; + Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize); + } else if (lacing == LACING_XIPH) { + int totalSamplesSize = 0; + int headerSize = 4; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; + int byteValue; + do { + readScratch(input, ++headerSize); + byteValue = scratch.data[headerSize - 1] & 0xFF; + blockSampleSizes[sampleIndex] += byteValue; + } while (byteValue == 0xFF); + totalSamplesSize += blockSampleSizes[sampleIndex]; + } + blockSampleSizes[blockSampleCount - 1] = + contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; + } else if (lacing == LACING_EBML) { + int totalSamplesSize = 0; + int headerSize = 4; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; + readScratch(input, ++headerSize); + if (scratch.data[headerSize - 1] == 0) { + throw new ParserException("No valid varint length mask found"); + } + long readValue = 0; + for (int i = 0; i < 8; i++) { + int lengthMask = 1 << (7 - i); + if ((scratch.data[headerSize - 1] & lengthMask) != 0) { + int readPosition = headerSize - 1; + headerSize += i; + readScratch(input, headerSize); + readValue = (scratch.data[readPosition++] & 0xFF) & ~lengthMask; + while (readPosition < headerSize) { + readValue <<= 8; + readValue |= (scratch.data[readPosition++] & 0xFF); + } + // The first read value is the first size. Later values are signed offsets. + if (sampleIndex > 0) { + readValue -= (1L << (6 + i * 7)) - 1; + } + break; + } + } + if (readValue < Integer.MIN_VALUE || readValue > Integer.MAX_VALUE) { + throw new ParserException("EBML lacing sample size out of range."); + } + int intReadValue = (int) readValue; + blockSampleSizes[sampleIndex] = + sampleIndex == 0 + ? intReadValue + : blockSampleSizes[sampleIndex - 1] + intReadValue; + totalSamplesSize += blockSampleSizes[sampleIndex]; + } + blockSampleSizes[blockSampleCount - 1] = + contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; + } else { + // Lacing is always in the range 0--3. + throw new ParserException("Unexpected lacing value: " + lacing); + } + } + + int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF); + blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode); + boolean isInvisible = (scratch.data[2] & 0x08) == 0x08; + boolean isKeyframe = track.type == TRACK_TYPE_AUDIO + || (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80); + blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0) + | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0); + blockState = BLOCK_STATE_DATA; + blockSampleIndex = 0; + } + + if (id == ID_SIMPLE_BLOCK) { + // For SimpleBlock, we can write sample data and immediately commit the corresponding + // sample metadata. + while (blockSampleIndex < blockSampleCount) { + int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + long sampleTimeUs = + blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; + commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0); + blockSampleIndex++; + } + blockState = BLOCK_STATE_START; + } else { + // For Block, we need to wait until the end of the BlockGroup element before committing + // sample metadata. This is so that we can handle ReferenceBlock (which can be used to + // infer whether the first sample in the block is a keyframe), and BlockAdditions (which + // can contain additional sample data to append) contained in the block group. Just output + // the sample data, storing the final sample sizes for when we commit the metadata. + while (blockSampleIndex < blockSampleCount) { + blockSampleSizes[blockSampleIndex] = + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + blockSampleIndex++; + } + } + + break; + case ID_BLOCK_ADDITIONAL: + if (blockState != BLOCK_STATE_DATA) { + return; + } + handleBlockAdditionalData( + tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize); + break; + default: + throw new ParserException("Unexpected id: " + id); + } + } + + protected void handleBlockAdditionalData( + Track track, int blockAdditionalId, ExtractorInput input, int contentSize) + throws IOException, InterruptedException { + if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 + && CODEC_ID_VP9.equals(track.codecId)) { + blockAdditionalData.reset(contentSize); + input.readFully(blockAdditionalData.data, 0, contentSize); + } else { + // Unhandled block additional data. + input.skipFully(contentSize); + } + } + + private void commitSampleToOutput( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (track.trueHdSampleRechunker != null) { + track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); + } else { + if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { + if (blockSampleCount > 1) { + Log.w(TAG, "Skipping subtitle sample in laced block."); + } else if (blockDurationUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping subtitle sample with no duration."); + } else { + setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data); + // Note: If we ever want to support DRM protected subtitles then we'll need to output the + // appropriate encryption data here. + track.output.sampleData(subtitleSample, subtitleSample.limit()); + size += subtitleSample.limit(); + } + } + + if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { + if (blockSampleCount > 1) { + // There were multiple samples in the block. Appending the additional data to the last + // sample doesn't make sense. Skip instead. + flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + } else { + // Append supplemental data. + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData(blockAdditionalData, blockAdditionalSize); + size += blockAdditionalSize; + } + } + track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); + } + haveOutputSample = true; + } + + /** + * Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from + * the extractor input if necessary. + */ + private void readScratch(ExtractorInput input, int requiredLength) + throws IOException, InterruptedException { + if (scratch.limit() >= requiredLength) { + return; + } + if (scratch.capacity() < requiredLength) { + scratch.reset(Arrays.copyOf(scratch.data, Math.max(scratch.data.length * 2, requiredLength)), + scratch.limit()); + } + input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit()); + scratch.setLimit(requiredLength); + } + + /** + * Writes data for a single sample to the track output. + * + * @param input The input from which to read sample data. + * @param track The track to output the sample to. + * @param size The size of the sample data on the input side. + * @return The final size of the written sample. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int writeSampleData(ExtractorInput input, Track track, int size) + throws IOException, InterruptedException { + if (CODEC_ID_SUBRIP.equals(track.codecId)) { + writeSubtitleSampleData(input, SUBRIP_PREFIX, size); + return finishWriteSampleData(); + } else if (CODEC_ID_ASS.equals(track.codecId)) { + writeSubtitleSampleData(input, SSA_PREFIX, size); + return finishWriteSampleData(); + } + + TrackOutput output = track.output; + if (!sampleEncodingHandled) { + if (track.hasContentEncryption) { + // If the sample is encrypted, read its encryption signal byte and set the IV size. + // Clear the encrypted flag. + blockFlags &= ~C.BUFFER_FLAG_ENCRYPTED; + if (!sampleSignalByteRead) { + input.readFully(scratch.data, 0, 1); + sampleBytesRead++; + if ((scratch.data[0] & 0x80) == 0x80) { + throw new ParserException("Extension bit is set in signal byte"); + } + sampleSignalByte = scratch.data[0]; + sampleSignalByteRead = true; + } + boolean isEncrypted = (sampleSignalByte & 0x01) == 0x01; + if (isEncrypted) { + boolean hasSubsampleEncryption = (sampleSignalByte & 0x02) == 0x02; + blockFlags |= C.BUFFER_FLAG_ENCRYPTED; + if (!sampleInitializationVectorRead) { + input.readFully(encryptionInitializationVector.data, 0, ENCRYPTION_IV_SIZE); + sampleBytesRead += ENCRYPTION_IV_SIZE; + sampleInitializationVectorRead = true; + // Write the signal byte, containing the IV size and the subsample encryption flag. + scratch.data[0] = (byte) (ENCRYPTION_IV_SIZE | (hasSubsampleEncryption ? 0x80 : 0x00)); + scratch.setPosition(0); + output.sampleData(scratch, 1); + sampleBytesWritten++; + // Write the IV. + encryptionInitializationVector.setPosition(0); + output.sampleData(encryptionInitializationVector, ENCRYPTION_IV_SIZE); + sampleBytesWritten += ENCRYPTION_IV_SIZE; + } + if (hasSubsampleEncryption) { + if (!samplePartitionCountRead) { + input.readFully(scratch.data, 0, 1); + sampleBytesRead++; + scratch.setPosition(0); + samplePartitionCount = scratch.readUnsignedByte(); + samplePartitionCountRead = true; + } + int samplePartitionDataSize = samplePartitionCount * 4; + scratch.reset(samplePartitionDataSize); + input.readFully(scratch.data, 0, samplePartitionDataSize); + sampleBytesRead += samplePartitionDataSize; + short subsampleCount = (short) (1 + (samplePartitionCount / 2)); + int subsampleDataSize = 2 + 6 * subsampleCount; + if (encryptionSubsampleDataBuffer == null + || encryptionSubsampleDataBuffer.capacity() < subsampleDataSize) { + encryptionSubsampleDataBuffer = ByteBuffer.allocate(subsampleDataSize); + } + encryptionSubsampleDataBuffer.position(0); + encryptionSubsampleDataBuffer.putShort(subsampleCount); + // Loop through the partition offsets and write out the data in the way ExoPlayer + // wants it (ISO 23001-7 Part 7): + // 2 bytes - sub sample count. + // for each sub sample: + // 2 bytes - clear data size. + // 4 bytes - encrypted data size. + int partitionOffset = 0; + for (int i = 0; i < samplePartitionCount; i++) { + int previousPartitionOffset = partitionOffset; + partitionOffset = scratch.readUnsignedIntToInt(); + if ((i % 2) == 0) { + encryptionSubsampleDataBuffer.putShort( + (short) (partitionOffset - previousPartitionOffset)); + } else { + encryptionSubsampleDataBuffer.putInt(partitionOffset - previousPartitionOffset); + } + } + int finalPartitionSize = size - sampleBytesRead - partitionOffset; + if ((samplePartitionCount % 2) == 1) { + encryptionSubsampleDataBuffer.putInt(finalPartitionSize); + } else { + encryptionSubsampleDataBuffer.putShort((short) finalPartitionSize); + encryptionSubsampleDataBuffer.putInt(0); + } + encryptionSubsampleData.reset(encryptionSubsampleDataBuffer.array(), subsampleDataSize); + output.sampleData(encryptionSubsampleData, subsampleDataSize); + sampleBytesWritten += subsampleDataSize; + } + } + } else if (track.sampleStrippedBytes != null) { + // If the sample has header stripping, prepare to read/output the stripped bytes first. + sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length); + } + + if (track.maxBlockAdditionId > 0) { + blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + blockAdditionalData.reset(); + // If there is supplemental data, the structure of the sample data is: + // sample size (4 bytes) || sample data || supplemental data + scratch.reset(/* limit= */ 4); + scratch.data[0] = (byte) ((size >> 24) & 0xFF); + scratch.data[1] = (byte) ((size >> 16) & 0xFF); + scratch.data[2] = (byte) ((size >> 8) & 0xFF); + scratch.data[3] = (byte) (size & 0xFF); + output.sampleData(scratch, 4); + sampleBytesWritten += 4; + } + + sampleEncodingHandled = true; + } + size += sampleStrippedBytes.limit(); + + if (CODEC_ID_H264.equals(track.codecId) || CODEC_ID_H265.equals(track.codecId)) { + // TODO: Deduplicate with Mp4Extractor. + + // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesRead < size) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one. + writeToTarget( + input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; + nalLength.setPosition(0); + sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + sampleBytesWritten += 4; + } else { + // Write the payload of the NAL unit. + int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; + sampleCurrentNalBytesRemaining -= bytesWritten; + } + } + } else { + if (track.trueHdSampleRechunker != null) { + Assertions.checkState(sampleStrippedBytes.limit() == 0); + track.trueHdSampleRechunker.startSample(input); + } + while (sampleBytesRead < size) { + int bytesWritten = writeToOutput(input, output, size - sampleBytesRead); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; + } + } + + if (CODEC_ID_VORBIS.equals(track.codecId)) { + // Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be the + // number of samples in the current page. This definition holds good only for Ogg and + // irrelevant for Matroska. So we always set this to -1 (the decoder will ignore this value if + // we set it to -1). The android platform media extractor [2] does the same. + // [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314 + // [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474 + vorbisNumPageSamples.setPosition(0); + output.sampleData(vorbisNumPageSamples, 4); + sampleBytesWritten += 4; + } + + return finishWriteSampleData(); + } + + /** + * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been + * written. Returns the final sample size and resets state for the next sample. + */ + private int finishWriteSampleData() { + int sampleSize = sampleBytesWritten; + resetWriteSampleData(); + return sampleSize; + } + + /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */ + private void resetWriteSampleData() { + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + sampleEncodingHandled = false; + sampleSignalByteRead = false; + samplePartitionCountRead = false; + samplePartitionCount = 0; + sampleSignalByte = (byte) 0; + sampleInitializationVectorRead = false; + sampleStrippedBytes.reset(); + } + + private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size) + throws IOException, InterruptedException { + int sizeWithPrefix = samplePrefix.length + size; + if (subtitleSample.capacity() < sizeWithPrefix) { + // Initialize subripSample to contain the required prefix and have space to hold a subtitle + // twice as long as this one. + subtitleSample.data = Arrays.copyOf(samplePrefix, sizeWithPrefix + size); + } else { + System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.length); + } + input.readFully(subtitleSample.data, samplePrefix.length, size); + subtitleSample.reset(sizeWithPrefix); + // Defer writing the data to the track output. We need to modify the sample data by setting + // the correct end timecode, which we might not have yet. + } + + /** + * Overwrites the end timecode in {@code subtitleData} with the correctly formatted time derived + * from {@code durationUs}. + * + * <p>See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use + * the duration as the end timecode. + * + * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}. + * @param durationUs The duration of the sample, in microseconds. + * @param subtitleData The subtitle sample in which to overwrite the end timecode (output + * parameter). + */ + private static void setSubtitleEndTime(String codecId, long durationUs, byte[] subtitleData) { + byte[] endTimecode; + int endTimecodeOffset; + switch (codecId) { + case CODEC_ID_SUBRIP: + endTimecode = + formatSubtitleTimecode( + durationUs, SUBRIP_TIMECODE_FORMAT, SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SUBRIP_PREFIX_END_TIMECODE_OFFSET; + break; + case CODEC_ID_ASS: + endTimecode = + formatSubtitleTimecode( + durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET; + break; + default: + throw new IllegalArgumentException(); + } + System.arraycopy(endTimecode, 0, subtitleData, endTimecodeOffset, endTimecode.length); + } + + /** + * Formats {@code timeUs} using {@code timecodeFormat}, and sets it as the end timecode in {@code + * subtitleSampleData}. + */ + private static byte[] formatSubtitleTimecode( + long timeUs, String timecodeFormat, long lastTimecodeValueScalingFactor) { + Assertions.checkArgument(timeUs != C.TIME_UNSET); + byte[] timeCodeData; + int hours = (int) (timeUs / (3600 * C.MICROS_PER_SECOND)); + timeUs -= (hours * 3600 * C.MICROS_PER_SECOND); + int minutes = (int) (timeUs / (60 * C.MICROS_PER_SECOND)); + timeUs -= (minutes * 60 * C.MICROS_PER_SECOND); + int seconds = (int) (timeUs / C.MICROS_PER_SECOND); + timeUs -= (seconds * C.MICROS_PER_SECOND); + int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor); + timeCodeData = + Util.getUtf8Bytes( + String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); + return timeCodeData; + } + + /** + * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of + * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}. + */ + private void writeToTarget(ExtractorInput input, byte[] target, int offset, int length) + throws IOException, InterruptedException { + int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft()); + input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes); + if (pendingStrippedBytes > 0) { + sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes); + } + } + + /** + * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either + * {@link #sampleStrippedBytes} or data read from {@code input}. + */ + private int writeToOutput(ExtractorInput input, TrackOutput output, int length) + throws IOException, InterruptedException { + int bytesWritten; + int strippedBytesLeft = sampleStrippedBytes.bytesLeft(); + if (strippedBytesLeft > 0) { + bytesWritten = Math.min(length, strippedBytesLeft); + output.sampleData(sampleStrippedBytes, bytesWritten); + } else { + bytesWritten = output.sampleData(input, length, false); + } + return bytesWritten; + } + + /** + * Builds a {@link SeekMap} from the recently gathered Cues information. + * + * @return The built {@link SeekMap}. The returned {@link SeekMap} may be unseekable if cues + * information was missing or incomplete. + */ + private SeekMap buildSeekMap() { + if (segmentContentPosition == C.POSITION_UNSET || durationUs == C.TIME_UNSET + || cueTimesUs == null || cueTimesUs.size() == 0 + || cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) { + // Cues information is missing or incomplete. + cueTimesUs = null; + cueClusterPositions = null; + return new SeekMap.Unseekable(durationUs); + } + int cuePointsSize = cueTimesUs.size(); + int[] sizes = new int[cuePointsSize]; + long[] offsets = new long[cuePointsSize]; + long[] durationsUs = new long[cuePointsSize]; + long[] timesUs = new long[cuePointsSize]; + for (int i = 0; i < cuePointsSize; i++) { + timesUs[i] = cueTimesUs.get(i); + offsets[i] = segmentContentPosition + cueClusterPositions.get(i); + } + for (int i = 0; i < cuePointsSize - 1; i++) { + sizes[i] = (int) (offsets[i + 1] - offsets[i]); + durationsUs[i] = timesUs[i + 1] - timesUs[i]; + } + sizes[cuePointsSize - 1] = + (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]); + durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1]; + + long lastDurationUs = durationsUs[cuePointsSize - 1]; + if (lastDurationUs <= 0) { + Log.w(TAG, "Discarding last cue point with unexpected duration: " + lastDurationUs); + sizes = Arrays.copyOf(sizes, sizes.length - 1); + offsets = Arrays.copyOf(offsets, offsets.length - 1); + durationsUs = Arrays.copyOf(durationsUs, durationsUs.length - 1); + timesUs = Arrays.copyOf(timesUs, timesUs.length - 1); + } + + cueTimesUs = null; + cueClusterPositions = null; + return new ChunkIndex(sizes, offsets, durationsUs, timesUs); + } + + /** + * Updates the position of the holder to Cues element's position if the extractor configuration + * permits use of master seek entry. After building Cues sets the holder's position back to where + * it was before. + * + * @param seekPosition The holder whose position will be updated. + * @param currentPosition Current position of the input. + * @return Whether the seek position was updated. + */ + private boolean maybeSeekForCues(PositionHolder seekPosition, long currentPosition) { + if (seekForCues) { + seekPositionAfterBuildingCues = currentPosition; + seekPosition.position = cuesContentPosition; + seekForCues = false; + return true; + } + // After parsing Cues, seek back to original position if available. We will not do this unless + // we seeked to get to the Cues in the first place. + if (sentSeekMap && seekPositionAfterBuildingCues != C.POSITION_UNSET) { + seekPosition.position = seekPositionAfterBuildingCues; + seekPositionAfterBuildingCues = C.POSITION_UNSET; + return true; + } + return false; + } + + private long scaleTimecodeToUs(long unscaledTimecode) throws ParserException { + if (timecodeScale == C.TIME_UNSET) { + throw new ParserException("Can't scale timecode prior to timecodeScale being set."); + } + return Util.scaleLargeTimestamp(unscaledTimecode, timecodeScale, 1000); + } + + private static boolean isCodecSupported(String codecId) { + return CODEC_ID_VP8.equals(codecId) + || CODEC_ID_VP9.equals(codecId) + || CODEC_ID_AV1.equals(codecId) + || CODEC_ID_MPEG2.equals(codecId) + || CODEC_ID_MPEG4_SP.equals(codecId) + || CODEC_ID_MPEG4_ASP.equals(codecId) + || CODEC_ID_MPEG4_AP.equals(codecId) + || CODEC_ID_H264.equals(codecId) + || CODEC_ID_H265.equals(codecId) + || CODEC_ID_FOURCC.equals(codecId) + || CODEC_ID_THEORA.equals(codecId) + || CODEC_ID_OPUS.equals(codecId) + || CODEC_ID_VORBIS.equals(codecId) + || CODEC_ID_AAC.equals(codecId) + || CODEC_ID_MP2.equals(codecId) + || CODEC_ID_MP3.equals(codecId) + || CODEC_ID_AC3.equals(codecId) + || CODEC_ID_E_AC3.equals(codecId) + || CODEC_ID_TRUEHD.equals(codecId) + || CODEC_ID_DTS.equals(codecId) + || CODEC_ID_DTS_EXPRESS.equals(codecId) + || CODEC_ID_DTS_LOSSLESS.equals(codecId) + || CODEC_ID_FLAC.equals(codecId) + || CODEC_ID_ACM.equals(codecId) + || CODEC_ID_PCM_INT_LIT.equals(codecId) + || CODEC_ID_SUBRIP.equals(codecId) + || CODEC_ID_ASS.equals(codecId) + || CODEC_ID_VOBSUB.equals(codecId) + || CODEC_ID_PGS.equals(codecId) + || CODEC_ID_DVBSUB.equals(codecId); + } + + /** + * Returns an array that can store (at least) {@code length} elements, which will be either a new + * array or {@code array} if it's not null and large enough. + */ + private static int[] ensureArrayCapacity(int[] array, int length) { + if (array == null) { + return new int[length]; + } else if (array.length >= length) { + return array; + } else { + // Double the size to avoid allocating constantly if the required length increases gradually. + return new int[Math.max(array.length * 2, length)]; + } + } + + /** Passes events through to the outer {@link MatroskaExtractor}. */ + private final class InnerEbmlProcessor implements EbmlProcessor { + + @Override + @ElementType + public int getElementType(int id) { + return MatroskaExtractor.this.getElementType(id); + } + + @Override + public boolean isLevel1Element(int id) { + return MatroskaExtractor.this.isLevel1Element(id); + } + + @Override + public void startMasterElement(int id, long contentPosition, long contentSize) + throws ParserException { + MatroskaExtractor.this.startMasterElement(id, contentPosition, contentSize); + } + + @Override + public void endMasterElement(int id) throws ParserException { + MatroskaExtractor.this.endMasterElement(id); + } + + @Override + public void integerElement(int id, long value) throws ParserException { + MatroskaExtractor.this.integerElement(id, value); + } + + @Override + public void floatElement(int id, double value) throws ParserException { + MatroskaExtractor.this.floatElement(id, value); + } + + @Override + public void stringElement(int id, String value) throws ParserException { + MatroskaExtractor.this.stringElement(id, value); + } + + @Override + public void binaryElement(int id, int contentsSize, ExtractorInput input) + throws IOException, InterruptedException { + MatroskaExtractor.this.binaryElement(id, contentsSize, input); + } + } + + /** + * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples. + */ + private static final class TrueHdSampleRechunker { + + private final byte[] syncframePrefix; + + private boolean foundSyncframe; + private int chunkSampleCount; + private long chunkTimeUs; + private @C.BufferFlags int chunkFlags; + private int chunkSize; + private int chunkOffset; + + public TrueHdSampleRechunker() { + syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; + } + + public void reset() { + foundSyncframe = false; + chunkSampleCount = 0; + } + + public void startSample(ExtractorInput input) throws IOException, InterruptedException { + if (foundSyncframe) { + return; + } + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { + return; + } + foundSyncframe = true; + } + + public void sampleMetadata( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (!foundSyncframe) { + return; + } + if (chunkSampleCount++ == 0) { + // This is the first sample in the chunk. + chunkTimeUs = timeUs; + chunkFlags = flags; + chunkSize = 0; + } + chunkSize += size; + chunkOffset = offset; // The offset is to the end of the sample. + if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + outputPendingSampleMetadata(track); + } + } + + public void outputPendingSampleMetadata(Track track) { + if (chunkSampleCount > 0) { + track.output.sampleMetadata( + chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData); + chunkSampleCount = 0; + } + } + } + + private static final class Track { + + private static final int DISPLAY_UNIT_PIXELS = 0; + private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3. + /** + * Default max content light level (CLL) that should be encoded into hdrStaticInfo. + */ + private static final int DEFAULT_MAX_CLL = 1000; // nits. + + /** + * Default frame-average light level (FALL) that should be encoded into hdrStaticInfo. + */ + private static final int DEFAULT_MAX_FALL = 200; // nits. + + // Common elements. + public String name; + public String codecId; + public int number; + public int type; + public int defaultSampleDurationNs; + public int maxBlockAdditionId; + public boolean hasContentEncryption; + public byte[] sampleStrippedBytes; + public TrackOutput.CryptoData cryptoData; + public byte[] codecPrivate; + public DrmInitData drmInitData; + + // Video elements. + public int width = Format.NO_VALUE; + public int height = Format.NO_VALUE; + public int displayWidth = Format.NO_VALUE; + public int displayHeight = Format.NO_VALUE; + public int displayUnit = DISPLAY_UNIT_PIXELS; + @C.Projection public int projectionType = Format.NO_VALUE; + public float projectionPoseYaw = 0f; + public float projectionPosePitch = 0f; + public float projectionPoseRoll = 0f; + public byte[] projectionData = null; + @C.StereoMode + public int stereoMode = Format.NO_VALUE; + public boolean hasColorInfo = false; + @C.ColorSpace + public int colorSpace = Format.NO_VALUE; + @C.ColorTransfer + public int colorTransfer = Format.NO_VALUE; + @C.ColorRange + public int colorRange = Format.NO_VALUE; + public int maxContentLuminance = DEFAULT_MAX_CLL; + public int maxFrameAverageLuminance = DEFAULT_MAX_FALL; + public float primaryRChromaticityX = Format.NO_VALUE; + public float primaryRChromaticityY = Format.NO_VALUE; + public float primaryGChromaticityX = Format.NO_VALUE; + public float primaryGChromaticityY = Format.NO_VALUE; + public float primaryBChromaticityX = Format.NO_VALUE; + public float primaryBChromaticityY = Format.NO_VALUE; + public float whitePointChromaticityX = Format.NO_VALUE; + public float whitePointChromaticityY = Format.NO_VALUE; + public float maxMasteringLuminance = Format.NO_VALUE; + public float minMasteringLuminance = Format.NO_VALUE; + + // Audio elements. Initially set to their default values. + public int channelCount = 1; + public int audioBitDepth = Format.NO_VALUE; + public int sampleRate = 8000; + public long codecDelayNs = 0; + public long seekPreRollNs = 0; + @Nullable public TrueHdSampleRechunker trueHdSampleRechunker; + + // Text elements. + public boolean flagForced; + public boolean flagDefault = true; + private String language = "eng"; + + // Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265. + public TrackOutput output; + public int nalUnitLengthFieldLength; + + /** Initializes the track with an output. */ + public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException { + String mimeType; + int maxInputSize = Format.NO_VALUE; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; + List<byte[]> initializationData = null; + switch (codecId) { + case CODEC_ID_VP8: + mimeType = MimeTypes.VIDEO_VP8; + break; + case CODEC_ID_VP9: + mimeType = MimeTypes.VIDEO_VP9; + break; + case CODEC_ID_AV1: + mimeType = MimeTypes.VIDEO_AV1; + break; + case CODEC_ID_MPEG2: + mimeType = MimeTypes.VIDEO_MPEG2; + break; + case CODEC_ID_MPEG4_SP: + case CODEC_ID_MPEG4_ASP: + case CODEC_ID_MPEG4_AP: + mimeType = MimeTypes.VIDEO_MP4V; + initializationData = + codecPrivate == null ? null : Collections.singletonList(codecPrivate); + break; + case CODEC_ID_H264: + mimeType = MimeTypes.VIDEO_H264; + AvcConfig avcConfig = AvcConfig.parse(new ParsableByteArray(codecPrivate)); + initializationData = avcConfig.initializationData; + nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength; + break; + case CODEC_ID_H265: + mimeType = MimeTypes.VIDEO_H265; + HevcConfig hevcConfig = HevcConfig.parse(new ParsableByteArray(codecPrivate)); + initializationData = hevcConfig.initializationData; + nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; + break; + case CODEC_ID_FOURCC: + Pair<String, List<byte[]>> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate)); + mimeType = pair.first; + initializationData = pair.second; + break; + case CODEC_ID_THEORA: + // TODO: This can be set to the real mimeType if/when we work out what initializationData + // should be set to for this case. + mimeType = MimeTypes.VIDEO_UNKNOWN; + break; + case CODEC_ID_VORBIS: + mimeType = MimeTypes.AUDIO_VORBIS; + maxInputSize = VORBIS_MAX_INPUT_SIZE; + initializationData = parseVorbisCodecPrivate(codecPrivate); + break; + case CODEC_ID_OPUS: + mimeType = MimeTypes.AUDIO_OPUS; + maxInputSize = OPUS_MAX_INPUT_SIZE; + initializationData = new ArrayList<>(3); + initializationData.add(codecPrivate); + initializationData.add( + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(codecDelayNs).array()); + initializationData.add( + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(seekPreRollNs).array()); + break; + case CODEC_ID_AAC: + mimeType = MimeTypes.AUDIO_AAC; + initializationData = Collections.singletonList(codecPrivate); + break; + case CODEC_ID_MP2: + mimeType = MimeTypes.AUDIO_MPEG_L2; + maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + break; + case CODEC_ID_MP3: + mimeType = MimeTypes.AUDIO_MPEG; + maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + break; + case CODEC_ID_AC3: + mimeType = MimeTypes.AUDIO_AC3; + break; + case CODEC_ID_E_AC3: + mimeType = MimeTypes.AUDIO_E_AC3; + break; + case CODEC_ID_TRUEHD: + mimeType = MimeTypes.AUDIO_TRUEHD; + trueHdSampleRechunker = new TrueHdSampleRechunker(); + break; + case CODEC_ID_DTS: + case CODEC_ID_DTS_EXPRESS: + mimeType = MimeTypes.AUDIO_DTS; + break; + case CODEC_ID_DTS_LOSSLESS: + mimeType = MimeTypes.AUDIO_DTS_HD; + break; + case CODEC_ID_FLAC: + mimeType = MimeTypes.AUDIO_FLAC; + initializationData = Collections.singletonList(codecPrivate); + break; + case CODEC_ID_ACM: + mimeType = MimeTypes.AUDIO_RAW; + if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); + } + } else { + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Non-PCM MS/ACM is unsupported. Setting mimeType to " + mimeType); + } + break; + case CODEC_ID_PCM_INT_LIT: + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); + } + break; + case CODEC_ID_SUBRIP: + mimeType = MimeTypes.APPLICATION_SUBRIP; + break; + case CODEC_ID_ASS: + mimeType = MimeTypes.TEXT_SSA; + break; + case CODEC_ID_VOBSUB: + mimeType = MimeTypes.APPLICATION_VOBSUB; + initializationData = Collections.singletonList(codecPrivate); + break; + case CODEC_ID_PGS: + mimeType = MimeTypes.APPLICATION_PGS; + break; + case CODEC_ID_DVBSUB: + mimeType = MimeTypes.APPLICATION_DVBSUBS; + // Init data: composition_page (2), ancillary_page (2) + initializationData = Collections.singletonList(new byte[] {codecPrivate[0], + codecPrivate[1], codecPrivate[2], codecPrivate[3]}); + break; + default: + throw new ParserException("Unrecognized codec identifier."); + } + + int type; + Format format; + @C.SelectionFlags int selectionFlags = 0; + selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; + selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0; + // TODO: Consider reading the name elements of the tracks and, if present, incorporating them + // into the trackId passed when creating the formats. + if (MimeTypes.isAudio(mimeType)) { + type = C.TRACK_TYPE_AUDIO; + format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, + initializationData, drmInitData, selectionFlags, language); + } else if (MimeTypes.isVideo(mimeType)) { + type = C.TRACK_TYPE_VIDEO; + if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { + displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; + displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight; + } + float pixelWidthHeightRatio = Format.NO_VALUE; + if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) { + pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight); + } + ColorInfo colorInfo = null; + if (hasColorInfo) { + byte[] hdrStaticInfo = getHdrStaticInfo(); + colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); + } + int rotationDegrees = Format.NO_VALUE; + // Some HTC devices signal rotation in track names. + if ("htc_video_rotA-000".equals(name)) { + rotationDegrees = 0; + } else if ("htc_video_rotA-090".equals(name)) { + rotationDegrees = 90; + } else if ("htc_video_rotA-180".equals(name)) { + rotationDegrees = 180; + } else if ("htc_video_rotA-270".equals(name)) { + rotationDegrees = 270; + } + if (projectionType == C.PROJECTION_RECTANGULAR + && Float.compare(projectionPoseYaw, 0f) == 0 + && Float.compare(projectionPosePitch, 0f) == 0) { + // The range of projectionPoseRoll is [-180, 180]. + if (Float.compare(projectionPoseRoll, 0f) == 0) { + rotationDegrees = 0; + } else if (Float.compare(projectionPosePitch, 90f) == 0) { + rotationDegrees = 90; + } else if (Float.compare(projectionPosePitch, -180f) == 0 + || Float.compare(projectionPosePitch, 180f) == 0) { + rotationDegrees = 180; + } else if (Float.compare(projectionPosePitch, -90f) == 0) { + rotationDegrees = 270; + } + } + format = + Format.createVideoSampleFormat( + Integer.toString(trackId), + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + maxInputSize, + width, + height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + drmInitData); + } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; + format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags, + language, drmInitData); + } else if (MimeTypes.TEXT_SSA.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; + initializationData = new ArrayList<>(2); + initializationData.add(SSA_DIALOGUE_FORMAT); + initializationData.add(codecPrivate); + format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData, + Format.OFFSET_SAMPLE_RELATIVE, initializationData); + } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; + format = + Format.createImageSampleFormat( + Integer.toString(trackId), + mimeType, + null, + Format.NO_VALUE, + selectionFlags, + initializationData, + language, + drmInitData); + } else { + throw new ParserException("Unexpected MIME type."); + } + + this.output = output.track(number, type); + this.output.format(format); + } + + /** Forces any pending sample metadata to be flushed to the output. */ + public void outputPendingSampleMetadata() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.outputPendingSampleMetadata(this); + } + } + + /** Resets any state stored in the track in response to a seek. */ + public void reset() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.reset(); + } + } + + /** Returns the HDR Static Info as defined in CTA-861.3. */ + @Nullable + private byte[] getHdrStaticInfo() { + // Are all fields present. + if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE + || primaryGChromaticityX == Format.NO_VALUE || primaryGChromaticityY == Format.NO_VALUE + || primaryBChromaticityX == Format.NO_VALUE || primaryBChromaticityY == Format.NO_VALUE + || whitePointChromaticityX == Format.NO_VALUE + || whitePointChromaticityY == Format.NO_VALUE || maxMasteringLuminance == Format.NO_VALUE + || minMasteringLuminance == Format.NO_VALUE) { + return null; + } + + byte[] hdrStaticInfoData = new byte[25]; + ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData).order(ByteOrder.LITTLE_ENDIAN); + hdrStaticInfo.put((byte) 0); // Type. + hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f)); + hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f)); + hdrStaticInfo.putShort((short) maxContentLuminance); + hdrStaticInfo.putShort((short) maxFrameAverageLuminance); + return hdrStaticInfoData; + } + + /** + * Builds initialization data for a {@link Format} from FourCC codec private data. + * + * @return The codec mime type and initialization data. If the compression type is not supported + * then the mime type is set to {@link MimeTypes#VIDEO_UNKNOWN} and the initialization data + * is {@code null}. + * @throws ParserException If the initialization data could not be built. + */ + private static Pair<String, List<byte[]>> parseFourCcPrivate(ParsableByteArray buffer) + throws ParserException { + try { + buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). + long compression = buffer.readLittleEndianUnsignedInt(); + if (compression == FOURCC_COMPRESSION_DIVX) { + return new Pair<>(MimeTypes.VIDEO_DIVX, null); + } else if (compression == FOURCC_COMPRESSION_H263) { + return new Pair<>(MimeTypes.VIDEO_H263, null); + } else if (compression == FOURCC_COMPRESSION_VC1) { + // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 + // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). + int startOffset = buffer.getPosition() + 20; + byte[] bufferData = buffer.data; + for (int offset = startOffset; offset < bufferData.length - 4; offset++) { + if (bufferData[offset] == 0x00 + && bufferData[offset + 1] == 0x00 + && bufferData[offset + 2] == 0x01 + && bufferData[offset + 3] == 0x0F) { + // We've found the initialization data. + byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); + return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData)); + } + } + throw new ParserException("Failed to find FourCC VC1 initialization data"); + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing FourCC private data"); + } + + Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); + return new Pair<>(MimeTypes.VIDEO_UNKNOWN, null); + } + + /** + * Builds initialization data for a {@link Format} from Vorbis codec private data. + * + * @return The initialization data for the {@link Format}. + * @throws ParserException If the initialization data could not be built. + */ + private static List<byte[]> parseVorbisCodecPrivate(byte[] codecPrivate) + throws ParserException { + try { + if (codecPrivate[0] != 0x02) { + throw new ParserException("Error parsing vorbis codec private"); + } + int offset = 1; + int vorbisInfoLength = 0; + while (codecPrivate[offset] == (byte) 0xFF) { + vorbisInfoLength += 0xFF; + offset++; + } + vorbisInfoLength += codecPrivate[offset++]; + + int vorbisSkipLength = 0; + while (codecPrivate[offset] == (byte) 0xFF) { + vorbisSkipLength += 0xFF; + offset++; + } + vorbisSkipLength += codecPrivate[offset++]; + + if (codecPrivate[offset] != 0x01) { + throw new ParserException("Error parsing vorbis codec private"); + } + byte[] vorbisInfo = new byte[vorbisInfoLength]; + System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength); + offset += vorbisInfoLength; + if (codecPrivate[offset] != 0x03) { + throw new ParserException("Error parsing vorbis codec private"); + } + offset += vorbisSkipLength; + if (codecPrivate[offset] != 0x05) { + throw new ParserException("Error parsing vorbis codec private"); + } + byte[] vorbisBooks = new byte[codecPrivate.length - offset]; + System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset); + List<byte[]> initializationData = new ArrayList<>(2); + initializationData.add(vorbisInfo); + initializationData.add(vorbisBooks); + return initializationData; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing vorbis codec private"); + } + } + + /** + * Parses an MS/ACM codec private, returning whether it indicates PCM audio. + * + * @return Whether the codec private indicates PCM audio. + * @throws ParserException If a parsing error occurs. + */ + private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException { + try { + int formatTag = buffer.readLittleEndianUnsignedShort(); + if (formatTag == WAVE_FORMAT_PCM) { + return true; + } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) { + buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4) + return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits() + && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits(); + } else { + return false; + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing MS/ACM codec private"); + } + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java new file mode 100644 index 0000000000..f84cd084a3 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * Utility class that peeks from the input stream in order to determine whether it appears to be + * compatible input for this extractor. + */ +/* package */ final class Sniffer { + + /** + * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}. + */ + private static final int SEARCH_LENGTH = 1024; + private static final int ID_EBML = 0x1A45DFA3; + + private final ParsableByteArray scratch; + private int peekLength; + + public Sniffer() { + scratch = new ParsableByteArray(8); + } + + /** + * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput) + */ + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH + ? SEARCH_LENGTH : inputLength); + // Find four bytes equal to ID_EBML near the start of the input. + input.peekFully(scratch.data, 0, 4); + long tag = scratch.readUnsignedInt(); + peekLength = 4; + while (tag != ID_EBML) { + if (++peekLength == bytesToSearch) { + return false; + } + input.peekFully(scratch.data, 0, 1); + tag = (tag << 8) & 0xFFFFFF00; + tag |= scratch.data[0] & 0xFF; + } + + // Read the size of the EBML header and make sure it is within the stream. + long headerSize = readUint(input); + long headerStart = peekLength; + if (headerSize == Long.MIN_VALUE + || (inputLength != C.LENGTH_UNSET && headerStart + headerSize >= inputLength)) { + return false; + } + + // Read the payload elements in the EBML header. + while (peekLength < headerStart + headerSize) { + long id = readUint(input); + if (id == Long.MIN_VALUE) { + return false; + } + long size = readUint(input); + if (size < 0 || size > Integer.MAX_VALUE) { + return false; + } + if (size != 0) { + int sizeInt = (int) size; + input.advancePeekPosition(sizeInt); + peekLength += sizeInt; + } + } + return peekLength == headerStart + headerSize; + } + + /** + * Peeks a variable-length unsigned EBML integer from the input. + */ + private long readUint(ExtractorInput input) throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 1); + int value = scratch.data[0] & 0xFF; + if (value == 0) { + return Long.MIN_VALUE; + } + int mask = 0x80; + int length = 0; + while ((value & mask) == 0) { + mask >>= 1; + length++; + } + value &= ~mask; + input.peekFully(scratch.data, 1, length); + for (int i = 0; i < length; i++) { + value <<= 8; + value += scratch.data[i + 1] & 0xFF; + } + peekLength += length + 1; + return value; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java new file mode 100644 index 0000000000..8a8d572ea5 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import java.io.EOFException; +import java.io.IOException; + +/** + * Reads EBML variable-length integers (varints) from an {@link ExtractorInput}. + */ +/* package */ final class VarintReader { + + private static final int STATE_BEGIN_READING = 0; + private static final int STATE_READ_CONTENTS = 1; + + /** + * The first byte of a variable-length integer (varint) will have one of these bit masks + * indicating the total length in bytes. + * + * <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes. + */ + private static final long[] VARINT_LENGTH_MASKS = new long[] { + 0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L + }; + + private final byte[] scratch; + + private int state; + private int length; + + public VarintReader() { + scratch = new byte[8]; + } + + /** + * Resets the reader to start reading a new variable-length integer. + */ + public void reset() { + state = STATE_BEGIN_READING; + length = 0; + } + + /** + * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that + * reading can be resumed later if an error occurs having read only some of it. + * <p> + * If an value is successfully read, then the reader will automatically reset itself ready to + * read another value. + * <p> + * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed + * later by calling this method again, passing an {@link ExtractorInput} providing data starting + * where the previous one left off. + * + * @param input The {@link ExtractorInput} from which the integer should be read. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @param maximumAllowedLength Maximum allowed length of the variable integer to be read. + * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true + * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the + * length of the varint exceeded maximumAllowedLength. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput, + boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException { + if (state == STATE_BEGIN_READING) { + // Read the first byte to establish the length. + if (!input.readFully(scratch, 0, 1, allowEndOfInput)) { + return C.RESULT_END_OF_INPUT; + } + int firstByte = scratch[0] & 0xFF; + length = parseUnsignedVarintLength(firstByte); + if (length == C.LENGTH_UNSET) { + throw new IllegalStateException("No valid varint length mask found"); + } + state = STATE_READ_CONTENTS; + } + + if (length > maximumAllowedLength) { + state = STATE_BEGIN_READING; + return C.RESULT_MAX_LENGTH_EXCEEDED; + } + + if (length != 1) { + // Read the remaining bytes. + input.readFully(scratch, 1, length - 1); + } + + state = STATE_BEGIN_READING; + return assembleVarint(scratch, length, removeLengthMask); + } + + /** + * Returns the number of bytes occupied by the most recently parsed varint. + */ + public int getLastLength() { + return length; + } + + /** + * Parses and the length of the varint given the first byte. + * + * @param firstByte First byte of the varint. + * @return Length of the varint beginning with the given byte if it was valid, + * {@link C#LENGTH_UNSET} otherwise. + */ + public static int parseUnsignedVarintLength(int firstByte) { + int varIntLength = C.LENGTH_UNSET; + for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) { + if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) { + varIntLength = i + 1; + break; + } + } + return varIntLength; + } + + /** + * Assemble a varint from the given byte array. + * + * @param varintBytes Bytes that make up the varint. + * @param varintLength Length of the varint to assemble. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @return Parsed and assembled varint. + */ + public static long assembleVarint(byte[] varintBytes, int varintLength, + boolean removeLengthMask) { + long varint = varintBytes[0] & 0xFFL; + if (removeLengthMask) { + varint &= ~VARINT_LENGTH_MASKS[varintLength - 1]; + } + for (int i = 1; i < varintLength; i++) { + varint = (varint << 8) | (varintBytes[i] & 0xFFL); + } + return varint; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java new file mode 100644 index 0000000000..1a442110e3 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; + +/** + * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. + */ +/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker { + + /** + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFramePosition The position of the first frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the first frame. + */ + public ConstantBitrateSeeker( + long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) { + super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize); + } + + @Override + public long getTimeUs(long position) { + return getTimeUsAtPosition(position); + } + + @Override + public long getDataEndPosition() { + return C.POSITION_UNSET; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java new file mode 100644 index 0000000000..662ded4ec3 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import android.util.Pair; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.MlltFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** MP3 seeker that uses metadata from an {@link MlltFrame}. */ +/* package */ final class MlltSeeker implements Seeker { + + /** + * Returns an {@link MlltSeeker} for seeking in the stream. + * + * @param firstFramePosition The position of the start of the first frame in the stream. + * @param mlltFrame The MLLT frame with seeking metadata. + * @return An {@link MlltSeeker} for seeking in the stream. + */ + public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) { + int referenceCount = mlltFrame.bytesDeviations.length; + long[] referencePositions = new long[1 + referenceCount]; + long[] referenceTimesMs = new long[1 + referenceCount]; + referencePositions[0] = firstFramePosition; + referenceTimesMs[0] = 0; + long position = firstFramePosition; + long timeMs = 0; + for (int i = 1; i <= referenceCount; i++) { + position += mlltFrame.bytesBetweenReference + mlltFrame.bytesDeviations[i - 1]; + timeMs += mlltFrame.millisecondsBetweenReference + mlltFrame.millisecondsDeviations[i - 1]; + referencePositions[i] = position; + referenceTimesMs[i] = timeMs; + } + return new MlltSeeker(referencePositions, referenceTimesMs); + } + + private final long[] referencePositions; + private final long[] referenceTimesMs; + private final long durationUs; + + private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) { + this.referencePositions = referencePositions; + this.referenceTimesMs = referenceTimesMs; + // Use the last reference point as the duration, as extrapolating variable bitrate at the end of + // the stream may give a large error. + durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]); + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + timeUs = Util.constrainValue(timeUs, 0, durationUs); + Pair<Long, Long> timeMsAndPosition = + linearlyInterpolate(C.usToMs(timeUs), referenceTimesMs, referencePositions); + timeUs = C.msToUs(timeMsAndPosition.first); + long position = timeMsAndPosition.second; + return new SeekPoints(new SeekPoint(timeUs, position)); + } + + @Override + public long getTimeUs(long position) { + Pair<Long, Long> positionAndTimeMs = + linearlyInterpolate(position, referencePositions, referenceTimesMs); + return C.msToUs(positionAndTimeMs.second); + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** + * Given a set of reference points as coordinates in {@code xReferences} and {@code yReferences} + * and an x-axis value, linearly interpolates between corresponding reference points to give a + * y-axis value. + * + * @param x The x-axis value for which a y-axis value is needed. + * @param xReferences x coordinates of reference points. + * @param yReferences y coordinates of reference points. + * @return The linearly interpolated y-axis value. + */ + private static Pair<Long, Long> linearlyInterpolate( + long x, long[] xReferences, long[] yReferences) { + int previousReferenceIndex = + Util.binarySearchFloor(xReferences, x, /* inclusive= */ true, /* stayInBounds= */ true); + long xPreviousReference = xReferences[previousReferenceIndex]; + long yPreviousReference = yReferences[previousReferenceIndex]; + int nextReferenceIndex = previousReferenceIndex + 1; + if (nextReferenceIndex == xReferences.length) { + return Pair.create(xPreviousReference, yPreviousReference); + } else { + long xNextReference = xReferences[nextReferenceIndex]; + long yNextReference = yReferences[nextReferenceIndex]; + double proportion = + xNextReference == xPreviousReference + ? 0.0 + : ((double) x - xPreviousReference) / (xNextReference - xPreviousReference); + long y = (long) (proportion * (yNextReference - yPreviousReference)) + yPreviousReference; + return Pair.create(x, y); + } + } + + @Override + public long getDataEndPosition() { + return C.POSITION_UNSET; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java new file mode 100644 index 0000000000..2829a1e519 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Id3Peeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.MlltFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Extracts data from the MP3 container format. + */ +public final class Mp3Extractor implements Extractor { + + /** Factory for {@link Mp3Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag values are {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 2; + + /** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */ + private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE = + (majorVersion, id0, id1, id2, id3) -> + ((id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2)) + || (id0 == 'M' && id1 == 'L' && id2 == 'L' && (id3 == 'T' || majorVersion == 2))); + + /** + * The maximum number of bytes to search when synchronizing, before giving up. + */ + private static final int MAX_SYNC_BYTES = 128 * 1024; + /** + * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. + */ + private static final int MAX_SNIFF_BYTES = 16 * 1024; + /** + * Maximum length of data read into {@link #scratch}. + */ + private static final int SCRATCH_LENGTH = 10; + + /** + * Mask that includes the audio header values that must match between frames. + */ + private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; + + private static final int SEEK_HEADER_XING = 0x58696e67; + private static final int SEEK_HEADER_INFO = 0x496e666f; + private static final int SEEK_HEADER_VBRI = 0x56425249; + private static final int SEEK_HEADER_UNSET = 0; + + @Flags private final int flags; + private final long forcedFirstSampleTimestampUs; + private final ParsableByteArray scratch; + private final MpegAudioHeader synchronizedHeader; + private final GaplessInfoHolder gaplessInfoHolder; + private final Id3Peeker id3Peeker; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + private TrackOutput trackOutput; + + private int synchronizedHeaderData; + + private Metadata metadata; + @Nullable private Seeker seeker; + private boolean disableSeeking; + private long basisTimeUs; + private long samplesRead; + private long firstSamplePosition; + private int sampleBytesRemaining; + + public Mp3Extractor() { + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public Mp3Extractor(@Flags int flags) { + this(flags, C.TIME_UNSET); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or + * {@link C#TIME_UNSET} if forcing is not required. + */ + public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { + this.flags = flags; + this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; + scratch = new ParsableByteArray(SCRATCH_LENGTH); + synchronizedHeader = new MpegAudioHeader(); + gaplessInfoHolder = new GaplessInfoHolder(); + basisTimeUs = C.TIME_UNSET; + id3Peeker = new Id3Peeker(); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return synchronize(input, true); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); + extractorOutput.endTracks(); + } + + @Override + public void seek(long position, long timeUs) { + synchronizedHeaderData = 0; + basisTimeUs = C.TIME_UNSET; + samplesRead = 0; + sampleBytesRemaining = 0; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (synchronizedHeaderData == 0) { + try { + synchronize(input, false); + } catch (EOFException e) { + return RESULT_END_OF_INPUT; + } + } + if (seeker == null) { + // Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata + // takes priority as it can provide greater precision. + Seeker seekFrameSeeker = maybeReadSeekFrame(input); + Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); + + if (disableSeeking) { + seeker = new UnseekableSeeker(); + } else { + if (metadataSeeker != null) { + seeker = metadataSeeker; + } else if (seekFrameSeeker != null) { + seeker = seekFrameSeeker; + } + if (seeker == null + || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { + seeker = getConstantBitrateSeeker(input); + } + } + extractorOutput.seekMap(seeker); + trackOutput.format( + Format.createAudioSampleFormat( + /* id= */ null, + synchronizedHeader.mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + MpegAudioHeader.MAX_FRAME_SIZE_BYTES, + synchronizedHeader.channels, + synchronizedHeader.sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + firstSamplePosition = input.getPosition(); + } else if (firstSamplePosition != 0) { + long inputPosition = input.getPosition(); + if (inputPosition < firstSamplePosition) { + // Skip past the seek frame. + input.skipFully((int) (firstSamplePosition - inputPosition)); + } + } + return readSample(input); + } + + /** + * Disables the extractor from being able to seek through the media. + * + * <p>Please note that this needs to be called before {@link #read}. + */ + public void disableSeeking() { + disableSeeking = true; + } + + // Internal methods. + + private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { + if (sampleBytesRemaining == 0) { + extractorInput.resetPeekPosition(); + if (peekEndOfStreamOrHeader(extractorInput)) { + return RESULT_END_OF_INPUT; + } + scratch.setPosition(0); + int sampleHeaderData = scratch.readInt(); + if (!headersMatch(sampleHeaderData, synchronizedHeaderData) + || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { + // We have lost synchronization, so attempt to resynchronize starting at the next byte. + extractorInput.skipFully(1); + synchronizedHeaderData = 0; + return RESULT_CONTINUE; + } + MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader); + if (basisTimeUs == C.TIME_UNSET) { + basisTimeUs = seeker.getTimeUs(extractorInput.getPosition()); + if (forcedFirstSampleTimestampUs != C.TIME_UNSET) { + long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0); + basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs; + } + } + sampleBytesRemaining = synchronizedHeader.frameSize; + } + int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + sampleBytesRemaining -= bytesAppended; + if (sampleBytesRemaining > 0) { + return RESULT_CONTINUE; + } + long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate); + trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0, + null); + samplesRead += synchronizedHeader.samplesPerFrame; + sampleBytesRemaining = 0; + return RESULT_CONTINUE; + } + + private boolean synchronize(ExtractorInput input, boolean sniffing) + throws IOException, InterruptedException { + int validFrameCount = 0; + int candidateSynchronizedHeaderData = 0; + int peekedId3Bytes = 0; + int searchedBytes = 0; + int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; + input.resetPeekPosition(); + if (input.getPosition() == 0) { + // We need to parse enough ID3 metadata to retrieve any gapless/seeking playback information + // even if ID3 metadata parsing is disabled. + boolean parseAllId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) == 0; + Id3Decoder.FramePredicate id3FramePredicate = + parseAllId3Frames ? null : REQUIRED_ID3_FRAME_PREDICATE; + metadata = id3Peeker.peekId3Data(input, id3FramePredicate); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); + } + peekedId3Bytes = (int) input.getPeekPosition(); + if (!sniffing) { + input.skipFully(peekedId3Bytes); + } + } + while (true) { + if (peekEndOfStreamOrHeader(input)) { + if (validFrameCount > 0) { + // We reached the end of the stream but found at least one valid frame. + break; + } + throw new EOFException(); + } + scratch.setPosition(0); + int headerData = scratch.readInt(); + int frameSize; + if ((candidateSynchronizedHeaderData != 0 + && !headersMatch(headerData, candidateSynchronizedHeaderData)) + || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) { + // The header doesn't match the candidate header or is invalid. Try the next byte offset. + if (searchedBytes++ == searchLimitBytes) { + if (!sniffing) { + throw new ParserException("Searched too many bytes."); + } + return false; + } + validFrameCount = 0; + candidateSynchronizedHeaderData = 0; + if (sniffing) { + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes + searchedBytes); + } else { + input.skipFully(1); + } + } else { + // The header matches the candidate header and/or is valid. + validFrameCount++; + if (validFrameCount == 1) { + MpegAudioHeader.populateHeader(headerData, synchronizedHeader); + candidateSynchronizedHeaderData = headerData; + } else if (validFrameCount == 4) { + break; + } + input.advancePeekPosition(frameSize - 4); + } + } + // Prepare to read the synchronized frame. + if (sniffing) { + input.skipFully(peekedId3Bytes + searchedBytes); + } else { + input.resetPeekPosition(); + } + synchronizedHeaderData = candidateSynchronizedHeaderData; + return true; + } + + /** + * Returns whether the extractor input is peeking the end of the stream. If {@code false}, + * populates the scratch buffer with the next four bytes. + */ + private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput) + throws IOException, InterruptedException { + if (seeker != null) { + long dataEndPosition = seeker.getDataEndPosition(); + if (dataEndPosition != C.POSITION_UNSET + && extractorInput.getPeekPosition() > dataEndPosition - 4) { + return true; + } + } + try { + return !extractorInput.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true); + } catch (EOFException e) { + return true; + } + } + + /** + * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, + * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. + * After this method returns, the input position is the start of the first frame of audio. + * + * @param input The {@link ExtractorInput} from which to read. + * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise. + * @throws IOException Thrown if there was an error reading from the stream. Not expected if the + * next two frames were already peeked during synchronization. + * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if + * the next two frames were already peeked during synchronization. + */ + private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException { + ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); + input.peekFully(frame.data, 0, synchronizedHeader.frameSize); + int xingBase = (synchronizedHeader.version & 1) != 0 + ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 + : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 + int seekHeader = getSeekFrameHeader(frame, xingBase); + Seeker seeker; + if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { + seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); + if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { + // If there is a Xing header, read gapless playback metadata at a fixed offset. + input.resetPeekPosition(); + input.advancePeekPosition(xingBase + 141); + input.peekFully(scratch.data, 0, 3); + scratch.setPosition(0); + gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); + } + input.skipFully(synchronizedHeader.frameSize); + if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) { + // Fall back to constant bitrate seeking for Info headers missing a table of contents. + return getConstantBitrateSeeker(input); + } + } else if (seekHeader == SEEK_HEADER_VBRI) { + seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); + input.skipFully(synchronizedHeader.frameSize); + } else { // seekerHeader == SEEK_HEADER_UNSET + // This frame doesn't contain seeking information, so reset the peek position. + seeker = null; + input.resetPeekPosition(); + } + return seeker; + } + + /** + * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. + */ + private Seeker getConstantBitrateSeeker(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); + } + + /** + * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. + */ + private static boolean headersMatch(int headerA, long headerB) { + return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK); + } + + /** + * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if + * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise. + * If seeking metadata is present, {@code frame}'s position is advanced past the header. + */ + private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) { + if (frame.limit() >= xingBase + 4) { + frame.setPosition(xingBase); + int headerData = frame.readInt(); + if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) { + return headerData; + } + } + if (frame.limit() >= 40) { + frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. + if (frame.readInt() == SEEK_HEADER_VBRI) { + return SEEK_HEADER_VBRI; + } + } + return SEEK_HEADER_UNSET; + } + + @Nullable + private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstFramePosition) { + if (metadata != null) { + int length = metadata.length(); + for (int i = 0; i < length; i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof MlltFrame) { + return MlltSeeker.create(firstFramePosition, (MlltFrame) entry); + } + } + } + return null; + } + + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java new file mode 100644 index 0000000000..da0306cc60 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; + +/** + * {@link SeekMap} that provides the end position of audio data and also allows mapping from + * position (byte offset) back to time, which can be used to work out the new sample basis timestamp + * after seeking and resynchronization. + */ +/* package */ interface Seeker extends SeekMap { + + /** + * Maps a position (byte offset) to a corresponding sample timestamp. + * + * @param position A seek position (byte offset) relative to the start of the stream. + * @return The corresponding timestamp of the next sample to be read, in microseconds. + */ + long getTimeUs(long position); + + /** + * Returns the position (byte offset) in the stream that is immediately after audio data, or + * {@link C#POSITION_UNSET} if not known. + */ + long getDataEndPosition(); + + /** A {@link Seeker} that does not support seeking through audio data. */ + /* package */ class UnseekableSeeker extends SeekMap.Unseekable implements Seeker { + + public UnseekableSeeker() { + super(/* durationUs= */ C.TIME_UNSET); + } + + @Override + public long getTimeUs(long position) { + return 0; + } + + @Override + public long getDataEndPosition() { + // Position unset as we do not know the data end position. Note that returning 0 doesn't work. + return C.POSITION_UNSET; + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java new file mode 100644 index 0000000000..8bb142f496 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** MP3 seeker that uses metadata from a VBRI header. */ +/* package */ final class VbriSeeker implements Seeker { + + private static final String TAG = "VbriSeeker"; + + /** + * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. + * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the + * caller should reset it. + * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the frame. + * @param frame The data in this audio frame, with its position set to immediately after the + * 'VBRI' tag. + * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required + * information is not present. + */ + public static @Nullable VbriSeeker create( + long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { + frame.skipBytes(10); + int numFrames = frame.readInt(); + if (numFrames <= 0) { + return null; + } + int sampleRate = mpegAudioHeader.sampleRate; + long durationUs = Util.scaleLargeTimestamp(numFrames, + C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate); + int entryCount = frame.readUnsignedShort(); + int scale = frame.readUnsignedShort(); + int entrySize = frame.readUnsignedShort(); + frame.skipBytes(2); + + long minPosition = position + mpegAudioHeader.frameSize; + // Read table of contents entries. + long[] timesUs = new long[entryCount]; + long[] positions = new long[entryCount]; + for (int index = 0; index < entryCount; index++) { + timesUs[index] = (index * durationUs) / entryCount; + // Ensure positions do not fall within the frame containing the VBRI header. This constraint + // will normally only apply to the first entry in the table. + positions[index] = Math.max(position, minPosition); + int segmentSize; + switch (entrySize) { + case 1: + segmentSize = frame.readUnsignedByte(); + break; + case 2: + segmentSize = frame.readUnsignedShort(); + break; + case 3: + segmentSize = frame.readUnsignedInt24(); + break; + case 4: + segmentSize = frame.readUnsignedIntToInt(); + break; + default: + return null; + } + position += segmentSize * scale; + } + if (inputLength != C.LENGTH_UNSET && inputLength != position) { + Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position); + } + return new VbriSeeker(timesUs, positions, durationUs, /* dataEndPosition= */ position); + } + + private final long[] timesUs; + private final long[] positions; + private final long durationUs; + private final long dataEndPosition; + + private VbriSeeker(long[] timesUs, long[] positions, long durationUs, long dataEndPosition) { + this.timesUs = timesUs; + this.positions = positions; + this.durationUs = durationUs; + this.dataEndPosition = dataEndPosition; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true); + SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]); + if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } + } + + @Override + public long getTimeUs(long position) { + return timesUs[Util.binarySearchFloor(positions, position, true, true)]; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public long getDataEndPosition() { + return dataEndPosition; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java new file mode 100644 index 0000000000..61568aac93 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** MP3 seeker that uses metadata from a Xing header. */ +/* package */ final class XingSeeker implements Seeker { + + private static final String TAG = "XingSeeker"; + + /** + * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. + * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the + * caller should reset it. + * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the frame. + * @param frame The data in this audio frame, with its position set to immediately after the + * 'Xing' or 'Info' tag. + * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required + * information is not present. + */ + public static @Nullable XingSeeker create( + long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { + int samplesPerFrame = mpegAudioHeader.samplesPerFrame; + int sampleRate = mpegAudioHeader.sampleRate; + + int flags = frame.readInt(); + int frameCount; + if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) { + // If the frame count is missing/invalid, the header can't be used to determine the duration. + return null; + } + long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND, + sampleRate); + if ((flags & 0x06) != 0x06) { + // If the size in bytes or table of contents is missing, the stream is not seekable. + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); + } + + long dataSize = frame.readUnsignedIntToInt(); + long[] tableOfContents = new long[100]; + for (int i = 0; i < 100; i++) { + tableOfContents[i] = frame.readUnsignedByte(); + } + + // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: + // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); + // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); + + if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) { + Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize)); + } + return new XingSeeker( + position, mpegAudioHeader.frameSize, durationUs, dataSize, tableOfContents); + } + + private final long dataStartPosition; + private final int xingFrameSize; + private final long durationUs; + /** Data size, including the XING frame. */ + private final long dataSize; + + private final long dataEndPosition; + /** + * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the + * table of contents was missing from the header, in which case seeking is not be supported. + */ + @Nullable private final long[] tableOfContents; + + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { + this( + dataStartPosition, + xingFrameSize, + durationUs, + /* dataSize= */ C.LENGTH_UNSET, + /* tableOfContents= */ null); + } + + private XingSeeker( + long dataStartPosition, + int xingFrameSize, + long durationUs, + long dataSize, + @Nullable long[] tableOfContents) { + this.dataStartPosition = dataStartPosition; + this.xingFrameSize = xingFrameSize; + this.durationUs = durationUs; + this.tableOfContents = tableOfContents; + this.dataSize = dataSize; + dataEndPosition = dataSize == C.LENGTH_UNSET ? C.POSITION_UNSET : dataStartPosition + dataSize; + } + + @Override + public boolean isSeekable() { + return tableOfContents != null; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + if (!isSeekable()) { + return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize)); + } + timeUs = Util.constrainValue(timeUs, 0, durationUs); + double percent = (timeUs * 100d) / durationUs; + double scaledPosition; + if (percent <= 0) { + scaledPosition = 0; + } else if (percent >= 100) { + scaledPosition = 256; + } else { + int prevTableIndex = (int) percent; + long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + double prevScaledPosition = tableOfContents[prevTableIndex]; + double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two scaled positions. + double interpolateFraction = percent - prevTableIndex; + scaledPosition = prevScaledPosition + + (interpolateFraction * (nextScaledPosition - prevScaledPosition)); + } + long positionOffset = Math.round((scaledPosition / 256) * dataSize); + // Ensure returned positions skip the frame containing the XING header. + positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); + return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset)); + } + + @Override + public long getTimeUs(long position) { + long positionOffset = position - dataStartPosition; + if (!isSeekable() || positionOffset <= xingFrameSize) { + return 0L; + } + long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + double scaledPosition = (positionOffset * 256d) / dataSize; + int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); + long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); + long prevScaledPosition = tableOfContents[prevTableIndex]; + long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1); + long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two table entries. + double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0 + : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition)); + return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs)); + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public long getDataEndPosition() { + return dataEndPosition; + } + + /** + * Returns the time in microseconds for a given table index. + * + * @param tableIndex A table index in the range [0, 100]. + * @return The corresponding time in microseconds. + */ + private long getTimeUsForTableIndex(int tableIndex) { + return (durationUs * tableIndex) / 100; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java new file mode 100644 index 0000000000..56f0eab1cd --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -0,0 +1,558 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("ConstantField") +/* package */ abstract class Atom { + + /** + * Size of an atom header, in bytes. + */ + public static final int HEADER_SIZE = 8; + + /** + * Size of a full atom header, in bytes. + */ + public static final int FULL_HEADER_SIZE = 12; + + /** + * Size of a long atom header, in bytes. + */ + public static final int LONG_HEADER_SIZE = 16; + + /** + * Value for the size field in an atom that defines its size in the largesize field. + */ + public static final int DEFINES_LARGE_SIZE = 1; + + /** + * Value for the size field in an atom that extends to the end of the file. + */ + public static final int EXTENDS_TO_END_SIZE = 0; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ftyp = 0x66747970; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avc1 = 0x61766331; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avc3 = 0x61766333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avcC = 0x61766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hvc1 = 0x68766331; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hev1 = 0x68657631; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hvcC = 0x68766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vp08 = 0x76703038; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vp09 = 0x76703039; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vpcC = 0x76706343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_av01 = 0x61763031; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_av1C = 0x61763143; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvav = 0x64766176; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dva1 = 0x64766131; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvhe = 0x64766865; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvh1 = 0x64766831; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvcC = 0x64766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvvC = 0x64767643; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_s263 = 0x73323633; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_d263 = 0x64323633; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdat = 0x6d646174; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mp4a = 0x6d703461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE__mp3 = 0x2e6d7033; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_wave = 0x77617665; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_lpcm = 0x6c70636d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sowt = 0x736f7774; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ac_3 = 0x61632d33; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dac3 = 0x64616333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ec_3 = 0x65632d33; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dec3 = 0x64656333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ac_4 = 0x61632d34; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dac4 = 0x64616334; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsc = 0x64747363; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsh = 0x64747368; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsl = 0x6474736c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtse = 0x64747365; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ddts = 0x64647473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tfdt = 0x74666474; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tfhd = 0x74666864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trex = 0x74726578; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trun = 0x7472756e; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sidx = 0x73696478; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_moov = 0x6d6f6f76; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mvhd = 0x6d766864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trak = 0x7472616b; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdia = 0x6d646961; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_minf = 0x6d696e66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stbl = 0x7374626c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_esds = 0x65736473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_moof = 0x6d6f6f66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_traf = 0x74726166; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mvex = 0x6d766578; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mehd = 0x6d656864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tkhd = 0x746b6864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_edts = 0x65647473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_elst = 0x656c7374; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdhd = 0x6d646864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hdlr = 0x68646c72; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsd = 0x73747364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_pssh = 0x70737368; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sinf = 0x73696e66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_schm = 0x7363686d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_schi = 0x73636869; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tenc = 0x74656e63; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_encv = 0x656e6376; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_enca = 0x656e6361; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_frma = 0x66726d61; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_saiz = 0x7361697a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_saio = 0x7361696f; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sbgp = 0x73626770; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sgpd = 0x73677064; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_uuid = 0x75756964; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_senc = 0x73656e63; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_pasp = 0x70617370; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_TTML = 0x54544d4c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vmhd = 0x766d6864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mp4v = 0x6d703476; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stts = 0x73747473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stss = 0x73747373; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ctts = 0x63747473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsc = 0x73747363; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsz = 0x7374737a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stz2 = 0x73747a32; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stco = 0x7374636f; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_co64 = 0x636f3634; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tx3g = 0x74783367; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_wvtt = 0x77767474; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stpp = 0x73747070; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_c608 = 0x63363038; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_samr = 0x73616d72; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sawb = 0x73617762; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_udta = 0x75647461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_keys = 0x6b657973; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ilst = 0x696c7374; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mean = 0x6d65616e; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_name = 0x6e616d65; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_data = 0x64617461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_emsg = 0x656d7367; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_st3d = 0x73743364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sv3d = 0x73763364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_proj = 0x70726f6a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_camm = 0x63616d6d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_alac = 0x616c6163; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_alaw = 0x616c6177; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ulaw = 0x756c6177; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_Opus = 0x4f707573; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dOps = 0x644f7073; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_fLaC = 0x664c6143; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dfLa = 0x64664c61; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; + + public final int type; + + public Atom(int type) { + this.type = type; + } + + @Override + public String toString() { + return getAtomTypeString(type); + } + + /** + * An MP4 atom that is a leaf. + */ + /* package */ static final class LeafAtom extends Atom { + + /** + * The atom data. + */ + public final ParsableByteArray data; + + /** + * @param type The type of the atom. + * @param data The atom data. + */ + public LeafAtom(int type, ParsableByteArray data) { + super(type); + this.data = data; + } + + } + + /** + * An MP4 atom that has child atoms. + */ + /* package */ static final class ContainerAtom extends Atom { + + public final long endPosition; + public final List<LeafAtom> leafChildren; + public final List<ContainerAtom> containerChildren; + + /** + * @param type The type of the atom. + * @param endPosition The position of the first byte after the end of the atom. + */ + public ContainerAtom(int type, long endPosition) { + super(type); + this.endPosition = endPosition; + leafChildren = new ArrayList<>(); + containerChildren = new ArrayList<>(); + } + + /** + * Adds a child leaf to this container. + * + * @param atom The child to add. + */ + public void add(LeafAtom atom) { + leafChildren.add(atom); + } + + /** + * Adds a child container to this container. + * + * @param atom The child to add. + */ + public void add(ContainerAtom atom) { + containerChildren.add(atom); + } + + /** + * Returns the child leaf of the given type. + * + * <p>If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. + * + * @param type The leaf type. + * @return The child leaf of the given type, or null if no such child exists. + */ + @Nullable + public LeafAtom getLeafAtomOfType(int type) { + int childrenSize = leafChildren.size(); + for (int i = 0; i < childrenSize; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == type) { + return atom; + } + } + return null; + } + + /** + * Returns the child container of the given type. + * + * <p>If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. + * + * @param type The container type. + * @return The child container of the given type, or null if no such child exists. + */ + @Nullable + public ContainerAtom getContainerAtomOfType(int type) { + int childrenSize = containerChildren.size(); + for (int i = 0; i < childrenSize; i++) { + ContainerAtom atom = containerChildren.get(i); + if (atom.type == type) { + return atom; + } + } + return null; + } + + /** + * Returns the total number of leaf/container children of this atom with the given type. + * + * @param type The type of child atoms to count. + * @return The total number of leaf/container children of this atom with the given type. + */ + public int getChildAtomOfTypeCount(int type) { + int count = 0; + int size = leafChildren.size(); + for (int i = 0; i < size; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == type) { + count++; + } + } + size = containerChildren.size(); + for (int i = 0; i < size; i++) { + ContainerAtom atom = containerChildren.get(i); + if (atom.type == type) { + count++; + } + } + return count; + } + + @Override + public String toString() { + return getAtomTypeString(type) + + " leaves: " + Arrays.toString(leafChildren.toArray()) + + " containers: " + Arrays.toString(containerChildren.toArray()); + } + + } + + /** + * Parses the version number out of the additional integer component of a full atom. + */ + public static int parseFullAtomVersion(int fullAtomInt) { + return 0x000000FF & (fullAtomInt >> 24); + } + + /** + * Parses the atom flags out of the additional integer component of a full atom. + */ + public static int parseFullAtomFlags(int fullAtomInt) { + return 0x00FFFFFF & fullAtomInt; + } + + /** + * Converts a numeric atom type to the corresponding four character string. + * + * @param type The numeric atom type. + * @return The corresponding four character string. + */ + public static String getAtomTypeString(int type) { + return "" + (char) ((type >> 24) & 0xFF) + + (char) ((type >> 16) & 0xFF) + + (char) ((type >> 8) & 0xFF) + + (char) (type & 0xFF); + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java new file mode 100644 index 0000000000..93ee2d6810 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -0,0 +1,1607 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.DolbyVisionConfig; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.HevcConfig; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ +@SuppressWarnings({"ConstantField"}) +/* package */ final class AtomParsers { + + private static final String TAG = "AtomParsers"; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vide = 0x76696465; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_soun = 0x736f756e; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_text = 0x74657874; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sbtl = 0x7362746c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_subt = 0x73756274; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_clcp = 0x636c6370; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_mdta = 0x6d647461; + + /** + * The threshold number of samples to trim from the start/end of an audio track when applying an + * edit below which gapless info can be used (rather than removing samples from the sample table). + */ + private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4; + + /** The magic signature for an Opus Identification header, as defined in RFC-7845. */ + private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead"); + + /** + * Parses a trak atom (defined in 14496-12). + * + * @param trak Atom to decode. + * @param mvhd Movie header atom, used to get the timescale. + * @param duration The duration in units of the timescale declared in the mvhd atom, or + * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @param ignoreEditLists Whether to ignore any edit lists in the trak box. + * @param isQuickTime True for QuickTime media. False otherwise. + * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. + */ + public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, + DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) + throws ParserException { + Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); + int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data)); + if (trackType == C.TRACK_TYPE_UNKNOWN) { + return null; + } + + TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); + if (duration == C.TIME_UNSET) { + duration = tkhdData.duration; + } + long movieTimescale = parseMvhd(mvhd.data); + long durationUs; + if (duration == C.TIME_UNSET) { + durationUs = C.TIME_UNSET; + } else { + durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale); + } + Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + + Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); + StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, + tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); + long[] editListDurations = null; + long[] editListMediaTimes = null; + if (!ignoreEditLists) { + Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + editListDurations = edtsData.first; + editListMediaTimes = edtsData.second; + } + return stsdData.format == null ? null + : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, + stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, + stsdData.nalUnitLengthFieldLength, editListDurations, editListMediaTimes); + } + + /** + * Parses an stbl atom (defined in 14496-12). + * + * @param track Track to which this sample table corresponds. + * @param stblAtom stbl (sample table) atom to decode. + * @param gaplessInfoHolder Holder to populate with gapless playback information. + * @return Sample table described by the stbl atom. + * @throws ParserException Thrown if the stbl atom can't be parsed. + */ + public static TrackSampleTable parseStbl( + Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) + throws ParserException { + SampleSizeBox sampleSizeBox; + Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); + if (stszAtom != null) { + sampleSizeBox = new StszSampleSizeBox(stszAtom); + } else { + Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2); + if (stz2Atom == null) { + throw new ParserException("Track has no sample table size information"); + } + sampleSizeBox = new Stz2SampleSizeBox(stz2Atom); + } + + int sampleCount = sampleSizeBox.getSampleCount(); + if (sampleCount == 0) { + return new TrackSampleTable( + track, + /* offsets= */ new long[0], + /* sizes= */ new int[0], + /* maximumSize= */ 0, + /* timestampsUs= */ new long[0], + /* flags= */ new int[0], + /* durationUs= */ C.TIME_UNSET); + } + + // Entries are byte offsets of chunks. + boolean chunkOffsetsAreLongs = false; + Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco); + if (chunkOffsetsAtom == null) { + chunkOffsetsAreLongs = true; + chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64); + } + ParsableByteArray chunkOffsets = chunkOffsetsAtom.data; + // Entries are (chunk number, number of samples per chunk, sample description index). + ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data; + // Entries are (number of samples, timestamp delta between those samples). + ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data; + // Entries are the indices of samples that are synchronization samples. + Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss); + ParsableByteArray stss = stssAtom != null ? stssAtom.data : null; + // Entries are (number of samples, timestamp offset). + Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); + ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; + + // Prepare to read chunk information. + ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs); + + // Prepare to read sample timestamps. + stts.setPosition(Atom.FULL_HEADER_SIZE); + int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1; + int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); + int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); + + // Prepare to read sample timestamp offsets, if ctts is present. + int remainingSamplesAtTimestampOffset = 0; + int remainingTimestampOffsetChanges = 0; + int timestampOffset = 0; + if (ctts != null) { + ctts.setPosition(Atom.FULL_HEADER_SIZE); + remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt(); + } + + int nextSynchronizationSampleIndex = C.INDEX_UNSET; + int remainingSynchronizationSamples = 0; + if (stss != null) { + stss.setPosition(Atom.FULL_HEADER_SIZE); + remainingSynchronizationSamples = stss.readUnsignedIntToInt(); + if (remainingSynchronizationSamples > 0) { + nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; + } else { + // Ignore empty stss boxes, which causes all samples to be treated as sync samples. + stss = null; + } + } + + // Fixed sample size raw audio may need to be rechunked. + boolean isFixedSampleSizeRawAudio = + sampleSizeBox.isFixedSampleSize() + && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) + && remainingTimestampDeltaChanges == 0 + && remainingTimestampOffsetChanges == 0 + && remainingSynchronizationSamples == 0; + + long[] offsets; + int[] sizes; + int maximumSize = 0; + long[] timestamps; + int[] flags; + long timestampTimeUnits = 0; + long duration; + + if (!isFixedSampleSizeRawAudio) { + offsets = new long[sampleCount]; + sizes = new int[sampleCount]; + timestamps = new long[sampleCount]; + flags = new int[sampleCount]; + long offset = 0; + int remainingSamplesInChunk = 0; + + for (int i = 0; i < sampleCount; i++) { + // Advance to the next chunk if necessary. + boolean chunkDataComplete = true; + while (remainingSamplesInChunk == 0 && (chunkDataComplete = chunkIterator.moveNext())) { + offset = chunkIterator.offset; + remainingSamplesInChunk = chunkIterator.numSamples; + } + if (!chunkDataComplete) { + Log.w(TAG, "Unexpected end of chunk data"); + sampleCount = i; + offsets = Arrays.copyOf(offsets, sampleCount); + sizes = Arrays.copyOf(sizes, sampleCount); + timestamps = Arrays.copyOf(timestamps, sampleCount); + flags = Arrays.copyOf(flags, sampleCount); + break; + } + + // Add on the timestamp offset if ctts is present. + if (ctts != null) { + while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) { + remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt(); + // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers + // in version 0 ctts boxes, however some streams violate the spec and use signed + // integers instead. It's safe to always decode sample offsets as signed integers here, + // because unsigned integers will still be parsed correctly (unless their top bit is + // set, which is never true in practice because sample offsets are always small). + timestampOffset = ctts.readInt(); + remainingTimestampOffsetChanges--; + } + remainingSamplesAtTimestampOffset--; + } + + offsets[i] = offset; + sizes[i] = sampleSizeBox.readNextSampleSize(); + if (sizes[i] > maximumSize) { + maximumSize = sizes[i]; + } + timestamps[i] = timestampTimeUnits + timestampOffset; + + // All samples are synchronization samples if the stss is not present. + flags[i] = stss == null ? C.BUFFER_FLAG_KEY_FRAME : 0; + if (i == nextSynchronizationSampleIndex) { + flags[i] = C.BUFFER_FLAG_KEY_FRAME; + remainingSynchronizationSamples--; + if (remainingSynchronizationSamples > 0) { + nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; + } + } + + // Add on the duration of this sample. + timestampTimeUnits += timestampDeltaInTimeUnits; + remainingSamplesAtTimestampDelta--; + if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) { + remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); + // The BMFF spec (ISO 14496-12) states that sample deltas should be unsigned integers + // in stts boxes, however some streams violate the spec and use signed integers instead. + // See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample + // deltas as signed integers here, because unsigned integers will still be parsed + // correctly (unless their top bit is set, which is never true in practice because sample + // deltas are always small). + timestampDeltaInTimeUnits = stts.readInt(); + remainingTimestampDeltaChanges--; + } + + offset += sizes[i]; + remainingSamplesInChunk--; + } + duration = timestampTimeUnits + timestampOffset; + + // If the stbl's child boxes are not consistent the container is malformed, but the stream may + // still be playable. + boolean isCttsValid = true; + while (remainingTimestampOffsetChanges > 0) { + if (ctts.readUnsignedIntToInt() != 0) { + isCttsValid = false; + break; + } + ctts.readInt(); // Ignore offset. + remainingTimestampOffsetChanges--; + } + if (remainingSynchronizationSamples != 0 + || remainingSamplesAtTimestampDelta != 0 + || remainingSamplesInChunk != 0 + || remainingTimestampDeltaChanges != 0 + || remainingSamplesAtTimestampOffset != 0 + || !isCttsValid) { + Log.w( + TAG, + "Inconsistent stbl box for track " + + track.id + + ": remainingSynchronizationSamples " + + remainingSynchronizationSamples + + ", remainingSamplesAtTimestampDelta " + + remainingSamplesAtTimestampDelta + + ", remainingSamplesInChunk " + + remainingSamplesInChunk + + ", remainingTimestampDeltaChanges " + + remainingTimestampDeltaChanges + + ", remainingSamplesAtTimestampOffset " + + remainingSamplesAtTimestampOffset + + (!isCttsValid ? ", ctts invalid" : "")); + } + } else { + long[] chunkOffsetsBytes = new long[chunkIterator.length]; + int[] chunkSampleCounts = new int[chunkIterator.length]; + while (chunkIterator.moveNext()) { + chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; + chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; + } + int fixedSampleSize = + Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount); + FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( + fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); + offsets = rechunkedResults.offsets; + sizes = rechunkedResults.sizes; + maximumSize = rechunkedResults.maximumSize; + timestamps = rechunkedResults.timestamps; + flags = rechunkedResults.flags; + duration = rechunkedResults.duration; + } + long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale); + + if (track.editListDurations == null) { + Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); + } + + // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a + // sync sample after reordering are not supported. Partial audio sample truncation is only + // supported in edit lists with one edit that removes less than MAX_GAPLESS_TRIM_SIZE_SAMPLES + // samples from the start/end of the track. This implementation handles simple + // discarding/delaying of samples. The extractor may place further restrictions on what edited + // streams are playable. + + if (track.editListDurations.length == 1 + && track.type == C.TRACK_TYPE_AUDIO + && timestamps.length >= 2) { + long editStartTime = track.editListMediaTimes[0]; + long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0], + track.timescale, track.movieTimescale); + if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) { + long paddingTimeUnits = duration - editEndTime; + long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0], + track.format.sampleRate, track.timescale); + long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, + track.format.sampleRate, track.timescale); + if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE + && encoderPadding <= Integer.MAX_VALUE) { + gaplessInfoHolder.encoderDelay = (int) encoderDelay; + gaplessInfoHolder.encoderPadding = (int) encoderPadding; + Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); + long editedDurationUs = + Util.scaleLargeTimestamp( + track.editListDurations[0], C.MICROS_PER_SECOND, track.movieTimescale); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, editedDurationUs); + } + } + } + + if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) { + // The current version of the spec leaves handling of an edit with zero segment_duration in + // unfragmented files open to interpretation. We handle this as a special case and include all + // samples in the edit. + long editStartTime = track.editListMediaTimes[0]; + for (int i = 0; i < timestamps.length; i++) { + timestamps[i] = + Util.scaleLargeTimestamp( + timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale); + } + durationUs = + Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); + } + + // Omit any sample at the end point of an edit for audio tracks. + boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO; + + // Count the number of samples after applying edits. + int editedSampleCount = 0; + int nextSampleIndex = 0; + boolean copyMetadata = false; + int[] startIndices = new int[track.editListDurations.length]; + int[] endIndices = new int[track.editListDurations.length]; + for (int i = 0; i < track.editListDurations.length; i++) { + long editMediaTime = track.editListMediaTimes[i]; + if (editMediaTime != -1) { + long editDuration = + Util.scaleLargeTimestamp( + track.editListDurations[i], track.timescale, track.movieTimescale); + startIndices[i] = + Util.binarySearchFloor( + timestamps, editMediaTime, /* inclusive= */ true, /* stayInBounds= */ true); + endIndices[i] = + Util.binarySearchCeil( + timestamps, + editMediaTime + editDuration, + /* inclusive= */ omitClippedSample, + /* stayInBounds= */ false); + while (startIndices[i] < endIndices[i] + && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // Applying the edit correctly would require prerolling from the previous sync sample. In + // the current implementation we advance to the next sync sample instead. Only other + // tracks (i.e. audio) will be rendered until the time of the first sync sample. + // See https://github.com/google/ExoPlayer/issues/1659. + startIndices[i]++; + } + editedSampleCount += endIndices[i] - startIndices[i]; + copyMetadata |= nextSampleIndex != startIndices[i]; + nextSampleIndex = endIndices[i]; + } + } + copyMetadata |= editedSampleCount != sampleCount; + + // Calculate edited sample timestamps and update the corresponding metadata arrays. + long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets; + int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes; + int editedMaximumSize = copyMetadata ? 0 : maximumSize; + int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags; + long[] editedTimestamps = new long[editedSampleCount]; + long pts = 0; + int sampleIndex = 0; + for (int i = 0; i < track.editListDurations.length; i++) { + long editMediaTime = track.editListMediaTimes[i]; + int startIndex = startIndices[i]; + int endIndex = endIndices[i]; + if (copyMetadata) { + int count = endIndex - startIndex; + System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); + System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); + System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); + } + for (int j = startIndex; j < endIndex; j++) { + long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); + long timeInSegmentUs = + Util.scaleLargeTimestamp( + Math.max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale); + editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; + if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { + editedMaximumSize = sizes[j]; + } + sampleIndex++; + } + pts += track.editListDurations[i]; + } + long editedDurationUs = + Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); + return new TrackSampleTable( + track, + editedOffsets, + editedSizes, + editedMaximumSize, + editedTimestamps, + editedFlags, + editedDurationUs); + } + + /** + * Parses a udta atom. + * + * @param udtaAtom The udta (user data) atom to decode. + * @param isQuickTime True for QuickTime media. False otherwise. + * @return Parsed metadata, or null. + */ + @Nullable + public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { + if (isQuickTime) { + // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and + // decode one. + return null; + } + ParsableByteArray udtaData = udtaAtom.data; + udtaData.setPosition(Atom.HEADER_SIZE); + while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) { + int atomPosition = udtaData.getPosition(); + int atomSize = udtaData.readInt(); + int atomType = udtaData.readInt(); + if (atomType == Atom.TYPE_meta) { + udtaData.setPosition(atomPosition); + return parseUdtaMeta(udtaData, atomPosition + atomSize); + } + udtaData.setPosition(atomPosition + atomSize); + } + return null; + } + + /** + * Parses a metadata meta atom if it contains metadata with handler 'mdta'. + * + * @param meta The metadata atom to decode. + * @return Parsed metadata, or null. + */ + @Nullable + public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) { + Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); + Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); + Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); + if (hdlrAtom == null + || keysAtom == null + || ilstAtom == null + || AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) { + // There isn't enough information to parse the metadata, or the handler type is unexpected. + return null; + } + + // Parse metadata keys. + ParsableByteArray keys = keysAtom.data; + keys.setPosition(Atom.FULL_HEADER_SIZE); + int entryCount = keys.readInt(); + String[] keyNames = new String[entryCount]; + for (int i = 0; i < entryCount; i++) { + int entrySize = keys.readInt(); + keys.skipBytes(4); // keyNamespace + int keySize = entrySize - 8; + keyNames[i] = keys.readString(keySize); + } + + // Parse metadata items. + ParsableByteArray ilst = ilstAtom.data; + ilst.setPosition(Atom.HEADER_SIZE); + ArrayList<Metadata.Entry> entries = new ArrayList<>(); + while (ilst.bytesLeft() > Atom.HEADER_SIZE) { + int atomPosition = ilst.getPosition(); + int atomSize = ilst.readInt(); + int keyIndex = ilst.readInt() - 1; + if (keyIndex >= 0 && keyIndex < keyNames.length) { + String key = keyNames[keyIndex]; + Metadata.Entry entry = + MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key); + if (entry != null) { + entries.add(entry); + } + } else { + Log.w(TAG, "Skipped metadata with unknown key index: " + keyIndex); + } + ilst.setPosition(atomPosition + atomSize); + } + return entries.isEmpty() ? null : new Metadata(entries); + } + + @Nullable + private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) { + meta.skipBytes(Atom.FULL_HEADER_SIZE); + while (meta.getPosition() < limit) { + int atomPosition = meta.getPosition(); + int atomSize = meta.readInt(); + int atomType = meta.readInt(); + if (atomType == Atom.TYPE_ilst) { + meta.setPosition(atomPosition); + return parseIlst(meta, atomPosition + atomSize); + } + meta.setPosition(atomPosition + atomSize); + } + return null; + } + + @Nullable + private static Metadata parseIlst(ParsableByteArray ilst, int limit) { + ilst.skipBytes(Atom.HEADER_SIZE); + ArrayList<Metadata.Entry> entries = new ArrayList<>(); + while (ilst.getPosition() < limit) { + Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst); + if (entry != null) { + entries.add(entry); + } + } + return entries.isEmpty() ? null : new Metadata(entries); + } + + /** + * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie. + * + * @param mvhd Contents of the mvhd atom to be parsed. + * @return Timescale for the movie. + */ + private static long parseMvhd(ParsableByteArray mvhd) { + mvhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = mvhd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + mvhd.skipBytes(version == 0 ? 8 : 16); + return mvhd.readUnsignedInt(); + } + + /** + * Parses a tkhd atom (defined in 14496-12). + * + * @return An object containing the parsed data. + */ + private static TkhdData parseTkhd(ParsableByteArray tkhd) { + tkhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = tkhd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + + tkhd.skipBytes(version == 0 ? 8 : 16); + int trackId = tkhd.readInt(); + + tkhd.skipBytes(4); + boolean durationUnknown = true; + int durationPosition = tkhd.getPosition(); + int durationByteCount = version == 0 ? 4 : 8; + for (int i = 0; i < durationByteCount; i++) { + if (tkhd.data[durationPosition + i] != -1) { + durationUnknown = false; + break; + } + } + long duration; + if (durationUnknown) { + tkhd.skipBytes(durationByteCount); + duration = C.TIME_UNSET; + } else { + duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); + if (duration == 0) { + // 0 duration normally indicates that the file is fully fragmented (i.e. all of the media + // samples are in fragments). Treat as unknown. + duration = C.TIME_UNSET; + } + } + + tkhd.skipBytes(16); + int a00 = tkhd.readInt(); + int a01 = tkhd.readInt(); + tkhd.skipBytes(4); + int a10 = tkhd.readInt(); + int a11 = tkhd.readInt(); + + int rotationDegrees; + int fixedOne = 65536; + if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) { + rotationDegrees = 90; + } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) { + rotationDegrees = 270; + } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) { + rotationDegrees = 180; + } else { + // Only 0, 90, 180 and 270 are supported. Treat anything else as 0. + rotationDegrees = 0; + } + + return new TkhdData(trackId, duration, rotationDegrees); + } + + /** + * Parses an hdlr atom. + * + * @param hdlr The hdlr atom to decode. + * @return The handler value. + */ + private static int parseHdlr(ParsableByteArray hdlr) { + hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4); + return hdlr.readInt(); + } + + /** Returns the track type for a given handler value. */ + private static int getTrackTypeForHdlr(int hdlr) { + if (hdlr == TYPE_soun) { + return C.TRACK_TYPE_AUDIO; + } else if (hdlr == TYPE_vide) { + return C.TRACK_TYPE_VIDEO; + } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) { + return C.TRACK_TYPE_TEXT; + } else if (hdlr == TYPE_meta) { + return C.TRACK_TYPE_METADATA; + } else { + return C.TRACK_TYPE_UNKNOWN; + } + } + + /** + * Parses an mdhd atom (defined in 14496-12). + * + * @param mdhd The mdhd atom to decode. + * @return A pair consisting of the media timescale defined as the number of time units that pass + * in one second, and the language code. + */ + private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) { + mdhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = mdhd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + mdhd.skipBytes(version == 0 ? 8 : 16); + long timescale = mdhd.readUnsignedInt(); + mdhd.skipBytes(version == 0 ? 4 : 8); + int languageCode = mdhd.readUnsignedShort(); + String language = + "" + + (char) (((languageCode >> 10) & 0x1F) + 0x60) + + (char) (((languageCode >> 5) & 0x1F) + 0x60) + + (char) ((languageCode & 0x1F) + 0x60); + return Pair.create(timescale, language); + } + + /** + * Parses a stsd atom (defined in 14496-12). + * + * @param stsd The stsd atom to decode. + * @param trackId The track's identifier in its container. + * @param rotationDegrees The rotation of the track in degrees. + * @param language The language of the track. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @param isQuickTime True for QuickTime media. False otherwise. + * @return An object containing the parsed data. + */ + private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, + String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException { + stsd.setPosition(Atom.FULL_HEADER_SIZE); + int numberOfEntries = stsd.readInt(); + StsdData out = new StsdData(numberOfEntries); + for (int i = 0; i < numberOfEntries; i++) { + int childStartPosition = stsd.getPosition(); + int childAtomSize = stsd.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = stsd.readInt(); + if (childAtomType == Atom.TYPE_avc1 + || childAtomType == Atom.TYPE_avc3 + || childAtomType == Atom.TYPE_encv + || childAtomType == Atom.TYPE_mp4v + || childAtomType == Atom.TYPE_hvc1 + || childAtomType == Atom.TYPE_hev1 + || childAtomType == Atom.TYPE_s263 + || childAtomType == Atom.TYPE_vp08 + || childAtomType == Atom.TYPE_vp09 + || childAtomType == Atom.TYPE_av01 + || childAtomType == Atom.TYPE_dvav + || childAtomType == Atom.TYPE_dva1 + || childAtomType == Atom.TYPE_dvhe + || childAtomType == Atom.TYPE_dvh1) { + parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, + rotationDegrees, drmInitData, out, i); + } else if (childAtomType == Atom.TYPE_mp4a + || childAtomType == Atom.TYPE_enca + || childAtomType == Atom.TYPE_ac_3 + || childAtomType == Atom.TYPE_ec_3 + || childAtomType == Atom.TYPE_ac_4 + || childAtomType == Atom.TYPE_dtsc + || childAtomType == Atom.TYPE_dtse + || childAtomType == Atom.TYPE_dtsh + || childAtomType == Atom.TYPE_dtsl + || childAtomType == Atom.TYPE_samr + || childAtomType == Atom.TYPE_sawb + || childAtomType == Atom.TYPE_lpcm + || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos + || childAtomType == Atom.TYPE__mp3 + || childAtomType == Atom.TYPE_alac + || childAtomType == Atom.TYPE_alaw + || childAtomType == Atom.TYPE_ulaw + || childAtomType == Atom.TYPE_Opus + || childAtomType == Atom.TYPE_fLaC) { + parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, + language, isQuickTime, drmInitData, out, i); + } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g + || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp + || childAtomType == Atom.TYPE_c608) { + parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, + language, out); + } else if (childAtomType == Atom.TYPE_camm) { + out.format = Format.createSampleFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null); + } + stsd.setPosition(childStartPosition + childAtomSize); + } + return out; + } + + private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, + int atomSize, int trackId, String language, StsdData out) throws ParserException { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + + // Default values. + List<byte[]> initializationData = null; + long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE; + + String mimeType; + if (atomType == Atom.TYPE_TTML) { + mimeType = MimeTypes.APPLICATION_TTML; + } else if (atomType == Atom.TYPE_tx3g) { + mimeType = MimeTypes.APPLICATION_TX3G; + int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8; + byte[] sampleDescriptionData = new byte[sampleDescriptionLength]; + parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength); + initializationData = Collections.singletonList(sampleDescriptionData); + } else if (atomType == Atom.TYPE_wvtt) { + mimeType = MimeTypes.APPLICATION_MP4VTT; + } else if (atomType == Atom.TYPE_stpp) { + mimeType = MimeTypes.APPLICATION_TTML; + subsampleOffsetUs = 0; // Subsample timing is absolute. + } else if (atomType == Atom.TYPE_c608) { + // Defined by the QuickTime File Format specification. + mimeType = MimeTypes.APPLICATION_MP4CEA608; + out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; + } else { + // Never happens. + throw new IllegalStateException(); + } + + out.format = + Format.createTextSampleFormat( + Integer.toString(trackId), + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language, + /* accessibilityChannel= */ Format.NO_VALUE, + /* drmInitData= */ null, + subsampleOffsetUs, + initializationData); + } + + private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, + int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out, + int entryIndex) throws ParserException { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + + parent.skipBytes(16); + int width = parent.readUnsignedShort(); + int height = parent.readUnsignedShort(); + boolean pixelWidthHeightRatioFromPasp = false; + float pixelWidthHeightRatio = 1; + parent.skipBytes(50); + + int childPosition = parent.getPosition(); + if (atomType == Atom.TYPE_encv) { + Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData == null ? null + : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; + } + parent.setPosition(childPosition); + } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } + + List<byte[]> initializationData = null; + String mimeType = null; + String codecs = null; + byte[] projectionData = null; + @C.StereoMode + int stereoMode = Format.NO_VALUE; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childStartPosition = parent.getPosition(); + int childAtomSize = parent.readInt(); + if (childAtomSize == 0 && parent.getPosition() - position == size) { + // Handle optional terminating four zero bytes in MOV files. + break; + } + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_avcC) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H264; + parent.setPosition(childStartPosition + Atom.HEADER_SIZE); + AvcConfig avcConfig = AvcConfig.parse(parent); + initializationData = avcConfig.initializationData; + out.nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength; + if (!pixelWidthHeightRatioFromPasp) { + pixelWidthHeightRatio = avcConfig.pixelWidthAspectRatio; + } + } else if (childAtomType == Atom.TYPE_hvcC) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H265; + parent.setPosition(childStartPosition + Atom.HEADER_SIZE); + HevcConfig hevcConfig = HevcConfig.parse(parent); + initializationData = hevcConfig.initializationData; + out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; + } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { + DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); + if (dolbyVisionConfig != null) { + codecs = dolbyVisionConfig.codecs; + mimeType = MimeTypes.VIDEO_DOLBY_VISION; + } + } else if (childAtomType == Atom.TYPE_vpcC) { + Assertions.checkState(mimeType == null); + mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9; + } else if (childAtomType == Atom.TYPE_av1C) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_AV1; + } else if (childAtomType == Atom.TYPE_d263) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H263; + } else if (childAtomType == Atom.TYPE_esds) { + Assertions.checkState(mimeType == null); + Pair<String, byte[]> mimeTypeAndInitializationData = + parseEsdsFromParent(parent, childStartPosition); + mimeType = mimeTypeAndInitializationData.first; + initializationData = Collections.singletonList(mimeTypeAndInitializationData.second); + } else if (childAtomType == Atom.TYPE_pasp) { + pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition); + pixelWidthHeightRatioFromPasp = true; + } else if (childAtomType == Atom.TYPE_sv3d) { + projectionData = parseProjFromParent(parent, childStartPosition, childAtomSize); + } else if (childAtomType == Atom.TYPE_st3d) { + int version = parent.readUnsignedByte(); + parent.skipBytes(3); // Flags. + if (version == 0) { + int layout = parent.readUnsignedByte(); + switch (layout) { + case 0: + stereoMode = C.STEREO_MODE_MONO; + break; + case 1: + stereoMode = C.STEREO_MODE_TOP_BOTTOM; + break; + case 2: + stereoMode = C.STEREO_MODE_LEFT_RIGHT; + break; + case 3: + stereoMode = C.STEREO_MODE_STEREO_MESH; + break; + default: + break; + } + } + } + childPosition += childAtomSize; + } + + // If the media type was not recognized, ignore the track. + if (mimeType == null) { + return; + } + + out.format = + Format.createVideoSampleFormat( + Integer.toString(trackId), + mimeType, + codecs, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + width, + height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + /* colorInfo= */ null, + drmInitData); + } + + /** + * Parses the edts atom (defined in 14496-12 subsection 8.6.5). + * + * @param edtsAtom edts (edit box) atom to decode. + * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are + * not present. + */ + private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) { + Atom.LeafAtom elst; + if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) { + return Pair.create(null, null); + } + ParsableByteArray elstData = elst.data; + elstData.setPosition(Atom.HEADER_SIZE); + int fullAtom = elstData.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + int entryCount = elstData.readUnsignedIntToInt(); + long[] editListDurations = new long[entryCount]; + long[] editListMediaTimes = new long[entryCount]; + for (int i = 0; i < entryCount; i++) { + editListDurations[i] = + version == 1 ? elstData.readUnsignedLongToLong() : elstData.readUnsignedInt(); + editListMediaTimes[i] = version == 1 ? elstData.readLong() : elstData.readInt(); + int mediaRateInteger = elstData.readShort(); + if (mediaRateInteger != 1) { + // The extractor does not handle dwell edits (mediaRateInteger == 0). + throw new IllegalArgumentException("Unsupported media rate."); + } + elstData.skipBytes(2); + } + return Pair.create(editListDurations, editListMediaTimes); + } + + private static float parsePaspFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Atom.HEADER_SIZE); + int hSpacing = parent.readUnsignedIntToInt(); + int vSpacing = parent.readUnsignedIntToInt(); + return (float) hSpacing / vSpacing; + } + + private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, + int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, + StsdData out, int entryIndex) throws ParserException { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + + int quickTimeSoundDescriptionVersion = 0; + if (isQuickTime) { + quickTimeSoundDescriptionVersion = parent.readUnsignedShort(); + parent.skipBytes(6); + } else { + parent.skipBytes(8); + } + + int channelCount; + int sampleRate; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; + + if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { + channelCount = parent.readUnsignedShort(); + parent.skipBytes(6); // sampleSize, compressionId, packetSize. + sampleRate = parent.readUnsignedFixedPoint1616(); + + if (quickTimeSoundDescriptionVersion == 1) { + parent.skipBytes(16); + } + } else if (quickTimeSoundDescriptionVersion == 2) { + parent.skipBytes(16); // always[3,16,Minus2,0,65536], sizeOfStructOnly + + sampleRate = (int) Math.round(parent.readDouble()); + channelCount = parent.readUnsignedIntToInt(); + + // Skip always7F000000, sampleSize, formatSpecificFlags, constBytesPerAudioPacket, + // constLPCMFramesPerAudioPacket. + parent.skipBytes(20); + } else { + // Unsupported version. + return; + } + + int childPosition = parent.getPosition(); + if (atomType == Atom.TYPE_enca) { + Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData == null ? null + : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; + } + parent.setPosition(childPosition); + } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } + + // If the atom type determines a MIME type, set it immediately. + String mimeType = null; + if (atomType == Atom.TYPE_ac_3) { + mimeType = MimeTypes.AUDIO_AC3; + } else if (atomType == Atom.TYPE_ec_3) { + mimeType = MimeTypes.AUDIO_E_AC3; + } else if (atomType == Atom.TYPE_ac_4) { + mimeType = MimeTypes.AUDIO_AC4; + } else if (atomType == Atom.TYPE_dtsc) { + mimeType = MimeTypes.AUDIO_DTS; + } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) { + mimeType = MimeTypes.AUDIO_DTS_HD; + } else if (atomType == Atom.TYPE_dtse) { + mimeType = MimeTypes.AUDIO_DTS_EXPRESS; + } else if (atomType == Atom.TYPE_samr) { + mimeType = MimeTypes.AUDIO_AMR_NB; + } else if (atomType == Atom.TYPE_sawb) { + mimeType = MimeTypes.AUDIO_AMR_WB; + } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT; + } else if (atomType == Atom.TYPE_twos) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; + } else if (atomType == Atom.TYPE__mp3) { + mimeType = MimeTypes.AUDIO_MPEG; + } else if (atomType == Atom.TYPE_alac) { + mimeType = MimeTypes.AUDIO_ALAC; + } else if (atomType == Atom.TYPE_alaw) { + mimeType = MimeTypes.AUDIO_ALAW; + } else if (atomType == Atom.TYPE_ulaw) { + mimeType = MimeTypes.AUDIO_MLAW; + } else if (atomType == Atom.TYPE_Opus) { + mimeType = MimeTypes.AUDIO_OPUS; + } else if (atomType == Atom.TYPE_fLaC) { + mimeType = MimeTypes.AUDIO_FLAC; + } + + byte[] initializationData = null; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) { + int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition + : findEsdsPosition(parent, childPosition, childAtomSize); + if (esdsAtomPosition != C.POSITION_UNSET) { + Pair<String, byte[]> mimeTypeAndInitializationData = + parseEsdsFromParent(parent, esdsAtomPosition); + mimeType = mimeTypeAndInitializationData.first; + initializationData = mimeTypeAndInitializationData.second; + if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See [Internal: b/10903778]. + Pair<Integer, Integer> audioSpecificConfig = + CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; + } + } + } else if (childAtomType == Atom.TYPE_dac3) { + parent.setPosition(Atom.HEADER_SIZE + childPosition); + out.format = Ac3Util.parseAc3AnnexFFormat(parent, Integer.toString(trackId), language, + drmInitData); + } else if (childAtomType == Atom.TYPE_dec3) { + parent.setPosition(Atom.HEADER_SIZE + childPosition); + out.format = Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), language, + drmInitData); + } else if (childAtomType == Atom.TYPE_dac4) { + parent.setPosition(Atom.HEADER_SIZE + childPosition); + out.format = + Ac4Util.parseAc4AnnexEFormat(parent, Integer.toString(trackId), language, drmInitData); + } else if (childAtomType == Atom.TYPE_ddts) { + out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, + language); + } else if (childAtomType == Atom.TYPE_dOps) { + // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic + // Signature and the body of the dOps atom. + int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE; + initializationData = new byte[opusMagic.length + childAtomBodySize]; + System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); + parent.setPosition(childPosition + Atom.HEADER_SIZE); + parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); + } else if (childAtomType == Atom.TYPE_dfLa) { + int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; + initializationData = new byte[4 + childAtomBodySize]; + initializationData[0] = 0x66; // f + initializationData[1] = 0x4C; // L + initializationData[2] = 0x61; // a + initializationData[3] = 0x43; // C + parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); + parent.readBytes(initializationData, /* offset= */ 4, childAtomBodySize); + } else if (childAtomType == Atom.TYPE_alac) { + int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; + initializationData = new byte[childAtomBodySize]; + parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); + parent.readBytes(initializationData, /* offset= */ 0, childAtomBodySize); + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See https://github.com/google/ExoPlayer/pull/6629. + Pair<Integer, Integer> audioSpecificConfig = + CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; + } + childPosition += childAtomSize; + } + + if (out.format == null && mimeType != null) { + out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, + initializationData == null ? null : Collections.singletonList(initializationData), + drmInitData, 0, language); + } + } + + /** + * Returns the position of the esds box within a parent, or {@link C#POSITION_UNSET} if no esds + * box is found + */ + private static int findEsdsPosition(ParsableByteArray parent, int position, int size) { + int childAtomPosition = parent.getPosition(); + while (childAtomPosition - position < size) { + parent.setPosition(childAtomPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childType = parent.readInt(); + if (childType == Atom.TYPE_esds) { + return childAtomPosition; + } + childAtomPosition += childAtomSize; + } + return C.POSITION_UNSET; + } + + /** + * Returns codec-specific initialization data contained in an esds box. + */ + private static Pair<String, byte[]> parseEsdsFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Atom.HEADER_SIZE + 4); + // Start of the ES_Descriptor (defined in 14496-1) + parent.skipBytes(1); // ES_Descriptor tag + parseExpandableClassSize(parent); + parent.skipBytes(2); // ES_ID + + int flags = parent.readUnsignedByte(); + if ((flags & 0x80 /* streamDependenceFlag */) != 0) { + parent.skipBytes(2); + } + if ((flags & 0x40 /* URL_Flag */) != 0) { + parent.skipBytes(parent.readUnsignedShort()); + } + if ((flags & 0x20 /* OCRstreamFlag */) != 0) { + parent.skipBytes(2); + } + + // Start of the DecoderConfigDescriptor (defined in 14496-1) + parent.skipBytes(1); // DecoderConfigDescriptor tag + parseExpandableClassSize(parent); + + // Set the MIME type based on the object type indication (14496-1 table 5). + int objectTypeIndication = parent.readUnsignedByte(); + String mimeType = getMimeTypeFromMp4ObjectType(objectTypeIndication); + if (MimeTypes.AUDIO_MPEG.equals(mimeType) + || MimeTypes.AUDIO_DTS.equals(mimeType) + || MimeTypes.AUDIO_DTS_HD.equals(mimeType)) { + return Pair.create(mimeType, null); + } + + parent.skipBytes(12); + + // Start of the DecoderSpecificInfo. + parent.skipBytes(1); // DecoderSpecificInfo tag + int initializationDataSize = parseExpandableClassSize(parent); + byte[] initializationData = new byte[initializationDataSize]; + parent.readBytes(initializationData, 0, initializationDataSize); + return Pair.create(mimeType, initializationData); + } + + /** + * Parses encryption data from an audio/video sample entry, returning a pair consisting of the + * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common + * encryption sinf atom was present. + */ + private static Pair<Integer, TrackEncryptionBox> parseSampleEntryEncryptionData( + ParsableByteArray parent, int position, int size) { + int childPosition = parent.getPosition(); + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_sinf) { + Pair<Integer, TrackEncryptionBox> result = parseCommonEncryptionSinfFromParent(parent, + childPosition, childAtomSize); + if (result != null) { + return result; + } + } + childPosition += childAtomSize; + } + return null; + } + + /* package */ static Pair<Integer, TrackEncryptionBox> parseCommonEncryptionSinfFromParent( + ParsableByteArray parent, int position, int size) { + int childPosition = position + Atom.HEADER_SIZE; + int schemeInformationBoxPosition = C.POSITION_UNSET; + int schemeInformationBoxSize = 0; + String schemeType = null; + Integer dataFormat = null; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_frma) { + dataFormat = parent.readInt(); + } else if (childAtomType == Atom.TYPE_schm) { + parent.skipBytes(4); + // Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1. + schemeType = parent.readString(4); + } else if (childAtomType == Atom.TYPE_schi) { + schemeInformationBoxPosition = childPosition; + schemeInformationBoxSize = childAtomSize; + } + childPosition += childAtomSize; + } + + if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType) + || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) { + Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); + Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, + "schi atom is mandatory"); + TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, + schemeInformationBoxSize, schemeType); + Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory"); + return Pair.create(dataFormat, encryptionBox); + } else { + return null; + } + } + + private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, + int size, String schemeType) { + int childPosition = position + Atom.HEADER_SIZE; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_tenc) { + int fullAtom = parent.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + parent.skipBytes(1); // reserved = 0. + int defaultCryptByteBlock = 0; + int defaultSkipByteBlock = 0; + if (version == 0) { + parent.skipBytes(1); // reserved = 0. + } else /* version 1 or greater */ { + int patternByte = parent.readUnsignedByte(); + defaultCryptByteBlock = (patternByte & 0xF0) >> 4; + defaultSkipByteBlock = patternByte & 0x0F; + } + boolean defaultIsProtected = parent.readUnsignedByte() == 1; + int defaultPerSampleIvSize = parent.readUnsignedByte(); + byte[] defaultKeyId = new byte[16]; + parent.readBytes(defaultKeyId, 0, defaultKeyId.length); + byte[] constantIv = null; + if (defaultIsProtected && defaultPerSampleIvSize == 0) { + int constantIvSize = parent.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + parent.readBytes(constantIv, 0, constantIvSize); + } + return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize, + defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv); + } + childPosition += childAtomSize; + } + return null; + } + + /** + * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. + */ + private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) { + int childPosition = position + Atom.HEADER_SIZE; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_proj) { + return Arrays.copyOfRange(parent.data, childPosition, childPosition + childAtomSize); + } + childPosition += childAtomSize; + } + return null; + } + + /** + * Parses the size of an expandable class, as specified by ISO 14496-1 subsection 8.3.3. + */ + private static int parseExpandableClassSize(ParsableByteArray data) { + int currentByte = data.readUnsignedByte(); + int size = currentByte & 0x7F; + while ((currentByte & 0x80) == 0x80) { + currentByte = data.readUnsignedByte(); + size = (size << 7) | (currentByte & 0x7F); + } + return size; + } + + /** Returns whether it's possible to apply the specified edit using gapless playback info. */ + private static boolean canApplyEditWithGaplessInfo( + long[] timestamps, long duration, long editStartTime, long editEndTime) { + int lastIndex = timestamps.length - 1; + int latestDelayIndex = Util.constrainValue(MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex); + int earliestPaddingIndex = + Util.constrainValue(timestamps.length - MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex); + return timestamps[0] <= editStartTime + && editStartTime < timestamps[latestDelayIndex] + && timestamps[earliestPaddingIndex] < editEndTime + && editEndTime <= duration; + } + + private AtomParsers() { + // Prevent instantiation. + } + + private static final class ChunkIterator { + + public final int length; + + public int index; + public int numSamples; + public long offset; + + private final boolean chunkOffsetsAreLongs; + private final ParsableByteArray chunkOffsets; + private final ParsableByteArray stsc; + + private int nextSamplesPerChunkChangeIndex; + private int remainingSamplesPerChunkChanges; + + public ChunkIterator(ParsableByteArray stsc, ParsableByteArray chunkOffsets, + boolean chunkOffsetsAreLongs) { + this.stsc = stsc; + this.chunkOffsets = chunkOffsets; + this.chunkOffsetsAreLongs = chunkOffsetsAreLongs; + chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE); + length = chunkOffsets.readUnsignedIntToInt(); + stsc.setPosition(Atom.FULL_HEADER_SIZE); + remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); + Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); + index = -1; + } + + public boolean moveNext() { + if (++index == length) { + return false; + } + offset = chunkOffsetsAreLongs ? chunkOffsets.readUnsignedLongToLong() + : chunkOffsets.readUnsignedInt(); + if (index == nextSamplesPerChunkChangeIndex) { + numSamples = stsc.readUnsignedIntToInt(); + stsc.skipBytes(4); // Skip sample_description_index + nextSamplesPerChunkChangeIndex = --remainingSamplesPerChunkChanges > 0 + ? (stsc.readUnsignedIntToInt() - 1) : C.INDEX_UNSET; + } + return true; + } + + } + + /** + * Holds data parsed from a tkhd atom. + */ + private static final class TkhdData { + + private final int id; + private final long duration; + private final int rotationDegrees; + + public TkhdData(int id, long duration, int rotationDegrees) { + this.id = id; + this.duration = duration; + this.rotationDegrees = rotationDegrees; + } + + } + + /** + * Holds data parsed from an stsd atom and its children. + */ + private static final class StsdData { + + public static final int STSD_HEADER_SIZE = 8; + + public final TrackEncryptionBox[] trackEncryptionBoxes; + + public Format format; + public int nalUnitLengthFieldLength; + @Track.Transformation + public int requiredSampleTransformation; + + public StsdData(int numberOfEntries) { + trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; + requiredSampleTransformation = Track.TRANSFORMATION_NONE; + } + + } + + /** + * A box containing sample sizes (e.g. stsz, stz2). + */ + private interface SampleSizeBox { + + /** + * Returns the number of samples. + */ + int getSampleCount(); + + /** + * Returns the size for the next sample. + */ + int readNextSampleSize(); + + /** + * Returns whether samples have a fixed size. + */ + boolean isFixedSampleSize(); + + } + + /** + * An stsz sample size box. + */ + /* package */ static final class StszSampleSizeBox implements SampleSizeBox { + + private final int fixedSampleSize; + private final int sampleCount; + private final ParsableByteArray data; + + public StszSampleSizeBox(Atom.LeafAtom stszAtom) { + data = stszAtom.data; + data.setPosition(Atom.FULL_HEADER_SIZE); + fixedSampleSize = data.readUnsignedIntToInt(); + sampleCount = data.readUnsignedIntToInt(); + } + + @Override + public int getSampleCount() { + return sampleCount; + } + + @Override + public int readNextSampleSize() { + return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize; + } + + @Override + public boolean isFixedSampleSize() { + return fixedSampleSize != 0; + } + + } + + /** + * An stz2 sample size box. + */ + /* package */ static final class Stz2SampleSizeBox implements SampleSizeBox { + + private final ParsableByteArray data; + private final int sampleCount; + private final int fieldSize; // Can be 4, 8, or 16. + + // Used only if fieldSize == 4. + private int sampleIndex; + private int currentByte; + + public Stz2SampleSizeBox(Atom.LeafAtom stz2Atom) { + data = stz2Atom.data; + data.setPosition(Atom.FULL_HEADER_SIZE); + fieldSize = data.readUnsignedIntToInt() & 0x000000FF; + sampleCount = data.readUnsignedIntToInt(); + } + + @Override + public int getSampleCount() { + return sampleCount; + } + + @Override + public int readNextSampleSize() { + if (fieldSize == 8) { + return data.readUnsignedByte(); + } else if (fieldSize == 16) { + return data.readUnsignedShort(); + } else { + // fieldSize == 4. + if ((sampleIndex++ % 2) == 0) { + // Read the next byte into our cached byte when we are reading the upper bits. + currentByte = data.readUnsignedByte(); + // Read the upper bits from the byte and shift them to the lower 4 bits. + return (currentByte & 0xF0) >> 4; + } else { + // Mask out the upper 4 bits of the last byte we read. + return currentByte & 0x0F; + } + } + } + + @Override + public boolean isFixedSampleSize() { + return false; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java new file mode 100644 index 0000000000..0942673435 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +/* package */ final class DefaultSampleValues { + + public final int sampleDescriptionIndex; + public final int duration; + public final int size; + public final int flags; + + public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) { + this.sampleDescriptionIndex = sampleDescriptionIndex; + this.duration = duration; + this.size = size; + this.flags = flags; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java new file mode 100644 index 0000000000..78d30ba582 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Rechunks fixed sample size media in which every sample is a key frame (e.g. uncompressed audio). + */ +/* package */ final class FixedSampleSizeRechunker { + + /** + * The result of a rechunking operation. + */ + public static final class Results { + + public final long[] offsets; + public final int[] sizes; + public final int maximumSize; + public final long[] timestamps; + public final int[] flags; + public final long duration; + + private Results( + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestamps, + int[] flags, + long duration) { + this.offsets = offsets; + this.sizes = sizes; + this.maximumSize = maximumSize; + this.timestamps = timestamps; + this.flags = flags; + this.duration = duration; + } + + } + + /** + * Maximum number of bytes for each buffer in rechunked output. + */ + private static final int MAX_SAMPLE_SIZE = 8 * 1024; + + /** + * Rechunk the given fixed sample size input to produce a new sequence of samples. + * + * @param fixedSampleSize Size in bytes of each sample. + * @param chunkOffsets Chunk offsets in the MP4 stream to rechunk. + * @param chunkSampleCounts Sample counts for each of the MP4 stream's chunks. + * @param timestampDeltaInTimeUnits Timestamp delta between each sample in time units. + */ + public static Results rechunk(int fixedSampleSize, long[] chunkOffsets, int[] chunkSampleCounts, + long timestampDeltaInTimeUnits) { + int maxSampleCount = MAX_SAMPLE_SIZE / fixedSampleSize; + + // Count the number of new, rechunked buffers. + int rechunkedSampleCount = 0; + for (int chunkSampleCount : chunkSampleCounts) { + rechunkedSampleCount += Util.ceilDivide(chunkSampleCount, maxSampleCount); + } + + long[] offsets = new long[rechunkedSampleCount]; + int[] sizes = new int[rechunkedSampleCount]; + int maximumSize = 0; + long[] timestamps = new long[rechunkedSampleCount]; + int[] flags = new int[rechunkedSampleCount]; + + int originalSampleIndex = 0; + int newSampleIndex = 0; + for (int chunkIndex = 0; chunkIndex < chunkSampleCounts.length; chunkIndex++) { + int chunkSamplesRemaining = chunkSampleCounts[chunkIndex]; + long sampleOffset = chunkOffsets[chunkIndex]; + + while (chunkSamplesRemaining > 0) { + int bufferSampleCount = Math.min(maxSampleCount, chunkSamplesRemaining); + + offsets[newSampleIndex] = sampleOffset; + sizes[newSampleIndex] = fixedSampleSize * bufferSampleCount; + maximumSize = Math.max(maximumSize, sizes[newSampleIndex]); + timestamps[newSampleIndex] = (timestampDeltaInTimeUnits * originalSampleIndex); + flags[newSampleIndex] = C.BUFFER_FLAG_KEY_FRAME; + + sampleOffset += sizes[newSampleIndex]; + originalSampleIndex += bufferSampleCount; + + chunkSamplesRemaining -= bufferSampleCount; + newSampleIndex++; + } + } + long duration = timestampDeltaInTimeUnits * originalSampleIndex; + + return new Results(offsets, sizes, maximumSize, timestamps, flags, duration); + } + + private FixedSampleSizeRechunker() { + // Prevent instantiation. + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java new file mode 100644 index 0000000000..291a9ade27 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -0,0 +1,1660 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import android.util.Pair; +import android.util.SparseArray; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessage; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +/** Extracts data from the FMP4 container format. */ +@SuppressWarnings("ConstantField") +public class FragmentedMp4Extractor implements Extractor { + + /** Factory for {@link FragmentedMp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = + () -> new Extractor[] {new FragmentedMp4Extractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag values are {@link + * #FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME}, {@link #FLAG_WORKAROUND_IGNORE_TFDT_BOX}, + * {@link #FLAG_ENABLE_EMSG_TRACK}, {@link #FLAG_SIDELOADED} and {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, + FLAG_WORKAROUND_IGNORE_TFDT_BOX, + FLAG_ENABLE_EMSG_TRACK, + FLAG_SIDELOADED, + FLAG_WORKAROUND_IGNORE_EDIT_LISTS + }) + public @interface Flags {} + /** + * Flag to work around an issue in some video streams where every frame is marked as a sync frame. + * The workaround overrides the sync frame flags in the stream, forcing them to false except for + * the first sample in each segment. + * <p> + * This flag does nothing if the stream is not a video stream. + */ + public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; + /** Flag to ignore any tfdt boxes in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2 + /** + * Flag to indicate that the extractor should output an event message metadata track. Any event + * messages in the stream will be delivered as samples to this track. + */ + public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4 + /** + * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 + * container. + */ + private static final int FLAG_SIDELOADED = 1 << 3; // 8 + /** Flag to ignore any edit lists in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16 + + private static final String TAG = "FragmentedMp4Extractor"; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967; + + private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = + new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; + private static final Format EMSG_FORMAT = + Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + + // Parser states. + private static final int STATE_READING_ATOM_HEADER = 0; + private static final int STATE_READING_ATOM_PAYLOAD = 1; + private static final int STATE_READING_ENCRYPTION_DATA = 2; + private static final int STATE_READING_SAMPLE_START = 3; + private static final int STATE_READING_SAMPLE_CONTINUE = 4; + + // Workarounds. + @Flags private final int flags; + @Nullable private final Track sideloadedTrack; + + // Sideloaded data. + private final List<Format> closedCaptionFormats; + + // Track-linked data bundle, accessible as a whole through trackID. + private final SparseArray<TrackBundle> trackBundles; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalPrefix; + private final ParsableByteArray nalBuffer; + private final byte[] scratchBytes; + private final ParsableByteArray scratch; + + // Adjusts sample timestamps. + @Nullable private final TimestampAdjuster timestampAdjuster; + + private final EventMessageEncoder eventMessageEncoder; + + // Parser state. + private final ParsableByteArray atomHeader; + private final ArrayDeque<ContainerAtom> containerAtoms; + private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos; + @Nullable private final TrackOutput additionalEmsgTrackOutput; + + private int parserState; + private int atomType; + private long atomSize; + private int atomHeaderBytesRead; + private ParsableByteArray atomData; + private long endOfMdatPosition; + private int pendingMetadataSampleBytes; + private long pendingSeekTimeUs; + + private long durationUs; + private long segmentIndexEarliestPresentationTimeUs; + private TrackBundle currentTrackBundle; + private int sampleSize; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; + private boolean processSeiNalUnitPayload; + + // Extractor output. + private ExtractorOutput extractorOutput; + private TrackOutput[] emsgTrackOutputs; + private TrackOutput[] cea608TrackOutputs; + + // Whether extractorOutput.seekMap has been called. + private boolean haveOutputSeekMap; + + public FragmentedMp4Extractor() { + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public FragmentedMp4Extractor(@Flags int flags) { + this(flags, /* timestampAdjuster= */ null); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + */ + public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { + this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack) { + this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + List<Format> closedCaptionFormats) { + this( + flags, + timestampAdjuster, + sideloadedTrack, + closedCaptionFormats, + /* additionalEmsgTrackOutput= */ null); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages + * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special + * handling of emsg messages for players is not required. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + List<Format> closedCaptionFormats, + @Nullable TrackOutput additionalEmsgTrackOutput) { + this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); + this.timestampAdjuster = timestampAdjuster; + this.sideloadedTrack = sideloadedTrack; + this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); + this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; + eventMessageEncoder = new EventMessageEncoder(); + atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalPrefix = new ParsableByteArray(5); + nalBuffer = new ParsableByteArray(); + scratchBytes = new byte[16]; + scratch = new ParsableByteArray(scratchBytes); + containerAtoms = new ArrayDeque<>(); + pendingMetadataSampleInfos = new ArrayDeque<>(); + trackBundles = new SparseArray<>(); + durationUs = C.TIME_UNSET; + pendingSeekTimeUs = C.TIME_UNSET; + segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; + enterReadingAtomHeaderState(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffFragmented(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + if (sideloadedTrack != null) { + TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); + bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); + trackBundles.put(0, bundle); + maybeInitExtraTracks(); + extractorOutput.endTracks(); + } + } + + @Override + public void seek(long position, long timeUs) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).reset(); + } + pendingMetadataSampleInfos.clear(); + pendingMetadataSampleBytes = 0; + pendingSeekTimeUs = timeUs; + containerAtoms.clear(); + enterReadingAtomHeaderState(); + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + while (true) { + switch (parserState) { + case STATE_READING_ATOM_HEADER: + if (!readAtomHeader(input)) { + return Extractor.RESULT_END_OF_INPUT; + } + break; + case STATE_READING_ATOM_PAYLOAD: + readAtomPayload(input); + break; + case STATE_READING_ENCRYPTION_DATA: + readEncryptionData(input); + break; + default: + if (readSample(input)) { + return RESULT_CONTINUE; + } + } + } + } + + private void enterReadingAtomHeaderState() { + parserState = STATE_READING_ATOM_HEADER; + atomHeaderBytesRead = 0; + } + + private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + if (atomHeaderBytesRead == 0) { + // Read the standard length atom header. + if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { + return false; + } + atomHeaderBytesRead = Atom.HEADER_SIZE; + atomHeader.setPosition(0); + atomSize = atomHeader.readUnsignedInt(); + atomType = atomHeader.readInt(); + } + + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. + int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; + input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); + atomHeaderBytesRead += headerBytesRemaining; + atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } + } + + if (atomSize < atomHeaderBytesRead) { + throw new ParserException("Atom size less than header length (unsupported)."); + } + + long atomPosition = input.getPosition() - atomHeaderBytesRead; + if (atomType == Atom.TYPE_moof) { + // The data positions may be updated when parsing the tfhd/trun. + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + TrackFragment fragment = trackBundles.valueAt(i).fragment; + fragment.atomPosition = atomPosition; + fragment.auxiliaryDataPosition = atomPosition; + fragment.dataPosition = atomPosition; + } + } + + if (atomType == Atom.TYPE_mdat) { + currentTrackBundle = null; + endOfMdatPosition = atomPosition + atomSize; + if (!haveOutputSeekMap) { + // This must be the first mdat in the stream. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); + haveOutputSeekMap = true; + } + parserState = STATE_READING_ENCRYPTION_DATA; + return true; + } + + if (shouldParseContainerAtom(atomType)) { + long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; + containerAtoms.push(new ContainerAtom(atomType, endPosition)); + if (atomSize == atomHeaderBytesRead) { + processAtomEnded(endPosition); + } else { + // Start reading the first child atom. + enterReadingAtomHeaderState(); + } + } else if (shouldParseLeafAtom(atomType)) { + if (atomHeaderBytesRead != Atom.HEADER_SIZE) { + throw new ParserException("Leaf atom defines extended atom size (unsupported)."); + } + if (atomSize > Integer.MAX_VALUE) { + throw new ParserException("Leaf atom with length > 2147483647 (unsupported)."); + } + atomData = new ParsableByteArray((int) atomSize); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); + parserState = STATE_READING_ATOM_PAYLOAD; + } else { + if (atomSize > Integer.MAX_VALUE) { + throw new ParserException("Skipping atom with length > 2147483647 (unsupported)."); + } + atomData = null; + parserState = STATE_READING_ATOM_PAYLOAD; + } + + return true; + } + + private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException { + int atomPayloadSize = (int) atomSize - atomHeaderBytesRead; + if (atomData != null) { + input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize); + onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition()); + } else { + input.skipFully(atomPayloadSize); + } + processAtomEnded(input.getPosition()); + } + + private void processAtomEnded(long atomEndPosition) throws ParserException { + while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { + onContainerAtomRead(containerAtoms.pop()); + } + enterReadingAtomHeaderState(); + } + + private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException { + if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(leaf); + } else if (leaf.type == Atom.TYPE_sidx) { + Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition); + segmentIndexEarliestPresentationTimeUs = result.first; + extractorOutput.seekMap(result.second); + haveOutputSeekMap = true; + } else if (leaf.type == Atom.TYPE_emsg) { + onEmsgLeafAtomRead(leaf.data); + } + } + + private void onContainerAtomRead(ContainerAtom container) throws ParserException { + if (container.type == Atom.TYPE_moov) { + onMoovContainerAtomRead(container); + } else if (container.type == Atom.TYPE_moof) { + onMoofContainerAtomRead(container); + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(container); + } + } + + private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { + Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); + + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); + + // Read declaration of track fragments in the Moov box. + ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); + SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>(); + long duration = C.TIME_UNSET; + int mvexChildrenSize = mvex.leafChildren.size(); + for (int i = 0; i < mvexChildrenSize; i++) { + Atom.LeafAtom atom = mvex.leafChildren.get(i); + if (atom.type == Atom.TYPE_trex) { + Pair<Integer, DefaultSampleValues> trexData = parseTrex(atom.data); + defaultSampleValuesArray.put(trexData.first, trexData.second); + } else if (atom.type == Atom.TYPE_mehd) { + duration = parseMehd(atom.data); + } + } + + // Construction of tracks. + SparseArray<Track> tracks = new SparseArray<>(); + int moovContainerChildrenSize = moov.containerChildren.size(); + for (int i = 0; i < moovContainerChildrenSize; i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type == Atom.TYPE_trak) { + Track track = + modifyTrack( + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + duration, + drmInitData, + (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, + false)); + if (track != null) { + tracks.put(track.id, track); + } + } + } + + int trackCount = tracks.size(); + if (trackBundles.size() == 0) { + // We need to create the track bundles. + for (int i = 0; i < trackCount; i++) { + Track track = tracks.valueAt(i); + TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); + trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); + trackBundles.put(track.id, trackBundle); + durationUs = Math.max(durationUs, track.durationUs); + } + maybeInitExtraTracks(); + extractorOutput.endTracks(); + } else { + Assertions.checkState(trackBundles.size() == trackCount); + for (int i = 0; i < trackCount; i++) { + Track track = tracks.valueAt(i); + trackBundles + .get(track.id) + .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); + } + } + } + + @Nullable + protected Track modifyTrack(@Nullable Track track) { + return track; + } + + private DefaultSampleValues getDefaultSampleValues( + SparseArray<DefaultSampleValues> defaultSampleValuesArray, int trackId) { + if (defaultSampleValuesArray.size() == 1) { + // Ignore track id if there is only one track to cope with non-matching track indices. + // See https://github.com/google/ExoPlayer/issues/4477. + return defaultSampleValuesArray.valueAt(/* index= */ 0); + } + return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId)); + } + + private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { + parseMoof(moof, trackBundles, flags, scratchBytes); + + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); + if (drmInitData != null) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).updateDrmInitData(drmInitData); + } + } + // If we have a pending seek, advance tracks to their preceding sync frames. + if (pendingSeekTimeUs != C.TIME_UNSET) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).seek(pendingSeekTimeUs); + } + pendingSeekTimeUs = C.TIME_UNSET; + } + } + + private void maybeInitExtraTracks() { + if (emsgTrackOutputs == null) { + emsgTrackOutputs = new TrackOutput[2]; + int emsgTrackOutputCount = 0; + if (additionalEmsgTrackOutput != null) { + emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput; + } + if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) { + emsgTrackOutputs[emsgTrackOutputCount++] = + extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); + } + emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount); + + for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) { + eventMessageTrackOutput.format(EMSG_FORMAT); + } + } + if (cea608TrackOutputs == null) { + cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; + for (int i = 0; i < cea608TrackOutputs.length; i++) { + TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT); + output.format(closedCaptionFormats.get(i)); + cea608TrackOutputs[i] = output; + } + } + } + + /** Handles an emsg atom (defined in 23009-1). */ + private void onEmsgLeafAtomRead(ParsableByteArray atom) { + if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { + return; + } + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + String schemeIdUri; + String value; + long timescale; + long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0 + long sampleTimeUs = C.TIME_UNSET; + long durationMs; + long id; + switch (version) { + case 0: + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + timescale = atom.readUnsignedInt(); + presentationTimeDeltaUs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + } + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + break; + case 1: + timescale = atom.readUnsignedInt(); + sampleTimeUs = + Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale); + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + break; + default: + Log.w(TAG, "Skipping unsupported emsg version: " + version); + return; + } + + byte[] messageData = new byte[atom.bytesLeft()]; + atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft()); + EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData); + ParsableByteArray encodedEventMessage = + new ParsableByteArray(eventMessageEncoder.encode(eventMessage)); + int sampleSize = encodedEventMessage.bytesLeft(); + + // Output the sample data. + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + encodedEventMessage.setPosition(0); + emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); + } + + // Output the sample metadata. This is made a little complicated because emsg-v0 atoms + // have presentation time *delta* while v1 atoms have absolute presentation time. + if (sampleTimeUs == C.TIME_UNSET) { + // We need the first sample timestamp in the segment before we can output the metadata. + pendingMetadataSampleInfos.addLast( + new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); + pendingMetadataSampleBytes += sampleSize; + } else { + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); + } + } + } + + /** Parses a trex atom (defined in 14496-12). */ + private static Pair<Integer, DefaultSampleValues> parseTrex(ParsableByteArray trex) { + trex.setPosition(Atom.FULL_HEADER_SIZE); + int trackId = trex.readInt(); + int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; + int defaultSampleDuration = trex.readUnsignedIntToInt(); + int defaultSampleSize = trex.readUnsignedIntToInt(); + int defaultSampleFlags = trex.readInt(); + + return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags)); + } + + /** + * Parses an mehd atom (defined in 14496-12). + */ + private static long parseMehd(ParsableByteArray mehd) { + mehd.setPosition(Atom.HEADER_SIZE); + int fullAtom = mehd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong(); + } + + private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray, + @Flags int flags, byte[] extendedTypeScratch) throws ParserException { + int moofContainerChildrenSize = moof.containerChildren.size(); + for (int i = 0; i < moofContainerChildrenSize; i++) { + Atom.ContainerAtom child = moof.containerChildren.get(i); + // TODO: Support multiple traf boxes per track in a single moof. + if (child.type == Atom.TYPE_traf) { + parseTraf(child, trackBundleArray, flags, extendedTypeScratch); + } + } + } + + /** + * Parses a traf atom (defined in 14496-12). + */ + private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray, + @Flags int flags, byte[] extendedTypeScratch) throws ParserException { + LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); + TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); + if (trackBundle == null) { + return; + } + + TrackFragment fragment = trackBundle.fragment; + long decodeTime = fragment.nextFragmentDecodeTime; + trackBundle.reset(); + + LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); + if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) { + decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); + } + + parseTruns(traf, trackBundle, decodeTime, flags); + + TrackEncryptionBox encryptionBox = trackBundle.track + .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); + if (saiz != null) { + parseSaiz(encryptionBox, saiz.data, fragment); + } + + LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); + if (saio != null) { + parseSaio(saio.data, fragment); + } + + LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); + if (senc != null) { + parseSenc(senc.data, fragment); + } + + LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); + LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); + if (sbgp != null && sgpd != null) { + parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, + fragment); + } + + int leafChildrenSize = traf.leafChildren.size(); + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom atom = traf.leafChildren.get(i); + if (atom.type == Atom.TYPE_uuid) { + parseUuid(atom.data, fragment, extendedTypeScratch); + } + } + } + + private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime, + @Flags int flags) { + int trunCount = 0; + int totalSampleCount = 0; + List<LeafAtom> leafChildren = traf.leafChildren; + int leafChildrenSize = leafChildren.size(); + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == Atom.TYPE_trun) { + ParsableByteArray trunData = atom.data; + trunData.setPosition(Atom.FULL_HEADER_SIZE); + int trunSampleCount = trunData.readUnsignedIntToInt(); + if (trunSampleCount > 0) { + totalSampleCount += trunSampleCount; + trunCount++; + } + } + } + trackBundle.currentTrackRunIndex = 0; + trackBundle.currentSampleInTrackRun = 0; + trackBundle.currentSampleIndex = 0; + trackBundle.fragment.initTables(trunCount, totalSampleCount); + + int trunIndex = 0; + int trunStartPosition = 0; + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom trun = leafChildren.get(i); + if (trun.type == Atom.TYPE_trun) { + trunStartPosition = parseTrun(trackBundle, trunIndex++, decodeTime, flags, trun.data, + trunStartPosition); + } + } + } + + private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, + TrackFragment out) throws ParserException { + int vectorSize = encryptionBox.perSampleIvSize; + saiz.setPosition(Atom.HEADER_SIZE); + int fullAtom = saiz.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + if ((flags & 0x01) == 1) { + saiz.skipBytes(8); + } + int defaultSampleInfoSize = saiz.readUnsignedByte(); + + int sampleCount = saiz.readUnsignedIntToInt(); + if (sampleCount != out.sampleCount) { + throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); + } + + int totalSize = 0; + if (defaultSampleInfoSize == 0) { + boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable; + for (int i = 0; i < sampleCount; i++) { + int sampleInfoSize = saiz.readUnsignedByte(); + totalSize += sampleInfoSize; + sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize; + } + } else { + boolean subsampleEncryption = defaultSampleInfoSize > vectorSize; + totalSize += defaultSampleInfoSize * sampleCount; + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + } + out.initEncryptionData(totalSize); + } + + /** + * Parses a saio atom (defined in 14496-12). + * + * @param saio The saio atom to decode. + * @param out The {@link TrackFragment} to populate with data from the saio atom. + */ + private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException { + saio.setPosition(Atom.HEADER_SIZE); + int fullAtom = saio.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + if ((flags & 0x01) == 1) { + saio.skipBytes(8); + } + + int entryCount = saio.readUnsignedIntToInt(); + if (entryCount != 1) { + // We only support one trun element currently, so always expect one entry. + throw new ParserException("Unexpected saio entry count: " + entryCount); + } + + int version = Atom.parseFullAtomVersion(fullAtom); + out.auxiliaryDataPosition += + version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong(); + } + + /** + * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and + * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer + * to any {@link TrackBundle}, {@code null} is returned and no changes are made. + * + * @param tfhd The tfhd atom to decode. + * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed. + * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd + * does not refer to any {@link TrackBundle}. + */ + private static TrackBundle parseTfhd( + ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) { + tfhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = tfhd.readInt(); + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + int trackId = tfhd.readInt(); + TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); + if (trackBundle == null) { + return null; + } + if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) { + long baseDataPosition = tfhd.readUnsignedLongToLong(); + trackBundle.fragment.dataPosition = baseDataPosition; + trackBundle.fragment.auxiliaryDataPosition = baseDataPosition; + } + + DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; + int defaultSampleDescriptionIndex = + ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) + ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; + int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; + int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size; + int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags; + trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags); + return trackBundle; + } + + private static @Nullable TrackBundle getTrackBundle( + SparseArray<TrackBundle> trackBundles, int trackId) { + if (trackBundles.size() == 1) { + // Ignore track id if there is only one track. This is either because we have a side-loaded + // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see + // https://github.com/google/ExoPlayer/issues/4083). + return trackBundles.valueAt(/* index= */ 0); + } + return trackBundles.get(trackId); + } + + /** + * Parses a tfdt atom (defined in 14496-12). + * + * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the + * media, expressed in the media's timescale. + */ + private static long parseTfdt(ParsableByteArray tfdt) { + tfdt.setPosition(Atom.HEADER_SIZE); + int fullAtom = tfdt.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt(); + } + + /** + * Parses a trun atom (defined in 14496-12). + * + * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into + * which parsed data should be placed. + * @param index Index of the track run in the fragment. + * @param decodeTime The decode time of the first sample in the fragment run. + * @param flags Flags to allow any required workaround to be executed. + * @param trun The trun atom to decode. + * @return The starting position of samples for the next run. + */ + private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime, + @Flags int flags, ParsableByteArray trun, int trackRunStart) { + trun.setPosition(Atom.HEADER_SIZE); + int fullAtom = trun.readInt(); + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + + Track track = trackBundle.track; + TrackFragment fragment = trackBundle.fragment; + DefaultSampleValues defaultSampleValues = fragment.header; + + fragment.trunLength[index] = trun.readUnsignedIntToInt(); + fragment.trunDataPosition[index] = fragment.dataPosition; + if ((atomFlags & 0x01 /* data_offset_present */) != 0) { + fragment.trunDataPosition[index] += trun.readInt(); + } + + boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; + int firstSampleFlags = defaultSampleValues.flags; + if (firstSampleFlagsPresent) { + firstSampleFlags = trun.readUnsignedIntToInt(); + } + + boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0; + boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0; + boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0; + boolean sampleCompositionTimeOffsetsPresent = + (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0; + + // Offset to the entire video timeline. In the presence of B-frames this is usually used to + // ensure that the first frame's presentation timestamp is zero. + long edtsOffset = 0; + + // Currently we only support a single edit that moves the entire media timeline (indicated by + // duration == 0). Other uses of edit lists are uncommon and unsupported. + if (track.editListDurations != null && track.editListDurations.length == 1 + && track.editListDurations[0] == 0) { + edtsOffset = + Util.scaleLargeTimestamp( + track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); + } + + int[] sampleSizeTable = fragment.sampleSizeTable; + int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; + long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; + boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; + + boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO + && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0; + + int trackRunEnd = trackRunStart + fragment.trunLength[index]; + long timescale = track.timescale; + long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime; + for (int i = trackRunStart; i < trackRunEnd; i++) { + // Use trun values if present, otherwise tfhd, otherwise trex. + int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() + : defaultSampleValues.duration; + int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size; + int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags + : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; + if (sampleCompositionTimeOffsetsPresent) { + // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in + // version 0 trun boxes, however a significant number of streams violate the spec and use + // signed integers instead. It's safe to always decode sample offsets as signed integers + // here, because unsigned integers will still be parsed correctly (unless their top bit is + // set, which is never true in practice because sample offsets are always small). + int sampleOffset = trun.readInt(); + sampleCompositionTimeOffsetTable[i] = + (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); + } else { + sampleCompositionTimeOffsetTable[i] = 0; + } + sampleDecodingTimeTable[i] = + Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; + sampleSizeTable[i] = sampleSize; + sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 + && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); + cumulativeTime += sampleDuration; + } + fragment.nextFragmentDecodeTime = cumulativeTime; + return trackRunEnd; + } + + private static void parseUuid(ParsableByteArray uuid, TrackFragment out, + byte[] extendedTypeScratch) throws ParserException { + uuid.setPosition(Atom.HEADER_SIZE); + uuid.readBytes(extendedTypeScratch, 0, 16); + + // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox. + if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) { + return; + } + + // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of + // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al, + // Section 5.3.2.1." + parseSenc(uuid, 16, out); + } + + private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException { + parseSenc(senc, 0, out); + } + + private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) + throws ParserException { + senc.setPosition(Atom.HEADER_SIZE + offset); + int fullAtom = senc.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + + if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { + // TODO: Implement this. + throw new ParserException("Overriding TrackEncryptionBox parameters is unsupported."); + } + + boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; + int sampleCount = senc.readUnsignedIntToInt(); + if (sampleCount != out.sampleCount) { + throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); + } + + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + out.initEncryptionData(senc.bytesLeft()); + out.fillEncryptionData(senc); + } + + private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, + TrackFragment out) throws ParserException { + sbgp.setPosition(Atom.HEADER_SIZE); + int sbgpFullAtom = sbgp.readInt(); + if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { + // Only seig grouping type is supported. + return; + } + if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) { + sbgp.skipBytes(4); // default_length. + } + if (sbgp.readInt() != 1) { // entry_count. + throw new ParserException("Entry count in sbgp != 1 (unsupported)."); + } + + sgpd.setPosition(Atom.HEADER_SIZE); + int sgpdFullAtom = sgpd.readInt(); + if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) { + // Only seig grouping type is supported. + return; + } + int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom); + if (sgpdVersion == 1) { + if (sgpd.readUnsignedInt() == 0) { + throw new ParserException("Variable length description in sgpd found (unsupported)"); + } + } else if (sgpdVersion >= 2) { + sgpd.skipBytes(4); // default_sample_description_index. + } + if (sgpd.readUnsignedInt() != 1) { // entry_count. + throw new ParserException("Entry count in sgpd != 1 (unsupported)."); + } + // CencSampleEncryptionInformationGroupEntry + sgpd.skipBytes(1); // reserved = 0. + int patternByte = sgpd.readUnsignedByte(); + int cryptByteBlock = (patternByte & 0xF0) >> 4; + int skipByteBlock = patternByte & 0x0F; + boolean isProtected = sgpd.readUnsignedByte() == 1; + if (!isProtected) { + return; + } + int perSampleIvSize = sgpd.readUnsignedByte(); + byte[] keyId = new byte[16]; + sgpd.readBytes(keyId, 0, keyId.length); + byte[] constantIv = null; + if (perSampleIvSize == 0) { + int constantIvSize = sgpd.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + sgpd.readBytes(constantIv, 0, constantIvSize); + } + out.definesEncryptionData = true; + out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId, + cryptByteBlock, skipByteBlock, constantIv); + } + + /** + * Parses a sidx atom (defined in 14496-12). + * + * @param atom The atom data. + * @param inputPosition The input position of the first byte after the atom. + * @return A pair consisting of the earliest presentation time in microseconds, and the parsed + * {@link ChunkIndex}. + */ + private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition) + throws ParserException { + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + + atom.skipBytes(4); + long timescale = atom.readUnsignedInt(); + long earliestPresentationTime; + long offset = inputPosition; + if (version == 0) { + earliestPresentationTime = atom.readUnsignedInt(); + offset += atom.readUnsignedInt(); + } else { + earliestPresentationTime = atom.readUnsignedLongToLong(); + offset += atom.readUnsignedLongToLong(); + } + long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime, + C.MICROS_PER_SECOND, timescale); + + atom.skipBytes(2); + + int referenceCount = atom.readUnsignedShort(); + int[] sizes = new int[referenceCount]; + long[] offsets = new long[referenceCount]; + long[] durationsUs = new long[referenceCount]; + long[] timesUs = new long[referenceCount]; + + long time = earliestPresentationTime; + long timeUs = earliestPresentationTimeUs; + for (int i = 0; i < referenceCount; i++) { + int firstInt = atom.readInt(); + + int type = 0x80000000 & firstInt; + if (type != 0) { + throw new ParserException("Unhandled indirect reference"); + } + long referenceDuration = atom.readUnsignedInt(); + + sizes[i] = 0x7FFFFFFF & firstInt; + offsets[i] = offset; + + // Calculate time and duration values such that any rounding errors are consistent. i.e. That + // timesUs[i] + durationsUs[i] == timesUs[i + 1]. + timesUs[i] = timeUs; + time += referenceDuration; + timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); + durationsUs[i] = timeUs - timesUs[i]; + + atom.skipBytes(4); + offset += sizes[i]; + } + + return Pair.create(earliestPresentationTimeUs, + new ChunkIndex(sizes, offsets, durationsUs, timesUs)); + } + + private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + TrackBundle nextTrackBundle = null; + long nextDataOffset = Long.MAX_VALUE; + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackFragment trackFragment = trackBundles.valueAt(i).fragment; + if (trackFragment.sampleEncryptionDataNeedsFill + && trackFragment.auxiliaryDataPosition < nextDataOffset) { + nextDataOffset = trackFragment.auxiliaryDataPosition; + nextTrackBundle = trackBundles.valueAt(i); + } + } + if (nextTrackBundle == null) { + parserState = STATE_READING_SAMPLE_START; + return; + } + int bytesToSkip = (int) (nextDataOffset - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to encryption data was negative."); + } + input.skipFully(bytesToSkip); + nextTrackBundle.fragment.fillEncryptionData(input); + } + + /** + * Attempts to read the next sample in the current mdat atom. The read sample may be output or + * skipped. + * + * <p>If there are no more samples in the current mdat atom then the parser state is transitioned + * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned. + * + * <p>It is possible for a sample to be partially read in the case that an exception is thrown. In + * this case the method can be called again to read the remainder of the sample. + * + * @param input The {@link ExtractorInput} from which to read data. + * @return Whether a sample was read. The read sample may have been output or skipped. False + * indicates that there are no samples left to read in the current mdat. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { + if (parserState == STATE_READING_SAMPLE_START) { + if (currentTrackBundle == null) { + TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); + if (currentTrackBundle == null) { + // We've run out of samples in the current mdat. Discard any trailing data and prepare to + // read the header of the next atom. + int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to end of mdat was negative."); + } + input.skipFully(bytesToSkip); + enterReadingAtomHeaderState(); + return false; + } + + long nextDataPosition = currentTrackBundle.fragment + .trunDataPosition[currentTrackBundle.currentTrackRunIndex]; + // We skip bytes preceding the next sample to read. + int bytesToSkip = (int) (nextDataPosition - input.getPosition()); + if (bytesToSkip < 0) { + // Assume the sample data must be contiguous in the mdat with no preceding data. + Log.w(TAG, "Ignoring negative offset to sample data."); + bytesToSkip = 0; + } + input.skipFully(bytesToSkip); + this.currentTrackBundle = currentTrackBundle; + } + + sampleSize = currentTrackBundle.fragment + .sampleSizeTable[currentTrackBundle.currentSampleIndex]; + + if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) { + input.skipFully(sampleSize); + currentTrackBundle.skipSampleEncryptionData(); + if (!currentTrackBundle.next()) { + currentTrackBundle = null; + } + parserState = STATE_READING_SAMPLE_START; + return true; + } + + if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + sampleSize -= Atom.HEADER_SIZE; + input.skipFully(Atom.HEADER_SIZE); + } + + if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { + // AC4 samples need to be prefixed with a clear sample header. + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } else { + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); + } + sampleSize += sampleBytesWritten; + parserState = STATE_READING_SAMPLE_CONTINUE; + sampleCurrentNalBytesRemaining = 0; + } + + TrackFragment fragment = currentTrackBundle.fragment; + Track track = currentTrackBundle.track; + TrackOutput output = currentTrackBundle.output; + int sampleIndex = currentTrackBundle.currentSampleIndex; + long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } + if (track.nalUnitLengthFieldLength != 0) { + // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalPrefixData = nalPrefix.data; + nalPrefixData[0] = 0; + nalPrefixData[1] = 0; + nalPrefixData[2] = 0; + int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; + int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesWritten < sampleSize) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one, and its type. + input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); + nalPrefix.setPosition(0); + int nalLengthInt = nalPrefix.readInt(); + if (nalLengthInt < 1) { + throw new ParserException("Invalid NAL length"); + } + sampleCurrentNalBytesRemaining = nalLengthInt - 1; + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + // Write the NAL unit type byte. + output.sampleData(nalPrefix, 1); + processSeiNalUnitPayload = cea608TrackOutputs.length > 0 + && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); + sampleBytesWritten += 5; + sampleSize += nalUnitLengthFieldLengthDiff; + } else { + int writtenBytes; + if (processSeiNalUnitPayload) { + // Read and write the payload of the SEI NAL unit. + nalBuffer.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); + output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); + writtenBytes = sampleCurrentNalBytesRemaining; + // Unescape and process the SEI NAL unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); + // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. + nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); + nalBuffer.setLimit(unescapedLength); + CeaUtil.consume(sampleTimeUs, nalBuffer, cea608TrackOutputs); + } else { + // Write the payload of the NAL unit. + writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + } + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + } else { + while (sampleBytesWritten < sampleSize) { + int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesWritten += writtenBytes; + } + } + + @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex] + ? C.BUFFER_FLAG_KEY_FRAME : 0; + + // Encryption data. + TrackOutput.CryptoData cryptoData = null; + TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted(); + if (encryptionBox != null) { + sampleFlags |= C.BUFFER_FLAG_ENCRYPTED; + cryptoData = encryptionBox.cryptoData; + } + + output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); + + // After we have the sampleTimeUs, we can commit all the pending metadata samples + outputPendingMetadataSamples(sampleTimeUs); + if (!currentTrackBundle.next()) { + currentTrackBundle = null; + } + parserState = STATE_READING_SAMPLE_START; + return true; + } + + private void outputPendingMetadataSamples(long sampleTimeUs) { + while (!pendingMetadataSampleInfos.isEmpty()) { + MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); + pendingMetadataSampleBytes -= sampleInfo.size; + long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs; + if (timestampAdjuster != null) { + metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs); + } + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + metadataTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + sampleInfo.size, + pendingMetadataSampleBytes, + null); + } + } + } + + /** + * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those + * yet to be consumed, or null if all have been consumed. + */ + private static TrackBundle getNextFragmentRun(SparseArray<TrackBundle> trackBundles) { + TrackBundle nextTrackBundle = null; + long nextTrackRunOffset = Long.MAX_VALUE; + + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackBundle trackBundle = trackBundles.valueAt(i); + if (trackBundle.currentTrackRunIndex == trackBundle.fragment.trunCount) { + // This track fragment contains no more runs in the next mdat box. + } else { + long trunOffset = trackBundle.fragment.trunDataPosition[trackBundle.currentTrackRunIndex]; + if (trunOffset < nextTrackRunOffset) { + nextTrackBundle = trackBundle; + nextTrackRunOffset = trunOffset; + } + } + } + return nextTrackBundle; + } + + /** Returns DrmInitData from leaf atoms. */ + @Nullable + private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) { + ArrayList<SchemeData> schemeDatas = null; + int leafChildrenSize = leafChildren.size(); + for (int i = 0; i < leafChildrenSize; i++) { + LeafAtom child = leafChildren.get(i); + if (child.type == Atom.TYPE_pssh) { + if (schemeDatas == null) { + schemeDatas = new ArrayList<>(); + } + byte[] psshData = child.data.data; + UUID uuid = PsshAtomUtil.parseUuid(psshData); + if (uuid == null) { + Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); + } else { + schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData)); + } + } + } + return schemeDatas == null ? null : new DrmInitData(schemeDatas); + } + + /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */ + private static boolean shouldParseLeafAtom(int atom) { + return atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd + || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd || atom == Atom.TYPE_tfdt + || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_trex + || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz + || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid + || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst + || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg; + } + + /** Returns whether the extractor should decode a container atom with type {@code atom}. */ + private static boolean shouldParseContainerAtom(int atom) { + return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_moof + || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; + } + + /** + * Holds data corresponding to a metadata sample. + */ + private static final class MetadataSampleInfo { + + public final long presentationTimeDeltaUs; + public final int size; + + public MetadataSampleInfo(long presentationTimeDeltaUs, int size) { + this.presentationTimeDeltaUs = presentationTimeDeltaUs; + this.size = size; + } + + } + + /** + * Holds data corresponding to a single track. + */ + private static final class TrackBundle { + + private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8; + + public final TrackOutput output; + public final TrackFragment fragment; + public final ParsableByteArray scratch; + + public Track track; + public DefaultSampleValues defaultSampleValues; + public int currentSampleIndex; + public int currentSampleInTrackRun; + public int currentTrackRunIndex; + public int firstSampleToOutputIndex; + + private final ParsableByteArray encryptionSignalByte; + private final ParsableByteArray defaultInitializationVector; + + public TrackBundle(TrackOutput output) { + this.output = output; + fragment = new TrackFragment(); + scratch = new ParsableByteArray(); + encryptionSignalByte = new ParsableByteArray(1); + defaultInitializationVector = new ParsableByteArray(); + } + + public void init(Track track, DefaultSampleValues defaultSampleValues) { + this.track = Assertions.checkNotNull(track); + this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues); + output.format(track.format); + reset(); + } + + public void updateDrmInitData(DrmInitData drmInitData) { + TrackEncryptionBox encryptionBox = + track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); + } + + /** Resets the current fragment and sample indices. */ + public void reset() { + fragment.reset(); + currentSampleIndex = 0; + currentTrackRunIndex = 0; + currentSampleInTrackRun = 0; + firstSampleToOutputIndex = 0; + } + + /** + * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified + * seek time in the current fragment. + * + * @param timeUs The seek time, in microseconds. + */ + public void seek(long timeUs) { + long timeMs = C.usToMs(timeUs); + int searchIndex = currentSampleIndex; + while (searchIndex < fragment.sampleCount + && fragment.getSamplePresentationTime(searchIndex) < timeMs) { + if (fragment.sampleIsSyncFrameTable[searchIndex]) { + firstSampleToOutputIndex = searchIndex; + } + searchIndex++; + } + } + + /** + * Advances the indices in the bundle to point to the next sample in the current fragment. If + * the current sample is the last one in the current fragment, then the advanced state will be + * {@code currentSampleIndex == fragment.sampleCount}, {@code currentTrackRunIndex == + * fragment.trunCount} and {@code #currentSampleInTrackRun == 0}. + * + * @return Whether the next sample is in the same track run as the previous one. + */ + public boolean next() { + currentSampleIndex++; + currentSampleInTrackRun++; + if (currentSampleInTrackRun == fragment.trunLength[currentTrackRunIndex]) { + currentTrackRunIndex++; + currentSampleInTrackRun = 0; + return false; + } + return true; + } + + /** + * Outputs the encryption data for the current sample. + * + * @param sampleSize The size of the current sample in bytes, excluding any additional clear + * header that will be prefixed to the sample by the extractor. + * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the + * extractor, or 0. + * @return The number of written bytes. + */ + public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { + return 0; + } + + ParsableByteArray initializationVectorData; + int vectorSize; + if (encryptionBox.perSampleIvSize != 0) { + initializationVectorData = fragment.sampleEncryptionData; + vectorSize = encryptionBox.perSampleIvSize; + } else { + // The default initialization vector should be used. + byte[] initVectorData = encryptionBox.defaultInitializationVector; + defaultInitializationVector.reset(initVectorData, initVectorData.length); + initializationVectorData = defaultInitializationVector; + vectorSize = initVectorData.length; + } + + boolean haveSubsampleEncryptionTable = + fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean writeSubsampleEncryptionData = haveSubsampleEncryptionTable || clearHeaderSize != 0; + + // Write the signal byte, containing the vector size and the subsample encryption flag. + encryptionSignalByte.data[0] = + (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); + encryptionSignalByte.setPosition(0); + output.sampleData(encryptionSignalByte, 1); + // Write the vector. + output.sampleData(initializationVectorData, vectorSize); + + if (!writeSubsampleEncryptionData) { + return 1 + vectorSize; + } + + if (!haveSubsampleEncryptionTable) { + // The sample is fully encrypted, except for the additional clear header that the extractor + // is going to prefix. We need to synthesize subsample encryption data that takes the header + // into account. + scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + // subsampleCount = 1 (unsigned short) + scratch.data[0] = (byte) 0; + scratch.data[1] = (byte) 1; + // clearDataSize = clearHeaderSize (unsigned short) + scratch.data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF); + scratch.data[3] = (byte) (clearHeaderSize & 0xFF); + // encryptedDataSize = sampleSize (unsigned short) + scratch.data[4] = (byte) ((sampleSize >> 24) & 0xFF); + scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF); + scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF); + scratch.data[7] = (byte) (sampleSize & 0xFF); + output.sampleData(scratch, SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH; + } + + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; + int subsampleCount = subsampleEncryptionData.readUnsignedShort(); + subsampleEncryptionData.skipBytes(-2); + int subsampleDataLength = 2 + 6 * subsampleCount; + + if (clearHeaderSize != 0) { + // We need to account for the additional clear header by adding clearHeaderSize to + // clearDataSize for the first subsample specified in the subsample encryption data. + scratch.reset(subsampleDataLength); + scratch.readBytes(subsampleEncryptionData.data, /* offset= */ 0, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + + int clearDataSize = (scratch.data[2] & 0xFF) << 8 | (scratch.data[3] & 0xFF); + int adjustedClearDataSize = clearDataSize + clearHeaderSize; + scratch.data[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF); + scratch.data[3] = (byte) (adjustedClearDataSize & 0xFF); + subsampleEncryptionData = scratch; + } + + output.sampleData(subsampleEncryptionData, subsampleDataLength); + return 1 + vectorSize + subsampleDataLength; + } + + /** Skips the encryption data for the current sample. */ + private void skipSampleEncryptionData() { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { + return; + } + + ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData; + if (encryptionBox.perSampleIvSize != 0) { + sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize); + } + if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) { + sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort()); + } + } + + private TrackEncryptionBox getEncryptionBoxIfEncrypted() { + int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + TrackEncryptionBox encryptionBox = + fragment.trackEncryptionBox != null + ? fragment.trackEncryptionBox + : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); + return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java new file mode 100644 index 0000000000..7040df6425 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format + * Specification. + */ +public final class MdtaMetadataEntry implements Metadata.Entry { + + /** The metadata key name. */ + public final String key; + /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */ + public final byte[] value; + /** The four byte locale indicator. */ + public final int localeIndicator; + /** The four byte type indicator. */ + public final int typeIndicator; + + /** Creates a new metadata entry for the specified metadata key/value. */ + public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) { + this.key = key; + this.value = value; + this.localeIndicator = localeIndicator; + this.typeIndicator = typeIndicator; + } + + private MdtaMetadataEntry(Parcel in) { + key = Util.castNonNull(in.readString()); + value = new byte[in.readInt()]; + in.readByteArray(value); + localeIndicator = in.readInt(); + typeIndicator = in.readInt(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MdtaMetadataEntry other = (MdtaMetadataEntry) obj; + return key.equals(other.key) + && Arrays.equals(value, other.value) + && localeIndicator == other.localeIndicator + && typeIndicator == other.typeIndicator; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + Arrays.hashCode(value); + result = 31 * result + localeIndicator; + result = 31 * result + typeIndicator; + return result; + } + + @Override + public String toString() { + return "mdta: key=" + key; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeInt(value.length); + dest.writeByteArray(value); + dest.writeInt(localeIndicator); + dest.writeInt(typeIndicator); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<MdtaMetadataEntry> CREATOR = + new Parcelable.Creator<MdtaMetadataEntry>() { + + @Override + public MdtaMetadataEntry createFromParcel(Parcel in) { + return new MdtaMetadataEntry(in); + } + + @Override + public MdtaMetadataEntry[] newArray(int size) { + return new MdtaMetadataEntry[size]; + } + }; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java new file mode 100644 index 0000000000..7d4de0e498 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -0,0 +1,588 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.ApicFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.CommentFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Frame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.InternalFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; + +/** Utilities for handling metadata in MP4. */ +/* package */ final class MetadataUtil { + + private static final String TAG = "MetadataUtil"; + + // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. + private static final int SHORT_TYPE_NAME_1 = 0x006e616d; + private static final int SHORT_TYPE_NAME_2 = 0x0074726b; + private static final int SHORT_TYPE_COMMENT = 0x00636d74; + private static final int SHORT_TYPE_YEAR = 0x00646179; + private static final int SHORT_TYPE_ARTIST = 0x00415254; + private static final int SHORT_TYPE_ENCODER = 0x00746f6f; + private static final int SHORT_TYPE_ALBUM = 0x00616c62; + private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d; + private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274; + private static final int SHORT_TYPE_LYRICS = 0x006c7972; + private static final int SHORT_TYPE_GENRE = 0x0067656e; + + // Codes that have equivalent ID3 frames. + private static final int TYPE_COVER_ART = 0x636f7672; + private static final int TYPE_GENRE = 0x676e7265; + private static final int TYPE_GROUPING = 0x00677270; + private static final int TYPE_DISK_NUMBER = 0x6469736b; + private static final int TYPE_TRACK_NUMBER = 0x74726b6e; + private static final int TYPE_TEMPO = 0x746d706f; + private static final int TYPE_COMPILATION = 0x6370696c; + private static final int TYPE_ALBUM_ARTIST = 0x61415254; + private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d; + private static final int TYPE_SORT_ALBUM = 0x736f616c; + private static final int TYPE_SORT_ARTIST = 0x736f6172; + private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161; + private static final int TYPE_SORT_COMPOSER = 0x736f636f; + + // Types that do not have equivalent ID3 frames. + private static final int TYPE_RATING = 0x72746e67; + private static final int TYPE_GAPLESS_ALBUM = 0x70676170; + private static final int TYPE_TV_SORT_SHOW = 0x736f736e; + private static final int TYPE_TV_SHOW = 0x74767368; + + // Type for items that are intended for internal use by the player. + private static final int TYPE_INTERNAL = 0x2d2d2d2d; + + private static final int PICTURE_TYPE_FRONT_COVER = 3; + + // Standard genres. + @VisibleForTesting + /* package */ static final String[] STANDARD_GENRES = + new String[] { + // These are the official ID3v1 genres. + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec. + "Folk", + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall", + // Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec. + "Goa", + "Drum & Bass", + "Club-House", + "Hardcore", + "Terror", + "Indie", + "BritPop", + "Afro-Punk", + "Polsk Punk", + "Beat", + "Christian Gangsta Rap", + "Heavy Metal", + "Black Metal", + "Crossover", + "Contemporary Christian", + "Christian Rock", + "Merengue", + "Salsa", + "Thrash Metal", + "Anime", + "Jpop", + "Synthpop", + // Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec. + "Abstract", + "Art Rock", + "Baroque", + "Bhangra", + "Big beat", + "Breakbeat", + "Chillout", + "Downtempo", + "Dub", + "EBM", + "Eclectic", + "Electro", + "Electroclash", + "Emo", + "Experimental", + "Garage", + "Global", + "IDM", + "Illbient", + "Industro-Goth", + "Jam Band", + "Krautrock", + "Leftfield", + "Lounge", + "Math Rock", + "New Romantic", + "Nu-Breakz", + "Post-Punk", + "Post-Rock", + "Psytrance", + "Shoegaze", + "Space Rock", + "Trop Rock", + "World Music", + "Neoclassical", + "Audiobook", + "Audio theatre", + "Neue Deutsche Welle", + "Podcast", + "Indie-Rock", + "G-Funk", + "Dubstep", + "Garage Rock", + "Psybient" + }; + + private static final String LANGUAGE_UNDEFINED = "und"; + + private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9; + private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD. + + private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps"; + private static final int MDTA_TYPE_INDICATOR_FLOAT = 23; + + private MetadataUtil() {} + + /** + * Returns a {@link Format} that is the same as the input format but includes information from the + * specified sources of metadata. + */ + public static Format getFormatWithMetadata( + int trackType, + Format format, + @Nullable Metadata udtaMetadata, + @Nullable Metadata mdtaMetadata, + GaplessInfoHolder gaplessInfoHolder) { + if (trackType == C.TRACK_TYPE_AUDIO) { + if (gaplessInfoHolder.hasGaplessInfo()) { + format = + format.copyWithGaplessInfo( + gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); + } + // We assume all udta metadata is associated with the audio track. + if (udtaMetadata != null) { + format = format.copyWithMetadata(udtaMetadata); + } + } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) { + // Populate only metadata keys that are known to be specific to video. + for (int i = 0; i < mdtaMetadata.length(); i++) { + Metadata.Entry entry = mdtaMetadata.get(i); + if (entry instanceof MdtaMetadataEntry) { + MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry; + if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key) + && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) { + try { + float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get(); + format = format.copyWithFrameRate(fps); + format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring invalid framerate"); + } + } + } + } + } + return format; + } + + /** + * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read + * starting from the current position of the {@link ParsableByteArray}, and the position is + * advanced by the size of the element. The position is advanced even if the element's type is + * unrecognized. + * + * @param ilst Holds the data to be parsed. + * @return The parsed element, or null if the element's type was not recognized. + */ + @Nullable + public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) { + int position = ilst.getPosition(); + int endPosition = position + ilst.readInt(); + int type = ilst.readInt(); + int typeTopByte = (type >> 24) & 0xFF; + try { + if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) { + int shortType = type & 0x00FFFFFF; + if (shortType == SHORT_TYPE_COMMENT) { + return parseCommentAttribute(type, ilst); + } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) { + return parseTextAttribute(type, "TIT2", ilst); + } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) { + return parseTextAttribute(type, "TCOM", ilst); + } else if (shortType == SHORT_TYPE_YEAR) { + return parseTextAttribute(type, "TDRC", ilst); + } else if (shortType == SHORT_TYPE_ARTIST) { + return parseTextAttribute(type, "TPE1", ilst); + } else if (shortType == SHORT_TYPE_ENCODER) { + return parseTextAttribute(type, "TSSE", ilst); + } else if (shortType == SHORT_TYPE_ALBUM) { + return parseTextAttribute(type, "TALB", ilst); + } else if (shortType == SHORT_TYPE_LYRICS) { + return parseTextAttribute(type, "USLT", ilst); + } else if (shortType == SHORT_TYPE_GENRE) { + return parseTextAttribute(type, "TCON", ilst); + } else if (shortType == TYPE_GROUPING) { + return parseTextAttribute(type, "TIT1", ilst); + } + } else if (type == TYPE_GENRE) { + return parseStandardGenreAttribute(ilst); + } else if (type == TYPE_DISK_NUMBER) { + return parseIndexAndCountAttribute(type, "TPOS", ilst); + } else if (type == TYPE_TRACK_NUMBER) { + return parseIndexAndCountAttribute(type, "TRCK", ilst); + } else if (type == TYPE_TEMPO) { + return parseUint8Attribute(type, "TBPM", ilst, true, false); + } else if (type == TYPE_COMPILATION) { + return parseUint8Attribute(type, "TCMP", ilst, true, true); + } else if (type == TYPE_COVER_ART) { + return parseCoverArt(ilst); + } else if (type == TYPE_ALBUM_ARTIST) { + return parseTextAttribute(type, "TPE2", ilst); + } else if (type == TYPE_SORT_TRACK_NAME) { + return parseTextAttribute(type, "TSOT", ilst); + } else if (type == TYPE_SORT_ALBUM) { + return parseTextAttribute(type, "TSO2", ilst); + } else if (type == TYPE_SORT_ARTIST) { + return parseTextAttribute(type, "TSOA", ilst); + } else if (type == TYPE_SORT_ALBUM_ARTIST) { + return parseTextAttribute(type, "TSOP", ilst); + } else if (type == TYPE_SORT_COMPOSER) { + return parseTextAttribute(type, "TSOC", ilst); + } else if (type == TYPE_RATING) { + return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false); + } else if (type == TYPE_GAPLESS_ALBUM) { + return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true); + } else if (type == TYPE_TV_SORT_SHOW) { + return parseTextAttribute(type, "TVSHOWSORT", ilst); + } else if (type == TYPE_TV_SHOW) { + return parseTextAttribute(type, "TVSHOW", ilst); + } else if (type == TYPE_INTERNAL) { + return parseInternalAttribute(ilst, endPosition); + } + Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type)); + return null; + } finally { + ilst.setPosition(endPosition); + } + } + + /** + * Parses an 'mdta' metadata entry starting at the current position in an ilst box. + * + * @param ilst The ilst box. + * @param endPosition The end position of the entry in the ilst box. + * @param key The mdta metadata entry key for the entry. + * @return The parsed element, or null if the entry wasn't recognized. + */ + @Nullable + public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst( + ParsableByteArray ilst, int endPosition, String key) { + int atomPosition; + while ((atomPosition = ilst.getPosition()) < endPosition) { + int atomSize = ilst.readInt(); + int atomType = ilst.readInt(); + if (atomType == Atom.TYPE_data) { + int typeIndicator = ilst.readInt(); + int localeIndicator = ilst.readInt(); + int dataSize = atomSize - 16; + byte[] value = new byte[dataSize]; + ilst.readBytes(value, 0, dataSize); + return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator); + } + ilst.setPosition(atomPosition + atomSize); + } + return null; + } + + @Nullable + private static TextInformationFrame parseTextAttribute( + int type, String id, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(atomSize - 16); + return new TextInformationFrame(id, /* description= */ null, value); + } + Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(atomSize - 16); + return new CommentFrame(LANGUAGE_UNDEFINED, value, value); + } + Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static Id3Frame parseUint8Attribute( + int type, + String id, + ParsableByteArray data, + boolean isTextInformationFrame, + boolean isBoolean) { + int value = parseUint8AttributeValue(data); + if (isBoolean) { + value = Math.min(1, value); + } + if (value >= 0) { + return isTextInformationFrame + ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value)) + : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); + } + Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static TextInformationFrame parseIndexAndCountAttribute( + int type, String attributeName, ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data && atomSize >= 22) { + data.skipBytes(10); // version (1), flags (3), empty (4), empty (2) + int index = data.readUnsignedShort(); + if (index > 0) { + String value = "" + index; + int count = data.readUnsignedShort(); + if (count > 0) { + value += "/" + count; + } + return new TextInformationFrame(attributeName, /* description= */ null, value); + } + } + Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); + return null; + } + + @Nullable + private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { + int genreCode = parseUint8AttributeValue(data); + String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) + ? STANDARD_GENRES[genreCode - 1] : null; + if (genreString != null) { + return new TextInformationFrame("TCON", /* description= */ null, genreString); + } + Log.w(TAG, "Failed to parse standard genre code"); + return null; + } + + @Nullable + private static ApicFrame parseCoverArt(ParsableByteArray data) { + int atomSize = data.readInt(); + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + int fullVersionInt = data.readInt(); + int flags = Atom.parseFullAtomFlags(fullVersionInt); + String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; + if (mimeType == null) { + Log.w(TAG, "Unrecognized cover art flags: " + flags); + return null; + } + data.skipBytes(4); // empty (4) + byte[] pictureData = new byte[atomSize - 16]; + data.readBytes(pictureData, 0, pictureData.length); + return new ApicFrame( + mimeType, + /* description= */ null, + /* pictureType= */ PICTURE_TYPE_FRONT_COVER, + pictureData); + } + Log.w(TAG, "Failed to parse cover art attribute"); + return null; + } + + @Nullable + private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { + String domain = null; + String name = null; + int dataAtomPosition = -1; + int dataAtomSize = -1; + while (data.getPosition() < endPosition) { + int atomPosition = data.getPosition(); + int atomSize = data.readInt(); + int atomType = data.readInt(); + data.skipBytes(4); // version (1), flags (3) + if (atomType == Atom.TYPE_mean) { + domain = data.readNullTerminatedString(atomSize - 12); + } else if (atomType == Atom.TYPE_name) { + name = data.readNullTerminatedString(atomSize - 12); + } else { + if (atomType == Atom.TYPE_data) { + dataAtomPosition = atomPosition; + dataAtomSize = atomSize; + } + data.skipBytes(atomSize - 12); + } + } + if (domain == null || name == null || dataAtomPosition == -1) { + return null; + } + data.setPosition(dataAtomPosition); + data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) + String value = data.readNullTerminatedString(dataAtomSize - 16); + return new InternalFrame(domain, name, value); + } + + private static int parseUint8AttributeValue(ParsableByteArray data) { + data.skipBytes(4); // atomSize + int atomType = data.readInt(); + if (atomType == Atom.TYPE_data) { + data.skipBytes(8); // version (1), flags (3), empty (4) + return data.readUnsignedByte(); + } + Log.w(TAG, "Failed to parse uint8 attribute value"); + return -1; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java new file mode 100644 index 0000000000..254cad1eb1 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -0,0 +1,824 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; + +/** + * Extracts data from the MP4 container format. + */ +public final class Mp4Extractor implements Extractor, SeekMap { + + /** Factory for {@link Mp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + public @interface Flags {} + /** + * Flag to ignore any edit lists in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1; + + /** Parser states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE}) + private @interface State {} + + private static final int STATE_READING_ATOM_HEADER = 0; + private static final int STATE_READING_ATOM_PAYLOAD = 1; + private static final int STATE_READING_SAMPLE = 2; + + /** Brand stored in the ftyp atom for QuickTime media. */ + private static final int BRAND_QUICKTIME = 0x71742020; + + /** + * When seeking within the source, if the offset is greater than or equal to this value (or the + * offset is negative), the source will be reloaded. + */ + private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + + /** + * For poorly interleaved streams, the maximum byte difference one track is allowed to be read + * ahead before the source will be reloaded at a new position to read another track. + */ + private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024; + + private final @Flags int flags; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + private final ParsableByteArray scratch; + + private final ParsableByteArray atomHeader; + private final ArrayDeque<ContainerAtom> containerAtoms; + + @State private int parserState; + private int atomType; + private long atomSize; + private int atomHeaderBytesRead; + private ParsableByteArray atomData; + + private int sampleTrackIndex; + private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + private Mp4Track[] tracks; + private long[][] accumulatedSampleSizes; + private int firstVideoTrackIndex; + private long durationUs; + private boolean isQuickTime; + + /** + * Creates a new extractor for unfragmented MP4 streams. + */ + public Mp4Extractor() { + this(0); + } + + /** + * Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the + * extractor's behavior. + * + * @param flags Flags that control the extractor's behavior. + */ + public Mp4Extractor(@Flags int flags) { + this.flags = flags; + atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); + containerAtoms = new ArrayDeque<>(); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + scratch = new ParsableByteArray(); + sampleTrackIndex = C.INDEX_UNSET; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffUnfragmented(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + } + + @Override + public void seek(long position, long timeUs) { + containerAtoms.clear(); + atomHeaderBytesRead = 0; + sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + if (position == 0) { + enterReadingAtomHeaderState(); + } else if (tracks != null) { + updateSampleIndices(timeUs); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + while (true) { + switch (parserState) { + case STATE_READING_ATOM_HEADER: + if (!readAtomHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + case STATE_READING_ATOM_PAYLOAD: + if (readAtomPayload(input, seekPosition)) { + return RESULT_SEEK; + } + break; + case STATE_READING_SAMPLE: + return readSample(input, seekPosition); + default: + throw new IllegalStateException(); + } + } + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + if (tracks.length == 0) { + return new SeekPoints(SeekPoint.START); + } + + long firstTimeUs; + long firstOffset; + long secondTimeUs = C.TIME_UNSET; + long secondOffset = C.POSITION_UNSET; + + // If we have a video track, use it to establish one or two seek points. + if (firstVideoTrackIndex != C.INDEX_UNSET) { + TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable; + int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs); + if (sampleIndex == C.INDEX_UNSET) { + return new SeekPoints(SeekPoint.START); + } + long sampleTimeUs = sampleTable.timestampsUs[sampleIndex]; + firstTimeUs = sampleTimeUs; + firstOffset = sampleTable.offsets[sampleIndex]; + if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) { + int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) { + secondTimeUs = sampleTable.timestampsUs[secondSampleIndex]; + secondOffset = sampleTable.offsets[secondSampleIndex]; + } + } + } else { + firstTimeUs = timeUs; + firstOffset = Long.MAX_VALUE; + } + + // Take into account other tracks. + for (int i = 0; i < tracks.length; i++) { + if (i != firstVideoTrackIndex) { + TrackSampleTable sampleTable = tracks[i].sampleTable; + firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); + if (secondTimeUs != C.TIME_UNSET) { + secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + } + } + } + + SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset); + if (secondTimeUs == C.TIME_UNSET) { + return new SeekPoints(firstSeekPoint); + } else { + SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset); + return new SeekPoints(firstSeekPoint, secondSeekPoint); + } + } + + // Private methods. + + private void enterReadingAtomHeaderState() { + parserState = STATE_READING_ATOM_HEADER; + atomHeaderBytesRead = 0; + } + + private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + if (atomHeaderBytesRead == 0) { + // Read the standard length atom header. + if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { + return false; + } + atomHeaderBytesRead = Atom.HEADER_SIZE; + atomHeader.setPosition(0); + atomSize = atomHeader.readUnsignedInt(); + atomType = atomHeader.readInt(); + } + + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. + int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; + input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); + atomHeaderBytesRead += headerBytesRemaining; + atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } + } + + if (atomSize < atomHeaderBytesRead) { + throw new ParserException("Atom size less than header length (unsupported)."); + } + + if (shouldParseContainerAtom(atomType)) { + long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; + if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } + containerAtoms.push(new ContainerAtom(atomType, endPosition)); + if (atomSize == atomHeaderBytesRead) { + processAtomEnded(endPosition); + } else { + // Start reading the first child atom. + enterReadingAtomHeaderState(); + } + } else if (shouldParseLeafAtom(atomType)) { + // We don't support parsing of leaf atoms that define extended atom sizes, or that have + // lengths greater than Integer.MAX_VALUE. + Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE); + Assertions.checkState(atomSize <= Integer.MAX_VALUE); + atomData = new ParsableByteArray((int) atomSize); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); + parserState = STATE_READING_ATOM_PAYLOAD; + } else { + atomData = null; + parserState = STATE_READING_ATOM_PAYLOAD; + } + + return true; + } + + /** + * Processes the atom payload. If {@link #atomData} is null and the size is at or above the + * threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should + * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped. + */ + private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder) + throws IOException, InterruptedException { + long atomPayloadSize = atomSize - atomHeaderBytesRead; + long atomEndPosition = input.getPosition() + atomPayloadSize; + boolean seekRequired = false; + if (atomData != null) { + input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize); + if (atomType == Atom.TYPE_ftyp) { + isQuickTime = processFtypAtom(atomData); + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData)); + } + } else { + // We don't need the data. Skip or seek, depending on how large the atom is. + if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) { + input.skipFully((int) atomPayloadSize); + } else { + positionHolder.position = input.getPosition() + atomPayloadSize; + seekRequired = true; + } + } + processAtomEnded(atomEndPosition); + return seekRequired && parserState != STATE_READING_SAMPLE; + } + + private void processAtomEnded(long atomEndPosition) throws ParserException { + while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { + Atom.ContainerAtom containerAtom = containerAtoms.pop(); + if (containerAtom.type == Atom.TYPE_moov) { + // We've reached the end of the moov atom. Process it and prepare to read samples. + processMoovAtom(containerAtom); + containerAtoms.clear(); + parserState = STATE_READING_SAMPLE; + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(containerAtom); + } + } + if (parserState != STATE_READING_SAMPLE) { + enterReadingAtomHeaderState(); + } + } + + /** + * Updates the stored track metadata to reflect the contents of the specified moov atom. + */ + private void processMoovAtom(ContainerAtom moov) throws ParserException { + int firstVideoTrackIndex = C.INDEX_UNSET; + long durationUs = C.TIME_UNSET; + List<Mp4Track> tracks = new ArrayList<>(); + + // Process metadata. + Metadata udtaMetadata = null; + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); + Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); + if (udta != null) { + udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime); + if (udtaMetadata != null) { + gaplessInfoHolder.setFromMetadata(udtaMetadata); + } + } + Metadata mdtaMetadata = null; + Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); + if (meta != null) { + mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta); + } + + boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0; + ArrayList<TrackSampleTable> trackSampleTables = + getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists); + + int trackCount = trackSampleTables.size(); + for (int i = 0; i < trackCount; i++) { + TrackSampleTable trackSampleTable = trackSampleTables.get(i); + Track track = trackSampleTable.track; + long trackDurationUs = + track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs; + durationUs = Math.max(durationUs, trackDurationUs); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); + + // Each sample has up to three bytes of overhead for the start code that replaces its length. + // Allow ten source samples per output sample, like the platform extractor. + int maxInputSize = trackSampleTable.maximumSize + 3 * 10; + Format format = track.format.copyWithMaxInputSize(maxInputSize); + if (track.type == C.TRACK_TYPE_VIDEO + && trackDurationUs > 0 + && trackSampleTable.sampleCount > 1) { + float frameRate = trackSampleTable.sampleCount / (trackDurationUs / 1000000f); + format = format.copyWithFrameRate(frameRate); + } + format = + MetadataUtil.getFormatWithMetadata( + track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder); + mp4Track.trackOutput.format(format); + + if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { + firstVideoTrackIndex = tracks.size(); + } + tracks.add(mp4Track); + } + this.firstVideoTrackIndex = firstVideoTrackIndex; + this.durationUs = durationUs; + this.tracks = tracks.toArray(new Mp4Track[0]); + accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks); + + extractorOutput.endTracks(); + extractorOutput.seekMap(this); + } + + private ArrayList<TrackSampleTable> getTrackSampleTables( + ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists) + throws ParserException { + ArrayList<TrackSampleTable> trackSampleTables = new ArrayList<>(); + for (int i = 0; i < moov.containerChildren.size(); i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type != Atom.TYPE_trak) { + continue; + } + Track track = + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + /* duration= */ C.TIME_UNSET, + /* drmInitData= */ null, + ignoreEditLists, + isQuickTime); + if (track == null) { + continue; + } + Atom.ContainerAtom stblAtom = + atom.getContainerAtomOfType(Atom.TYPE_mdia) + .getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); + if (trackSampleTable.sampleCount == 0) { + continue; + } + trackSampleTables.add(trackSampleTable); + } + return trackSampleTables; + } + + /** + * Attempts to extract the next sample in the current mdat atom for the specified track. + * <p> + * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in + * {@code positionHolder}. + * <p> + * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns + * {@link #RESULT_CONTINUE}. + * + * @param input The {@link ExtractorInput} from which to read data. + * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the + * position of the required data. + * @return One of the {@code RESULT_*} flags in {@link Extractor}. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int readSample(ExtractorInput input, PositionHolder positionHolder) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + if (sampleTrackIndex == C.INDEX_UNSET) { + sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition); + if (sampleTrackIndex == C.INDEX_UNSET) { + return RESULT_END_OF_INPUT; + } + } + Mp4Track track = tracks[sampleTrackIndex]; + TrackOutput trackOutput = track.trackOutput; + int sampleIndex = track.sampleIndex; + long position = track.sampleTable.offsets[sampleIndex]; + int sampleSize = track.sampleTable.sizes[sampleIndex]; + long skipAmount = position - inputPosition + sampleBytesRead; + if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { + positionHolder.position = position; + return RESULT_SEEK; + } + if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + // The sample information is contained in a cdat atom. The header must be discarded for + // committing. + skipAmount += Atom.HEADER_SIZE; + sampleSize -= Atom.HEADER_SIZE; + } + input.skipFully((int) skipAmount); + if (track.track.nalUnitLengthFieldLength != 0) { + // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLength = track.track.nalUnitLengthFieldLength; + int nalUnitLengthFieldLengthDiff = 4 - track.track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesWritten < sampleSize) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one. + input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; + nalLength.setPosition(0); + int nalLengthInt = nalLength.readInt(); + if (nalLengthInt < 0) { + throw new ParserException("Invalid NAL length"); + } + sampleCurrentNalBytesRemaining = nalLengthInt; + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + trackOutput.sampleData(nalStartCode, 4); + sampleBytesWritten += 4; + sampleSize += nalUnitLengthFieldLengthDiff; + } else { + // Write the payload of the NAL unit. + int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesRead += writtenBytes; + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + } else { + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + if (sampleBytesWritten == 0) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; + } + while (sampleBytesWritten < sampleSize) { + int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesRead += writtenBytes; + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], + track.sampleTable.flags[sampleIndex], sampleSize, 0, null); + track.sampleIndex++; + sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + return RESULT_CONTINUE; + } + + /** + * Returns the index of the track that contains the next sample to be read, or {@link + * C#INDEX_UNSET} if no samples remain. + * + * <p>The preferred choice is the sample with the smallest offset not requiring a source reload, + * or if not available the sample with the smallest overall offset to avoid subsequent source + * reloads. + * + * <p>To deal with poor sample interleaving, we also check whether the required memory to catch up + * with the next logical sample (based on sample time) exceeds {@link + * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even + * though it may require a source reload. + */ + private int getTrackIndexOfNextReadSample(long inputPosition) { + long preferredSkipAmount = Long.MAX_VALUE; + boolean preferredRequiresReload = true; + int preferredTrackIndex = C.INDEX_UNSET; + long preferredAccumulatedBytes = Long.MAX_VALUE; + long minAccumulatedBytes = Long.MAX_VALUE; + boolean minAccumulatedBytesRequiresReload = true; + int minAccumulatedBytesTrackIndex = C.INDEX_UNSET; + for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { + Mp4Track track = tracks[trackIndex]; + int sampleIndex = track.sampleIndex; + if (sampleIndex == track.sampleTable.sampleCount) { + continue; + } + long sampleOffset = track.sampleTable.offsets[sampleIndex]; + long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex]; + long skipAmount = sampleOffset - inputPosition; + boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE; + if ((!requiresReload && preferredRequiresReload) + || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) { + preferredRequiresReload = requiresReload; + preferredSkipAmount = skipAmount; + preferredTrackIndex = trackIndex; + preferredAccumulatedBytes = sampleAccumulatedBytes; + } + if (sampleAccumulatedBytes < minAccumulatedBytes) { + minAccumulatedBytes = sampleAccumulatedBytes; + minAccumulatedBytesRequiresReload = requiresReload; + minAccumulatedBytesTrackIndex = trackIndex; + } + } + return minAccumulatedBytes == Long.MAX_VALUE + || !minAccumulatedBytesRequiresReload + || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM + ? preferredTrackIndex + : minAccumulatedBytesTrackIndex; + } + + /** + * Updates every track's sample index to point its latest sync sample before/at {@code timeUs}. + */ + private void updateSampleIndices(long timeUs) { + for (Mp4Track track : tracks) { + TrackSampleTable sampleTable = track.sampleTable; + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + track.sampleIndex = sampleIndex; + } + } + + /** + * Possibly skips the version and flags fields (1+3 byte) of a full meta atom of the {@code + * input}. + * + * <p>Atoms of type {@link Atom#TYPE_meta} are defined to be full atoms which have four additional + * bytes for a version and a flags field (see 4.2 'Object Structure' in ISO/IEC 14496-12:2005). + * QuickTime do not have such a full box structure. Since some of these files are encoded wrongly, + * we can't rely on the file type though. Instead we must check the 8 bytes after the common + * header bytes ourselves. + */ + private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) + throws IOException, InterruptedException { + scratch.reset(8); + // Peek the next 8 bytes which can be either + // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom] + // (qt) [4 byte size of next atom ][4 byte hdlr atom type ] + // In case of (iso) we need to skip the next 4 bytes. + input.peekFully(scratch.data, 0, 8); + scratch.skipBytes(4); + if (scratch.readInt() == Atom.TYPE_hdlr) { + input.resetPeekPosition(); + } else { + input.skipFully(4); + } + } + + /** + * For each sample of each track, calculates accumulated size of all samples which need to be read + * before this sample can be used. + */ + private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) { + long[][] accumulatedSampleSizes = new long[tracks.length][]; + int[] nextSampleIndex = new int[tracks.length]; + long[] nextSampleTimesUs = new long[tracks.length]; + boolean[] tracksFinished = new boolean[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount]; + nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0]; + } + long accumulatedSampleSize = 0; + int finishedTracks = 0; + while (finishedTracks < tracks.length) { + long minTimeUs = Long.MAX_VALUE; + int minTimeTrackIndex = -1; + for (int i = 0; i < tracks.length; i++) { + if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) { + minTimeTrackIndex = i; + minTimeUs = nextSampleTimesUs[i]; + } + } + int trackSampleIndex = nextSampleIndex[minTimeTrackIndex]; + accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize; + accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex]; + nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex; + if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) { + nextSampleTimesUs[minTimeTrackIndex] = + tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex]; + } else { + tracksFinished[minTimeTrackIndex] = true; + finishedTracks++; + } + } + return accumulatedSampleSizes; + } + + /** + * Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, + * for a given {@code seekTimeUs}. + * + * @param sampleTable The sample table to use. + * @param seekTimeUs The seek time in microseconds. + * @param offset The current offset. + * @return The adjusted offset. + */ + private static long maybeAdjustSeekOffset( + TrackSampleTable sampleTable, long seekTimeUs, long offset) { + int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs); + if (sampleIndex == C.INDEX_UNSET) { + return offset; + } + long sampleOffset = sampleTable.offsets[sampleIndex]; + return Math.min(sampleOffset, offset); + } + + /** + * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if + * there are no synchronization samples in the table. + * + * @param sampleTable The sample table in which to locate a synchronization sample. + * @param timeUs A time in microseconds. + * @return The index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} + * if there are no synchronization samples in the table. + */ + private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) { + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + return sampleIndex; + } + + /** + * Process an ftyp atom to determine whether the media is QuickTime. + * + * @param atomData The ftyp atom data. + * @return Whether the media is QuickTime. + */ + private static boolean processFtypAtom(ParsableByteArray atomData) { + atomData.setPosition(Atom.HEADER_SIZE); + int majorBrand = atomData.readInt(); + if (majorBrand == BRAND_QUICKTIME) { + return true; + } + atomData.skipBytes(4); // minor_version + while (atomData.bytesLeft() > 0) { + if (atomData.readInt() == BRAND_QUICKTIME) { + return true; + } + } + return false; + } + + /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */ + private static boolean shouldParseLeafAtom(int atom) { + return atom == Atom.TYPE_mdhd + || atom == Atom.TYPE_mvhd + || atom == Atom.TYPE_hdlr + || atom == Atom.TYPE_stsd + || atom == Atom.TYPE_stts + || atom == Atom.TYPE_stss + || atom == Atom.TYPE_ctts + || atom == Atom.TYPE_elst + || atom == Atom.TYPE_stsc + || atom == Atom.TYPE_stsz + || atom == Atom.TYPE_stz2 + || atom == Atom.TYPE_stco + || atom == Atom.TYPE_co64 + || atom == Atom.TYPE_tkhd + || atom == Atom.TYPE_ftyp + || atom == Atom.TYPE_udta + || atom == Atom.TYPE_keys + || atom == Atom.TYPE_ilst; + } + + /** Returns whether the extractor should decode a container atom with type {@code atom}. */ + private static boolean shouldParseContainerAtom(int atom) { + return atom == Atom.TYPE_moov + || atom == Atom.TYPE_trak + || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf + || atom == Atom.TYPE_stbl + || atom == Atom.TYPE_edts + || atom == Atom.TYPE_meta; + } + + private static final class Mp4Track { + + public final Track track; + public final TrackSampleTable sampleTable; + public final TrackOutput trackOutput; + + public int sampleIndex; + + public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) { + this.track = track; + this.sampleTable = sampleTable; + this.trackOutput = trackOutput; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java new file mode 100644 index 0000000000..ddb13aeb9c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * Utility methods for handling PSSH atoms. + */ +public final class PsshAtomUtil { + + private static final String TAG = "PsshAtomUtil"; + + private PsshAtomUtil() {} + + /** + * Builds a version 0 PSSH atom for a given system id, containing the given data. + * + * @param systemId The system id of the scheme. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + public static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) { + return buildPsshAtom(systemId, null, data); + } + + /** + * Builds a PSSH atom for the given system id, containing the given key ids and data. + * + * @param systemId The system id of the scheme. + * @param keyIds The key ids for a version 1 PSSH atom, or null for a version 0 PSSH atom. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + // dereference of possibly-null reference keyId + @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"}) + public static byte[] buildPsshAtom( + UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { + int dataLength = data != null ? data.length : 0; + int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength; + if (keyIds != null) { + psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */; + } + ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength); + psshBox.putInt(psshBoxLength); + psshBox.putInt(Atom.TYPE_pssh); + psshBox.putInt(keyIds != null ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */); + psshBox.putLong(systemId.getMostSignificantBits()); + psshBox.putLong(systemId.getLeastSignificantBits()); + if (keyIds != null) { + psshBox.putInt(keyIds.length); + for (UUID keyId : keyIds) { + psshBox.putLong(keyId.getMostSignificantBits()); + psshBox.putLong(keyId.getLeastSignificantBits()); + } + } + if (data != null && data.length != 0) { + psshBox.putInt(data.length); + psshBox.put(data); + } // Else the last 4 bytes are a 0 DataSize. + return psshBox.array(); + } + + /** + * Returns whether the data is a valid PSSH atom. + * + * @param data The data to parse. + * @return Whether the data is a valid PSSH atom. + */ + public static boolean isPsshAtom(byte[] data) { + return parsePsshAtom(data) != null; + } + + /** + * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * + * <p>The UUID is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an + * unsupported version. + */ + public static @Nullable UUID parseUuid(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return null; + } + return parsedAtom.uuid; + } + + /** + * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * <p> + * The version is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed version. -1 if the input is not a valid PSSH atom, or if the PSSH atom has + * an unsupported version. + */ + public static int parseVersion(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return -1; + } + return parsedAtom.version; + } + + /** + * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * + * <p>The scheme specific data is only parsed if the data is a valid PSSH atom matching the given + * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null. + * + * @param atom The atom to parse. + * @param uuid The required UUID of the PSSH atom, or null to accept any UUID. + * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the + * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. + */ + public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return null; + } + if (uuid != null && !uuid.equals(parsedAtom.uuid)) { + Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + "."); + return null; + } + return parsedAtom.schemeData; + } + + /** + * Parses a PSSH atom. Version 0 and 1 PSSH atoms are supported. + * + * @param atom The atom to parse. + * @return The parsed PSSH atom. Null if the input is not a valid PSSH atom, or if the PSSH atom + * has an unsupported version. + */ + // TODO: Support parsing of the key ids for version 1 PSSH atoms. + private static @Nullable PsshAtom parsePsshAtom(byte[] atom) { + ParsableByteArray atomData = new ParsableByteArray(atom); + if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { + // Data too short. + return null; + } + atomData.setPosition(0); + int atomSize = atomData.readInt(); + if (atomSize != atomData.bytesLeft() + 4) { + // Not an atom, or incorrect atom size. + return null; + } + int atomType = atomData.readInt(); + if (atomType != Atom.TYPE_pssh) { + // Not an atom, or incorrect atom type. + return null; + } + int atomVersion = Atom.parseFullAtomVersion(atomData.readInt()); + if (atomVersion > 1) { + Log.w(TAG, "Unsupported pssh version: " + atomVersion); + return null; + } + UUID uuid = new UUID(atomData.readLong(), atomData.readLong()); + if (atomVersion == 1) { + int keyIdCount = atomData.readUnsignedIntToInt(); + atomData.skipBytes(16 * keyIdCount); + } + int dataSize = atomData.readUnsignedIntToInt(); + if (dataSize != atomData.bytesLeft()) { + // Incorrect dataSize. + return null; + } + byte[] data = new byte[dataSize]; + atomData.readBytes(data, 0, dataSize); + return new PsshAtom(uuid, atomVersion, data); + } + + // TODO: Consider exposing this and making parsePsshAtom public. + private static class PsshAtom { + + private final UUID uuid; + private final int version; + private final byte[] schemeData; + + public PsshAtom(UUID uuid, int version, byte[] schemeData) { + this.uuid = uuid; + this.version = version; + this.schemeData = schemeData; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java new file mode 100644 index 0000000000..d58c2f06eb --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * Provides methods that peek data from an {@link ExtractorInput} and return whether the input + * appears to be in MP4 format. + */ +/* package */ final class Sniffer { + + /** The maximum number of bytes to peek when sniffing. */ + private static final int SEARCH_LENGTH = 4 * 1024; + + private static final int[] COMPATIBLE_BRANDS = + new int[] { + 0x69736f6d, // isom + 0x69736f32, // iso2 + 0x69736f33, // iso3 + 0x69736f34, // iso4 + 0x69736f35, // iso5 + 0x69736f36, // iso6 + 0x61766331, // avc1 + 0x68766331, // hvc1 + 0x68657631, // hev1 + 0x61763031, // av01 + 0x6d703431, // mp41 + 0x6d703432, // mp42 + 0x33673261, // 3g2a + 0x33673262, // 3g2b + 0x33677236, // 3gr6 + 0x33677336, // 3gs6 + 0x33676536, // 3ge6 + 0x33676736, // 3gg6 + 0x4d345620, // M4V[space] + 0x4d344120, // M4A[space] + 0x66347620, // f4v[space] + 0x6b646469, // kddi + 0x4d345650, // M4VP + 0x71742020, // qt[space][space], Apple QuickTime + 0x4d534e56, // MSNV, Sony PSP + 0x64627931, // dby1, Dolby Vision + }; + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being a fragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return Whether the input appears to be in the fragmented MP4 format. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffFragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, true); + } + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being an unfragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return Whether the input appears to be in the unfragmented MP4 format. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffUnfragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, false); + } + + private static boolean sniffInternal(ExtractorInput input, boolean fragmented) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH + ? SEARCH_LENGTH : inputLength); + + ParsableByteArray buffer = new ParsableByteArray(64); + int bytesSearched = 0; + boolean foundGoodFileType = false; + boolean isFragmented = false; + while (bytesSearched < bytesToSearch) { + // Read an atom header. + int headerSize = Atom.HEADER_SIZE; + buffer.reset(headerSize); + input.peekFully(buffer.data, 0, headerSize); + long atomSize = buffer.readUnsignedInt(); + int atomType = buffer.readInt(); + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large atom size. + headerSize = Atom.LONG_HEADER_SIZE; + input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); + buffer.setLimit(Atom.LONG_HEADER_SIZE); + atomSize = buffer.readLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. + long fileEndPosition = input.getLength(); + if (fileEndPosition != C.LENGTH_UNSET) { + atomSize = fileEndPosition - input.getPeekPosition() + headerSize; + } + } + + if (atomSize < headerSize) { + // The file is invalid because the atom size is too small for its header. + return false; + } + bytesSearched += headerSize; + + if (atomType == Atom.TYPE_moov) { + // We have seen the moov atom. We increase the search size to make sure we don't miss an + // mvex atom because the moov's size exceeds the search length. + bytesToSearch += (int) atomSize; + if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) { + // Make sure we don't exceed the file size. + bytesToSearch = (int) inputLength; + } + // Check for an mvex atom inside the moov atom to identify whether the file is fragmented. + continue; + } + + if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) { + // The movie is fragmented. Stop searching as we must have read any ftyp atom already. + isFragmented = true; + break; + } + + if (bytesSearched + atomSize - headerSize >= bytesToSearch) { + // Stop searching as peeking this atom would exceed the search limit. + break; + } + + int atomDataSize = (int) (atomSize - headerSize); + bytesSearched += atomDataSize; + if (atomType == Atom.TYPE_ftyp) { + // Parse the atom and check the file type/brand is compatible with the extractors. + if (atomDataSize < 8) { + return false; + } + buffer.reset(atomDataSize); + input.peekFully(buffer.data, 0, atomDataSize); + int brandsCount = atomDataSize / 4; + for (int i = 0; i < brandsCount; i++) { + if (i == 1) { + // This index refers to the minorVersion, not a brand, so skip it. + buffer.skipBytes(4); + } else if (isCompatibleBrand(buffer.readInt())) { + foundGoodFileType = true; + break; + } + } + if (!foundGoodFileType) { + // The types were not compatible and there is only one ftyp atom, so reject the file. + return false; + } + } else if (atomDataSize != 0) { + // Skip the atom. + input.advancePeekPosition(atomDataSize); + } + } + return foundGoodFileType && fragmented == isFragmented; + } + + /** + * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors. + */ + private static boolean isCompatibleBrand(int brand) { + // Accept all brands starting '3gp'. + if (brand >>> 8 == 0x00336770) { + return true; + } + for (int compatibleBrand : COMPATIBLE_BRANDS) { + if (compatibleBrand == brand) { + return true; + } + } + return false; + } + + private Sniffer() { + // Prevent instantiation. + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java new file mode 100644 index 0000000000..b7a1555a76 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Encapsulates information describing an MP4 track. + */ +public final class Track { + + /** + * The transformation to apply to samples in the track, if any. One of {@link + * #TRANSFORMATION_NONE} or {@link #TRANSFORMATION_CEA608_CDAT}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT}) + public @interface Transformation {} + /** + * A no-op sample transformation. + */ + public static final int TRANSFORMATION_NONE = 0; + /** + * A transformation for caption samples in cdat atoms. + */ + public static final int TRANSFORMATION_CEA608_CDAT = 1; + + /** + * The track identifier. + */ + public final int id; + + /** + * One of {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}. + */ + public final int type; + + /** + * The track timescale, defined as the number of time units that pass in one second. + */ + public final long timescale; + + /** + * The movie timescale. + */ + public final long movieTimescale; + + /** + * The duration of the track in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public final long durationUs; + + /** + * The format. + */ + public final Format format; + + /** + * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each + * sample. + */ + @Transformation public final int sampleTransformation; + + /** + * Durations of edit list segments in the movie timescale. Null if there is no edit list. + */ + @Nullable public final long[] editListDurations; + + /** + * Media times for edit list segments in the track timescale. Null if there is no edit list. + */ + @Nullable public final long[] editListMediaTimes; + + /** + * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for + * other track types. + */ + public final int nalUnitLengthFieldLength; + + @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; + + public Track(int id, int type, long timescale, long movieTimescale, long durationUs, + Format format, @Transformation int sampleTransformation, + @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, + @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) { + this.id = id; + this.type = type; + this.timescale = timescale; + this.movieTimescale = movieTimescale; + this.durationUs = durationUs; + this.format = format; + this.sampleTransformation = sampleTransformation; + this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes; + this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; + this.editListDurations = editListDurations; + this.editListMediaTimes = editListMediaTimes; + } + + /** + * Returns the {@link TrackEncryptionBox} for the given sample description index. + * + * @param sampleDescriptionIndex The given sample description index + * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no + * such entry exists. + */ + @Nullable + public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { + return sampleDescriptionEncryptionBoxes == null ? null + : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; + } + + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") + public Track copyWithFormat(Format format) { + return new Track( + id, + type, + timescale, + movieTimescale, + durationUs, + format, + sampleTransformation, + sampleDescriptionEncryptionBoxes, + nalUnitLengthFieldLength, + editListDurations, + editListMediaTimes); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java new file mode 100644 index 0000000000..04bfb82210 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; + +/** + * Encapsulates information parsed from a track encryption (tenc) box or sample group description + * (sgpd) box in an MP4 stream. + */ +public final class TrackEncryptionBox { + + private static final String TAG = "TrackEncryptionBox"; + + /** + * Indicates the encryption state of the samples in the sample group. + */ + public final boolean isEncrypted; + + /** + * The protection scheme type, as defined by the 'schm' box, or null if unknown. + */ + @Nullable public final String schemeType; + + /** + * A {@link TrackOutput.CryptoData} instance containing the encryption information from this + * {@link TrackEncryptionBox}. + */ + public final TrackOutput.CryptoData cryptoData; + + /** The initialization vector size in bytes for the samples in the corresponding sample group. */ + public final int perSampleIvSize; + + /** + * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the + * track encryption box or sample group description box. Null otherwise. + */ + @Nullable public final byte[] defaultInitializationVector; + + /** + * @param isEncrypted See {@link #isEncrypted}. + * @param schemeType See {@link #schemeType}. + * @param perSampleIvSize See {@link #perSampleIvSize}. + * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}. + * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}. + * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}. + * @param defaultInitializationVector See {@link #defaultInitializationVector}. + */ + public TrackEncryptionBox( + boolean isEncrypted, + @Nullable String schemeType, + int perSampleIvSize, + byte[] keyId, + int defaultEncryptedBlocks, + int defaultClearBlocks, + @Nullable byte[] defaultInitializationVector) { + Assertions.checkArgument(perSampleIvSize == 0 ^ defaultInitializationVector == null); + this.isEncrypted = isEncrypted; + this.schemeType = schemeType; + this.perSampleIvSize = perSampleIvSize; + this.defaultInitializationVector = defaultInitializationVector; + cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId, + defaultEncryptedBlocks, defaultClearBlocks); + } + + @C.CryptoMode + private static int schemeToCryptoMode(@Nullable String schemeType) { + if (schemeType == null) { + // If unknown, assume cenc. + return C.CRYPTO_MODE_AES_CTR; + } + switch (schemeType) { + case C.CENC_TYPE_cenc: + case C.CENC_TYPE_cens: + return C.CRYPTO_MODE_AES_CTR; + case C.CENC_TYPE_cbc1: + case C.CENC_TYPE_cbcs: + return C.CRYPTO_MODE_AES_CBC; + default: + Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR " + + "crypto mode."); + return C.CRYPTO_MODE_AES_CTR; + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java new file mode 100644 index 0000000000..e027d6ed76 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * A holder for information corresponding to a single fragment of an mp4 file. + */ +/* package */ final class TrackFragment { + + /** + * The default values for samples from the track fragment header. + */ + public DefaultSampleValues header; + /** + * The position (byte offset) of the start of fragment. + */ + public long atomPosition; + /** + * The position (byte offset) of the start of data contained in the fragment. + */ + public long dataPosition; + /** + * The position (byte offset) of the start of auxiliary data. + */ + public long auxiliaryDataPosition; + /** + * The number of track runs of the fragment. + */ + public int trunCount; + /** + * The total number of samples in the fragment. + */ + public int sampleCount; + /** + * The position (byte offset) of the start of sample data of each track run in the fragment. + */ + public long[] trunDataPosition; + /** + * The number of samples contained by each track run in the fragment. + */ + public int[] trunLength; + /** + * The size of each sample in the fragment. + */ + public int[] sampleSizeTable; + /** + * The composition time offset of each sample in the fragment. + */ + public int[] sampleCompositionTimeOffsetTable; + /** + * The decoding time of each sample in the fragment. + */ + public long[] sampleDecodingTimeTable; + /** + * Indicates which samples are sync frames. + */ + public boolean[] sampleIsSyncFrameTable; + /** + * Whether the fragment defines encryption data. + */ + public boolean definesEncryptionData; + /** + * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption. + * Undefined otherwise. + */ + public boolean[] sampleHasSubsampleEncryptionTable; + /** + * Fragment specific track encryption. May be null. + */ + public TrackEncryptionBox trackEncryptionBox; + /** + * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data. + * Undefined otherwise. + */ + public int sampleEncryptionDataLength; + /** + * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined + * otherwise. + */ + public ParsableByteArray sampleEncryptionData; + /** + * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data. + */ + public boolean sampleEncryptionDataNeedsFill; + /** + * The absolute decode time of the start of the next fragment. + */ + public long nextFragmentDecodeTime; + + /** + * Resets the fragment. + * <p> + * {@link #sampleCount} and {@link #nextFragmentDecodeTime} are set to 0, and both + * {@link #definesEncryptionData} and {@link #sampleEncryptionDataNeedsFill} is set to false, + * and {@link #trackEncryptionBox} is set to null. + */ + public void reset() { + trunCount = 0; + nextFragmentDecodeTime = 0; + definesEncryptionData = false; + sampleEncryptionDataNeedsFill = false; + trackEncryptionBox = null; + } + + /** + * Configures the fragment for the specified number of samples. + * <p> + * The {@link #sampleCount} of the fragment is set to the specified sample count, and the + * contained tables are resized if necessary such that they are at least this length. + * + * @param sampleCount The number of samples in the new run. + */ + public void initTables(int trunCount, int sampleCount) { + this.trunCount = trunCount; + this.sampleCount = sampleCount; + if (trunLength == null || trunLength.length < trunCount) { + trunDataPosition = new long[trunCount]; + trunLength = new int[trunCount]; + } + if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) { + // Size the tables 25% larger than needed, so as to make future resize operations less + // likely. The choice of 25% is relatively arbitrary. + int tableSize = (sampleCount * 125) / 100; + sampleSizeTable = new int[tableSize]; + sampleCompositionTimeOffsetTable = new int[tableSize]; + sampleDecodingTimeTable = new long[tableSize]; + sampleIsSyncFrameTable = new boolean[tableSize]; + sampleHasSubsampleEncryptionTable = new boolean[tableSize]; + } + } + + /** + * Configures the fragment to be one that defines encryption data of the specified length. + * <p> + * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to + * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it + * is at least this length. + * + * @param length The length in bytes of the encryption data. + */ + public void initEncryptionData(int length) { + if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) { + sampleEncryptionData = new ParsableByteArray(length); + } + sampleEncryptionDataLength = length; + definesEncryptionData = true; + sampleEncryptionDataNeedsFill = true; + } + + /** + * Fills {@link #sampleEncryptionData} from the provided input. + * + * @param input An {@link ExtractorInput} from which to read the encryption data. + */ + public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + } + + /** + * Fills {@link #sampleEncryptionData} from the provided source. + * + * @param source A source from which to read the encryption data. + */ + public void fillEncryptionData(ParsableByteArray source) { + source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + } + + public long getSamplePresentationTime(int index) { + return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; + } + + /** Returns whether the sample at the given index has a subsample encryption table. */ + public boolean sampleHasSubsampleEncryptionTable(int index) { + return definesEncryptionData && sampleHasSubsampleEncryptionTable[index]; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java new file mode 100644 index 0000000000..bb9891b302 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Sample table for a track in an MP4 file. + */ +/* package */ final class TrackSampleTable { + + /** The track corresponding to this sample table. */ + public final Track track; + /** Number of samples. */ + public final int sampleCount; + /** Sample offsets in bytes. */ + public final long[] offsets; + /** Sample sizes in bytes. */ + public final int[] sizes; + /** Maximum sample size in {@link #sizes}. */ + public final int maximumSize; + /** Sample timestamps in microseconds. */ + public final long[] timestampsUs; + /** Sample flags. */ + public final int[] flags; + /** + * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample + * table is empty. + */ + public final long durationUs; + + public TrackSampleTable( + Track track, + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestampsUs, + int[] flags, + long durationUs) { + Assertions.checkArgument(sizes.length == timestampsUs.length); + Assertions.checkArgument(offsets.length == timestampsUs.length); + Assertions.checkArgument(flags.length == timestampsUs.length); + + this.track = track; + this.offsets = offsets; + this.sizes = sizes; + this.maximumSize = maximumSize; + this.timestampsUs = timestampsUs; + this.flags = flags; + this.durationUs = durationUs; + sampleCount = offsets.length; + if (flags.length > 0) { + flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE; + } + } + + /** + * Returns the sample index of the closest synchronization sample at or before the given + * timestamp, if one is available. + * + * @param timeUs Timestamp adjacent to which to find a synchronization sample. + * @return Index of the synchronization sample, or {@link C#INDEX_UNSET} if none. + */ + public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { + // Video frame timestamps may not be sorted, so the behavior of this call can be undefined. + // Frames are not reordered past synchronization samples so this works in practice. + int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); + for (int i = startIndex; i >= 0; i--) { + if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + return i; + } + } + return C.INDEX_UNSET; + } + + /** + * Returns the sample index of the closest synchronization sample at or after the given timestamp, + * if one is available. + * + * @param timeUs Timestamp adjacent to which to find a synchronization sample. + * @return index Index of the synchronization sample, or {@link C#INDEX_UNSET} if none. + */ + public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { + int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); + for (int i = startIndex; i < timestampsUs.length; i++) { + if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + return i; + } + } + return C.INDEX_UNSET; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java new file mode 100644 index 0000000000..5d3b27e294 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; + +/** Seeks in an Ogg stream. */ +/* package */ final class DefaultOggSeeker implements OggSeeker { + + private static final int MATCH_RANGE = 72000; + private static final int MATCH_BYTE_RANGE = 100000; + private static final int DEFAULT_OFFSET = 30000; + + private static final int STATE_SEEK_TO_END = 0; + private static final int STATE_READ_LAST_PAGE = 1; + private static final int STATE_SEEK = 2; + private static final int STATE_SKIP = 3; + private static final int STATE_IDLE = 4; + + private final OggPageHeader pageHeader = new OggPageHeader(); + private final long payloadStartPosition; + private final long payloadEndPosition; + private final StreamReader streamReader; + + private int state; + private long totalGranules; + private long positionBeforeSeekToEnd; + private long targetGranule; + + private long start; + private long end; + private long startGranule; + private long endGranule; + + /** + * Constructs an OggSeeker. + * + * @param streamReader The {@link StreamReader} that owns this seeker. + * @param payloadStartPosition Start position of the payload (inclusive). + * @param payloadEndPosition End position of the payload (exclusive). + * @param firstPayloadPageSize The total size of the first payload page, in bytes. + * @param firstPayloadPageGranulePosition The granule position of the first payload page. + * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page. + */ + public DefaultOggSeeker( + StreamReader streamReader, + long payloadStartPosition, + long payloadEndPosition, + long firstPayloadPageSize, + long firstPayloadPageGranulePosition, + boolean firstPayloadPageIsLastPage) { + Assertions.checkArgument( + payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition); + this.streamReader = streamReader; + this.payloadStartPosition = payloadStartPosition; + this.payloadEndPosition = payloadEndPosition; + if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition + || firstPayloadPageIsLastPage) { + totalGranules = firstPayloadPageGranulePosition; + state = STATE_IDLE; + } else { + state = STATE_SEEK_TO_END; + } + } + + @Override + @SuppressWarnings("fallthrough") + public long read(ExtractorInput input) throws IOException, InterruptedException { + switch (state) { + case STATE_IDLE: + return -1; + case STATE_SEEK_TO_END: + positionBeforeSeekToEnd = input.getPosition(); + state = STATE_READ_LAST_PAGE; + // Seek to the end just before the last page of stream to get the duration. + long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE; + if (lastPageSearchPosition > positionBeforeSeekToEnd) { + return lastPageSearchPosition; + } + // Fall through. + case STATE_READ_LAST_PAGE: + totalGranules = readGranuleOfLastPage(input); + state = STATE_IDLE; + return positionBeforeSeekToEnd; + case STATE_SEEK: + long position = getNextSeekPosition(input); + if (position != C.POSITION_UNSET) { + return position; + } + state = STATE_SKIP; + // Fall through. + case STATE_SKIP: + skipToPageOfTargetGranule(input); + state = STATE_IDLE; + return -(startGranule + 2); + default: + // Never happens. + throw new IllegalStateException(); + } + } + + @Override + public OggSeekMap createSeekMap() { + return totalGranules != 0 ? new OggSeekMap() : null; + } + + @Override + public void startSeek(long targetGranule) { + this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1); + state = STATE_SEEK; + start = payloadStartPosition; + end = payloadEndPosition; + startGranule = 0; + endGranule = totalGranules; + } + + /** + * Performs a single step of a seeking binary search, returning the byte position from which data + * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged. + * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be + * called to skip to the target page. + * + * @param input The {@link ExtractorInput} to read from. + * @return The byte position from which data should be provided for the next step, or {@link + * C#POSITION_UNSET} if the search has converged. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException { + if (start == end) { + return C.POSITION_UNSET; + } + + long currentPosition = input.getPosition(); + if (!skipToNextPage(input, end)) { + if (start == currentPosition) { + throw new IOException("No ogg page can be found."); + } + return start; + } + + pageHeader.populate(input, /* quiet= */ false); + input.resetPeekPosition(); + + long granuleDistance = targetGranule - pageHeader.granulePosition; + int pageSize = pageHeader.headerSize + pageHeader.bodySize; + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; + } + + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } + + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; + } + + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); + long nextPosition = + input.getPosition() + - offset + + (granuleDistance * (end - start) / (endGranule - startGranule)); + return Util.constrainValue(nextPosition, start, end - 1); + } + + /** + * Skips forward to the start of the page containing the {@code targetGranule}. + * + * @param input The {@link ExtractorInput} to read from. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + private void skipToPageOfTargetGranule(ExtractorInput input) + throws IOException, InterruptedException { + pageHeader.populate(input, /* quiet= */ false); + while (pageHeader.granulePosition <= targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + start = input.getPosition(); + startGranule = pageHeader.granulePosition; + pageHeader.populate(input, /* quiet= */ false); + } + input.resetPeekPosition(); + } + + /** + * Skips to the next page. + * + * @param input The {@code ExtractorInput} to skip to the next page. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If the thread is interrupted. + * @throws EOFException If the next page can't be found before the end of the input. + */ + @VisibleForTesting + void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { + if (!skipToNextPage(input, payloadEndPosition)) { + // Not found until eof. + throw new EOFException(); + } + } + + /** + * Skips to the next page. Searches for the next page header. + * + * @param input The {@code ExtractorInput} to skip to the next page. + * @param limit The limit up to which the search should take place. + * @return Whether the next page was found. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If interrupted while peeking/reading from the input. + */ + private boolean skipToNextPage(ExtractorInput input, long limit) + throws IOException, InterruptedException { + limit = Math.min(limit + 3, payloadEndPosition); + byte[] buffer = new byte[2048]; + int peekLength = buffer.length; + while (true) { + if (input.getPosition() + peekLength > limit) { + // Make sure to not peek beyond the end of the input. + peekLength = (int) (limit - input.getPosition()); + if (peekLength < 4) { + // Not found until end. + return false; + } + } + input.peekFully(buffer, 0, peekLength, false); + for (int i = 0; i < peekLength - 3; i++) { + if (buffer[i] == 'O' + && buffer[i + 1] == 'g' + && buffer[i + 2] == 'g' + && buffer[i + 3] == 'S') { + // Match! Skip to the start of the pattern. + input.skipFully(i); + return true; + } + } + // Overlap by not skipping the entire peekLength. + input.skipFully(peekLength - 3); + } + } + + /** + * Skips to the last Ogg page in the stream and reads the header's granule field which is the + * total number of samples per channel. + * + * @param input The {@link ExtractorInput} to read from. + * @return The total number of samples of this input. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If the thread is interrupted. + */ + @VisibleForTesting + long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { + skipToNextPage(input); + pageHeader.reset(); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) { + pageHeader.populate(input, /* quiet= */ false); + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + } + return pageHeader.granulePosition; + } + + private final class OggSeekMap implements SeekMap { + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long targetGranule = streamReader.convertTimeToGranule(timeUs); + long estimatedPosition = + payloadStartPosition + + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules) + - DEFAULT_OFFSET; + estimatedPosition = + Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java new file mode 100644 index 0000000000..449bf35f78 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.Arrays; + +/** + * {@link StreamReader} to extract Flac data out of Ogg byte stream. + */ +/* package */ final class FlacReader extends StreamReader { + + private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; + + private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; + + private FlacStreamMetadata streamMetadata; + private FlacOggSeeker flacOggSeeker; + + public static boolean verifyBitstreamType(ParsableByteArray data) { + return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type + data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC" + } + + @Override + protected void reset(boolean headerData) { + super.reset(headerData); + if (headerData) { + streamMetadata = null; + flacOggSeeker = null; + } + } + + private static boolean isAudioPacket(byte[] data) { + return data[0] == AUDIO_PACKET_TYPE; + } + + @Override + protected long preparePayload(ParsableByteArray packet) { + if (!isAudioPacket(packet.data)) { + return -1; + } + return getFlacFrameBlockSize(packet); + } + + @Override + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { + byte[] data = packet.data; + if (streamMetadata == null) { + streamMetadata = new FlacStreamMetadata(data, 17); + byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); + setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null); + } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) { + flacOggSeeker = new FlacOggSeeker(); + FlacStreamMetadata.SeekTable seekTable = + FlacMetadataReader.readSeekTableMetadataBlock(packet); + streamMetadata = streamMetadata.copyWithSeekTable(seekTable); + } else if (isAudioPacket(data)) { + if (flacOggSeeker != null) { + flacOggSeeker.setFirstFrameOffset(position); + setupData.oggSeeker = flacOggSeeker; + } + return false; + } + return true; + } + + private int getFlacFrameBlockSize(ParsableByteArray packet) { + int blockSizeKey = (packet.data[2] & 0xFF) >> 4; + if (blockSizeKey == 6 || blockSizeKey == 7) { + // Skip the sample number. + packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); + packet.readUtf8EncodedLong(); + } + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey); + packet.setPosition(0); + return result; + } + + private class FlacOggSeeker implements OggSeeker { + + private long firstFrameOffset; + private long pendingSeekGranule; + + public FlacOggSeeker() { + firstFrameOffset = -1; + pendingSeekGranule = -1; + } + + public void setFirstFrameOffset(long firstFrameOffset) { + this.firstFrameOffset = firstFrameOffset; + } + + @Override + public long read(ExtractorInput input) throws IOException, InterruptedException { + if (pendingSeekGranule >= 0) { + long result = -(pendingSeekGranule + 2); + pendingSeekGranule = -1; + return result; + } + return -1; + } + + @Override + public void startSeek(long targetGranule) { + Assertions.checkNotNull(streamMetadata.seekTable); + long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers; + int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); + pendingSeekGranule = seekPointGranules[index]; + } + + @Override + public SeekMap createSeekMap() { + Assertions.checkState(firstFrameOffset != -1); + return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset); + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java new file mode 100644 index 0000000000..da53a47dc0 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * Extracts data from the Ogg container format. + */ +public class OggExtractor implements Extractor { + + /** Factory for {@link OggExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new OggExtractor()}; + + private static final int MAX_VERIFICATION_BYTES = 8; + + private ExtractorOutput output; + private StreamReader streamReader; + private boolean streamReaderInitialized; + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + try { + return sniffInternal(input); + } catch (ParserException e) { + return false; + } + } + + @Override + public void init(ExtractorOutput output) { + this.output = output; + } + + @Override + public void seek(long position, long timeUs) { + if (streamReader != null) { + streamReader.seek(position, timeUs); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (streamReader == null) { + if (!sniffInternal(input)) { + throw new ParserException("Failed to determine bitstream type"); + } + input.resetPeekPosition(); + } + if (!streamReaderInitialized) { + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + streamReader.init(output, trackOutput); + streamReaderInitialized = true; + } + return streamReader.read(input, seekPosition); + } + + private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException { + OggPageHeader header = new OggPageHeader(); + if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { + return false; + } + + int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); + ParsableByteArray scratch = new ParsableByteArray(length); + input.peekFully(scratch.data, 0, length); + + if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new FlacReader(); + } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new VorbisReader(); + } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new OpusReader(); + } else { + return false; + } + return true; + } + + private static ParsableByteArray resetPosition(ParsableByteArray scratch) { + scratch.setPosition(0); + return scratch; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java new file mode 100644 index 0000000000..1f3bf38c73 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.Arrays; + +/** + * OGG packet class. + */ +/* package */ final class OggPacket { + + private final OggPageHeader pageHeader = new OggPageHeader(); + private final ParsableByteArray packetArray = new ParsableByteArray( + new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); + + private int currentSegmentIndex = C.INDEX_UNSET; + private int segmentCount; + private boolean populated; + + /** + * Resets this reader. + */ + public void reset() { + pageHeader.reset(); + packetArray.reset(); + currentSegmentIndex = C.INDEX_UNSET; + populated = false; + } + + /** + * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make + * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader + * can resume properly from an error while reading a continued packet spanned across multiple + * pages. + * + * @param input The {@link ExtractorInput} to read data from. + * @return {@code true} if the read was successful. The read fails if the end of the input is + * encountered without reading data. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If the thread is interrupted. + */ + public boolean populate(ExtractorInput input) throws IOException, InterruptedException { + Assertions.checkState(input != null); + + if (populated) { + populated = false; + packetArray.reset(); + } + + while (!populated) { + if (currentSegmentIndex < 0) { + // We're at the start of a page. + if (!pageHeader.populate(input, true)) { + return false; + } + int segmentIndex = 0; + int bytesToSkip = pageHeader.headerSize; + if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) { + // After seeking, the first packet may be the remainder + // part of a continued packet which has to be discarded. + bytesToSkip += calculatePacketSize(segmentIndex); + segmentIndex += segmentCount; + } + input.skipFully(bytesToSkip); + currentSegmentIndex = segmentIndex; + } + + int size = calculatePacketSize(currentSegmentIndex); + int segmentIndex = currentSegmentIndex + segmentCount; + if (size > 0) { + if (packetArray.capacity() < packetArray.limit() + size) { + packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size); + } + input.readFully(packetArray.data, packetArray.limit(), size); + packetArray.setLimit(packetArray.limit() + size); + populated = pageHeader.laces[segmentIndex - 1] != 255; + } + // Advance now since we are sure reading didn't throw an exception. + currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? C.INDEX_UNSET + : segmentIndex; + } + return true; + } + + /** + * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read, + * or an empty header if the packet has yet to be populated. + * + * <p>Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent + * calls to {@link #populate(ExtractorInput)}. + * + * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet + * to be populated. + */ + public OggPageHeader getPageHeader() { + return pageHeader; + } + + /** + * Returns a {@link ParsableByteArray} containing the packet's payload. + */ + public ParsableByteArray getPayload() { + return packetArray; + } + + /** + * Trims the packet data array. + */ + public void trimPayload() { + if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) { + return; + } + packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD, + packetArray.limit())); + } + + /** + * Calculates the size of the packet starting from {@code startSegmentIndex}. + * + * @param startSegmentIndex the index of the first segment of the packet. + * @return Size of the packet. + */ + private int calculatePacketSize(int startSegmentIndex) { + segmentCount = 0; + int size = 0; + while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) { + int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++]; + size += segmentLength; + if (segmentLength != 255) { + // packets end at first lace < 255 + break; + } + } + return size; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java new file mode 100644 index 0000000000..afdccf80fd --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; + +/** + * Data object to store header information. + */ +/* package */ final class OggPageHeader { + + public static final int EMPTY_PAGE_HEADER_SIZE = 27; + public static final int MAX_SEGMENT_COUNT = 255; + public static final int MAX_PAGE_PAYLOAD = 255 * 255; + public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT + + MAX_PAGE_PAYLOAD; + + private static final int TYPE_OGGS = 0x4f676753; + + public int revision; + public int type; + /** + * The absolute granule position of the page. This is the total number of samples from the start + * of the file up to the <em>end</em> of the page. Samples partially in the page that continue on + * the next page do not count. + */ + public long granulePosition; + + public long streamSerialNumber; + public long pageSequenceNumber; + public long pageChecksum; + public int pageSegmentCount; + public int headerSize; + public int bodySize; + /** + * Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use + * {@link #pageSegmentCount} to iterate. + */ + public final int[] laces = new int[MAX_SEGMENT_COUNT]; + + private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT); + + /** + * Resets all primitive member fields to zero. + */ + public void reset() { + revision = 0; + type = 0; + granulePosition = 0; + streamSerialNumber = 0; + pageSequenceNumber = 0; + pageChecksum = 0; + pageSegmentCount = 0; + headerSize = 0; + bodySize = 0; + } + + /** + * Peeks an Ogg page header and updates this {@link OggPageHeader}. + * + * @param input The {@link ExtractorInput} to read from. + * @param quiet Whether to return {@code false} rather than throwing an exception if the header + * cannot be populated. + * @return Whether the read was successful. The read fails if the end of the input is encountered + * without reading data. + * @throws IOException If reading data fails or the stream is invalid. + * @throws InterruptedException If the thread is interrupted. + */ + public boolean populate(ExtractorInput input, boolean quiet) + throws IOException, InterruptedException { + scratch.reset(); + reset(); + boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNSET + || input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE; + if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) { + if (quiet) { + return false; + } else { + throw new EOFException(); + } + } + if (scratch.readUnsignedInt() != TYPE_OGGS) { + if (quiet) { + return false; + } else { + throw new ParserException("expected OggS capture pattern at begin of page"); + } + } + + revision = scratch.readUnsignedByte(); + if (revision != 0x00) { + if (quiet) { + return false; + } else { + throw new ParserException("unsupported bit stream revision"); + } + } + type = scratch.readUnsignedByte(); + + granulePosition = scratch.readLittleEndianLong(); + streamSerialNumber = scratch.readLittleEndianUnsignedInt(); + pageSequenceNumber = scratch.readLittleEndianUnsignedInt(); + pageChecksum = scratch.readLittleEndianUnsignedInt(); + pageSegmentCount = scratch.readUnsignedByte(); + headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount; + + // calculate total size of header including laces + scratch.reset(); + input.peekFully(scratch.data, 0, pageSegmentCount); + for (int i = 0; i < pageSegmentCount; i++) { + laces[i] = scratch.readUnsignedByte(); + bodySize += laces[i]; + } + + return true; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java new file mode 100644 index 0000000000..0a0be963f7 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import java.io.IOException; + +/** + * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive + * seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position + * and start the seeking with an initial estimated position. + */ +/* package */ interface OggSeeker { + + /** + * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking + * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1. + */ + SeekMap createSeekMap(); + + /** + * Starts a seek operation. + * + * @param targetGranule The target granule position. + */ + void startSeek(long targetGranule); + + /** + * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek. + * <p/> + * If more data is required or if the position of the input needs to be modified then a position + * from which data should be provided is returned. Else a negative value is returned. If a seek + * has been completed then the value returned is -(currentGranule + 2). Else it is -1. + * + * @param input The {@link ExtractorInput} to read from. + * @return A non-negative position to seek the {@link ExtractorInput} to, or -(currentGranule + 2) + * if the progressive seek has completed, or -1 otherwise. + * @throws IOException If reading from the {@link ExtractorInput} fails. + * @throws InterruptedException If the thread is interrupted. + */ + long read(ExtractorInput input) throws IOException, InterruptedException; + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java new file mode 100644 index 0000000000..c3f3a13d54 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * {@link StreamReader} to extract Opus data out of Ogg byte stream. + */ +/* package */ final class OpusReader extends StreamReader { + + private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840; + + /** + * Opus streams are always decoded at 48000 Hz. + */ + private static final int SAMPLE_RATE = 48000; + + private static final int OPUS_CODE = 0x4f707573; + private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; + + private boolean headerRead; + + public static boolean verifyBitstreamType(ParsableByteArray data) { + if (data.bytesLeft() < OPUS_SIGNATURE.length) { + return false; + } + byte[] header = new byte[OPUS_SIGNATURE.length]; + data.readBytes(header, 0, OPUS_SIGNATURE.length); + return Arrays.equals(header, OPUS_SIGNATURE); + } + + @Override + protected void reset(boolean headerData) { + super.reset(headerData); + if (headerData) { + headerRead = false; + } + } + + @Override + protected long preparePayload(ParsableByteArray packet) { + return convertTimeToGranule(getPacketDurationUs(packet.data)); + } + + @Override + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { + if (!headerRead) { + byte[] metadata = Arrays.copyOf(packet.data, packet.limit()); + int channelCount = metadata[9] & 0xFF; + int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF); + + List<byte[]> initializationData = new ArrayList<>(3); + initializationData.add(metadata); + putNativeOrderLong(initializationData, preskip); + putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES); + + setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, + null); + headerRead = true; + } else { + boolean headerPacket = packet.readInt() == OPUS_CODE; + packet.setPosition(0); + return headerPacket; + } + return true; + } + + private void putNativeOrderLong(List<byte[]> initializationData, int samples) { + long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE; + byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array(); + initializationData.add(array); + } + + /** + * Returns the duration of the given audio packet. + * + * @param packet Contains audio data. + * @return Returns the duration of the given audio packet. + */ + private long getPacketDurationUs(byte[] packet) { + int toc = packet[0] & 0xFF; + int frames; + switch (toc & 0x3) { + case 0: + frames = 1; + break; + case 1: + case 2: + frames = 2; + break; + default: + frames = packet[1] & 0x3F; + break; + } + + int config = toc >> 3; + int length = config & 0x3; + if (config >= 16) { + length = 2500 << length; + } else if (config >= 12) { + length = 10000 << (length & 0x1); + } else if (length == 3) { + length = 60000; + } else { + length = 10000 << length; + } + return (long) frames * length; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java new file mode 100644 index 0000000000..067c8aef03 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** StreamReader abstract class. */ +@SuppressWarnings("UngroupedOverloads") +/* package */ abstract class StreamReader { + + private static final int STATE_READ_HEADERS = 0; + private static final int STATE_SKIP_HEADERS = 1; + private static final int STATE_READ_PAYLOAD = 2; + private static final int STATE_END_OF_INPUT = 3; + + static class SetupData { + Format format; + OggSeeker oggSeeker; + } + + private final OggPacket oggPacket; + + private TrackOutput trackOutput; + private ExtractorOutput extractorOutput; + private OggSeeker oggSeeker; + private long targetGranule; + private long payloadStartPosition; + private long currentGranule; + private int state; + private int sampleRate; + private SetupData setupData; + private long lengthOfReadPacket; + private boolean seekMapSet; + private boolean formatSet; + + public StreamReader() { + oggPacket = new OggPacket(); + } + + void init(ExtractorOutput output, TrackOutput trackOutput) { + this.extractorOutput = output; + this.trackOutput = trackOutput; + reset(true); + } + + /** + * Resets the state of the {@link StreamReader}. + * + * @param headerData Resets parsed header data too. + */ + protected void reset(boolean headerData) { + if (headerData) { + setupData = new SetupData(); + payloadStartPosition = 0; + state = STATE_READ_HEADERS; + } else { + state = STATE_SKIP_HEADERS; + } + targetGranule = -1; + currentGranule = 0; + } + + /** + * @see Extractor#seek(long, long) + */ + final void seek(long position, long timeUs) { + oggPacket.reset(); + if (position == 0) { + reset(!seekMapSet); + } else { + if (state != STATE_READ_HEADERS) { + targetGranule = convertTimeToGranule(timeUs); + oggSeeker.startSeek(targetGranule); + state = STATE_READ_PAYLOAD; + } + } + } + + /** + * @see Extractor#read(ExtractorInput, PositionHolder) + */ + final int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + switch (state) { + case STATE_READ_HEADERS: + return readHeaders(input); + case STATE_SKIP_HEADERS: + input.skipFully((int) payloadStartPosition); + state = STATE_READ_PAYLOAD; + return Extractor.RESULT_CONTINUE; + case STATE_READ_PAYLOAD: + return readPayload(input, seekPosition); + default: + // Never happens. + throw new IllegalStateException(); + } + } + + private int readHeaders(ExtractorInput input) throws IOException, InterruptedException { + boolean readingHeaders = true; + while (readingHeaders) { + if (!oggPacket.populate(input)) { + state = STATE_END_OF_INPUT; + return Extractor.RESULT_END_OF_INPUT; + } + lengthOfReadPacket = input.getPosition() - payloadStartPosition; + + readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData); + if (readingHeaders) { + payloadStartPosition = input.getPosition(); + } + } + + sampleRate = setupData.format.sampleRate; + if (!formatSet) { + trackOutput.format(setupData.format); + formatSet = true; + } + + if (setupData.oggSeeker != null) { + oggSeeker = setupData.oggSeeker; + } else if (input.getLength() == C.LENGTH_UNSET) { + oggSeeker = new UnseekableOggSeeker(); + } else { + OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader(); + boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. + oggSeeker = + new DefaultOggSeeker( + this, + payloadStartPosition, + input.getLength(), + firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, + firstPayloadPageHeader.granulePosition, + isLastPage); + } + + setupData = null; + state = STATE_READ_PAYLOAD; + // First payload packet. Trim the payload array of the ogg packet after headers have been read. + oggPacket.trimPayload(); + return Extractor.RESULT_CONTINUE; + } + + private int readPayload(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + long position = oggSeeker.read(input); + if (position >= 0) { + seekPosition.position = position; + return Extractor.RESULT_SEEK; + } else if (position < -1) { + onSeekEnd(-(position + 2)); + } + if (!seekMapSet) { + SeekMap seekMap = oggSeeker.createSeekMap(); + extractorOutput.seekMap(seekMap); + seekMapSet = true; + } + + if (lengthOfReadPacket > 0 || oggPacket.populate(input)) { + lengthOfReadPacket = 0; + ParsableByteArray payload = oggPacket.getPayload(); + long granulesInPacket = preparePayload(payload); + if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) { + // calculate time and send payload data to codec + long timeUs = convertGranuleToTime(currentGranule); + trackOutput.sampleData(payload, payload.limit()); + trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null); + targetGranule = -1; + } + currentGranule += granulesInPacket; + } else { + state = STATE_END_OF_INPUT; + return Extractor.RESULT_END_OF_INPUT; + } + return Extractor.RESULT_CONTINUE; + } + + /** + * Converts granule value to time. + * + * @param granule The granule value. + * @return Time in milliseconds. + */ + protected long convertGranuleToTime(long granule) { + return (granule * C.MICROS_PER_SECOND) / sampleRate; + } + + /** + * Converts time value to granule. + * + * @param timeUs Time in milliseconds. + * @return The granule value. + */ + protected long convertTimeToGranule(long timeUs) { + return (sampleRate * timeUs) / C.MICROS_PER_SECOND; + } + + /** + * Prepares payload data in the packet for submitting to TrackOutput and returns number of + * granules in the packet. + * + * @param packet Ogg payload data packet. + * @return Number of granules in the packet or -1 if the packet doesn't contain payload data. + */ + protected abstract long preparePayload(ParsableByteArray packet); + + /** + * Checks if the given packet is a header packet and reads it. + * + * @param packet An ogg packet. + * @param position Position of the given header packet. + * @param setupData Setup data to be filled. + * @return Whether the packet contains header data. + */ + protected abstract boolean readHeaders(ParsableByteArray packet, long position, + SetupData setupData) throws IOException, InterruptedException; + + /** + * Called on end of seeking. + * + * @param currentGranule The granule at the current input position. + */ + protected void onSeekEnd(long currentGranule) { + this.currentGranule = currentGranule; + } + + private static final class UnseekableOggSeeker implements OggSeeker { + + @Override + public long read(ExtractorInput input) { + return -1; + } + + @Override + public void startSeek(long targetGranule) { + // Do nothing. + } + + @Override + public SeekMap createSeekMap() { + return new SeekMap.Unseekable(C.TIME_UNSET); + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java new file mode 100644 index 0000000000..cb0678a285 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; + +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.Mode; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.ArrayList; + +/** + * {@link StreamReader} to extract Vorbis data out of Ogg byte stream. + */ +/* package */ final class VorbisReader extends StreamReader { + + private VorbisSetup vorbisSetup; + private int previousPacketBlockSize; + private boolean seenFirstAudioPacket; + + private VorbisUtil.VorbisIdHeader vorbisIdHeader; + private VorbisUtil.CommentHeader commentHeader; + + public static boolean verifyBitstreamType(ParsableByteArray data) { + try { + return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true); + } catch (ParserException e) { + return false; + } + } + + @Override + protected void reset(boolean headerData) { + super.reset(headerData); + if (headerData) { + vorbisSetup = null; + vorbisIdHeader = null; + commentHeader = null; + } + previousPacketBlockSize = 0; + seenFirstAudioPacket = false; + } + + @Override + protected void onSeekEnd(long currentGranule) { + super.onSeekEnd(currentGranule); + seenFirstAudioPacket = currentGranule != 0; + previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0; + } + + @Override + protected long preparePayload(ParsableByteArray packet) { + // if this is not an audio packet... + if ((packet.data[0] & 0x01) == 1) { + return -1; + } + + // ... we need to decode the block size + int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup); + // a packet contains samples produced from overlapping the previous and current frame data + // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2) + int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 + : 0; + // codec expects the number of samples appended to audio data + appendNumberOfSamples(packet, samplesInPacket); + + // update state in members for next iteration + seenFirstAudioPacket = true; + previousPacketBlockSize = packetBlockSize; + return samplesInPacket; + } + + @Override + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) + throws IOException, InterruptedException { + if (vorbisSetup != null) { + return false; + } + + vorbisSetup = readSetupHeaders(packet); + if (vorbisSetup == null) { + return true; + } + + ArrayList<byte[]> codecInitialisationData = new ArrayList<>(); + codecInitialisationData.add(vorbisSetup.idHeader.data); + codecInitialisationData.add(vorbisSetup.setupHeaderData); + + setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, + this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE, + this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, + codecInitialisationData, null, 0, null); + return true; + } + + @VisibleForTesting + /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException { + + if (vorbisIdHeader == null) { + vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch); + return null; + } + + if (commentHeader == null) { + commentHeader = VorbisUtil.readVorbisCommentHeader(scratch); + return null; + } + + // the third packet contains the setup header + byte[] setupHeaderData = new byte[scratch.limit()]; + // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2 + System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit()); + // partially decode setup header to get the modes + Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels); + // we need the ilog of modes all the time when extracting, so we compute it once + int iLogModes = VorbisUtil.iLog(modes.length - 1); + + return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes); + } + + /** + * Reads an int of {@code length} bits from {@code src} starting at {@code + * leastSignificantBitIndex}. + * + * @param src the {@code byte} to read from. + * @param length the length in bits of the int to read. + * @param leastSignificantBitIndex the index of the least significant bit of the int to read. + * @return the int value read. + */ + @VisibleForTesting + /* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) { + return (src >> leastSignificantBitIndex) & (255 >>> (8 - length)); + } + + @VisibleForTesting + /* package */ static void appendNumberOfSamples( + ParsableByteArray buffer, long packetSampleCount) { + + buffer.setLimit(buffer.limit() + 4); + // The vorbis decoder expects the number of samples in the packet + // to be appended to the audio data as an int32 + buffer.data[buffer.limit() - 4] = (byte) (packetSampleCount & 0xFF); + buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF); + buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF); + buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF); + } + + private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) { + // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1) + int modeNumber = readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1); + int currentBlockSize; + if (!vorbisSetup.modes[modeNumber].blockFlag) { + currentBlockSize = vorbisSetup.idHeader.blockSize0; + } else { + currentBlockSize = vorbisSetup.idHeader.blockSize1; + } + return currentBlockSize; + } + + /** + * Class to hold all data read from Vorbis setup headers. + */ + /* package */ static final class VorbisSetup { + + public final VorbisUtil.VorbisIdHeader idHeader; + public final VorbisUtil.CommentHeader commentHeader; + public final byte[] setupHeaderData; + public final Mode[] modes; + public final int iLogModes; + + public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader + commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) { + this.idHeader = idHeader; + this.commentHeader = commentHeader; + this.setupHeaderData = setupHeaderData; + this.modes = modes; + this.iLogModes = iLogModes; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java new file mode 100644 index 0000000000..a7b32782ff --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.rawcc; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * Extracts data from the RawCC container format. + */ +public final class RawCcExtractor implements Extractor { + + private static final int SCRATCH_SIZE = 9; + private static final int HEADER_SIZE = 8; + private static final int HEADER_ID = 0x52434301; + private static final int TIMESTAMP_SIZE_V0 = 4; + private static final int TIMESTAMP_SIZE_V1 = 8; + + // Parser states. + private static final int STATE_READING_HEADER = 0; + private static final int STATE_READING_TIMESTAMP_AND_COUNT = 1; + private static final int STATE_READING_SAMPLES = 2; + + private final Format format; + + private final ParsableByteArray dataScratch; + + private TrackOutput trackOutput; + + private int parserState; + private int version; + private long timestampUs; + private int remainingSampleCount; + private int sampleBytesWritten; + + public RawCcExtractor(Format format) { + this.format = format; + dataScratch = new ParsableByteArray(SCRATCH_SIZE); + parserState = STATE_READING_HEADER; + } + + @Override + public void init(ExtractorOutput output) { + output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + trackOutput = output.track(0, C.TRACK_TYPE_TEXT); + output.endTracks(); + trackOutput.format(format); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + dataScratch.reset(); + input.peekFully(dataScratch.data, 0, HEADER_SIZE); + return dataScratch.readInt() == HEADER_ID; + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + while (true) { + switch (parserState) { + case STATE_READING_HEADER: + if (parseHeader(input)) { + parserState = STATE_READING_TIMESTAMP_AND_COUNT; + } else { + return RESULT_END_OF_INPUT; + } + break; + case STATE_READING_TIMESTAMP_AND_COUNT: + if (parseTimestampAndSampleCount(input)) { + parserState = STATE_READING_SAMPLES; + } else { + parserState = STATE_READING_HEADER; + return RESULT_END_OF_INPUT; + } + break; + case STATE_READING_SAMPLES: + parseSamples(input); + parserState = STATE_READING_TIMESTAMP_AND_COUNT; + return RESULT_CONTINUE; + default: + throw new IllegalStateException(); + } + } + } + + @Override + public void seek(long position, long timeUs) { + parserState = STATE_READING_HEADER; + } + + @Override + public void release() { + // Do nothing + } + + private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException { + dataScratch.reset(); + if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) { + if (dataScratch.readInt() != HEADER_ID) { + throw new IOException("Input not RawCC"); + } + version = dataScratch.readUnsignedByte(); + // no versions use the flag fields yet + return true; + } else { + return false; + } + } + + private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException, + InterruptedException { + dataScratch.reset(); + if (version == 0) { + if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V0 + 1, true)) { + return false; + } + // version 0 timestamps are 45kHz, so we need to convert them into us + timestampUs = dataScratch.readUnsignedInt() * 1000 / 45; + } else if (version == 1) { + if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V1 + 1, true)) { + return false; + } + timestampUs = dataScratch.readLong(); + } else { + throw new ParserException("Unsupported version number: " + version); + } + + remainingSampleCount = dataScratch.readUnsignedByte(); + sampleBytesWritten = 0; + return true; + } + + private void parseSamples(ExtractorInput input) throws IOException, InterruptedException { + for (; remainingSampleCount > 0; remainingSampleCount--) { + dataScratch.reset(); + input.readFully(dataScratch.data, 0, 3); + + trackOutput.sampleData(dataScratch, 3); + sampleBytesWritten += 3; + } + + if (sampleBytesWritten > 0) { + trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java new file mode 100644 index 0000000000..a0a1365935 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** + * Extracts data from (E-)AC-3 bitstreams. + */ +public final class Ac3Extractor implements Extractor { + + /** Factory for {@link Ac3Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac3Extractor()}; + + /** + * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving + * up. + */ + private static final int MAX_SNIFF_BYTES = 8 * 1024; + private static final int AC3_SYNC_WORD = 0x0B77; + private static final int MAX_SYNC_FRAME_SIZE = 2786; + + private final Ac3Reader reader; + private final ParsableByteArray sampleData; + + private boolean startedPacket; + + /** Creates a new extractor for AC-3 bitstreams. */ + public Ac3Extractor() { + reader = new Ac3Reader(); + sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Skip any ID3 headers. + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); + int startPosition = 0; + while (true) { + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); // version, flags + int length = scratch.readSynchSafeInt(); + startPosition += 10 + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(startPosition); + + int headerPosition = startPosition; + int validFramesCount = 0; + while (true) { + input.peekFully(scratch.data, 0, 6); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (syncBytes != AC3_SYNC_WORD) { + validFramesCount = 0; + input.resetPeekPosition(); + if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + input.advancePeekPosition(headerPosition); + } else { + if (++validFramesCount >= 4) { + return true; + } + int frameSize = Ac3Util.parseAc3SyncframeSize(scratch.data); + if (frameSize == C.LENGTH_UNSET) { + return false; + } + input.advancePeekPosition(frameSize - 6); + } + } + } + + @Override + public void init(ExtractorOutput output) { + reader.createTracks(output, new TrackIdGenerator(0, 1)); + output.endTracks(); + output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + } + + @Override + public void seek(long position, long timeUs) { + startedPacket = false; + reader.seek(); + } + + @Override + public void release() { + // Do nothing. + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { + int bytesRead = input.read(sampleData.data, 0, MAX_SYNC_FRAME_SIZE); + if (bytesRead == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + + // Feed whatever data we have to the reader, regardless of whether the read finished or not. + sampleData.setPosition(0); + sampleData.setLimit(bytesRead); + + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); + startedPacket = true; + } + // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + reader.consume(sampleData); + return RESULT_CONTINUE; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java new file mode 100644 index 0000000000..3a6eebbcd2 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. + */ +public final class Ac3Reader implements ElementaryStreamReader { + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE}) + private @interface State {} + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private static final int HEADER_SIZE = 128; + + private final ParsableBitArray headerScratchBits; + private final ParsableByteArray headerScratchBytes; + private final String language; + + private String trackFormatId; + private TrackOutput output; + + @State private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWas0B; + + // Used when parsing the header. + private long sampleDurationUs; + private Format format; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** + * Constructs a new reader for (E-)AC-3 elementary streams. + */ + public Ac3Reader() { + this(null); + } + + /** + * Constructs a new reader for (E-)AC-3 elementary streams. + * + * @param language Track language. + */ + public Ac3Reader(String language) { + headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); + headerScratchBytes = new ParsableByteArray(headerScratchBits.data); + state = STATE_FINDING_SYNC; + this.language = language; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWas0B = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + state = STATE_READING_HEADER; + headerScratchBytes.data[0] = 0x0B; + headerScratchBytes.data[1] = 0x77; + bytesRead = 2; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, HEADER_SIZE); + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + output.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + default: + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next syncword, advancing the position to the byte that immediately follows it. If a + * syncword was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return Whether a syncword position was found. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + if (!lastByteWas0B) { + lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B; + continue; + } + int secondByte = pesBuffer.readUnsignedByte(); + if (secondByte == 0x77) { + lastByteWas0B = false; + return true; + } else { + lastByteWas0B = secondByte == 0x0B; + } + } + return false; + } + + /** + * Parses the sample header. + */ + @SuppressWarnings("ReferenceEquality") + private void parseHeader() { + headerScratchBits.setPosition(0); + SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); + if (format == null || frameInfo.channelCount != format.channelCount + || frameInfo.sampleRate != format.sampleRate + || frameInfo.mimeType != format.sampleMimeType) { + format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, + null, 0, language); + output.format(format); + } + sampleSize = frameInfo.frameSize; + // In this class a sample is an access unit (syncframe in AC-3), but Format#sampleRate + // specifies the number of PCM audio samples per second. + sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java new file mode 100644 index 0000000000..9578d110b7 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** Extracts data from AC-4 bitstreams. */ +public final class Ac4Extractor implements Extractor { + + /** Factory for {@link Ac4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac4Extractor()}; + + /** + * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving + * up. + */ + private static final int MAX_SNIFF_BYTES = 8 * 1024; + + /** + * The size of the reading buffer, in bytes. This value is determined based on the maximum frame + * size used in broadcast applications. + */ + private static final int READ_BUFFER_SIZE = 16384; + + /** The size of the frame header, in bytes. */ + private static final int FRAME_HEADER_SIZE = 7; + + private final Ac4Reader reader; + private final ParsableByteArray sampleData; + + private boolean startedPacket; + + /** Creates a new extractor for AC-4 bitstreams. */ + public Ac4Extractor() { + reader = new Ac4Reader(); + sampleData = new ParsableByteArray(READ_BUFFER_SIZE); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Skip any ID3 headers. + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); + int startPosition = 0; + while (true) { + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); // version, flags + int length = scratch.readSynchSafeInt(); + startPosition += 10 + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(startPosition); + + int headerPosition = startPosition; + int validFramesCount = 0; + while (true) { + input.peekFully(scratch.data, /* offset= */ 0, /* length= */ FRAME_HEADER_SIZE); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (syncBytes != AC40_SYNCWORD && syncBytes != AC41_SYNCWORD) { + validFramesCount = 0; + input.resetPeekPosition(); + if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + input.advancePeekPosition(headerPosition); + } else { + if (++validFramesCount >= 4) { + return true; + } + int frameSize = Ac4Util.parseAc4SyncframeSize(scratch.data, syncBytes); + if (frameSize == C.LENGTH_UNSET) { + return false; + } + input.advancePeekPosition(frameSize - FRAME_HEADER_SIZE); + } + } + } + + @Override + public void init(ExtractorOutput output) { + reader.createTracks( + output, new TrackIdGenerator(/* firstTrackId= */ 0, /* trackIdIncrement= */ 1)); + output.endTracks(); + output.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET)); + } + + @Override + public void seek(long position, long timeUs) { + startedPacket = false; + reader.seek(); + } + + @Override + public void release() { + // Do nothing. + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + int bytesRead = input.read(sampleData.data, /* offset= */ 0, /* length= */ READ_BUFFER_SIZE); + if (bytesRead == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + + // Feed whatever data we have to the reader, regardless of whether the read finished or not. + sampleData.setPosition(0); + sampleData.setLimit(bytesRead); + + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); + startedPacket = true; + } + // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + reader.consume(sampleData); + return RESULT_CONTINUE; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java new file mode 100644 index 0000000000..2b9965b19b --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Parses a continuous AC-4 byte stream and extracts individual samples. */ +public final class Ac4Reader implements ElementaryStreamReader { + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE}) + private @interface State {} + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private final ParsableBitArray headerScratchBits; + private final ParsableByteArray headerScratchBytes; + private final String language; + + private String trackFormatId; + private TrackOutput output; + + @State private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWasAC; + private boolean hasCRC; + + // Used when parsing the header. + private long sampleDurationUs; + private Format format; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** Constructs a new reader for AC-4 elementary streams. */ + public Ac4Reader() { + this(null); + } + + /** + * Constructs a new reader for AC-4 elementary streams. + * + * @param language Track language. + */ + public Ac4Reader(String language) { + headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); + headerScratchBytes = new ParsableByteArray(headerScratchBits.data); + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWasAC = false; + hasCRC = false; + this.language = language; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWasAC = false; + hasCRC = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + state = STATE_READING_HEADER; + headerScratchBytes.data[0] = (byte) 0xAC; + headerScratchBytes.data[1] = (byte) (hasCRC ? 0x41 : 0x40); + bytesRead = 2; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, Ac4Util.HEADER_SIZE_FOR_PARSER)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, Ac4Util.HEADER_SIZE_FOR_PARSER); + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + output.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + default: + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next syncword, advancing the position to the byte that immediately follows it. If a + * syncword was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return Whether a syncword position was found. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + if (!lastByteWasAC) { + lastByteWasAC = (pesBuffer.readUnsignedByte() == 0xAC); + continue; + } + int secondByte = pesBuffer.readUnsignedByte(); + lastByteWasAC = secondByte == 0xAC; + if (secondByte == 0x40 || secondByte == 0x41) { + hasCRC = secondByte == 0x41; + return true; + } + } + return false; + } + + /** Parses the sample header. */ + @SuppressWarnings("ReferenceEquality") + private void parseHeader() { + headerScratchBits.setPosition(0); + SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); + if (format == null + || frameInfo.channelCount != format.channelCount + || frameInfo.sampleRate != format.sampleRate + || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { + format = + Format.createAudioSampleFormat( + trackFormatId, + MimeTypes.AUDIO_AC4, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + frameInfo.channelCount, + frameInfo.sampleRate, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + language); + output.format(format); + } + sampleSize = frameInfo.frameSize; + // In this class a sample is an AC-4 sync frame, but Format#sampleRate specifies the number of + // PCM audio samples per second. + sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java new file mode 100644 index 0000000000..b91abfc75a --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Extracts data from AAC bit streams with ADTS framing. + */ +public final class AdtsExtractor implements Extractor { + + /** Factory for {@link AdtsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + * + * <p>Note that this approach may result in approximated stream duration and seek position that + * are not precise, especially when the stream bitrate varies a lot. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + + private static final int MAX_PACKET_SIZE = 2 * 1024; + /** + * The maximum number of bytes to search when sniffing, excluding the header, before giving up. + * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes. + */ + private static final int MAX_SNIFF_BYTES = 8 * 1024; + /** + * The maximum number of frames to use when calculating the average frame size for constant + * bitrate seeking. + */ + private static final int NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE = 1000; + + private final @Flags int flags; + + private final AdtsReader reader; + private final ParsableByteArray packetBuffer; + private final ParsableByteArray scratch; + private final ParsableBitArray scratchBits; + + @Nullable private ExtractorOutput extractorOutput; + + private long firstSampleTimestampUs; + private long firstFramePosition; + private int averageFrameSize; + private boolean hasCalculatedAverageFrameSize; + private boolean startedPacket; + private boolean hasOutputSeekMap; + + /** Creates a new extractor for ADTS bitstreams. */ + public AdtsExtractor() { + this(/* flags= */ 0); + } + + /** + * Creates a new extractor for ADTS bitstreams. + * + * @param flags Flags that control the extractor's behavior. + */ + public AdtsExtractor(@Flags int flags) { + this.flags = flags; + reader = new AdtsReader(true); + packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); + averageFrameSize = C.LENGTH_UNSET; + firstFramePosition = C.POSITION_UNSET; + // Allocate scratch space for an ID3 header. The same buffer is also used to read 4 byte values. + scratch = new ParsableByteArray(ID3_HEADER_LENGTH); + scratchBits = new ParsableBitArray(scratch.data); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Skip any ID3 headers. + int startPosition = peekId3Header(input); + + // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size. + int headerPosition = startPosition; + int totalValidFramesSize = 0; + int validFramesCount = 0; + while (true) { + input.peekFully(scratch.data, 0, 2); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { + validFramesCount = 0; + totalValidFramesSize = 0; + input.resetPeekPosition(); + if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + input.advancePeekPosition(headerPosition); + } else { + if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) { + return true; + } + + // Skip the frame. + input.peekFully(scratch.data, 0, 4); + scratchBits.setPosition(14); + int frameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (frameSize <= 6) { + return false; + } + input.advancePeekPosition(frameSize - 6); + totalValidFramesSize += frameSize; + } + } + } + + @Override + public void init(ExtractorOutput output) { + this.extractorOutput = output; + reader.createTracks(output, new TrackIdGenerator(0, 1)); + output.endTracks(); + } + + @Override + public void seek(long position, long timeUs) { + startedPacket = false; + reader.seek(); + firstSampleTimestampUs = timeUs; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + boolean canUseConstantBitrateSeeking = + (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET; + if (canUseConstantBitrateSeeking) { + calculateAverageFrameSize(input); + } + + int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE); + boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT; + maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream); + if (readEndOfStream) { + return RESULT_END_OF_INPUT; + } + + // Feed whatever data we have to the reader, regardless of whether the read finished or not. + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesRead); + + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); + startedPacket = true; + } + // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + reader.consume(packetBuffer); + return RESULT_CONTINUE; + } + + private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException { + int firstFramePosition = 0; + while (true) { + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); + int length = scratch.readSynchSafeInt(); + firstFramePosition += ID3_HEADER_LENGTH + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(firstFramePosition); + if (this.firstFramePosition == C.POSITION_UNSET) { + this.firstFramePosition = firstFramePosition; + } + return firstFramePosition; + } + + private void maybeOutputSeekMap( + long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) { + if (hasOutputSeekMap) { + return; + } + boolean useConstantBitrateSeeking = canUseConstantBitrateSeeking && averageFrameSize > 0; + if (useConstantBitrateSeeking + && reader.getSampleDurationUs() == C.TIME_UNSET + && !readEndOfStream) { + // Wait for the sampleDurationUs to be available, or for the end of the stream to be reached, + // before creating seek map. + return; + } + + ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput); + if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) { + extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength)); + } else { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + } + hasOutputSeekMap = true; + } + + private void calculateAverageFrameSize(ExtractorInput input) + throws IOException, InterruptedException { + if (hasCalculatedAverageFrameSize) { + return; + } + averageFrameSize = C.LENGTH_UNSET; + input.resetPeekPosition(); + if (input.getPosition() == 0) { + // Skip any ID3 headers. + peekId3Header(input); + } + + int numValidFrames = 0; + long totalValidFramesSize = 0; + try { + while (input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { + // Invalid sync byte pattern. + // Constant bit-rate seeking will probably fail for this stream. + numValidFrames = 0; + break; + } else { + // Read the frame size. + if (!input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { + break; + } + scratchBits.setPosition(14); + int currentFrameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (currentFrameSize <= 6) { + hasCalculatedAverageFrameSize = true; + throw new ParserException("Malformed ADTS stream"); + } + totalValidFramesSize += currentFrameSize; + if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { + break; + } + if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + break; + } + } + } + } catch (EOFException e) { + // We reached the end of the input during a peekFully() or advancePeekPosition() operation. + // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally + // ExtractorInput would allow these operations to encounter end-of-input without throwing an + // exception [internal: b/145586657]. + } + input.resetPeekPosition(); + if (numValidFrames > 0) { + averageFrameSize = (int) (totalValidFramesSize / numValidFrames); + } else { + averageFrameSize = C.LENGTH_UNSET; + } + hasCalculatedAverageFrameSize = true; + } + + private SeekMap getConstantBitrateSeekMap(long inputLength) { + int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs()); + return new ConstantBitrateSeekMap(inputLength, firstFramePosition, bitrate, averageFrameSize); + } + + /** + * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. + * + * @param frameSize The size of each frame in the stream. + * @param durationUsPerFrame The duration of the given frame in microseconds. + * @return The stream bitrate. + */ + private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) { + return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java new file mode 100644 index 0000000000..f577747ec2 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import android.util.Pair; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Arrays; +import java.util.Collections; + +/** + * Parses a continuous ADTS byte stream and extracts individual frames. + */ +public final class AdtsReader implements ElementaryStreamReader { + + private static final String TAG = "AdtsReader"; + + private static final int STATE_FINDING_SAMPLE = 0; + private static final int STATE_CHECKING_ADTS_HEADER = 1; + private static final int STATE_READING_ID3_HEADER = 2; + private static final int STATE_READING_ADTS_HEADER = 3; + private static final int STATE_READING_SAMPLE = 4; + + private static final int HEADER_SIZE = 5; + private static final int CRC_SIZE = 2; + + // Match states used while looking for the next sample + private static final int MATCH_STATE_VALUE_SHIFT = 8; + private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT; + + private static final int ID3_HEADER_SIZE = 10; + private static final int ID3_SIZE_OFFSET = 6; + private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; + private static final int VERSION_UNSET = -1; + + private final boolean exposeId3; + private final ParsableBitArray adtsScratch; + private final ParsableByteArray id3HeaderBuffer; + private final String language; + + private String formatId; + private TrackOutput output; + private TrackOutput id3Output; + + private int state; + private int bytesRead; + + private int matchState; + + private boolean hasCrc; + private boolean foundFirstFrame; + + // Used to verifies sync words + private int firstFrameVersion; + private int firstFrameSampleRateIndex; + + private int currentFrameVersion; + + // Used when parsing the header. + private boolean hasOutputFormat; + private long sampleDurationUs; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + private TrackOutput currentOutput; + private long currentSampleDuration; + + /** + * @param exposeId3 True if the reader should expose ID3 information. + */ + public AdtsReader(boolean exposeId3) { + this(exposeId3, null); + } + + /** + * @param exposeId3 True if the reader should expose ID3 information. + * @param language Track language. + */ + public AdtsReader(boolean exposeId3, String language) { + adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); + id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); + setFindingSampleState(); + firstFrameVersion = VERSION_UNSET; + firstFrameSampleRateIndex = C.INDEX_UNSET; + sampleDurationUs = C.TIME_UNSET; + this.exposeId3 = exposeId3; + this.language = language; + } + + /** Returns whether an integer matches an ADTS SYNC word. */ + public static boolean isAdtsSyncWord(int candidateSyncWord) { + return (candidateSyncWord & 0xFFF6) == 0xFFF0; + } + + @Override + public void seek() { + resetSync(); + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + if (exposeId3) { + idGenerator.generateNewId(); + id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); + } else { + id3Output = new DummyTrackOutput(); + } + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) throws ParserException { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SAMPLE: + findNextSample(data); + break; + case STATE_READING_ID3_HEADER: + if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) { + parseId3Header(); + } + break; + case STATE_CHECKING_ADTS_HEADER: + checkAdtsHeader(data); + break; + case STATE_READING_ADTS_HEADER: + int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; + if (continueRead(data, adtsScratch.data, targetLength)) { + parseAdtsHeader(); + } + break; + case STATE_READING_SAMPLE: + readSample(data); + break; + default: + throw new IllegalStateException(); + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Returns the duration in microseconds per sample, or {@link C#TIME_UNSET} if the sample duration + * is not available. + */ + public long getSampleDurationUs() { + return sampleDurationUs; + } + + private void resetSync() { + foundFirstFrame = false; + setFindingSampleState(); + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Sets the state to STATE_FINDING_SAMPLE. + */ + private void setFindingSampleState() { + state = STATE_FINDING_SAMPLE; + bytesRead = 0; + matchState = MATCH_STATE_START; + } + + /** + * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for + * {@link #parseId3Header()}. + */ + private void setReadingId3HeaderState() { + state = STATE_READING_ID3_HEADER; + bytesRead = ID3_IDENTIFIER.length; + sampleSize = 0; + id3HeaderBuffer.setPosition(0); + } + + /** + * Sets the state to STATE_READING_SAMPLE. + * + * @param outputToUse TrackOutput object to write the sample to + * @param currentSampleDuration Duration of the sample to be read + * @param priorReadBytes Size of prior read bytes + * @param sampleSize Size of the sample + */ + private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration, + int priorReadBytes, int sampleSize) { + state = STATE_READING_SAMPLE; + bytesRead = priorReadBytes; + this.currentOutput = outputToUse; + this.currentSampleDuration = currentSampleDuration; + this.sampleSize = sampleSize; + } + + /** + * Sets the state to STATE_READING_ADTS_HEADER. + */ + private void setReadingAdtsHeaderState() { + state = STATE_READING_ADTS_HEADER; + bytesRead = 0; + } + + /** Sets the state to STATE_CHECKING_ADTS_HEADER. */ + private void setCheckingAdtsHeaderState() { + state = STATE_CHECKING_ADTS_HEADER; + bytesRead = 0; + } + + /** + * Locates the next sample start, advancing the position to the byte that immediately follows + * identifier. If a sample was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + */ + private void findNextSample(ParsableByteArray pesBuffer) { + byte[] adtsData = pesBuffer.data; + int position = pesBuffer.getPosition(); + int endOffset = pesBuffer.limit(); + while (position < endOffset) { + int data = adtsData[position++] & 0xFF; + if (matchState == MATCH_STATE_FF && isAdtsSyncBytes((byte) 0xFF, (byte) data)) { + if (foundFirstFrame + || checkSyncPositionValid(pesBuffer, /* syncPositionCandidate= */ position - 2)) { + currentFrameVersion = (data & 0x8) >> 3; + hasCrc = (data & 0x1) == 0; + if (!foundFirstFrame) { + setCheckingAdtsHeaderState(); + } else { + setReadingAdtsHeaderState(); + } + pesBuffer.setPosition(position); + return; + } + } + + switch (matchState | data) { + case MATCH_STATE_START | 0xFF: + matchState = MATCH_STATE_FF; + break; + case MATCH_STATE_START | 'I': + matchState = MATCH_STATE_I; + break; + case MATCH_STATE_I | 'D': + matchState = MATCH_STATE_ID; + break; + case MATCH_STATE_ID | '3': + setReadingId3HeaderState(); + pesBuffer.setPosition(position); + return; + default: + if (matchState != MATCH_STATE_START) { + // If matching fails in a later state, revert to MATCH_STATE_START and + // check this byte again + matchState = MATCH_STATE_START; + position--; + } + break; + } + } + pesBuffer.setPosition(position); + } + + /** + * Peeks the Adts header of the current frame and checks if it is valid. If the header is valid, + * transition to {@link #STATE_READING_ADTS_HEADER}; else, transition to {@link + * #STATE_FINDING_SAMPLE}. + */ + private void checkAdtsHeader(ParsableByteArray buffer) { + if (buffer.bytesLeft() == 0) { + // Not enough data to check yet, defer this check. + return; + } + // Peek the next byte of buffer into scratch array. + adtsScratch.data[0] = buffer.data[buffer.getPosition()]; + + adtsScratch.setPosition(2); + int currentFrameSampleRateIndex = adtsScratch.readBits(4); + if (firstFrameSampleRateIndex != C.INDEX_UNSET + && currentFrameSampleRateIndex != firstFrameSampleRateIndex) { + // Invalid header. + resetSync(); + return; + } + + if (!foundFirstFrame) { + foundFirstFrame = true; + firstFrameVersion = currentFrameVersion; + firstFrameSampleRateIndex = currentFrameSampleRateIndex; + } + setReadingAdtsHeaderState(); + } + + /** + * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word. + * The caller must check that the first byte of the SYNC word is 0xFF before calling this method. + * This method performs the following checks: + * + * <ul> + * <li>The MPEG version of this frame must match the previously detected version. + * <li>The sample rate index of this frame must match the previously detected sample rate index. + * <li>The frame size must be at least 7 bytes + * <li>The bytes following the frame must be either another SYNC word with the same MPEG + * version, or the start of an ID3 header. + * </ul> + * + * With the exception of the first check, if there is insufficient data in the buffer then checks + * are optimistically skipped and {@code true} is returned. + * + * @param pesBuffer The buffer containing at data to check. + * @param syncPositionCandidate The candidate SYNC word position. May be -1 if the first byte of + * the candidate was the last byte of the previously consumed buffer. + * @return True if all checks were passed or skipped, indicating the position is likely to be the + * position of a real SYNC word. False otherwise. + */ + private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) { + pesBuffer.setPosition(syncPositionCandidate + 1); + if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + return false; + } + + // The MPEG version of this frame must match the previously detected version. + adtsScratch.setPosition(4); + int currentFrameVersion = adtsScratch.readBits(1); + if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) { + return false; + } + + // The sample rate index of this frame must match the previously detected sample rate index. + if (firstFrameSampleRateIndex != C.INDEX_UNSET) { + if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + // Insufficient data for further checks. + return true; + } + adtsScratch.setPosition(2); + int currentFrameSampleRateIndex = adtsScratch.readBits(4); + if (currentFrameSampleRateIndex != firstFrameSampleRateIndex) { + return false; + } + pesBuffer.setPosition(syncPositionCandidate + 2); + } + + // The frame size must be at least 7 bytes. + if (!tryRead(pesBuffer, adtsScratch.data, 4)) { + // Insufficient data for further checks. + return true; + } + adtsScratch.setPosition(14); + int frameSize = adtsScratch.readBits(13); + if (frameSize < 7) { + return false; + } + + // The bytes following the frame must be either another SYNC word with the same MPEG version, or + // the start of an ID3 header. + byte[] data = pesBuffer.data; + int dataLimit = pesBuffer.limit(); + int nextSyncPosition = syncPositionCandidate + frameSize; + if (nextSyncPosition >= dataLimit) { + // Insufficient data for further checks. + return true; + } + if (data[nextSyncPosition] == (byte) 0xFF) { + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return isAdtsSyncBytes((byte) 0xFF, data[nextSyncPosition + 1]) + && ((data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion; + } else { + if (data[nextSyncPosition] != 'I') { + return false; + } + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + if (data[nextSyncPosition + 1] != 'D') { + return false; + } + if (nextSyncPosition + 2 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return data[nextSyncPosition + 2] == '3'; + } + } + + private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) { + int syncWord = (firstByte & 0xFF) << 8 | (secondByte & 0xFF); + return isAdtsSyncWord(syncWord); + } + + /** Reads {@code targetLength} bytes into target, and returns whether the read succeeded. */ + private boolean tryRead(ParsableByteArray source, byte[] target, int targetLength) { + if (source.bytesLeft() < targetLength) { + return false; + } + source.readBytes(target, /* offset= */ 0, targetLength); + return true; + } + + /** + * Parses the Id3 header. + */ + private void parseId3Header() { + id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); + id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); + setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE, + id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); + } + + /** + * Parses the sample header. + */ + private void parseAdtsHeader() throws ParserException { + adtsScratch.setPosition(0); + + if (!hasOutputFormat) { + int audioObjectType = adtsScratch.readBits(2) + 1; + if (audioObjectType != 2) { + // The stream indicates AAC-Main (1), AAC-SSR (3) or AAC-LTP (4). When the stream indicates + // AAC-Main it's more likely that the stream contains HE-AAC (5), which cannot be + // represented correctly in the 2 bit audio_object_type field in the ADTS header. In + // practice when the stream indicates AAC-SSR or AAC-LTP it more commonly contains AAC-LC or + // HE-AAC. Since most Android devices don't support AAC-Main, AAC-SSR or AAC-LTP, and since + // indicating AAC-LC works for HE-AAC streams, we pretend that we're dealing with AAC-LC and + // hope for the best. In practice this often works. + // See: https://github.com/google/ExoPlayer/issues/774 + // See: https://github.com/google/ExoPlayer/issues/1383 + Log.w(TAG, "Detected audio object type: " + audioObjectType + ", but assuming AAC LC."); + audioObjectType = 2; + } + + adtsScratch.skipBits(5); + int channelConfig = adtsScratch.readBits(3); + + byte[] audioSpecificConfig = + CodecSpecificDataUtil.buildAacAudioSpecificConfig( + audioObjectType, firstFrameSampleRateIndex, channelConfig); + Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( + audioSpecificConfig); + + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, + Collections.singletonList(audioSpecificConfig), null, 0, language); + // In this class a sample is an access unit, but the MediaFormat sample rate specifies the + // number of PCM audio samples per second. + sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; + output.format(format); + hasOutputFormat = true; + } else { + adtsScratch.skipBits(10); + } + + adtsScratch.skipBits(4); + int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; + if (hasCrc) { + sampleSize -= CRC_SIZE; + } + + setReadingSampleState(output, sampleDurationUs, 0, sampleSize); + } + + /** + * Reads the rest of the sample + */ + private void readSample(ParsableByteArray data) { + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + currentOutput.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + currentOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + timeUs += currentSampleDuration; + setFindingSampleState(); + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java new file mode 100644 index 0000000000..cfbc64d2ee --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import android.util.SparseArray; +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.Cea708InitializationData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Default {@link TsPayloadReader.Factory} implementation. + */ +public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { + + /** + * Flags controlling elementary stream readers' behavior. Possible flag values are {@link + * #FLAG_ALLOW_NON_IDR_KEYFRAMES}, {@link #FLAG_IGNORE_AAC_STREAM}, {@link + * #FLAG_IGNORE_H264_STREAM}, {@link #FLAG_DETECT_ACCESS_UNITS}, {@link + * #FLAG_IGNORE_SPLICE_INFO_STREAM}, {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} and {@link + * #FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FLAG_ALLOW_NON_IDR_KEYFRAMES, + FLAG_IGNORE_AAC_STREAM, + FLAG_IGNORE_H264_STREAM, + FLAG_DETECT_ACCESS_UNITS, + FLAG_IGNORE_SPLICE_INFO_STREAM, + FLAG_OVERRIDE_CAPTION_DESCRIPTORS, + FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS + }) + public @interface Flags {} + + /** + * When extracting H.264 samples, whether to treat samples consisting of non-IDR I slices as + * synchronization samples (key-frames). + */ + public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; + /** + * Prevents the creation of {@link AdtsReader} and {@link LatmReader} instances. This flag should + * be enabled if the transport stream contains no packets for an AAC elementary stream that is + * declared in the PMT. + */ + public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; + /** + * Prevents the creation of {@link H264Reader} instances. This flag should be enabled if the + * transport stream contains no packets for an H.264 elementary stream that is declared in the + * PMT. + */ + public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; + /** + * When extracting H.264 samples, whether to split the input stream into access units (samples) + * based on slice headers. This flag should be disabled if the stream contains access unit + * delimiters (AUDs). + */ + public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; + /** Prevents the creation of {@link SpliceInfoSectionReader} instances. */ + public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; + /** + * Whether the list of {@code closedCaptionFormats} passed to {@link + * DefaultTsPayloadReaderFactory#DefaultTsPayloadReaderFactory(int, List)} should be used in spite + * of any closed captions service descriptors. If this flag is disabled, {@code + * closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors. + */ + public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; + /** + * Sets whether HDMV DTS audio streams will be handled. If this flag is set, SCTE subtitles will + * not be detected, as they share the same elementary stream type as HDMV DTS. + */ + public static final int FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS = 1 << 6; + + private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; + + @Flags private final int flags; + private final List<Format> closedCaptionFormats; + + public DefaultTsPayloadReaderFactory() { + this(0); + } + + /** + * @param flags A combination of {@code FLAG_*} values that control the behavior of the created + * readers. + */ + public DefaultTsPayloadReaderFactory(@Flags int flags) { + this( + flags, + Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); + } + + /** + * @param flags A combination of {@code FLAG_*} values that control the behavior of the created + * readers. + * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with + * embedded closed captions when no caption service descriptors are provided. If + * {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides + * any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a + * closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will + * be exposed. + */ + public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) { + this.flags = flags; + this.closedCaptionFormats = closedCaptionFormats; + } + + @Override + public SparseArray<TsPayloadReader> createInitialPayloadReaders() { + return new SparseArray<>(); + } + + @Override + @SuppressWarnings("fallthrough") + public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { + switch (streamType) { + case TsExtractor.TS_STREAM_TYPE_MPA: + case TsExtractor.TS_STREAM_TYPE_MPA_LSF: + return new PesReader(new MpegAudioReader(esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AAC_ADTS: + return isSet(FLAG_IGNORE_AAC_STREAM) + ? null : new PesReader(new AdtsReader(false, esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AAC_LATM: + return isSet(FLAG_IGNORE_AAC_STREAM) + ? null : new PesReader(new LatmReader(esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AC3: + case TsExtractor.TS_STREAM_TYPE_E_AC3: + return new PesReader(new Ac3Reader(esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AC4: + return new PesReader(new Ac4Reader(esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: + if (!isSet(FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)) { + return null; + } + // Fall through. + case TsExtractor.TS_STREAM_TYPE_DTS: + return new PesReader(new DtsReader(esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_H262: + return new PesReader(new H262Reader(buildUserDataReader(esInfo))); + case TsExtractor.TS_STREAM_TYPE_H264: + return isSet(FLAG_IGNORE_H264_STREAM) ? null + : new PesReader(new H264Reader(buildSeiReader(esInfo), + isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); + case TsExtractor.TS_STREAM_TYPE_H265: + return new PesReader(new H265Reader(buildSeiReader(esInfo))); + case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: + return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) + ? null : new SectionReader(new SpliceInfoSectionReader()); + case TsExtractor.TS_STREAM_TYPE_ID3: + return new PesReader(new Id3Reader()); + case TsExtractor.TS_STREAM_TYPE_DVBSUBS: + return new PesReader( + new DvbSubtitleReader(esInfo.dvbSubtitleInfos)); + default: + return null; + } + } + + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor + * is not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link SeiReader} for closed caption tracks. + */ + private SeiReader buildSeiReader(EsInfo esInfo) { + return new SeiReader(getClosedCaptionFormats(esInfo)); + } + + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link UserDataReader} for + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link UserDataReader} for the declared formats, or {@link #closedCaptionFormats} if the + * descriptor is not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link UserDataReader} for closed caption tracks. + */ + private UserDataReader buildUserDataReader(EsInfo esInfo) { + return new UserDataReader(getClosedCaptionFormats(esInfo)); + } + + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link List<Format>} of {@link + * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link + * List<Format>} for the declared formats, or {@link #closedCaptionFormats} if the descriptor is + * not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link List<Format>} containing list of closed caption formats. + */ + private List<Format> getClosedCaptionFormats(EsInfo esInfo) { + if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { + return closedCaptionFormats; + } + ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); + List<Format> closedCaptionFormats = this.closedCaptionFormats; + while (scratchDescriptorData.bytesLeft() > 0) { + int descriptorTag = scratchDescriptorData.readUnsignedByte(); + int descriptorLength = scratchDescriptorData.readUnsignedByte(); + int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength; + if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) { + // Note: see ATSC A/65 for detailed information about the caption service descriptor. + closedCaptionFormats = new ArrayList<>(); + int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F; + for (int i = 0; i < numberOfServices; i++) { + String language = scratchDescriptorData.readString(3); + int captionTypeByte = scratchDescriptorData.readUnsignedByte(); + boolean isDigital = (captionTypeByte & 0x80) != 0; + String mimeType; + int accessibilityChannel; + if (isDigital) { + mimeType = MimeTypes.APPLICATION_CEA708; + accessibilityChannel = captionTypeByte & 0x3F; + } else { + mimeType = MimeTypes.APPLICATION_CEA608; + accessibilityChannel = 1; + } + + // easy_reader(1), wide_aspect_ratio(1), reserved(6). + byte flags = (byte) scratchDescriptorData.readUnsignedByte(); + // Skip reserved (8). + scratchDescriptorData.skipBytes(1); + + List<byte[]> initializationData = null; + // The wide_aspect_ratio flag only has meaning for CEA-708. + if (isDigital) { + boolean isWideAspectRatio = (flags & 0x40) != 0; + initializationData = Cea708InitializationData.buildData(isWideAspectRatio); + } + + closedCaptionFormats.add( + Format.createTextSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language, + accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + initializationData)); + } + } else { + // Unknown descriptor. Ignore. + } + scratchDescriptorData.setPosition(nextDescriptorPosition); + } + + return closedCaptionFormats; + } + + private boolean isSet(@Flags int flag) { + return (flags & flag) != 0; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java new file mode 100644 index 0000000000..a4205add7b --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.DtsUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Parses a continuous DTS byte stream and extracts individual samples. + */ +public final class DtsReader implements ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private static final int HEADER_SIZE = 18; + + private final ParsableByteArray headerScratchBytes; + private final String language; + + private String formatId; + private TrackOutput output; + + private int state; + private int bytesRead; + + // Used to find the header. + private int syncBytes; + + // Used when parsing the header. + private long sampleDurationUs; + private Format format; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** + * Constructs a new reader for DTS elementary streams. + * + * @param language Track language. + */ + public DtsReader(String language) { + headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); + state = STATE_FINDING_SYNC; + this.language = language; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + syncBytes = 0; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + state = STATE_READING_HEADER; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, HEADER_SIZE); + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + output.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + default: + throw new IllegalStateException(); + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next SYNC value in the buffer, advancing the position to the byte that immediately + * follows it. If SYNC was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return Whether SYNC was found. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + syncBytes <<= 8; + syncBytes |= pesBuffer.readUnsignedByte(); + if (DtsUtil.isSyncWord(syncBytes)) { + headerScratchBytes.data[0] = (byte) ((syncBytes >> 24) & 0xFF); + headerScratchBytes.data[1] = (byte) ((syncBytes >> 16) & 0xFF); + headerScratchBytes.data[2] = (byte) ((syncBytes >> 8) & 0xFF); + headerScratchBytes.data[3] = (byte) (syncBytes & 0xFF); + bytesRead = 4; + syncBytes = 0; + return true; + } + } + return false; + } + + /** + * Parses the sample header. + */ + private void parseHeader() { + byte[] frameData = headerScratchBytes.data; + if (format == null) { + format = DtsUtil.parseDtsFormat(frameData, formatId, language, null); + output.format(format); + } + sampleSize = DtsUtil.getDtsFrameSize(frameData); + // In this class a sample is an access unit (frame in DTS), but the format's sample rate + // specifies the number of PCM audio samples per second. + sampleDurationUs = (int) (C.MICROS_PER_SECOND + * DtsUtil.parseDtsAudioSampleCount(frameData) / format.sampleRate); + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java new file mode 100644 index 0000000000..aceab78bf0 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; +import java.util.List; + +/** + * Parses DVB subtitle data and extracts individual frames. + */ +public final class DvbSubtitleReader implements ElementaryStreamReader { + + private final List<DvbSubtitleInfo> subtitleInfos; + private final TrackOutput[] outputs; + + private boolean writingSample; + private int bytesToCheck; + private int sampleBytesWritten; + private long sampleTimeUs; + + /** + * @param subtitleInfos Information about the DVB subtitles associated to the stream. + */ + public DvbSubtitleReader(List<DvbSubtitleInfo> subtitleInfos) { + this.subtitleInfos = subtitleInfos; + outputs = new TrackOutput[subtitleInfos.size()]; + } + + @Override + public void seek() { + writingSample = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i); + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + output.format( + Format.createImageSampleFormat( + idGenerator.getFormatId(), + MimeTypes.APPLICATION_DVBSUBS, + null, + Format.NO_VALUE, + 0, + Collections.singletonList(subtitleInfo.initializationData), + subtitleInfo.language, + null)); + outputs[i] = output; + } + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) { + return; + } + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleBytesWritten = 0; + bytesToCheck = 2; + } + + @Override + public void packetFinished() { + if (writingSample) { + for (TrackOutput output : outputs) { + output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); + } + writingSample = false; + } + } + + @Override + public void consume(ParsableByteArray data) { + if (writingSample) { + if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) { + // Failed to check data_identifier + return; + } + if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) { + // Check and discard the subtitle_stream_id + return; + } + int dataPosition = data.getPosition(); + int bytesAvailable = data.bytesLeft(); + for (TrackOutput output : outputs) { + data.setPosition(dataPosition); + output.sampleData(data, bytesAvailable); + } + sampleBytesWritten += bytesAvailable; + } + } + + private boolean checkNextByte(ParsableByteArray data, int expectedValue) { + if (data.bytesLeft() == 0) { + return false; + } + if (data.readUnsignedByte() != expectedValue) { + writingSample = false; + } + bytesToCheck--; + return writingSample; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java new file mode 100644 index 0000000000..edd33d02c2 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Extracts individual samples from an elementary media stream, preserving original order. + */ +public interface ElementaryStreamReader { + + /** + * Notifies the reader that a seek has occurred. + */ + void seek(); + + /** + * Initializes the reader by providing outputs and ids for the tracks. + * + * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data. + * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the + * {@link TrackOutput}s. + */ + void createTracks(ExtractorOutput extractorOutput, PesReader.TrackIdGenerator idGenerator); + + /** + * Called when a packet starts. + * + * @param pesTimeUs The timestamp associated with the packet. + * @param flags See {@link TsPayloadReader.Flags}. + */ + void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags); + + /** + * Consumes (possibly partial) data from the current packet. + * + * @param data The data to consume. + * @throws ParserException If the data could not be parsed. + */ + void consume(ParsableByteArray data) throws ParserException; + + /** + * Called when a packet ends. + */ + void packetFinished(); + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java new file mode 100644 index 0000000000..576607366e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import android.util.Pair; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Arrays; +import java.util.Collections; + +/** + * Parses a continuous H262 byte stream and extracts individual frames. + */ +public final class H262Reader implements ElementaryStreamReader { + + private static final int START_PICTURE = 0x00; + private static final int START_SEQUENCE_HEADER = 0xB3; + private static final int START_EXTENSION = 0xB5; + private static final int START_GROUP = 0xB8; + private static final int START_USER_DATA = 0xB2; + + private String formatId; + private TrackOutput output; + + // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. + private static final double[] FRAME_RATE_VALUES = new double[] { + 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + private long frameDurationUs; + + private final UserDataReader userDataReader; + private final ParsableByteArray userDataParsable; + + // State that should be reset on seek. + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + private final NalUnitTargetBuffer userData; + private long totalBytesWritten; + private boolean startedFirstSample; + + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + + // Per sample state that gets reset at the start of each sample. + private long samplePosition; + private long sampleTimeUs; + private boolean sampleIsKeyframe; + private boolean sampleHasPicture; + + public H262Reader() { + this(null); + } + + /* package */ H262Reader(UserDataReader userDataReader) { + this.userDataReader = userDataReader; + prefixFlags = new boolean[4]; + csdBuffer = new CsdBuffer(128); + if (userDataReader != null) { + userData = new NalUnitTargetBuffer(START_USER_DATA, 128); + userDataParsable = new ParsableByteArray(); + } else { + userData = null; + userDataParsable = null; + } + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + csdBuffer.reset(); + if (userDataReader != null) { + userData.reset(); + } + totalBytesWritten = 0; + startedFirstSample = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + if (userDataReader != null) { + userDataReader.createTracks(extractorOutput, idGenerator); + } + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + while (true) { + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + + if (startCodeOffset == limit) { + // We've scanned to the end of the data without finding another start code. + if (!hasOutputFormat) { + csdBuffer.onData(dataArray, offset, limit); + } + if (userDataReader != null) { + userData.appendToNalUnit(dataArray, offset, limit); + } + return; + } + + // We've found a start code with the following value. + int startCodeValue = data.data[startCodeOffset + 3] & 0xFF; + // This is the number of bytes from the current offset to the start of the next start + // code. It may be negative if the start code started in the previously consumed data. + int lengthToStartCode = startCodeOffset - offset; + + if (!hasOutputFormat) { + if (lengthToStartCode > 0) { + csdBuffer.onData(dataArray, offset, startCodeOffset); + } + // This is the number of bytes belonging to the next start code that have already been + // passed to csdBuffer. + int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; + if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { + // The csd data is complete, so we can decode and output the media format. + Pair<Format, Long> result = parseCsdBuffer(csdBuffer, formatId); + output.format(result.first); + frameDurationUs = result.second; + hasOutputFormat = true; + } + } + if (userDataReader != null) { + int bytesAlreadyPassed = 0; + if (lengthToStartCode > 0) { + userData.appendToNalUnit(dataArray, offset, startCodeOffset); + } else { + bytesAlreadyPassed = -lengthToStartCode; + } + + if (userData.endNalUnit(bytesAlreadyPassed)) { + int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); + userDataParsable.reset(userData.nalData, unescapedLength); + userDataReader.consume(sampleTimeUs, userDataParsable); + } + + if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { + userData.startNalUnit(startCodeValue); + } + } + if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) { + int bytesWrittenPastStartCode = limit - startCodeOffset; + if (startedFirstSample && sampleHasPicture && hasOutputFormat) { + // Output the sample. + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; + output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null); + } + if (!startedFirstSample || sampleHasPicture) { + // Start the next sample. + samplePosition = totalBytesWritten - bytesWrittenPastStartCode; + sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs + : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0); + sampleIsKeyframe = false; + pesTimeUs = C.TIME_UNSET; + startedFirstSample = true; + } + sampleHasPicture = startCodeValue == START_PICTURE; + } else if (startCodeValue == START_GROUP) { + sampleIsKeyframe = true; + } + + offset = startCodeOffset + 3; + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses the {@link Format} and frame duration from a csd buffer. + * + * @param csdBuffer The csd buffer. + * @param formatId The id for the generated format. May be null. + * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or + * 0 if the duration could not be determined. + */ + private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { + byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); + + int firstByte = csdData[4] & 0xFF; + int secondByte = csdData[5] & 0xFF; + int thirdByte = csdData[6] & 0xFF; + int width = (firstByte << 4) | (secondByte >> 4); + int height = (secondByte & 0x0F) << 8 | thirdByte; + + float pixelWidthHeightRatio = 1f; + int aspectRatioCode = (csdData[7] & 0xF0) >> 4; + switch(aspectRatioCode) { + case 2: + pixelWidthHeightRatio = (4 * height) / (float) (3 * width); + break; + case 3: + pixelWidthHeightRatio = (16 * height) / (float) (9 * width); + break; + case 4: + pixelWidthHeightRatio = (121 * height) / (float) (100 * width); + break; + default: + // Do nothing. + break; + } + + Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null, + Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, + Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null); + + long frameDurationUs = 0; + int frameRateCodeMinusOne = (csdData[7] & 0x0F) - 1; + if (0 <= frameRateCodeMinusOne && frameRateCodeMinusOne < FRAME_RATE_VALUES.length) { + double frameRate = FRAME_RATE_VALUES[frameRateCodeMinusOne]; + int sequenceExtensionPosition = csdBuffer.sequenceExtensionPosition; + int frameRateExtensionN = (csdData[sequenceExtensionPosition + 9] & 0x60) >> 5; + int frameRateExtensionD = (csdData[sequenceExtensionPosition + 9] & 0x1F); + if (frameRateExtensionN != frameRateExtensionD) { + frameRate *= (frameRateExtensionN + 1d) / (frameRateExtensionD + 1); + } + frameDurationUs = (long) (C.MICROS_PER_SECOND / frameRate); + } + + return Pair.create(format, frameDurationUs); + } + + private static final class CsdBuffer { + + private static final byte[] START_CODE = new byte[] {0, 0, 1}; + + private boolean isFilling; + + public int length; + public int sequenceExtensionPosition; + public byte[] data; + + public CsdBuffer(int initialCapacity) { + data = new byte[initialCapacity]; + } + + /** + * Resets the buffer, clearing any data that it holds. + */ + public void reset() { + isFilling = false; + length = 0; + sequenceExtensionPosition = 0; + } + + /** + * Called when a start code is encountered in the stream. + * + * @param startCodeValue The start code value. + * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to + * {@link #onData(byte[], int, int)}, or 0. + * @return Whether the csd data is now complete. If true is returned, neither + * this method nor {@link #onData(byte[], int, int)} should be called again without an + * interleaving call to {@link #reset()}. + */ + public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { + if (isFilling) { + length -= bytesAlreadyPassed; + if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { + sequenceExtensionPosition = length; + } else { + isFilling = false; + return true; + } + } else if (startCodeValue == START_SEQUENCE_HEADER) { + isFilling = true; + } + onData(START_CODE, 0, START_CODE.length); + return false; + } + + /** + * Called to pass stream data. + * + * @param newData Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void onData(byte[] newData, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (data.length < length + readLength) { + data = Arrays.copyOf(data, (length + readLength) * 2); + } + System.arraycopy(newData, offset, data, length, readLength); + length += readLength; + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java new file mode 100644 index 0000000000..164c115159 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR; + +import android.util.SparseArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil.SpsData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Parses a continuous H264 byte stream and extracts individual frames. + */ +public final class H264Reader implements ElementaryStreamReader { + + private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + + private final SeiReader seiReader; + private final boolean allowNonIdrKeyframes; + private final boolean detectAccessUnits; + private final NalUnitTargetBuffer sps; + private final NalUnitTargetBuffer pps; + private final NalUnitTargetBuffer sei; + private long totalBytesWritten; + private final boolean[] prefixFlags; + + private String formatId; + private TrackOutput output; + private SampleReader sampleReader; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // Per PES packet state that gets reset at the start of each PES packet. + private long pesTimeUs; + + // State inherited from the TS packet header. + private boolean randomAccessIndicator; + + // Scratch variables to avoid allocations. + private final ParsableByteArray seiWrapper; + + /** + * @param seiReader An SEI reader for consuming closed caption channels. + * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as + * synchronization samples (key-frames). + * @param detectAccessUnits Whether to split the input stream into access units (samples) based on + * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). + */ + public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) { + this.seiReader = seiReader; + this.allowNonIdrKeyframes = allowNonIdrKeyframes; + this.detectAccessUnits = detectAccessUnits; + prefixFlags = new boolean[3]; + sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); + pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); + sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); + seiWrapper = new ParsableByteArray(); + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + sps.reset(); + pps.reset(); + sei.reset(); + sampleReader.reset(); + totalBytesWritten = 0; + randomAccessIndicator = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); + seiReader.createTracks(extractorOutput, idGenerator); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + this.pesTimeUs = pesTimeUs; + randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0; + } + + @Override + public void consume(ParsableByteArray data) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + // Scan the appended data, processing NAL units as they are encountered + while (true) { + int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + + if (nalUnitOffset == limit) { + // We've scanned to the end of the data without finding the start of another NAL unit. + nalUnitData(dataArray, offset, limit); + return; + } + + // We've seen the start of a NAL unit of the following type. + int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset); + + // This is the number of bytes from the current offset to the start of the next NAL unit. + // It may be negative if the NAL unit started in the previously consumed data. + int lengthToNalUnit = nalUnitOffset - offset; + if (lengthToNalUnit > 0) { + nalUnitData(dataArray, offset, nalUnitOffset); + } + int bytesWrittenPastPosition = limit - nalUnitOffset; + long absolutePosition = totalBytesWritten - bytesWrittenPastPosition; + // Indicate the end of the previous NAL unit. If the length to the start of the next unit + // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes + // when notifying that the unit has ended. + endNalUnit(absolutePosition, bytesWrittenPastPosition, + lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs); + // Indicate the start of the next NAL unit. + startNalUnit(absolutePosition, nalUnitType, pesTimeUs); + // Continue scanning the data. + offset = nalUnitOffset + 3; + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + private void startNalUnit(long position, int nalUnitType, long pesTimeUs) { + if (!hasOutputFormat || sampleReader.needsSpsPps()) { + sps.startNalUnit(nalUnitType); + pps.startNalUnit(nalUnitType); + } + sei.startNalUnit(nalUnitType); + sampleReader.startNalUnit(position, nalUnitType, pesTimeUs); + } + + private void nalUnitData(byte[] dataArray, int offset, int limit) { + if (!hasOutputFormat || sampleReader.needsSpsPps()) { + sps.appendToNalUnit(dataArray, offset, limit); + pps.appendToNalUnit(dataArray, offset, limit); + } + sei.appendToNalUnit(dataArray, offset, limit); + sampleReader.appendToNalUnit(dataArray, offset, limit); + } + + private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { + if (!hasOutputFormat || sampleReader.needsSpsPps()) { + sps.endNalUnit(discardPadding); + pps.endNalUnit(discardPadding); + if (!hasOutputFormat) { + if (sps.isCompleted() && pps.isCompleted()) { + List<byte[]> initializationData = new ArrayList<>(); + initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength)); + initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); + NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); + NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); + output.format( + Format.createVideoSampleFormat( + formatId, + MimeTypes.VIDEO_H264, + CodecSpecificDataUtil.buildAvcCodecString( + spsData.profileIdc, + spsData.constraintsFlagsAndReservedZero2Bits, + spsData.levelIdc), + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + spsData.width, + spsData.height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + /* rotationDegrees= */ Format.NO_VALUE, + spsData.pixelWidthAspectRatio, + /* drmInitData= */ null)); + hasOutputFormat = true; + sampleReader.putSps(spsData); + sampleReader.putPps(ppsData); + sps.reset(); + pps.reset(); + } + } else if (sps.isCompleted()) { + NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); + sampleReader.putSps(spsData); + sps.reset(); + } else if (pps.isCompleted()) { + NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); + sampleReader.putPps(ppsData); + pps.reset(); + } + } + if (sei.endNalUnit(discardPadding)) { + int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength); + seiWrapper.reset(sei.nalData, unescapedLength); + seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. + seiReader.consume(pesTimeUs, seiWrapper); + } + boolean sampleIsKeyFrame = + sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator); + if (sampleIsKeyFrame) { + // This is either an IDR frame or the first I-frame since the random access indicator, so mark + // it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as + // keyframes until we see another random access indicator. + randomAccessIndicator = false; + } + } + + /** Consumes a stream of NAL units and outputs samples. */ + private static final class SampleReader { + + private static final int DEFAULT_BUFFER_SIZE = 128; + + private static final int NAL_UNIT_TYPE_NON_IDR = 1; // Coded slice of a non-IDR picture + private static final int NAL_UNIT_TYPE_PARTITION_A = 2; // Coded slice data partition A + private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture + private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter + + private final TrackOutput output; + private final boolean allowNonIdrKeyframes; + private final boolean detectAccessUnits; + private final SparseArray<NalUnitUtil.SpsData> sps; + private final SparseArray<NalUnitUtil.PpsData> pps; + private final ParsableNalUnitBitArray bitArray; + + private byte[] buffer; + private int bufferLength; + + // Per NAL unit state. A sample consists of one or more NAL units. + private int nalUnitType; + private long nalUnitStartPosition; + private boolean isFilling; + private long nalUnitTimeUs; + private SliceHeaderData previousSliceHeader; + private SliceHeaderData sliceHeader; + + // Per sample state that gets reset at the start of each sample. + private boolean readingSample; + private long samplePosition; + private long sampleTimeUs; + private boolean sampleIsKeyframe; + + public SampleReader(TrackOutput output, boolean allowNonIdrKeyframes, + boolean detectAccessUnits) { + this.output = output; + this.allowNonIdrKeyframes = allowNonIdrKeyframes; + this.detectAccessUnits = detectAccessUnits; + sps = new SparseArray<>(); + pps = new SparseArray<>(); + previousSliceHeader = new SliceHeaderData(); + sliceHeader = new SliceHeaderData(); + buffer = new byte[DEFAULT_BUFFER_SIZE]; + bitArray = new ParsableNalUnitBitArray(buffer, 0, 0); + reset(); + } + + public boolean needsSpsPps() { + return detectAccessUnits; + } + + public void putSps(NalUnitUtil.SpsData spsData) { + sps.append(spsData.seqParameterSetId, spsData); + } + + public void putPps(NalUnitUtil.PpsData ppsData) { + pps.append(ppsData.picParameterSetId, ppsData); + } + + public void reset() { + isFilling = false; + readingSample = false; + sliceHeader.clear(); + } + + public void startNalUnit(long position, int type, long pesTimeUs) { + nalUnitType = type; + nalUnitTimeUs = pesTimeUs; + nalUnitStartPosition = position; + if ((allowNonIdrKeyframes && nalUnitType == NAL_UNIT_TYPE_NON_IDR) + || (detectAccessUnits && (nalUnitType == NAL_UNIT_TYPE_IDR + || nalUnitType == NAL_UNIT_TYPE_NON_IDR + || nalUnitType == NAL_UNIT_TYPE_PARTITION_A))) { + // Store the previous header and prepare to populate the new one. + SliceHeaderData newSliceHeader = previousSliceHeader; + previousSliceHeader = sliceHeader; + sliceHeader = newSliceHeader; + sliceHeader.clear(); + bufferLength = 0; + isFilling = true; + } + } + + /** + * Called to pass stream data. The data passed should not include the 3 byte start code. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (buffer.length < bufferLength + readLength) { + buffer = Arrays.copyOf(buffer, (bufferLength + readLength) * 2); + } + System.arraycopy(data, offset, buffer, bufferLength, readLength); + bufferLength += readLength; + + bitArray.reset(buffer, 0, bufferLength); + if (!bitArray.canReadBits(8)) { + return; + } + bitArray.skipBit(); // forbidden_zero_bit + int nalRefIdc = bitArray.readBits(2); + bitArray.skipBits(5); // nal_unit_type + + // Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013) + // subsection 7.3.3. + if (!bitArray.canReadExpGolombCodedNum()) { + return; + } + bitArray.readUnsignedExpGolombCodedInt(); // first_mb_in_slice + if (!bitArray.canReadExpGolombCodedNum()) { + return; + } + int sliceType = bitArray.readUnsignedExpGolombCodedInt(); + if (!detectAccessUnits) { + // There are AUDs in the stream so the rest of the header can be ignored. + isFilling = false; + sliceHeader.setSliceType(sliceType); + return; + } + if (!bitArray.canReadExpGolombCodedNum()) { + return; + } + int picParameterSetId = bitArray.readUnsignedExpGolombCodedInt(); + if (pps.indexOfKey(picParameterSetId) < 0) { + // We have not seen the PPS yet, so don't try to decode the slice header. + isFilling = false; + return; + } + NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId); + NalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId); + if (spsData.separateColorPlaneFlag) { + if (!bitArray.canReadBits(2)) { + return; + } + bitArray.skipBits(2); // colour_plane_id + } + if (!bitArray.canReadBits(spsData.frameNumLength)) { + return; + } + boolean fieldPicFlag = false; + boolean bottomFieldFlagPresent = false; + boolean bottomFieldFlag = false; + int frameNum = bitArray.readBits(spsData.frameNumLength); + if (!spsData.frameMbsOnlyFlag) { + if (!bitArray.canReadBits(1)) { + return; + } + fieldPicFlag = bitArray.readBit(); + if (fieldPicFlag) { + if (!bitArray.canReadBits(1)) { + return; + } + bottomFieldFlag = bitArray.readBit(); + bottomFieldFlagPresent = true; + } + } + boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR; + int idrPicId = 0; + if (idrPicFlag) { + if (!bitArray.canReadExpGolombCodedNum()) { + return; + } + idrPicId = bitArray.readUnsignedExpGolombCodedInt(); + } + int picOrderCntLsb = 0; + int deltaPicOrderCntBottom = 0; + int deltaPicOrderCnt0 = 0; + int deltaPicOrderCnt1 = 0; + if (spsData.picOrderCountType == 0) { + if (!bitArray.canReadBits(spsData.picOrderCntLsbLength)) { + return; + } + picOrderCntLsb = bitArray.readBits(spsData.picOrderCntLsbLength); + if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) { + if (!bitArray.canReadExpGolombCodedNum()) { + return; + } + deltaPicOrderCntBottom = bitArray.readSignedExpGolombCodedInt(); + } + } else if (spsData.picOrderCountType == 1 + && !spsData.deltaPicOrderAlwaysZeroFlag) { + if (!bitArray.canReadExpGolombCodedNum()) { + return; + } + deltaPicOrderCnt0 = bitArray.readSignedExpGolombCodedInt(); + if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) { + if (!bitArray.canReadExpGolombCodedNum()) { + return; + } + deltaPicOrderCnt1 = bitArray.readSignedExpGolombCodedInt(); + } + } + sliceHeader.setAll(spsData, nalRefIdc, sliceType, frameNum, picParameterSetId, fieldPicFlag, + bottomFieldFlagPresent, bottomFieldFlag, idrPicFlag, idrPicId, picOrderCntLsb, + deltaPicOrderCntBottom, deltaPicOrderCnt0, deltaPicOrderCnt1); + isFilling = false; + } + + public boolean endNalUnit( + long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) { + if (nalUnitType == NAL_UNIT_TYPE_AUD + || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) { + // If the NAL unit ending is the start of a new sample, output the previous one. + if (hasOutputFormat && readingSample) { + int nalUnitLength = (int) (position - nalUnitStartPosition); + outputSample(offset + nalUnitLength); + } + samplePosition = nalUnitStartPosition; + sampleTimeUs = nalUnitTimeUs; + sampleIsKeyframe = false; + readingSample = true; + } + boolean treatIFrameAsKeyframe = + allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator; + sampleIsKeyframe |= + nalUnitType == NAL_UNIT_TYPE_IDR + || (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR); + return sampleIsKeyframe; + } + + private void outputSample(int offset) { + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + int size = (int) (nalUnitStartPosition - samplePosition); + output.sampleMetadata(sampleTimeUs, flags, size, offset, null); + } + + private static final class SliceHeaderData { + + private static final int SLICE_TYPE_I = 2; + private static final int SLICE_TYPE_ALL_I = 7; + + private boolean isComplete; + private boolean hasSliceType; + + private SpsData spsData; + private int nalRefIdc; + private int sliceType; + private int frameNum; + private int picParameterSetId; + private boolean fieldPicFlag; + private boolean bottomFieldFlagPresent; + private boolean bottomFieldFlag; + private boolean idrPicFlag; + private int idrPicId; + private int picOrderCntLsb; + private int deltaPicOrderCntBottom; + private int deltaPicOrderCnt0; + private int deltaPicOrderCnt1; + + public void clear() { + hasSliceType = false; + isComplete = false; + } + + public void setSliceType(int sliceType) { + this.sliceType = sliceType; + hasSliceType = true; + } + + public void setAll( + SpsData spsData, + int nalRefIdc, + int sliceType, + int frameNum, + int picParameterSetId, + boolean fieldPicFlag, + boolean bottomFieldFlagPresent, + boolean bottomFieldFlag, + boolean idrPicFlag, + int idrPicId, + int picOrderCntLsb, + int deltaPicOrderCntBottom, + int deltaPicOrderCnt0, + int deltaPicOrderCnt1) { + this.spsData = spsData; + this.nalRefIdc = nalRefIdc; + this.sliceType = sliceType; + this.frameNum = frameNum; + this.picParameterSetId = picParameterSetId; + this.fieldPicFlag = fieldPicFlag; + this.bottomFieldFlagPresent = bottomFieldFlagPresent; + this.bottomFieldFlag = bottomFieldFlag; + this.idrPicFlag = idrPicFlag; + this.idrPicId = idrPicId; + this.picOrderCntLsb = picOrderCntLsb; + this.deltaPicOrderCntBottom = deltaPicOrderCntBottom; + this.deltaPicOrderCnt0 = deltaPicOrderCnt0; + this.deltaPicOrderCnt1 = deltaPicOrderCnt1; + isComplete = true; + hasSliceType = true; + } + + public boolean isISlice() { + return hasSliceType && (sliceType == SLICE_TYPE_ALL_I || sliceType == SLICE_TYPE_I); + } + + private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { + // See ISO 14496-10 subsection 7.4.1.2.4. + return isComplete + && (!other.isComplete + || frameNum != other.frameNum + || picParameterSetId != other.picParameterSetId + || fieldPicFlag != other.fieldPicFlag + || (bottomFieldFlagPresent + && other.bottomFieldFlagPresent + && bottomFieldFlag != other.bottomFieldFlag) + || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) + || (spsData.picOrderCountType == 0 + && other.spsData.picOrderCountType == 0 + && (picOrderCntLsb != other.picOrderCntLsb + || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) + || (spsData.picOrderCountType == 1 + && other.spsData.picOrderCountType == 1 + && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 + || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) + || idrPicFlag != other.idrPicFlag + || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + } + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java new file mode 100644 index 0000000000..6aa7c5d71d --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import java.util.Collections; + +/** + * Parses a continuous H.265 byte stream and extracts individual frames. + */ +public final class H265Reader implements ElementaryStreamReader { + + private static final String TAG = "H265Reader"; + + // nal_unit_type values from H.265/HEVC (2014) Table 7-1. + private static final int RASL_R = 9; + private static final int BLA_W_LP = 16; + private static final int CRA_NUT = 21; + private static final int VPS_NUT = 32; + private static final int SPS_NUT = 33; + private static final int PPS_NUT = 34; + private static final int PREFIX_SEI_NUT = 39; + private static final int SUFFIX_SEI_NUT = 40; + + private final SeiReader seiReader; + + private String formatId; + private TrackOutput output; + private SampleReader sampleReader; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // State that should be reset on seek. + private final boolean[] prefixFlags; + private final NalUnitTargetBuffer vps; + private final NalUnitTargetBuffer sps; + private final NalUnitTargetBuffer pps; + private final NalUnitTargetBuffer prefixSei; + private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? + private long totalBytesWritten; + + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + + // Scratch variables to avoid allocations. + private final ParsableByteArray seiWrapper; + + /** + * @param seiReader An SEI reader for consuming closed caption channels. + */ + public H265Reader(SeiReader seiReader) { + this.seiReader = seiReader; + prefixFlags = new boolean[3]; + vps = new NalUnitTargetBuffer(VPS_NUT, 128); + sps = new NalUnitTargetBuffer(SPS_NUT, 128); + pps = new NalUnitTargetBuffer(PPS_NUT, 128); + prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128); + suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128); + seiWrapper = new ParsableByteArray(); + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + vps.reset(); + sps.reset(); + pps.reset(); + prefixSei.reset(); + suffixSei.reset(); + sampleReader.reset(); + totalBytesWritten = 0; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + sampleReader = new SampleReader(output); + seiReader.createTracks(extractorOutput, idGenerator); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + // Scan the appended data, processing NAL units as they are encountered + while (offset < limit) { + int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + + if (nalUnitOffset == limit) { + // We've scanned to the end of the data without finding the start of another NAL unit. + nalUnitData(dataArray, offset, limit); + return; + } + + // We've seen the start of a NAL unit of the following type. + int nalUnitType = NalUnitUtil.getH265NalUnitType(dataArray, nalUnitOffset); + + // This is the number of bytes from the current offset to the start of the next NAL unit. + // It may be negative if the NAL unit started in the previously consumed data. + int lengthToNalUnit = nalUnitOffset - offset; + if (lengthToNalUnit > 0) { + nalUnitData(dataArray, offset, nalUnitOffset); + } + + int bytesWrittenPastPosition = limit - nalUnitOffset; + long absolutePosition = totalBytesWritten - bytesWrittenPastPosition; + // Indicate the end of the previous NAL unit. If the length to the start of the next unit + // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes + // when notifying that the unit has ended. + endNalUnit(absolutePosition, bytesWrittenPastPosition, + lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs); + // Indicate the start of the next NAL unit. + startNalUnit(absolutePosition, bytesWrittenPastPosition, nalUnitType, pesTimeUs); + // Continue scanning the data. + offset = nalUnitOffset + 3; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { + if (hasOutputFormat) { + sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); + } else { + vps.startNalUnit(nalUnitType); + sps.startNalUnit(nalUnitType); + pps.startNalUnit(nalUnitType); + } + prefixSei.startNalUnit(nalUnitType); + suffixSei.startNalUnit(nalUnitType); + } + + private void nalUnitData(byte[] dataArray, int offset, int limit) { + if (hasOutputFormat) { + sampleReader.readNalUnitData(dataArray, offset, limit); + } else { + vps.appendToNalUnit(dataArray, offset, limit); + sps.appendToNalUnit(dataArray, offset, limit); + pps.appendToNalUnit(dataArray, offset, limit); + } + prefixSei.appendToNalUnit(dataArray, offset, limit); + suffixSei.appendToNalUnit(dataArray, offset, limit); + } + + private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { + if (hasOutputFormat) { + sampleReader.endNalUnit(position, offset); + } else { + vps.endNalUnit(discardPadding); + sps.endNalUnit(discardPadding); + pps.endNalUnit(discardPadding); + if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) { + output.format(parseMediaFormat(formatId, vps, sps, pps)); + hasOutputFormat = true; + } + } + if (prefixSei.endNalUnit(discardPadding)) { + int unescapedLength = NalUnitUtil.unescapeStream(prefixSei.nalData, prefixSei.nalLength); + seiWrapper.reset(prefixSei.nalData, unescapedLength); + + // Skip the NAL prefix and type. + seiWrapper.skipBytes(5); + seiReader.consume(pesTimeUs, seiWrapper); + } + if (suffixSei.endNalUnit(discardPadding)) { + int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength); + seiWrapper.reset(suffixSei.nalData, unescapedLength); + + // Skip the NAL prefix and type. + seiWrapper.skipBytes(5); + seiReader.consume(pesTimeUs, seiWrapper); + } + } + + private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { + // Build codec-specific data. + byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; + System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); + System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength); + System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength); + + // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1. + ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength); + bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id + int maxSubLayersMinus1 = bitArray.readBits(3); + bitArray.skipBit(); // sps_temporal_id_nesting_flag + + // profile_tier_level(1, sps_max_sub_layers_minus1) + bitArray.skipBits(88); // if (profilePresentFlag) {...} + bitArray.skipBits(8); // general_level_idc + int toSkip = 0; + for (int i = 0; i < maxSubLayersMinus1; i++) { + if (bitArray.readBit()) { // sub_layer_profile_present_flag[i] + toSkip += 89; + } + if (bitArray.readBit()) { // sub_layer_level_present_flag[i] + toSkip += 8; + } + } + bitArray.skipBits(toSkip); + if (maxSubLayersMinus1 > 0) { + bitArray.skipBits(2 * (8 - maxSubLayersMinus1)); + } + + bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id + int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); + if (chromaFormatIdc == 3) { + bitArray.skipBit(); // separate_colour_plane_flag + } + int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); + int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); + if (bitArray.readBit()) { // conformance_window_flag + int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); + // H.265/HEVC (2014) Table 6-1 + int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1; + int subHeightC = chromaFormatIdc == 1 ? 2 : 1; + picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset); + picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset); + } + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 + int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt(); + // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...) + for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i] + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i] + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i] + } + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size + bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter + bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra + // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}} + boolean scalingListEnabled = bitArray.readBit(); + if (scalingListEnabled && bitArray.readBit()) { + skipScalingList(bitArray); + } + bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1) + if (bitArray.readBit()) { // pcm_enabled_flag + // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4) + bitArray.skipBits(8); + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size + bitArray.skipBit(); // pcm_loop_filter_disabled_flag + } + // Skips all short term reference picture sets. + skipShortTermRefPicSets(bitArray); + if (bitArray.readBit()) { // long_term_ref_pics_present_flag + // num_long_term_ref_pics_sps + for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) { + int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4; + // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i] + bitArray.skipBits(ltRefPicPocLsbSpsLength + 1); + } + } + bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag + float pixelWidthHeightRatio = 1; + if (bitArray.readBit()) { // vui_parameters_present_flag + if (bitArray.readBit()) { // aspect_ratio_info_present_flag + int aspectRatioIdc = bitArray.readBits(8); + if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) { + int sarWidth = bitArray.readBits(16); + int sarHeight = bitArray.readBits(16); + if (sarWidth != 0 && sarHeight != 0) { + pixelWidthHeightRatio = (float) sarWidth / sarHeight; + } + } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) { + pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc]; + } else { + Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc); + } + } + } + + return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, + Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE, + Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null); + } + + /** + * Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4. + */ + private static void skipScalingList(ParsableNalUnitBitArray bitArray) { + for (int sizeId = 0; sizeId < 4; sizeId++) { + for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) { + if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId] + // scaling_list_pred_matrix_id_delta[sizeId][matrixId] + bitArray.readUnsignedExpGolombCodedInt(); + } else { + int coefNum = Math.min(64, 1 << (4 + (sizeId << 1))); + if (sizeId > 1) { + // scaling_list_dc_coef_minus8[sizeId - 2][matrixId] + bitArray.readSignedExpGolombCodedInt(); + } + for (int i = 0; i < coefNum; i++) { + bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef + } + } + } + } + } + + /** + * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of + * them. See H.265/HEVC (2014) 7.3.7. + */ + private static void skipShortTermRefPicSets(ParsableNalUnitBitArray bitArray) { + int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt(); + boolean interRefPicSetPredictionFlag = false; + int numNegativePics; + int numPositivePics; + // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous + // one, so we just keep track of that rather than storing the whole array. + // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS. + int previousNumDeltaPocs = 0; + for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) { + if (stRpsIdx != 0) { + interRefPicSetPredictionFlag = bitArray.readBit(); + } + if (interRefPicSetPredictionFlag) { + bitArray.skipBit(); // delta_rps_sign + bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1 + for (int j = 0; j <= previousNumDeltaPocs; j++) { + if (bitArray.readBit()) { // used_by_curr_pic_flag[j] + bitArray.skipBit(); // use_delta_flag[j] + } + } + } else { + numNegativePics = bitArray.readUnsignedExpGolombCodedInt(); + numPositivePics = bitArray.readUnsignedExpGolombCodedInt(); + previousNumDeltaPocs = numNegativePics + numPositivePics; + for (int i = 0; i < numNegativePics; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i] + bitArray.skipBit(); // used_by_curr_pic_s0_flag[i] + } + for (int i = 0; i < numPositivePics; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i] + bitArray.skipBit(); // used_by_curr_pic_s1_flag[i] + } + } + } + } + + private static final class SampleReader { + + /** + * Offset in bytes of the first_slice_segment_in_pic_flag in a NAL unit containing a + * slice_segment_layer_rbsp. + */ + private static final int FIRST_SLICE_FLAG_OFFSET = 2; + + private final TrackOutput output; + + // Per NAL unit state. A sample consists of one or more NAL units. + private long nalUnitStartPosition; + private boolean nalUnitHasKeyframeData; + private int nalUnitBytesRead; + private long nalUnitTimeUs; + private boolean lookingForFirstSliceFlag; + private boolean isFirstSlice; + private boolean isFirstParameterSet; + + // Per sample state that gets reset at the start of each sample. + private boolean readingSample; + private boolean writingParameterSets; + private long samplePosition; + private long sampleTimeUs; + private boolean sampleIsKeyframe; + + public SampleReader(TrackOutput output) { + this.output = output; + } + + public void reset() { + lookingForFirstSliceFlag = false; + isFirstSlice = false; + isFirstParameterSet = false; + readingSample = false; + writingParameterSets = false; + } + + public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { + isFirstSlice = false; + isFirstParameterSet = false; + nalUnitTimeUs = pesTimeUs; + nalUnitBytesRead = 0; + nalUnitStartPosition = position; + + if (nalUnitType >= VPS_NUT) { + if (!writingParameterSets && readingSample) { + // This is a non-VCL NAL unit, so flush the previous sample. + outputSample(offset); + readingSample = false; + } + if (nalUnitType <= PPS_NUT) { + // This sample will have parameter sets at the start. + isFirstParameterSet = !writingParameterSets; + writingParameterSets = true; + } + } + + // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp. + nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT); + lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R; + } + + public void readNalUnitData(byte[] data, int offset, int limit) { + if (lookingForFirstSliceFlag) { + int headerOffset = offset + FIRST_SLICE_FLAG_OFFSET - nalUnitBytesRead; + if (headerOffset < limit) { + isFirstSlice = (data[headerOffset] & 0x80) != 0; + lookingForFirstSliceFlag = false; + } else { + nalUnitBytesRead += limit - offset; + } + } + } + + public void endNalUnit(long position, int offset) { + if (writingParameterSets && isFirstSlice) { + // This sample has parameter sets. Reset the key-frame flag based on the first slice. + sampleIsKeyframe = nalUnitHasKeyframeData; + writingParameterSets = false; + } else if (isFirstParameterSet || isFirstSlice) { + // This NAL unit is at the start of a new sample (access unit). + if (readingSample) { + // Output the sample ending before this NAL unit. + int nalUnitLength = (int) (position - nalUnitStartPosition); + outputSample(offset + nalUnitLength); + } + samplePosition = nalUnitStartPosition; + sampleTimeUs = nalUnitTimeUs; + readingSample = true; + sampleIsKeyframe = nalUnitHasKeyframeData; + } + } + + private void outputSample(int offset) { + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + int size = (int) (nalUnitStartPosition - samplePosition); + output.sampleMetadata(sampleTimeUs, flags, size, offset, null); + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java new file mode 100644 index 0000000000..da63e143c2 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Parses ID3 data and extracts individual text information frames. + */ +public final class Id3Reader implements ElementaryStreamReader { + + private static final String TAG = "Id3Reader"; + + private final ParsableByteArray id3Header; + + private TrackOutput output; + + // State that should be reset on seek. + private boolean writingSample; + + // Per sample state that gets reset at the start of each sample. + private long sampleTimeUs; + private int sampleSize; + private int sampleBytesRead; + + public Id3Reader() { + id3Header = new ParsableByteArray(ID3_HEADER_LENGTH); + } + + @Override + public void seek() { + writingSample = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, + null, Format.NO_VALUE, null)); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) { + return; + } + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleSize = 0; + sampleBytesRead = 0; + } + + @Override + public void consume(ParsableByteArray data) { + if (!writingSample) { + return; + } + int bytesAvailable = data.bytesLeft(); + if (sampleBytesRead < ID3_HEADER_LENGTH) { + // We're still reading the ID3 header. + int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead); + System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead, + headerBytesAvailable); + if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_LENGTH) { + // We've finished reading the ID3 header. Extract the sample size. + id3Header.setPosition(0); + if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte() + || '3' != id3Header.readUnsignedByte()) { + Log.w(TAG, "Discarding invalid ID3 tag"); + writingSample = false; + return; + } + id3Header.skipBytes(3); // version (2) + flags (1) + sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt(); + } + } + // Write data to the output. + int bytesToWrite = Math.min(bytesAvailable, sampleSize - sampleBytesRead); + output.sampleData(data, bytesToWrite); + sampleBytesRead += bytesToWrite; + } + + @Override + public void packetFinished() { + if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { + return; + } + output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + writingSample = false; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java new file mode 100644 index 0000000000..1a41adfa69 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; + +/** + * Parses and extracts samples from an AAC/LATM elementary stream. + */ +public final class LatmReader implements ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC_1 = 0; + private static final int STATE_FINDING_SYNC_2 = 1; + private static final int STATE_READING_HEADER = 2; + private static final int STATE_READING_SAMPLE = 3; + + private static final int INITIAL_BUFFER_SIZE = 1024; + private static final int SYNC_BYTE_FIRST = 0x56; + private static final int SYNC_BYTE_SECOND = 0xE0; + + private final String language; + private final ParsableByteArray sampleDataBuffer; + private final ParsableBitArray sampleBitArray; + + // Track output info. + private TrackOutput output; + private Format format; + private String formatId; + + // Parser state info. + private int state; + private int bytesRead; + private int sampleSize; + private int secondHeaderByte; + private long timeUs; + + // Container data. + private boolean streamMuxRead; + private int audioMuxVersionA; + private int numSubframes; + private int frameLengthType; + private boolean otherDataPresent; + private long otherDataLenBits; + private int sampleRateHz; + private long sampleDurationUs; + private int channelCount; + + /** + * @param language Track language. + */ + public LatmReader(@Nullable String language) { + this.language = language; + sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE); + sampleBitArray = new ParsableBitArray(sampleDataBuffer.data); + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC_1; + streamMuxRead = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + formatId = idGenerator.getFormatId(); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) throws ParserException { + int bytesToRead; + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC_1: + if (data.readUnsignedByte() == SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_2; + } + break; + case STATE_FINDING_SYNC_2: + int secondByte = data.readUnsignedByte(); + if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) { + secondHeaderByte = secondByte; + state = STATE_READING_HEADER; + } else if (secondByte != SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_1; + } + break; + case STATE_READING_HEADER: + sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte(); + if (sampleSize > sampleDataBuffer.data.length) { + resetBufferForSize(sampleSize); + } + bytesRead = 0; + state = STATE_READING_SAMPLE; + break; + case STATE_READING_SAMPLE: + bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + data.readBytes(sampleBitArray.data, bytesRead, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + sampleBitArray.setPosition(0); + parseAudioMuxElement(sampleBitArray); + state = STATE_FINDING_SYNC_1; + } + break; + default: + throw new IllegalStateException(); + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41. + * + * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. + */ + private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { + boolean useSameStreamMux = data.readBit(); + if (!useSameStreamMux) { + streamMuxRead = true; + parseStreamMuxConfig(data); + } else if (!streamMuxRead) { + return; // Parsing cannot continue without StreamMuxConfig information. + } + + if (audioMuxVersionA == 0) { + if (numSubframes != 0) { + throw new ParserException(); + } + int muxSlotLengthBytes = parsePayloadLengthInfo(data); + parsePayloadMux(data, muxSlotLengthBytes); + if (otherDataPresent) { + data.skipBits((int) otherDataLenBits); + } + } else { + throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009. + } + } + + /** + * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. + */ + private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { + int audioMuxVersion = data.readBits(1); + audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; + if (audioMuxVersionA == 0) { + if (audioMuxVersion == 1) { + latmGetValue(data); // Skip taraBufferFullness. + } + if (!data.readBit()) { + throw new ParserException(); + } + numSubframes = data.readBits(6); + int numProgram = data.readBits(4); + int numLayer = data.readBits(3); + if (numProgram != 0 || numLayer != 0) { + throw new ParserException(); + } + if (audioMuxVersion == 0) { + int startPosition = data.getPosition(); + int readBits = parseAudioSpecificConfig(data); + data.setPosition(startPosition); + byte[] initData = new byte[(readBits + 7) / 8]; + data.readBits(initData, 0, readBits); + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, + Collections.singletonList(initData), null, 0, language); + if (!format.equals(this.format)) { + this.format = format; + sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; + output.format(format); + } + } else { + int ascLen = (int) latmGetValue(data); + int bitsRead = parseAudioSpecificConfig(data); + data.skipBits(ascLen - bitsRead); // fillBits. + } + parseFrameLength(data); + otherDataPresent = data.readBit(); + otherDataLenBits = 0; + if (otherDataPresent) { + if (audioMuxVersion == 1) { + otherDataLenBits = latmGetValue(data); + } else { + boolean otherDataLenEsc; + do { + otherDataLenEsc = data.readBit(); + otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8); + } while (otherDataLenEsc); + } + } + boolean crcCheckPresent = data.readBit(); + if (crcCheckPresent) { + data.skipBits(8); // crcCheckSum. + } + } else { + throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009. + } + } + + private void parseFrameLength(ParsableBitArray data) { + frameLengthType = data.readBits(3); + switch (frameLengthType) { + case 0: + data.skipBits(8); // latmBufferFullness. + break; + case 1: + data.skipBits(9); // frameLength. + break; + case 3: + case 4: + case 5: + data.skipBits(6); // CELPframeLengthTableIndex. + break; + case 6: + case 7: + data.skipBits(1); // HVXCframeLengthTableIndex. + break; + default: + throw new IllegalStateException(); + } + } + + private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException { + int bitsLeft = data.bitsLeft(); + Pair<Integer, Integer> config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true); + sampleRateHz = config.first; + channelCount = config.second; + return bitsLeft - data.bitsLeft(); + } + + private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException { + int muxSlotLengthBytes = 0; + // Assuming single program and single layer. + if (frameLengthType == 0) { + int tmp; + do { + tmp = data.readBits(8); + muxSlotLengthBytes += tmp; + } while (tmp == 255); + return muxSlotLengthBytes; + } else { + throw new ParserException(); + } + } + + private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { + // The start of sample data in + int bitPosition = data.getPosition(); + if ((bitPosition & 0x07) == 0) { + // Sample data is byte-aligned. We can output it directly. + sampleDataBuffer.setPosition(bitPosition >> 3); + } else { + // Sample data is not byte-aligned and we need align it ourselves before outputting. + // Byte alignment is needed because LATM framing is not supported by MediaCodec. + data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8); + sampleDataBuffer.setPosition(0); + } + output.sampleData(sampleDataBuffer, muxLengthBytes); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null); + timeUs += sampleDurationUs; + } + + private void resetBufferForSize(int newSize) { + sampleDataBuffer.reset(newSize); + sampleBitArray.reset(sampleDataBuffer.data); + } + + private static long latmGetValue(ParsableBitArray data) { + int bytesForValue = data.readBits(2); + return data.readBits((bytesForValue + 1) * 8); + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java new file mode 100644 index 0000000000..6fefab6314 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Parses a continuous MPEG Audio byte stream and extracts individual frames. + */ +public final class MpegAudioReader implements ElementaryStreamReader { + + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_FRAME = 2; + + private static final int HEADER_SIZE = 4; + + private final ParsableByteArray headerScratch; + private final MpegAudioHeader header; + private final String language; + + private String formatId; + private TrackOutput output; + + private int state; + private int frameBytesRead; + private boolean hasOutputFormat; + + // Used when finding the frame header. + private boolean lastByteWasFF; + + // Parsed from the frame header. + private long frameDurationUs; + private int frameSize; + + // The timestamp to attach to the next sample in the current packet. + private long timeUs; + + public MpegAudioReader() { + this(null); + } + + public MpegAudioReader(String language) { + state = STATE_FINDING_HEADER; + // The first byte of an MPEG Audio frame header is always 0xFF. + headerScratch = new ParsableByteArray(4); + headerScratch.data[0] = (byte) 0xFF; + header = new MpegAudioHeader(); + this.language = language; + } + + @Override + public void seek() { + state = STATE_FINDING_HEADER; + frameBytesRead = 0; + lastByteWasFF = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + findHeader(data); + break; + case STATE_READING_HEADER: + readHeaderRemainder(data); + break; + case STATE_READING_FRAME: + readFrameRemainder(data); + break; + default: + throw new IllegalStateException(); + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Attempts to locate the start of the next frame header. + * <p> + * If a frame header is located then the state is changed to {@link #STATE_READING_HEADER}, the + * first two bytes of the header are written into {@link #headerScratch}, and the position of the + * source is advanced to the byte that immediately follows these two bytes. + * <p> + * If a frame header is not located then the position of the source is advanced to the limit, and + * the method should be called again with the next source to continue the search. + * + * @param source The source from which to read. + */ + private void findHeader(ParsableByteArray source) { + byte[] data = source.data; + int startOffset = source.getPosition(); + int endOffset = source.limit(); + for (int i = startOffset; i < endOffset; i++) { + boolean byteIsFF = (data[i] & 0xFF) == 0xFF; + boolean found = lastByteWasFF && (data[i] & 0xE0) == 0xE0; + lastByteWasFF = byteIsFF; + if (found) { + source.setPosition(i + 1); + // Reset lastByteWasFF for next time. + lastByteWasFF = false; + headerScratch.data[1] = data[i]; + frameBytesRead = 2; + state = STATE_READING_HEADER; + return; + } + } + source.setPosition(endOffset); + } + + /** + * Attempts to read the remaining two bytes of the frame header. + * <p> + * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, + * the media format is output if this has not previously occurred, the four header bytes are + * output as sample data, and the position of the source is advanced to the byte that immediately + * follows the header. + * <p> + * If a frame header is read in full but cannot be parsed then the state is changed to + * {@link #STATE_READING_HEADER}. + * <p> + * If a frame header is not read in full then the position of the source is advanced to the limit, + * and the method should be called again with the next source to continue the read. + * + * @param source The source from which to read. + */ + private void readHeaderRemainder(ParsableByteArray source) { + int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); + source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); + frameBytesRead += bytesToRead; + if (frameBytesRead < HEADER_SIZE) { + // We haven't read the whole header yet. + return; + } + + headerScratch.setPosition(0); + boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header); + if (!parsedHeader) { + // We thought we'd located a frame header, but we hadn't. + frameBytesRead = 0; + state = STATE_READING_HEADER; + return; + } + + frameSize = header.frameSize; + if (!hasOutputFormat) { + frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; + Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, + null, null, 0, language); + output.format(format); + hasOutputFormat = true; + } + + headerScratch.setPosition(0); + output.sampleData(headerScratch, HEADER_SIZE); + state = STATE_READING_FRAME; + } + + /** + * Attempts to read the remainder of the frame. + * <p> + * If a frame is read in full then true is returned. The frame will have been output, and the + * position of the source will have been advanced to the byte that immediately follows the end of + * the frame. + * <p> + * If a frame is not read in full then the position of the source will have been advanced to the + * limit, and the method should be called again with the next source to continue the read. + * + * @param source The source from which to read. + */ + private void readFrameRemainder(ParsableByteArray source) { + int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead); + output.sampleData(source, bytesToRead); + frameBytesRead += bytesToRead; + if (frameBytesRead < frameSize) { + // We haven't read the whole of the frame yet. + return; + } + + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, frameSize, 0, null); + timeUs += frameDurationUs; + frameBytesRead = 0; + state = STATE_FINDING_HEADER; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java new file mode 100644 index 0000000000..4941aa29a0 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; + +/** + * A buffer that fills itself with data corresponding to a specific NAL unit, as it is + * encountered in the stream. + */ +/* package */ final class NalUnitTargetBuffer { + + private final int targetType; + + private boolean isFilling; + private boolean isCompleted; + + public byte[] nalData; + public int nalLength; + + public NalUnitTargetBuffer(int targetType, int initialCapacity) { + this.targetType = targetType; + + // Initialize data with a start code in the first three bytes. + nalData = new byte[3 + initialCapacity]; + nalData[2] = 1; + } + + /** + * Resets the buffer, clearing any data that it holds. + */ + public void reset() { + isFilling = false; + isCompleted = false; + } + + /** + * Returns whether the buffer currently holds a complete NAL unit of the target type. + */ + public boolean isCompleted() { + return isCompleted; + } + + /** + * Called to indicate that a NAL unit has started. + * + * @param type The type of the NAL unit. + */ + public void startNalUnit(int type) { + Assertions.checkState(!isFilling); + isFilling = type == targetType; + if (isFilling) { + // Skip the three byte start code when writing data. + nalLength = 3; + isCompleted = false; + } + } + + /** + * Called to pass stream data. The data passed should not include the 3 byte start code. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (nalData.length < nalLength + readLength) { + nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2); + } + System.arraycopy(data, offset, nalData, nalLength, readLength); + nalLength += readLength; + } + + /** + * Called to indicate that a NAL unit has ended. + * + * @param discardPadding The number of excess bytes that were passed to + * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded. + * @return Whether the ended NAL unit is of the target type. + */ + public boolean endNalUnit(int discardPadding) { + if (!isFilling) { + return false; + } + nalLength -= discardPadding; + isFilling = false; + isCompleted = true; + return true; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java new file mode 100644 index 0000000000..86afe22563 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; + +/** + * Parses PES packet data and extracts samples. + */ +public final class PesReader implements TsPayloadReader { + + private static final String TAG = "PesReader"; + + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_HEADER_EXTENSION = 2; + private static final int STATE_READING_BODY = 3; + + private static final int HEADER_SIZE = 9; + private static final int MAX_HEADER_EXTENSION_SIZE = 10; + private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE) + + private final ElementaryStreamReader reader; + private final ParsableBitArray pesScratch; + + private int state; + private int bytesRead; + + private TimestampAdjuster timestampAdjuster; + private boolean ptsFlag; + private boolean dtsFlag; + private boolean seenFirstDts; + private int extendedHeaderLength; + private int payloadSize; + private boolean dataAlignmentIndicator; + private long timeUs; + + public PesReader(ElementaryStreamReader reader) { + this.reader = reader; + pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); + state = STATE_FINDING_HEADER; + } + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + this.timestampAdjuster = timestampAdjuster; + reader.createTracks(extractorOutput, idGenerator); + } + + // TsPayloadReader implementation. + + @Override + public final void seek() { + state = STATE_FINDING_HEADER; + bytesRead = 0; + seenFirstDts = false; + reader.seek(); + } + + @Override + public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { + if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) { + switch (state) { + case STATE_FINDING_HEADER: + case STATE_READING_HEADER: + // Expected. + break; + case STATE_READING_HEADER_EXTENSION: + Log.w(TAG, "Unexpected start indicator reading extended header"); + break; + case STATE_READING_BODY: + // If payloadSize == -1 then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This + // is expected. If payloadSize != -1, then the length of the previous packet was known, + // but we didn't receive that amount of data. This is not expected. + if (payloadSize != -1) { + Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); + } + // Either way, notify the reader that it has now finished. + reader.packetFinished(); + break; + default: + throw new IllegalStateException(); + } + setState(STATE_READING_HEADER); + } + + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + data.skipBytes(data.bytesLeft()); + break; + case STATE_READING_HEADER: + if (continueRead(data, pesScratch.data, HEADER_SIZE)) { + setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); + } + break; + case STATE_READING_HEADER_EXTENSION: + int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); + // Read as much of the extended header as we're interested in, and skip the rest. + if (continueRead(data, pesScratch.data, readLength) + && continueRead(data, null, extendedHeaderLength)) { + parseHeaderExtension(); + flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; + reader.packetStarted(timeUs, flags); + setState(STATE_READING_BODY); + } + break; + case STATE_READING_BODY: + readLength = data.bytesLeft(); + int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + if (padding > 0) { + readLength -= padding; + data.setLimit(data.getPosition() + readLength); + } + reader.consume(data); + if (payloadSize != -1) { + payloadSize -= readLength; + if (payloadSize == 0) { + reader.packetFinished(); + setState(STATE_READING_HEADER); + } + } + break; + default: + throw new IllegalStateException(); + } + } + } + + private void setState(int state) { + this.state = state; + bytesRead = 0; + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read, or {@code null} to skip. + * @param targetLength The target length of the read. + * @return Whether the target length has been reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + if (bytesToRead <= 0) { + return true; + } else if (target == null) { + source.skipBytes(bytesToRead); + } else { + source.readBytes(target, bytesRead, bytesToRead); + } + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + private boolean parseHeader() { + // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of + // the header. + pesScratch.setPosition(0); + int startCodePrefix = pesScratch.readBits(24); + if (startCodePrefix != 0x000001) { + Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); + payloadSize = -1; + return false; + } + + pesScratch.skipBits(8); // stream_id. + int packetLength = pesScratch.readBits(16); + pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1) + dataAlignmentIndicator = pesScratch.readBit(); + pesScratch.skipBits(2); // copyright (1), original_or_copy (1) + ptsFlag = pesScratch.readBit(); + dtsFlag = pesScratch.readBit(); + // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(6); + extendedHeaderLength = pesScratch.readBits(8); + + if (packetLength == 0) { + payloadSize = -1; + } else { + payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ + - HEADER_SIZE - extendedHeaderLength; + } + return true; + } + + private void parseHeaderExtension() { + pesScratch.setPosition(0); + timeUs = C.TIME_UNSET; + if (ptsFlag) { + pesScratch.skipBits(4); // '0010' or '0011' + long pts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + if (!seenFirstDts && dtsFlag) { + pesScratch.skipBits(4); // '0011' + long dts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + // Subsequent PES packets may have earlier presentation timestamps than this one, but they + // should all be greater than or equal to this packet's decode timestamp. We feed the + // decode timestamp to the adjuster here so that in the case that this is the first to be + // fed, the adjuster will be able to compute an offset to apply such that the adjusted + // presentation timestamps of all future packets are non-negative. + timestampAdjuster.adjustTsTimestamp(dts); + seenFirstDts = true; + } + timeUs = timestampAdjuster.adjustTsTimestamp(pts); + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java new file mode 100644 index 0000000000..acd08a2f12 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A seeker that supports seeking within PS stream using binary search. + * + * <p>This seeker uses the first and last SCR values within the stream, as well as the stream + * duration to interpolate the SCR value of the seeking position. Then it performs binary search + * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from + * the target SCR. + */ +/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker { + + private static final long SEEK_TOLERANCE_US = 100_000; + private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000; + private static final int TIMESTAMP_SEARCH_BYTES = 20000; + + public PsBinarySearchSeeker( + TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) { + super( + new DefaultSeekTimestampConverter(), + new PsScrSeeker(scrTimestampAdjuster), + streamDurationUs, + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ streamDurationUs + 1, + /* floorBytePosition= */ 0, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE, + MINIMUM_SEARCH_RANGE_BYTES); + } + + /** + * A seeker that looks for a given SCR timestamp at a given position in a PS stream. + * + * <p>Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link + * #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and + * then compare the SCR timestamps (if available) of these packets to the target timestamp. + */ + private static final class PsScrSeeker implements TimestampSeeker { + + private final TimestampAdjuster scrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) { + this.scrTimestampAdjuster = scrTimestampAdjuster; + packetBuffer = new ParsableByteArray(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); + + packetBuffer.reset(bytesToSearch); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); + } + + @Override + public void onSeekFinished() { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + } + + private TimestampSearchResult searchForScrValueInBuffer( + ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) { + int startOfLastPacketPosition = C.POSITION_UNSET; + int endOfLastPacketPosition = C.POSITION_UNSET; + long lastScrTimeUsInRange = C.TIME_UNSET; + + while (packetBuffer.bytesLeft() >= 4) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode != PsExtractor.PACK_START_CODE) { + packetBuffer.skipBytes(1); + continue; + } else { + packetBuffer.skipBytes(4); + } + + // We found a pack. + long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue); + if (scrTimeUs > targetScrTimeUs) { + if (lastScrTimeUsInRange == C.TIME_UNSET) { + // First SCR timestamp is already over target. + return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset); + } else { + // Last SCR timestamp < target timestamp < this timestamp. + return TimestampSearchResult.targetFoundResult( + bufferStartOffset + startOfLastPacketPosition); + } + } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) { + long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition(); + return TimestampSearchResult.targetFoundResult(startOfPacketInStream); + } + + lastScrTimeUsInRange = scrTimeUs; + startOfLastPacketPosition = packetBuffer.getPosition(); + } + skipToEndOfCurrentPack(packetBuffer); + endOfLastPacketPosition = packetBuffer.getPosition(); + } + + if (lastScrTimeUsInRange != C.TIME_UNSET) { + long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition; + return TimestampSearchResult.underestimatedResult( + lastScrTimeUsInRange, endOfLastPacketPositionInStream); + } else { + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; + } + } + + /** + * Skips the buffer position to the position after the end of the current PS pack in the buffer, + * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in + * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer. + */ + private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) { + int limit = packetBuffer.limit(); + + if (packetBuffer.bytesLeft() < 10) { + // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing + // length. + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(9); + + int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07; + if (packetBuffer.bytesLeft() < packStuffingLength) { + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(packStuffingLength); + + if (packetBuffer.bytesLeft() < 4) { + packetBuffer.setPosition(limit); + return; + } + + int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) { + packetBuffer.skipBytes(4); + int systemHeaderLength = packetBuffer.readUnsignedShort(); + if (packetBuffer.bytesLeft() < systemHeaderLength) { + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(systemHeaderLength); + } + + // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right + // after the end position of this pack. + // If we couldn't find these codes within the buffer, return the buffer limit, or return + // the first position which PES packets pattern does not match (some malformed packets). + while (packetBuffer.bytesLeft() >= 4) { + nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode == PsExtractor.PACK_START_CODE + || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) { + break; + } + if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) { + break; + } + packetBuffer.skipBytes(4); + + if (packetBuffer.bytesLeft() < 2) { + // 2 bytes for PES_packet length. + packetBuffer.setPosition(limit); + return; + } + int pesPacketLength = packetBuffer.readUnsignedShort(); + packetBuffer.setPosition( + Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength)); + } + } + } + + private static int peekIntAtPosition(byte[] data, int position) { + return (data[position] & 0xFF) << 24 + | (data[position + 1] & 0xFF) << 16 + | (data[position + 2] & 0xFF) << 8 + | (data[position + 3] & 0xFF); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java new file mode 100644 index 0000000000..a5960fbe15 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A reader that can extract the approximate duration from a given MPEG program stream (PS). + * + * <p>This reader extracts the duration by reading system clock reference (SCR) values from the + * header of a pack at the start and at the end of the stream, calculating the difference, and + * converting that into stream duration. This reader also handles the case when a single SCR + * wraparound takes place within the stream, which can make SCR values at the beginning of the + * stream larger than SCR values at the end. This class can only be used once to read duration from + * a given stream, and the usage of the class is not thread-safe, so all calls should be made from + * the same thread. + * + * <p>Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header. + */ +/* package */ final class PsDurationReader { + + private static final int TIMESTAMP_SEARCH_BYTES = 20000; + + private final TimestampAdjuster scrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private boolean isDurationRead; + private boolean isFirstScrValueRead; + private boolean isLastScrValueRead; + + private long firstScrValue; + private long lastScrValue; + private long durationUs; + + /* package */ PsDurationReader() { + scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + firstScrValue = C.TIME_UNSET; + lastScrValue = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + packetBuffer = new ParsableByteArray(); + } + + /** Returns true if a PS duration has been read. */ + public boolean isDurationReadFinished() { + return isDurationRead; + } + + public TimestampAdjuster getScrTimestampAdjuster() { + return scrTimestampAdjuster; + } + + /** + * Reads a PS duration from the input. + * + * <p>This reader reads the duration by reading SCR values from the header of a pack at the start + * and at the end of the stream, calculating the difference, and converting that into stream + * duration. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public @Extractor.ReadResult int readDuration( + ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + if (!isLastScrValueRead) { + return readLastScrValue(input, seekPositionHolder); + } + if (lastScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + if (!isFirstScrValueRead) { + return readFirstScrValue(input, seekPositionHolder); + } + if (firstScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + + long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue); + long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue); + durationUs = maxScrPositionUs - minScrPositionUs; + return finishReadDuration(input); + } + + /** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the SCR value read from the next pack in the stream, given the buffer at the pack + * header start position (just behind the pack start code). + */ + public static long readScrValueFromPack(ParsableByteArray packetBuffer) { + int originalPosition = packetBuffer.getPosition(); + if (packetBuffer.bytesLeft() < 9) { + // We require at 9 bytes for pack header to read scr value + return C.TIME_UNSET; + } + byte[] scrBytes = new byte[9]; + packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length); + packetBuffer.setPosition(originalPosition); + if (!checkMarkerBits(scrBytes)) { + return C.TIME_UNSET; + } + return readScrValueFromPackHeader(scrBytes); + } + + private int finishReadDuration(ExtractorInput input) { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + isDurationRead = true; + input.resetPeekPosition(); + return Extractor.RESULT_CONTINUE; + } + + private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength()); + int searchStartPosition = 0; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + firstScrValue = readFirstScrValueFromBuffer(packetBuffer); + isFirstScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchStartPosition; + searchPosition < searchEndPosition - 3; + searchPosition++) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + packetBuffer.setPosition(searchPosition + 4); + long scrValue = readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength); + long searchStartPosition = inputLength - bytesToSearch; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + lastScrValue = readLastScrValueFromBuffer(packetBuffer); + isLastScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchEndPosition - 4; + searchPosition >= searchStartPosition; + searchPosition--) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + packetBuffer.setPosition(searchPosition + 4); + long scrValue = readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int peekIntAtPosition(byte[] data, int position) { + return (data[position] & 0xFF) << 24 + | (data[position + 1] & 0xFF) << 16 + | (data[position + 2] & 0xFF) << 8 + | (data[position + 3] & 0xFF); + } + + private static boolean checkMarkerBits(byte[] scrBytes) { + // Verify the 01xxx1xx marker on the 0th byte + if ((scrBytes[0] & 0xC4) != 0x44) { + return false; + } + // 1st byte belongs to scr field. + // Verify the xxxxx1xx marker on the 2nd byte + if ((scrBytes[2] & 0x04) != 0x04) { + return false; + } + // 3rd byte belongs to scr field. + // Verify the xxxxx1xx marker on the 4rd byte + if ((scrBytes[4] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxxxx1 marker on the 5th byte + if ((scrBytes[5] & 0x01) != 0x01) { + return false; + } + // 6th and 7th bytes belongs to program_max_rate field. + // Verify the xxxxxx11 marker on the 8th byte + return (scrBytes[8] & 0x03) == 0x03; + } + + /** + * Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring + * the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in + * pack_header. + * + * <p>We ignore SCR Ext, because it's too small to have any significance. + */ + private static long readScrValueFromPackHeader(byte[] scrBytes) { + return ((scrBytes[0] & 0b00111000L) >> 3) << 30 + | (scrBytes[0] & 0b00000011L) << 28 + | (scrBytes[1] & 0xFFL) << 20 + | ((scrBytes[2] & 0b11111000L) >> 3) << 15 + | (scrBytes[2] & 0b00000011L) << 13 + | (scrBytes[3] & 0xFFL) << 5 + | (scrBytes[4] & 0b11111000L) >> 3; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java new file mode 100644 index 0000000000..8dcccbe459 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import android.util.SparseArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; + +/** + * Extracts data from the MPEG-2 PS container format. + */ +public final class PsExtractor implements Extractor { + + /** Factory for {@link PsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new PsExtractor()}; + + /* package */ static final int PACK_START_CODE = 0x000001BA; + /* package */ static final int SYSTEM_HEADER_START_CODE = 0x000001BB; + /* package */ static final int PACKET_START_CODE_PREFIX = 0x000001; + /* package */ static final int MPEG_PROGRAM_END_CODE = 0x000001B9; + private static final int MAX_STREAM_ID_PLUS_ONE = 0x100; + + // Max search length for first audio and video track in input data. + private static final long MAX_SEARCH_LENGTH = 1024 * 1024; + // Max search length for additional audio and video tracks in input data after at least one audio + // and video track has been found. + private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024; + + public static final int PRIVATE_STREAM_1 = 0xBD; + public static final int AUDIO_STREAM = 0xC0; + public static final int AUDIO_STREAM_MASK = 0xE0; + public static final int VIDEO_STREAM = 0xE0; + public static final int VIDEO_STREAM_MASK = 0xF0; + + private final TimestampAdjuster timestampAdjuster; + private final SparseArray<PesReader> psPayloadReaders; // Indexed by pid + private final ParsableByteArray psPacketBuffer; + private final PsDurationReader durationReader; + + private boolean foundAllTracks; + private boolean foundAudioTrack; + private boolean foundVideoTrack; + private long lastTrackPosition; + + // Accessed only by the loading thread. + private PsBinarySearchSeeker psBinarySearchSeeker; + private ExtractorOutput output; + private boolean hasOutputSeekMap; + + public PsExtractor() { + this(new TimestampAdjuster(0)); + } + + public PsExtractor(TimestampAdjuster timestampAdjuster) { + this.timestampAdjuster = timestampAdjuster; + psPacketBuffer = new ParsableByteArray(4096); + psPayloadReaders = new SparseArray<>(); + durationReader = new PsDurationReader(); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + byte[] scratch = new byte[14]; + input.peekFully(scratch, 0, 14); + + // Verify the PACK_START_CODE for the first 4 bytes + if (PACK_START_CODE != (((scratch[0] & 0xFF) << 24) | ((scratch[1] & 0xFF) << 16) + | ((scratch[2] & 0xFF) << 8) | (scratch[3] & 0xFF))) { + return false; + } + // Verify the 01xxx1xx marker on the 5th byte + if ((scratch[4] & 0xC4) != 0x44) { + return false; + } + // Verify the xxxxx1xx marker on the 7th byte + if ((scratch[6] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxx1xx marker on the 9th byte + if ((scratch[8] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxxxx1 marker on the 10th byte + if ((scratch[9] & 0x01) != 0x01) { + return false; + } + // Verify the xxxxxx11 marker on the 13th byte + if ((scratch[12] & 0x03) != 0x03) { + return false; + } + // Read the stuffing length from the 14th byte (last 3 bits) + int packStuffingLength = scratch[13] & 0x07; + input.advancePeekPosition(packStuffingLength); + // Now check that the next 3 bytes are the beginning of an MPEG start code + input.peekFully(scratch, 0, 3); + return (PACKET_START_CODE_PREFIX == (((scratch[0] & 0xFF) << 16) | ((scratch[1] & 0xFF) << 8) + | (scratch[2] & 0xFF))); + } + + @Override + public void init(ExtractorOutput output) { + this.output = output; + } + + @Override + public void seek(long position, long timeUs) { + boolean hasNotEncounteredFirstTimestamp = + timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET; + if (hasNotEncounteredFirstTimestamp + || (timestampAdjuster.getFirstSampleTimestampUs() != 0 + && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) { + // - If the timestamp adjuster in the PS stream has not encountered any sample, it's going to + // treat the first timestamp encountered as sample time 0, which is incorrect. In this case, + // we have to set the first sample timestamp manually. + // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a + // different position, we need to set the first sample timestamp manually again. + timestampAdjuster.reset(); + timestampAdjuster.setFirstSampleTimestampUs(timeUs); + } + + if (psBinarySearchSeeker != null) { + psBinarySearchSeeker.setSeekTargetUs(timeUs); + } + for (int i = 0; i < psPayloadReaders.size(); i++) { + psPayloadReaders.valueAt(i).seek(); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + + long inputLength = input.getLength(); + boolean canReadDuration = inputLength != C.LENGTH_UNSET; + if (canReadDuration && !durationReader.isDurationReadFinished()) { + return durationReader.readDuration(input, seekPosition); + } + maybeOutputSeekMap(inputLength); + if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) { + return psBinarySearchSeeker.handlePendingSeek(input, seekPosition); + } + + input.resetPeekPosition(); + long peekBytesLeft = + inputLength != C.LENGTH_UNSET ? inputLength - input.getPeekPosition() : C.LENGTH_UNSET; + if (peekBytesLeft != C.LENGTH_UNSET && peekBytesLeft < 4) { + return RESULT_END_OF_INPUT; + } + // First peek and check what type of start code is next. + if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) { + return RESULT_END_OF_INPUT; + } + + psPacketBuffer.setPosition(0); + int nextStartCode = psPacketBuffer.readInt(); + if (nextStartCode == MPEG_PROGRAM_END_CODE) { + return RESULT_END_OF_INPUT; + } else if (nextStartCode == PACK_START_CODE) { + // Now peek the rest of the pack_header. + input.peekFully(psPacketBuffer.data, 0, 10); + + // We only care about the pack_stuffing_length in here, skip the first 77 bits. + psPacketBuffer.setPosition(9); + + // Last 3 bits is the length. + int packStuffingLength = psPacketBuffer.readUnsignedByte() & 0x07; + + // Now skip the stuffing and the pack header. + input.skipFully(packStuffingLength + 14); + return RESULT_CONTINUE; + } else if (nextStartCode == SYSTEM_HEADER_START_CODE) { + // We just skip all this, but we need to get the length first. + input.peekFully(psPacketBuffer.data, 0, 2); + + // Length is the next 2 bytes. + psPacketBuffer.setPosition(0); + int systemHeaderLength = psPacketBuffer.readUnsignedShort(); + input.skipFully(systemHeaderLength + 6); + return RESULT_CONTINUE; + } else if (((nextStartCode & 0xFFFFFF00) >> 8) != PACKET_START_CODE_PREFIX) { + input.skipFully(1); // Skip bytes until we see a valid start code again. + return RESULT_CONTINUE; + } + + // We're at the start of a regular PES packet now. + // Get the stream ID off the last byte of the start code. + int streamId = nextStartCode & 0xFF; + + // Check to see if we have this one in our map yet, and if not, then add it. + PesReader payloadReader = psPayloadReaders.get(streamId); + if (!foundAllTracks) { + if (payloadReader == null) { + ElementaryStreamReader elementaryStreamReader = null; + if (streamId == PRIVATE_STREAM_1) { + // Private stream, used for AC3 audio. + // NOTE: This may need further parsing to determine if its DTS, but that's likely only + // valid for DVDs. + elementaryStreamReader = new Ac3Reader(); + foundAudioTrack = true; + lastTrackPosition = input.getPosition(); + } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { + elementaryStreamReader = new MpegAudioReader(); + foundAudioTrack = true; + lastTrackPosition = input.getPosition(); + } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { + elementaryStreamReader = new H262Reader(); + foundVideoTrack = true; + lastTrackPosition = input.getPosition(); + } + if (elementaryStreamReader != null) { + TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); + elementaryStreamReader.createTracks(output, idGenerator); + payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster); + psPayloadReaders.put(streamId, payloadReader); + } + } + long maxSearchPosition = + foundAudioTrack && foundVideoTrack + ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND + : MAX_SEARCH_LENGTH; + if (input.getPosition() > maxSearchPosition) { + foundAllTracks = true; + output.endTracks(); + } + } + + // The next 2 bytes are the length. Once we have that we can consume the complete packet. + input.peekFully(psPacketBuffer.data, 0, 2); + psPacketBuffer.setPosition(0); + int payloadLength = psPacketBuffer.readUnsignedShort(); + int pesLength = payloadLength + 6; + + if (payloadReader == null) { + // Just skip this data. + input.skipFully(pesLength); + } else { + psPacketBuffer.reset(pesLength); + // Read the whole packet and the header for consumption. + input.readFully(psPacketBuffer.data, 0, pesLength); + psPacketBuffer.setPosition(6); + payloadReader.consume(psPacketBuffer); + psPacketBuffer.setLimit(psPacketBuffer.capacity()); + } + + return RESULT_CONTINUE; + } + + // Internals. + + private void maybeOutputSeekMap(long inputLength) { + if (!hasOutputSeekMap) { + hasOutputSeekMap = true; + if (durationReader.getDurationUs() != C.TIME_UNSET) { + psBinarySearchSeeker = + new PsBinarySearchSeeker( + durationReader.getScrTimestampAdjuster(), + durationReader.getDurationUs(), + inputLength); + output.seekMap(psBinarySearchSeeker.getSeekMap()); + } else { + output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); + } + } + } + + /** + * Parses PES packet data and extracts samples. + */ + private static final class PesReader { + + private static final int PES_SCRATCH_SIZE = 64; + + private final ElementaryStreamReader pesPayloadReader; + private final TimestampAdjuster timestampAdjuster; + private final ParsableBitArray pesScratch; + + private boolean ptsFlag; + private boolean dtsFlag; + private boolean seenFirstDts; + private int extendedHeaderLength; + private long timeUs; + + public PesReader(ElementaryStreamReader pesPayloadReader, TimestampAdjuster timestampAdjuster) { + this.pesPayloadReader = pesPayloadReader; + this.timestampAdjuster = timestampAdjuster; + pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); + } + + /** + * Notifies the reader that a seek has occurred. + * <p> + * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray)} will not be a continuation of the data that was + * previously passed. Hence the reader should reset any internal state. + */ + public void seek() { + seenFirstDts = false; + pesPayloadReader.seek(); + } + + /** + * Consumes the payload of a PS packet. + * + * @param data The PES packet. The position will be set to the start of the payload. + * @throws ParserException If the payload could not be parsed. + */ + public void consume(ParsableByteArray data) throws ParserException { + data.readBytes(pesScratch.data, 0, 3); + pesScratch.setPosition(0); + parseHeader(); + data.readBytes(pesScratch.data, 0, extendedHeaderLength); + pesScratch.setPosition(0); + parseHeaderExtension(); + pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR); + pesPayloadReader.consume(data); + // We always have complete PES packets with program stream. + pesPayloadReader.packetFinished(); + } + + private void parseHeader() { + // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of + // the header. + // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), + // data_alignment_indicator (1), copyright (1), original_or_copy (1) + pesScratch.skipBits(8); + ptsFlag = pesScratch.readBit(); + dtsFlag = pesScratch.readBit(); + // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(6); + extendedHeaderLength = pesScratch.readBits(8); + } + + private void parseHeaderExtension() { + timeUs = 0; + if (ptsFlag) { + pesScratch.skipBits(4); // '0010' or '0011' + long pts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + if (!seenFirstDts && dtsFlag) { + pesScratch.skipBits(4); // '0011' + long dts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + // Subsequent PES packets may have earlier presentation timestamps than this one, but they + // should all be greater than or equal to this packet's decode timestamp. We feed the + // decode timestamp to the adjuster here so that in the case that this is the first to be + // fed, the adjuster will be able to compute an offset to apply such that the adjusted + // presentation timestamps of all future packets are non-negative. + timestampAdjuster.adjustTsTimestamp(dts); + seenFirstDts = true; + } + timeUs = timestampAdjuster.adjustTsTimestamp(pts); + } + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java new file mode 100644 index 0000000000..b5942b8bcc --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; + +/** + * Reads section data. + */ +public interface SectionPayloadReader { + + /** + * Initializes the section payload reader. + * + * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data. + * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the + * {@link TrackOutput}s. + */ + void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator); + + /** + * Called by a {@link SectionReader} when a full section is received. + * + * @param sectionData The data belonging to a section starting from the table_id. If + * section_syntax_indicator is set to '1', {@code sectionData} excludes the CRC_32 field. + * Otherwise, all bytes belonging to the table section are included. + */ + void consume(ParsableByteArray sectionData); + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java new file mode 100644 index 0000000000..61b53cfa72 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Reads section data packets and feeds the whole sections to a given {@link SectionPayloadReader}. + * Useful information on PSI sections can be found in ISO/IEC 13818-1, section 2.4.4. + */ +public final class SectionReader implements TsPayloadReader { + + private static final int SECTION_HEADER_LENGTH = 3; + private static final int DEFAULT_SECTION_BUFFER_LENGTH = 32; + private static final int MAX_SECTION_LENGTH = 4098; + + private final SectionPayloadReader reader; + private final ParsableByteArray sectionData; + + private int totalSectionLength; + private int bytesRead; + private boolean sectionSyntaxIndicator; + private boolean waitingForPayloadStart; + + public SectionReader(SectionPayloadReader reader) { + this.reader = reader; + sectionData = new ParsableByteArray(DEFAULT_SECTION_BUFFER_LENGTH); + } + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + reader.init(timestampAdjuster, extractorOutput, idGenerator); + waitingForPayloadStart = true; + } + + @Override + public void seek() { + waitingForPayloadStart = true; + } + + @Override + public void consume(ParsableByteArray data, @Flags int flags) { + boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0; + int payloadStartPosition = C.POSITION_UNSET; + if (payloadUnitStartIndicator) { + int payloadStartOffset = data.readUnsignedByte(); + payloadStartPosition = data.getPosition() + payloadStartOffset; + } + + if (waitingForPayloadStart) { + if (!payloadUnitStartIndicator) { + return; + } + waitingForPayloadStart = false; + data.setPosition(payloadStartPosition); + bytesRead = 0; + } + + while (data.bytesLeft() > 0) { + if (bytesRead < SECTION_HEADER_LENGTH) { + // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of + // the header. + if (bytesRead == 0) { + int tableId = data.readUnsignedByte(); + data.setPosition(data.getPosition() - 1); + if (tableId == 0xFF /* forbidden value */) { + // No more sections in this ts packet. + waitingForPayloadStart = true; + return; + } + } + int headerBytesToRead = Math.min(data.bytesLeft(), SECTION_HEADER_LENGTH - bytesRead); + data.readBytes(sectionData.data, bytesRead, headerBytesToRead); + bytesRead += headerBytesToRead; + if (bytesRead == SECTION_HEADER_LENGTH) { + sectionData.reset(SECTION_HEADER_LENGTH); + sectionData.skipBytes(1); // Skip table id (8). + int secondHeaderByte = sectionData.readUnsignedByte(); + int thirdHeaderByte = sectionData.readUnsignedByte(); + sectionSyntaxIndicator = (secondHeaderByte & 0x80) != 0; + totalSectionLength = + (((secondHeaderByte & 0x0F) << 8) | thirdHeaderByte) + SECTION_HEADER_LENGTH; + if (sectionData.capacity() < totalSectionLength) { + // Ensure there is enough space to keep the whole section. + byte[] bytes = sectionData.data; + sectionData.reset( + Math.min(MAX_SECTION_LENGTH, Math.max(totalSectionLength, bytes.length * 2))); + System.arraycopy(bytes, 0, sectionData.data, 0, SECTION_HEADER_LENGTH); + } + } + } else { + // Reading the body. + int bodyBytesToRead = Math.min(data.bytesLeft(), totalSectionLength - bytesRead); + data.readBytes(sectionData.data, bytesRead, bodyBytesToRead); + bytesRead += bodyBytesToRead; + if (bytesRead == totalSectionLength) { + if (sectionSyntaxIndicator) { + // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11. + if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { + // The CRC is invalid so discard the section. + waitingForPayloadStart = true; + return; + } + sectionData.reset(totalSectionLength - 4); // Exclude the CRC_32 field. + } else { + // This is a private section with private defined syntax. + sectionData.reset(totalSectionLength); + } + reader.consume(sectionData); + bytesRead = 0; + } + } + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java new file mode 100644 index 0000000000..88ea482be4 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.List; + +/** Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ +public final class SeiReader { + + private final List<Format> closedCaptionFormats; + private final TrackOutput[] outputs; + + /** + * @param closedCaptionFormats A list of formats for the closed caption channels to expose. + */ + public SeiReader(List<Format> closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; + } + + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + Format channelFormat = closedCaptionFormats.get(i); + String channelMimeType = channelFormat.sampleMimeType; + Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); + String formatId = channelFormat.id != null ? channelFormat.id : idGenerator.getFormatId(); + output.format( + Format.createTextSampleFormat( + formatId, + channelMimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + channelFormat.selectionFlags, + channelFormat.language, + channelFormat.accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + channelFormat.initializationData)); + outputs[i] = output; + } + } + + public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { + CeaUtil.consume(pesTimeUs, seiBuffer, outputs); + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java new file mode 100644 index 0000000000..17223bad7c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; + +/** + * Parses splice info sections as defined by SCTE35. + */ +public final class SpliceInfoSectionReader implements SectionPayloadReader { + + private TimestampAdjuster timestampAdjuster; + private TrackOutput output; + private boolean formatDeclared; + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TsPayloadReader.TrackIdGenerator idGenerator) { + this.timestampAdjuster = timestampAdjuster; + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, + null, Format.NO_VALUE, null)); + } + + @Override + public void consume(ParsableByteArray sectionData) { + if (!formatDeclared) { + if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { + // There is not enough information to initialize the timestamp adjuster. + return; + } + output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, + timestampAdjuster.getTimestampOffsetUs())); + formatDeclared = true; + } + int sampleSize = sectionData.bytesLeft(); + output.sampleData(sectionData, sampleSize); + output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME, + sampleSize, 0, null); + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java new file mode 100644 index 0000000000..136691bdaf --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A seeker that supports seeking within TS stream using binary search. + * + * <p>This seeker uses the first and last PCR values within the stream, as well as the stream + * duration to interpolate the PCR value of the seeking position. Then it performs binary search + * within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the + * target PCR. + */ +/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker { + + private static final long SEEK_TOLERANCE_US = 100_000; + private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE; + private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE; + + public TsBinarySearchSeeker( + TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) { + super( + new DefaultSeekTimestampConverter(), + new TsPcrSeeker(pcrPid, pcrTimestampAdjuster), + streamDurationUs, + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ streamDurationUs + 1, + /* floorBytePosition= */ 0, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE, + MINIMUM_SEARCH_RANGE_BYTES); + } + + /** + * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given + * position in a TS stream. + * + * <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link + * #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to + * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target + * timestamp. + */ + private static final class TsPcrSeeker implements TimestampSeeker { + + private final TimestampAdjuster pcrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + private final int pcrPid; + + public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) { + this.pcrPid = pcrPid; + this.pcrTimestampAdjuster = pcrTimestampAdjuster; + packetBuffer = new ParsableByteArray(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); + + packetBuffer.reset(bytesToSearch); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); + } + + private TimestampSearchResult searchForPcrValueInBuffer( + ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) { + int limit = packetBuffer.limit(); + + long startOfLastPacketPosition = C.POSITION_UNSET; + long endOfLastPacketPosition = C.POSITION_UNSET; + long lastPcrTimeUsInRange = C.TIME_UNSET; + + while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) { + int startOfPacket = + TsUtil.findSyncBytePosition(packetBuffer.data, packetBuffer.getPosition(), limit); + int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE; + if (endOfPacket > limit) { + break; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid); + if (pcrValue != C.TIME_UNSET) { + long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue); + if (pcrTimeUs > targetPcrTimeUs) { + if (lastPcrTimeUsInRange == C.TIME_UNSET) { + // First PCR timestamp is already over target. + return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset); + } else { + // Last PCR timestamp < target timestamp < this timestamp. + return TimestampSearchResult.targetFoundResult( + bufferStartOffset + startOfLastPacketPosition); + } + } else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) { + long startOfPacketInStream = bufferStartOffset + startOfPacket; + return TimestampSearchResult.targetFoundResult(startOfPacketInStream); + } + + lastPcrTimeUsInRange = pcrTimeUs; + startOfLastPacketPosition = startOfPacket; + } + packetBuffer.setPosition(endOfPacket); + endOfLastPacketPosition = endOfPacket; + } + + if (lastPcrTimeUsInRange != C.TIME_UNSET) { + long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition; + return TimestampSearchResult.underestimatedResult( + lastPcrTimeUsInRange, endOfLastPacketPositionInStream); + } else { + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; + } + } + + @Override + public void onSeekFinished() { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java new file mode 100644 index 0000000000..ed4b66a7e4 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A reader that can extract the approximate duration from a given MPEG transport stream (TS). + * + * <p>This reader extracts the duration by reading PCR values of the PCR PID packets at the start + * and at the end of the stream, calculating the difference, and converting that into stream + * duration. This reader also handles the case when a single PCR wraparound takes place within the + * stream, which can make PCR values at the beginning of the stream larger than PCR values at the + * end. This class can only be used once to read duration from a given stream, and the usage of the + * class is not thread-safe, so all calls should be made from the same thread. + */ +/* package */ final class TsDurationReader { + + private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE; + + private final TimestampAdjuster pcrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private boolean isDurationRead; + private boolean isFirstPcrValueRead; + private boolean isLastPcrValueRead; + + private long firstPcrValue; + private long lastPcrValue; + private long durationUs; + + /* package */ TsDurationReader() { + pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + firstPcrValue = C.TIME_UNSET; + lastPcrValue = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + packetBuffer = new ParsableByteArray(); + } + + /** Returns true if a TS duration has been read. */ + public boolean isDurationReadFinished() { + return isDurationRead; + } + + /** + * Reads a TS duration from the input, using the given PCR PID. + * + * <p>This reader reads the duration by reading PCR values of the PCR PID packets at the start and + * at the end of the stream, calculating the difference, and converting that into stream duration. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @param pcrPid The PID of the packet stream within this TS stream that contains PCR values. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public @Extractor.ReadResult int readDuration( + ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + if (pcrPid <= 0) { + return finishReadDuration(input); + } + if (!isLastPcrValueRead) { + return readLastPcrValue(input, seekPositionHolder, pcrPid); + } + if (lastPcrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + if (!isFirstPcrValueRead) { + return readFirstPcrValue(input, seekPositionHolder, pcrPid); + } + if (firstPcrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + + long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue); + long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue); + durationUs = maxPcrPositionUs - minPcrPositionUs; + return finishReadDuration(input); + } + + /** + * Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder, int)}. + */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the {@link TimestampAdjuster} that this class uses to adjust timestamps read from the + * input TS stream. + */ + public TimestampAdjuster getPcrTimestampAdjuster() { + return pcrTimestampAdjuster; + } + + private int finishReadDuration(ExtractorInput input) { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + isDurationRead = true; + input.resetPeekPosition(); + return Extractor.RESULT_CONTINUE; + } + + private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength()); + int searchStartPosition = 0; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid); + isFirstPcrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readFirstPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchStartPosition; + searchPosition < searchEndPosition; + searchPosition++) { + if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { + continue; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid); + if (pcrValue != C.TIME_UNSET) { + return pcrValue; + } + } + return C.TIME_UNSET; + } + + private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength); + long searchStartPosition = inputLength - bytesToSearch; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid); + isLastPcrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readLastPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchEndPosition - 1; + searchPosition >= searchStartPosition; + searchPosition--) { + if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { + continue; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid); + if (pcrValue != C.TIME_UNSET) { + return pcrValue; + } + } + return C.TIME_UNSET; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java new file mode 100644 index 0000000000..a52e56bd32 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -0,0 +1,698 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR; + +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.Flags; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Extracts data from the MPEG-2 TS container format. + */ +public final class TsExtractor implements Extractor { + + /** Factory for {@link TsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new TsExtractor()}; + + /** + * Modes for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} or {@link + * #MODE_HLS}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS}) + public @interface Mode {} + + /** + * Behave as defined in ISO/IEC 13818-1. + */ + public static final int MODE_MULTI_PMT = 0; + /** + * Assume only one PMT will be contained in the stream, even if more are declared by the PAT. + */ + public static final int MODE_SINGLE_PMT = 1; + /** + * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore + * continuity counters. + */ + public static final int MODE_HLS = 2; + + public static final int TS_STREAM_TYPE_MPA = 0x03; + public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; + public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F; + public static final int TS_STREAM_TYPE_AAC_LATM = 0x11; + public static final int TS_STREAM_TYPE_AC3 = 0x81; + public static final int TS_STREAM_TYPE_DTS = 0x8A; + public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; + public static final int TS_STREAM_TYPE_E_AC3 = 0x87; + public static final int TS_STREAM_TYPE_AC4 = 0xAC; // DVB/ATSC AC-4 Descriptor + public static final int TS_STREAM_TYPE_H262 = 0x02; + public static final int TS_STREAM_TYPE_H264 = 0x1B; + public static final int TS_STREAM_TYPE_H265 = 0x24; + public static final int TS_STREAM_TYPE_ID3 = 0x15; + public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; + public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; + + public static final int TS_PACKET_SIZE = 188; + public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + + private static final int TS_PAT_PID = 0; + private static final int MAX_PID_PLUS_ONE = 0x2000; + + private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33; + private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333; + private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34; + private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643; + + private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; + private static final int SNIFF_TS_PACKET_COUNT = 5; + + private final @Mode int mode; + private final List<TimestampAdjuster> timestampAdjusters; + private final ParsableByteArray tsPacketBuffer; + private final SparseIntArray continuityCounters; + private final TsPayloadReader.Factory payloadReaderFactory; + private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid + private final SparseBooleanArray trackIds; + private final SparseBooleanArray trackPids; + private final TsDurationReader durationReader; + + // Accessed only by the loading thread. + private TsBinarySearchSeeker tsBinarySearchSeeker; + private ExtractorOutput output; + private int remainingPmts; + private boolean tracksEnded; + private boolean hasOutputSeekMap; + private boolean pendingSeekToStart; + private TsPayloadReader id3Reader; + private int bytesSinceLastSync; + private int pcrPid; + + public TsExtractor() { + this(0); + } + + /** + * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} + * {@code FLAG_*} values that control the behavior of the payload readers. + */ + public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { + this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags); + } + + /** + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. + * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} + * {@code FLAG_*} values that control the behavior of the payload readers. + */ + public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) { + this( + mode, + new TimestampAdjuster(0), + new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); + } + + /** + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. + * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param payloadReaderFactory Factory for injecting a custom set of payload readers. + */ + public TsExtractor( + @Mode int mode, + TimestampAdjuster timestampAdjuster, + TsPayloadReader.Factory payloadReaderFactory) { + this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); + this.mode = mode; + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) { + timestampAdjusters = Collections.singletonList(timestampAdjuster); + } else { + timestampAdjusters = new ArrayList<>(); + timestampAdjusters.add(timestampAdjuster); + } + tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0); + trackIds = new SparseBooleanArray(); + trackPids = new SparseBooleanArray(); + tsPayloadReaders = new SparseArray<>(); + continuityCounters = new SparseIntArray(); + durationReader = new TsDurationReader(); + pcrPid = -1; + resetPayloadReaders(); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + byte[] buffer = tsPacketBuffer.data; + input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT); + for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) { + // Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE. + boolean isSyncBytePatternCorrect = true; + for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) { + if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) { + isSyncBytePatternCorrect = false; + break; + } + } + if (isSyncBytePatternCorrect) { + input.skipFully(startPosCandidate); + return true; + } + } + return false; + } + + @Override + public void init(ExtractorOutput output) { + this.output = output; + } + + @Override + public void seek(long position, long timeUs) { + Assertions.checkState(mode != MODE_HLS); + int timestampAdjustersCount = timestampAdjusters.size(); + for (int i = 0; i < timestampAdjustersCount; i++) { + TimestampAdjuster timestampAdjuster = timestampAdjusters.get(i); + boolean hasNotEncounteredFirstTimestamp = + timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET; + if (hasNotEncounteredFirstTimestamp + || (timestampAdjuster.getTimestampOffsetUs() != 0 + && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) { + // - If a track in the TS stream has not encountered any sample, it's going to treat the + // first sample encountered as timestamp 0, which is incorrect. So we have to set the first + // sample timestamp for that track manually. + // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a + // different position, we need to set the first sample timestamp manually again. + timestampAdjuster.reset(); + timestampAdjuster.setFirstSampleTimestampUs(timeUs); + } + } + if (timeUs != 0 && tsBinarySearchSeeker != null) { + tsBinarySearchSeeker.setSeekTargetUs(timeUs); + } + tsPacketBuffer.reset(); + continuityCounters.clear(); + for (int i = 0; i < tsPayloadReaders.size(); i++) { + tsPayloadReaders.valueAt(i).seek(); + } + bytesSinceLastSync = 0; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + if (tracksEnded) { + boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS; + if (canReadDuration && !durationReader.isDurationReadFinished()) { + return durationReader.readDuration(input, seekPosition, pcrPid); + } + maybeOutputSeekMap(inputLength); + + if (pendingSeekToStart) { + pendingSeekToStart = false; + seek(/* position= */ 0, /* timeUs= */ 0); + if (input.getPosition() != 0) { + seekPosition.position = 0; + return RESULT_SEEK; + } + } + + if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) { + return tsBinarySearchSeeker.handlePendingSeek(input, seekPosition); + } + } + + if (!fillBufferWithAtLeastOnePacket(input)) { + return RESULT_END_OF_INPUT; + } + + int endOfPacket = findEndOfFirstTsPacketInBuffer(); + int limit = tsPacketBuffer.limit(); + if (endOfPacket > limit) { + return RESULT_CONTINUE; + } + + @TsPayloadReader.Flags int packetHeaderFlags = 0; + + // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. + int tsPacketHeader = tsPacketBuffer.readInt(); + if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator + // There are uncorrectable errors in this packet. + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } + packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0; + // Ignoring transport_priority (tsPacketHeader & 0x200000) + int pid = (tsPacketHeader & 0x1FFF00) >> 8; + // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0) + boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; + boolean payloadExists = (tsPacketHeader & 0x10) != 0; + + TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null; + if (payloadReader == null) { + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } + + // Discontinuity check. + if (mode != MODE_HLS) { + int continuityCounter = tsPacketHeader & 0xF; + int previousCounter = continuityCounters.get(pid, continuityCounter - 1); + continuityCounters.put(pid, continuityCounter); + if (previousCounter == continuityCounter) { + // Duplicate packet found. + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } else if (continuityCounter != ((previousCounter + 1) & 0xF)) { + // Discontinuity found. + payloadReader.seek(); + } + } + + // Skip the adaptation field. + if (adaptationFieldExists) { + int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); + int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte(); + + packetHeaderFlags |= + (adaptationFieldFlags & 0x40) != 0 // random_access_indicator. + ? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR + : 0; + tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */); + } + + // Read the payload. + boolean wereTracksEnded = tracksEnded; + if (shouldConsumePacketPayload(pid)) { + tsPacketBuffer.setLimit(endOfPacket); + payloadReader.consume(tsPacketBuffer, packetHeaderFlags); + tsPacketBuffer.setLimit(limit); + } + if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) { + // We have read all tracks from all PMTs in this non-live stream. Now seek to the beginning + // and read again to make sure we output all media, including any contained in packets prior + // to those containing the track information. + pendingSeekToStart = true; + } + + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } + + // Internals. + + private void maybeOutputSeekMap(long inputLength) { + if (!hasOutputSeekMap) { + hasOutputSeekMap = true; + if (durationReader.getDurationUs() != C.TIME_UNSET) { + tsBinarySearchSeeker = + new TsBinarySearchSeeker( + durationReader.getPcrTimestampAdjuster(), + durationReader.getDurationUs(), + inputLength, + pcrPid); + output.seekMap(tsBinarySearchSeeker.getSeekMap()); + } else { + output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); + } + } + } + + private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input) + throws IOException, InterruptedException { + byte[] data = tsPacketBuffer.data; + // Shift bytes to the start of the buffer if there isn't enough space left at the end. + if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) { + int bytesLeft = tsPacketBuffer.bytesLeft(); + if (bytesLeft > 0) { + System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft); + } + tsPacketBuffer.reset(data, bytesLeft); + } + // Read more bytes until we have at least one packet. + while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) { + int limit = tsPacketBuffer.limit(); + int read = input.read(data, limit, BUFFER_SIZE - limit); + if (read == C.RESULT_END_OF_INPUT) { + return false; + } + tsPacketBuffer.setLimit(limit + read); + } + return true; + } + + /** + * Returns the position of the end of the first TS packet (exclusive) in the packet buffer. + * + * <p>This may be a position beyond the buffer limit if the packet has not been read fully into + * the buffer, or if no packet could be found within the buffer. + */ + private int findEndOfFirstTsPacketInBuffer() throws ParserException { + int searchStart = tsPacketBuffer.getPosition(); + int limit = tsPacketBuffer.limit(); + int syncBytePosition = TsUtil.findSyncBytePosition(tsPacketBuffer.data, searchStart, limit); + // Discard all bytes before the sync byte. + // If sync byte is not found, this means discard the whole buffer. + tsPacketBuffer.setPosition(syncBytePosition); + int endOfPacket = syncBytePosition + TS_PACKET_SIZE; + if (endOfPacket > limit) { + bytesSinceLastSync += syncBytePosition - searchStart; + if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) { + throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream."); + } + } else { + // We have found a packet within the buffer. + bytesSinceLastSync = 0; + } + return endOfPacket; + } + + private boolean shouldConsumePacketPayload(int packetPid) { + return mode == MODE_HLS + || tracksEnded + || !trackPids.get(packetPid, /* valueIfKeyNotFound= */ false); // It's a PSI packet + } + + private void resetPayloadReaders() { + trackIds.clear(); + tsPayloadReaders.clear(); + SparseArray<TsPayloadReader> initialPayloadReaders = + payloadReaderFactory.createInitialPayloadReaders(); + int initialPayloadReadersSize = initialPayloadReaders.size(); + for (int i = 0; i < initialPayloadReadersSize; i++) { + tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i)); + } + tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader())); + id3Reader = null; + } + + /** + * Parses Program Association Table data. + */ + private class PatReader implements SectionPayloadReader { + + private final ParsableBitArray patScratch; + + public PatReader() { + patScratch = new ParsableBitArray(new byte[4]); + } + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + + @Override + public void consume(ParsableByteArray sectionData) { + int tableId = sectionData.readUnsignedByte(); + if (tableId != 0x00 /* program_association_section */) { + // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. + return; + } + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), + // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8) + sectionData.skipBytes(7); + + int programCount = sectionData.bytesLeft() / 4; + for (int i = 0; i < programCount; i++) { + sectionData.readBytes(patScratch, 4); + int programNumber = patScratch.readBits(16); + patScratch.skipBits(3); // reserved (3) + if (programNumber == 0) { + patScratch.skipBits(13); // network_PID (13) + } else { + int pid = patScratch.readBits(13); + tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); + remainingPmts++; + } + } + if (mode != MODE_HLS) { + tsPayloadReaders.remove(TS_PAT_PID); + } + } + + } + + /** + * Parses Program Map Table. + */ + private class PmtReader implements SectionPayloadReader { + + private static final int TS_PMT_DESC_REGISTRATION = 0x05; + private static final int TS_PMT_DESC_ISO639_LANG = 0x0A; + private static final int TS_PMT_DESC_AC3 = 0x6A; + private static final int TS_PMT_DESC_EAC3 = 0x7A; + private static final int TS_PMT_DESC_DTS = 0x7B; + private static final int TS_PMT_DESC_DVB_EXT = 0x7F; + private static final int TS_PMT_DESC_DVBSUBS = 0x59; + + private static final int TS_PMT_DESC_DVB_EXT_AC4 = 0x15; + + private final ParsableBitArray pmtScratch; + private final SparseArray<TsPayloadReader> trackIdToReaderScratch; + private final SparseIntArray trackIdToPidScratch; + private final int pid; + + public PmtReader(int pid) { + pmtScratch = new ParsableBitArray(new byte[5]); + trackIdToReaderScratch = new SparseArray<>(); + trackIdToPidScratch = new SparseIntArray(); + this.pid = pid; + } + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator) { + // Do nothing. + } + + @Override + public void consume(ParsableByteArray sectionData) { + int tableId = sectionData.readUnsignedByte(); + if (tableId != 0x02 /* TS_program_map_section */) { + // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. + return; + } + // TimestampAdjuster assignment. + TimestampAdjuster timestampAdjuster; + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) { + timestampAdjuster = timestampAdjusters.get(0); + } else { + timestampAdjuster = new TimestampAdjuster( + timestampAdjusters.get(0).getFirstSampleTimestampUs()); + timestampAdjusters.add(timestampAdjuster); + } + + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) + sectionData.skipBytes(2); + int programNumber = sectionData.readUnsignedShort(); + + // Skip 3 bytes (24 bits), including: + // reserved (2), version_number (5), current_next_indicator (1), section_number (8), + // last_section_number (8) + sectionData.skipBytes(3); + + sectionData.readBytes(pmtScratch, 2); + // reserved (3), PCR_PID (13) + pmtScratch.skipBits(3); + pcrPid = pmtScratch.readBits(13); + + // Read program_info_length. + sectionData.readBytes(pmtScratch, 2); + pmtScratch.skipBits(4); + int programInfoLength = pmtScratch.readBits(12); + + // Skip the descriptors. + sectionData.skipBytes(programInfoLength); + + if (mode == MODE_HLS && id3Reader == null) { + // Setup an ID3 track regardless of whether there's a corresponding entry, in case one + // appears intermittently during playback. See [Internal: b/20261500]. + EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, Util.EMPTY_BYTE_ARRAY); + id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); + id3Reader.init(timestampAdjuster, output, + new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + } + + trackIdToReaderScratch.clear(); + trackIdToPidScratch.clear(); + int remainingEntriesLength = sectionData.bytesLeft(); + while (remainingEntriesLength > 0) { + sectionData.readBytes(pmtScratch, 5); + int streamType = pmtScratch.readBits(8); + pmtScratch.skipBits(3); // reserved + int elementaryPid = pmtScratch.readBits(13); + pmtScratch.skipBits(4); // reserved + int esInfoLength = pmtScratch.readBits(12); // ES_info_length. + EsInfo esInfo = readEsInfo(sectionData, esInfoLength); + if (streamType == 0x06) { + streamType = esInfo.streamType; + } + remainingEntriesLength -= esInfoLength + 5; + + int trackId = mode == MODE_HLS ? streamType : elementaryPid; + if (trackIds.get(trackId)) { + continue; + } + + TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); + if (mode != MODE_HLS + || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { + trackIdToPidScratch.put(trackId, elementaryPid); + trackIdToReaderScratch.put(trackId, reader); + } + } + + int trackIdCount = trackIdToPidScratch.size(); + for (int i = 0; i < trackIdCount; i++) { + int trackId = trackIdToPidScratch.keyAt(i); + int trackPid = trackIdToPidScratch.valueAt(i); + trackIds.put(trackId, true); + trackPids.put(trackPid, true); + TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + if (reader != null) { + if (reader != id3Reader) { + reader.init(timestampAdjuster, output, + new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); + } + tsPayloadReaders.put(trackPid, reader); + } + } + + if (mode == MODE_HLS) { + if (!tracksEnded) { + output.endTracks(); + remainingPmts = 0; + tracksEnded = true; + } + } else { + tsPayloadReaders.remove(pid); + remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1; + if (remainingPmts == 0) { + output.endTracks(); + tracksEnded = true; + } + } + } + + /** + * Returns the stream info read from the available descriptors. Sets {@code data}'s position to + * the end of the descriptors. + * + * @param data A buffer with its position set to the start of the first descriptor. + * @param length The length of descriptors to read from the current position in {@code data}. + * @return The stream info read from the available descriptors. + */ + private EsInfo readEsInfo(ParsableByteArray data, int length) { + int descriptorsStartPosition = data.getPosition(); + int descriptorsEndPosition = descriptorsStartPosition + length; + int streamType = -1; + String language = null; + List<DvbSubtitleInfo> dvbSubtitleInfos = null; + while (data.getPosition() < descriptorsEndPosition) { + int descriptorTag = data.readUnsignedByte(); + int descriptorLength = data.readUnsignedByte(); + int positionOfNextDescriptor = data.getPosition() + descriptorLength; + if (descriptorTag == TS_PMT_DESC_REGISTRATION) { // registration_descriptor + long formatIdentifier = data.readUnsignedInt(); + if (formatIdentifier == AC3_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_AC3; + } else if (formatIdentifier == E_AC3_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_E_AC3; + } else if (formatIdentifier == AC4_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_AC4; + } else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_H265; + } + } else if (descriptorTag == TS_PMT_DESC_AC3) { // AC-3_descriptor in DVB (ETSI EN 300 468) + streamType = TS_STREAM_TYPE_AC3; + } else if (descriptorTag == TS_PMT_DESC_EAC3) { // enhanced_AC-3_descriptor + streamType = TS_STREAM_TYPE_E_AC3; + } else if (descriptorTag == TS_PMT_DESC_DVB_EXT) { + // Extension descriptor in DVB (ETSI EN 300 468). + int descriptorTagExt = data.readUnsignedByte(); + if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_AC4) { + // AC-4_descriptor in DVB (ETSI EN 300 468). + streamType = TS_STREAM_TYPE_AC4; + } + } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor + streamType = TS_STREAM_TYPE_DTS; + } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) { + language = data.readString(3).trim(); + // Audio type is ignored. + } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) { + streamType = TS_STREAM_TYPE_DVBSUBS; + dvbSubtitleInfos = new ArrayList<>(); + while (data.getPosition() < positionOfNextDescriptor) { + String dvbLanguage = data.readString(3).trim(); + int dvbSubtitlingType = data.readUnsignedByte(); + byte[] initializationData = new byte[4]; + data.readBytes(initializationData, 0, 4); + dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType, + initializationData)); + } + } + // Skip unused bytes of current descriptor. + data.skipBytes(positionOfNextDescriptor - data.getPosition()); + } + data.setPosition(descriptorsEndPosition); + return new EsInfo(streamType, language, dvbSubtitleInfos, + Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition)); + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java new file mode 100644 index 0000000000..940c1c7937 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import android.util.SparseArray; +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; + +/** + * Parses TS packet payload data. + */ +public interface TsPayloadReader { + + /** + * Factory of {@link TsPayloadReader} instances. + */ + interface Factory { + + /** + * Returns the initial mapping from PIDs to payload readers. + * <p> + * This method allows the injection of payload readers for reserved PIDs, excluding PID 0. + * + * @return A {@link SparseArray} that maps PIDs to payload readers. + */ + SparseArray<TsPayloadReader> createInitialPayloadReaders(); + + /** + * Returns a {@link TsPayloadReader} for a given stream type and elementary stream information. + * May return null if the stream type is not supported. + * + * @param streamType Stream type value as defined in the PMT entry or associated descriptors. + * @param esInfo Information associated to the elementary stream provided in the PMT. + * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid. + * {@code null} if the stream is not supported. + */ + TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo); + + } + + /** + * Holds information associated with a PMT entry. + */ + final class EsInfo { + + public final int streamType; + public final String language; + public final List<DvbSubtitleInfo> dvbSubtitleInfos; + public final byte[] descriptorBytes; + + /** + * @param streamType The type of the stream as defined by the + * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. + * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. + * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. + * @param descriptorBytes The descriptor bytes associated to the stream. + */ + public EsInfo(int streamType, String language, List<DvbSubtitleInfo> dvbSubtitleInfos, + byte[] descriptorBytes) { + this.streamType = streamType; + this.language = language; + this.dvbSubtitleInfos = + dvbSubtitleInfos == null + ? Collections.emptyList() + : Collections.unmodifiableList(dvbSubtitleInfos); + this.descriptorBytes = descriptorBytes; + } + + } + + /** + * Holds information about a DVB subtitle, as defined in ETSI EN 300 468 V1.11.1 section 6.2.41. + */ + final class DvbSubtitleInfo { + + public final String language; + public final int type; + public final byte[] initializationData; + + /** + * @param language The ISO 639-2 three-letter language code. + * @param type The subtitling type. + * @param initializationData The composition and ancillary page ids. + */ + public DvbSubtitleInfo(String language, int type, byte[] initializationData) { + this.language = language; + this.type = type; + this.initializationData = initializationData; + } + + } + + /** + * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s. + */ + final class TrackIdGenerator { + + private static final int ID_UNSET = Integer.MIN_VALUE; + + private final String formatIdPrefix; + private final int firstTrackId; + private final int trackIdIncrement; + private int trackId; + private String formatId; + + public TrackIdGenerator(int firstTrackId, int trackIdIncrement) { + this(ID_UNSET, firstTrackId, trackIdIncrement); + } + + public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) { + this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : ""; + this.firstTrackId = firstTrackId; + this.trackIdIncrement = trackIdIncrement; + trackId = ID_UNSET; + } + + /** + * Generates a new set of track and track format ids. Must be called before {@code get*} + * methods. + */ + public void generateNewId() { + trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement; + formatId = formatIdPrefix + trackId; + } + + /** + * Returns the last generated track id. Must be called after the first {@link #generateNewId()} + * call. + * + * @return The last generated track id. + */ + public int getTrackId() { + maybeThrowUninitializedError(); + return trackId; + } + + /** + * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be + * called after the first {@link #generateNewId()} call. + * + * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as + * format id. + */ + public String getFormatId() { + maybeThrowUninitializedError(); + return formatId; + } + + private void maybeThrowUninitializedError() { + if (trackId == ID_UNSET) { + throw new IllegalStateException("generateNewId() must be called before retrieving ids."); + } + } + + } + + /** + * Contextual flags indicating the presence of indicators in the TS packet or PES packet headers. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FLAG_PAYLOAD_UNIT_START_INDICATOR, + FLAG_RANDOM_ACCESS_INDICATOR, + FLAG_DATA_ALIGNMENT_INDICATOR + }) + @interface Flags {} + + /** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */ + int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1; + /** + * Indicates the presence of the random_access_indicator in the TS packet header adaptation field. + */ + int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1; + /** Indicates the presence of the data_alignment_indicator in the PES header. */ + int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2; + + /** + * Initializes the payload reader. + * + * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data. + * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the + * {@link TrackOutput}s. + */ + void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + TrackIdGenerator idGenerator); + + /** + * Notifies the reader that a seek has occurred. + * + * <p>Following a call to this method, the data passed to the next invocation of {@link #consume} + * will not be a continuation of the data that was previously passed. Hence the reader should + * reset any internal state. + */ + void seek(); + + /** + * Consumes the payload of a TS packet. + * + * @param data The TS packet. The position will be set to the start of the payload. + * @param flags See {@link Flags}. + * @throws ParserException If the payload could not be parsed. + */ + void consume(ParsableByteArray data, @Flags int flags) throws ParserException; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java new file mode 100644 index 0000000000..8cd24ff1e9 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** Utilities method for extracting MPEG-TS streams. */ +public final class TsUtil { + /** + * Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition) + * from the provided data array, or returns limitPosition if sync byte could not be found. + */ + public static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) { + int position = startPosition; + while (position < limitPosition && data[position] != TsExtractor.TS_SYNC_BYTE) { + position++; + } + return position; + } + + /** + * Returns the PCR value read from a given TS packet. + * + * @param packetBuffer The buffer that holds the packet. + * @param startOfPacket The starting position of the packet in the buffer. + * @param pcrPid The PID for valid packets that contain PCR values. + * @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it + * contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise. + */ + public static long readPcrFromPacket( + ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) { + packetBuffer.setPosition(startOfPacket); + if (packetBuffer.bytesLeft() < 5) { + // Header = 4 bytes, adaptationFieldLength = 1 byte. + return C.TIME_UNSET; + } + // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. + int tsPacketHeader = packetBuffer.readInt(); + if ((tsPacketHeader & 0x800000) != 0) { + // transport_error_indicator != 0 means there are uncorrectable errors in this packet. + return C.TIME_UNSET; + } + int pid = (tsPacketHeader & 0x1FFF00) >> 8; + if (pid != pcrPid) { + return C.TIME_UNSET; + } + boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; + if (!adaptationFieldExists) { + return C.TIME_UNSET; + } + + int adaptationFieldLength = packetBuffer.readUnsignedByte(); + if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) { + int flags = packetBuffer.readUnsignedByte(); + boolean pcrFlagSet = (flags & 0x10) == 0x10; + if (pcrFlagSet) { + byte[] pcrBytes = new byte[6]; + packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length); + return readPcrValueFromPcrBytes(pcrBytes); + } + } + return C.TIME_UNSET; + } + + /** + * Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes. + * + * <p>We ignore PCR Ext, because it's too small to have any significance. + */ + private static long readPcrValueFromPcrBytes(byte[] pcrBytes) { + return (pcrBytes[0] & 0xFFL) << 25 + | (pcrBytes[1] & 0xFFL) << 17 + | (pcrBytes[2] & 0xFFL) << 9 + | (pcrBytes[3] & 0xFFL) << 1 + | (pcrBytes[4] & 0xFFL) >> 7; + } + + private TsUtil() { + // Prevent instantiation. + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java new file mode 100644 index 0000000000..fb56fe379c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.List; + +/** Consumes user data, outputting contained CEA-608/708 messages to a {@link TrackOutput}. */ +/* package */ final class UserDataReader { + + private static final int USER_DATA_START_CODE = 0x0001B2; + + private final List<Format> closedCaptionFormats; + private final TrackOutput[] outputs; + + public UserDataReader(List<Format> closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; + } + + public void createTracks( + ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + Format channelFormat = closedCaptionFormats.get(i); + String channelMimeType = channelFormat.sampleMimeType; + Assertions.checkArgument( + MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); + output.format( + Format.createTextSampleFormat( + idGenerator.getFormatId(), + channelMimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + channelFormat.selectionFlags, + channelFormat.language, + channelFormat.accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + channelFormat.initializationData)); + outputs[i] = output; + } + } + + public void consume(long pesTimeUs, ParsableByteArray userDataPayload) { + if (userDataPayload.bytesLeft() < 9) { + return; + } + int userDataStartCode = userDataPayload.readInt(); + int userDataIdentifier = userDataPayload.readInt(); + int userDataTypeCode = userDataPayload.readUnsignedByte(); + if (userDataStartCode == USER_DATA_START_CODE + && userDataIdentifier == CeaUtil.USER_DATA_IDENTIFIER_GA94 + && userDataTypeCode == CeaUtil.USER_DATA_TYPE_CODE_MPEG_CC) { + CeaUtil.consumeCcData(pesTimeUs, userDataPayload, outputs); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java new file mode 100644 index 0000000000..d4ac3ef8e1 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; + +import android.util.Pair; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.WavUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Extracts data from WAV byte streams. + */ +public final class WavExtractor implements Extractor { + + /** + * When outputting PCM data to a {@link TrackOutput}, we can choose how many frames are grouped + * into each sample, and hence each sample's duration. This is the target number of samples to + * output for each second of media, meaning that each sample will have a duration of ~100ms. + */ + private static final int TARGET_SAMPLES_PER_SECOND = 10; + + /** Factory for {@link WavExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; + + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private OutputWriter outputWriter; + private int dataStartPosition; + private long dataEndPosition; + + public WavExtractor() { + dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return WavHeaderReader.peek(input) != null; + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + } + + @Override + public void seek(long position, long timeUs) { + if (outputWriter != null) { + outputWriter.reset(timeUs); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + assertInitialized(); + if (outputWriter == null) { + WavHeader header = WavHeaderReader.peek(input); + if (header == null) { + // Should only happen if the media wasn't sniffed. + throw new ParserException("Unsupported or unrecognized wav header."); + } + + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else if (header.formatType == WavUtil.TYPE_ALAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_ALAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else if (header.formatType == WavUtil.TYPE_MLAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_MLAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = + new PassthroughOutputWriter( + extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); + } + } + + if (dataStartPosition == C.POSITION_UNSET) { + Pair<Long, Long> dataBounds = WavHeaderReader.skipToData(input); + dataStartPosition = dataBounds.first.intValue(); + dataEndPosition = dataBounds.second; + outputWriter.init(dataStartPosition, dataEndPosition); + } else if (input.getPosition() == 0) { + input.skipFully(dataStartPosition); + } + + Assertions.checkState(dataEndPosition != C.POSITION_UNSET); + long bytesLeft = dataEndPosition - input.getPosition(); + return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + } + + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + + /** Writes to the extractor's output. */ + private interface OutputWriter { + + /** + * Resets the writer. + * + * @param timeUs The new start position in microseconds. + */ + void reset(long timeUs); + + /** + * Initializes the writer. + * + * <p>Must be called once, before any calls to {@link #sampleData(ExtractorInput, long)}. + * + * @param dataStartPosition The byte position (inclusive) in the stream at which data starts. + * @param dataEndPosition The end position (exclusive) in the stream at which data ends. + * @throws ParserException If an error occurs initializing the writer. + */ + void init(int dataStartPosition, long dataEndPosition) throws ParserException; + + /** + * Consumes sample data from {@code input}, writing corresponding samples to the extractor's + * output. + * + * <p>Must not be called until after {@link #init(int, long)} has been called. + * + * @param input The input from which to read. + * @param bytesLeft The number of sample data bytes left to be read from the input. + * @return Whether the end of the sample data has been reached. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException; + } + + private static final class PassthroughOutputWriter implements OutputWriter { + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + private final Format format; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public PassthroughOutputWriter( + ExtractorOutput extractorOutput, + TrackOutput trackOutput, + WavHeader header, + String mimeType, + @C.PcmEncoding int pcmEncoding) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + + int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; + // Validate the header. Blocks are expected to correspond to single frames. + if (header.blockSize != bytesPerFrame) { + throw new ParserException( + "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); + } + + targetSampleSizeBytes = + Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); + format = + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, + /* maxInputSize= */ targetSampleSizeBytes, + header.numChannels, + header.frameRateHz, + pcmEncoding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } + + @Override + public void reset(long timeUs) { + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Write sample data until we've reached the target sample size, or the end of the data. + while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); + int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); + if (bytesAppended == RESULT_END_OF_INPUT) { + bytesLeft = 0; + } else { + pendingOutputBytes += bytesAppended; + bytesLeft -= bytesAppended; + } + } + + // Write the corresponding sample metadata. Samples must be a whole number of frames. It's + // possible that the number of pending output bytes is not a whole number of frames if the + // stream ended unexpectedly. + int bytesPerFrame = header.blockSize; + int pendingFrames = pendingOutputBytes / bytesPerFrame; + if (pendingFrames > 0) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp( + outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = pendingFrames * bytesPerFrame; + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += pendingFrames; + pendingOutputBytes = offset; + } + + return bytesLeft <= 0; + } + } + + private static final class ImaAdPcmOutputWriter implements OutputWriter { + + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + + /** Number of frames per block of the input (yet to be decoded) data. */ + private final int framesPerBlock; + /** Target for the input (yet to be decoded) data. */ + private final byte[] inputData; + /** Target for decoded (yet to be output) data. */ + private final ParsableByteArray decodedData; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + /** The output format. */ + private final Format format; + + /** The number of pending bytes in {@link #inputData}. */ + private int pendingInputBytes; + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + + int numChannels = header.numChannels; + // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update + // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI + // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // Calculate the number of blocks we'll need to decode to obtain an output sample of the + // target sample size, and allocate suitably sized buffers for input and decoded data. + int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); + inputData = new byte[maxBlocksToDecode * header.blockSize]; + decodedData = + new ParsableByteArray( + maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); + + // Create the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + format = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + bitrate, + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels), + header.numChannels, + header.frameRateHz, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } + + @Override + public void reset(long timeUs) { + pendingInputBytes = 0; + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); + // Calculate the whole number of blocks that we need to decode to obtain this many frames. + int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); + int targetReadBytes = blocksToDecode * header.blockSize; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.numChannels; + + // The input data consists for a four byte header [Ci] for each of the N channels, followed + // by interleaved data segments [Ci-DATAj], each of which are four bytes long. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return numOutputFramesToBytes(frames, header.numChannels); + } + + private static int numOutputFramesToBytes(int frames, int numChannels) { + return frames * 2 * numChannels; + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java new file mode 100644 index 0000000000..bc6cf8999b --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; + +/** Header for a WAV file. */ +/* package */ final class WavHeader { + + /** + * The format type. Standard format types are the "WAVE form Registration Number" constants + * defined in RFC 2361 Appendix A. + */ + public final int formatType; + /** The number of channels. */ + public final int numChannels; + /** The sample rate in Hertz. */ + public final int frameRateHz; + /** The average bytes per second for the sample data. */ + public final int averageBytesPerSecond; + /** The block size in bytes. */ + public final int blockSize; + /** Bits per sample for a single channel. */ + public final int bitsPerSample; + /** Extra data appended to the format chunk of the header. */ + public final byte[] extraData; + + public WavHeader( + int formatType, + int numChannels, + int frameRateHz, + int averageBytesPerSecond, + int blockSize, + int bitsPerSample, + byte[] extraData) { + this.formatType = formatType; + this.numChannels = numChannels; + this.frameRateHz = frameRateHz; + this.averageBytesPerSecond = averageBytesPerSecond; + this.blockSize = blockSize; + this.bitsPerSample = bitsPerSample; + this.extraData = extraData; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java new file mode 100644 index 0000000000..1c36aaa3c3 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.WavUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ +/* package */ final class WavHeaderReader { + + private static final String TAG = "WavHeaderReader"; + + /** + * Peeks and returns a {@code WavHeader}. + * + * @param input Input stream to peek the WAV header from. + * @throws ParserException If the input file is an incorrect RIFF WAV. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a + * supported WAV format. + */ + @Nullable + public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException { + Assertions.checkNotNull(input); + + // Allocate a scratch buffer large enough to store the format chunk. + ParsableByteArray scratch = new ParsableByteArray(16); + + // Attempt to read the RIFF chunk. + ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + if (chunkHeader.id != WavUtil.RIFF_FOURCC) { + return null; + } + + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + int riffFormat = scratch.readInt(); + if (riffFormat != WavUtil.WAVE_FOURCC) { + Log.e(TAG, "Unsupported RIFF format: " + riffFormat); + return null; + } + + // Skip chunks until we find the format chunk. + chunkHeader = ChunkHeader.peek(input, scratch); + while (chunkHeader.id != WavUtil.FMT_FOURCC) { + input.advancePeekPosition((int) chunkHeader.size); + chunkHeader = ChunkHeader.peek(input, scratch); + } + + Assertions.checkState(chunkHeader.size >= 16); + input.peekFully(scratch.data, 0, 16); + scratch.setPosition(0); + int audioFormatType = scratch.readLittleEndianUnsignedShort(); + int numChannels = scratch.readLittleEndianUnsignedShort(); + int frameRateHz = scratch.readLittleEndianUnsignedIntToInt(); + int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt(); + int blockSize = scratch.readLittleEndianUnsignedShort(); + int bitsPerSample = scratch.readLittleEndianUnsignedShort(); + + int bytesLeft = (int) chunkHeader.size - 16; + byte[] extraData; + if (bytesLeft > 0) { + extraData = new byte[bytesLeft]; + input.peekFully(extraData, 0, bytesLeft); + } else { + extraData = Util.EMPTY_BYTE_ARRAY; + } + + return new WavHeader( + audioFormatType, + numChannels, + frameRateHz, + averageBytesPerSecond, + blockSize, + bitsPerSample, + extraData); + } + + /** + * Skips to the data in the given WAV input stream, and returns its bounds. After calling, the + * input stream's position will point to the start of sample data in the WAV. If an exception is + * thrown, the input position will be left pointing to a chunk header. + * + * @param input The input stream, whose read position must be pointing to a valid chunk header. + * @return The byte positions at which the data starts (inclusive) and ends (exclusive). + * @throws ParserException If an error occurs parsing chunks. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from input. + */ + public static Pair<Long, Long> skipToData(ExtractorInput input) + throws IOException, InterruptedException { + Assertions.checkNotNull(input); + + // Make sure the peek position is set to the read position before we peek the first header. + input.resetPeekPosition(); + + ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); + // Skip all chunks until we hit the data header. + ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + while (chunkHeader.id != WavUtil.DATA_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + } + long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; + // Override size of RIFF chunk, since it describes its size as the entire file. + if (chunkHeader.id == WavUtil.RIFF_FOURCC) { + bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; + } + if (bytesToSkip > Integer.MAX_VALUE) { + throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id); + } + input.skipFully((int) bytesToSkip); + chunkHeader = ChunkHeader.peek(input, scratch); + } + // Skip past the "data" header. + input.skipFully(ChunkHeader.SIZE_IN_BYTES); + + long dataStartPosition = input.getPosition(); + long dataEndPosition = dataStartPosition + chunkHeader.size; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } + return Pair.create(dataStartPosition, dataEndPosition); + } + + private WavHeaderReader() { + // Prevent instantiation. + } + + /** Container for a WAV chunk header. */ + private static final class ChunkHeader { + + /** Size in bytes of a WAV chunk header. */ + public static final int SIZE_IN_BYTES = 8; + + /** 4-character identifier, stored as an integer, for this chunk. */ + public final int id; + /** Size of this chunk in bytes. */ + public final long size; + + private ChunkHeader(int id, long size) { + this.id = id; + this.size = size; + } + + /** + * Peeks and returns a {@link ChunkHeader}. + * + * @param input Input stream to peek the chunk header from. + * @param scratch Buffer for temporary use. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + * @return A new {@code ChunkHeader} peeked from {@code input}. + */ + public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch) + throws IOException, InterruptedException { + input.peekFully(scratch.data, /* offset= */ 0, /* length= */ SIZE_IN_BYTES); + scratch.setPosition(0); + + int id = scratch.readInt(); + long size = scratch.readLittleEndianUnsignedInt(); + + return new ChunkHeader(id, size); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java new file mode 100644 index 0000000000..d14268d120 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/* package */ final class WavSeekMap implements SeekMap { + + private final WavHeader wavHeader; + private final int framesPerBlock; + private final long firstBlockPosition; + private final long blockCount; + private final long durationUs; + + public WavSeekMap( + WavHeader wavHeader, int framesPerBlock, long dataStartPosition, long dataEndPosition) { + this.wavHeader = wavHeader; + this.framesPerBlock = framesPerBlock; + this.firstBlockPosition = dataStartPosition; + this.blockCount = (dataEndPosition - dataStartPosition) / wavHeader.blockSize; + durationUs = blockIndexToTimeUs(blockCount); + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + // Calculate the containing block index, constraining to valid indices. + long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); + blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); + + long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); + long seekTimeUs = blockIndexToTimeUs(blockIndex); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { + return new SeekPoints(seekPoint); + } else { + long secondBlockIndex = blockIndex + 1; + long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); + long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private long blockIndexToTimeUs(long blockIndex) { + return Util.scaleLargeTimestamp( + blockIndex * framesPerBlock, C.MICROS_PER_SECOND, wavHeader.frameRateHz); + } +} |