-Improved player queue stability by using more aggressive synchronization policy.

-Added sync buttons on live streams to allow seeking to live edge.
-Added custom cache key for extractor sources to allow more persistent reuse.
-Refactored player data source factories into own class and separating live and non-live data sources.
This commit is contained in:
John Zhen Mo 2018-02-26 19:57:59 -08:00
parent 1444fe5468
commit b3b2748bb7
17 changed files with 320 additions and 86 deletions

View file

@ -46,6 +46,7 @@ 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.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -398,7 +399,8 @@ public final class BackgroundPlayer extends Service {
if (index < 0 || index >= info.audio_streams.size()) return null; if (index < 0 || index >= info.audio_streams.size()) return null;
final AudioStream audio = info.audio_streams.get(index); final AudioStream audio = info.audio_streams.get(index);
return buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.getFormatId())); return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()));
} }
@Override @Override

View file

@ -43,20 +43,11 @@ 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.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.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
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.AdaptiveTrackSelection;
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.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;
@ -66,8 +57,8 @@ 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.LoadController; import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
@ -149,14 +140,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
protected boolean isPrepared = false; protected boolean isPrepared = false;
protected CustomTrackSelector trackSelector; protected CustomTrackSelector trackSelector;
protected DataSource.Factory cacheDataSourceFactory;
protected DataSource.Factory cachelessDataSourceFactory;
protected SsMediaSource.Factory ssMediaSourceFactory; protected PlayerDataSource dataSource;
protected HlsMediaSource.Factory hlsMediaSourceFactory;
protected DashMediaSource.Factory dashMediaSourceFactory;
protected ExtractorMediaSource.Factory extractorMediaSourceFactory;
protected SingleSampleMediaSource.Factory sampleMediaSourceFactory;
protected Disposable progressUpdateReactor; protected Disposable progressUpdateReactor;
protected CompositeDisposable databaseUpdateReactor; protected CompositeDisposable databaseUpdateReactor;
@ -193,20 +178,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
final String userAgent = Downloader.USER_AGENT; final String userAgent = Downloader.USER_AGENT;
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
final AdaptiveTrackSelection.Factory trackSelectionFactory = final AdaptiveTrackSelection.Factory trackSelectionFactory =
new AdaptiveTrackSelection.Factory(bandwidthMeter); new AdaptiveTrackSelection.Factory(bandwidthMeter);
trackSelector = new CustomTrackSelector(trackSelectionFactory); trackSelector = new CustomTrackSelector(trackSelectionFactory);
cacheDataSourceFactory = new CacheFactory(context, userAgent, bandwidthMeter);
cachelessDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
ssMediaSourceFactory = new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory);
hlsMediaSourceFactory = new HlsMediaSource.Factory(cachelessDataSourceFactory);
dashMediaSourceFactory = new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(cachelessDataSourceFactory), cachelessDataSourceFactory);
extractorMediaSourceFactory = new ExtractorMediaSource.Factory(cacheDataSourceFactory);
sampleMediaSourceFactory = new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
final LoadControl loadControl = new LoadController(context); final LoadControl loadControl = new LoadController(context);
final RenderersFactory renderFactory = new DefaultRenderersFactory(context); final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
@ -319,26 +295,56 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
recordManager = null; recordManager = null;
} }
public MediaSource buildMediaSource(String url, String overrideExtension) { /*//////////////////////////////////////////////////////////////////////////
// MediaSource Building
//////////////////////////////////////////////////////////////////////////*/
public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl,
@C.ContentType final int type) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "buildMediaSource() called with: url = [" + url + Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl +
"], overrideExtension = [" + overrideExtension + "]"); "], content type = [" + type + "]");
} }
Uri uri = Uri.parse(url); if (dataSource == null) return null;
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) :
Util.inferContentType("." + overrideExtension); final Uri uri = Uri.parse(sourceUrl);
switch (type) { switch (type) {
case C.TYPE_SS: case C.TYPE_SS:
return ssMediaSourceFactory.createMediaSource(uri); return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri);
case C.TYPE_DASH: case C.TYPE_DASH:
return dashMediaSourceFactory.createMediaSource(uri); return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri);
case C.TYPE_HLS: case C.TYPE_HLS:
return hlsMediaSourceFactory.createMediaSource(uri); return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri);
case C.TYPE_OTHER: default:
return extractorMediaSourceFactory.createMediaSource(uri); throw new IllegalStateException("Unsupported type: " + type);
default: { }
}
public MediaSource buildMediaSource(@NonNull final String sourceUrl,
@NonNull final String cacheKey,
@NonNull final String overrideExtension) {
if (DEBUG) {
Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl +
"], cacheKey = [" + cacheKey + "]" +
"], overrideExtension = [" + overrideExtension + "]");
}
if (dataSource == null) return null;
final Uri uri = Uri.parse(sourceUrl);
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ?
Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
switch (type) {
case C.TYPE_SS:
return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri);
case C.TYPE_DASH:
return dataSource.getDashMediaSourceFactory().createMediaSource(uri);
case C.TYPE_HLS:
return dataSource.getHlsMediaSourceFactory().createMediaSource(uri);
case C.TYPE_OTHER:
return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
}
} }
} }
@ -478,7 +484,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
// ExoPlayer Listener // ExoPlayer Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void recover() { private void maybeRecover() {
final int currentSourceIndex = playQueue.getIndex(); final int currentSourceIndex = playQueue.getIndex();
final PlayQueueItem currentSourceItem = playQueue.getItem(); final PlayQueueItem currentSourceItem = playQueue.getItem();
@ -554,7 +560,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
} }
break; break;
case Player.STATE_READY: //3 case Player.STATE_READY: //3
recover(); maybeRecover();
if (!isPrepared) { if (!isPrepared) {
isPrepared = true; isPrepared = true;
onPrepared(playWhenReady); onPrepared(playWhenReady);
@ -566,7 +572,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
case Player.STATE_ENDED: // 4 case Player.STATE_ENDED: // 4
// Ensure the current window has actually ended // Ensure the current window has actually ended
// since single windows that are still loading may produce an ended state // since single windows that are still loading may produce an ended state
if (isCurrentWindowValid() && simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { if (isCurrentWindowValid() &&
simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) {
changeState(STATE_COMPLETED); changeState(STATE_COMPLETED);
isPrepared = false; isPrepared = false;
} }
@ -730,9 +737,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
@Override @Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
if (!info.getHlsUrl().isEmpty()) { if (!info.getHlsUrl().isEmpty()) {
return buildMediaSource(info.getHlsUrl(), "m3u8"); return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
} else if (!info.getDashMpdUrl().isEmpty()) { } else if (!info.getDashMpdUrl().isEmpty()) {
return buildMediaSource(info.getDashMpdUrl(), "mpd"); return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH);
} }
return null; return null;
@ -852,8 +859,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
public void seekBy(int milliSeconds) { public void seekBy(int milliSeconds) {
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) ||
((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) {
return; return;
}
int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds);
if (progress < 0) progress = 0; if (progress < 0) progress = 0;
simpleExoPlayer.seekTo(progress); simpleExoPlayer.seekTo(progress);
@ -864,6 +874,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
&& simpleExoPlayer.getCurrentPosition() >= 0; && simpleExoPlayer.getCurrentPosition() >= 0;
} }
public void seekToDefault() {
if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View file

@ -76,6 +76,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private SeekBar progressSeekBar; private SeekBar progressSeekBar;
private TextView progressCurrentTime; private TextView progressCurrentTime;
private TextView progressEndTime; private TextView progressEndTime;
private TextView progressLiveSync;
private TextView seekDisplay; private TextView seekDisplay;
private ImageButton repeatButton; private ImageButton repeatButton;
@ -294,9 +295,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
progressCurrentTime = rootView.findViewById(R.id.current_time); progressCurrentTime = rootView.findViewById(R.id.current_time);
progressSeekBar = rootView.findViewById(R.id.seek_bar); progressSeekBar = rootView.findViewById(R.id.seek_bar);
progressEndTime = rootView.findViewById(R.id.end_time); progressEndTime = rootView.findViewById(R.id.end_time);
progressLiveSync = rootView.findViewById(R.id.live_sync);
seekDisplay = rootView.findViewById(R.id.seek_display); seekDisplay = rootView.findViewById(R.id.seek_display);
progressSeekBar.setOnSeekBarChangeListener(this); progressSeekBar.setOnSeekBarChangeListener(this);
progressLiveSync.setOnClickListener(this);
} }
private void buildControls() { private void buildControls() {
@ -513,6 +516,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
} else if (view.getId() == metadata.getId()) { } else if (view.getId() == metadata.getId()) {
scrollToSelected(); scrollToSelected();
} else if (view.getId() == progressLiveSync.getId()) {
player.seekToDefault();
} }
} }
@ -574,6 +580,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (info != null) { if (info != null) {
metadataTitle.setText(info.getName()); metadataTitle.setText(info.getName());
metadataArtist.setText(info.uploader_name); metadataArtist.setText(info.uploader_name);
progressEndTime.setVisibility(View.GONE);
progressLiveSync.setVisibility(View.GONE);
switch (info.getStreamType()) {
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
progressLiveSync.setVisibility(View.VISIBLE);
break;
default:
progressEndTime.setVisibility(View.VISIBLE);
break;
}
scrollToSelected(); scrollToSelected();
} }
} }

View file

@ -50,7 +50,6 @@ import android.widget.TextView;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player; 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.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
@ -58,6 +57,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.exoplayer2.video.VideoListener;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
@ -87,7 +87,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
*/ */
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
public abstract class VideoPlayer extends BasePlayer public abstract class VideoPlayer extends BasePlayer
implements SimpleExoPlayer.VideoListener, implements VideoListener,
SeekBar.OnSeekBarChangeListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, View.OnClickListener,
Player.EventListener, Player.EventListener,
@ -131,6 +131,7 @@ public abstract class VideoPlayer extends BasePlayer
private SeekBar playbackSeekBar; private SeekBar playbackSeekBar;
private TextView playbackCurrentTime; private TextView playbackCurrentTime;
private TextView playbackEndTime; private TextView playbackEndTime;
private TextView playbackLiveSync;
private TextView playbackSpeedTextView; private TextView playbackSpeedTextView;
private View topControlsRoot; private View topControlsRoot;
@ -180,6 +181,7 @@ public abstract class VideoPlayer extends BasePlayer
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync);
this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed); this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
this.topControlsRoot = rootView.findViewById(R.id.topControls); this.topControlsRoot = rootView.findViewById(R.id.topControls);
@ -221,6 +223,7 @@ public abstract class VideoPlayer extends BasePlayer
qualityTextView.setOnClickListener(this); qualityTextView.setOnClickListener(this);
captionTextView.setOnClickListener(this); captionTextView.setOnClickListener(this);
resizeView.setOnClickListener(this); resizeView.setOnClickListener(this);
playbackLiveSync.setOnClickListener(this);
} }
@Override @Override
@ -261,7 +264,8 @@ public abstract class VideoPlayer extends BasePlayer
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
for (int i = 0; i < availableStreams.size(); i++) { for (int i = 0; i < availableStreams.size(); i++) {
VideoStream videoStream = availableStreams.get(i); VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE,
MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
} }
if (getSelectedVideoStream() != null) { if (getSelectedVideoStream() != null) {
qualityTextView.setText(getSelectedVideoStream().resolution); qualityTextView.setText(getSelectedVideoStream().resolution);
@ -327,9 +331,22 @@ public abstract class VideoPlayer extends BasePlayer
qualityTextView.setVisibility(View.GONE); qualityTextView.setVisibility(View.GONE);
playbackSpeedTextView.setVisibility(View.GONE); playbackSpeedTextView.setVisibility(View.GONE);
playbackEndTime.setVisibility(View.GONE);
playbackLiveSync.setVisibility(View.GONE);
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
switch (streamType) { switch (streamType) {
case AUDIO_STREAM:
surfaceView.setVisibility(View.GONE);
break;
case AUDIO_LIVE_STREAM:
surfaceView.setVisibility(View.GONE);
case LIVE_STREAM:
playbackLiveSync.setVisibility(View.VISIBLE);
break;
case VIDEO_STREAM: case VIDEO_STREAM:
if (info.video_streams.size() + info.video_only_streams.size() == 0) break; if (info.video_streams.size() + info.video_only_streams.size() == 0) break;
@ -344,14 +361,10 @@ public abstract class VideoPlayer extends BasePlayer
buildQualityMenu(); buildQualityMenu();
qualityTextView.setVisibility(View.VISIBLE); qualityTextView.setVisibility(View.VISIBLE);
surfaceView.setVisibility(View.VISIBLE);
break;
case AUDIO_STREAM: surfaceView.setVisibility(View.VISIBLE);
case AUDIO_LIVE_STREAM:
surfaceView.setVisibility(View.GONE);
break;
default: default:
playbackEndTime.setVisibility(View.VISIBLE);
break; break;
} }
@ -381,6 +394,7 @@ public abstract class VideoPlayer extends BasePlayer
final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null;
if (video != null) { if (video != null) {
final MediaSource streamSource = buildMediaSource(video.getUrl(), final MediaSource streamSource = buildMediaSource(video.getUrl(),
PlayerHelper.cacheKeyOf(info, video),
MediaFormat.getSuffixById(video.getFormatId())); MediaFormat.getSuffixById(video.getFormatId()));
mediaSources.add(streamSource); mediaSources.add(streamSource);
} }
@ -393,6 +407,7 @@ public abstract class VideoPlayer extends BasePlayer
// Merge with audio stream in case if video does not contain audio // Merge with audio stream in case if video does not contain audio
if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
final MediaSource audioSource = buildMediaSource(audio.getUrl(), final MediaSource audioSource = buildMediaSource(audio.getUrl(),
PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId())); MediaFormat.getSuffixById(audio.getFormatId()));
mediaSources.add(audioSource); mediaSources.add(audioSource);
} }
@ -408,8 +423,8 @@ public abstract class VideoPlayer extends BasePlayer
final Format textFormat = Format.createTextSampleFormat(null, mimeType, final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = sampleMediaSourceFactory.createMediaSource( final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource); mediaSources.add(textSource);
} }
@ -635,6 +650,8 @@ public abstract class VideoPlayer extends BasePlayer
onResizeClicked(); onResizeClicked();
} else if (v.getId() == captionTextView.getId()) { } else if (v.getId() == captionTextView.getId()) {
onCaptionClicked(); onCaptionClicked();
} else if (v.getId() == playbackLiveSync.getId()) {
seekToDefault();
} }
} }

View file

@ -21,7 +21,7 @@ import org.schabi.newpipe.Downloader;
import java.io.File; import java.io.File;
public class CacheFactory implements DataSource.Factory { /* package-private */ class CacheFactory implements DataSource.Factory {
private static final String TAG = "CacheFactory"; private static final String TAG = "CacheFactory";
private static final String CACHE_FOLDER_NAME = "exoplayer"; private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;

View file

@ -0,0 +1,76 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
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.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.TransferListener;
public class PlayerDataSource {
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
private final DataSource.Factory cacheDataSourceFactory;
private final DataSource.Factory cachelessDataSourceFactory;
public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener<? super DataSource> transferListener) {
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
}
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY);
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public SsMediaSource.Factory getSsMediaSourceFactory() {
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
cacheDataSourceFactory), cacheDataSourceFactory);
}
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cacheDataSourceFactory);
}
public DashMediaSource.Factory getDashMediaSourceFactory() {
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
cacheDataSourceFactory), cacheDataSourceFactory);
}
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() {
return new ExtractorMediaSource.Factory(cacheDataSourceFactory);
}
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) {
return new ExtractorMediaSource.Factory(cacheDataSourceFactory).setCustomCacheKey(key);
}
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
}
}

View file

@ -10,7 +10,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
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.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesFormat; import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
@ -69,8 +72,7 @@ public class PlayerHelper {
public static String captionLanguageOf(@NonNull final Context context, public static String captionLanguageOf(@NonNull final Context context,
@NonNull final Subtitles subtitles) { @NonNull final Subtitles subtitles) {
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
return displayName + (subtitles.isAutoGenerated() ? return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
" (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
} }
public static String resizeTypeOf(@NonNull final Context context, public static String resizeTypeOf(@NonNull final Context context,
@ -83,6 +85,14 @@ public class PlayerHelper {
} }
} }
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) {
return info.getUrl() + video.getResolution() + video.getFormat().getName();
}
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) {
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
}
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
return isResumeAfterAudioFocusGain(context, false); return isResumeAfterAudioFocusGain(context, false);
} }

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.player.mediasource; package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
@ -11,6 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException; import java.io.IOException;
public class FailedMediaSource implements ManagedMediaSource { public class FailedMediaSource implements ManagedMediaSource {
private final String TAG = "ManagedMediaSource@" + Integer.toHexString(hashCode());
private final PlayQueueItem playQueueItem; private final PlayQueueItem playQueueItem;
private final Throwable error; private final Throwable error;
@ -36,7 +38,7 @@ public class FailedMediaSource implements ManagedMediaSource {
this.retryTimestamp = Long.MAX_VALUE; this.retryTimestamp = Long.MAX_VALUE;
} }
public PlayQueueItem getPlayQueueItem() { public PlayQueueItem getStream() {
return playQueueItem; return playQueueItem;
} }
@ -44,12 +46,14 @@ public class FailedMediaSource implements ManagedMediaSource {
return error; return error;
} }
public boolean canRetry() { private boolean canRetry() {
return System.currentTimeMillis() >= retryTimestamp; return System.currentTimeMillis() >= retryTimestamp;
} }
@Override @Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Log.e(TAG, "Loading failed source: ", error);
}
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
@ -68,7 +72,7 @@ public class FailedMediaSource implements ManagedMediaSource {
public void releaseSource() {} public void releaseSource() {}
@Override @Override
public boolean canReplace() { public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
return canRetry(); return newIdentity != playQueueItem || canRetry();
} }
} }

View file

@ -7,7 +7,6 @@ import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException; import java.io.IOException;
@ -15,15 +14,25 @@ import java.io.IOException;
public class LoadedMediaSource implements ManagedMediaSource { public class LoadedMediaSource implements ManagedMediaSource {
private final MediaSource source; private final MediaSource source;
private final PlayQueueItem stream;
private final long expireTimestamp; private final long expireTimestamp;
public LoadedMediaSource(@NonNull final MediaSource source, final long expireTimestamp) { public LoadedMediaSource(@NonNull final MediaSource source,
@NonNull final PlayQueueItem stream,
final long expireTimestamp) {
this.source = source; this.source = source;
this.stream = stream;
this.expireTimestamp = expireTimestamp; this.expireTimestamp = expireTimestamp;
} }
public PlayQueueItem getStream() {
return stream;
}
private boolean isExpired() {
return System.currentTimeMillis() >= expireTimestamp;
}
@Override @Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
source.prepareSource(player, isTopLevelSource, listener); source.prepareSource(player, isTopLevelSource, listener);
@ -50,7 +59,7 @@ public class LoadedMediaSource implements ManagedMediaSource {
} }
@Override @Override
public boolean canReplace() { public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
return System.currentTimeMillis() >= expireTimestamp; return newIdentity != stream || isExpired();
} }
} }

View file

@ -1,7 +1,11 @@
package org.schabi.newpipe.player.mediasource; package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.playlist.PlayQueueItem;
public interface ManagedMediaSource extends MediaSource { public interface ManagedMediaSource extends MediaSource {
boolean canReplace(); boolean canReplace(@NonNull final PlayQueueItem newIdentity);
} }

View file

@ -1,10 +1,13 @@
package org.schabi.newpipe.player.mediasource; package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException; import java.io.IOException;
public class PlaceholderMediaSource implements ManagedMediaSource { public class PlaceholderMediaSource implements ManagedMediaSource {
@ -16,7 +19,7 @@ public class PlaceholderMediaSource implements ManagedMediaSource {
@Override public void releaseSource() {} @Override public void releaseSource() {}
@Override @Override
public boolean canReplace() { public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
return true; return true;
} }
} }

View file

@ -1,6 +1,7 @@
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 com.google.android.exoplayer2.source.MediaSource;
@ -34,7 +35,11 @@ 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;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager { public class MediaSourceManager {
private final String TAG = "MediaSourceManager";
// 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;
@ -233,10 +238,16 @@ public class MediaSourceManager {
return playQueue.isComplete() || isWindowLoaded; return playQueue.isComplete() || isWindowLoaded;
} }
// Checks if the current playback media source is a placeholder, if so, then it is not ready.
private boolean isPlaybackReady() { private boolean isPlaybackReady() {
return sources != null && playQueue != null && sources.getSize() > playQueue.getIndex() && if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) {
!(sources.getMediaSource(playQueue.getIndex()) instanceof PlaceholderMediaSource); return false;
}
final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex());
if (!(mediaSource instanceof LoadedMediaSource)) return false;
final PlayQueueItem playQueueItem = playQueue.getItem();
return playQueueItem == ((LoadedMediaSource) mediaSource).getStream();
} }
private void tryBlock() { private void tryBlock() {
@ -280,7 +291,7 @@ public class MediaSourceManager {
if (playQueue == null || playbackListener == null) return; if (playQueue == null || playbackListener == null) return;
// Ensure the current item is up to date with the play queue // Ensure the current item is up to date with the play queue
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
playbackListener.sync(syncedItem,info); playbackListener.sync(syncedItem, info);
} }
} }
@ -329,14 +340,19 @@ public class MediaSourceManager {
if (index > sources.getSize() - 1) return; if (index > sources.getSize() - 1) return;
final Consumer<ManagedMediaSource> onDone = mediaSource -> { final Consumer<ManagedMediaSource> onDone = mediaSource -> {
update(playQueue.indexOf(item), mediaSource); if (DEBUG) Log.d(TAG, " Loaded: [" + item.getTitle() +
"] with url: " + item.getUrl());
if (isCorrectionNeeded(item)) update(playQueue.indexOf(item), mediaSource);
loadingItems.remove(item); loadingItems.remove(item);
tryUnblock(); tryUnblock();
sync(); sync();
}; };
if (!loadingItems.contains(item) && if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) { if (DEBUG) Log.d(TAG, "Loading: [" + item.getTitle() +
"] with url: " + item.getUrl());
loadingItems.add(item); loadingItems.add(item);
final Disposable loader = getLoadedMediaSource(item) final Disposable loader = getLoadedMediaSource(item)
@ -358,16 +374,32 @@ public class MediaSourceManager {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo); final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) { if (source == null) {
return new FailedMediaSource(stream, new IllegalStateException( final Exception exception = new IllegalStateException(
"MediaSource cannot be resolved")); "Unable to resolve source from stream info." +
" URL: " + stream.getUrl() +
", audio count: " + streamInfo.audio_streams.size() +
", video count: " + streamInfo.video_only_streams.size() +
streamInfo.video_streams.size());
return new FailedMediaSource(stream, new IllegalStateException(exception));
} }
final long expiration = System.currentTimeMillis() + final long expiration = System.currentTimeMillis() +
TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit); TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit);
return new LoadedMediaSource(source, expiration); return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
} }
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
if (playQueue == null || sources == null) return false;
final int index = playQueue.indexOf(item);
if (index == -1 || index >= sources.getSize()) return false;
final MediaSource mediaSource = sources.getMediaSource(index);
return !(mediaSource instanceof ManagedMediaSource) ||
((ManagedMediaSource) mediaSource).canReplace(item);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers // MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View file

@ -296,5 +296,15 @@
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText" tools:ignore="HardcodedText"
tools:text="1:23:49"/> tools:text="1:23:49"/>
<TextView
android:id="@+id/live_sync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/live_sync"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -397,6 +397,17 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
tools:ignore="HardcodedText" tools:ignore="HardcodedText"
tools:text="1:23:49"/> tools:text="1:23:49"/>
<TextView
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="@string/live_sync"
android:textColor="@android:color/white"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -146,6 +146,16 @@
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText" tools:ignore="HardcodedText"
tools:text="1:23:49"/> tools:text="1:23:49"/>
<TextView
android:id="@+id/live_sync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/live_sync"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>
<RelativeLayout <RelativeLayout

View file

@ -190,6 +190,17 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry"
tools:text="1:23:49"/> tools:text="1:23:49"/>
<TextView
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="@string/live_sync"
android:textColor="@android:color/white"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -413,6 +413,8 @@
<string name="normal_caption_font_size">Normal Font</string> <string name="normal_caption_font_size">Normal Font</string>
<string name="larger_caption_font_size">Larger Font</string> <string name="larger_caption_font_size">Larger Font</string>
<string name="live_sync">SYNC</string>
<!-- Debug Settings --> <!-- Debug Settings -->
<string name="enable_leak_canary_title">Enable LeakCanary</string> <string name="enable_leak_canary_title">Enable LeakCanary</string>
<string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string>