-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'
|
exclude module: 'support-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:86db415b181'
|
implementation 'com.github.karyogamy:NewPipeExtractor:837dbd6b86'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
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.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
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.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
|
@ -1192,11 +1193,20 @@ public class VideoDetailFragment
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) {
|
switch (info.getStreamType()) {
|
||||||
detailControlsBackground.setVisibility(View.GONE);
|
case LIVE_STREAM:
|
||||||
detailControlsPopup.setVisibility(View.GONE);
|
case AUDIO_LIVE_STREAM:
|
||||||
spinnerToolbar.setVisibility(View.GONE);
|
detailControlsDownload.setVisibility(View.GONE);
|
||||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
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) {
|
if (autoPlayEnabled) {
|
||||||
|
@ -1216,8 +1226,6 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||||
onBlockedByGemaError();
|
onBlockedByGemaError();
|
||||||
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
|
|
||||||
showError(getString(R.string.live_streams_not_supported), false);
|
|
||||||
} else if (exception instanceof ContentNotAvailableException) {
|
} else if (exception instanceof ContentNotAvailableException) {
|
||||||
showError(getString(R.string.content_not_available), false);
|
showError(getString(R.string.content_not_available), false);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -391,6 +391,9 @@ public final class BackgroundPlayer extends Service {
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
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);
|
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
|
||||||
if (index < 0 || index >= info.audio_streams.size()) return null;
|
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.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
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.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
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.DefaultSsChunkSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
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.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.Downloader;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.helper.AudioReactor;
|
import org.schabi.newpipe.player.helper.AudioReactor;
|
||||||
import org.schabi.newpipe.player.helper.CacheFactory;
|
import org.schabi.newpipe.player.helper.CacheFactory;
|
||||||
import org.schabi.newpipe.player.helper.LoadController;
|
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.player.playback.PlaybackListener;
|
||||||
import org.schabi.newpipe.playlist.PlayQueue;
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueAdapter;
|
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_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 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 PlayQueue playQueue;
|
||||||
|
|
||||||
protected StreamInfo currentInfo;
|
protected StreamInfo currentInfo;
|
||||||
|
@ -147,9 +148,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
|
|
||||||
protected boolean isPrepared = false;
|
protected boolean isPrepared = false;
|
||||||
|
|
||||||
protected DefaultTrackSelector trackSelector;
|
protected CustomTrackSelector trackSelector;
|
||||||
protected DataSource.Factory cacheDataSourceFactory;
|
protected DataSource.Factory cacheDataSourceFactory;
|
||||||
protected DefaultExtractorsFactory extractorsFactory;
|
protected DataSource.Factory cachelessDataSourceFactory;
|
||||||
|
|
||||||
protected SsMediaSource.Factory ssMediaSourceFactory;
|
protected SsMediaSource.Factory ssMediaSourceFactory;
|
||||||
protected HlsMediaSource.Factory hlsMediaSourceFactory;
|
protected HlsMediaSource.Factory hlsMediaSourceFactory;
|
||||||
|
@ -190,23 +191,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||||
databaseUpdateReactor = new CompositeDisposable();
|
databaseUpdateReactor = new CompositeDisposable();
|
||||||
|
|
||||||
|
final String userAgent = Downloader.USER_AGENT;
|
||||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||||
final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
final AdaptiveTrackSelection.Factory trackSelectionFactory =
|
||||||
final LoadControl loadControl = new LoadController(context);
|
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||||
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
|
|
||||||
|
|
||||||
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
trackSelector = new CustomTrackSelector(trackSelectionFactory);
|
||||||
extractorsFactory = new DefaultExtractorsFactory();
|
cacheDataSourceFactory = new CacheFactory(context, userAgent, bandwidthMeter);
|
||||||
cacheDataSourceFactory = new CacheFactory(context);
|
cachelessDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
|
||||||
|
|
||||||
ssMediaSourceFactory = new SsMediaSource.Factory(
|
ssMediaSourceFactory = new SsMediaSource.Factory(
|
||||||
new DefaultSsChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory);
|
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory);
|
||||||
hlsMediaSourceFactory = new HlsMediaSource.Factory(cacheDataSourceFactory);
|
hlsMediaSourceFactory = new HlsMediaSource.Factory(cachelessDataSourceFactory);
|
||||||
dashMediaSourceFactory = new DashMediaSource.Factory(
|
dashMediaSourceFactory = new DashMediaSource.Factory(
|
||||||
new DefaultDashChunkSource.Factory(cacheDataSourceFactory), cacheDataSourceFactory);
|
new DefaultDashChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory);
|
||||||
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory);
|
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory);
|
||||||
sampleMediaSourceFactory = new SingleSampleMediaSource.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);
|
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
|
||||||
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
||||||
|
|
||||||
|
@ -262,7 +265,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
protected void initPlayback(final PlayQueue queue) {
|
protected void initPlayback(final PlayQueue queue) {
|
||||||
playQueue = queue;
|
playQueue = queue;
|
||||||
playQueue.init();
|
playQueue.init();
|
||||||
playbackManager = new MediaSourceManagerAlt(this, playQueue);
|
playbackManager = new MediaSourceManager(this, playQueue);
|
||||||
|
|
||||||
if (playQueueAdapter != null) playQueueAdapter.dispose();
|
if (playQueueAdapter != null) playQueueAdapter.dispose();
|
||||||
playQueueAdapter = new PlayQueueAdapter(context, playQueue);
|
playQueueAdapter = new PlayQueueAdapter(context, playQueue);
|
||||||
|
@ -316,6 +319,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
recordManager = null;
|
recordManager = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MediaSource buildMediaSource(String url) {
|
||||||
|
return buildMediaSource(url, "");
|
||||||
|
}
|
||||||
|
|
||||||
public MediaSource buildMediaSource(String url, String overrideExtension) {
|
public MediaSource buildMediaSource(String url, String overrideExtension) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]");
|
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;
|
if (intent == null || intent.getAction() == null) return;
|
||||||
switch (intent.getAction()) {
|
switch (intent.getAction()) {
|
||||||
case AudioManager.ACTION_AUDIO_BECOMING_NOISY:
|
case AudioManager.ACTION_AUDIO_BECOMING_NOISY:
|
||||||
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
|
if (isPlaying()) onVideoPlayPause();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -721,6 +728,18 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
|
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
|
@Override
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
if (DEBUG) Log.d(TAG, "Shutting down...");
|
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.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
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.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
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.Subtitles;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
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.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
|
@ -305,8 +305,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
captionItem.setOnMenuItemClickListener(menuItem -> {
|
captionItem.setOnMenuItemClickListener(menuItem -> {
|
||||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||||
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
|
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||||
trackSelector.setParameters(trackSelector.getParameters().buildUpon()
|
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||||
.setPreferredTextLanguage(captionLanguage).build());
|
|
||||||
trackSelector.setRendererDisabled(textRendererIndex, false);
|
trackSelector.setRendererDisabled(textRendererIndex, false);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -328,21 +327,32 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
qualityTextView.setVisibility(View.GONE);
|
qualityTextView.setVisibility(View.GONE);
|
||||||
playbackSpeedTextView.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();
|
||||||
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();
|
switch (streamType) {
|
||||||
qualityTextView.setVisibility(View.VISIBLE);
|
case VIDEO_STREAM:
|
||||||
surfaceView.setVisibility(View.VISIBLE);
|
if (info.video_streams.size() + info.video_only_streams.size() == 0) break;
|
||||||
} else {
|
|
||||||
surfaceView.setVisibility(View.GONE);
|
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();
|
buildPlaybackSpeedMenu();
|
||||||
|
@ -352,6 +362,9 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
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<>();
|
List<MediaSource> mediaSources = new ArrayList<>();
|
||||||
|
|
||||||
// Create video stream source
|
// Create video stream source
|
||||||
|
@ -529,26 +542,15 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize mismatching language strings
|
// Normalize mismatching language strings
|
||||||
final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
|
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build UI
|
// Build UI
|
||||||
buildCaptionMenu(availableLanguages);
|
buildCaptionMenu(availableLanguages);
|
||||||
if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null ||
|
if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null ||
|
||||||
!availableLanguages.contains(formattedPreferredLanguage)) {
|
!availableLanguages.contains(preferredLanguage)) {
|
||||||
captionTextView.setText(R.string.caption_none);
|
captionTextView.setText(R.string.caption_none);
|
||||||
} else {
|
} else {
|
||||||
captionTextView.setText(formattedPreferredLanguage);
|
captionTextView.setText(preferredLanguage);
|
||||||
}
|
}
|
||||||
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
|
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,14 @@ import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
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.FileDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
||||||
|
@ -33,18 +36,21 @@ public class CacheFactory implements DataSource.Factory {
|
||||||
// todo: make this a singleton?
|
// todo: make this a singleton?
|
||||||
private static SimpleCache cache;
|
private static SimpleCache cache;
|
||||||
|
|
||||||
public CacheFactory(@NonNull final Context context) {
|
public CacheFactory(@NonNull final Context context,
|
||||||
this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(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) {
|
private CacheFactory(@NonNull final Context context,
|
||||||
super();
|
@NonNull final String userAgent,
|
||||||
|
@NonNull final TransferListener<? super DataSource> transferListener,
|
||||||
|
final long maxCacheSize,
|
||||||
|
final long maxFileSize) {
|
||||||
this.maxFileSize = maxFileSize;
|
this.maxFileSize = maxFileSize;
|
||||||
|
|
||||||
final String userAgent = Downloader.USER_AGENT;
|
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
|
||||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
|
||||||
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter);
|
|
||||||
|
|
||||||
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||||
if (!cacheDir.exists()) {
|
if (!cacheDir.exists()) {
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//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;
|
package org.schabi.newpipe.player.playback;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
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.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.playlist.events.MoveEvent;
|
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 org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.reactivex.Single;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.annotations.NonNull;
|
import io.reactivex.annotations.NonNull;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.disposables.SerialDisposable;
|
import io.reactivex.disposables.SerialDisposable;
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
import io.reactivex.subjects.PublishSubject;
|
||||||
|
|
||||||
public class MediaSourceManager {
|
public class MediaSourceManager {
|
||||||
private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
|
|
||||||
// One-side rolling window size for default loading
|
// One-side rolling window size for default loading
|
||||||
// Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0
|
// Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0
|
||||||
private final int windowSize;
|
private final int windowSize;
|
||||||
|
@ -40,17 +48,17 @@ public class MediaSourceManager {
|
||||||
private final PublishSubject<Long> debouncedLoadSignal;
|
private final PublishSubject<Long> debouncedLoadSignal;
|
||||||
private final Disposable debouncedLoader;
|
private final Disposable debouncedLoader;
|
||||||
|
|
||||||
private final DeferredMediaSource.Callback sourceBuilder;
|
|
||||||
|
|
||||||
private DynamicConcatenatingMediaSource sources;
|
private DynamicConcatenatingMediaSource sources;
|
||||||
|
|
||||||
private Subscription playQueueReactor;
|
private Subscription playQueueReactor;
|
||||||
private SerialDisposable syncReactor;
|
private CompositeDisposable loaderReactor;
|
||||||
|
|
||||||
private PlayQueueItem syncedItem;
|
|
||||||
|
|
||||||
private boolean isBlocked;
|
private boolean isBlocked;
|
||||||
|
|
||||||
|
private SerialDisposable syncReactor;
|
||||||
|
private PlayQueueItem syncedItem;
|
||||||
|
private Set<PlayQueueItem> loadingItems;
|
||||||
|
|
||||||
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||||
@NonNull final PlayQueue playQueue) {
|
@NonNull final PlayQueue playQueue) {
|
||||||
this(listener, playQueue, 1, 400L);
|
this(listener, playQueue, 1, 400L);
|
||||||
|
@ -61,7 +69,8 @@ public class MediaSourceManager {
|
||||||
final int windowSize,
|
final int windowSize,
|
||||||
final long loadDebounceMillis) {
|
final long loadDebounceMillis) {
|
||||||
if (windowSize <= 0) {
|
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;
|
this.playbackListener = listener;
|
||||||
|
@ -69,27 +78,20 @@ public class MediaSourceManager {
|
||||||
this.windowSize = windowSize;
|
this.windowSize = windowSize;
|
||||||
this.loadDebounceMillis = loadDebounceMillis;
|
this.loadDebounceMillis = loadDebounceMillis;
|
||||||
|
|
||||||
this.syncReactor = new SerialDisposable();
|
this.loaderReactor = new CompositeDisposable();
|
||||||
this.debouncedLoadSignal = PublishSubject.create();
|
this.debouncedLoadSignal = PublishSubject.create();
|
||||||
this.debouncedLoader = getDebouncedLoader();
|
this.debouncedLoader = getDebouncedLoader();
|
||||||
|
|
||||||
this.sourceBuilder = getSourceBuilder();
|
|
||||||
|
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
|
|
||||||
|
this.syncReactor = new SerialDisposable();
|
||||||
|
this.loadingItems = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
playQueue.getBroadcastReceiver()
|
playQueue.getBroadcastReceiver()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(getReactor());
|
.subscribe(getReactor());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// DeferredMediaSource listener
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private DeferredMediaSource.Callback getSourceBuilder() {
|
|
||||||
return playbackListener::sourceOf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Exposed Methods
|
// Exposed Methods
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -100,10 +102,12 @@ public class MediaSourceManager {
|
||||||
if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
|
if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
|
||||||
if (debouncedLoader != null) debouncedLoader.dispose();
|
if (debouncedLoader != null) debouncedLoader.dispose();
|
||||||
if (playQueueReactor != null) playQueueReactor.cancel();
|
if (playQueueReactor != null) playQueueReactor.cancel();
|
||||||
|
if (loaderReactor != null) loaderReactor.dispose();
|
||||||
if (syncReactor != null) syncReactor.dispose();
|
if (syncReactor != null) syncReactor.dispose();
|
||||||
if (sources != null) sources.releaseSource();
|
if (sources != null) sources.releaseSource();
|
||||||
|
|
||||||
playQueueReactor = null;
|
playQueueReactor = null;
|
||||||
|
loaderReactor = null;
|
||||||
syncReactor = null;
|
syncReactor = null;
|
||||||
syncedItem = null;
|
syncedItem = null;
|
||||||
sources = null;
|
sources = null;
|
||||||
|
@ -121,7 +125,8 @@ public class MediaSourceManager {
|
||||||
/**
|
/**
|
||||||
* Blocks the player and repopulate the sources.
|
* 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() {
|
public void reset() {
|
||||||
tryBlock();
|
tryBlock();
|
||||||
|
@ -210,41 +215,45 @@ public class MediaSourceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Internal Helpers
|
// Playback Locking
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private boolean isPlayQueueReady() {
|
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() {
|
private boolean isPlaybackReady() {
|
||||||
if (!isBlocked) {
|
return sources.getSize() > 0 &&
|
||||||
playbackListener.block();
|
sources.getMediaSource(playQueue.getIndex()) instanceof LoadedMediaSource;
|
||||||
resetSources();
|
|
||||||
isBlocked = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryUnblock() {
|
private void tryBlock() {
|
||||||
if (isPlayQueueReady() && isBlocked && sources != null) {
|
if (isBlocked) return;
|
||||||
|
|
||||||
|
playbackListener.block();
|
||||||
|
resetSources();
|
||||||
|
|
||||||
|
isBlocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryUnblock() {
|
||||||
|
if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) {
|
||||||
isBlocked = false;
|
isBlocked = false;
|
||||||
playbackListener.unblock(sources);
|
playbackListener.unblock(sources);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Metadata Synchronization TODO: maybe this should be a separate manager
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void sync() {
|
private void sync() {
|
||||||
final PlayQueueItem currentItem = playQueue.getItem();
|
final PlayQueueItem currentItem = playQueue.getItem();
|
||||||
if (currentItem == null) return;
|
if (isBlocked || currentItem == null) return;
|
||||||
|
|
||||||
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
|
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
|
||||||
final Consumer<Throwable> onError = throwable -> {
|
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
|
||||||
Log.e(TAG, "Sync error:", throwable);
|
|
||||||
syncInternal(currentItem, null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (syncedItem != currentItem) {
|
if (syncedItem != currentItem) {
|
||||||
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() {
|
private void loadDebounced() {
|
||||||
debouncedLoadSignal.onNext(System.currentTimeMillis());
|
debouncedLoadSignal.onNext(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
@ -279,76 +299,113 @@ public class MediaSourceManager {
|
||||||
final int leftBound = Math.max(0, currentIndex - windowSize);
|
final int leftBound = Math.max(0, currentIndex - windowSize);
|
||||||
final int rightLimit = currentIndex + windowSize + 1;
|
final int rightLimit = currentIndex + windowSize + 1;
|
||||||
final int rightBound = Math.min(playQueue.size(), rightLimit);
|
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
|
// Do a round robin
|
||||||
final int excess = rightLimit - playQueue.size();
|
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);
|
for (final PlayQueueItem item: items) loadItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadItem(@Nullable final PlayQueueItem item) {
|
private void loadItem(@Nullable final PlayQueueItem item) {
|
||||||
if (item == null) return;
|
if (sources == null || item == null) return;
|
||||||
|
|
||||||
final int index = playQueue.indexOf(item);
|
final int index = playQueue.indexOf(item);
|
||||||
if (index > sources.getSize() - 1) return;
|
if (index > sources.getSize() - 1) return;
|
||||||
|
|
||||||
final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item));
|
final Consumer<ManagedMediaSource> onDone = mediaSource -> {
|
||||||
if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load();
|
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();
|
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() {
|
private void resetSources() {
|
||||||
if (this.sources != null) this.sources.releaseSource();
|
if (this.sources != null) this.sources.releaseSource();
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateSources() {
|
private void populateSources() {
|
||||||
if (sources == null) return;
|
if (sources == null || sources.getSize() >= playQueue.size()) return;
|
||||||
|
|
||||||
for (final PlayQueueItem item : playQueue.getStreams()) {
|
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
|
||||||
insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder));
|
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
|
* Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource}
|
||||||
* in respect to the play queue.
|
* with position * in respect to the play queue only if no {@link MediaSource}
|
||||||
*
|
* already exists at the given index.
|
||||||
* If the play queue index already exists, then the insert is ignored.
|
|
||||||
* */
|
* */
|
||||||
private void insert(final int queueIndex, final DeferredMediaSource source) {
|
private void emplace(final int index, final MediaSource source) {
|
||||||
if (sources == null) return;
|
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.
|
* Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource}
|
||||||
*
|
* at the given index. If this index is out of bound, then the removal is ignored.
|
||||||
* If the play queue index does not exist, the removal is ignored.
|
|
||||||
* */
|
* */
|
||||||
private void remove(final int queueIndex) {
|
private void remove(final int index) {
|
||||||
if (sources == null) return;
|
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) {
|
private void move(final int source, final int target) {
|
||||||
if (sources == null) return;
|
if (sources == null) return;
|
||||||
if (source < 0 || target < 0) return;
|
if (source < 0 || target < 0) return;
|
||||||
|
@ -356,4 +413,18 @@ public class MediaSourceManager {
|
||||||
|
|
||||||
sources.moveMediaSource(source, target);
|
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();
|
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
|
||||||
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||||
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
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) {
|
} else if (exception instanceof ContentNotAvailableException) {
|
||||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue