diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk')
15 files changed, 2296 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java new file mode 100644 index 0000000000..406cd1617a --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -0,0 +1,100 @@ +/* + * 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.source.chunk; + +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.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; + +/** + * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}. + */ +public abstract class BaseMediaChunk extends MediaChunk { + + /** + * The time from which output will begin, or {@link C#TIME_UNSET} if output will begin from the + * start of the chunk. + */ + public final long clippedStartTimeUs; + /** + * The time from which output will end, or {@link C#TIME_UNSET} if output will end at the end of + * the chunk. + */ + public final long clippedEndTimeUs; + + private BaseMediaChunkOutput output; + private int[] firstSampleIndices; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link + * C#TIME_UNSET} to output from the start of the chunk. + * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link + * C#TIME_UNSET} to output to the end of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. + */ + public BaseMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long clippedStartTimeUs, + long clippedEndTimeUs, + long chunkIndex) { + super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, + endTimeUs, chunkIndex); + this.clippedStartTimeUs = clippedStartTimeUs; + this.clippedEndTimeUs = clippedEndTimeUs; + } + + /** + * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive + * samples as they are loaded. + * + * @param output The output that will receive the loaded media samples. + */ + public void init(BaseMediaChunkOutput output) { + this.output = output; + firstSampleIndices = output.getWriteIndices(); + } + + /** + * Returns the index of the first sample in the specified track of the output that will originate + * from this chunk. + */ + public final int getFirstSampleIndex(int trackIndex) { + return firstSampleIndices[trackIndex]; + } + + /** + * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}. + */ + protected final BaseMediaChunkOutput getOutput() { + return output; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java new file mode 100644 index 0000000000..3987260578 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java @@ -0,0 +1,75 @@ +/* + * 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.source.chunk; + +import java.util.NoSuchElementException; + +/** + * Base class for {@link MediaChunkIterator}s. Handles {@link #next()} and {@link #isEnded()}, and + * provides a bounds check for child classes. + */ +public abstract class BaseMediaChunkIterator implements MediaChunkIterator { + + private final long fromIndex; + private final long toIndex; + + private long currentIndex; + + /** + * Creates base iterator. + * + * @param fromIndex The first available index. + * @param toIndex The last available index. + */ + @SuppressWarnings("method.invocation.invalid") + public BaseMediaChunkIterator(long fromIndex, long toIndex) { + this.fromIndex = fromIndex; + this.toIndex = toIndex; + reset(); + } + + @Override + public boolean isEnded() { + return currentIndex > toIndex; + } + + @Override + public boolean next() { + currentIndex++; + return !isEnded(); + } + + @Override + public void reset() { + currentIndex = fromIndex - 1; + } + + /** + * Verifies that the iterator points to a valid element. + * + * @throws NoSuchElementException If the iterator does not point to a valid element. + */ + protected final void checkInBounds() { + if (currentIndex < fromIndex || currentIndex > toIndex) { + throw new NoSuchElementException(); + } + } + + /** Returns the current index this iterator is pointing to. */ + protected final long getCurrentIndex() { + return currentIndex; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java new file mode 100644 index 0000000000..5d1f93bf01 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -0,0 +1,80 @@ +/* + * 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.source.chunk; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; + +/** + * A {@link TrackOutputProvider} that provides {@link TrackOutput TrackOutputs} based on a + * predefined mapping from track type to output. + */ +public final class BaseMediaChunkOutput implements TrackOutputProvider { + + private static final String TAG = "BaseMediaChunkOutput"; + + private final int[] trackTypes; + private final SampleQueue[] sampleQueues; + + /** + * @param trackTypes The track types of the individual track outputs. + * @param sampleQueues The individual sample queues. + */ + public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { + this.trackTypes = trackTypes; + this.sampleQueues = sampleQueues; + } + + @Override + public TrackOutput track(int id, int type) { + for (int i = 0; i < trackTypes.length; i++) { + if (type == trackTypes[i]) { + return sampleQueues[i]; + } + } + Log.e(TAG, "Unmatched track of type: " + type); + return new DummyTrackOutput(); + } + + /** + * Returns the current absolute write indices of the individual sample queues. + */ + public int[] getWriteIndices() { + int[] writeIndices = new int[sampleQueues.length]; + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueues[i] != null) { + writeIndices[i] = sampleQueues[i].getWriteIndex(); + } + } + return writeIndices; + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently written to the sample queues. + */ + public void setSampleOffsetUs(long sampleOffsetUs) { + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue != null) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); + } + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java new file mode 100644 index 0000000000..3f4450eddd --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -0,0 +1,137 @@ +/* + * 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.source.chunk; + +import android.net.Uri; +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.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.StatsDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.List; +import java.util.Map; + +/** + * An abstract base class for {@link Loadable} implementations that load chunks of data required + * for the playback of streams. + */ +public abstract class Chunk implements Loadable { + + /** + * The {@link DataSpec} that defines the data to be loaded. + */ + public final DataSpec dataSpec; + /** + * The type of the chunk. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For + * reporting only. + */ + public final int type; + /** + * The format of the track to which this chunk belongs, or null if the chunk does not belong to + * a track. + */ + public final Format trackFormat; + /** + * One of the {@link C} {@code SELECTION_REASON_*} constants if the chunk belongs to a track. + * {@link C#SELECTION_REASON_UNKNOWN} if the chunk does not belong to a track. + */ + public final int trackSelectionReason; + /** + * Optional data associated with the selection of the track to which this chunk belongs. Null if + * the chunk does not belong to a track. + */ + @Nullable public final Object trackSelectionData; + /** + * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data + * being loaded does not contain media samples. + */ + public final long startTimeUs; + /** + * The end time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data being + * loaded does not contain media samples. + */ + public final long endTimeUs; + + protected final StatsDataSource dataSource; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. + * @param type See {@link #type}. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param startTimeUs See {@link #startTimeUs}. + * @param endTimeUs See {@link #endTimeUs}. + */ + public Chunk( + DataSource dataSource, + DataSpec dataSpec, + int type, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs) { + this.dataSource = new StatsDataSource(dataSource); + this.dataSpec = Assertions.checkNotNull(dataSpec); + this.type = type; + this.trackFormat = trackFormat; + this.trackSelectionReason = trackSelectionReason; + this.trackSelectionData = trackSelectionData; + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; + } + + /** + * Returns the duration of the chunk in microseconds. + */ + public final long getDurationUs() { + return endTimeUs - startTimeUs; + } + + /** + * Returns the number of bytes that have been loaded. Must only be called after the load + * completed, failed, or was canceled. + */ + public final long bytesLoaded() { + return dataSource.getBytesRead(); + } + + /** + * Returns the {@link Uri} associated with the last {@link DataSource#open} call. If redirection + * occurred, this is the redirected uri. Must only be called after the load completed, failed, or + * was canceled. + * + * @see DataSource#getUri() + */ + public final Uri getUri() { + return dataSource.getLastOpenedUri(); + } + + /** + * Returns the response headers associated with the last {@link DataSource#open} call. Must only + * be called after the load completed, failed, or was canceled. + * + * @see DataSource#getResponseHeaders() + */ + public final Map<String, List<String>> getResponseHeaders() { + return dataSource.getLastResponseHeaders(); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java new file mode 100644 index 0000000000..04cef9198c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -0,0 +1,220 @@ +/* + * 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.source.chunk; + +import android.util.SparseArray; +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.extractor.DummyTrackOutput; +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.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.ParsableByteArray; +import java.io.IOException; + +/** + * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly + * additional embedded tracks. + * <p> + * The wrapper allows switching of the {@link TrackOutput}s that receive parsed data. + */ +public final class ChunkExtractorWrapper implements ExtractorOutput { + + /** + * Provides {@link TrackOutput} instances to be written to by the wrapper. + */ + public interface TrackOutputProvider { + + /** + * Called 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); + + } + + public final Extractor extractor; + + private final int primaryTrackType; + private final Format primaryTrackManifestFormat; + private final SparseArray<BindingTrackOutput> bindingTrackOutputs; + + private boolean extractorInitialized; + private TrackOutputProvider trackOutputProvider; + private long endTimeUs; + private SeekMap seekMap; + private Format[] sampleFormats; + + /** + * @param extractor The extractor to wrap. + * @param primaryTrackType The type of the primary track. Typically one of the + * {@link org.mozilla.thirdparty.com.google.android.exoplayer2C} {@code TRACK_TYPE_*} constants. + * @param primaryTrackManifestFormat A manifest defined {@link Format} whose data should be merged + * into any sample {@link Format} output from the {@link Extractor} for the primary track. + */ + public ChunkExtractorWrapper(Extractor extractor, int primaryTrackType, + Format primaryTrackManifestFormat) { + this.extractor = extractor; + this.primaryTrackType = primaryTrackType; + this.primaryTrackManifestFormat = primaryTrackManifestFormat; + bindingTrackOutputs = new SparseArray<>(); + } + + /** + * Returns the {@link SeekMap} most recently output by the extractor, or null. + */ + public SeekMap getSeekMap() { + return seekMap; + } + + /** + * Returns the sample {@link Format}s most recently output by the extractor, or null. + */ + public Format[] getSampleFormats() { + return sampleFormats; + } + + /** + * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified {@link + * TrackOutputProvider}, and configures the extractor to receive data from a new chunk. + * + * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. + * @param startTimeUs The start position in the new chunk, or {@link C#TIME_UNSET} to output + * samples from the start of the chunk. + * @param endTimeUs The end position in the new chunk, or {@link C#TIME_UNSET} to output samples + * to the end of the chunk. + */ + public void init( + @Nullable TrackOutputProvider trackOutputProvider, long startTimeUs, long endTimeUs) { + this.trackOutputProvider = trackOutputProvider; + this.endTimeUs = endTimeUs; + if (!extractorInitialized) { + extractor.init(this); + if (startTimeUs != C.TIME_UNSET) { + extractor.seek(/* position= */ 0, startTimeUs); + } + extractorInitialized = true; + } else { + extractor.seek(/* position= */ 0, startTimeUs == C.TIME_UNSET ? 0 : startTimeUs); + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + bindingTrackOutputs.valueAt(i).bind(trackOutputProvider, endTimeUs); + } + } + } + + // ExtractorOutput implementation. + + @Override + public TrackOutput track(int id, int type) { + BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id); + if (bindingTrackOutput == null) { + // Assert that if we're seeing a new track we have not seen endTracks. + Assertions.checkState(sampleFormats == null); + // TODO: Manifest formats for embedded tracks should also be passed here. + bindingTrackOutput = new BindingTrackOutput(id, type, + type == primaryTrackType ? primaryTrackManifestFormat : null); + bindingTrackOutput.bind(trackOutputProvider, endTimeUs); + bindingTrackOutputs.put(id, bindingTrackOutput); + } + return bindingTrackOutput; + } + + @Override + public void endTracks() { + Format[] sampleFormats = new Format[bindingTrackOutputs.size()]; + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat; + } + this.sampleFormats = sampleFormats; + } + + @Override + public void seekMap(SeekMap seekMap) { + this.seekMap = seekMap; + } + + // Internal logic. + + private static final class BindingTrackOutput implements TrackOutput { + + private final int id; + private final int type; + private final Format manifestFormat; + private final DummyTrackOutput dummyTrackOutput; + + public Format sampleFormat; + private TrackOutput trackOutput; + private long endTimeUs; + + public BindingTrackOutput(int id, int type, Format manifestFormat) { + this.id = id; + this.type = type; + this.manifestFormat = manifestFormat; + dummyTrackOutput = new DummyTrackOutput(); + } + + public void bind(TrackOutputProvider trackOutputProvider, long endTimeUs) { + if (trackOutputProvider == null) { + trackOutput = dummyTrackOutput; + return; + } + this.endTimeUs = endTimeUs; + trackOutput = trackOutputProvider.track(id, type); + if (sampleFormat != null) { + trackOutput.format(sampleFormat); + } + } + + @Override + public void format(Format format) { + sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat) + : format; + trackOutput.format(sampleFormat); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return trackOutput.sampleData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + trackOutput.sampleData(data, length); + } + + @Override + public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, + CryptoData cryptoData) { + if (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) { + trackOutput = dummyTrackOutput; + } + trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } + + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java new file mode 100644 index 0000000000..ef9daddd2c --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java @@ -0,0 +1,41 @@ +/* + * 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.source.chunk; + +import androidx.annotation.Nullable; + +/** + * Holds a chunk or an indication that the end of the stream has been reached. + */ +public final class ChunkHolder { + + /** The chunk. */ + @Nullable public Chunk chunk; + + /** + * Indicates that the end of the stream has been reached. + */ + public boolean endOfStream; + + /** + * Clears the holder. + */ + public void clear() { + chunk = null; + endOfStream = false; + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java new file mode 100644 index 0000000000..a789805cd7 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -0,0 +1,791 @@ +/* + * 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.source.chunk; + +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.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; +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.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. + * May also be configured to expose additional embedded {@link SampleStream}s. + */ +public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader, + Loader.Callback<Chunk>, Loader.ReleaseCallback { + + /** A callback to be notified when a sample stream has finished being released. */ + public interface ReleaseCallback<T extends ChunkSource> { + + /** + * Called when the {@link ChunkSampleStream} has finished being released. + * + * @param chunkSampleStream The released sample stream. + */ + void onSampleStreamReleased(ChunkSampleStream<T> chunkSampleStream); + } + + private static final String TAG = "ChunkSampleStream"; + + public final int primaryTrackType; + + @Nullable private final int[] embeddedTrackTypes; + @Nullable private final Format[] embeddedTrackFormats; + private final boolean[] embeddedTracksSelected; + private final T chunkSource; + private final SequenceableLoader.Callback<ChunkSampleStream<T>> callback; + private final EventDispatcher eventDispatcher; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private final Loader loader; + private final ChunkHolder nextChunkHolder; + private final ArrayList<BaseMediaChunk> mediaChunks; + private final List<BaseMediaChunk> readOnlyMediaChunks; + private final SampleQueue primarySampleQueue; + private final SampleQueue[] embeddedSampleQueues; + private final BaseMediaChunkOutput chunkOutput; + + private Format primaryDownstreamTrackFormat; + @Nullable private ReleaseCallback<T> releaseCallback; + private long pendingResetPositionUs; + private long lastSeekPositionUs; + private int nextNotifyPrimaryFormatMediaChunkIndex; + + /* package */ long decodeOnlyUntilPositionUs; + /* package */ boolean loadingFinished; + + /** + * Constructs an instance. + * + * @param primaryTrackType The type of the primary track. One of the {@link C} {@code + * TRACK_TYPE_*} constants. + * @param embeddedTrackTypes The types of any embedded tracks, or null. + * @param embeddedTrackFormats The formats of the embedded tracks, or null. + * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. + * @param callback An {@link Callback} for the stream. + * @param allocator An {@link Allocator} from which allocations can be obtained. + * @param positionUs The position from which to start loading media. + * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. + * @param eventDispatcher A dispatcher to notify of events. + */ + public ChunkSampleStream( + int primaryTrackType, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, + T chunkSource, + Callback<ChunkSampleStream<T>> callback, + Allocator allocator, + long positionUs, + DrmSessionManager<?> drmSessionManager, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher) { + this.primaryTrackType = primaryTrackType; + this.embeddedTrackTypes = embeddedTrackTypes; + this.embeddedTrackFormats = embeddedTrackFormats; + this.chunkSource = chunkSource; + this.callback = callback; + this.eventDispatcher = eventDispatcher; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + loader = new Loader("Loader:ChunkSampleStream"); + nextChunkHolder = new ChunkHolder(); + mediaChunks = new ArrayList<>(); + readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); + + int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; + embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; + embeddedTracksSelected = new boolean[embeddedTrackCount]; + int[] trackTypes = new int[1 + embeddedTrackCount]; + SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; + + primarySampleQueue = new SampleQueue(allocator, drmSessionManager); + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + + for (int i = 0; i < embeddedTrackCount; i++) { + SampleQueue sampleQueue = + new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()); + embeddedSampleQueues[i] = sampleQueue; + sampleQueues[i + 1] = sampleQueue; + trackTypes[i + 1] = embeddedTrackTypes[i]; + } + + chunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); + pendingResetPositionUs = positionUs; + lastSeekPositionUs = positionUs; + } + + /** + * Discards buffered media up to the specified position. + * + * @param positionUs The position to discard up to, in microseconds. + * @param toKeyframe If true then for each track discards samples up to the keyframe before or at + * the specified position, rather than any sample before or at that position. + */ + public void discardBuffer(long positionUs, boolean toKeyframe) { + if (isPendingReset()) { + return; + } + int oldFirstSampleIndex = primarySampleQueue.getFirstIndex(); + primarySampleQueue.discardTo(positionUs, toKeyframe, true); + int newFirstSampleIndex = primarySampleQueue.getFirstIndex(); + if (newFirstSampleIndex > oldFirstSampleIndex) { + long discardToUs = primarySampleQueue.getFirstTimestampUs(); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]); + } + } + discardDownstreamMediaChunks(newFirstSampleIndex); + } + + /** + * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's + * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned + * stream when the track is no longer required, and before calling this method again to obtain + * another stream for the same track. + * + * @param positionUs The current playback position in microseconds. + * @param trackType The type of the embedded track to enable. + * @return The {@link EmbeddedSampleStream} for the embedded track. + */ + public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (embeddedTrackTypes[i] == trackType) { + Assertions.checkState(!embeddedTracksSelected[i]); + embeddedTracksSelected[i] = true; + embeddedSampleQueues[i].seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); + return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); + } + } + // Should never happen. + throw new IllegalStateException(); + } + + /** + * Returns the {@link ChunkSource} used by this stream. + */ + public T getChunkSource() { + return chunkSource; + } + + /** + * Returns an estimate of the position up to which data is buffered. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, or + * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. + */ + @Override + public long getBufferedPositionUs() { + if (loadingFinished) { + return C.TIME_END_OF_SOURCE; + } else if (isPendingReset()) { + return pendingResetPositionUs; + } else { + long bufferedPositionUs = lastSeekPositionUs; + BaseMediaChunk lastMediaChunk = getLastMediaChunk(); + BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk + : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; + if (lastCompletedMediaChunk != null) { + bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); + } + return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs()); + } + } + + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + + /** + * Seeks to the specified position in microseconds. + * + * @param positionUs The seek position in microseconds. + */ + public void seekToUs(long positionUs) { + lastSeekPositionUs = positionUs; + if (isPendingReset()) { + // A reset is already pending. We only need to update its position. + pendingResetPositionUs = positionUs; + return; + } + + // Detect whether the seek is to the start of a chunk that's at least partially buffered. + BaseMediaChunk seekToMediaChunk = null; + for (int i = 0; i < mediaChunks.size(); i++) { + BaseMediaChunk mediaChunk = mediaChunks.get(i); + long mediaChunkStartTimeUs = mediaChunk.startTimeUs; + if (mediaChunkStartTimeUs == positionUs && mediaChunk.clippedStartTimeUs == C.TIME_UNSET) { + seekToMediaChunk = mediaChunk; + break; + } else if (mediaChunkStartTimeUs > positionUs) { + // We're not going to find a chunk with a matching start time. + break; + } + } + + // See if we can seek inside the primary sample queue. + boolean seekInsideBuffer; + if (seekToMediaChunk != null) { + // When seeking to the start of a chunk we use the index of the first sample in the chunk + // rather than the seek position. This ensures we seek to the keyframe at the start of the + // chunk even if the sample timestamps are slightly offset from the chunk start times. + seekInsideBuffer = primarySampleQueue.seekTo(seekToMediaChunk.getFirstSampleIndex(0)); + decodeOnlyUntilPositionUs = 0; + } else { + seekInsideBuffer = + primarySampleQueue.seekTo( + positionUs, /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()); + decodeOnlyUntilPositionUs = lastSeekPositionUs; + } + + if (seekInsideBuffer) { + // We can seek inside the buffer. + nextNotifyPrimaryFormatMediaChunkIndex = + primarySampleIndexToMediaChunkIndex( + primarySampleQueue.getReadIndex(), /* minChunkIndex= */ 0); + // Seek the embedded sample queues. + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); + } + } else { + // We can't seek inside the buffer, and so need to reset. + pendingResetPositionUs = positionUs; + loadingFinished = false; + mediaChunks.clear(); + nextNotifyPrimaryFormatMediaChunkIndex = 0; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + loader.clearFatalError(); + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); + } + } + } + } + + /** + * Releases the stream. + * + * <p>This method should be called when the stream is no longer required. Either this method or + * {@link #release(ReleaseCallback)} can be used to release this stream. + */ + public void release() { + release(null); + } + + /** + * Releases the stream. + * + * <p>This method should be called when the stream is no longer required. Either this method or + * {@link #release()} can be used to release this stream. + * + * @param callback An optional callback to be called on the loading thread once the loader has + * been released. + */ + public void release(@Nullable ReleaseCallback<T> callback) { + this.releaseCallback = callback; + // Discard as much as we can synchronously. + primarySampleQueue.preRelease(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.preRelease(); + } + loader.release(this); + } + + @Override + public void onLoaderReleased() { + primarySampleQueue.release(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.release(); + } + if (releaseCallback != null) { + releaseCallback.onSampleStreamReleased(this); + } + } + + // SampleStream implementation. + + @Override + public boolean isReady() { + return !isPendingReset() && primarySampleQueue.isReady(loadingFinished); + } + + @Override + public void maybeThrowError() throws IOException { + loader.maybeThrowError(); + primarySampleQueue.maybeThrowError(); + if (!loader.isLoading()) { + chunkSource.maybeThrowError(); + } + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (isPendingReset()) { + return C.RESULT_NOTHING_READ; + } + maybeNotifyPrimaryTrackFormatChanged(); + + return primarySampleQueue.read( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + } + + @Override + public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; + } + int skipCount; + if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { + skipCount = primarySampleQueue.advanceToEnd(); + } else { + skipCount = primarySampleQueue.advanceTo(positionUs); + } + maybeNotifyPrimaryTrackFormatChanged(); + return skipCount; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { + chunkSource.onChunkLoadCompleted(loadable); + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + callback.onContinueLoadingRequested(this); + } + + @Override + public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + if (!released) { + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); + } + callback.onContinueLoadingRequested(this); + } + } + + @Override + public LoadErrorAction onLoadError( + Chunk loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + long bytesLoaded = loadable.bytesLoaded(); + boolean isMediaChunk = isMediaChunk(loadable); + int lastChunkIndex = mediaChunks.size() - 1; + boolean cancelable = + bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex); + long blacklistDurationMs = + cancelable + ? loadErrorHandlingPolicy.getBlacklistDurationMsFor( + loadable.type, loadDurationMs, error, errorCount) + : C.TIME_UNSET; + LoadErrorAction loadErrorAction = null; + if (chunkSource.onChunkLoadError(loadable, cancelable, error, blacklistDurationMs)) { + if (cancelable) { + loadErrorAction = Loader.DONT_RETRY; + if (isMediaChunk) { + BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex); + Assertions.checkState(removed == loadable); + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } + } + } else { + Log.w(TAG, "Ignoring attempt to cancel non-cancelable load."); + } + } + + if (loadErrorAction == null) { + // The load was not cancelled. Either the load must be retried or the error propagated. + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + loadErrorAction = + retryDelayMs != C.TIME_UNSET + ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) + : Loader.DONT_RETRY_FATAL; + } + + boolean canceled = !loadErrorAction.isRetry(); + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + canceled); + if (canceled) { + callback.onContinueLoadingRequested(this); + } + return loadErrorAction; + } + + // SequenceableLoader implementation + + @Override + public boolean continueLoading(long positionUs) { + if (loadingFinished || loader.isLoading() || loader.hasFatalError()) { + return false; + } + + boolean pendingReset = isPendingReset(); + List<BaseMediaChunk> chunkQueue; + long loadPositionUs; + if (pendingReset) { + chunkQueue = Collections.emptyList(); + loadPositionUs = pendingResetPositionUs; + } else { + chunkQueue = readOnlyMediaChunks; + loadPositionUs = getLastMediaChunk().endTimeUs; + } + chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); + boolean endOfStream = nextChunkHolder.endOfStream; + Chunk loadable = nextChunkHolder.chunk; + nextChunkHolder.clear(); + + if (endOfStream) { + pendingResetPositionUs = C.TIME_UNSET; + loadingFinished = true; + return true; + } + + if (loadable == null) { + return false; + } + + if (isMediaChunk(loadable)) { + BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; + if (pendingReset) { + boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs; + // Only enable setting of the decode only flag if we're not resetting to a chunk boundary. + decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs; + pendingResetPositionUs = C.TIME_UNSET; + } + mediaChunk.init(chunkOutput); + mediaChunks.add(mediaChunk); + } else if (loadable instanceof InitializationChunk) { + ((InitializationChunk) loadable).init(chunkOutput); + } + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); + eventDispatcher.loadStarted( + loadable.dataSpec, + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs); + return true; + } + + @Override + public boolean isLoading() { + return loader.isLoading(); + } + + @Override + public long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; + } + } + + @Override + public void reevaluateBuffer(long positionUs) { + if (loader.isLoading() || loader.hasFatalError() || isPendingReset()) { + return; + } + + int currentQueueSize = mediaChunks.size(); + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + if (currentQueueSize <= preferredQueueSize) { + return; + } + + int newQueueSize = currentQueueSize; + for (int i = preferredQueueSize; i < currentQueueSize; i++) { + if (!haveReadFromMediaChunk(i)) { + newQueueSize = i; + break; + } + } + if (newQueueSize == currentQueueSize) { + return; + } + + long endTimeUs = getLastMediaChunk().endTimeUs; + BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize); + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } + loadingFinished = false; + eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); + } + + // Internal methods + + private boolean isMediaChunk(Chunk chunk) { + return chunk instanceof BaseMediaChunk; + } + + /** Returns whether samples have been read from media chunk at given index. */ + private boolean haveReadFromMediaChunk(int mediaChunkIndex) { + BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex); + if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) { + return true; + } + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) { + return true; + } + } + return false; + } + + /* package */ boolean isPendingReset() { + return pendingResetPositionUs != C.TIME_UNSET; + } + + private void discardDownstreamMediaChunks(int discardToSampleIndex) { + int discardToMediaChunkIndex = + primarySampleIndexToMediaChunkIndex(discardToSampleIndex, /* minChunkIndex= */ 0); + // Don't discard any chunks that we haven't reported the primary format change for yet. + discardToMediaChunkIndex = + Math.min(discardToMediaChunkIndex, nextNotifyPrimaryFormatMediaChunkIndex); + if (discardToMediaChunkIndex > 0) { + Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex); + nextNotifyPrimaryFormatMediaChunkIndex -= discardToMediaChunkIndex; + } + } + + private void maybeNotifyPrimaryTrackFormatChanged() { + int readSampleIndex = primarySampleQueue.getReadIndex(); + int notifyToMediaChunkIndex = + primarySampleIndexToMediaChunkIndex( + readSampleIndex, /* minChunkIndex= */ nextNotifyPrimaryFormatMediaChunkIndex - 1); + while (nextNotifyPrimaryFormatMediaChunkIndex <= notifyToMediaChunkIndex) { + maybeNotifyPrimaryTrackFormatChanged(nextNotifyPrimaryFormatMediaChunkIndex++); + } + } + + private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) { + BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; + } + + /** + * Returns the media chunk index corresponding to a given primary sample index. + * + * @param primarySampleIndex The primary sample index for which the corresponding media chunk + * index is required. + * @param minChunkIndex A minimum chunk index from which to start searching, or -1 if no hint can + * be provided. + * @return The index of the media chunk corresponding to the sample index, or -1 if the list of + * media chunks is empty, or {@code minChunkIndex} if the sample precedes the first chunk in + * the search (i.e. the chunk at {@code minChunkIndex}, or at index 0 if {@code minChunkIndex} + * is -1. + */ + private int primarySampleIndexToMediaChunkIndex(int primarySampleIndex, int minChunkIndex) { + for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) { + if (mediaChunks.get(i).getFirstSampleIndex(0) > primarySampleIndex) { + return i - 1; + } + } + return mediaChunks.size() - 1; + } + + private BaseMediaChunk getLastMediaChunk() { + return mediaChunks.get(mediaChunks.size() - 1); + } + + /** + * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample + * queues. + * + * @param chunkIndex The index of the first chunk to discard. + * @return The chunk at given index. + */ + private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) { + BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex); + Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size()); + nextNotifyPrimaryFormatMediaChunkIndex = + Math.max(nextNotifyPrimaryFormatMediaChunkIndex, mediaChunks.size()); + primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); + } + return firstRemovedChunk; + } + + /** + * A {@link SampleStream} embedded in a {@link ChunkSampleStream}. + */ + public final class EmbeddedSampleStream implements SampleStream { + + public final ChunkSampleStream<T> parent; + + private final SampleQueue sampleQueue; + private final int index; + + private boolean notifiedDownstreamFormat; + + public EmbeddedSampleStream(ChunkSampleStream<T> parent, SampleQueue sampleQueue, int index) { + this.parent = parent; + this.sampleQueue = sampleQueue; + this.index = index; + } + + @Override + public boolean isReady() { + return !isPendingReset() && sampleQueue.isReady(loadingFinished); + } + + @Override + public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; + } + maybeNotifyDownstreamFormat(); + int skipCount; + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + skipCount = sampleQueue.advanceToEnd(); + } else { + skipCount = sampleQueue.advanceTo(positionUs); + } + return skipCount; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. Errors will be thrown from the primary stream. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (isPendingReset()) { + return C.RESULT_NOTHING_READ; + } + maybeNotifyDownstreamFormat(); + return sampleQueue.read( + formatHolder, + buffer, + formatRequired, + loadingFinished, + decodeOnlyUntilPositionUs); + } + + public void release() { + Assertions.checkState(embeddedTracksSelected[index]); + embeddedTracksSelected[index] = false; + } + + private void maybeNotifyDownstreamFormat() { + if (!notifiedDownstreamFormat) { + eventDispatcher.downstreamFormatChanged( + embeddedTrackTypes[index], + embeddedTrackFormats[index], + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + lastSeekPositionUs); + notifiedDownstreamFormat = true; + } + } + } + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java new file mode 100644 index 0000000000..33cee8e20e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -0,0 +1,111 @@ +/* + * 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.source.chunk; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; +import java.io.IOException; +import java.util.List; + +/** + * A provider of {@link Chunk}s for a {@link ChunkSampleStream} to load. + */ +public interface ChunkSource { + + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters); + + /** + * If the source is currently having difficulty providing chunks, then this method throws the + * underlying error. Otherwise does nothing. + * <p> + * This method should only be called after the source has been prepared. + * + * @throws IOException The underlying error. + */ + void maybeThrowError() throws IOException; + + /** + * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue. + * <p> + * Removing {@link MediaChunk}s from the back of the queue can be useful if they could be replaced + * with chunks of a significantly higher quality (e.g. because the available bandwidth has + * substantially increased). + * + * @param playbackPositionUs The current playback position. + * @param queue The queue of buffered {@link MediaChunk}s. + * @return The preferred queue size. + */ + int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue); + + /** + * Returns the next chunk to load. + * + * <p>If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has + * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the + * end of the stream has not been reached, the {@link ChunkHolder} is not modified. + * + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this chunk source belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. + * @param loadPositionUs The current load position in microseconds. If {@code queue} is empty, + * this is the starting position from which chunks should be provided. Else it's equal to + * {@link MediaChunk#endTimeUs} of the last chunk in the {@code queue}. + * @param queue The queue of buffered {@link MediaChunk}s. + * @param out A holder to populate. + */ + void getNextChunk( + long playbackPositionUs, + long loadPositionUs, + List<? extends MediaChunk> queue, + ChunkHolder out); + + /** + * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this + * source. + * + * <p>This method should only be called when the source is enabled. + * + * @param chunk The chunk whose load has been completed. + */ + void onChunkLoadCompleted(Chunk chunk); + + /** + * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from + * this source. + * + * <p>This method should only be called when the source is enabled. + * + * @param chunk The chunk whose load encountered the error. + * @param cancelable Whether the load can be canceled. + * @param e The error. + * @param blacklistDurationMs The duration for which the associated track may be blacklisted, or + * {@link C#TIME_UNSET} if the track may not be blacklisted. + * @return Whether the load should be canceled so that a replacement chunk can be loaded instead. + * Must be {@code false} if {@code cancelable} is {@code false}. If {@code true}, {@link + * #getNextChunk(long, long, List, ChunkHolder)} will be called to obtain the replacement + * chunk. + */ + boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs); +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java new file mode 100644 index 0000000000..98865e8b0e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -0,0 +1,157 @@ +/* + * 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.source.chunk; + +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.DefaultExtractorInput; +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.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data. + */ +public class ContainerMediaChunk extends BaseMediaChunk { + + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); + + private final int chunkCount; + private final long sampleOffsetUs; + private final ChunkExtractorWrapper extractorWrapper; + + private long nextLoadPosition; + private volatile boolean loadCanceled; + private boolean loadCompleted; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link + * C#TIME_UNSET} to output from the start of the chunk. + * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link + * C#TIME_UNSET} to output to the end of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. + * @param chunkCount The number of chunks in the underlying media that are spanned by this + * instance. Normally equal to one, but may be larger if multiple chunks as defined by the + * underlying media are being merged into a single load. + * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. + * @param extractorWrapper A wrapped extractor to use for parsing the data. + */ + public ContainerMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long clippedStartTimeUs, + long clippedEndTimeUs, + long chunkIndex, + int chunkCount, + long sampleOffsetUs, + ChunkExtractorWrapper extractorWrapper) { + super( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + clippedStartTimeUs, + clippedEndTimeUs, + chunkIndex); + this.chunkCount = chunkCount; + this.sampleOffsetUs = sampleOffsetUs; + this.extractorWrapper = extractorWrapper; + } + + @Override + public long getNextChunkIndex() { + return chunkIndex + chunkCount; + } + + @Override + public boolean isLoadCompleted() { + return loadCompleted; + } + + // Loadable implementation. + + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + @Override + public final void load() throws IOException, InterruptedException { + if (nextLoadPosition == 0) { + // Configure the output and set it as the target for the extractor wrapper. + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init( + getTrackOutputProvider(output), + clippedStartTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedStartTimeUs - sampleOffsetUs), + clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs)); + } + try { + // Create and open the input. + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); + // Load and decode the sample data. + try { + Extractor extractor = extractorWrapper.extractor; + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + result = extractor.read(input, DUMMY_POSITION_HOLDER); + } + Assertions.checkState(result != Extractor.RESULT_SEEK); + } finally { + nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; + } + } finally { + Util.closeQuietly(dataSource); + } + loadCompleted = true; + } + + /** + * Returns the {@link TrackOutputProvider} to be used by the wrapped extractor. + * + * @param baseMediaChunkOutput The {@link BaseMediaChunkOutput} most recently passed to {@link + * #init(BaseMediaChunkOutput)}. + * @return A {@link TrackOutputProvider} to be used by the wrapped extractor. + */ + protected TrackOutputProvider getTrackOutputProvider(BaseMediaChunkOutput baseMediaChunkOutput) { + return baseMediaChunkOutput; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java new file mode 100644 index 0000000000..583f8ceeee --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java @@ -0,0 +1,119 @@ +/* + * 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.source.chunk; + +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.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.Arrays; + +/** + * A base class for {@link Chunk} implementations where the data should be loaded into a + * {@code byte[]} before being consumed. + */ +public abstract class DataChunk extends Chunk { + + private static final int READ_GRANULARITY = 16 * 1024; + + private byte[] data; + + private volatile boolean loadCanceled; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. + * @param type See {@link #type}. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param data An optional recycled array that can be used as a holder for the data. + */ + public DataChunk( + DataSource dataSource, + DataSpec dataSpec, + int type, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + byte[] data) { + super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData, + C.TIME_UNSET, C.TIME_UNSET); + this.data = data; + } + + /** + * Returns the array in which the data is held. + * <p> + * This method should be used for recycling the holder only, and not for reading the data. + * + * @return The array in which the data is held. + */ + public byte[] getDataHolder() { + return data; + } + + // Loadable implementation + + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @Override + public final void load() throws IOException, InterruptedException { + try { + dataSource.open(dataSpec); + int limit = 0; + int bytesRead = 0; + while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) { + maybeExpandData(limit); + bytesRead = dataSource.read(data, limit, READ_GRANULARITY); + if (bytesRead != -1) { + limit += bytesRead; + } + } + if (!loadCanceled) { + consume(data, limit); + } + } finally { + Util.closeQuietly(dataSource); + } + } + + /** + * Called by {@link #load()}. Implementations should override this method to consume the loaded + * data. + * + * @param data An array containing the data. + * @param limit The limit of the data. + * @throws IOException If an error occurs consuming the loaded data. + */ + protected abstract void consume(byte[] data, int limit) throws IOException; + + private void maybeExpandData(int limit) { + if (data == null) { + data = new byte[READ_GRANULARITY]; + } else if (data.length < limit + READ_GRANULARITY) { + // The new length is calculated as (data.length + READ_GRANULARITY) rather than + // (limit + READ_GRANULARITY) in order to avoid small increments in the length. + data = Arrays.copyOf(data, data.length + READ_GRANULARITY); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java new file mode 100644 index 0000000000..db6e82c2c7 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -0,0 +1,112 @@ +/* + * 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.source.chunk; + +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.extractor.DefaultExtractorInput; +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.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +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 org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. + */ +public final class InitializationChunk extends Chunk { + + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); + + private final ChunkExtractorWrapper extractorWrapper; + + @MonotonicNonNull private TrackOutputProvider trackOutputProvider; + private long nextLoadPosition; + private volatile boolean loadCanceled; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param extractorWrapper A wrapped extractor to use for parsing the initialization data. + */ + public InitializationChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + ChunkExtractorWrapper extractorWrapper) { + super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason, + trackSelectionData, C.TIME_UNSET, C.TIME_UNSET); + this.extractorWrapper = extractorWrapper; + } + + /** + * Initializes the chunk for loading, setting a {@link TrackOutputProvider} for track outputs to + * which formats will be written as they are loaded. + * + * @param trackOutputProvider The {@link TrackOutputProvider} for track outputs to which formats + * will be written as they are loaded. + */ + public void init(TrackOutputProvider trackOutputProvider) { + this.trackOutputProvider = trackOutputProvider; + } + + // Loadable implementation. + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + @Override + public void load() throws IOException, InterruptedException { + if (nextLoadPosition == 0) { + extractorWrapper.init( + trackOutputProvider, /* startTimeUs= */ C.TIME_UNSET, /* endTimeUs= */ C.TIME_UNSET); + } + try { + // Create and open the input. + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); + // Load and decode the initialization data. + try { + Extractor extractor = extractorWrapper.extractor; + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + result = extractor.read(input, DUMMY_POSITION_HOLDER); + } + Assertions.checkState(result != Extractor.RESULT_SEEK); + } finally { + nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; + } + } finally { + Util.closeQuietly(dataSource); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java new file mode 100644 index 0000000000..81c9d216b9 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -0,0 +1,68 @@ +/* + * 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.source.chunk; + +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.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** + * An abstract base class for {@link Chunk}s that contain media samples. + */ +public abstract class MediaChunk extends Chunk { + + /** The chunk index, or {@link C#INDEX_UNSET} if it is not known. */ + public final long chunkIndex; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. + */ + public MediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long chunkIndex) { + super(dataSource, dataSpec, C.DATA_TYPE_MEDIA, trackFormat, trackSelectionReason, + trackSelectionData, startTimeUs, endTimeUs); + Assertions.checkNotNull(trackFormat); + this.chunkIndex = chunkIndex; + } + + /** Returns the next chunk index or {@link C#INDEX_UNSET} if it is not known. */ + public long getNextChunkIndex() { + return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET; + } + + /** + * Returns whether the chunk has been fully loaded. + */ + public abstract boolean isLoadCompleted(); + +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java new file mode 100644 index 0000000000..c6f5b1d41e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java @@ -0,0 +1,104 @@ +/* + * 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.source.chunk; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import java.util.NoSuchElementException; + +/** + * Iterator for media chunk sequences. + * + * <p>The iterator initially points in front of the first available element. The first call to + * {@link #next()} moves the iterator to the first element. Check the return value of {@link + * #next()} or {@link #isEnded()} to determine whether the iterator reached the end of the available + * data. + */ +public interface MediaChunkIterator { + + /** An empty media chunk iterator without available data. */ + MediaChunkIterator EMPTY = + new MediaChunkIterator() { + @Override + public boolean isEnded() { + return true; + } + + @Override + public boolean next() { + return false; + } + + @Override + public DataSpec getDataSpec() { + throw new NoSuchElementException(); + } + + @Override + public long getChunkStartTimeUs() { + throw new NoSuchElementException(); + } + + @Override + public long getChunkEndTimeUs() { + throw new NoSuchElementException(); + } + + @Override + public void reset() { + // Do nothing. + } + }; + + /** Returns whether the iteration has reached the end of the available data. */ + boolean isEnded(); + + /** + * Moves the iterator to the next media chunk. + * + * <p>Check the return value or {@link #isEnded()} to determine whether the iterator reached the + * end of the available data. + * + * @return Whether the iterator points to a media chunk with available data. + */ + boolean next(); + + /** + * Returns the {@link DataSpec} used to load the media chunk. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + DataSpec getDataSpec(); + + /** + * Returns the media start time of the chunk, in microseconds. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + long getChunkStartTimeUs(); + + /** + * Returns the media end time of the chunk, in microseconds. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + long getChunkEndTimeUs(); + + /** Resets the iterator to the initial position. */ + void reset(); +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java new file mode 100644 index 0000000000..1b3004418e --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java @@ -0,0 +1,61 @@ +/* + * 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.source.chunk; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import java.util.List; + +/** A {@link MediaChunkIterator} which iterates over a {@link List} of {@link MediaChunk}s. */ +public final class MediaChunkListIterator extends BaseMediaChunkIterator { + + private final List<? extends MediaChunk> chunks; + private final boolean reverseOrder; + + /** + * Creates iterator. + * + * @param chunks The list of chunks to iterate over. + * @param reverseOrder Whether to iterate in reverse order. + */ + public MediaChunkListIterator(List<? extends MediaChunk> chunks, boolean reverseOrder) { + super(0, chunks.size() - 1); + this.chunks = chunks; + this.reverseOrder = reverseOrder; + } + + @Override + public DataSpec getDataSpec() { + return getCurrentChunk().dataSpec; + } + + @Override + public long getChunkStartTimeUs() { + return getCurrentChunk().startTimeUs; + } + + @Override + public long getChunkEndTimeUs() { + return getCurrentChunk().endTimeUs; + } + + private MediaChunk getCurrentChunk() { + int index = (int) super.getCurrentIndex(); + if (reverseOrder) { + index = chunks.size() - 1 - index; + } + return chunks.get(index); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java new file mode 100644 index 0000000000..b3d30408ee --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -0,0 +1,120 @@ +/* + * 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.source.chunk; + +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.DefaultExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A {@link BaseMediaChunk} for chunks consisting of a single raw sample. + */ +public final class SingleSampleMediaChunk extends BaseMediaChunk { + + private final int trackType; + private final Format sampleFormat; + + private long nextLoadPosition; + private boolean loadCompleted; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. + * @param trackFormat See {@link #trackFormat}. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. + * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*} + * constants. + * @param sampleFormat The {@link Format} of the sample in the chunk. + */ + public SingleSampleMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long chunkIndex, + int trackType, + Format sampleFormat) { + super( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + /* clippedStartTimeUs= */ C.TIME_UNSET, + /* clippedEndTimeUs= */ C.TIME_UNSET, + chunkIndex); + this.trackType = trackType; + this.sampleFormat = sampleFormat; + } + + + @Override + public boolean isLoadCompleted() { + return loadCompleted; + } + + // Loadable implementation. + + @Override + public void cancelLoad() { + // Do nothing. + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + @Override + public void load() throws IOException, InterruptedException { + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(0); + TrackOutput trackOutput = output.track(0, trackType); + trackOutput.format(sampleFormat); + try { + // Create and open the input. + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + long length = dataSource.open(loadDataSpec); + if (length != C.LENGTH_UNSET) { + length += nextLoadPosition; + } + ExtractorInput extractorInput = + new DefaultExtractorInput(dataSource, nextLoadPosition, length); + // Load the sample data. + int result = 0; + while (result != C.RESULT_END_OF_INPUT) { + nextLoadPosition += result; + result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true); + } + int sampleSize = (int) nextLoadPosition; + trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + } finally { + Util.closeQuietly(dataSource); + } + loadCompleted = true; + } +} |