2017-09-20 06:46:16 +02:00
|
|
|
package org.schabi.newpipe.player.mediasource;
|
|
|
|
|
2017-09-24 02:02:05 +02:00
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
import android.util.Log;
|
2017-09-20 06:46:16 +02:00
|
|
|
|
|
|
|
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.extractor.stream.StreamInfo;
|
|
|
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
|
|
|
|
|
|
|
import java.io.IOException;
|
2017-09-24 02:02:05 +02:00
|
|
|
|
|
|
|
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;
|
2017-09-20 06:46:16 +02:00
|
|
|
|
2017-09-29 04:37:04 +02:00
|
|
|
/**
|
|
|
|
* DeferredMediaSource is specifically designed to allow external control over when
|
|
|
|
* the source metadata are loaded while being compatible with ExoPlayer's playlists.
|
|
|
|
*
|
|
|
|
* This media source follows the structure of how NewPipeExtractor's
|
|
|
|
* {@link org.schabi.newpipe.extractor.stream.StreamInfoItem} is converted into
|
|
|
|
* {@link org.schabi.newpipe.extractor.stream.StreamInfo}. Once conversion is complete,
|
|
|
|
* this media source behaves identically as any other native media sources.
|
|
|
|
* */
|
2017-09-20 06:46:16 +02:00
|
|
|
public final class DeferredMediaSource implements MediaSource {
|
2017-09-24 02:02:05 +02:00
|
|
|
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;
|
2017-09-20 06:46:16 +02:00
|
|
|
|
|
|
|
public interface Callback {
|
2017-09-29 04:37:04 +02:00
|
|
|
/**
|
|
|
|
* Player-specific MediaSource resolution from given StreamInfo.
|
|
|
|
* */
|
2017-09-20 06:46:16 +02:00
|
|
|
MediaSource sourceOf(final StreamInfo info);
|
|
|
|
}
|
|
|
|
|
2017-09-24 02:02:05 +02:00
|
|
|
private PlayQueueItem stream;
|
|
|
|
private Callback callback;
|
2017-09-20 06:46:16 +02:00
|
|
|
|
|
|
|
private MediaSource mediaSource;
|
|
|
|
|
2017-09-24 02:02:05 +02:00
|
|
|
private Disposable loader;
|
|
|
|
|
2017-09-20 06:46:16 +02:00
|
|
|
private ExoPlayer exoPlayer;
|
|
|
|
private Listener listener;
|
2017-09-24 02:02:05 +02:00
|
|
|
private Throwable error;
|
2017-09-20 06:46:16 +02:00
|
|
|
|
2017-09-24 02:02:05 +02:00
|
|
|
public DeferredMediaSource(@NonNull final PlayQueueItem stream,
|
|
|
|
@NonNull final Callback callback) {
|
2017-09-20 06:46:16 +02:00
|
|
|
this.stream = stream;
|
|
|
|
this.callback = callback;
|
2017-09-24 02:02:05 +02:00
|
|
|
this.state = STATE_INIT;
|
2017-09-20 06:46:16 +02:00
|
|
|
}
|
|
|
|
|
2017-09-29 04:37:04 +02:00
|
|
|
/**
|
|
|
|
* Parameters are kept in the class for delayed preparation.
|
|
|
|
* */
|
2017-09-20 06:46:16 +02:00
|
|
|
@Override
|
|
|
|
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
|
|
|
|
this.exoPlayer = exoPlayer;
|
|
|
|
this.listener = listener;
|
2017-09-24 02:02:05 +02:00
|
|
|
this.state = STATE_PREPARED;
|
|
|
|
}
|
2017-09-20 06:46:16 +02:00
|
|
|
|
2017-09-24 02:02:05 +02:00
|
|
|
public int state() {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2017-09-29 04:37:04 +02:00
|
|
|
/**
|
|
|
|
* Externally controlled loading. This method fully prepares the source to be used
|
|
|
|
* like any other native MediaSource.
|
|
|
|
*
|
|
|
|
* Ideally, this should be called after this source has entered PREPARED state and
|
|
|
|
* called once only.
|
|
|
|
*
|
|
|
|
* If loading fails here, an error will be propagated out and result in a
|
|
|
|
* {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated
|
|
|
|
* out to the player.
|
|
|
|
* */
|
2017-09-24 02:02:05 +02:00
|
|
|
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);
|
2017-09-20 06:46:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
2017-09-24 02:02:05 +02:00
|
|
|
if (error != null) {
|
|
|
|
throw new IOException(error);
|
|
|
|
}
|
|
|
|
|
2017-09-20 06:46:16 +02:00
|
|
|
if (mediaSource != null) {
|
|
|
|
mediaSource.maybeThrowSourceInfoRefreshError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) {
|
|
|
|
return mediaSource.createPeriod(mediaPeriodId, allocator);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void releasePeriod(MediaPeriod mediaPeriod) {
|
2017-09-24 02:02:05 +02:00
|
|
|
if (mediaSource == null) {
|
|
|
|
Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred.");
|
|
|
|
} else {
|
|
|
|
mediaSource.releasePeriod(mediaPeriod);
|
|
|
|
}
|
2017-09-20 06:46:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void releaseSource() {
|
2017-09-24 02:02:05 +02:00
|
|
|
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;
|
2017-09-20 06:46:16 +02:00
|
|
|
}
|
|
|
|
}
|