mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	-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.
This commit is contained in:
		| @@ -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' | ||||
|   | ||||
| @@ -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()) { | ||||
|         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 { | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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..."); | ||||
|   | ||||
| @@ -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,7 +327,12 @@ 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 StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); | ||||
|  | ||||
|         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); | ||||
| @@ -341,8 +345,14 @@ public abstract class VideoPlayer extends BasePlayer | ||||
|                 buildQualityMenu(); | ||||
|                 qualityTextView.setVisibility(View.VISIBLE); | ||||
|                 surfaceView.setVisibility(View.VISIBLE); | ||||
|         } else { | ||||
|                 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); | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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) { | ||||
|     private boolean isPlaybackReady() { | ||||
|         return sources.getSize() > 0 && | ||||
|                 sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource; | ||||
|     } | ||||
|  | ||||
|     private void tryBlock() { | ||||
|         if (isBlocked) return; | ||||
|  | ||||
|         playbackListener.block(); | ||||
|         resetSources(); | ||||
|  | ||||
|         isBlocked = true; | ||||
|             return true; | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     private boolean tryUnblock() { | ||||
|         if (isPlayQueueReady() && isBlocked && sources != null) { | ||||
|     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); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 John Zhen Mo
					John Zhen Mo