| /* |
| * 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 com.google.android.exoplayer2.source.dash; |
| |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.text.TextUtils; |
| import android.util.SparseArray; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.ExoPlayerLibraryInfo; |
| import com.google.android.exoplayer2.MediaItem; |
| import com.google.android.exoplayer2.ParserException; |
| import com.google.android.exoplayer2.Timeline; |
| import com.google.android.exoplayer2.drm.DrmSession; |
| import com.google.android.exoplayer2.drm.DrmSessionManager; |
| import com.google.android.exoplayer2.offline.FilteringManifestParser; |
| import com.google.android.exoplayer2.offline.StreamKey; |
| import com.google.android.exoplayer2.source.BaseMediaSource; |
| import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; |
| import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; |
| import com.google.android.exoplayer2.source.LoadEventInfo; |
| import com.google.android.exoplayer2.source.MediaPeriod; |
| import com.google.android.exoplayer2.source.MediaSource; |
| import com.google.android.exoplayer2.source.MediaSourceEventListener; |
| import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; |
| import com.google.android.exoplayer2.source.MediaSourceFactory; |
| import com.google.android.exoplayer2.source.SequenceableLoader; |
| import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; |
| import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; |
| import com.google.android.exoplayer2.source.dash.manifest.DashManifest; |
| import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; |
| import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; |
| import com.google.android.exoplayer2.upstream.Allocator; |
| import com.google.android.exoplayer2.upstream.DataSource; |
| import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; |
| import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; |
| import com.google.android.exoplayer2.upstream.Loader; |
| import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; |
| import com.google.android.exoplayer2.upstream.LoaderErrorThrower; |
| import com.google.android.exoplayer2.upstream.ParsingLoadable; |
| import com.google.android.exoplayer2.upstream.TransferListener; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.Log; |
| import com.google.android.exoplayer2.util.SntpClient; |
| import com.google.android.exoplayer2.util.Util; |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.nio.charset.Charset; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** A DASH {@link MediaSource}. */ |
| public final class DashMediaSource extends BaseMediaSource { |
| |
| static { |
| ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); |
| } |
| |
| /** Factory for {@link DashMediaSource}s. */ |
| public static final class Factory implements MediaSourceFactory { |
| |
| private final DashChunkSource.Factory chunkSourceFactory; |
| @Nullable private final DataSource.Factory manifestDataSourceFactory; |
| |
| private DrmSessionManager drmSessionManager; |
| private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; |
| private LoadErrorHandlingPolicy loadErrorHandlingPolicy; |
| private long livePresentationDelayMs; |
| private boolean livePresentationDelayOverridesManifest; |
| @Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser; |
| private List<StreamKey> streamKeys; |
| @Nullable private Object tag; |
| |
| /** |
| * Creates a new factory for {@link DashMediaSource}s. |
| * |
| * @param dataSourceFactory A factory for {@link DataSource} instances that will be used to load |
| * manifest and media data. |
| */ |
| public Factory(DataSource.Factory dataSourceFactory) { |
| this(new DefaultDashChunkSource.Factory(dataSourceFactory), dataSourceFactory); |
| } |
| |
| /** |
| * Creates a new factory for {@link DashMediaSource}s. |
| * |
| * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. |
| * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used |
| * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be |
| * used to create create media sources with sideloaded manifests via {@link |
| * #createMediaSource(DashManifest, Handler, MediaSourceEventListener)}. |
| */ |
| public Factory( |
| DashChunkSource.Factory chunkSourceFactory, |
| @Nullable DataSource.Factory manifestDataSourceFactory) { |
| this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); |
| this.manifestDataSourceFactory = manifestDataSourceFactory; |
| drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); |
| loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); |
| livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; |
| compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); |
| streamKeys = Collections.emptyList(); |
| } |
| |
| /** |
| * @deprecated Use {@link MediaItem.Builder#setTag(Object)} and {@link |
| * #createMediaSource(MediaItem)} instead. |
| */ |
| @Deprecated |
| public Factory setTag(@Nullable Object tag) { |
| this.tag = tag; |
| return this; |
| } |
| |
| /** |
| * @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link |
| * #createMediaSource(MediaItem)} instead. |
| */ |
| @SuppressWarnings("deprecation") |
| @Deprecated |
| @Override |
| public Factory setStreamKeys(@Nullable List<StreamKey> streamKeys) { |
| this.streamKeys = streamKeys != null ? streamKeys : Collections.emptyList(); |
| return this; |
| } |
| |
| /** |
| * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The |
| * default value is {@link DrmSessionManager#DUMMY}. |
| * |
| * @param drmSessionManager The {@link DrmSessionManager}. |
| * @return This factory, for convenience. |
| */ |
| @Override |
| public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { |
| this.drmSessionManager = |
| drmSessionManager != null |
| ? drmSessionManager |
| : DrmSessionManager.getDummyDrmSessionManager(); |
| return this; |
| } |
| |
| /** @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ |
| @Deprecated |
| public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { |
| return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); |
| } |
| |
| /** |
| * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link |
| * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. |
| * |
| * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. |
| * |
| * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. |
| * @return This factory, for convenience. |
| */ |
| public Factory setLoadErrorHandlingPolicy( |
| @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { |
| this.loadErrorHandlingPolicy = |
| loadErrorHandlingPolicy != null |
| ? loadErrorHandlingPolicy |
| : new DefaultLoadErrorHandlingPolicy(); |
| return this; |
| } |
| |
| /** @deprecated Use {@link #setLivePresentationDelayMs(long, boolean)} instead. */ |
| @Deprecated |
| @SuppressWarnings("deprecation") |
| public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { |
| if (livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) { |
| return setLivePresentationDelayMs(DEFAULT_LIVE_PRESENTATION_DELAY_MS, false); |
| } else { |
| return setLivePresentationDelayMs(livePresentationDelayMs, true); |
| } |
| } |
| |
| /** |
| * Sets the duration in milliseconds by which the default start position should precede the end |
| * of the live window for live playbacks. The {@code overridesManifest} parameter specifies |
| * whether the value is used in preference to one in the manifest, if present. The default value |
| * is {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}, and by default {@code overridesManifest} is |
| * false. |
| * |
| * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the |
| * default start position should precede the end of the live window. |
| * @param overridesManifest Whether the value is used in preference to one in the manifest, if |
| * present. |
| * @return This factory, for convenience. |
| */ |
| public Factory setLivePresentationDelayMs( |
| long livePresentationDelayMs, boolean overridesManifest) { |
| this.livePresentationDelayMs = livePresentationDelayMs; |
| this.livePresentationDelayOverridesManifest = overridesManifest; |
| return this; |
| } |
| |
| /** |
| * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. |
| * |
| * @param manifestParser A parser for loaded manifest data. |
| * @return This factory, for convenience. |
| */ |
| public Factory setManifestParser( |
| @Nullable ParsingLoadable.Parser<? extends DashManifest> manifestParser) { |
| this.manifestParser = manifestParser; |
| return this; |
| } |
| |
| /** |
| * Sets the factory to create composite {@link SequenceableLoader}s for when this media source |
| * loads data from multiple streams (video, audio etc...). The default is an instance of {@link |
| * DefaultCompositeSequenceableLoaderFactory}. |
| * |
| * @param compositeSequenceableLoaderFactory A factory to create composite {@link |
| * SequenceableLoader}s for when this media source loads data from multiple streams (video, |
| * audio etc...). |
| * @return This factory, for convenience. |
| */ |
| public Factory setCompositeSequenceableLoaderFactory( |
| @Nullable CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { |
| this.compositeSequenceableLoaderFactory = |
| compositeSequenceableLoaderFactory != null |
| ? compositeSequenceableLoaderFactory |
| : new DefaultCompositeSequenceableLoaderFactory(); |
| return this; |
| } |
| |
| /** |
| * Returns a new {@link DashMediaSource} using the current parameters and the specified |
| * sideloaded manifest. |
| * |
| * @param manifest The manifest. {@link DashManifest#dynamic} must be false. |
| * @return The new {@link DashMediaSource}. |
| * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true. |
| */ |
| public DashMediaSource createMediaSource(DashManifest manifest) { |
| Assertions.checkArgument(!manifest.dynamic); |
| if (!streamKeys.isEmpty()) { |
| manifest = manifest.copy(streamKeys); |
| } |
| return new DashMediaSource( |
| manifest, |
| /* manifestUri= */ null, |
| /* manifestDataSourceFactory= */ null, |
| /* manifestParser= */ null, |
| chunkSourceFactory, |
| compositeSequenceableLoaderFactory, |
| drmSessionManager, |
| loadErrorHandlingPolicy, |
| livePresentationDelayMs, |
| livePresentationDelayOverridesManifest, |
| tag); |
| } |
| |
| /** |
| * @deprecated Use {@link #createMediaSource(DashManifest)} and {@link |
| * #addEventListener(Handler, MediaSourceEventListener)} instead. |
| */ |
| @Deprecated |
| public DashMediaSource createMediaSource( |
| DashManifest manifest, |
| @Nullable Handler eventHandler, |
| @Nullable MediaSourceEventListener eventListener) { |
| DashMediaSource mediaSource = createMediaSource(manifest); |
| if (eventHandler != null && eventListener != null) { |
| mediaSource.addEventListener(eventHandler, eventListener); |
| } |
| return mediaSource; |
| } |
| |
| /** |
| * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, |
| * MediaSourceEventListener)} instead. |
| */ |
| @Deprecated |
| public DashMediaSource createMediaSource( |
| Uri manifestUri, |
| @Nullable Handler eventHandler, |
| @Nullable MediaSourceEventListener eventListener) { |
| DashMediaSource mediaSource = createMediaSource(manifestUri); |
| if (eventHandler != null && eventListener != null) { |
| mediaSource.addEventListener(eventHandler, eventListener); |
| } |
| return mediaSource; |
| } |
| |
| /** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */ |
| @SuppressWarnings("deprecation") |
| @Deprecated |
| @Override |
| public DashMediaSource createMediaSource(Uri uri) { |
| return createMediaSource(new MediaItem.Builder().setSourceUri(uri).build()); |
| } |
| |
| /** |
| * Returns a new {@link DashMediaSource} using the current parameters. |
| * |
| * @param mediaItem The media item of the dash stream. |
| * @return The new {@link DashMediaSource}. |
| * @throws NullPointerException if {@link MediaItem#playbackProperties} is {@code null}. |
| */ |
| @Override |
| public DashMediaSource createMediaSource(MediaItem mediaItem) { |
| Assertions.checkNotNull(mediaItem.playbackProperties); |
| @Nullable ParsingLoadable.Parser<? extends DashManifest> manifestParser = this.manifestParser; |
| if (manifestParser == null) { |
| manifestParser = new DashManifestParser(); |
| } |
| List<StreamKey> streamKeys = |
| !mediaItem.playbackProperties.streamKeys.isEmpty() |
| ? mediaItem.playbackProperties.streamKeys |
| : this.streamKeys; |
| if (!streamKeys.isEmpty()) { |
| manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys); |
| } |
| return new DashMediaSource( |
| /* manifest= */ null, |
| mediaItem.playbackProperties.sourceUri, |
| manifestDataSourceFactory, |
| manifestParser, |
| chunkSourceFactory, |
| compositeSequenceableLoaderFactory, |
| drmSessionManager, |
| loadErrorHandlingPolicy, |
| livePresentationDelayMs, |
| livePresentationDelayOverridesManifest, |
| mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag); |
| } |
| |
| @Override |
| public int[] getSupportedTypes() { |
| return new int[] {C.TYPE_DASH}; |
| } |
| } |
| |
| /** |
| * The default presentation delay for live streams. The presentation delay is the duration by |
| * which the default start position precedes the end of the live window. |
| */ |
| public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30000; |
| /** @deprecated Use {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. */ |
| @Deprecated |
| public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = |
| DEFAULT_LIVE_PRESENTATION_DELAY_MS; |
| /** @deprecated Use of this parameter is no longer necessary. */ |
| @Deprecated public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; |
| |
| /** |
| * The interval in milliseconds between invocations of {@link |
| * MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's {@link |
| * Timeline} is changing dynamically (for example, for incomplete live streams). |
| */ |
| private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; |
| /** |
| * The minimum default start position for live streams, relative to the start of the live window. |
| */ |
| private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; |
| |
| private static final String TAG = "DashMediaSource"; |
| |
| private final boolean sideloadedManifest; |
| private final DataSource.Factory manifestDataSourceFactory; |
| private final DashChunkSource.Factory chunkSourceFactory; |
| private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; |
| private final DrmSessionManager drmSessionManager; |
| private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; |
| private final long livePresentationDelayMs; |
| private final boolean livePresentationDelayOverridesManifest; |
| private final EventDispatcher manifestEventDispatcher; |
| private final ParsingLoadable.Parser<? extends DashManifest> manifestParser; |
| private final ManifestCallback manifestCallback; |
| private final Object manifestUriLock; |
| private final SparseArray<DashMediaPeriod> periodsById; |
| private final Runnable refreshManifestRunnable; |
| private final Runnable simulateManifestRefreshRunnable; |
| private final PlayerEmsgCallback playerEmsgCallback; |
| private final LoaderErrorThrower manifestLoadErrorThrower; |
| @Nullable private final Object tag; |
| |
| private DataSource dataSource; |
| private Loader loader; |
| @Nullable private TransferListener mediaTransferListener; |
| |
| private IOException manifestFatalError; |
| private Handler handler; |
| |
| private Uri initialManifestUri; |
| private Uri manifestUri; |
| private DashManifest manifest; |
| private boolean manifestLoadPending; |
| private long manifestLoadStartTimestampMs; |
| private long manifestLoadEndTimestampMs; |
| private long elapsedRealtimeOffsetMs; |
| |
| private int staleManifestReloadAttempt; |
| private long expiredManifestPublishTimeUs; |
| |
| private int firstPeriodId; |
| |
| /** |
| * Constructs an instance to play a given {@link DashManifest}, which must be static. |
| * |
| * @param manifest The manifest. {@link DashManifest#dynamic} must be false. |
| * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. |
| * @param eventHandler A handler for events. May be null if delivery of events is not required. |
| * @param eventListener A listener of events. May be null if delivery of events is not required. |
| * @deprecated Use {@link Factory} instead. |
| */ |
| @Deprecated |
| @SuppressWarnings("deprecation") |
| public DashMediaSource( |
| DashManifest manifest, |
| DashChunkSource.Factory chunkSourceFactory, |
| @Nullable Handler eventHandler, |
| @Nullable MediaSourceEventListener eventListener) { |
| this( |
| manifest, |
| chunkSourceFactory, |
| DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT, |
| eventHandler, |
| eventListener); |
| } |
| |
| /** |
| * Constructs an instance to play a given {@link DashManifest}, which must be static. |
| * |
| * @param manifest The manifest. {@link DashManifest#dynamic} must be false. |
| * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. |
| * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. |
| * @param eventHandler A handler for events. May be null if delivery of events is not required. |
| * @param eventListener A listener of events. May be null if delivery of events is not required. |
| * @deprecated Use {@link Factory} instead. |
| */ |
| @Deprecated |
| public DashMediaSource( |
| DashManifest manifest, |
| DashChunkSource.Factory chunkSourceFactory, |
| int minLoadableRetryCount, |
| @Nullable Handler eventHandler, |
| @Nullable MediaSourceEventListener eventListener) { |
| this( |
| manifest, |
| /* manifestUri= */ null, |
| /* manifestDataSourceFactory= */ null, |
| /* manifestParser= */ null, |
| chunkSourceFactory, |
| new DefaultCompositeSequenceableLoaderFactory(), |
| DrmSessionManager.getDummyDrmSessionManager(), |
| new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), |
| DEFAULT_LIVE_PRESENTATION_DELAY_MS, |
| /* livePresentationDelayOverridesManifest= */ false, |
| /* tag= */ null); |
| if (eventHandler != null && eventListener != null) { |
| addEventListener(eventHandler, eventListener); |
| } |
| } |
| |
| /** |
| * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or |
| * static. |
| * |
| * @param manifestUri The manifest {@link Uri}. |
| * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used |
| * to load (and refresh) the manifest. |
| * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. |
| * @param eventHandler A handler for events. May be null if delivery of events is not required. |
| * @param eventListener A listener of events. May be null if delivery of events is not required. |
| * @deprecated Use {@link Factory} instead. |
| */ |
| @Deprecated |
| @SuppressWarnings("deprecation") |
| public DashMediaSource( |
| Uri manifestUri, |
| DataSource.Factory manifestDataSourceFactory, |
| DashChunkSource.Factory chunkSourceFactory, |
| @Nullable Handler eventHandler, |
| @Nullable MediaSourceEventListener eventListener) { |
| this( |
| manifestUri, |
| manifestDataSourceFactory, |
| chunkSourceFactory, |
| DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT, |
| DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, |
| eventHandler, |
| eventListener); |
| } |
| |
| /** |
| * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or |
| * static. |
| * |
| * @param manifestUri The manifest {@link Uri}. |
| * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used |
| * to load (and refresh) the manifest. |
| * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. |
| * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. |
| * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the |
| * default start position should precede the end of the live window. Use {@link |
| * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the |
| * manifest, if present. |
| * @param eventHandler A handler for events. May be null if delivery of events is not required. |
| * @param eventListener A listener of events. May be null if delivery of events is not required. |
| * @deprecated Use {@link Factory} instead. |
| */ |
| @Deprecated |
| @SuppressWarnings("deprecation") |
| public DashMediaSource( |
| Uri manifestUri, |
| DataSource.Factory manifestDataSourceFactory, |
| DashChunkSource.Factory chunkSourceFactory, |
| int minLoadableRetryCount, |
| long livePresentationDelayMs, |
| @Nullable Handler eventHandler, |
| @Nullable MediaSourceEventListener eventListener) { |
| this( |
| manifestUri, |
| manifestDataSourceFactory, |
| new DashManifestParser(), |
| chunkSourceFactory, |
| minLoadableRetryCount, |
| livePresentationDelayMs, |
| eventHandler, |
| eventListener); |
| } |
| |
| /** |
| * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or |
| * static. |
| * |
| * @param manifestUri The manifest {@link Uri}. |
| * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used |
| * to load (and refresh) the manifest. |
| * @param manifestParser A parser for loaded manifest data. |
| * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. |
| * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. |
| * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the |
| * default start position should precede the end of the live window. Use {@link |
| * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the |
| * manifest, if present. |
| * @param eventHandler A handler for events. May be null if delivery of events is not required. |
| * @param eventListener A listener of events. May be null if delivery of events is not required. |
| * @deprecated Use {@link Factory} instead. |
| */ |
| @Deprecated |
| @SuppressWarnings("deprecation") |
| public DashMediaSource( |
| Uri manifestUri, |
| DataSource.Factory manifestDataSourceFactory, |
| ParsingLoadable.Parser<? extends DashManifest> manifestParser, |
| DashChunkSource.Factory chunkSourceFactory, |
| int minLoadableRetryCount, |
| long livePresentationDelayMs, |
| @Nullable Handler eventHandler, |
| @Nullable MediaSourceEventListener eventListener) { |
| this( |
| /* manifest= */ null, |
| manifestUri, |
| manifestDataSourceFactory, |
| manifestParser, |
| chunkSourceFactory, |
| new DefaultCompositeSequenceableLoaderFactory(), |
| DrmSessionManager.getDummyDrmSessionManager(), |
| new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), |
| livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS |
| ? DEFAULT_LIVE_PRESENTATION_DELAY_MS |
| : livePresentationDelayMs, |
| livePresentationDelayMs != DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, |
| /* tag= */ null); |
| if (eventHandler != null && eventListener != null) { |
| addEventListener(eventHandler, eventListener); |
| } |
| } |
| |
| private DashMediaSource( |
| @Nullable DashManifest manifest, |
| @Nullable Uri manifestUri, |
| @Nullable DataSource.Factory manifestDataSourceFactory, |
| @Nullable ParsingLoadable.Parser<? extends DashManifest> manifestParser, |
| DashChunkSource.Factory chunkSourceFactory, |
| CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, |
| DrmSessionManager drmSessionManager, |
| LoadErrorHandlingPolicy loadErrorHandlingPolicy, |
| long livePresentationDelayMs, |
| boolean livePresentationDelayOverridesManifest, |
| @Nullable Object tag) { |
| this.initialManifestUri = manifestUri; |
| this.manifest = manifest; |
| this.manifestUri = manifestUri; |
| this.manifestDataSourceFactory = manifestDataSourceFactory; |
| this.manifestParser = manifestParser; |
| this.chunkSourceFactory = chunkSourceFactory; |
| this.drmSessionManager = drmSessionManager; |
| this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; |
| this.livePresentationDelayMs = livePresentationDelayMs; |
| this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest; |
| this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; |
| this.tag = tag; |
| sideloadedManifest = manifest != null; |
| manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); |
| manifestUriLock = new Object(); |
| periodsById = new SparseArray<>(); |
| playerEmsgCallback = new DefaultPlayerEmsgCallback(); |
| expiredManifestPublishTimeUs = C.TIME_UNSET; |
| elapsedRealtimeOffsetMs = C.TIME_UNSET; |
| if (sideloadedManifest) { |
| Assertions.checkState(!manifest.dynamic); |
| manifestCallback = null; |
| refreshManifestRunnable = null; |
| simulateManifestRefreshRunnable = null; |
| manifestLoadErrorThrower = new LoaderErrorThrower.Dummy(); |
| } else { |
| manifestCallback = new ManifestCallback(); |
| manifestLoadErrorThrower = new ManifestLoadErrorThrower(); |
| refreshManifestRunnable = this::startLoadingManifest; |
| simulateManifestRefreshRunnable = () -> processManifest(false); |
| } |
| } |
| |
| /** |
| * Manually replaces the manifest {@link Uri}. |
| * |
| * @param manifestUri The replacement manifest {@link Uri}. |
| */ |
| public void replaceManifestUri(Uri manifestUri) { |
| synchronized (manifestUriLock) { |
| this.manifestUri = manifestUri; |
| this.initialManifestUri = manifestUri; |
| } |
| } |
| |
| // MediaSource implementation. |
| |
| @Override |
| @Nullable |
| public Object getTag() { |
| return tag; |
| } |
| |
| @Override |
| protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { |
| this.mediaTransferListener = mediaTransferListener; |
| drmSessionManager.prepare(); |
| if (sideloadedManifest) { |
| processManifest(false); |
| } else { |
| dataSource = manifestDataSourceFactory.createDataSource(); |
| loader = new Loader("Loader:DashMediaSource"); |
| handler = Util.createHandler(); |
| startLoadingManifest(); |
| } |
| } |
| |
| @Override |
| public void maybeThrowSourceInfoRefreshError() throws IOException { |
| manifestLoadErrorThrower.maybeThrowError(); |
| } |
| |
| @Override |
| public MediaPeriod createPeriod( |
| MediaPeriodId periodId, Allocator allocator, long startPositionUs) { |
| int periodIndex = (Integer) periodId.periodUid - firstPeriodId; |
| EventDispatcher periodEventDispatcher = |
| createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs); |
| DashMediaPeriod mediaPeriod = |
| new DashMediaPeriod( |
| firstPeriodId + periodIndex, |
| manifest, |
| periodIndex, |
| chunkSourceFactory, |
| mediaTransferListener, |
| drmSessionManager, |
| loadErrorHandlingPolicy, |
| periodEventDispatcher, |
| elapsedRealtimeOffsetMs, |
| manifestLoadErrorThrower, |
| allocator, |
| compositeSequenceableLoaderFactory, |
| playerEmsgCallback); |
| periodsById.put(mediaPeriod.id, mediaPeriod); |
| return mediaPeriod; |
| } |
| |
| @Override |
| public void releasePeriod(MediaPeriod mediaPeriod) { |
| DashMediaPeriod dashMediaPeriod = (DashMediaPeriod) mediaPeriod; |
| dashMediaPeriod.release(); |
| periodsById.remove(dashMediaPeriod.id); |
| } |
| |
| @Override |
| protected void releaseSourceInternal() { |
| manifestLoadPending = false; |
| dataSource = null; |
| if (loader != null) { |
| loader.release(); |
| loader = null; |
| } |
| manifestLoadStartTimestampMs = 0; |
| manifestLoadEndTimestampMs = 0; |
| manifest = sideloadedManifest ? manifest : null; |
| manifestUri = initialManifestUri; |
| manifestFatalError = null; |
| if (handler != null) { |
| handler.removeCallbacksAndMessages(null); |
| handler = null; |
| } |
| elapsedRealtimeOffsetMs = C.TIME_UNSET; |
| staleManifestReloadAttempt = 0; |
| expiredManifestPublishTimeUs = C.TIME_UNSET; |
| firstPeriodId = 0; |
| periodsById.clear(); |
| drmSessionManager.release(); |
| } |
| |
| // PlayerEmsgCallback callbacks. |
| |
| /* package */ void onDashManifestRefreshRequested() { |
| handler.removeCallbacks(simulateManifestRefreshRunnable); |
| startLoadingManifest(); |
| } |
| |
| /* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { |
| if (this.expiredManifestPublishTimeUs == C.TIME_UNSET |
| || this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) { |
| this.expiredManifestPublishTimeUs = expiredManifestPublishTimeUs; |
| } |
| } |
| |
| // Loadable callbacks. |
| |
| /* package */ void onManifestLoadCompleted(ParsingLoadable<DashManifest> loadable, |
| long elapsedRealtimeMs, long loadDurationMs) { |
| manifestEventDispatcher.loadCompleted( |
| new LoadEventInfo( |
| loadable.dataSpec, |
| loadable.getUri(), |
| loadable.getResponseHeaders(), |
| elapsedRealtimeMs, |
| loadDurationMs, |
| loadable.bytesLoaded()), |
| loadable.type); |
| DashManifest newManifest = loadable.getResult(); |
| |
| int oldPeriodCount = manifest == null ? 0 : manifest.getPeriodCount(); |
| int removedPeriodCount = 0; |
| long newFirstPeriodStartTimeMs = newManifest.getPeriod(0).startMs; |
| while (removedPeriodCount < oldPeriodCount |
| && manifest.getPeriod(removedPeriodCount).startMs < newFirstPeriodStartTimeMs) { |
| removedPeriodCount++; |
| } |
| |
| if (newManifest.dynamic) { |
| boolean isManifestStale = false; |
| if (oldPeriodCount - removedPeriodCount > newManifest.getPeriodCount()) { |
| // After discarding old periods, we should never have more periods than listed in the new |
| // manifest. That would mean that a previously announced period is no longer advertised. If |
| // this condition occurs, assume that we are hitting a manifest server that is out of sync |
| // and |
| // behind. |
| Log.w(TAG, "Loaded out of sync manifest"); |
| isManifestStale = true; |
| } else if (expiredManifestPublishTimeUs != C.TIME_UNSET |
| && newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs) { |
| // If we receive a dynamic manifest that's older than expected (i.e. its publish time has |
| // expired, or it's dynamic and we know the presentation has ended), then this manifest is |
| // stale. |
| Log.w( |
| TAG, |
| "Loaded stale dynamic manifest: " |
| + newManifest.publishTimeMs |
| + ", " |
| + expiredManifestPublishTimeUs); |
| isManifestStale = true; |
| } |
| |
| if (isManifestStale) { |
| if (staleManifestReloadAttempt++ |
| < loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)) { |
| scheduleManifestRefresh(getManifestLoadRetryDelayMillis()); |
| } else { |
| manifestFatalError = new DashManifestStaleException(); |
| } |
| return; |
| } |
| staleManifestReloadAttempt = 0; |
| } |
| |
| manifest = newManifest; |
| manifestLoadPending &= manifest.dynamic; |
| manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; |
| manifestLoadEndTimestampMs = elapsedRealtimeMs; |
| |
| synchronized (manifestUriLock) { |
| // Checks whether replaceManifestUri(Uri) was called to manually replace the URI between the |
| // start and end of this load. If it was then isSameUriInstance evaluates to false, and we |
| // prefer the manual replacement to one derived from the previous request. |
| @SuppressWarnings("ReferenceEquality") |
| boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; |
| if (isSameUriInstance) { |
| // Replace the manifest URI with one specified by a manifest Location element (if present), |
| // or with the final (possibly redirected) URI. This follows the recommendation in |
| // DASH-IF-IOP 4.3, section 3.2.15.3. See: https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf. |
| manifestUri = manifest.location != null ? manifest.location : loadable.getUri(); |
| } |
| } |
| |
| if (oldPeriodCount == 0) { |
| if (manifest.dynamic) { |
| if (manifest.utcTiming != null) { |
| resolveUtcTimingElement(manifest.utcTiming); |
| } else { |
| loadNtpTimeOffset(); |
| } |
| } else { |
| processManifest(true); |
| } |
| } else { |
| firstPeriodId += removedPeriodCount; |
| processManifest(true); |
| } |
| } |
| |
| /* package */ LoadErrorAction onManifestLoadError( |
| ParsingLoadable<DashManifest> loadable, |
| long elapsedRealtimeMs, |
| long loadDurationMs, |
| IOException error, |
| int errorCount) { |
| long retryDelayMs = |
| loadErrorHandlingPolicy.getRetryDelayMsFor( |
| C.DATA_TYPE_MANIFEST, loadDurationMs, error, errorCount); |
| LoadErrorAction loadErrorAction = |
| retryDelayMs == C.TIME_UNSET |
| ? Loader.DONT_RETRY_FATAL |
| : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs); |
| manifestEventDispatcher.loadError( |
| new LoadEventInfo( |
| loadable.dataSpec, |
| loadable.getUri(), |
| loadable.getResponseHeaders(), |
| elapsedRealtimeMs, |
| loadDurationMs, |
| loadable.bytesLoaded()), |
| loadable.type, |
| error, |
| !loadErrorAction.isRetry()); |
| return loadErrorAction; |
| } |
| |
| /* package */ void onUtcTimestampLoadCompleted(ParsingLoadable<Long> loadable, |
| long elapsedRealtimeMs, long loadDurationMs) { |
| manifestEventDispatcher.loadCompleted( |
| new LoadEventInfo( |
| loadable.dataSpec, |
| loadable.getUri(), |
| loadable.getResponseHeaders(), |
| elapsedRealtimeMs, |
| loadDurationMs, |
| loadable.bytesLoaded()), |
| loadable.type); |
| onUtcTimestampResolved(loadable.getResult() - elapsedRealtimeMs); |
| } |
| |
| /* package */ LoadErrorAction onUtcTimestampLoadError( |
| ParsingLoadable<Long> loadable, |
| long elapsedRealtimeMs, |
| long loadDurationMs, |
| IOException error) { |
| manifestEventDispatcher.loadError( |
| new LoadEventInfo( |
| loadable.dataSpec, |
| loadable.getUri(), |
| loadable.getResponseHeaders(), |
| elapsedRealtimeMs, |
| loadDurationMs, |
| loadable.bytesLoaded()), |
| loadable.type, |
| error, |
| /* wasCanceled= */ true); |
| onUtcTimestampResolutionError(error); |
| return Loader.DONT_RETRY; |
| } |
| |
| /* package */ void onLoadCanceled(ParsingLoadable<?> loadable, long elapsedRealtimeMs, |
| long loadDurationMs) { |
| manifestEventDispatcher.loadCanceled( |
| new LoadEventInfo( |
| loadable.dataSpec, |
| loadable.getUri(), |
| loadable.getResponseHeaders(), |
| elapsedRealtimeMs, |
| loadDurationMs, |
| loadable.bytesLoaded()), |
| loadable.type); |
| } |
| |
| // Internal methods. |
| |
| private void resolveUtcTimingElement(UtcTimingElement timingElement) { |
| String scheme = timingElement.schemeIdUri; |
| if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2014") |
| || Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { |
| resolveUtcTimingElementDirect(timingElement); |
| } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014") |
| || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2012")) { |
| resolveUtcTimingElementHttp(timingElement, new Iso8601Parser()); |
| } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014") |
| || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")) { |
| resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser()); |
| } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:ntp:2014") |
| || Util.areEqual(scheme, "urn:mpeg:dash:utc:ntp:2012")) { |
| loadNtpTimeOffset(); |
| } else { |
| // Unsupported scheme. |
| onUtcTimestampResolutionError(new IOException("Unsupported UTC timing scheme")); |
| } |
| } |
| |
| private void resolveUtcTimingElementDirect(UtcTimingElement timingElement) { |
| try { |
| long utcTimestampMs = Util.parseXsDateTime(timingElement.value); |
| onUtcTimestampResolved(utcTimestampMs - manifestLoadEndTimestampMs); |
| } catch (ParserException e) { |
| onUtcTimestampResolutionError(e); |
| } |
| } |
| |
| private void resolveUtcTimingElementHttp(UtcTimingElement timingElement, |
| ParsingLoadable.Parser<Long> parser) { |
| startLoading(new ParsingLoadable<>(dataSource, Uri.parse(timingElement.value), |
| C.DATA_TYPE_TIME_SYNCHRONIZATION, parser), new UtcTimestampCallback(), 1); |
| } |
| |
| private void loadNtpTimeOffset() { |
| SntpClient.initialize( |
| loader, |
| new SntpClient.InitializationCallback() { |
| @Override |
| public void onInitialized() { |
| onUtcTimestampResolved(SntpClient.getElapsedRealtimeOffsetMs()); |
| } |
| |
| @Override |
| public void onInitializationFailed(IOException error) { |
| onUtcTimestampResolutionError(error); |
| } |
| }); |
| } |
| |
| private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) { |
| this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; |
| processManifest(true); |
| } |
| |
| private void onUtcTimestampResolutionError(IOException error) { |
| Log.e(TAG, "Failed to resolve time offset.", error); |
| // Be optimistic and continue in the hope that the device clock is correct. |
| processManifest(true); |
| } |
| |
| private void processManifest(boolean scheduleRefresh) { |
| // Update any periods. |
| for (int i = 0; i < periodsById.size(); i++) { |
| int id = periodsById.keyAt(i); |
| if (id >= firstPeriodId) { |
| periodsById.valueAt(i).updateManifest(manifest, id - firstPeriodId); |
| } else { |
| // This period has been removed from the manifest so it doesn't need to be updated. |
| } |
| } |
| // Update the window. |
| boolean windowChangingImplicitly = false; |
| int lastPeriodIndex = manifest.getPeriodCount() - 1; |
| PeriodSeekInfo firstPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(manifest.getPeriod(0), |
| manifest.getPeriodDurationUs(0)); |
| PeriodSeekInfo lastPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo( |
| manifest.getPeriod(lastPeriodIndex), manifest.getPeriodDurationUs(lastPeriodIndex)); |
| // Get the period-relative start/end times. |
| long currentStartTimeUs = firstPeriodSeekInfo.availableStartTimeUs; |
| long currentEndTimeUs = lastPeriodSeekInfo.availableEndTimeUs; |
| if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { |
| // The manifest describes an incomplete live stream. Update the start/end times to reflect the |
| // live stream duration and the manifest's time shift buffer depth. |
| long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); |
| long liveStreamDurationUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs); |
| long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs |
| - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs); |
| currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); |
| if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { |
| long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); |
| long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs; |
| int periodIndex = lastPeriodIndex; |
| while (offsetInPeriodUs < 0 && periodIndex > 0) { |
| offsetInPeriodUs += manifest.getPeriodDurationUs(--periodIndex); |
| } |
| if (periodIndex == 0) { |
| currentStartTimeUs = Math.max(currentStartTimeUs, offsetInPeriodUs); |
| } else { |
| // The time shift buffer starts after the earliest period. |
| // TODO: Does this ever happen? |
| currentStartTimeUs = manifest.getPeriodDurationUs(0); |
| } |
| } |
| windowChangingImplicitly = true; |
| } |
| long windowDurationUs = currentEndTimeUs - currentStartTimeUs; |
| for (int i = 0; i < manifest.getPeriodCount() - 1; i++) { |
| windowDurationUs += manifest.getPeriodDurationUs(i); |
| } |
| long windowDefaultStartPositionUs = 0; |
| if (manifest.dynamic) { |
| long presentationDelayForManifestMs = livePresentationDelayMs; |
| if (!livePresentationDelayOverridesManifest |
| && manifest.suggestedPresentationDelayMs != C.TIME_UNSET) { |
| presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs; |
| } |
| // Snap the default position to the start of the segment containing it. |
| windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); |
| if (windowDefaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { |
| // The default start position is too close to the start of the live window. Set it to the |
| // minimum default start position provided the window is at least twice as big. Else set |
| // it to the middle of the window. |
| windowDefaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, |
| windowDurationUs / 2); |
| } |
| } |
| long windowStartTimeMs = C.TIME_UNSET; |
| if (manifest.availabilityStartTimeMs != C.TIME_UNSET) { |
| windowStartTimeMs = |
| manifest.availabilityStartTimeMs |
| + manifest.getPeriod(0).startMs |
| + C.usToMs(currentStartTimeUs); |
| } |
| DashTimeline timeline = |
| new DashTimeline( |
| manifest.availabilityStartTimeMs, |
| windowStartTimeMs, |
| elapsedRealtimeOffsetMs, |
| firstPeriodId, |
| currentStartTimeUs, |
| windowDurationUs, |
| windowDefaultStartPositionUs, |
| manifest, |
| tag); |
| refreshSourceInfo(timeline); |
| |
| if (!sideloadedManifest) { |
| // Remove any pending simulated refresh. |
| handler.removeCallbacks(simulateManifestRefreshRunnable); |
| // If the window is changing implicitly, post a simulated manifest refresh to update it. |
| if (windowChangingImplicitly) { |
| handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); |
| } |
| if (manifestLoadPending) { |
| startLoadingManifest(); |
| } else if (scheduleRefresh |
| && manifest.dynamic |
| && manifest.minUpdatePeriodMs != C.TIME_UNSET) { |
| // Schedule an explicit refresh if needed. |
| long minUpdatePeriodMs = manifest.minUpdatePeriodMs; |
| if (minUpdatePeriodMs == 0) { |
| // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where |
| // minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is |
| // explicit signaling in the stream, according to: |
| // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service |
| minUpdatePeriodMs = 5000; |
| } |
| long nextLoadTimestampMs = manifestLoadStartTimestampMs + minUpdatePeriodMs; |
| long delayUntilNextLoadMs = |
| Math.max(0, nextLoadTimestampMs - SystemClock.elapsedRealtime()); |
| scheduleManifestRefresh(delayUntilNextLoadMs); |
| } |
| } |
| } |
| |
| private void scheduleManifestRefresh(long delayUntilNextLoadMs) { |
| handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs); |
| } |
| |
| private void startLoadingManifest() { |
| handler.removeCallbacks(refreshManifestRunnable); |
| if (loader.hasFatalError()) { |
| return; |
| } |
| if (loader.isLoading()) { |
| manifestLoadPending = true; |
| return; |
| } |
| Uri manifestUri; |
| synchronized (manifestUriLock) { |
| manifestUri = this.manifestUri; |
| } |
| manifestLoadPending = false; |
| startLoading( |
| new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), |
| manifestCallback, |
| loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST)); |
| } |
| |
| private long getManifestLoadRetryDelayMillis() { |
| return Math.min((staleManifestReloadAttempt - 1) * 1000, 5000); |
| } |
| |
| private <T> void startLoading(ParsingLoadable<T> loadable, |
| Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) { |
| long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount); |
| manifestEventDispatcher.loadStarted( |
| new LoadEventInfo(loadable.dataSpec, elapsedRealtimeMs), loadable.type); |
| } |
| |
| private static final class PeriodSeekInfo { |
| |
| public static PeriodSeekInfo createPeriodSeekInfo( |
| com.google.android.exoplayer2.source.dash.manifest.Period period, long durationUs) { |
| int adaptationSetCount = period.adaptationSets.size(); |
| long availableStartTimeUs = 0; |
| long availableEndTimeUs = Long.MAX_VALUE; |
| boolean isIndexExplicit = false; |
| boolean seenEmptyIndex = false; |
| |
| boolean haveAudioVideoAdaptationSets = false; |
| for (int i = 0; i < adaptationSetCount; i++) { |
| int type = period.adaptationSets.get(i).type; |
| if (type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO) { |
| haveAudioVideoAdaptationSets = true; |
| break; |
| } |
| } |
| |
| for (int i = 0; i < adaptationSetCount; i++) { |
| AdaptationSet adaptationSet = period.adaptationSets.get(i); |
| // Exclude text adaptation sets from duration calculations, if we have at least one audio |
| // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 |
| if (haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) { |
| continue; |
| } |
| |
| DashSegmentIndex index = adaptationSet.representations.get(0).getIndex(); |
| if (index == null) { |
| return new PeriodSeekInfo(true, 0, durationUs); |
| } |
| isIndexExplicit |= index.isExplicit(); |
| int segmentCount = index.getSegmentCount(durationUs); |
| if (segmentCount == 0) { |
| seenEmptyIndex = true; |
| availableStartTimeUs = 0; |
| availableEndTimeUs = 0; |
| } else if (!seenEmptyIndex) { |
| long firstSegmentNum = index.getFirstSegmentNum(); |
| long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); |
| availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); |
| if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) { |
| long lastSegmentNum = firstSegmentNum + segmentCount - 1; |
| long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) |
| + index.getDurationUs(lastSegmentNum, durationUs); |
| availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); |
| } |
| } |
| } |
| return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); |
| } |
| |
| public final boolean isIndexExplicit; |
| public final long availableStartTimeUs; |
| public final long availableEndTimeUs; |
| |
| private PeriodSeekInfo(boolean isIndexExplicit, long availableStartTimeUs, |
| long availableEndTimeUs) { |
| this.isIndexExplicit = isIndexExplicit; |
| this.availableStartTimeUs = availableStartTimeUs; |
| this.availableEndTimeUs = availableEndTimeUs; |
| } |
| |
| } |
| |
| private static final class DashTimeline extends Timeline { |
| |
| private final long presentationStartTimeMs; |
| private final long windowStartTimeMs; |
| private final long elapsedRealtimeEpochOffsetMs; |
| |
| private final int firstPeriodId; |
| private final long offsetInFirstPeriodUs; |
| private final long windowDurationUs; |
| private final long windowDefaultStartPositionUs; |
| private final DashManifest manifest; |
| @Nullable private final Object windowTag; |
| |
| public DashTimeline( |
| long presentationStartTimeMs, |
| long windowStartTimeMs, |
| long elapsedRealtimeEpochOffsetMs, |
| int firstPeriodId, |
| long offsetInFirstPeriodUs, |
| long windowDurationUs, |
| long windowDefaultStartPositionUs, |
| DashManifest manifest, |
| @Nullable Object windowTag) { |
| this.presentationStartTimeMs = presentationStartTimeMs; |
| this.windowStartTimeMs = windowStartTimeMs; |
| this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; |
| this.firstPeriodId = firstPeriodId; |
| this.offsetInFirstPeriodUs = offsetInFirstPeriodUs; |
| this.windowDurationUs = windowDurationUs; |
| this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; |
| this.manifest = manifest; |
| this.windowTag = windowTag; |
| } |
| |
| @Override |
| public int getPeriodCount() { |
| return manifest.getPeriodCount(); |
| } |
| |
| @Override |
| public Period getPeriod(int periodIndex, Period period, boolean setIdentifiers) { |
| Assertions.checkIndex(periodIndex, 0, getPeriodCount()); |
| Object id = setIdentifiers ? manifest.getPeriod(periodIndex).id : null; |
| Object uid = setIdentifiers ? (firstPeriodId + periodIndex) : null; |
| return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), |
| C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) |
| - offsetInFirstPeriodUs); |
| } |
| |
| @Override |
| public int getWindowCount() { |
| return 1; |
| } |
| |
| @Override |
| public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { |
| Assertions.checkIndex(windowIndex, 0, 1); |
| long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( |
| defaultPositionProjectionUs); |
| return window.set( |
| Window.SINGLE_WINDOW_UID, |
| windowTag, |
| manifest, |
| presentationStartTimeMs, |
| windowStartTimeMs, |
| elapsedRealtimeEpochOffsetMs, |
| /* isSeekable= */ true, |
| /* isDynamic= */ isMovingLiveWindow(manifest), |
| /* isLive= */ manifest.dynamic, |
| windowDefaultStartPositionUs, |
| windowDurationUs, |
| /* firstPeriodIndex= */ 0, |
| /* lastPeriodIndex= */ getPeriodCount() - 1, |
| offsetInFirstPeriodUs); |
| } |
| |
| @Override |
| public int getIndexOfPeriod(Object uid) { |
| if (!(uid instanceof Integer)) { |
| return C.INDEX_UNSET; |
| } |
| int periodId = (int) uid; |
| int periodIndex = periodId - firstPeriodId; |
| return periodIndex < 0 || periodIndex >= getPeriodCount() ? C.INDEX_UNSET : periodIndex; |
| } |
| |
| private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) { |
| long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; |
| if (!isMovingLiveWindow(manifest)) { |
| return windowDefaultStartPositionUs; |
| } |
| if (defaultPositionProjectionUs > 0) { |
| windowDefaultStartPositionUs += defaultPositionProjectionUs; |
| if (windowDefaultStartPositionUs > windowDurationUs) { |
| // The projection takes us beyond the end of the live window. |
| return C.TIME_UNSET; |
| } |
| } |
| // Attempt to snap to the start of the corresponding video segment. |
| int periodIndex = 0; |
| long defaultStartPositionInPeriodUs = offsetInFirstPeriodUs + windowDefaultStartPositionUs; |
| long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); |
| while (periodIndex < manifest.getPeriodCount() - 1 |
| && defaultStartPositionInPeriodUs >= periodDurationUs) { |
| defaultStartPositionInPeriodUs -= periodDurationUs; |
| periodIndex++; |
| periodDurationUs = manifest.getPeriodDurationUs(periodIndex); |
| } |
| com.google.android.exoplayer2.source.dash.manifest.Period period = |
| manifest.getPeriod(periodIndex); |
| int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); |
| if (videoAdaptationSetIndex == C.INDEX_UNSET) { |
| // No video adaptation set for snapping. |
| return windowDefaultStartPositionUs; |
| } |
| // If there are multiple video adaptation sets with unaligned segments, the initial time may |
| // not correspond to the start of a segment in both, but this is an edge case. |
| DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) |
| .representations.get(0).getIndex(); |
| if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) { |
| // Video adaptation set does not include a non-empty index for snapping. |
| return windowDefaultStartPositionUs; |
| } |
| long segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); |
| return windowDefaultStartPositionUs + snapIndex.getTimeUs(segmentNum) |
| - defaultStartPositionInPeriodUs; |
| } |
| |
| @Override |
| public Object getUidOfPeriod(int periodIndex) { |
| Assertions.checkIndex(periodIndex, 0, getPeriodCount()); |
| return firstPeriodId + periodIndex; |
| } |
| |
| private static boolean isMovingLiveWindow(DashManifest manifest) { |
| return manifest.dynamic |
| && manifest.minUpdatePeriodMs != C.TIME_UNSET |
| && manifest.durationMs == C.TIME_UNSET; |
| } |
| } |
| |
| private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback { |
| |
| @Override |
| public void onDashManifestRefreshRequested() { |
| DashMediaSource.this.onDashManifestRefreshRequested(); |
| } |
| |
| @Override |
| public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { |
| DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); |
| } |
| } |
| |
| private final class ManifestCallback implements Loader.Callback<ParsingLoadable<DashManifest>> { |
| |
| @Override |
| public void onLoadCompleted( |
| ParsingLoadable<DashManifest> loadable, long elapsedRealtimeMs, long loadDurationMs) { |
| onManifestLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs); |
| } |
| |
| @Override |
| public void onLoadCanceled( |
| ParsingLoadable<DashManifest> loadable, |
| long elapsedRealtimeMs, |
| long loadDurationMs, |
| boolean released) { |
| DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs); |
| } |
| |
| @Override |
| public LoadErrorAction onLoadError( |
| ParsingLoadable<DashManifest> loadable, |
| long elapsedRealtimeMs, |
| long loadDurationMs, |
| IOException error, |
| int errorCount) { |
| return onManifestLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error, errorCount); |
| } |
| |
| } |
| |
| private final class UtcTimestampCallback implements Loader.Callback<ParsingLoadable<Long>> { |
| |
| @Override |
| public void onLoadCompleted( |
| ParsingLoadable<Long> loadable, long elapsedRealtimeMs, long loadDurationMs) { |
| onUtcTimestampLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs); |
| } |
| |
| @Override |
| public void onLoadCanceled( |
| ParsingLoadable<Long> loadable, |
| long elapsedRealtimeMs, |
| long loadDurationMs, |
| boolean released) { |
| DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs); |
| } |
| |
| @Override |
| public LoadErrorAction onLoadError( |
| ParsingLoadable<Long> loadable, |
| long elapsedRealtimeMs, |
| long loadDurationMs, |
| IOException error, |
| int errorCount) { |
| return onUtcTimestampLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error); |
| } |
| |
| } |
| |
| private static final class XsDateTimeParser implements ParsingLoadable.Parser<Long> { |
| |
| @Override |
| public Long parse(Uri uri, InputStream inputStream) throws IOException { |
| String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); |
| return Util.parseXsDateTime(firstLine); |
| } |
| |
| } |
| |
| /* package */ static final class Iso8601Parser implements ParsingLoadable.Parser<Long> { |
| |
| private static final Pattern TIMESTAMP_WITH_TIMEZONE_PATTERN = |
| Pattern.compile("(.+?)(Z|((\\+|-|−)(\\d\\d)(:?(\\d\\d))?))"); |
| |
| @Override |
| public Long parse(Uri uri, InputStream inputStream) throws IOException { |
| String firstLine = |
| new BufferedReader(new InputStreamReader(inputStream, Charset.forName(C.UTF8_NAME))) |
| .readLine(); |
| try { |
| Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); |
| if (!matcher.matches()) { |
| throw new ParserException("Couldn't parse timestamp: " + firstLine); |
| } |
| // Parse the timestamp. |
| String timestampWithoutTimezone = matcher.group(1); |
| SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); |
| format.setTimeZone(TimeZone.getTimeZone("UTC")); |
| long timestampMs = format.parse(timestampWithoutTimezone).getTime(); |
| // Parse the timezone. |
| String timezone = matcher.group(2); |
| if ("Z".equals(timezone)) { |
| // UTC (no offset). |
| } else { |
| long sign = "+".equals(matcher.group(4)) ? 1 : -1; |
| long hours = Long.parseLong(matcher.group(5)); |
| String minutesString = matcher.group(7); |
| long minutes = TextUtils.isEmpty(minutesString) ? 0 : Long.parseLong(minutesString); |
| long timestampOffsetMs = sign * (((hours * 60) + minutes) * 60 * 1000); |
| timestampMs -= timestampOffsetMs; |
| } |
| return timestampMs; |
| } catch (ParseException e) { |
| throw new ParserException(e); |
| } |
| } |
| |
| } |
| |
| /** |
| * A {@link LoaderErrorThrower} that throws fatal {@link IOException} that has occurred during |
| * manifest loading from the manifest {@code loader}, or exception with the loaded manifest. |
| */ |
| /* package */ final class ManifestLoadErrorThrower implements LoaderErrorThrower { |
| |
| @Override |
| public void maybeThrowError() throws IOException { |
| loader.maybeThrowError(); |
| maybeThrowManifestError(); |
| } |
| |
| @Override |
| public void maybeThrowError(int minRetryCount) throws IOException { |
| loader.maybeThrowError(minRetryCount); |
| maybeThrowManifestError(); |
| } |
| |
| private void maybeThrowManifestError() throws IOException { |
| if (manifestFatalError != null) { |
| throw manifestFatalError; |
| } |
| } |
| } |
| } |