-Fixed Deferred Media Source not working on non-extractor (e.g. dash) sources.

-Fixed NPE when extracting streams with no audio.
This commit is contained in:
John Zhen M 2017-09-23 17:02:05 -07:00 committed by John Zhen Mo
parent 9bc95f030c
commit 8e3be3826f
5 changed files with 156 additions and 74 deletions

View File

@ -551,6 +551,8 @@ public abstract class BasePlayer implements Player.EventListener,
//////////////////////////////////////////////////////////////////////////*/
private void refreshTimeline() {
playbackManager.load();
final int currentSourceIndex = playbackManager.getCurrentSourceIndex();
// Sanity checks
@ -558,15 +560,6 @@ public abstract class BasePlayer implements Player.EventListener,
// Check if already playing correct window
final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex;
if (isCurrentWindowCorrect && getCurrentState() == STATE_PLAYING) return;
// Check timeline is up-to-date and has window
if (playbackManager.expectedTimelineSize() != simpleExoPlayer.getCurrentTimeline().getWindowCount()) return;
// Check if window is ready
Timeline.Window window = new Timeline.Window();
simpleExoPlayer.getCurrentTimeline().getWindow(currentSourceIndex, window);
if (window.isDynamic) return;
// Check if on wrong window
if (!isCurrentWindowCorrect) {
@ -576,14 +569,16 @@ public abstract class BasePlayer implements Player.EventListener,
}
// Check if recovering
if (isRecovery && queuePos == playQueue.getIndex() && isCurrentWindowCorrect) {
if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)videoPos));
simpleExoPlayer.seekTo(videoPos);
if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
// todo: figure out exactly why this is the case
/* Rounding time to nearest second as certain media cannot guarantee a sub-second seek
will complete and the player might get stuck in buffering state forever */
final long roundedPos = (videoPos / 1000) * 1000;
if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)roundedPos));
simpleExoPlayer.seekTo(roundedPos);
isRecovery = false;
}
// Good to go...
simpleExoPlayer.setPlayWhenReady(true);
}
/*//////////////////////////////////////////////////////////////////////////
@ -628,7 +623,9 @@ public abstract class BasePlayer implements Player.EventListener,
isPrepared = false;
break;
case Player.STATE_BUFFERING: // 2
if (isPrepared) changeState(STATE_BUFFERING);
if (isPrepared) {
changeState(STATE_BUFFERING);
}
break;
case Player.STATE_READY: //3
if (!isPrepared) {

View File

@ -265,9 +265,11 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
}
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
final Uri audioUri = Uri.parse(audio.url);
final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null);
sources.add(audioSource);
if (audio != null) {
final Uri audioUri = Uri.parse(audio.url);
final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null);
sources.add(audioSource);
}
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
}

View File

@ -1,53 +1,107 @@
package org.schabi.newpipe.player.mediasource;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.util.Log;
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;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public final class DeferredMediaSource implements MediaSource {
private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode());
private int state = -1;
public final static int STATE_INIT = 0;
public final static int STATE_PREPARED = 1;
public final static int STATE_LOADED = 2;
public final static int STATE_DISPOSED = 3;
public interface Callback {
MediaSource sourceOf(final StreamInfo info);
}
final private PlayQueueItem stream;
final private Callback callback;
private PlayQueueItem stream;
private Callback callback;
private StreamInfo info;
private MediaSource mediaSource;
private ExoPlayer exoPlayer;
private boolean isTopLevel;
private Listener listener;
private Disposable loader;
public DeferredMediaSource(final PlayQueueItem stream, final Callback callback) {
private ExoPlayer exoPlayer;
private Listener listener;
private Throwable error;
public DeferredMediaSource(@NonNull final PlayQueueItem stream,
@NonNull final Callback callback) {
this.stream = stream;
this.callback = callback;
this.state = STATE_INIT;
}
@Override
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
this.exoPlayer = exoPlayer;
this.isTopLevel = isTopLevelSource;
this.listener = listener;
this.state = STATE_PREPARED;
}
listener.onSourceInfoRefreshed(new SinglePeriodTimeline(C.TIME_UNSET, false), null);
public int state() {
return state;
}
public synchronized void load() {
if (state != STATE_PREPARED || stream == null || loader != null) return;
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
@Override
public void accept(StreamInfo streamInfo) throws Exception {
if (exoPlayer == null && listener == null) {
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
} else {
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
mediaSource = callback.sourceOf(streamInfo);
mediaSource.prepareSource(exoPlayer, false, listener);
state = STATE_LOADED;
}
}
};
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, "Loading error:", throwable);
error = throwable;
state = STATE_LOADED;
}
};
loader = stream.getStream()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (error != null) {
throw new IOException(error);
}
if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
@ -55,28 +109,33 @@ public final class DeferredMediaSource implements MediaSource {
@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);
if (mediaSource == null) {
Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred.");
} else {
mediaSource.releasePeriod(mediaPeriod);
}
}
@Override
public void releaseSource() {
if (mediaSource != null) mediaSource.releaseSource();
info = null;
mediaSource = null;
state = STATE_DISPOSED;
if (mediaSource != null) {
mediaSource.releaseSource();
}
if (loader != null) {
loader.dispose();
}
/* Do not set mediaSource as null here as it may be called through releasePeriod */
stream = null;
callback = null;
exoPlayer = null;
listener = null;
}
}

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;
@ -13,16 +14,13 @@ import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.UpdateEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
@ -44,7 +42,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private Subscription playQueueReactor;
private Disposable syncReactor;
private CompositeDisposable disposables;
private boolean isBlocked;
@ -53,8 +50,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
this.playbackListener = listener;
this.playQueue = playQueue;
this.disposables = new CompositeDisposable();
this.sources = new DynamicConcatenatingMediaSource();
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
@ -85,18 +80,35 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
public void dispose() {
if (playQueueReactor != null) playQueueReactor.cancel();
if (disposables != null) disposables.dispose();
if (syncReactor != null) syncReactor.dispose();
if (sources != null) sources.releaseSource();
if (sourceToQueueIndex != null) sourceToQueueIndex.clear();
playQueueReactor = null;
disposables = null;
syncReactor = null;
sources = null;
sourceToQueueIndex = null;
}
public 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);
// The rest are just for seamless playback
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
final int excess = rightLimit - playQueue.size();
if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
for (final PlayQueueItem item: items) load(item);
}
/*//////////////////////////////////////////////////////////////////////////
// Event Reactor
//////////////////////////////////////////////////////////////////////////*/
@ -115,30 +127,26 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
// why no pattern matching in Java =(
switch (event.type()) {
case APPEND:
populateSources();
break;
case SELECT:
if (isBlocked) break;
if (isCurrentIndexLoaded()) {
sync();
} else {
tryBlock();
resetSources();
}
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
if (!removeEvent.isCurrent()) {
remove(removeEvent.index());
} else {
tryBlock();
resetSources();
break;
}
break;
case INIT:
case UPDATE:
case REORDER:
tryBlock();
resetSources();
populateSources();
if (tryUnblock()) sync();
break;
default:
break;
@ -204,31 +212,46 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
}
};
currentItem.getStream().subscribe(syncPlayback);
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, "Sync error:", throwable);
}
};
currentItem.getStream().subscribe(syncPlayback, onError);
}
private void load() {
for (final PlayQueueItem item : playQueue.getStreams()) {
insert(playQueue.indexOf(item), new DeferredMediaSource(item, this));
if (tryUnblock()) sync();
}
private void load(@Nullable final PlayQueueItem item) {
if (item == null) return;
final int index = playQueue.indexOf(item);
if (index > sources.getSize() - 1) return;
final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item));
if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load();
}
private void resetSources() {
if (this.disposables != null) this.disposables.clear();
if (this.sources != null) this.sources.releaseSource();
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
this.sources = new DynamicConcatenatingMediaSource();
}
private void populateSources() {
for (final PlayQueueItem item : playQueue.getStreams()) {
insert(playQueue.indexOf(item), new DeferredMediaSource(item, this));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Media Source List Manipulation
//////////////////////////////////////////////////////////////////////////*/
// 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) {
private void insert(final int queueIndex, final DeferredMediaSource source) {
if (queueIndex < 0) return;
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex);

View File

@ -15,6 +15,7 @@ import org.schabi.newpipe.playlist.events.UpdateEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -147,9 +148,9 @@ public abstract class PlayQueue implements Serializable {
broadcast(new UpdateEvent(index));
}
protected synchronized void append(final PlayQueueItem item) {
streams.add(item);
broadcast(new AppendEvent(1));
protected synchronized void append(final PlayQueueItem... items) {
streams.addAll(Arrays.asList(items));
broadcast(new AppendEvent(items.length));
}
protected synchronized void append(final Collection<PlayQueueItem> items) {