| /* |
| * 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 com.google.android.exoplayer2.offline; |
| |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Message; |
| import android.util.SparseIntArray; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.ExoPlaybackException; |
| import com.google.android.exoplayer2.Renderer; |
| import com.google.android.exoplayer2.RendererCapabilities; |
| import com.google.android.exoplayer2.RenderersFactory; |
| import com.google.android.exoplayer2.Timeline; |
| import com.google.android.exoplayer2.audio.AudioRendererEventListener; |
| import com.google.android.exoplayer2.drm.DrmSessionManager; |
| import com.google.android.exoplayer2.source.MediaPeriod; |
| import com.google.android.exoplayer2.source.MediaSource; |
| import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; |
| import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; |
| import com.google.android.exoplayer2.source.MediaSourceFactory; |
| import com.google.android.exoplayer2.source.ProgressiveMediaSource; |
| import com.google.android.exoplayer2.source.TrackGroup; |
| import com.google.android.exoplayer2.source.TrackGroupArray; |
| import com.google.android.exoplayer2.source.chunk.MediaChunk; |
| import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; |
| import com.google.android.exoplayer2.trackselection.BaseTrackSelection; |
| import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; |
| import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; |
| import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; |
| import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; |
| import com.google.android.exoplayer2.trackselection.TrackSelection; |
| import com.google.android.exoplayer2.trackselection.TrackSelectorResult; |
| import com.google.android.exoplayer2.upstream.Allocator; |
| import com.google.android.exoplayer2.upstream.BandwidthMeter; |
| import com.google.android.exoplayer2.upstream.DataSource; |
| import com.google.android.exoplayer2.upstream.DataSource.Factory; |
| import com.google.android.exoplayer2.upstream.DefaultAllocator; |
| import com.google.android.exoplayer2.upstream.TransferListener; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.Util; |
| import com.google.android.exoplayer2.video.VideoRendererEventListener; |
| import java.io.IOException; |
| import java.lang.reflect.Constructor; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import org.checkerframework.checker.nullness.compatqual.NullableType; |
| import org.checkerframework.checker.nullness.qual.EnsuresNonNull; |
| import org.checkerframework.checker.nullness.qual.MonotonicNonNull; |
| import org.checkerframework.checker.nullness.qual.RequiresNonNull; |
| |
| /** |
| * A helper for initializing and removing downloads. |
| * |
| * <p>The helper extracts track information from the media, selects tracks for downloading, and |
| * creates {@link DownloadRequest download requests} based on the selected tracks. |
| * |
| * <p>A typical usage of DownloadHelper follows these steps: |
| * |
| * <ol> |
| * <li>Build the helper using one of the {@code forXXX} methods. |
| * <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback. |
| * <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link |
| * #getTrackSelections(int, int)}, and make adjustments using {@link |
| * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link |
| * #addTrackSelection(int, Parameters)}. |
| * <li>Create a download request for the selected track using {@link #getDownloadRequest(byte[])}. |
| * <li>Release the helper using {@link #release()}. |
| * </ol> |
| */ |
| public final class DownloadHelper { |
| |
| /** |
| * Default track selection parameters for downloading, but without any {@link Context} |
| * constraints. |
| * |
| * <p>If possible, use {@link #getDefaultTrackSelectorParameters(Context)} instead. |
| * |
| * @see Parameters#DEFAULT_WITHOUT_CONTEXT |
| */ |
| public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT = |
| Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().setForceHighestSupportedBitrate(true).build(); |
| |
| /** |
| * @deprecated This instance does not have {@link Context} constraints. Use {@link |
| * #getDefaultTrackSelectorParameters(Context)} instead. |
| */ |
| @Deprecated |
| public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT = |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; |
| |
| /** |
| * @deprecated This instance does not have {@link Context} constraints. Use {@link |
| * #getDefaultTrackSelectorParameters(Context)} instead. |
| */ |
| @Deprecated |
| public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS = |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; |
| |
| /** Returns the default parameters used for track selection for downloading. */ |
| public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) { |
| return Parameters.getDefaults(context) |
| .buildUpon() |
| .setForceHighestSupportedBitrate(true) |
| .build(); |
| } |
| |
| /** A callback to be notified when the {@link DownloadHelper} is prepared. */ |
| public interface Callback { |
| |
| /** |
| * Called when preparation completes. |
| * |
| * @param helper The reporting {@link DownloadHelper}. |
| */ |
| void onPrepared(DownloadHelper helper); |
| |
| /** |
| * Called when preparation fails. |
| * |
| * @param helper The reporting {@link DownloadHelper}. |
| * @param e The error. |
| */ |
| void onPrepareError(DownloadHelper helper, IOException e); |
| } |
| |
| /** Thrown at an attempt to download live content. */ |
| public static class LiveContentUnsupportedException extends IOException {} |
| |
| @Nullable |
| private static final Constructor<? extends MediaSourceFactory> DASH_FACTORY_CONSTRUCTOR = |
| getConstructor("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); |
| |
| @Nullable |
| private static final Constructor<? extends MediaSourceFactory> SS_FACTORY_CONSTRUCTOR = |
| getConstructor("com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); |
| |
| @Nullable |
| private static final Constructor<? extends MediaSourceFactory> HLS_FACTORY_CONSTRUCTOR = |
| getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); |
| |
| /** |
| * Extracts renderer capabilities for the renderers created by the provided renderers factory. |
| * |
| * @param renderersFactory A {@link RenderersFactory}. |
| * @return The {@link RendererCapabilities} for each renderer created by the {@code |
| * renderersFactory}. |
| */ |
| public static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) { |
| Renderer[] renderers = |
| renderersFactory.createRenderers( |
| Util.createHandler(), |
| new VideoRendererEventListener() {}, |
| new AudioRendererEventListener() {}, |
| (cues) -> {}, |
| (metadata) -> {}); |
| RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length]; |
| for (int i = 0; i < renderers.length; i++) { |
| capabilities[i] = renderers[i].getCapabilities(); |
| } |
| return capabilities; |
| } |
| |
| /** @deprecated Use {@link #forProgressive(Context, Uri)} */ |
| @Deprecated |
| @SuppressWarnings("deprecation") |
| public static DownloadHelper forProgressive(Uri uri) { |
| return forProgressive(uri, /* cacheKey= */ null); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for progressive streams. |
| * |
| * @param context Any {@link Context}. |
| * @param uri A stream {@link Uri}. |
| * @return A {@link DownloadHelper} for progressive streams. |
| */ |
| public static DownloadHelper forProgressive(Context context, Uri uri) { |
| return forProgressive(context, uri, /* cacheKey= */ null); |
| } |
| |
| /** @deprecated Use {@link #forProgressive(Context, Uri, String)} */ |
| @Deprecated |
| public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) { |
| return new DownloadHelper( |
| DownloadRequest.TYPE_PROGRESSIVE, |
| uri, |
| cacheKey, |
| /* mediaSource= */ null, |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, |
| /* rendererCapabilities= */ new RendererCapabilities[0]); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for progressive streams. |
| * |
| * @param context Any {@link Context}. |
| * @param uri A stream {@link Uri}. |
| * @param cacheKey An optional cache key. |
| * @return A {@link DownloadHelper} for progressive streams. |
| */ |
| public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) { |
| return new DownloadHelper( |
| DownloadRequest.TYPE_PROGRESSIVE, |
| uri, |
| cacheKey, |
| /* mediaSource= */ null, |
| getDefaultTrackSelectorParameters(context), |
| /* rendererCapabilities= */ new RendererCapabilities[0]); |
| } |
| |
| /** @deprecated Use {@link #forDash(Context, Uri, Factory, RenderersFactory)} */ |
| @Deprecated |
| public static DownloadHelper forDash( |
| Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { |
| return forDash( |
| uri, |
| dataSourceFactory, |
| renderersFactory, |
| /* drmSessionManager= */ null, |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for DASH streams. |
| * |
| * @param context Any {@link Context}. |
| * @param uri A manifest {@link Uri}. |
| * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. |
| * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are |
| * selected. |
| * @return A {@link DownloadHelper} for DASH streams. |
| * @throws IllegalStateException If the DASH module is missing. |
| */ |
| public static DownloadHelper forDash( |
| Context context, |
| Uri uri, |
| DataSource.Factory dataSourceFactory, |
| RenderersFactory renderersFactory) { |
| return forDash( |
| uri, |
| dataSourceFactory, |
| renderersFactory, |
| /* drmSessionManager= */ null, |
| getDefaultTrackSelectorParameters(context)); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for DASH streams. |
| * |
| * @param uri A manifest {@link Uri}. |
| * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. |
| * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are |
| * selected. |
| * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which |
| * tracks can be selected. |
| * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for |
| * downloading. |
| * @return A {@link DownloadHelper} for DASH streams. |
| * @throws IllegalStateException If the DASH module is missing. |
| */ |
| public static DownloadHelper forDash( |
| Uri uri, |
| DataSource.Factory dataSourceFactory, |
| RenderersFactory renderersFactory, |
| @Nullable DrmSessionManager drmSessionManager, |
| DefaultTrackSelector.Parameters trackSelectorParameters) { |
| return new DownloadHelper( |
| DownloadRequest.TYPE_DASH, |
| uri, |
| /* cacheKey= */ null, |
| createMediaSourceInternal( |
| DASH_FACTORY_CONSTRUCTOR, |
| uri, |
| dataSourceFactory, |
| drmSessionManager, |
| /* streamKeys= */ null), |
| trackSelectorParameters, |
| getRendererCapabilities(renderersFactory)); |
| } |
| |
| /** @deprecated Use {@link #forHls(Context, Uri, Factory, RenderersFactory)} */ |
| @Deprecated |
| public static DownloadHelper forHls( |
| Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { |
| return forHls( |
| uri, |
| dataSourceFactory, |
| renderersFactory, |
| /* drmSessionManager= */ null, |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for HLS streams. |
| * |
| * @param context Any {@link Context}. |
| * @param uri A playlist {@link Uri}. |
| * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. |
| * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are |
| * selected. |
| * @return A {@link DownloadHelper} for HLS streams. |
| * @throws IllegalStateException If the HLS module is missing. |
| */ |
| public static DownloadHelper forHls( |
| Context context, |
| Uri uri, |
| DataSource.Factory dataSourceFactory, |
| RenderersFactory renderersFactory) { |
| return forHls( |
| uri, |
| dataSourceFactory, |
| renderersFactory, |
| /* drmSessionManager= */ null, |
| getDefaultTrackSelectorParameters(context)); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for HLS streams. |
| * |
| * @param uri A playlist {@link Uri}. |
| * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. |
| * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are |
| * selected. |
| * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which |
| * tracks can be selected. |
| * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for |
| * downloading. |
| * @return A {@link DownloadHelper} for HLS streams. |
| * @throws IllegalStateException If the HLS module is missing. |
| */ |
| public static DownloadHelper forHls( |
| Uri uri, |
| DataSource.Factory dataSourceFactory, |
| RenderersFactory renderersFactory, |
| @Nullable DrmSessionManager drmSessionManager, |
| DefaultTrackSelector.Parameters trackSelectorParameters) { |
| return new DownloadHelper( |
| DownloadRequest.TYPE_HLS, |
| uri, |
| /* cacheKey= */ null, |
| createMediaSourceInternal( |
| HLS_FACTORY_CONSTRUCTOR, |
| uri, |
| dataSourceFactory, |
| drmSessionManager, |
| /* streamKeys= */ null), |
| trackSelectorParameters, |
| getRendererCapabilities(renderersFactory)); |
| } |
| |
| /** @deprecated Use {@link #forSmoothStreaming(Context, Uri, Factory, RenderersFactory)} */ |
| @Deprecated |
| public static DownloadHelper forSmoothStreaming( |
| Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { |
| return forSmoothStreaming( |
| uri, |
| dataSourceFactory, |
| renderersFactory, |
| /* drmSessionManager= */ null, |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for SmoothStreaming streams. |
| * |
| * @param context Any {@link Context}. |
| * @param uri A manifest {@link Uri}. |
| * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. |
| * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are |
| * selected. |
| * @return A {@link DownloadHelper} for SmoothStreaming streams. |
| * @throws IllegalStateException If the SmoothStreaming module is missing. |
| */ |
| public static DownloadHelper forSmoothStreaming( |
| Context context, |
| Uri uri, |
| DataSource.Factory dataSourceFactory, |
| RenderersFactory renderersFactory) { |
| return forSmoothStreaming( |
| uri, |
| dataSourceFactory, |
| renderersFactory, |
| /* drmSessionManager= */ null, |
| getDefaultTrackSelectorParameters(context)); |
| } |
| |
| /** |
| * Creates a {@link DownloadHelper} for SmoothStreaming streams. |
| * |
| * @param uri A manifest {@link Uri}. |
| * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. |
| * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are |
| * selected. |
| * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which |
| * tracks can be selected. |
| * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for |
| * downloading. |
| * @return A {@link DownloadHelper} for SmoothStreaming streams. |
| * @throws IllegalStateException If the SmoothStreaming module is missing. |
| */ |
| public static DownloadHelper forSmoothStreaming( |
| Uri uri, |
| DataSource.Factory dataSourceFactory, |
| RenderersFactory renderersFactory, |
| @Nullable DrmSessionManager drmSessionManager, |
| DefaultTrackSelector.Parameters trackSelectorParameters) { |
| return new DownloadHelper( |
| DownloadRequest.TYPE_SS, |
| uri, |
| /* cacheKey= */ null, |
| createMediaSourceInternal( |
| SS_FACTORY_CONSTRUCTOR, |
| uri, |
| dataSourceFactory, |
| drmSessionManager, |
| /* streamKeys= */ null), |
| trackSelectorParameters, |
| getRendererCapabilities(renderersFactory)); |
| } |
| |
| /** |
| * Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager) |
| * createMediaSource(downloadRequest, dataSourceFactory, null)}. |
| */ |
| public static MediaSource createMediaSource( |
| DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { |
| return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null); |
| } |
| |
| /** |
| * Utility method to create a {@link MediaSource} that only exposes the tracks defined in {@code |
| * downloadRequest}. |
| * |
| * @param downloadRequest A {@link DownloadRequest}. |
| * @param dataSourceFactory A factory for {@link DataSource}s to read the media. |
| * @param drmSessionManager An optional {@link DrmSessionManager} to be passed to the {@link |
| * MediaSource}. |
| * @return A {@link MediaSource} that only exposes the tracks defined in {@code downloadRequest}. |
| */ |
| public static MediaSource createMediaSource( |
| DownloadRequest downloadRequest, |
| DataSource.Factory dataSourceFactory, |
| @Nullable DrmSessionManager drmSessionManager) { |
| @Nullable Constructor<? extends MediaSourceFactory> constructor; |
| switch (downloadRequest.type) { |
| case DownloadRequest.TYPE_DASH: |
| constructor = DASH_FACTORY_CONSTRUCTOR; |
| break; |
| case DownloadRequest.TYPE_SS: |
| constructor = SS_FACTORY_CONSTRUCTOR; |
| break; |
| case DownloadRequest.TYPE_HLS: |
| constructor = HLS_FACTORY_CONSTRUCTOR; |
| break; |
| case DownloadRequest.TYPE_PROGRESSIVE: |
| return new ProgressiveMediaSource.Factory(dataSourceFactory) |
| .setCustomCacheKey(downloadRequest.customCacheKey) |
| .createMediaSource(downloadRequest.uri); |
| default: |
| throw new IllegalStateException("Unsupported type: " + downloadRequest.type); |
| } |
| return createMediaSourceInternal( |
| constructor, |
| downloadRequest.uri, |
| dataSourceFactory, |
| drmSessionManager, |
| downloadRequest.streamKeys); |
| } |
| |
| private final String downloadType; |
| private final Uri uri; |
| @Nullable private final String cacheKey; |
| @Nullable private final MediaSource mediaSource; |
| private final DefaultTrackSelector trackSelector; |
| private final RendererCapabilities[] rendererCapabilities; |
| private final SparseIntArray scratchSet; |
| private final Handler callbackHandler; |
| private final Timeline.Window window; |
| |
| private boolean isPreparedWithMedia; |
| private @MonotonicNonNull Callback callback; |
| private @MonotonicNonNull MediaPreparer mediaPreparer; |
| private TrackGroupArray @MonotonicNonNull [] trackGroupArrays; |
| private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos; |
| private List<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer; |
| private List<TrackSelection> @MonotonicNonNull [][] immutableTrackSelectionsByPeriodAndRenderer; |
| |
| /** |
| * Creates download helper. |
| * |
| * @param downloadType A download type. This value will be used as {@link DownloadRequest#type}. |
| * @param uri A {@link Uri}. |
| * @param cacheKey An optional cache key. |
| * @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track |
| * selection needs to be made. |
| * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for |
| * downloading. |
| * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks |
| * are selected. |
| */ |
| public DownloadHelper( |
| String downloadType, |
| Uri uri, |
| @Nullable String cacheKey, |
| @Nullable MediaSource mediaSource, |
| DefaultTrackSelector.Parameters trackSelectorParameters, |
| RendererCapabilities[] rendererCapabilities) { |
| this.downloadType = downloadType; |
| this.uri = uri; |
| this.cacheKey = cacheKey; |
| this.mediaSource = mediaSource; |
| this.trackSelector = |
| new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory()); |
| this.rendererCapabilities = rendererCapabilities; |
| this.scratchSet = new SparseIntArray(); |
| trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); |
| callbackHandler = new Handler(Util.getLooper()); |
| window = new Timeline.Window(); |
| } |
| |
| /** |
| * Initializes the helper for starting a download. |
| * |
| * @param callback A callback to be notified when preparation completes or fails. |
| * @throws IllegalStateException If the download helper has already been prepared. |
| */ |
| public void prepare(Callback callback) { |
| Assertions.checkState(this.callback == null); |
| this.callback = callback; |
| if (mediaSource != null) { |
| mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this); |
| } else { |
| callbackHandler.post(() -> callback.onPrepared(this)); |
| } |
| } |
| |
| /** Releases the helper and all resources it is holding. */ |
| public void release() { |
| if (mediaPreparer != null) { |
| mediaPreparer.release(); |
| } |
| } |
| |
| /** |
| * Returns the manifest, or null if no manifest is loaded. Must not be called until after |
| * preparation completes. |
| */ |
| @Nullable |
| public Object getManifest() { |
| if (mediaSource == null) { |
| return null; |
| } |
| assertPreparedWithMedia(); |
| return mediaPreparer.timeline.getWindowCount() > 0 |
| ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest |
| : null; |
| } |
| |
| /** |
| * Returns the number of periods for which media is available. Must not be called until after |
| * preparation completes. |
| */ |
| public int getPeriodCount() { |
| if (mediaSource == null) { |
| return 0; |
| } |
| assertPreparedWithMedia(); |
| return trackGroupArrays.length; |
| } |
| |
| /** |
| * Returns the track groups for the given period. Must not be called until after preparation |
| * completes. |
| * |
| * <p>Use {@link #getMappedTrackInfo(int)} to get the track groups mapped to renderers. |
| * |
| * @param periodIndex The period index. |
| * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream |
| * content. |
| */ |
| public TrackGroupArray getTrackGroups(int periodIndex) { |
| assertPreparedWithMedia(); |
| return trackGroupArrays[periodIndex]; |
| } |
| |
| /** |
| * Returns the mapped track info for the given period. Must not be called until after preparation |
| * completes. |
| * |
| * @param periodIndex The period index. |
| * @return The {@link MappedTrackInfo} for the period. |
| */ |
| public MappedTrackInfo getMappedTrackInfo(int periodIndex) { |
| assertPreparedWithMedia(); |
| return mappedTrackInfos[periodIndex]; |
| } |
| |
| /** |
| * Returns all {@link TrackSelection track selections} for a period and renderer. Must not be |
| * called until after preparation completes. |
| * |
| * @param periodIndex The period index. |
| * @param rendererIndex The renderer index. |
| * @return A list of selected {@link TrackSelection track selections}. |
| */ |
| public List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) { |
| assertPreparedWithMedia(); |
| return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; |
| } |
| |
| /** |
| * Clears the selection of tracks for a period. Must not be called until after preparation |
| * completes. |
| * |
| * @param periodIndex The period index for which track selections are cleared. |
| */ |
| public void clearTrackSelections(int periodIndex) { |
| assertPreparedWithMedia(); |
| for (int i = 0; i < rendererCapabilities.length; i++) { |
| trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); |
| } |
| } |
| |
| /** |
| * Replaces a selection of tracks to be downloaded. Must not be called until after preparation |
| * completes. |
| * |
| * @param periodIndex The period index for which the track selection is replaced. |
| * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new |
| * selection of tracks. |
| */ |
| public void replaceTrackSelections( |
| int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { |
| clearTrackSelections(periodIndex); |
| addTrackSelection(periodIndex, trackSelectorParameters); |
| } |
| |
| /** |
| * Adds a selection of tracks to be downloaded. Must not be called until after preparation |
| * completes. |
| * |
| * @param periodIndex The period index this track selection is added for. |
| * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new |
| * selection of tracks. |
| */ |
| public void addTrackSelection( |
| int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { |
| assertPreparedWithMedia(); |
| trackSelector.setParameters(trackSelectorParameters); |
| runTrackSelection(periodIndex); |
| } |
| |
| /** |
| * Convenience method to add selections of tracks for all specified audio languages. If an audio |
| * track in one of the specified languages is not available, the default fallback audio track is |
| * used instead. Must not be called until after preparation completes. |
| * |
| * @param languages A list of audio languages for which tracks should be added to the download |
| * selection, as IETF BCP 47 conformant tags. |
| */ |
| public void addAudioLanguagesToSelection(String... languages) { |
| assertPreparedWithMedia(); |
| for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { |
| DefaultTrackSelector.ParametersBuilder parametersBuilder = |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); |
| MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; |
| int rendererCount = mappedTrackInfo.getRendererCount(); |
| for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { |
| if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) { |
| parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); |
| } |
| } |
| for (String language : languages) { |
| parametersBuilder.setPreferredAudioLanguage(language); |
| addTrackSelection(periodIndex, parametersBuilder.build()); |
| } |
| } |
| } |
| |
| /** |
| * Convenience method to add selections of tracks for all specified text languages. Must not be |
| * called until after preparation completes. |
| * |
| * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should be |
| * selected for downloading if no track with one of the specified {@code languages} is |
| * available. |
| * @param languages A list of text languages for which tracks should be added to the download |
| * selection, as IETF BCP 47 conformant tags. |
| */ |
| public void addTextLanguagesToSelection( |
| boolean selectUndeterminedTextLanguage, String... languages) { |
| assertPreparedWithMedia(); |
| for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { |
| DefaultTrackSelector.ParametersBuilder parametersBuilder = |
| DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); |
| MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; |
| int rendererCount = mappedTrackInfo.getRendererCount(); |
| for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { |
| if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) { |
| parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); |
| } |
| } |
| parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); |
| for (String language : languages) { |
| parametersBuilder.setPreferredTextLanguage(language); |
| addTrackSelection(periodIndex, parametersBuilder.build()); |
| } |
| } |
| } |
| |
| /** |
| * Convenience method to add a selection of tracks to be downloaded for a single renderer. Must |
| * not be called until after preparation completes. |
| * |
| * @param periodIndex The period index the track selection is added for. |
| * @param rendererIndex The renderer index the track selection is added for. |
| * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new |
| * selection of tracks. |
| * @param overrides A list of {@link SelectionOverride SelectionOverrides} to apply to the {@code |
| * trackSelectorParameters}. If empty, {@code trackSelectorParameters} are used as they are. |
| */ |
| public void addTrackSelectionForSingleRenderer( |
| int periodIndex, |
| int rendererIndex, |
| DefaultTrackSelector.Parameters trackSelectorParameters, |
| List<SelectionOverride> overrides) { |
| assertPreparedWithMedia(); |
| DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon(); |
| for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) { |
| builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex); |
| } |
| if (overrides.isEmpty()) { |
| addTrackSelection(periodIndex, builder.build()); |
| } else { |
| TrackGroupArray trackGroupArray = mappedTrackInfos[periodIndex].getTrackGroups(rendererIndex); |
| for (int i = 0; i < overrides.size(); i++) { |
| builder.setSelectionOverride(rendererIndex, trackGroupArray, overrides.get(i)); |
| addTrackSelection(periodIndex, builder.build()); |
| } |
| } |
| } |
| |
| /** |
| * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until |
| * after preparation completes. The uri of the {@link DownloadRequest} will be used as content id. |
| * |
| * @param data Application provided data to store in {@link DownloadRequest#data}. |
| * @return The built {@link DownloadRequest}. |
| */ |
| public DownloadRequest getDownloadRequest(@Nullable byte[] data) { |
| return getDownloadRequest(uri.toString(), data); |
| } |
| |
| /** |
| * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until |
| * after preparation completes. |
| * |
| * @param id The unique content id. |
| * @param data Application provided data to store in {@link DownloadRequest#data}. |
| * @return The built {@link DownloadRequest}. |
| */ |
| public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) { |
| if (mediaSource == null) { |
| return new DownloadRequest( |
| id, downloadType, uri, /* streamKeys= */ Collections.emptyList(), cacheKey, data); |
| } |
| assertPreparedWithMedia(); |
| List<StreamKey> streamKeys = new ArrayList<>(); |
| List<TrackSelection> allSelections = new ArrayList<>(); |
| int periodCount = trackSelectionsByPeriodAndRenderer.length; |
| for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { |
| allSelections.clear(); |
| int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length; |
| for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { |
| allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]); |
| } |
| streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); |
| } |
| return new DownloadRequest(id, downloadType, uri, streamKeys, cacheKey, data); |
| } |
| |
| // Initialization of array of Lists. |
| @SuppressWarnings("unchecked") |
| private void onMediaPrepared() { |
| Assertions.checkNotNull(mediaPreparer); |
| Assertions.checkNotNull(mediaPreparer.mediaPeriods); |
| Assertions.checkNotNull(mediaPreparer.timeline); |
| int periodCount = mediaPreparer.mediaPeriods.length; |
| int rendererCount = rendererCapabilities.length; |
| trackSelectionsByPeriodAndRenderer = |
| (List<TrackSelection>[][]) new List<?>[periodCount][rendererCount]; |
| immutableTrackSelectionsByPeriodAndRenderer = |
| (List<TrackSelection>[][]) new List<?>[periodCount][rendererCount]; |
| for (int i = 0; i < periodCount; i++) { |
| for (int j = 0; j < rendererCount; j++) { |
| trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>(); |
| immutableTrackSelectionsByPeriodAndRenderer[i][j] = |
| Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]); |
| } |
| } |
| trackGroupArrays = new TrackGroupArray[periodCount]; |
| mappedTrackInfos = new MappedTrackInfo[periodCount]; |
| for (int i = 0; i < periodCount; i++) { |
| trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups(); |
| TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i); |
| trackSelector.onSelectionActivated(trackSelectorResult.info); |
| mappedTrackInfos[i] = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); |
| } |
| setPreparedWithMedia(); |
| Assertions.checkNotNull(callbackHandler) |
| .post(() -> Assertions.checkNotNull(callback).onPrepared(this)); |
| } |
| |
| private void onMediaPreparationFailed(IOException error) { |
| Assertions.checkNotNull(callbackHandler) |
| .post(() -> Assertions.checkNotNull(callback).onPrepareError(this, error)); |
| } |
| |
| @RequiresNonNull({ |
| "trackGroupArrays", |
| "mappedTrackInfos", |
| "trackSelectionsByPeriodAndRenderer", |
| "immutableTrackSelectionsByPeriodAndRenderer", |
| "mediaPreparer", |
| "mediaPreparer.timeline", |
| "mediaPreparer.mediaPeriods" |
| }) |
| private void setPreparedWithMedia() { |
| isPreparedWithMedia = true; |
| } |
| |
| @EnsuresNonNull({ |
| "trackGroupArrays", |
| "mappedTrackInfos", |
| "trackSelectionsByPeriodAndRenderer", |
| "immutableTrackSelectionsByPeriodAndRenderer", |
| "mediaPreparer", |
| "mediaPreparer.timeline", |
| "mediaPreparer.mediaPeriods" |
| }) |
| @SuppressWarnings("nullness:contracts.postcondition.not.satisfied") |
| private void assertPreparedWithMedia() { |
| Assertions.checkState(isPreparedWithMedia); |
| } |
| |
| /** |
| * Runs the track selection for a given period index with the current parameters. The selected |
| * tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}. |
| */ |
| // Intentional reference comparison of track group instances. |
| @SuppressWarnings("ReferenceEquality") |
| @RequiresNonNull({ |
| "trackGroupArrays", |
| "trackSelectionsByPeriodAndRenderer", |
| "mediaPreparer", |
| "mediaPreparer.timeline" |
| }) |
| private TrackSelectorResult runTrackSelection(int periodIndex) { |
| try { |
| TrackSelectorResult trackSelectorResult = |
| trackSelector.selectTracks( |
| rendererCapabilities, |
| trackGroupArrays[periodIndex], |
| new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), |
| mediaPreparer.timeline); |
| for (int i = 0; i < trackSelectorResult.length; i++) { |
| @Nullable TrackSelection newSelection = trackSelectorResult.selections.get(i); |
| if (newSelection == null) { |
| continue; |
| } |
| List<TrackSelection> existingSelectionList = |
| trackSelectionsByPeriodAndRenderer[periodIndex][i]; |
| boolean mergedWithExistingSelection = false; |
| for (int j = 0; j < existingSelectionList.size(); j++) { |
| TrackSelection existingSelection = existingSelectionList.get(j); |
| if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) { |
| // Merge with existing selection. |
| scratchSet.clear(); |
| for (int k = 0; k < existingSelection.length(); k++) { |
| scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0); |
| } |
| for (int k = 0; k < newSelection.length(); k++) { |
| scratchSet.put(newSelection.getIndexInTrackGroup(k), 0); |
| } |
| int[] mergedTracks = new int[scratchSet.size()]; |
| for (int k = 0; k < scratchSet.size(); k++) { |
| mergedTracks[k] = scratchSet.keyAt(k); |
| } |
| existingSelectionList.set( |
| j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks)); |
| mergedWithExistingSelection = true; |
| break; |
| } |
| } |
| if (!mergedWithExistingSelection) { |
| existingSelectionList.add(newSelection); |
| } |
| } |
| return trackSelectorResult; |
| } catch (ExoPlaybackException e) { |
| // DefaultTrackSelector does not throw exceptions during track selection. |
| throw new UnsupportedOperationException(e); |
| } |
| } |
| |
| @Nullable |
| private static Constructor<? extends MediaSourceFactory> getConstructor(String className) { |
| try { |
| // LINT.IfChange |
| Class<? extends MediaSourceFactory> factoryClazz = |
| Class.forName(className).asSubclass(MediaSourceFactory.class); |
| return factoryClazz.getConstructor(Factory.class); |
| // LINT.ThenChange(../../../../../../../../proguard-rules.txt) |
| } catch (ClassNotFoundException e) { |
| // Expected if the app was built without the respective module. |
| return null; |
| } catch (NoSuchMethodException e) { |
| // Something is wrong with the library or the proguard configuration. |
| throw new IllegalStateException(e); |
| } |
| } |
| |
| private static MediaSource createMediaSourceInternal( |
| @Nullable Constructor<? extends MediaSourceFactory> constructor, |
| Uri uri, |
| Factory dataSourceFactory, |
| @Nullable DrmSessionManager drmSessionManager, |
| @Nullable List<StreamKey> streamKeys) { |
| if (constructor == null) { |
| throw new IllegalStateException("Module missing to create media source."); |
| } |
| try { |
| MediaSourceFactory factory = constructor.newInstance(dataSourceFactory); |
| if (drmSessionManager != null) { |
| factory.setDrmSessionManager(drmSessionManager); |
| } |
| if (streamKeys != null) { |
| factory.setStreamKeys(streamKeys); |
| } |
| return Assertions.checkNotNull(factory.createMediaSource(uri)); |
| } catch (Exception e) { |
| throw new IllegalStateException("Failed to instantiate media source.", e); |
| } |
| } |
| |
| private static final class MediaPreparer |
| implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback { |
| |
| private static final int MESSAGE_PREPARE_SOURCE = 0; |
| private static final int MESSAGE_CHECK_FOR_FAILURE = 1; |
| private static final int MESSAGE_CONTINUE_LOADING = 2; |
| private static final int MESSAGE_RELEASE = 3; |
| |
| private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED = 0; |
| private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED = 1; |
| |
| private final MediaSource mediaSource; |
| private final DownloadHelper downloadHelper; |
| private final Allocator allocator; |
| private final ArrayList<MediaPeriod> pendingMediaPeriods; |
| private final Handler downloadHelperHandler; |
| private final HandlerThread mediaSourceThread; |
| private final Handler mediaSourceHandler; |
| |
| public @MonotonicNonNull Timeline timeline; |
| public MediaPeriod @MonotonicNonNull [] mediaPeriods; |
| |
| private boolean released; |
| |
| public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) { |
| this.mediaSource = mediaSource; |
| this.downloadHelper = downloadHelper; |
| allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); |
| pendingMediaPeriods = new ArrayList<>(); |
| @SuppressWarnings("methodref.receiver.bound.invalid") |
| Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage); |
| this.downloadHelperHandler = downloadThreadHandler; |
| mediaSourceThread = new HandlerThread("ExoPlayer:DownloadHelper"); |
| mediaSourceThread.start(); |
| mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); |
| mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); |
| } |
| |
| public void release() { |
| if (released) { |
| return; |
| } |
| released = true; |
| mediaSourceHandler.sendEmptyMessage(MESSAGE_RELEASE); |
| } |
| |
| // Handler.Callback |
| |
| @Override |
| public boolean handleMessage(Message msg) { |
| switch (msg.what) { |
| case MESSAGE_PREPARE_SOURCE: |
| mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null); |
| mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); |
| return true; |
| case MESSAGE_CHECK_FOR_FAILURE: |
| try { |
| if (mediaPeriods == null) { |
| mediaSource.maybeThrowSourceInfoRefreshError(); |
| } else { |
| for (int i = 0; i < pendingMediaPeriods.size(); i++) { |
| pendingMediaPeriods.get(i).maybeThrowPrepareError(); |
| } |
| } |
| mediaSourceHandler.sendEmptyMessageDelayed( |
| MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100); |
| } catch (IOException e) { |
| downloadHelperHandler |
| .obtainMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, /* obj= */ e) |
| .sendToTarget(); |
| } |
| return true; |
| case MESSAGE_CONTINUE_LOADING: |
| MediaPeriod mediaPeriod = (MediaPeriod) msg.obj; |
| if (pendingMediaPeriods.contains(mediaPeriod)) { |
| mediaPeriod.continueLoading(/* positionUs= */ 0); |
| } |
| return true; |
| case MESSAGE_RELEASE: |
| if (mediaPeriods != null) { |
| for (MediaPeriod period : mediaPeriods) { |
| mediaSource.releasePeriod(period); |
| } |
| } |
| mediaSource.releaseSource(this); |
| mediaSourceHandler.removeCallbacksAndMessages(null); |
| mediaSourceThread.quit(); |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| // MediaSource.MediaSourceCaller implementation. |
| |
| @Override |
| public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { |
| if (this.timeline != null) { |
| // Ignore dynamic updates. |
| return; |
| } |
| if (timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).isLive) { |
| downloadHelperHandler |
| .obtainMessage( |
| DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, |
| /* obj= */ new LiveContentUnsupportedException()) |
| .sendToTarget(); |
| return; |
| } |
| this.timeline = timeline; |
| mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; |
| for (int i = 0; i < mediaPeriods.length; i++) { |
| MediaPeriod mediaPeriod = |
| mediaSource.createPeriod( |
| new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)), |
| allocator, |
| /* startPositionUs= */ 0); |
| mediaPeriods[i] = mediaPeriod; |
| pendingMediaPeriods.add(mediaPeriod); |
| } |
| for (MediaPeriod mediaPeriod : mediaPeriods) { |
| mediaPeriod.prepare(/* callback= */ this, /* positionUs= */ 0); |
| } |
| } |
| |
| // MediaPeriod.Callback implementation. |
| |
| @Override |
| public void onPrepared(MediaPeriod mediaPeriod) { |
| pendingMediaPeriods.remove(mediaPeriod); |
| if (pendingMediaPeriods.isEmpty()) { |
| mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE); |
| downloadHelperHandler.sendEmptyMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED); |
| } |
| } |
| |
| @Override |
| public void onContinueLoadingRequested(MediaPeriod mediaPeriod) { |
| if (pendingMediaPeriods.contains(mediaPeriod)) { |
| mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING, mediaPeriod).sendToTarget(); |
| } |
| } |
| |
| private boolean handleDownloadHelperCallbackMessage(Message msg) { |
| if (released) { |
| // Stale message. |
| return false; |
| } |
| switch (msg.what) { |
| case DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED: |
| downloadHelper.onMediaPrepared(); |
| return true; |
| case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED: |
| release(); |
| downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj)); |
| return true; |
| default: |
| return false; |
| } |
| } |
| } |
| |
| private static final class DownloadTrackSelection extends BaseTrackSelection { |
| |
| private static final class Factory implements TrackSelection.Factory { |
| |
| @Override |
| public @NullableType TrackSelection[] createTrackSelections( |
| @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { |
| @NullableType TrackSelection[] selections = new TrackSelection[definitions.length]; |
| for (int i = 0; i < definitions.length; i++) { |
| selections[i] = |
| definitions[i] == null |
| ? null |
| : new DownloadTrackSelection(definitions[i].group, definitions[i].tracks); |
| } |
| return selections; |
| } |
| } |
| |
| public DownloadTrackSelection(TrackGroup trackGroup, int[] tracks) { |
| super(trackGroup, tracks); |
| } |
| |
| @Override |
| public int getSelectedIndex() { |
| return 0; |
| } |
| |
| @Override |
| public int getSelectionReason() { |
| return C.SELECTION_REASON_UNKNOWN; |
| } |
| |
| @Override |
| @Nullable |
| public Object getSelectionData() { |
| return null; |
| } |
| |
| @Override |
| public void updateSelectedTrack( |
| long playbackPositionUs, |
| long bufferedDurationUs, |
| long availableDurationUs, |
| List<? extends MediaChunk> queue, |
| MediaChunkIterator[] mediaChunkIterators) { |
| // Do nothing. |
| } |
| } |
| |
| private static final class DummyBandwidthMeter implements BandwidthMeter { |
| |
| @Override |
| public long getBitrateEstimate() { |
| return 0; |
| } |
| |
| @Override |
| @Nullable |
| public TransferListener getTransferListener() { |
| return null; |
| } |
| |
| @Override |
| public void addEventListener(Handler eventHandler, EventListener eventListener) { |
| // Do nothing. |
| } |
| |
| @Override |
| public void removeEventListener(EventListener eventListener) { |
| // Do nothing. |
| } |
| } |
| } |