From bc6fdf81d2405facccfb4e7f1230b664fd2586ad Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 10 May 2018 13:28:31 -0700 Subject: [PATCH] -Refactored player media source resolution into external helpers. -Baked resolved media metadata into media source for one-way data passing. --- .../newpipe/player/BackgroundPlayer.java | 36 ++-- .../org/schabi/newpipe/player/BasePlayer.java | 169 +++++++----------- .../newpipe/player/MainVideoPlayer.java | 29 +-- .../newpipe/player/PopupVideoPlayer.java | 35 ++-- .../schabi/newpipe/player/VideoPlayer.java | 110 ++---------- .../player/playback/MediaSourceManager.java | 28 +-- .../player/playback/PlaybackListener.java | 2 +- .../resolver/AudioPlaybackResolver.java | 39 ++++ .../player/resolver/MediaSourceTag.java | 51 ++++++ .../player/resolver/PlaybackResolver.java | 84 +++++++++ .../newpipe/player/resolver/Resolver.java | 7 + .../resolver/VideoPlaybackResolver.java | 122 +++++++++++++ 12 files changed, 436 insertions(+), 276 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index f25c20bb2..b5135bd84 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -42,14 +42,12 @@ import com.google.android.exoplayer2.source.MediaSource; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; -import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; +import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -279,10 +277,18 @@ public final class BackgroundPlayer extends Service { protected class BasePlayerImpl extends BasePlayer { + @Nullable private AudioPlaybackResolver resolver; + BasePlayerImpl(Context context) { super(context); } + @Override + public void initPlayer(boolean playOnReady) { + super.initPlayer(playOnReady); + resolver = new AudioPlaybackResolver(context, dataSource); + } + @Override public void handleIntent(final Intent intent) { super.handleIntent(intent); @@ -390,11 +396,9 @@ public final class BackgroundPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - if (shouldUpdateOnProgress || hasPlayQueueItemChanged) { + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + if (shouldUpdateOnProgress) { resetNotification(); updateNotification(-1); updateMetadata(); @@ -404,15 +408,7 @@ public final class BackgroundPlayer extends Service { @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - final MediaSource liveSource = super.sourceOf(item, info); - if (liveSource != null) return liveSource; - - final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); - if (index < 0 || index >= info.getAudioStreams().size()) return null; - - final AudioStream audio = info.getAudioStreams().get(index); - return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId())); + return resolver == null ? null : resolver.resolve(info); } @Override @@ -439,8 +435,8 @@ public final class BackgroundPlayer extends Service { } private void updateMetadata() { - if (activityListener != null && currentInfo != null) { - activityListener.onMetadataUpdate(currentInfo); + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 11d8381dc..2cbef93e4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -25,15 +25,12 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.media.AudioManager; -import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -49,7 +46,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; @@ -57,7 +53,6 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; @@ -72,6 +67,7 @@ import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; @@ -130,12 +126,12 @@ public abstract class BasePlayer implements protected PlayQueue playQueue; protected PlayQueueAdapter playQueueAdapter; - protected MediaSourceManager playbackManager; + @Nullable protected MediaSourceManager playbackManager; - protected StreamInfo currentInfo; - protected PlayQueueItem currentItem; + @Nullable private PlayQueueItem currentItem; + @Nullable private MediaSourceTag currentMetadata; - protected Toast errorToast; + @Nullable protected Toast errorToast; /*////////////////////////////////////////////////////////////////////////// // Player @@ -329,58 +325,6 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "]"); } - /*////////////////////////////////////////////////////////////////////////// - // MediaSource Building - //////////////////////////////////////////////////////////////////////////*/ - - public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl, - @C.ContentType final int type) { - if (DEBUG) { - Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl + - "], content type = [" + type + "]"); - } - if (dataSource == null) return null; - - final Uri uri = Uri.parse(sourceUrl); - switch (type) { - case C.TYPE_SS: - return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_DASH: - return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri); - case C.TYPE_HLS: - return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } - - public MediaSource buildMediaSource(@NonNull final String sourceUrl, - @NonNull final String cacheKey, - @NonNull final String overrideExtension) { - if (DEBUG) { - Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl + - "], cacheKey = [" + cacheKey + "]" + - "], overrideExtension = [" + overrideExtension + "]"); - } - if (dataSource == null) return null; - - final Uri uri = Uri.parse(sourceUrl); - @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? - Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); - - switch (type) { - case C.TYPE_SS: - return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_DASH: - return dataSource.getDashMediaSourceFactory().createMediaSource(uri); - case C.TYPE_HLS: - return dataSource.getHlsMediaSourceFactory().createMediaSource(uri); - case C.TYPE_OTHER: - return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri); - default: - throw new IllegalStateException("Unsupported type: " + type); - } - } /*////////////////////////////////////////////////////////////////////////// // Broadcast Receiver @@ -614,6 +558,7 @@ public abstract class BasePlayer implements } break; case Player.STATE_READY: //3 + maybeUpdateCurrentMetadata(); maybeCorrectSeekPosition(); if (!isPrepared) { isPrepared = true; @@ -630,10 +575,12 @@ public abstract class BasePlayer implements } private void maybeCorrectSeekPosition() { - if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; + if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) return; final int currentSourceIndex = playQueue.getIndex(); final PlayQueueItem currentSourceItem = playQueue.getItem(); + final StreamInfo currentInfo = currentMetadata.getMetadata(); + if (currentSourceItem == null) return; final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); @@ -649,16 +596,15 @@ public abstract class BasePlayer implements playQueue.unsetRecovery(currentSourceIndex); } else if (isSynchronizing && isLive()) { - if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); // Is still synchronizing? + if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); seekToDefault(); } else if (isSynchronizing && presetStartPositionMillis > 0L) { + // Has another start position? if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + "position=[" + presetStartPositionMillis + "]"); - // Has another start position? seekTo(presetStartPositionMillis); - currentInfo.setStartPosition(0); } isSynchronizing = false; @@ -732,6 +678,9 @@ public abstract class BasePlayer implements public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + "reason = [" + reason + "]"); + + maybeUpdateCurrentMetadata(); + // Refresh the playback if there is a transition to the next video final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); @@ -793,7 +742,7 @@ public abstract class BasePlayer implements if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); currentItem = null; - currentInfo = null; + currentMetadata = null; simpleExoPlayer.stop(); isPrepared = false; @@ -810,42 +759,21 @@ public abstract class BasePlayer implements simpleExoPlayer.prepare(mediaSource); } - @Override - public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { + public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + - (info != null ? "available" : "null") + " info, " + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); if (simpleExoPlayer == null || playQueue == null) return; final boolean onPlaybackInitial = currentItem == null; final boolean hasPlayQueueItemChanged = currentItem != item; - final boolean hasStreamInfoChanged = currentInfo != info; final int currentPlayQueueIndex = playQueue.indexOf(item); final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); - // when starting playback on the last item when not repeating, maybe auto queue - if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && - getRepeatMode() == Player.REPEAT_MODE_OFF && - PlayerHelper.isAutoQueueEnabled(context)) { - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); - if (autoQueue != null) playQueue.append(autoQueue.getStreams()); - } // If nothing to synchronize - if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { - return; - } - + if (!hasPlayQueueItemChanged) return; currentItem = item; - currentInfo = info; - if (hasPlayQueueItemChanged) { - // updates only to the stream info should not trigger another view count - registerView(); - initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); - } - onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { @@ -873,26 +801,21 @@ public abstract class BasePlayer implements } } - abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged); + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + Log.d(TAG, "Playback - onMetadataChanged() called, " + + "playing: " + tag.getMetadata().getName()); + final StreamInfo info = tag.getMetadata(); - @Nullable - @Override - public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { - final StreamType streamType = info.getStreamType(); - if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { - return null; + initThumbnail(info.getThumbnailUrl()); + registerView(); + + // when starting playback on the last item when not repeating, maybe auto queue + if (playQueue.getIndex() == playQueue.size() - 1 && + getRepeatMode() == Player.REPEAT_MODE_OFF && + PlayerHelper.isAutoQueueEnabled(context)) { + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); + if (autoQueue != null) playQueue.append(autoQueue.getStreams()); } - - if (!info.getHlsUrl().isEmpty()) { - return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); - } else if (!info.getDashMpdUrl().isEmpty()) { - return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH); - } - - return null; } @Override @@ -1051,7 +974,8 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ private void registerView() { - if (databaseUpdateReactor == null || currentInfo == null) return; + if (databaseUpdateReactor == null || currentMetadata == null) return; + final StreamInfo currentInfo = currentMetadata.getMetadata(); databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() .subscribe( ignored -> {/* successful */}, @@ -1082,7 +1006,8 @@ public abstract class BasePlayer implements } private void savePlaybackState() { - if (simpleExoPlayer == null || currentInfo == null) return; + if (simpleExoPlayer == null || currentMetadata == null) return; + final StreamInfo currentInfo = currentMetadata.getMetadata(); if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && simpleExoPlayer.getCurrentPosition() < @@ -1090,6 +1015,23 @@ public abstract class BasePlayer implements savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } } + + private void maybeUpdateCurrentMetadata() { + if (simpleExoPlayer == null) return; + + final MediaSourceTag metadata; + try { + metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); + } catch (IndexOutOfBoundsException | ClassCastException error) { + return; + } + + if (metadata == null || currentMetadata == metadata) return; + + currentMetadata = metadata; + onMetadataChanged(metadata); + } + /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -1106,19 +1048,28 @@ public abstract class BasePlayer implements return currentState; } + @Nullable + public MediaSourceTag getCurrentMetadata() { + return currentMetadata; + } + + @NonNull public String getVideoUrl() { return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUrl(); } + @NonNull public String getVideoTitle() { return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getTitle(); } + @NonNull public String getUploaderName() { return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); } /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isLiveEdge() { if (simpleExoPlayer == null || !isLive()) return false; diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 56c8f8855..a66906c73 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -58,7 +58,6 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; @@ -67,6 +66,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -497,11 +498,8 @@ public final class MainVideoPlayer extends AppCompatActivity // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - super.onMetadataChanged(item, info, newPlayQueueIndex, false); + protected void onMetadataChanged(@Nullable final MediaSourceTag tag) { + super.onMetadataChanged(tag); titleTextView.setText(getVideoTitle()); channelTextView.setText(getUploaderName()); @@ -686,14 +684,19 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - protected int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getDefaultResolutionIndex(context, sortedVideos); - } + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(List sortedVideos) { + return ListHelper.getDefaultResolutionIndex(context, sortedVideos); + } - @Override - protected int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); + @Override + public int getOverrideResolutionIndex(List sortedVideos, + String playbackQuality) { + return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); + } + }; } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 8b0fc0ddc..476ad73ef 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -59,13 +59,13 @@ import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.old.PlayVideoActivity; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -511,14 +511,20 @@ public final class PopupVideoPlayer extends Service { } @Override - protected int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); - } + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(List sortedVideos) { + return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } - @Override - protected int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); + @Override + public int getOverrideResolutionIndex(List sortedVideos, + String playbackQuality) { + return ListHelper.getPopupResolutionIndex(context, sortedVideos, + playbackQuality); + } + }; } /*////////////////////////////////////////////////////////////////////////// @@ -539,8 +545,8 @@ public final class PopupVideoPlayer extends Service { } private void updateMetadata() { - if (activityListener != null && currentInfo != null) { - activityListener.onMetadataUpdate(currentInfo); + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); } } @@ -586,11 +592,8 @@ public final class PopupVideoPlayer extends Service { // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { - super.onMetadataChanged(item, info, newPlayQueueIndex, false); + protected void onMetadataChanged(@Nullable final MediaSourceTag tag) { + super.onMetadataChanged(tag); updateMetadata(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 250d02b42..eaec59f65 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -29,7 +29,6 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; -import android.net.Uri; import android.os.Build; import android.os.Handler; import android.support.annotation.NonNull; @@ -47,11 +46,9 @@ import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.CaptionStyleCompat; @@ -62,21 +59,17 @@ import com.google.android.exoplayer2.video.VideoListener; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.Subtitles; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.ListHelper; import java.util.ArrayList; import java.util.List; -import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; -import static com.google.android.exoplayer2.C.TIME_UNSET; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -105,13 +98,12 @@ public abstract class VideoPlayer extends BasePlayer public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds - private ArrayList availableStreams; + private List availableStreams; private int selectedStreamIndex; - protected String playbackQuality; - protected boolean wasPlaying = false; + @Nullable private VideoPlaybackResolver resolver; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -244,6 +236,8 @@ public abstract class VideoPlayer extends BasePlayer trackSelector.setParameters(trackSelector.buildUponParameters() .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); } + + resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); } @Override @@ -326,23 +320,18 @@ public abstract class VideoPlayer extends BasePlayer // Playback Listener //////////////////////////////////////////////////////////////////////////*/ - protected abstract int getDefaultResolutionIndex(final List sortedVideos); + protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver(); - protected abstract int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality); + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); - protected void onMetadataChanged(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info, - final int newPlayQueueIndex, - final boolean hasPlayQueueItemChanged) { qualityTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE); playbackEndTime.setVisibility(View.GONE); playbackLiveSync.setVisibility(View.GONE); - final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); - - switch (streamType) { + switch (tag.getMetadata().getStreamType()) { case AUDIO_STREAM: surfaceView.setVisibility(View.GONE); playbackEndTime.setVisibility(View.VISIBLE); @@ -359,20 +348,14 @@ public abstract class VideoPlayer extends BasePlayer break; case VIDEO_STREAM: + final StreamInfo info = tag.getMetadata(); if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break; - final List videos = ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false); - availableStreams = new ArrayList<>(videos); - if (playbackQuality == null) { - selectedStreamIndex = getDefaultResolutionIndex(videos); - } else { - selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality()); - } - + availableStreams = tag.getSortedAvailableVideoStreams(); + selectedStreamIndex = tag.getSelectedVideoStreamIndex(); buildQualityMenu(); - qualityTextView.setVisibility(View.VISIBLE); + qualityTextView.setVisibility(View.VISIBLE); surfaceView.setVisibility(View.VISIBLE); default: playbackEndTime.setVisibility(View.VISIBLE); @@ -386,65 +369,7 @@ public abstract class VideoPlayer extends BasePlayer @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - final MediaSource liveSource = super.sourceOf(item, info); - if (liveSource != null) return liveSource; - - List mediaSources = new ArrayList<>(); - - // Create video stream source - final List videos = ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false); - final int index; - if (videos.isEmpty()) { - index = -1; - } else if (playbackQuality == null) { - index = getDefaultResolutionIndex(videos); - } else { - index = getOverrideResolutionIndex(videos, getPlaybackQuality()); - } - final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; - if (video != null) { - final MediaSource streamSource = buildMediaSource(video.getUrl(), - PlayerHelper.cacheKeyOf(info, video), - MediaFormat.getSuffixById(video.getFormatId())); - mediaSources.add(streamSource); - } - - // Create optional audio stream source - final List audioStreams = info.getAudioStreams(); - final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( - ListHelper.getDefaultAudioFormat(context, audioStreams)); - // Use the audio stream if there is no video stream, or - // Merge with audio stream in case if video does not contain audio - if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { - final MediaSource audioSource = buildMediaSource(audio.getUrl(), - PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId())); - mediaSources.add(audioSource); - } - - // If there is no audio or video sources, then this media source cannot be played back - if (mediaSources.isEmpty()) return null; - // Below are auxiliary media sources - - // Create subtitle sources - for (final Subtitles subtitle : info.getSubtitles()) { - final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); - if (mimeType == null) continue; - - final Format textFormat = Format.createTextSampleFormat(null, mimeType, - SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); - final MediaSource textSource = dataSource.getSampleMediaSourceFactory() - .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); - mediaSources.add(textSource); - } - - if (mediaSources.size() == 1) { - return mediaSources.get(0); - } else { - return new MergingMediaSource(mediaSources.toArray( - new MediaSource[mediaSources.size()])); - } + return resolver == null ? null : resolver.resolve(info); } /*////////////////////////////////////////////////////////////////////////// @@ -908,11 +833,12 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ public void setPlaybackQuality(final String quality) { - this.playbackQuality = quality; + if (resolver != null) resolver.setPlaybackQuality(quality); } + @Nullable public String getPlaybackQuality() { - return playbackQuality; + return resolver == null ? null : resolver.getPlaybackQuality(); } public AspectRatioFrameLayout getAspectRatioFrameLayout() { diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 67a8debef..b27dc3dd6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -5,12 +5,10 @@ import android.support.annotation.Nullable; import android.support.v4.util.ArraySet; import android.util.Log; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.mediasource.LoadedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSource; @@ -24,10 +22,8 @@ import org.schabi.newpipe.player.playqueue.events.RemoveEvent; import org.schabi.newpipe.player.playqueue.events.ReorderEvent; import org.schabi.newpipe.util.ServiceHelper; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,8 +33,6 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.disposables.SerialDisposable; -import io.reactivex.functions.Consumer; import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; @@ -104,7 +98,6 @@ public class MediaSourceManager { private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; @NonNull private final CompositeDisposable loaderReactor; @NonNull private final Set loadingItems; - @NonNull private final SerialDisposable syncReactor; @NonNull private final AtomicBoolean isBlocked; @@ -144,7 +137,6 @@ public class MediaSourceManager { this.playQueueReactor = EmptySubscription.INSTANCE; this.loaderReactor = new CompositeDisposable(); - this.syncReactor = new SerialDisposable(); this.isBlocked = new AtomicBoolean(false); @@ -171,7 +163,6 @@ public class MediaSourceManager { playQueueReactor.cancel(); loaderReactor.dispose(); - syncReactor.dispose(); } /*////////////////////////////////////////////////////////////////////////// @@ -310,21 +301,7 @@ public class MediaSourceManager { final PlayQueueItem currentItem = playQueue.getItem(); if (isBlocked.get() || currentItem == null) return; - final Consumer onSuccess = info -> syncInternal(currentItem, info); - final Consumer onError = throwable -> syncInternal(currentItem, null); - - final Disposable sync = currentItem.getStream() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, onError); - syncReactor.set(sync); - } - - private void syncInternal(@NonNull final PlayQueueItem item, - @Nullable final StreamInfo info) { - // Ensure the current item is up to date with the play queue - if (playQueue.getItem() == item) { - playbackListener.onPlaybackSynchronize(item, info); - } + playbackListener.onPlaybackSynchronize(currentItem); } private synchronized void maybeSynchronizePlayer() { @@ -423,7 +400,8 @@ public class MediaSourceManager { } /** - * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} + * Checks if the corresponding MediaSource in + * {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource} * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback * readiness or playlist desynchronization. *

diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index 4dcb30aa3..238bdfcd0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -45,7 +45,7 @@ public interface PlaybackListener { * * May be called anytime at any amount once unblock is called. * */ - void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); + void onPlaybackSynchronize(@NonNull final PlayQueueItem item); /** * Requests the listener to resolve a stream info into a media source diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java new file mode 100644 index 000000000..f95f0e3bb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java @@ -0,0 +1,39 @@ +package org.schabi.newpipe.player.resolver; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.google.android.exoplayer2.source.MediaSource; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.util.ListHelper; + +public class AudioPlaybackResolver implements PlaybackResolver { + + @NonNull private final Context context; + @NonNull private final PlayerDataSource dataSource; + + public AudioPlaybackResolver(@NonNull final Context context, + @NonNull final PlayerDataSource dataSource) { + this.context = context; + this.dataSource = dataSource; + } + + @Override + public MediaSource resolve(@NonNull StreamInfo info) { + final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); + if (liveSource != null) return liveSource; + + final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); + if (index < 0 || index >= info.getAudioStreams().size()) return null; + + final AudioStream audio = info.getAudioStreams().get(index); + final MediaSourceTag tag = new MediaSourceTag(info); + return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), + MediaFormat.getSuffixById(audio.getFormatId()), tag); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java new file mode 100644 index 000000000..bbe5d33ca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java @@ -0,0 +1,51 @@ +package org.schabi.newpipe.player.resolver; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +public class MediaSourceTag implements Serializable { + @NonNull private final StreamInfo metadata; + + @NonNull private final List sortedAvailableVideoStreams; + private final int selectedVideoStreamIndex; + + public MediaSourceTag(@NonNull final StreamInfo metadata, + @NonNull final List sortedAvailableVideoStreams, + final int selectedVideoStreamIndex) { + this.metadata = metadata; + this.sortedAvailableVideoStreams = sortedAvailableVideoStreams; + this.selectedVideoStreamIndex = selectedVideoStreamIndex; + } + + public MediaSourceTag(@NonNull final StreamInfo metadata) { + this(metadata, Collections.emptyList(), /*indexNotAvailable=*/-1); + } + + @NonNull + public StreamInfo getMetadata() { + return metadata; + } + + @NonNull + public List getSortedAvailableVideoStreams() { + return sortedAvailableVideoStreams; + } + + public int getSelectedVideoStreamIndex() { + return selectedVideoStreamIndex; + } + + @Nullable + public VideoStream getSelectedVideoStream() { + return selectedVideoStreamIndex < 0 || + selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() ? null : + sortedAvailableVideoStreams.get(selectedVideoStreamIndex); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java new file mode 100644 index 000000000..1da3ec211 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -0,0 +1,84 @@ +package org.schabi.newpipe.player.resolver; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.Util; + +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.player.helper.PlayerDataSource; + +public interface PlaybackResolver extends Resolver { + + @Nullable + default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource, + @NonNull final StreamInfo info) { + final StreamType streamType = info.getStreamType(); + if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { + return null; + } + + final MediaSourceTag tag = new MediaSourceTag(info); + if (!info.getHlsUrl().isEmpty()) { + return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); + } else if (!info.getDashMpdUrl().isEmpty()) { + return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); + } + + return null; + } + + @NonNull + default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, + @NonNull final String sourceUrl, + @C.ContentType final int type, + @NonNull final MediaSourceTag metadata) { + final Uri uri = Uri.parse(sourceUrl); + switch (type) { + case C.TYPE_SS: + return dataSource.getLiveSsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_DASH: + return dataSource.getLiveDashMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_HLS: + return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + @NonNull + default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource, + @NonNull final String sourceUrl, + @NonNull final String cacheKey, + @NonNull final String overrideExtension, + @NonNull final MediaSourceTag metadata) { + final Uri uri = Uri.parse(sourceUrl); + @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? + Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + + switch (type) { + case C.TYPE_SS: + return dataSource.getLiveSsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_DASH: + return dataSource.getDashMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_HLS: + return dataSource.getHlsMediaSourceFactory().setTag(metadata) + .createMediaSource(uri); + case C.TYPE_OTHER: + return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata) + .createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java new file mode 100644 index 000000000..e4f81385a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.player.resolver; + +import android.support.annotation.NonNull; + +public interface Resolver { + Produce resolve(@NonNull Source source); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java new file mode 100644 index 000000000..5aa42ce3c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -0,0 +1,122 @@ +package org.schabi.newpipe.player.resolver; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MergingMediaSource; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.Subtitles; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.util.ListHelper; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; +import static com.google.android.exoplayer2.C.TIME_UNSET; + +public class VideoPlaybackResolver implements PlaybackResolver { + + public interface QualityResolver { + int getDefaultResolutionIndex(final List sortedVideos); + int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality); + } + + @NonNull private final Context context; + @NonNull private final PlayerDataSource dataSource; + @NonNull private final QualityResolver qualityResolver; + + @Nullable private String playbackQuality; + + public VideoPlaybackResolver(@NonNull final Context context, + @NonNull final PlayerDataSource dataSource, + @NonNull final QualityResolver qualityResolver) { + this.context = context; + this.dataSource = dataSource; + this.qualityResolver = qualityResolver; + } + + @Override + public MediaSource resolve(@NonNull StreamInfo info) { + final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); + if (liveSource != null) return liveSource; + + List mediaSources = new ArrayList<>(); + + // Create video stream source + final List videos = ListHelper.getSortedStreamVideosList(context, + info.getVideoStreams(), info.getVideoOnlyStreams(), false); + final int index; + if (videos.isEmpty()) { + index = -1; + } else if (playbackQuality == null) { + index = qualityResolver.getDefaultResolutionIndex(videos); + } else { + index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality()); + } + final MediaSourceTag tag = new MediaSourceTag(info, videos, index); + @Nullable final VideoStream video = tag.getSelectedVideoStream(); + + if (video != null) { + final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(), + PlayerHelper.cacheKeyOf(info, video), + MediaFormat.getSuffixById(video.getFormatId()), tag); + mediaSources.add(streamSource); + } + + // Create optional audio stream source + final List audioStreams = info.getAudioStreams(); + final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( + ListHelper.getDefaultAudioFormat(context, audioStreams)); + // Use the audio stream if there is no video stream, or + // Merge with audio stream in case if video does not contain audio + if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { + final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(), + PlayerHelper.cacheKeyOf(info, audio), + MediaFormat.getSuffixById(audio.getFormatId()), tag); + mediaSources.add(audioSource); + } + + // If there is no audio or video sources, then this media source cannot be played back + if (mediaSources.isEmpty()) return null; + // Below are auxiliary media sources + + // Create subtitle sources + for (final Subtitles subtitle : info.getSubtitles()) { + final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); + if (mimeType == null) continue; + + final Format textFormat = Format.createTextSampleFormat(null, mimeType, + SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); + final MediaSource textSource = dataSource.getSampleMediaSourceFactory() + .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); + mediaSources.add(textSource); + } + + if (mediaSources.size() == 1) { + return mediaSources.get(0); + } else { + return new MergingMediaSource(mediaSources.toArray( + new MediaSource[mediaSources.size()])); + } + } + + @Nullable + public String getPlaybackQuality() { + return playbackQuality; + } + + public void setPlaybackQuality(@Nullable String playbackQuality) { + this.playbackQuality = playbackQuality; + } +}