/* * 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; 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.Timeline; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end * positions. The wrapped source must consist of a single period. */ public final class ClippingMediaSource extends CompositeMediaSource { /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */ public static final class IllegalClippingException extends IOException { /** * The reason clipping failed. One of {@link #REASON_INVALID_PERIOD_COUNT}, {@link * #REASON_NOT_SEEKABLE_TO_START} or {@link #REASON_START_EXCEEDS_END}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) public @interface Reason {} /** The wrapped source doesn't consist of a single period. */ public static final int REASON_INVALID_PERIOD_COUNT = 0; /** The wrapped source is not seekable and a non-zero clipping start position was specified. */ public static final int REASON_NOT_SEEKABLE_TO_START = 1; /** The wrapped source ends before the specified clipping start position. */ public static final int REASON_START_EXCEEDS_END = 2; /** The reason clipping failed. */ public final @Reason int reason; /** * @param reason The reason clipping failed. */ public IllegalClippingException(@Reason int reason) { super("Illegal clipping: " + getReasonDescription(reason)); this.reason = reason; } private static String getReasonDescription(@Reason int reason) { switch (reason) { case REASON_INVALID_PERIOD_COUNT: return "invalid period count"; case REASON_NOT_SEEKABLE_TO_START: return "not seekable to start"; case REASON_START_EXCEEDS_END: return "start exceeds end"; default: return "unknown"; } } } private final MediaSource mediaSource; private final long startUs; private final long endUs; private final boolean enableInitialDiscontinuity; private final boolean allowDynamicClippingUpdates; private final boolean relativeToDefaultPosition; private final ArrayList mediaPeriods; private final Timeline.Window window; @Nullable private ClippingTimeline clippingTimeline; @Nullable private IllegalClippingException clippingError; private long periodStartUs; private long periodEndUs; /** * Creates a new clipping source that wraps the specified source and provides samples between the * specified start and end position. * * @param mediaSource The single-period source to wrap. * @param startPositionUs The start position within {@code mediaSource}'s window at which to start * providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples * from the specified start point up to the end of the source. Specifying a position that * exceeds the {@code mediaSource}'s duration will also result in the end of the source not * being clipped. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { this( mediaSource, startPositionUs, endPositionUs, /* enableInitialDiscontinuity= */ true, /* allowDynamicClippingUpdates= */ false, /* relativeToDefaultPosition= */ false); } /** * Creates a new clipping source that wraps the specified source and provides samples from the * default position for the specified duration. * * @param mediaSource The single-period source to wrap. * @param durationUs The duration from the default position in the window in {@code mediaSource}'s * timeline at which to stop providing samples. Specifying a duration that exceeds the {@code * mediaSource}'s duration will result in the end of the source not being clipped. */ public ClippingMediaSource(MediaSource mediaSource, long durationUs) { this( mediaSource, /* startPositionUs= */ 0, /* endPositionUs= */ durationUs, /* enableInitialDiscontinuity= */ true, /* allowDynamicClippingUpdates= */ false, /* relativeToDefaultPosition= */ true); } /** * Creates a new clipping source that wraps the specified source. * *

If the start point is guaranteed to be a key frame, pass {@code false} to {@code * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period is first * read from. * *

For live streams, if the clipping positions should move with the live window, pass {@code * true} to {@code allowDynamicClippingUpdates}. Otherwise, the live stream ends when the playback * reaches {@code endPositionUs} in the last reported live window at the time a media period was * created. * * @param mediaSource The single-period source to wrap. * @param startPositionUs The start position at which to start providing samples, in microseconds. * If {@code relativeToDefaultPosition} is {@code false}, this position is relative to the * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} * is {@code true}, this position is relative to the default position in the window in {@code * mediaSource}'s timeline. * @param endPositionUs The end position at which to stop providing samples, in microseconds. * Specify {@link C#TIME_END_OF_SOURCE} to provide samples from the specified start point up * to the end of the source. Specifying a position that exceeds the {@code mediaSource}'s * duration will also result in the end of the source not being clipped. If {@code * relativeToDefaultPosition} is {@code false}, the specified position is relative to the * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} * is {@code true}, this position is relative to the default position in the window in {@code * mediaSource}'s timeline. * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. * @param allowDynamicClippingUpdates Whether the clipping of active media periods moves with a * live window. If {@code false}, playback ends when it reaches {@code endPositionUs} in the * last reported live window at the time a media period was created. * @param relativeToDefaultPosition Whether {@code startPositionUs} and {@code endPositionUs} are * relative to the default position in the window in {@code mediaSource}'s timeline. */ public ClippingMediaSource( MediaSource mediaSource, long startPositionUs, long endPositionUs, boolean enableInitialDiscontinuity, boolean allowDynamicClippingUpdates, boolean relativeToDefaultPosition) { Assertions.checkArgument(startPositionUs >= 0); this.mediaSource = Assertions.checkNotNull(mediaSource); startUs = startPositionUs; endUs = endPositionUs; this.enableInitialDiscontinuity = enableInitialDiscontinuity; this.allowDynamicClippingUpdates = allowDynamicClippingUpdates; this.relativeToDefaultPosition = relativeToDefaultPosition; mediaPeriods = new ArrayList<>(); window = new Timeline.Window(); } @Override @Nullable public Object getTag() { return mediaSource.getTag(); } @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); prepareChildSource(/* id= */ null, mediaSource); } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { if (clippingError != null) { throw clippingError; } super.maybeThrowSourceInfoRefreshError(); } @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( mediaSource.createPeriod(id, allocator, startPositionUs), enableInitialDiscontinuity, periodStartUs, periodEndUs); mediaPeriods.add(mediaPeriod); return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { Assertions.checkState(mediaPeriods.remove(mediaPeriod)); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) { refreshClippedTimeline(Assertions.checkNotNull(clippingTimeline).timeline); } } @Override protected void releaseSourceInternal() { super.releaseSourceInternal(); clippingError = null; clippingTimeline = null; } @Override protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { if (clippingError != null) { return; } refreshClippedTimeline(timeline); } private void refreshClippedTimeline(Timeline timeline) { long windowStartUs; long windowEndUs; timeline.getWindow(/* windowIndex= */ 0, window); long windowPositionInPeriodUs = window.getPositionInFirstPeriodUs(); if (clippingTimeline == null || mediaPeriods.isEmpty() || allowDynamicClippingUpdates) { windowStartUs = startUs; windowEndUs = endUs; if (relativeToDefaultPosition) { long windowDefaultPositionUs = window.getDefaultPositionUs(); windowStartUs += windowDefaultPositionUs; windowEndUs += windowDefaultPositionUs; } periodStartUs = windowPositionInPeriodUs + windowStartUs; periodEndUs = endUs == C.TIME_END_OF_SOURCE ? C.TIME_END_OF_SOURCE : windowPositionInPeriodUs + windowEndUs; int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { mediaPeriods.get(i).updateClipping(periodStartUs, periodEndUs); } } else { // Keep window fixed at previous period position. windowStartUs = periodStartUs - windowPositionInPeriodUs; windowEndUs = endUs == C.TIME_END_OF_SOURCE ? C.TIME_END_OF_SOURCE : periodEndUs - windowPositionInPeriodUs; } try { clippingTimeline = new ClippingTimeline(timeline, windowStartUs, windowEndUs); } catch (IllegalClippingException e) { clippingError = e; return; } refreshSourceInfo(clippingTimeline); } @Override protected long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) { if (mediaTimeMs == C.TIME_UNSET) { return C.TIME_UNSET; } long startMs = C.usToMs(startUs); long clippedTimeMs = Math.max(0, mediaTimeMs - startMs); if (endUs != C.TIME_END_OF_SOURCE) { clippedTimeMs = Math.min(C.usToMs(endUs) - startMs, clippedTimeMs); } return clippedTimeMs; } /** * Provides a clipped view of a specified timeline. */ private static final class ClippingTimeline extends ForwardingTimeline { private final long startUs; private final long endUs; private final long durationUs; private final boolean isDynamic; /** * Creates a new clipping timeline that wraps the specified timeline. * * @param timeline The timeline to clip. * @param startUs The number of microseconds to clip from the start of {@code timeline}. * @param endUs The end position in microseconds for the clipped timeline relative to the start * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. * @throws IllegalClippingException If the timeline could not be clipped. */ public ClippingTimeline(Timeline timeline, long startUs, long endUs) throws IllegalClippingException { super(timeline); if (timeline.getPeriodCount() != 1) { throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT); } Window window = timeline.getWindow(0, new Window()); startUs = Math.max(0, startUs); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : Math.max(0, endUs); if (window.durationUs != C.TIME_UNSET) { if (resolvedEndUs > window.durationUs) { resolvedEndUs = window.durationUs; } if (startUs != 0 && !window.isSeekable) { throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START); } if (startUs > resolvedEndUs) { throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END); } } this.startUs = startUs; this.endUs = resolvedEndUs; durationUs = resolvedEndUs == C.TIME_UNSET ? C.TIME_UNSET : (resolvedEndUs - startUs); isDynamic = window.isDynamic && (resolvedEndUs == C.TIME_UNSET || (window.durationUs != C.TIME_UNSET && resolvedEndUs == window.durationUs)); } @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { timeline.getWindow(/* windowIndex= */ 0, window, /* defaultPositionProjectionUs= */ 0); window.positionInFirstPeriodUs += startUs; window.durationUs = durationUs; window.isDynamic = isDynamic; if (window.defaultPositionUs != C.TIME_UNSET) { window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs); window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs : Math.min(window.defaultPositionUs, endUs); window.defaultPositionUs -= startUs; } long startMs = C.usToMs(startUs); if (window.presentationStartTimeMs != C.TIME_UNSET) { window.presentationStartTimeMs += startMs; } if (window.windowStartTimeMs != C.TIME_UNSET) { window.windowStartTimeMs += startMs; } return window; } @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { timeline.getPeriod(/* periodIndex= */ 0, period, setIds); long positionInClippedWindowUs = period.getPositionInWindowUs() - startUs; long periodDurationUs = durationUs == C.TIME_UNSET ? C.TIME_UNSET : durationUs - positionInClippedWindowUs; return period.set( period.id, period.uid, /* windowIndex= */ 0, periodDurationUs, positionInClippedWindowUs); } } }