-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:
parent
1444fe5468
commit
b3b2748bb7
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue