-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.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
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;
|
||||
|
||||
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
|
||||
|
|
|
@ -43,20 +43,11 @@ import com.google.android.exoplayer2.Player;
|
|||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
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.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.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
@ -66,8 +57,8 @@ import org.schabi.newpipe.R;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.helper.AudioReactor;
|
||||
import org.schabi.newpipe.player.helper.CacheFactory;
|
||||
import org.schabi.newpipe.player.helper.LoadController;
|
||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||
import org.schabi.newpipe.player.playback.CustomTrackSelector;
|
||||
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||
|
@ -149,14 +140,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
protected boolean isPrepared = false;
|
||||
|
||||
protected CustomTrackSelector trackSelector;
|
||||
protected DataSource.Factory cacheDataSourceFactory;
|
||||
protected DataSource.Factory cachelessDataSourceFactory;
|
||||
|
||||
protected SsMediaSource.Factory ssMediaSourceFactory;
|
||||
protected HlsMediaSource.Factory hlsMediaSourceFactory;
|
||||
protected DashMediaSource.Factory dashMediaSourceFactory;
|
||||
protected ExtractorMediaSource.Factory extractorMediaSourceFactory;
|
||||
protected SingleSampleMediaSource.Factory sampleMediaSourceFactory;
|
||||
protected PlayerDataSource dataSource;
|
||||
|
||||
protected Disposable progressUpdateReactor;
|
||||
protected CompositeDisposable databaseUpdateReactor;
|
||||
|
@ -193,20 +178,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
|
||||
final String userAgent = Downloader.USER_AGENT;
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
final AdaptiveTrackSelection.Factory trackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
|
||||
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 RenderersFactory renderFactory = new DefaultRenderersFactory(context);
|
||||
|
@ -319,26 +295,56 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
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) {
|
||||
Log.d(TAG, "buildMediaSource() called with: url = [" + url +
|
||||
"], overrideExtension = [" + overrideExtension + "]");
|
||||
Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl +
|
||||
"], content type = [" + type + "]");
|
||||
}
|
||||
Uri uri = Uri.parse(url);
|
||||
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) :
|
||||
Util.inferContentType("." + overrideExtension);
|
||||
if (dataSource == null) return null;
|
||||
|
||||
final Uri uri = Uri.parse(sourceUrl);
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return ssMediaSourceFactory.createMediaSource(uri);
|
||||
return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri);
|
||||
case C.TYPE_DASH:
|
||||
return dashMediaSourceFactory.createMediaSource(uri);
|
||||
return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return hlsMediaSourceFactory.createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return extractorMediaSourceFactory.createMediaSource(uri);
|
||||
default: {
|
||||
return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -478,7 +484,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
// ExoPlayer Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void recover() {
|
||||
private void maybeRecover() {
|
||||
final int currentSourceIndex = playQueue.getIndex();
|
||||
final PlayQueueItem currentSourceItem = playQueue.getItem();
|
||||
|
||||
|
@ -554,7 +560,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
}
|
||||
break;
|
||||
case Player.STATE_READY: //3
|
||||
recover();
|
||||
maybeRecover();
|
||||
if (!isPrepared) {
|
||||
isPrepared = true;
|
||||
onPrepared(playWhenReady);
|
||||
|
@ -566,7 +572,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
case Player.STATE_ENDED: // 4
|
||||
// Ensure the current window has actually ended
|
||||
// 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);
|
||||
isPrepared = false;
|
||||
}
|
||||
|
@ -730,9 +737,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
@Override
|
||||
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
|
||||
if (!info.getHlsUrl().isEmpty()) {
|
||||
return buildMediaSource(info.getHlsUrl(), "m3u8");
|
||||
return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
|
||||
} else if (!info.getDashMpdUrl().isEmpty()) {
|
||||
return buildMediaSource(info.getDashMpdUrl(), "mpd");
|
||||
return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -852,8 +859,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
|
||||
public void seekBy(int 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;
|
||||
}
|
||||
|
||||
int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds);
|
||||
if (progress < 0) progress = 0;
|
||||
simpleExoPlayer.seekTo(progress);
|
||||
|
@ -864,6 +874,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
|||
&& simpleExoPlayer.getCurrentPosition() >= 0;
|
||||
}
|
||||
|
||||
public void seekToDefault() {
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
|
@ -76,6 +76,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
private SeekBar progressSeekBar;
|
||||
private TextView progressCurrentTime;
|
||||
private TextView progressEndTime;
|
||||
private TextView progressLiveSync;
|
||||
private TextView seekDisplay;
|
||||
|
||||
private ImageButton repeatButton;
|
||||
|
@ -294,9 +295,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
progressCurrentTime = rootView.findViewById(R.id.current_time);
|
||||
progressSeekBar = rootView.findViewById(R.id.seek_bar);
|
||||
progressEndTime = rootView.findViewById(R.id.end_time);
|
||||
progressLiveSync = rootView.findViewById(R.id.live_sync);
|
||||
seekDisplay = rootView.findViewById(R.id.seek_display);
|
||||
|
||||
progressSeekBar.setOnSeekBarChangeListener(this);
|
||||
progressLiveSync.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void buildControls() {
|
||||
|
@ -513,6 +516,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
} else if (view.getId() == metadata.getId()) {
|
||||
scrollToSelected();
|
||||
|
||||
} else if (view.getId() == progressLiveSync.getId()) {
|
||||
player.seekToDefault();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -574,6 +580,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
if (info != null) {
|
||||
metadataTitle.setText(info.getName());
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,6 @@ import android.widget.TextView;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
|
@ -58,6 +57,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
|
|||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
|
@ -87,7 +87,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
|||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class VideoPlayer extends BasePlayer
|
||||
implements SimpleExoPlayer.VideoListener,
|
||||
implements VideoListener,
|
||||
SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener,
|
||||
Player.EventListener,
|
||||
|
@ -131,6 +131,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
private SeekBar playbackSeekBar;
|
||||
private TextView playbackCurrentTime;
|
||||
private TextView playbackEndTime;
|
||||
private TextView playbackLiveSync;
|
||||
private TextView playbackSpeedTextView;
|
||||
|
||||
private View topControlsRoot;
|
||||
|
@ -180,6 +181,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
|
||||
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
|
||||
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
|
||||
this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync);
|
||||
this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
|
||||
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
|
||||
this.topControlsRoot = rootView.findViewById(R.id.topControls);
|
||||
|
@ -221,6 +223,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
qualityTextView.setOnClickListener(this);
|
||||
captionTextView.setOnClickListener(this);
|
||||
resizeView.setOnClickListener(this);
|
||||
playbackLiveSync.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -261,7 +264,8 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
|
||||
for (int i = 0; i < availableStreams.size(); 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) {
|
||||
qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
|
@ -327,9 +331,22 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
qualityTextView.setVisibility(View.GONE);
|
||||
playbackSpeedTextView.setVisibility(View.GONE);
|
||||
|
||||
playbackEndTime.setVisibility(View.GONE);
|
||||
playbackLiveSync.setVisibility(View.GONE);
|
||||
|
||||
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
|
||||
|
||||
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:
|
||||
if (info.video_streams.size() + info.video_only_streams.size() == 0) break;
|
||||
|
||||
|
@ -344,14 +361,10 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
|
||||
buildQualityMenu();
|
||||
qualityTextView.setVisibility(View.VISIBLE);
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case AUDIO_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
break;
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
default:
|
||||
playbackEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -381,6 +394,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null;
|
||||
if (video != null) {
|
||||
final MediaSource streamSource = buildMediaSource(video.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, video),
|
||||
MediaFormat.getSuffixById(video.getFormatId()));
|
||||
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
|
||||
if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
|
||||
final MediaSource audioSource = buildMediaSource(audio.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, audio),
|
||||
MediaFormat.getSuffixById(audio.getFormatId()));
|
||||
mediaSources.add(audioSource);
|
||||
}
|
||||
|
@ -408,8 +423,8 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = sampleMediaSourceFactory.createMediaSource(
|
||||
Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
|
||||
|
@ -635,6 +650,8 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
onResizeClicked();
|
||||
} else if (v.getId() == captionTextView.getId()) {
|
||||
onCaptionClicked();
|
||||
} else if (v.getId() == playbackLiveSync.getId()) {
|
||||
seekToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import org.schabi.newpipe.Downloader;
|
|||
|
||||
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 CACHE_FOLDER_NAME = "exoplayer";
|
||||
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.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.VideoStream;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
@ -69,8 +72,7 @@ public class PlayerHelper {
|
|||
public static String captionLanguageOf(@NonNull final Context context,
|
||||
@NonNull final Subtitles subtitles) {
|
||||
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
|
||||
return displayName + (subtitles.isAutoGenerated() ?
|
||||
" (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
}
|
||||
|
||||
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) {
|
||||
return isResumeAfterAudioFocusGain(context, false);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
|
@ -11,6 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
|
|||
import java.io.IOException;
|
||||
|
||||
public class FailedMediaSource implements ManagedMediaSource {
|
||||
private final String TAG = "ManagedMediaSource@" + Integer.toHexString(hashCode());
|
||||
|
||||
private final PlayQueueItem playQueueItem;
|
||||
private final Throwable error;
|
||||
|
@ -36,7 +38,7 @@ public class FailedMediaSource implements ManagedMediaSource {
|
|||
this.retryTimestamp = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
public PlayQueueItem getPlayQueueItem() {
|
||||
public PlayQueueItem getStream() {
|
||||
return playQueueItem;
|
||||
}
|
||||
|
||||
|
@ -44,12 +46,14 @@ public class FailedMediaSource implements ManagedMediaSource {
|
|||
return error;
|
||||
}
|
||||
|
||||
public boolean canRetry() {
|
||||
private boolean canRetry() {
|
||||
return System.currentTimeMillis() >= retryTimestamp;
|
||||
}
|
||||
|
||||
@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
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
|
@ -68,7 +72,7 @@ public class FailedMediaSource implements ManagedMediaSource {
|
|||
public void releaseSource() {}
|
||||
|
||||
@Override
|
||||
public boolean canReplace() {
|
||||
return canRetry();
|
||||
public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
|
||||
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.upstream.Allocator;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -15,15 +14,25 @@ import java.io.IOException;
|
|||
public class LoadedMediaSource implements ManagedMediaSource {
|
||||
|
||||
private final MediaSource source;
|
||||
|
||||
private final PlayQueueItem stream;
|
||||
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.stream = stream;
|
||||
this.expireTimestamp = expireTimestamp;
|
||||
}
|
||||
|
||||
public PlayQueueItem getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
private boolean isExpired() {
|
||||
return System.currentTimeMillis() >= expireTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
source.prepareSource(player, isTopLevelSource, listener);
|
||||
|
@ -50,7 +59,7 @@ public class LoadedMediaSource implements ManagedMediaSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean canReplace() {
|
||||
return System.currentTimeMillis() >= expireTimestamp;
|
||||
public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
|
||||
return newIdentity != stream || isExpired();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
|
||||
public interface ManagedMediaSource extends MediaSource {
|
||||
boolean canReplace();
|
||||
boolean canReplace(@NonNull final PlayQueueItem newIdentity);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PlaceholderMediaSource implements ManagedMediaSource {
|
||||
|
@ -16,7 +19,7 @@ public class PlaceholderMediaSource implements ManagedMediaSource {
|
|||
@Override public void releaseSource() {}
|
||||
|
||||
@Override
|
||||
public boolean canReplace() {
|
||||
public boolean canReplace(@NonNull final PlayQueueItem newIdentity) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
@ -34,7 +35,11 @@ import io.reactivex.disposables.SerialDisposable;
|
|||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
|
||||
|
||||
public class MediaSourceManager {
|
||||
private final String TAG = "MediaSourceManager";
|
||||
|
||||
// One-side rolling window size for default loading
|
||||
// Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0
|
||||
private final int windowSize;
|
||||
|
@ -233,10 +238,16 @@ public class MediaSourceManager {
|
|||
return playQueue.isComplete() || isWindowLoaded;
|
||||
}
|
||||
|
||||
// Checks if the current playback media source is a placeholder, if so, then it is not ready.
|
||||
private boolean isPlaybackReady() {
|
||||
return sources != null && playQueue != null && sources.getSize() > playQueue.getIndex() &&
|
||||
!(sources.getMediaSource(playQueue.getIndex()) instanceof PlaceholderMediaSource);
|
||||
if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) {
|
||||
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() {
|
||||
|
@ -280,7 +291,7 @@ public class MediaSourceManager {
|
|||
if (playQueue == null || playbackListener == null) return;
|
||||
// Ensure the current item is up to date with the play queue
|
||||
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
|
||||
playbackListener.sync(syncedItem,info);
|
||||
playbackListener.sync(syncedItem, info);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,14 +340,19 @@ public class MediaSourceManager {
|
|||
if (index > sources.getSize() - 1) return;
|
||||
|
||||
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);
|
||||
tryUnblock();
|
||||
sync();
|
||||
};
|
||||
|
||||
if (!loadingItems.contains(item) &&
|
||||
((ManagedMediaSource) sources.getMediaSource(index)).canReplace()) {
|
||||
if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
|
||||
if (DEBUG) Log.d(TAG, "Loading: [" + item.getTitle() +
|
||||
"] with url: " + item.getUrl());
|
||||
|
||||
loadingItems.add(item);
|
||||
final Disposable loader = getLoadedMediaSource(item)
|
||||
|
@ -358,16 +374,32 @@ public class MediaSourceManager {
|
|||
|
||||
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
|
||||
if (source == null) {
|
||||
return new FailedMediaSource(stream, new IllegalStateException(
|
||||
"MediaSource cannot be resolved"));
|
||||
final Exception exception = new IllegalStateException(
|
||||
"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() +
|
||||
TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit);
|
||||
return new LoadedMediaSource(source, expiration);
|
||||
return new LoadedMediaSource(source, stream, expiration);
|
||||
}).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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
|
@ -296,5 +296,15 @@
|
|||
android:textColor="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText"
|
||||
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>
|
||||
</RelativeLayout>
|
|
@ -397,6 +397,17 @@
|
|||
android:textColor="@android:color/white"
|
||||
tools:ignore="HardcodedText"
|
||||
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>
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
|
@ -146,6 +146,16 @@
|
|||
android:textColor="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText"
|
||||
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>
|
||||
|
||||
<RelativeLayout
|
||||
|
|
|
@ -190,6 +190,17 @@
|
|||
android:textColor="@android:color/white"
|
||||
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry"
|
||||
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>
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
|
@ -413,6 +413,8 @@
|
|||
<string name="normal_caption_font_size">Normal Font</string>
|
||||
<string name="larger_caption_font_size">Larger Font</string>
|
||||
|
||||
<string name="live_sync">SYNC</string>
|
||||
|
||||
<!-- Debug Settings -->
|
||||
<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>
|
||||
|
|
Loading…
Reference in New Issue