mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-23 08:30:44 +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:
parent
19cbcd0c1d
commit
563a4137bd
@ -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()) {
|
||||
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 {
|
||||
|
@ -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,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);
|
||||
}
|
||||
|
@ -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) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user