From 563a4137bd9d21a1f2ee0efaefcd5dc0b33256bf Mon Sep 17 00:00:00 2001
From: John Zhen Mo <zhenmogukl@gmail.com>
Date: Sun, 25 Feb 2018 15:10:11 -0800
Subject: [PATCH] -Fixed inconsistent audio focus state when audio becomes
 noisy (e.g. headset unplugged). -Fixed live media sources failing when using
 cached data source by introducing cacheless data sources. -Added custom track
 selector to circumvent ExoPlayer's language normalization NPE. -Updated
 Extractor to correctly load live streams. -Removed deprecated deferred media
 source and media source manager. -Removed Livestream exceptions.

---
 app/build.gradle                              |   2 +-
 .../fragments/detail/VideoDetailFragment.java |  22 +-
 .../newpipe/player/BackgroundPlayer.java      |   3 +
 .../org/schabi/newpipe/player/BasePlayer.java |  53 ++-
 .../schabi/newpipe/player/VideoPlayer.java    |  66 +--
 .../newpipe/player/helper/CacheFactory.java   |  22 +-
 .../player/playback/CustomTrackSelector.java  | 114 +++++
 .../player/playback/DeferredMediaSource.java  | 216 ---------
 .../player/playback/MediaSourceManager.java   | 207 ++++++---
 .../playback/MediaSourceManagerAlt.java       | 422 ------------------
 .../schabi/newpipe/util/ExtractorHelper.java  |   2 -
 11 files changed, 356 insertions(+), 773 deletions(-)
 create mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
 delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java
 delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java

diff --git a/app/build.gradle b/app/build.gradle
index c9bd8d003..ba6406d4b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -55,7 +55,7 @@ dependencies {
         exclude module: 'support-annotations'
     }
 
-    implementation 'com.github.TeamNewPipe:NewPipeExtractor:86db415b181'
+    implementation 'com.github.karyogamy:NewPipeExtractor:837dbd6b86'
 
     testImplementation 'junit:junit:4.12'
     testImplementation 'org.mockito:mockito-core:1.10.19'
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 94a2f8ec0..b306721ba 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -56,6 +56,7 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
 import org.schabi.newpipe.extractor.stream.AudioStream;
 import org.schabi.newpipe.extractor.stream.StreamInfo;
 import org.schabi.newpipe.extractor.stream.StreamInfoItem;
+import org.schabi.newpipe.extractor.stream.StreamType;
 import org.schabi.newpipe.extractor.stream.VideoStream;
 import org.schabi.newpipe.fragments.BackPressable;
 import org.schabi.newpipe.fragments.BaseStateFragment;
@@ -1192,11 +1193,20 @@ public class VideoDetailFragment
                     0);
         }
 
-        if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) {
-            detailControlsBackground.setVisibility(View.GONE);
-            detailControlsPopup.setVisibility(View.GONE);
-            spinnerToolbar.setVisibility(View.GONE);
-            thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
+        switch (info.getStreamType()) {
+            case LIVE_STREAM:
+            case AUDIO_LIVE_STREAM:
+                detailControlsDownload.setVisibility(View.GONE);
+                spinnerToolbar.setVisibility(View.GONE);
+                break;
+            default:
+                if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break;
+
+                detailControlsBackground.setVisibility(View.GONE);
+                detailControlsPopup.setVisibility(View.GONE);
+                spinnerToolbar.setVisibility(View.GONE);
+                thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
+                break;
         }
 
         if (autoPlayEnabled) {
@@ -1216,8 +1226,6 @@ public class VideoDetailFragment
 
         if (exception instanceof YoutubeStreamExtractor.GemaException) {
             onBlockedByGemaError();
-        } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
-            showError(getString(R.string.live_streams_not_supported), false);
         } else if (exception instanceof ContentNotAvailableException) {
             showError(getString(R.string.content_not_available), false);
         } else {
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 fd47a7167..2fc3252a7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
@@ -391,6 +391,9 @@ 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.audio_streams);
             if (index < 0 || index >= info.audio_streams.size()) return null;
 
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 9ee83427d..de86ea587 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -43,7 +43,6 @@ import com.google.android.exoplayer2.Player;
 import com.google.android.exoplayer2.RenderersFactory;
 import com.google.android.exoplayer2.SimpleExoPlayer;
 import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
 import com.google.android.exoplayer2.source.ExtractorMediaSource;
 import com.google.android.exoplayer2.source.MediaSource;
 import com.google.android.exoplayer2.source.SingleSampleMediaSource;
@@ -54,21 +53,23 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource;
 import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
 import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
 import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
 import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
 import com.google.android.exoplayer2.upstream.DataSource;
 import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
 import com.google.android.exoplayer2.util.Util;
 import com.nostra13.universalimageloader.core.ImageLoader;
 import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
 
+import org.schabi.newpipe.Downloader;
 import org.schabi.newpipe.R;
 import org.schabi.newpipe.extractor.stream.StreamInfo;
 import org.schabi.newpipe.history.HistoryRecordManager;
 import org.schabi.newpipe.player.helper.AudioReactor;
 import org.schabi.newpipe.player.helper.CacheFactory;
 import org.schabi.newpipe.player.helper.LoadController;
-import org.schabi.newpipe.player.playback.MediaSourceManagerAlt;
+import org.schabi.newpipe.player.playback.CustomTrackSelector;
+import org.schabi.newpipe.player.playback.MediaSourceManager;
 import org.schabi.newpipe.player.playback.PlaybackListener;
 import org.schabi.newpipe.playlist.PlayQueue;
 import org.schabi.newpipe.playlist.PlayQueueAdapter;
@@ -125,7 +126,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
     protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
     protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f};
 
-    protected MediaSourceManagerAlt playbackManager;
+    protected MediaSourceManager playbackManager;
     protected PlayQueue playQueue;
 
     protected StreamInfo currentInfo;
@@ -147,9 +148,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
 
     protected boolean isPrepared = false;
 
-    protected DefaultTrackSelector trackSelector;
+    protected CustomTrackSelector trackSelector;
     protected DataSource.Factory cacheDataSourceFactory;
-    protected DefaultExtractorsFactory extractorsFactory;
+    protected DataSource.Factory cachelessDataSourceFactory;
 
     protected SsMediaSource.Factory ssMediaSourceFactory;
     protected HlsMediaSource.Factory hlsMediaSourceFactory;
@@ -190,23 +191,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
         databaseUpdateReactor = new CompositeDisposable();
 
+        final String userAgent = Downloader.USER_AGENT;
         final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
-        final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
-        final LoadControl loadControl = new LoadController(context);
-        final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
+        final AdaptiveTrackSelection.Factory trackSelectionFactory =
+                new AdaptiveTrackSelection.Factory(bandwidthMeter);
 
-        trackSelector = new DefaultTrackSelector(trackSelectionFactory);
-        extractorsFactory = new DefaultExtractorsFactory();
-        cacheDataSourceFactory = new CacheFactory(context);
+        trackSelector = new CustomTrackSelector(trackSelectionFactory);
+        cacheDataSourceFactory = new CacheFactory(context, userAgent, bandwidthMeter);
+        cachelessDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
 
         ssMediaSourceFactory = new SsMediaSource.Factory(
-                new DefaultSsChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory);
-        hlsMediaSourceFactory = new HlsMediaSource.Factory(cacheDataSourceFactory);
+                new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory);
+        hlsMediaSourceFactory = new HlsMediaSource.Factory(cachelessDataSourceFactory);
         dashMediaSourceFactory = new DashMediaSource.Factory(
-                new DefaultDashChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory);
+                new DefaultDashChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory);
         extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory);
         sampleMediaSourceFactory = new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
 
+        final LoadControl loadControl = new LoadController(context);
+        final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
         simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
         audioReactor = new AudioReactor(context, simpleExoPlayer);
 
@@ -262,7 +265,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
     protected void initPlayback(final PlayQueue queue) {
         playQueue = queue;
         playQueue.init();
-        playbackManager = new MediaSourceManagerAlt(this, playQueue);
+        playbackManager = new MediaSourceManager(this, playQueue);
 
         if (playQueueAdapter != null) playQueueAdapter.dispose();
         playQueueAdapter = new PlayQueueAdapter(context, playQueue);
@@ -316,6 +319,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         recordManager = null;
     }
 
+    public MediaSource buildMediaSource(String url) {
+        return buildMediaSource(url, "");
+    }
+
     public MediaSource buildMediaSource(String url, String overrideExtension) {
         if (DEBUG) {
             Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]");
@@ -360,7 +367,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         if (intent == null || intent.getAction() == null) return;
         switch (intent.getAction()) {
             case AudioManager.ACTION_AUDIO_BECOMING_NOISY:
-                if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
+                if (isPlaying()) onVideoPlayPause();
                 break;
         }
     }
@@ -721,6 +728,18 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
     }
 
+    @Nullable
+    @Override
+    public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
+        if (!info.getHlsUrl().isEmpty()) {
+            return buildMediaSource(info.getHlsUrl());
+        } else if (!info.getDashMpdUrl().isEmpty()) {
+            return buildMediaSource(info.getDashMpdUrl());
+        }
+
+        return null;
+    }
+
     @Override
     public void shutdown() {
         if (DEBUG) Log.d(TAG, "Shutting down...");
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 f8844c15e..3e03bc207 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -53,7 +53,6 @@ import com.google.android.exoplayer2.Player;
 import com.google.android.exoplayer2.SimpleExoPlayer;
 import com.google.android.exoplayer2.source.MediaSource;
 import com.google.android.exoplayer2.source.MergingMediaSource;
-import com.google.android.exoplayer2.source.SingleSampleMediaSource;
 import com.google.android.exoplayer2.source.TrackGroup;
 import com.google.android.exoplayer2.source.TrackGroupArray;
 import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -65,6 +64,7 @@ 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.playlist.PlayQueueItem;
@@ -305,8 +305,7 @@ public abstract class VideoPlayer extends BasePlayer
             captionItem.setOnMenuItemClickListener(menuItem -> {
                 final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
                 if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
-                    trackSelector.setParameters(trackSelector.getParameters().buildUpon()
-                            .setPreferredTextLanguage(captionLanguage).build());
+                    trackSelector.setPreferredTextLanguage(captionLanguage);
                     trackSelector.setRendererDisabled(textRendererIndex, false);
                 }
                 return true;
@@ -328,21 +327,32 @@ public abstract class VideoPlayer extends BasePlayer
         qualityTextView.setVisibility(View.GONE);
         playbackSpeedTextView.setVisibility(View.GONE);
 
-        if (info != null && info.video_streams.size() + info.video_only_streams.size() > 0) {
-            final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
-                    info.video_streams, info.video_only_streams, false);
-            availableStreams = new ArrayList<>(videos);
-            if (playbackQuality == null) {
-                selectedStreamIndex = getDefaultResolutionIndex(videos);
-            } else {
-                selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
-            }
+        final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
 
-            buildQualityMenu();
-            qualityTextView.setVisibility(View.VISIBLE);
-            surfaceView.setVisibility(View.VISIBLE);
-        } else {
-            surfaceView.setVisibility(View.GONE);
+        switch (streamType) {
+            case VIDEO_STREAM:
+                if (info.video_streams.size() + info.video_only_streams.size() == 0) break;
+
+                final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
+                        info.video_streams, info.video_only_streams, false);
+                availableStreams = new ArrayList<>(videos);
+                if (playbackQuality == null) {
+                    selectedStreamIndex = getDefaultResolutionIndex(videos);
+                } else {
+                    selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
+                }
+
+                buildQualityMenu();
+                qualityTextView.setVisibility(View.VISIBLE);
+                surfaceView.setVisibility(View.VISIBLE);
+                break;
+
+            case AUDIO_STREAM:
+            case AUDIO_LIVE_STREAM:
+                surfaceView.setVisibility(View.GONE);
+                break;
+            default:
+                break;
         }
 
         buildPlaybackSpeedMenu();
@@ -352,6 +362,9 @@ 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<MediaSource> mediaSources = new ArrayList<>();
 
         // Create video stream source
@@ -529,26 +542,15 @@ public abstract class VideoPlayer extends BasePlayer
         }
 
         // Normalize mismatching language strings
-        final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
-        // Because ExoPlayer normalizes the preferred language string but not the text track
-        // language strings, some preferred language string will have the language name in lowercase
-        String formattedPreferredLanguage = null;
-        if (preferredLanguage != null) {
-            for (final String language : availableLanguages) {
-                if (language.compareToIgnoreCase(preferredLanguage) == 0) {
-                    formattedPreferredLanguage = language;
-                    break;
-                }
-            }
-        }
+        final String preferredLanguage = trackSelector.getPreferredTextLanguage();
 
         // Build UI
         buildCaptionMenu(availableLanguages);
-        if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null ||
-                !availableLanguages.contains(formattedPreferredLanguage)) {
+        if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null ||
+                !availableLanguages.contains(preferredLanguage)) {
             captionTextView.setText(R.string.caption_none);
         } else {
-            captionTextView.setText(formattedPreferredLanguage);
+            captionTextView.setText(preferredLanguage);
         }
         captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
     }
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
index dce74ffb5..900f13ae9 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
@@ -4,11 +4,14 @@ import android.content.Context;
 import android.support.annotation.NonNull;
 import android.util.Log;
 
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
 import com.google.android.exoplayer2.upstream.DataSource;
 import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
 import com.google.android.exoplayer2.upstream.DefaultDataSource;
 import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
 import com.google.android.exoplayer2.upstream.FileDataSource;
+import com.google.android.exoplayer2.upstream.TransferListener;
 import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
 import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
 import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
@@ -33,18 +36,21 @@ public class CacheFactory implements DataSource.Factory {
     // todo: make this a singleton?
     private static SimpleCache cache;
 
-    public CacheFactory(@NonNull final Context context) {
-        this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context));
+    public CacheFactory(@NonNull final Context context,
+                        @NonNull final String userAgent,
+                        @NonNull final TransferListener<? super DataSource> transferListener) {
+        this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context),
+                PlayerHelper.getPreferredFileSize(context));
     }
 
-    CacheFactory(@NonNull final Context context, final long maxCacheSize, final long maxFileSize) {
-        super();
+    private CacheFactory(@NonNull final Context context,
+                         @NonNull final String userAgent,
+                         @NonNull final TransferListener<? super DataSource> transferListener,
+                         final long maxCacheSize,
+                         final long maxFileSize) {
         this.maxFileSize = maxFileSize;
 
-        final String userAgent = Downloader.USER_AGENT;
-        final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
-        dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter);
-
+        dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
         cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
         if (!cacheDir.exists()) {
             //noinspection ResultOfMethodCallIgnored
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
new file mode 100644
index 000000000..d80ea5bae
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
@@ -0,0 +1,114 @@
+package org.schabi.newpipe.player.playback;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * This class allows irregular text language labels for use when selecting text captions and
+ * is mostly a copy-paste from {@link DefaultTrackSelector}.
+ *
+ * This is a hack and should be removed once ExoPlayer fixes language normalization to accept
+ * a broader set of languages. 
+ * */
+public class CustomTrackSelector extends DefaultTrackSelector {
+    private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
+
+    private String preferredTextLanguage;
+
+    public CustomTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) {
+        super(adaptiveTrackSelectionFactory);
+    }
+
+    public String getPreferredTextLanguage() {
+        return preferredTextLanguage;
+    }
+
+    public void setPreferredTextLanguage(@NonNull final String label) {
+        Assertions.checkNotNull(label);
+        if (!label.equals(preferredTextLanguage)) {
+            preferredTextLanguage = label;
+            invalidate();
+        }
+    }
+
+    /** @see DefaultTrackSelector#formatHasLanguage(Format, String)*/
+    protected static boolean formatHasLanguage(Format format, String language) {
+        return language != null && TextUtils.equals(language, format.language);
+    }
+
+    /** @see DefaultTrackSelector#formatHasNoLanguage(Format)*/
+    protected static boolean formatHasNoLanguage(Format format) {
+        return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED);
+    }
+
+    /** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */
+    @Override
+    protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
+                                             Parameters params) throws ExoPlaybackException {
+        TrackGroup selectedGroup = null;
+        int selectedTrackIndex = 0;
+        int selectedTrackScore = 0;
+        for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+            TrackGroup trackGroup = groups.get(groupIndex);
+            int[] trackFormatSupport = formatSupport[groupIndex];
+            for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+                if (isSupported(trackFormatSupport[trackIndex],
+                        params.exceedRendererCapabilitiesIfNecessary)) {
+                    Format format = trackGroup.getFormat(trackIndex);
+                    int maskedSelectionFlags =
+                            format.selectionFlags & ~params.disabledTextTrackSelectionFlags;
+                    boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+                    boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
+                    int trackScore;
+                    boolean preferredLanguageFound = formatHasLanguage(format, preferredTextLanguage);
+                    if (preferredLanguageFound
+                            || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) {
+                        if (isDefault) {
+                            trackScore = 8;
+                        } else if (!isForced) {
+                            // Prefer non-forced to forced if a preferred text language has been specified. Where
+                            // both are provided the non-forced track will usually contain the forced subtitles as
+                            // a subset.
+                            trackScore = 6;
+                        } else {
+                            trackScore = 4;
+                        }
+                        trackScore += preferredLanguageFound ? 1 : 0;
+                    } else if (isDefault) {
+                        trackScore = 3;
+                    } else if (isForced) {
+                        if (formatHasLanguage(format, params.preferredAudioLanguage)) {
+                            trackScore = 2;
+                        } else {
+                            trackScore = 1;
+                        }
+                    } else {
+                        // Track should not be selected.
+                        continue;
+                    }
+                    if (isSupported(trackFormatSupport[trackIndex], false)) {
+                        trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+                    }
+                    if (trackScore > selectedTrackScore) {
+                        selectedGroup = trackGroup;
+                        selectedTrackIndex = trackIndex;
+                        selectedTrackScore = trackScore;
+                    }
+                }
+            }
+        }
+        return selectedGroup == null ? null
+                : new FixedTrackSelection(selectedGroup, selectedTrackIndex);
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java
deleted file mode 100644
index 3ae744d18..000000000
--- a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package org.schabi.newpipe.player.playback;
-
-import android.support.annotation.NonNull;
-import android.util.Log;
-
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.source.MediaPeriod;
-import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.upstream.Allocator;
-
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.playlist.PlayQueueItem;
-
-import java.io.IOException;
-
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.functions.Consumer;
-import io.reactivex.functions.Function;
-import io.reactivex.schedulers.Schedulers;
-
-/**
- * DeferredMediaSource is specifically designed to allow external control over when
- * the source metadata are loaded while being compatible with ExoPlayer's playlists.
- *
- * This media source follows the structure of how NewPipeExtractor's
- * {@link org.schabi.newpipe.extractor.stream.StreamInfoItem} is converted into
- * {@link org.schabi.newpipe.extractor.stream.StreamInfo}. Once conversion is complete,
- * this media source behaves identically as any other native media sources.
- * */
-public final class DeferredMediaSource implements MediaSource {
-    private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode());
-
-    /**
-     * This state indicates the {@link DeferredMediaSource} has just been initialized or reset.
-     * The source must be prepared and loaded again before playback.
-     * */
-    public final static int STATE_INIT = 0;
-    /**
-     * This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load.
-     * */
-    public final static int STATE_PREPARED = 1;
-    /**
-     * This state indicates the {@link DeferredMediaSource} has been loaded without errors and
-     * is ready for playback.
-     * */
-    public final static int STATE_LOADED = 2;
-
-    public interface Callback {
-        /**
-         * Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution
-         * from a given StreamInfo.
-         * */
-        MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info);
-    }
-
-    private PlayQueueItem stream;
-    private Callback callback;
-    private int state;
-
-    private MediaSource mediaSource;
-
-    /* Custom internal objects */
-    private Disposable loader;
-    private ExoPlayer exoPlayer;
-    private Listener listener;
-    private Throwable error;
-
-    public DeferredMediaSource(@NonNull final PlayQueueItem stream,
-                               @NonNull final Callback callback) {
-        this.stream = stream;
-        this.callback = callback;
-        this.state = STATE_INIT;
-    }
-
-    /**
-     * Returns the current state of the {@link DeferredMediaSource}.
-     *
-     * @see DeferredMediaSource#STATE_INIT
-     * @see DeferredMediaSource#STATE_PREPARED
-     * @see DeferredMediaSource#STATE_LOADED
-     * */
-    public int state() {
-        return state;
-    }
-
-    /**
-     * Parameters are kept in the class for delayed preparation.
-     * */
-    @Override
-    public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
-        this.exoPlayer = exoPlayer;
-        this.listener = listener;
-        this.state = STATE_PREPARED;
-    }
-
-    /**
-     * Externally controlled loading. This method fully prepares the source to be used
-     * like any other native {@link com.google.android.exoplayer2.source.MediaSource}.
-     *
-     * Ideally, this should be called after this source has entered PREPARED state and
-     * called once only.
-     *
-     * If loading fails here, an error will be propagated out and result in an
-     * {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException},
-     * which is delegated to the player.
-     * */
-    public synchronized void load() {
-        if (stream == null) {
-            Log.e(TAG, "Stream Info missing, media source loading terminated.");
-            return;
-        }
-        if (state != STATE_PREPARED || loader != null) return;
-
-        Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
-
-        loader = stream.getStream()
-                .map(streamInfo -> onStreamInfoReceived(stream, streamInfo))
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(this::onMediaSourceReceived, this::onStreamInfoError);
-    }
-
-    private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,
-                                             @NonNull final StreamInfo info) throws Exception {
-        if (callback == null) {
-            throw new Exception("No available callback for resolving stream info.");
-        }
-
-        final MediaSource mediaSource = callback.sourceOf(item, info);
-
-        if (mediaSource == null) {
-            throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() +
-                    ", audio count: " + info.audio_streams.size() +
-                    ", video count: " + info.video_only_streams.size() + info.video_streams.size());
-        }
-
-        return mediaSource;
-    }
-
-    private void onMediaSourceReceived(final MediaSource mediaSource) throws Exception {
-        if (exoPlayer == null || listener == null || mediaSource == null) {
-            throw new Exception("MediaSource loading failed. URL: " + stream.getUrl());
-        }
-
-        Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
-        state = STATE_LOADED;
-
-        this.mediaSource = mediaSource;
-        this.mediaSource.prepareSource(exoPlayer, false, listener);
-    }
-
-    private void onStreamInfoError(final Throwable throwable) {
-        Log.e(TAG, "Loading error:", throwable);
-        error = throwable;
-        state = STATE_LOADED;
-    }
-
-    /**
-     * Delegate all errors to the player after {@link #load() load} is complete.
-     *
-     * Specifically, this method is called after an exception has occurred during loading or
-     * {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}.
-     * */
-    @Override
-    public void maybeThrowSourceInfoRefreshError() throws IOException {
-        if (error != null) {
-            throw new IOException(error);
-        }
-
-        if (mediaSource != null) {
-            mediaSource.maybeThrowSourceInfoRefreshError();
-        }
-    }
-
-    @Override
-    public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) {
-        return mediaSource.createPeriod(mediaPeriodId, allocator);
-    }
-
-    /**
-     * Releases the media period (buffers).
-     *
-     * This may be called after {@link #releaseSource releaseSource}.
-     * */
-    @Override
-    public void releasePeriod(MediaPeriod mediaPeriod) {
-        mediaSource.releasePeriod(mediaPeriod);
-    }
-
-    /**
-     * Cleans up all internal custom objects creating during loading.
-     *
-     * This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource}
-     * is released or when the player is stopped.
-     *
-     * This method should not release or set null the resources passed in through the constructor.
-     * This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}.
-     * */
-    @Override
-    public void releaseSource() {
-        if (mediaSource != null) {
-            mediaSource.releaseSource();
-        }
-        if (loader != null) {
-            loader.dispose();
-        }
-
-        /* Do not set mediaSource as null here as it may be called through releasePeriod */
-        loader = null;
-        exoPlayer = null;
-        listener = null;
-        error = null;
-
-        state = STATE_INIT;
-    }
-}
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 9dea4fdce..8d822fa54 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
@@ -1,13 +1,17 @@
 package org.schabi.newpipe.player.playback;
 
 import android.support.annotation.Nullable;
-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;
+import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
 import org.schabi.newpipe.playlist.PlayQueue;
 import org.schabi.newpipe.playlist.PlayQueueItem;
 import org.schabi.newpipe.playlist.events.MoveEvent;
@@ -15,18 +19,22 @@ import org.schabi.newpipe.playlist.events.PlayQueueEvent;
 import org.schabi.newpipe.playlist.events.RemoveEvent;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
+import io.reactivex.Single;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.CompositeDisposable;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.disposables.SerialDisposable;
 import io.reactivex.functions.Consumer;
 import io.reactivex.subjects.PublishSubject;
 
 public class MediaSourceManager {
-    private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
     // One-side rolling window size for default loading
     // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0
     private final int windowSize;
@@ -40,17 +48,17 @@ public class MediaSourceManager {
     private final PublishSubject<Long> debouncedLoadSignal;
     private final Disposable debouncedLoader;
 
-    private final DeferredMediaSource.Callback sourceBuilder;
-
     private DynamicConcatenatingMediaSource sources;
 
     private Subscription playQueueReactor;
-    private SerialDisposable syncReactor;
-
-    private PlayQueueItem syncedItem;
+    private CompositeDisposable loaderReactor;
 
     private boolean isBlocked;
 
+    private SerialDisposable syncReactor;
+    private PlayQueueItem syncedItem;
+    private Set<PlayQueueItem> loadingItems;
+
     public MediaSourceManager(@NonNull final PlaybackListener listener,
                               @NonNull final PlayQueue playQueue) {
         this(listener, playQueue, 1, 400L);
@@ -61,7 +69,8 @@ public class MediaSourceManager {
                                final int windowSize,
                                final long loadDebounceMillis) {
         if (windowSize <= 0) {
-            throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0");
+            throw new UnsupportedOperationException(
+                    "MediaSourceManager window size must be greater than 0");
         }
 
         this.playbackListener = listener;
@@ -69,27 +78,20 @@ public class MediaSourceManager {
         this.windowSize = windowSize;
         this.loadDebounceMillis = loadDebounceMillis;
 
-        this.syncReactor = new SerialDisposable();
+        this.loaderReactor = new CompositeDisposable();
         this.debouncedLoadSignal = PublishSubject.create();
         this.debouncedLoader = getDebouncedLoader();
 
-        this.sourceBuilder = getSourceBuilder();
-
         this.sources = new DynamicConcatenatingMediaSource();
 
+        this.syncReactor = new SerialDisposable();
+        this.loadingItems = Collections.synchronizedSet(new HashSet<>());
+
         playQueue.getBroadcastReceiver()
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(getReactor());
     }
 
-    /*//////////////////////////////////////////////////////////////////////////
-    // DeferredMediaSource listener
-    //////////////////////////////////////////////////////////////////////////*/
-
-    private DeferredMediaSource.Callback getSourceBuilder() {
-        return playbackListener::sourceOf;
-    }
-
     /*//////////////////////////////////////////////////////////////////////////
     // Exposed Methods
     //////////////////////////////////////////////////////////////////////////*/
@@ -100,10 +102,12 @@ public class MediaSourceManager {
         if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
         if (debouncedLoader != null) debouncedLoader.dispose();
         if (playQueueReactor != null) playQueueReactor.cancel();
+        if (loaderReactor != null) loaderReactor.dispose();
         if (syncReactor != null) syncReactor.dispose();
         if (sources != null) sources.releaseSource();
 
         playQueueReactor = null;
+        loaderReactor = null;
         syncReactor = null;
         syncedItem = null;
         sources = null;
@@ -121,7 +125,8 @@ public class MediaSourceManager {
     /**
      * Blocks the player and repopulate the sources.
      *
-     * Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}.
+     * Does not ensure the player is unblocked and should be done explicitly
+     * through {@link #load() load}.
      * */
     public void reset() {
         tryBlock();
@@ -210,41 +215,45 @@ public class MediaSourceManager {
     }
 
     /*//////////////////////////////////////////////////////////////////////////
-    // Internal Helpers
+    // Playback Locking
     //////////////////////////////////////////////////////////////////////////*/
 
     private boolean isPlayQueueReady() {
-        return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > windowSize;
+        final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize;
+        return playQueue.isComplete() || isWindowLoaded;
     }
 
-    private boolean tryBlock() {
-        if (!isBlocked) {
-            playbackListener.block();
-            resetSources();
-            isBlocked = true;
-            return true;
-        }
-        return false;
+    private boolean isPlaybackReady() {
+        return sources.getSize() > 0 &&
+                sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource;
     }
 
-    private boolean tryUnblock() {
-        if (isPlayQueueReady() && isBlocked && sources != null) {
+    private void tryBlock() {
+        if (isBlocked) return;
+
+        playbackListener.block();
+        resetSources();
+
+        isBlocked = true;
+    }
+
+    private void tryUnblock() {
+        if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) {
             isBlocked = false;
             playbackListener.unblock(sources);
-            return true;
         }
-        return false;
     }
 
+    /*//////////////////////////////////////////////////////////////////////////
+    // Metadata Synchronization TODO: maybe this should be a separate manager
+    //////////////////////////////////////////////////////////////////////////*/
+
     private void sync() {
         final PlayQueueItem currentItem = playQueue.getItem();
-        if (currentItem == null) return;
+        if (isBlocked || currentItem == null) return;
 
         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
-        final Consumer<Throwable> onError = throwable -> {
-            Log.e(TAG, "Sync error:", throwable);
-            syncInternal(currentItem, null);
-        };
+        final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
 
         if (syncedItem != currentItem) {
             syncedItem = currentItem;
@@ -264,6 +273,17 @@ public class MediaSourceManager {
         }
     }
 
+    /*//////////////////////////////////////////////////////////////////////////
+    // MediaSource Loading
+    //////////////////////////////////////////////////////////////////////////*/
+
+    private Disposable getDebouncedLoader() {
+        return debouncedLoadSignal
+                .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(timestamp -> loadImmediate());
+    }
+
     private void loadDebounced() {
         debouncedLoadSignal.onNext(System.currentTimeMillis());
     }
@@ -279,76 +299,113 @@ public class MediaSourceManager {
         final int leftBound = Math.max(0, currentIndex - windowSize);
         final int rightLimit = currentIndex + windowSize + 1;
         final int rightBound = Math.min(playQueue.size(), rightLimit);
-        final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
+        final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound,
+                rightBound));
 
         // Do a round robin
         final int excess = rightLimit - playQueue.size();
-        if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
+        if (excess >= 0) {
+            items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
+        }
 
         for (final PlayQueueItem item: items) loadItem(item);
     }
 
     private void loadItem(@Nullable final PlayQueueItem item) {
-        if (item == null) return;
+        if (sources == null || item == null) return;
 
         final int index = playQueue.indexOf(item);
         if (index > sources.getSize() - 1) return;
 
-        final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item));
-        if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load();
+        final Consumer<ManagedMediaSource> onDone = mediaSource -> {
+            update(playQueue.indexOf(item), mediaSource);
+            loadingItems.remove(item);
+            tryUnblock();
+            sync();
+        };
+
+        if (!loadingItems.contains(item) &&
+                ((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) {
+
+            loadingItems.add(item);
+            final Disposable loader = getLoadedMediaSource(item)
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe(onDone);
+            loaderReactor.add(loader);
+        }
 
         tryUnblock();
-        if (!isBlocked) sync();
+        sync();
     }
 
+    private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
+        return stream.getStream().map(streamInfo -> {
+            if (playbackListener == null) {
+                return new FailedMediaSource(stream, new IllegalStateException(
+                        "MediaSourceManager playback listener unavailable"));
+            }
+
+            final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
+            if (source == null) {
+                return new FailedMediaSource(stream, new IllegalStateException(
+                        "MediaSource resolution is null"));
+            }
+
+            final long expiration = System.currentTimeMillis() +
+                    TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS);
+            return new LoadedMediaSource(source, expiration);
+        }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // MediaSource Playlist Helpers
+    //////////////////////////////////////////////////////////////////////////*/
+
     private void resetSources() {
         if (this.sources != null) this.sources.releaseSource();
         this.sources = new DynamicConcatenatingMediaSource();
     }
 
     private void populateSources() {
-        if (sources == null) return;
+        if (sources == null || sources.getSize() >= playQueue.size()) return;
 
-        for (final PlayQueueItem item : playQueue.getStreams()) {
-            insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder));
+        for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
+            emplace(index, new PlaceholderMediaSource());
         }
     }
 
-    private Disposable getDebouncedLoader() {
-        return debouncedLoadSignal
-                .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(timestamp -> loadImmediate());
-    }
     /*//////////////////////////////////////////////////////////////////////////
-    // Media Source List Manipulation
+    // MediaSource Playlist Manipulation
     //////////////////////////////////////////////////////////////////////////*/
 
     /**
-     * Inserts a source into {@link DynamicConcatenatingMediaSource} with position
-     * in respect to the play queue.
-     *
-     * If the play queue index already exists, then the insert is ignored.
+     * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource}
+     * with position * in respect to the play queue only if no {@link MediaSource}
+     * already exists at the given index.
      * */
-    private void insert(final int queueIndex, final DeferredMediaSource source) {
+    private void emplace(final int index, final MediaSource source) {
         if (sources == null) return;
-        if (queueIndex < 0 || queueIndex < sources.getSize()) return;
+        if (index < 0 || index < sources.getSize()) return;
 
-        sources.addMediaSource(queueIndex, source);
+        sources.addMediaSource(index, source);
     }
 
     /**
-     * Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index.
-     *
-     * If the play queue index does not exist, the removal is ignored.
+     * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource}
+     * at the given index. If this index is out of bound, then the removal is ignored.
      * */
-    private void remove(final int queueIndex) {
+    private void remove(final int index) {
         if (sources == null) return;
-        if (queueIndex < 0 || queueIndex > sources.getSize()) return;
+        if (index < 0 || index > sources.getSize()) return;
 
-        sources.removeMediaSource(queueIndex);
+        sources.removeMediaSource(index);
     }
 
+    /**
+     * Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
+     * from the given source index to the target index. If either index is out of bound,
+     * then the call is ignored.
+     * */
     private void move(final int source, final int target) {
         if (sources == null) return;
         if (source < 0 || target < 0) return;
@@ -356,4 +413,18 @@ public class MediaSourceManager {
 
         sources.moveMediaSource(source, target);
     }
+
+    /**
+     * Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
+     * at the given index with a given {@link MediaSource}. If the index is out of bound,
+     * then the replacement is ignored.
+     * */
+    private void update(final int index, final MediaSource source) {
+        if (sources == null) return;
+        if (index < 0 || index >= sources.getSize()) return;
+
+        sources.addMediaSource(index + 1, source, () -> {
+            if (sources != null) sources.removeMediaSource(index);
+        });
+    }
 }
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java
deleted file mode 100644
index 03b583e07..000000000
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManagerAlt.java
+++ /dev/null
@@ -1,422 +0,0 @@
-package org.schabi.newpipe.player.playback;
-
-import android.support.annotation.Nullable;
-
-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;
-import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
-import org.schabi.newpipe.playlist.PlayQueue;
-import org.schabi.newpipe.playlist.PlayQueueItem;
-import org.schabi.newpipe.playlist.events.MoveEvent;
-import org.schabi.newpipe.playlist.events.PlayQueueEvent;
-import org.schabi.newpipe.playlist.events.RemoveEvent;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import io.reactivex.Single;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.annotations.NonNull;
-import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.disposables.SerialDisposable;
-import io.reactivex.functions.Consumer;
-import io.reactivex.subjects.PublishSubject;
-
-public class MediaSourceManagerAlt {
-    // One-side rolling window size for default loading
-    // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0
-    private final int windowSize;
-    private final PlaybackListener playbackListener;
-    private final PlayQueue playQueue;
-
-    // Process only the last load order when receiving a stream of load orders (lessens I/O)
-    // The higher it is, the less loading occurs during rapid noncritical timeline changes
-    // Not recommended to go below 100ms
-    private final long loadDebounceMillis;
-    private final PublishSubject<Long> debouncedLoadSignal;
-    private final Disposable debouncedLoader;
-
-    private DynamicConcatenatingMediaSource sources;
-
-    private Subscription playQueueReactor;
-    private CompositeDisposable loaderReactor;
-
-    private boolean isBlocked;
-
-    private SerialDisposable syncReactor;
-    private PlayQueueItem syncedItem;
-    private Set<PlayQueueItem> loadingItems;
-
-    public MediaSourceManagerAlt(@NonNull final PlaybackListener listener,
-                                 @NonNull final PlayQueue playQueue) {
-        this(listener, playQueue, 0, 400L);
-    }
-
-    private MediaSourceManagerAlt(@NonNull final PlaybackListener listener,
-                                  @NonNull final PlayQueue playQueue,
-                                  final int windowSize,
-                                  final long loadDebounceMillis) {
-        if (windowSize < 0) {
-            throw new UnsupportedOperationException(
-                    "MediaSourceManager window size must be greater than 0");
-        }
-
-        this.playbackListener = listener;
-        this.playQueue = playQueue;
-        this.windowSize = windowSize;
-        this.loadDebounceMillis = loadDebounceMillis;
-
-        this.loaderReactor = new CompositeDisposable();
-        this.debouncedLoadSignal = PublishSubject.create();
-        this.debouncedLoader = getDebouncedLoader();
-
-        this.sources = new DynamicConcatenatingMediaSource();
-
-        this.syncReactor = new SerialDisposable();
-        this.loadingItems = Collections.synchronizedSet(new HashSet<>());
-
-        playQueue.getBroadcastReceiver()
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(getReactor());
-    }
-
-    /*//////////////////////////////////////////////////////////////////////////
-    // Exposed Methods
-    //////////////////////////////////////////////////////////////////////////*/
-    /**
-     * Dispose the manager and releases all message buses and loaders.
-     * */
-    public void dispose() {
-        if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
-        if (debouncedLoader != null) debouncedLoader.dispose();
-        if (playQueueReactor != null) playQueueReactor.cancel();
-        if (loaderReactor != null) loaderReactor.dispose();
-        if (syncReactor != null) syncReactor.dispose();
-        if (sources != null) sources.releaseSource();
-
-        playQueueReactor = null;
-        loaderReactor = null;
-        syncReactor = null;
-        syncedItem = null;
-        sources = null;
-    }
-
-    /**
-     * Loads the current playing stream and the streams within its windowSize bound.
-     *
-     * Unblocks the player once the item at the current index is loaded.
-     * */
-    public void load() {
-        loadDebounced();
-    }
-
-    /**
-     * Blocks the player and repopulate the sources.
-     *
-     * Does not ensure the player is unblocked and should be done explicitly
-     * through {@link #load() load}.
-     * */
-    public void reset() {
-        tryBlock();
-
-        syncedItem = null;
-        populateSources();
-    }
-    /*//////////////////////////////////////////////////////////////////////////
-    // Event Reactor
-    //////////////////////////////////////////////////////////////////////////*/
-
-    private Subscriber<PlayQueueEvent> getReactor() {
-        return new Subscriber<PlayQueueEvent>() {
-            @Override
-            public void onSubscribe(@NonNull Subscription d) {
-                if (playQueueReactor != null) playQueueReactor.cancel();
-                playQueueReactor = d;
-                playQueueReactor.request(1);
-            }
-
-            @Override
-            public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
-                if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage);
-            }
-
-            @Override
-            public void onError(@NonNull Throwable e) {}
-
-            @Override
-            public void onComplete() {}
-        };
-    }
-
-    private void onPlayQueueChanged(final PlayQueueEvent event) {
-        if (playQueue.isEmpty() && playQueue.isComplete()) {
-            playbackListener.shutdown();
-            return;
-        }
-
-        // Event specific action
-        switch (event.type()) {
-            case INIT:
-            case REORDER:
-            case ERROR:
-                reset();
-                break;
-            case APPEND:
-                populateSources();
-                break;
-            case REMOVE:
-                final RemoveEvent removeEvent = (RemoveEvent) event;
-                remove(removeEvent.getRemoveIndex());
-                break;
-            case MOVE:
-                final MoveEvent moveEvent = (MoveEvent) event;
-                move(moveEvent.getFromIndex(), moveEvent.getToIndex());
-                break;
-            case SELECT:
-            case RECOVERY:
-            default:
-                break;
-        }
-
-        // Loading and Syncing
-        switch (event.type()) {
-            case INIT:
-            case REORDER:
-            case ERROR:
-                loadImmediate(); // low frequency, critical events
-                break;
-            case APPEND:
-            case REMOVE:
-            case SELECT:
-            case MOVE:
-            case RECOVERY:
-            default:
-                loadDebounced(); // high frequency or noncritical events
-                break;
-        }
-
-        if (!isPlayQueueReady()) {
-            tryBlock();
-            playQueue.fetch();
-        }
-        if (playQueueReactor != null) playQueueReactor.request(1);
-    }
-
-    /*//////////////////////////////////////////////////////////////////////////
-    // Playback Locking
-    //////////////////////////////////////////////////////////////////////////*/
-
-    private boolean isPlayQueueReady() {
-        final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > windowSize;
-        return playQueue.isComplete() || isWindowLoaded;
-    }
-
-    private boolean isPlaybackReady() {
-        return sources.getSize() > 0 &&
-                sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource;
-    }
-
-    private void tryBlock() {
-        if (isBlocked) return;
-
-        playbackListener.block();
-
-        if (this.sources != null) this.sources.releaseSource();
-        this.sources = new DynamicConcatenatingMediaSource();
-
-        isBlocked = true;
-    }
-
-    private void tryUnblock() {
-        if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) {
-            isBlocked = false;
-            playbackListener.unblock(sources);
-        }
-    }
-
-    /*//////////////////////////////////////////////////////////////////////////
-    // Metadata Synchronization TODO: maybe this should be a separate manager
-    //////////////////////////////////////////////////////////////////////////*/
-
-    private void sync() {
-        final PlayQueueItem currentItem = playQueue.getItem();
-        if (isBlocked || currentItem == null) return;
-
-        final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
-        final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
-
-        if (syncedItem != currentItem) {
-            syncedItem = currentItem;
-            final Disposable sync = currentItem.getStream()
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(onSuccess, onError);
-            syncReactor.set(sync);
-        }
-    }
-
-    private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item,
-                              @Nullable final StreamInfo info) {
-        if (playQueue == null || playbackListener == null) return;
-        // Ensure the current item is up to date with the play queue
-        if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
-            playbackListener.sync(syncedItem,info);
-        }
-    }
-
-    /*//////////////////////////////////////////////////////////////////////////
-    // MediaSource Loading
-    //////////////////////////////////////////////////////////////////////////*/
-
-    private Disposable getDebouncedLoader() {
-        return debouncedLoadSignal
-                .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(timestamp -> loadImmediate());
-    }
-
-    private void populateSources() {
-        if (sources == null || sources.getSize() >= playQueue.size()) return;
-
-        for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
-            emplace(index, new PlaceholderMediaSource());
-        }
-    }
-
-    private void loadDebounced() {
-        debouncedLoadSignal.onNext(System.currentTimeMillis());
-    }
-
-    private void loadImmediate() {
-        // The current item has higher priority
-        final int currentIndex = playQueue.getIndex();
-        final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
-        if (currentItem == null) return;
-        loadItem(currentItem);
-
-        // The rest are just for seamless playback
-        final int leftBound = Math.max(0, currentIndex - windowSize);
-        final int rightLimit = currentIndex + windowSize + 1;
-        final int rightBound = Math.min(playQueue.size(), rightLimit);
-        final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound,
-                rightBound));
-
-        // Do a round robin
-        final int excess = rightLimit - playQueue.size();
-        if (excess >= 0) {
-            items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
-        }
-
-        for (final PlayQueueItem item: items) loadItem(item);
-    }
-
-    private void loadItem(@Nullable final PlayQueueItem item) {
-        if (sources == null || item == null) return;
-
-        final int index = playQueue.indexOf(item);
-        if (index > sources.getSize() - 1) return;
-
-        final Consumer<ManagedMediaSource> onDone = mediaSource -> {
-            update(playQueue.indexOf(item), mediaSource);
-            loadingItems.remove(item);
-            tryUnblock();
-            sync();
-        };
-
-        if (!loadingItems.contains(item) &&
-                ((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) {
-
-            loadingItems.add(item);
-            final Disposable loader = getLoadedMediaSource(item)
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(onDone);
-            loaderReactor.add(loader);
-        }
-
-        tryUnblock();
-        sync();
-    }
-
-    private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
-        return stream.getStream().map(streamInfo -> {
-            if (playbackListener == null) {
-                return new FailedMediaSource(stream, new IllegalStateException(
-                        "MediaSourceManager playback listener unavailable"));
-            }
-
-            final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
-            if (source == null) {
-                return new FailedMediaSource(stream, new IllegalStateException(
-                        "MediaSource resolution is null"));
-            }
-
-            final long expiration = System.currentTimeMillis() +
-                    TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS);
-            return new LoadedMediaSource(source, expiration);
-        }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
-    }
-
-    /*//////////////////////////////////////////////////////////////////////////
-    // Media Source List Manipulation
-    //////////////////////////////////////////////////////////////////////////*/
-
-    /**
-     * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource}
-     * with position * in respect to the play queue only if no {@link MediaSource}
-     * already exists at the given index.
-     * */
-    private void emplace(final int index, final MediaSource source) {
-        if (sources == null) return;
-        if (index < 0 || index < sources.getSize()) return;
-
-        sources.addMediaSource(index, source);
-    }
-
-    /**
-     * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource}
-     * at the given index. If this index is out of bound, then the removal is ignored.
-     * */
-    private void remove(final int index) {
-        if (sources == null) return;
-        if (index < 0 || index > sources.getSize()) return;
-
-        sources.removeMediaSource(index);
-    }
-
-    /**
-     * Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
-     * from the given source index to the target index. If either index is out of bound,
-     * then the call is ignored.
-     * */
-    private void move(final int source, final int target) {
-        if (sources == null) return;
-        if (source < 0 || target < 0) return;
-        if (source >= sources.getSize() || target >= sources.getSize()) return;
-
-        sources.moveMediaSource(source, target);
-    }
-
-    /**
-     * Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
-     * at the given index with a given {@link MediaSource}. If the index is out of bound,
-     * then the replacement is ignored.
-     * */
-    private void update(final int index, final MediaSource source) {
-        if (sources == null) return;
-        if (index < 0 || index >= sources.getSize()) return;
-
-        sources.addMediaSource(index + 1, source);
-        sources.removeMediaSource(index);
-    }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
index 1148171d7..78d6a6318 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
@@ -224,8 +224,6 @@ public final class ExtractorHelper {
                 Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
             } else if (exception instanceof YoutubeStreamExtractor.GemaException) {
                 Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
-            } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
-                Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show();
             } else if (exception instanceof ContentNotAvailableException) {
                 Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
             } else {