-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() { private void refreshTimeline() {
playbackManager.load();
final int currentSourceIndex = playbackManager.getCurrentSourceIndex(); final int currentSourceIndex = playbackManager.getCurrentSourceIndex();
// Sanity checks // Sanity checks
@ -558,15 +560,6 @@ public abstract class BasePlayer implements Player.EventListener,
// Check if already playing correct window // Check if already playing correct window
final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex; 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 // Check if on wrong window
if (!isCurrentWindowCorrect) { if (!isCurrentWindowCorrect) {
@ -576,14 +569,16 @@ public abstract class BasePlayer implements Player.EventListener,
} }
// Check if recovering // Check if recovering
if (isRecovery && queuePos == playQueue.getIndex() && isCurrentWindowCorrect) { if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)videoPos)); // todo: figure out exactly why this is the case
simpleExoPlayer.seekTo(videoPos); /* 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; isRecovery = false;
} }
// Good to go...
simpleExoPlayer.setPlayWhenReady(true);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -628,7 +623,9 @@ public abstract class BasePlayer implements Player.EventListener,
isPrepared = false; isPrepared = false;
break; break;
case Player.STATE_BUFFERING: // 2 case Player.STATE_BUFFERING: // 2
if (isPrepared) changeState(STATE_BUFFERING); if (isPrepared) {
changeState(STATE_BUFFERING);
}
break; break;
case Player.STATE_READY: //3 case Player.STATE_READY: //3
if (!isPrepared) { 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 AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
if (audio != null) {
final Uri audioUri = Uri.parse(audio.url); final Uri audioUri = Uri.parse(audio.url);
final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null); final MediaSource audioSource = new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null);
sources.add(audioSource); sources.add(audioSource);
}
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()])); return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
} }

View File

@ -1,53 +1,107 @@
package org.schabi.newpipe.player.mediasource; 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.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; 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 com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException; 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 { 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 { public interface Callback {
MediaSource sourceOf(final StreamInfo info); MediaSource sourceOf(final StreamInfo info);
} }
final private PlayQueueItem stream; private PlayQueueItem stream;
final private Callback callback; private Callback callback;
private StreamInfo info;
private MediaSource mediaSource; private MediaSource mediaSource;
private ExoPlayer exoPlayer; private Disposable loader;
private boolean isTopLevel;
private Listener listener;
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.stream = stream;
this.callback = callback; this.callback = callback;
this.state = STATE_INIT;
} }
@Override @Override
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) { public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
this.exoPlayer = exoPlayer; this.exoPlayer = exoPlayer;
this.isTopLevel = isTopLevelSource;
this.listener = listener; 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 @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
if (error != null) {
throw new IOException(error);
}
if (mediaSource != null) { if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError(); mediaSource.maybeThrowSourceInfoRefreshError();
} }
@ -55,28 +109,33 @@ public final class DeferredMediaSource implements MediaSource {
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) { 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); return mediaSource.createPeriod(mediaPeriodId, allocator);
} }
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
if (mediaSource == null) {
Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred.");
} else {
mediaSource.releasePeriod(mediaPeriod); mediaSource.releasePeriod(mediaPeriod);
} }
}
@Override @Override
public void releaseSource() { public void releaseSource() {
if (mediaSource != null) mediaSource.releaseSource(); state = STATE_DISPOSED;
info = null;
mediaSource = null; 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; package org.schabi.newpipe.player.playback;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; 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.PlayQueueItem;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.UpdateEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull; import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
@ -44,7 +42,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private Subscription playQueueReactor; private Subscription playQueueReactor;
private Disposable syncReactor; private Disposable syncReactor;
private CompositeDisposable disposables;
private boolean isBlocked; private boolean isBlocked;
@ -53,8 +50,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
this.playbackListener = listener; this.playbackListener = listener;
this.playQueue = playQueue; this.playQueue = playQueue;
this.disposables = new CompositeDisposable();
this.sources = new DynamicConcatenatingMediaSource(); this.sources = new DynamicConcatenatingMediaSource();
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>()); this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
@ -85,18 +80,35 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
public void dispose() { public void dispose() {
if (playQueueReactor != null) playQueueReactor.cancel(); if (playQueueReactor != null) playQueueReactor.cancel();
if (disposables != null) disposables.dispose();
if (syncReactor != null) syncReactor.dispose(); if (syncReactor != null) syncReactor.dispose();
if (sources != null) sources.releaseSource(); if (sources != null) sources.releaseSource();
if (sourceToQueueIndex != null) sourceToQueueIndex.clear(); if (sourceToQueueIndex != null) sourceToQueueIndex.clear();
playQueueReactor = null; playQueueReactor = null;
disposables = null;
syncReactor = null; syncReactor = null;
sources = null; sources = null;
sourceToQueueIndex = 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 // Event Reactor
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -115,30 +127,26 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
// why no pattern matching in Java =( // why no pattern matching in Java =(
switch (event.type()) { switch (event.type()) {
case APPEND: case APPEND:
populateSources();
break; break;
case SELECT: case SELECT:
if (isBlocked) break;
if (isCurrentIndexLoaded()) { if (isCurrentIndexLoaded()) {
sync(); sync();
} else {
tryBlock();
resetSources();
} }
break; break;
case REMOVE: case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event; final RemoveEvent removeEvent = (RemoveEvent) event;
if (!removeEvent.isCurrent()) { if (!removeEvent.isCurrent()) {
remove(removeEvent.index()); remove(removeEvent.index());
} else {
tryBlock();
resetSources();
}
break; break;
}
case INIT: case INIT:
case UPDATE: case UPDATE:
case REORDER: case REORDER:
tryBlock(); tryBlock();
resetSources(); resetSources();
populateSources();
if (tryUnblock()) sync();
break; break;
default: default:
break; 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() { private void load(@Nullable final PlayQueueItem item) {
for (final PlayQueueItem item : playQueue.getStreams()) { if (item == null) return;
insert(playQueue.indexOf(item), new DeferredMediaSource(item, this));
if (tryUnblock()) sync(); 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() { private void resetSources() {
if (this.disposables != null) this.disposables.clear();
if (this.sources != null) this.sources.releaseSource(); if (this.sources != null) this.sources.releaseSource();
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear(); if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
this.sources = new DynamicConcatenatingMediaSource(); 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 // Media Source List Manipulation
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
// Insert source into playlist with position in respect to the play queue // Insert source into playlist with position in respect to the play queue
// If the play queue index already exists, then the insert is ignored // 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; if (queueIndex < 0) return;
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex); 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.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -147,9 +148,9 @@ public abstract class PlayQueue implements Serializable {
broadcast(new UpdateEvent(index)); broadcast(new UpdateEvent(index));
} }
protected synchronized void append(final PlayQueueItem item) { protected synchronized void append(final PlayQueueItem... items) {
streams.add(item); streams.addAll(Arrays.asList(items));
broadcast(new AppendEvent(1)); broadcast(new AppendEvent(items.length));
} }
protected synchronized void append(final Collection<PlayQueueItem> items) { protected synchronized void append(final Collection<PlayQueueItem> items) {