-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:
parent
9bc95f030c
commit
8e3be3826f
|
@ -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) {
|
||||
|
|
|
@ -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()]));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue