-Baked stream info resolution into custom media source, allowing for simpler playlist control.

-Added track merging on different stream qualities, allowing for implementation of smooth transition on A/V quality and captions change.
This commit is contained in:
John Zhen M 2017-09-19 21:46:16 -07:00 committed by John Zhen Mo
parent 9576d5bd89
commit 9bc95f030c
7 changed files with 151 additions and 129 deletions

View File

@ -37,6 +37,7 @@ import android.widget.RemoteViews;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
@ -49,6 +50,9 @@ import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.List;
/**
* Base players joining the common properties
@ -390,9 +394,14 @@ public final class BackgroundPlayer extends Service {
}
@Override
public MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex) {
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
public MediaSource sourceOf(final StreamInfo info) {
List<MediaSource> sources = new ArrayList<>();
for (final AudioStream audio : info.audio_streams) {
final MediaSource audioSource = buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
sources.add(audioSource);
}
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
}
@Override

View File

@ -72,8 +72,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playback.PlaybackManager;
import org.schabi.newpipe.playlist.ExternalPlayQueue;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
@ -139,7 +139,7 @@ public abstract class BasePlayer implements Player.EventListener,
// Playback
//////////////////////////////////////////////////////////////////////////*/
protected PlaybackManager playbackManager;
protected MediaSourceManager playbackManager;
protected PlayQueue playQueue;
private boolean isRecovery = false;
@ -158,7 +158,6 @@ public abstract class BasePlayer implements Player.EventListener,
protected SimpleExoPlayer simpleExoPlayer;
protected boolean isPrepared = false;
protected boolean wasPlaying = false;
protected CacheDataSourceFactory cacheDataSourceFactory;
protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
@ -297,7 +296,7 @@ public abstract class BasePlayer implements Player.EventListener,
playQueue = queue;
playQueue.init();
playbackManager = new PlaybackManager(this, playQueue);
playbackManager = new MediaSourceManager(this, playQueue);
}
public void initThumbnail(final String url) {
@ -442,14 +441,12 @@ public abstract class BasePlayer implements Player.EventListener,
if (isResumeAfterAudioFocusGain()) {
simpleExoPlayer.setPlayWhenReady(true);
wasPlaying = true;
}
}
protected void onAudioFocusLoss() {
if (DEBUG) Log.d(TAG, "onAudioFocusLoss() called");
simpleExoPlayer.setPlayWhenReady(false);
wasPlaying = false;
}
protected void onAudioFocusLossCanDuck() {
@ -586,7 +583,7 @@ public abstract class BasePlayer implements Player.EventListener,
}
// Good to go...
simpleExoPlayer.setPlayWhenReady(wasPlaying);
simpleExoPlayer.setPlayWhenReady(true);
}
/*//////////////////////////////////////////////////////////////////////////
@ -669,11 +666,10 @@ public abstract class BasePlayer implements Player.EventListener,
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " +
"window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]");
if (newQueueIndex == -1) {
playQueue.offsetIndex(+1);
} else {
playQueue.setIndex(newQueueIndex);
}
// If the user selects a new track, then the discontinuity occurs after the index is changed.
// Therefore, the only source that causes a discrepancy would be autoplay,
// which can only offset the current track by +1.
if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
}
@Override
@ -690,31 +686,20 @@ public abstract class BasePlayer implements Player.EventListener,
if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Blocking...");
simpleExoPlayer.removeListener(this);
changeState(STATE_BLOCKED);
wasPlaying = simpleExoPlayer.getPlayWhenReady();
simpleExoPlayer.setPlayWhenReady(false);
}
@Override
public void prepare(final MediaSource mediaSource) {
if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Preparing...");
simpleExoPlayer.stop();
isPrepared = false;
simpleExoPlayer.prepare(mediaSource);
changeState(STATE_BLOCKED);
}
@Override
public void unblock() {
public void unblock(final MediaSource mediaSource) {
if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Unblocking...");
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
simpleExoPlayer.addListener(this);
simpleExoPlayer.prepare(mediaSource);
}
@Override
@ -762,7 +747,6 @@ public abstract class BasePlayer implements Player.EventListener,
else playQueue.setIndex(0);
}
simpleExoPlayer.setPlayWhenReady(!isPlaying());
wasPlaying = simpleExoPlayer.getPlayWhenReady();
}
public void onFastRewind() {

View File

@ -63,7 +63,7 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.player.playback.PlaybackManager;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
@ -743,7 +743,7 @@ public final class PopupVideoPlayer extends Service {
public void run() {
playerImpl.playQueue = new SinglePlayQueue(info, PlayQueueItem.DEFAULT_QUALITY);
playerImpl.playQueue.init();
playerImpl.playbackManager = new PlaybackManager(playerImpl, playerImpl.playQueue);
playerImpl.playbackManager = new MediaSourceManager(playerImpl, playerImpl.playQueue);
}
});
}

View File

@ -57,7 +57,6 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.playback.PlaybackManager;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue;
@ -104,6 +103,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
private boolean startedFromNewPipe = true;
protected boolean wasPlaying = false;
/*//////////////////////////////////////////////////////////////////////////
// Views
@ -255,24 +255,21 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
buildPlaybackSpeedMenu(playbackSpeedPopupMenu);
}
@Override
public MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex) {
public MediaSource sourceOf(final StreamInfo info) {
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
List<MediaSource> sources = new ArrayList<>();
final VideoStream video;
if (sortedStreamsIndex == PlayQueueItem.DEFAULT_QUALITY) {
final int index = ListHelper.getDefaultResolutionIndex(context, videos);
video = videos.get(index);
} else {
video = videos.get(sortedStreamsIndex);
for (final VideoStream video : videos) {
final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
sources.add(mediaSource);
}
final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
if (!video.isVideoOnly) return mediaSource;
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
final Uri audioUri = Uri.parse(audio.url);
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null));
final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null);
sources.add(audioSource);
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
}
public void buildQualityMenu(PopupMenu popupMenu) {

View File

@ -0,0 +1,82 @@
package org.schabi.newpipe.player.mediasource;
import android.os.Looper;
import com.google.android.exoplayer2.C;
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.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
import java.util.List;
public final class DeferredMediaSource implements MediaSource {
public interface Callback {
MediaSource sourceOf(final StreamInfo info);
}
final private PlayQueueItem stream;
final private Callback callback;
private StreamInfo info;
private MediaSource mediaSource;
private ExoPlayer exoPlayer;
private boolean isTopLevel;
private Listener listener;
public DeferredMediaSource(final PlayQueueItem stream, final Callback callback) {
this.stream = stream;
this.callback = callback;
}
@Override
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
this.exoPlayer = exoPlayer;
this.isTopLevel = isTopLevelSource;
this.listener = listener;
listener.onSourceInfoRefreshed(new SinglePeriodTimeline(C.TIME_UNSET, false), null);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) {
// This must be called on a non-main thread
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new UnsupportedOperationException("Source preparation is blocking, it must be run on non-UI thread.");
}
info = stream.getStream().blockingGet();
mediaSource = callback.sourceOf(info);
mediaSource.prepareSource(exoPlayer, isTopLevel, listener);
return mediaSource.createPeriod(mediaPeriodId, allocator);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
mediaSource.releasePeriod(mediaPeriod);
}
@Override
public void releaseSource() {
if (mediaSource != null) mediaSource.releaseSource();
info = null;
mediaSource = null;
}
}

View File

@ -8,6 +8,7 @@ import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
@ -25,12 +26,12 @@ import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
public class PlaybackManager {
private final String TAG = "PlaybackManager@" + Integer.toHexString(hashCode());
public class MediaSourceManager implements DeferredMediaSource.Callback {
private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
// One-side rolling window size for default loading
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
// todo: inject this parameter, allow user settings perhaps
private static final int WINDOW_SIZE = 3;
private static final int WINDOW_SIZE = 1;
private final PlaybackListener playbackListener;
private final PlayQueue playQueue;
@ -46,10 +47,9 @@ public class PlaybackManager {
private CompositeDisposable disposables;
private boolean isBlocked;
private boolean hasReset;
public PlaybackManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this.playbackListener = listener;
this.playQueue = playQueue;
@ -114,22 +114,27 @@ public class PlaybackManager {
public void onNext(@NonNull PlayQueueMessage event) {
// why no pattern matching in Java =(
switch (event.type()) {
case INIT:
tryBlock();
resetSources();
break;
case APPEND:
break;
case SELECT:
if (isBlocked) break;
if (isCurrentIndexLoaded()) sync(); else tryBlock();
if (isCurrentIndexLoaded()) {
sync();
} else {
tryBlock();
resetSources();
}
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
if (!removeEvent.isCurrent()) {
remove(removeEvent.index());
break;
} else {
tryBlock();
resetSources();
}
break;
case INIT:
case UPDATE:
case REORDER:
tryBlock();
@ -182,13 +187,8 @@ public class PlaybackManager {
private boolean tryUnblock() {
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
if (hasReset) {
playbackListener.prepare(sources);
hasReset = false;
}
isBlocked = false;
playbackListener.unblock();
playbackListener.unblock(sources);
return true;
}
return false;
@ -208,53 +208,10 @@ public class PlaybackManager {
}
private void load() {
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.get(currentIndex);
if (currentItem == null) return;
load(currentItem);
// Load boundaries to ensure correct looping
if (sourceToQueueIndex.indexOf(0) == -1) load(playQueue.get(0));
if (sourceToQueueIndex.indexOf(playQueue.size() - 1) == -1) load(playQueue.get(playQueue.size() - 1));
// The rest are just for seamless playback
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightBound = Math.min(playQueue.size(), currentIndex + WINDOW_SIZE + 1);
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
for (final PlayQueueItem item: items) load(item);
}
private void load(@Nullable final PlayQueueItem item) {
if (item == null) return;
item.getStream().subscribe(new SingleObserver<StreamInfo>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (disposables == null) {
d.dispose();
return;
}
disposables.add(d);
}
@Override
public void onSuccess(@NonNull StreamInfo streamInfo) {
final MediaSource source = playbackListener.sourceOf(streamInfo, item.getSortedQualityIndex());
final int itemIndex = playQueue.indexOf(item);
// replace all except the currently playing
insert(itemIndex, source, itemIndex != playQueue.getIndex());
if (tryUnblock()) sync();
}
@Override
public void onError(@NonNull Throwable e) {
playQueue.remove(playQueue.indexOf(item));
load();
}
});
for (final PlayQueueItem item : playQueue.getStreams()) {
insert(playQueue.indexOf(item), new DeferredMediaSource(item, this));
if (tryUnblock()) sync();
}
}
private void resetSources() {
@ -263,7 +220,6 @@ public class PlaybackManager {
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
this.sources = new DynamicConcatenatingMediaSource();
this.hasReset = true;
}
/*//////////////////////////////////////////////////////////////////////////
@ -272,7 +228,7 @@ public class PlaybackManager {
// Insert source into playlist with position in respect to the play queue
// If the play queue index already exists, then the insert is ignored
private void insert(final int queueIndex, final MediaSource source, final boolean replace) {
private void insert(final int queueIndex, final MediaSource source) {
if (queueIndex < 0) return;
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex);
@ -280,9 +236,6 @@ public class PlaybackManager {
final int sourceIndex = -pos-1;
sourceToQueueIndex.add(sourceIndex, queueIndex);
sources.addMediaSource(sourceIndex, source);
} else if (replace) {
sources.addMediaSource(pos + 1, source);
sources.removeMediaSource(pos);
}
}
@ -300,4 +253,9 @@ public class PlaybackManager {
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
}
}
@Override
public MediaSource sourceOf(StreamInfo info) {
return playbackListener.sourceOf(info);
}
}

View File

@ -4,6 +4,8 @@ import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.List;
public interface PlaybackListener {
/*
* Called when the stream at the current queue index is not ready yet.
@ -13,23 +15,13 @@ public interface PlaybackListener {
* */
void block();
/*
* Called when the media source is rebuilt.
* Signals to the listener to prepare the media source again.
* The provided media source is always non-empty.
*
* May be called only after blocking and before unblocking.
* */
void prepare(final MediaSource mediaSource);
/*
* Called when the stream at the current queue index is ready.
* Signals to the listener to resume the player.
*
* May be called only when the player is blocked.
* */
void unblock();
void unblock(final MediaSource mediaSource);
/*
* Called when the queue index is refreshed.
@ -46,7 +38,7 @@ public interface PlaybackListener {
*
* May be called at any time.
* */
MediaSource sourceOf(final StreamInfo info, final int sortedStreamsIndex);
MediaSource sourceOf(final StreamInfo info);
/*
* Called when the play queue can no longer to played or used.