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

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

View File

@ -46,6 +46,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.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

View File

@ -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
//////////////////////////////////////////////////////////////////////////*/

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

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

View File

@ -10,7 +10,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.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);
}

View File

@ -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();
}
}

View File

@ -7,7 +7,6 @@ import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.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();
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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
//////////////////////////////////////////////////////////////////////////*/

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>