mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-23 16:40:32 +00:00
-Added better assertions and documentations to new mechanism in MediaSourceManager.
-Modified LoadController to allow fast playback start and increased buffer zigzag window. -Removed unnecessary loading on timeline changes. -Changed select message in MediaSourceManager to cause immediate load. -Reduced default expiration time in MediaSourceManager. -Fixed main video player not showing end time on audio-only streams. -Fixed live stream has player view disabled after transitioning from audio stream. -Fixed inconsistent progress bar height between live and non-live video on main player.
This commit is contained in:
parent
77da40e507
commit
b4668367c6
@ -55,7 +55,7 @@ dependencies {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
implementation 'com.github.karyogamy:NewPipeExtractor:837dbd6b86'
|
||||
implementation 'com.github.karyogamy:NewPipeExtractor:4cf4ee394f'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
@ -511,15 +511,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
||||
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
|
||||
|
||||
switch (reason) {
|
||||
case Player.TIMELINE_CHANGE_REASON_PREPARED:
|
||||
case Player.TIMELINE_CHANGE_REASON_RESET:
|
||||
case Player.TIMELINE_CHANGE_REASON_DYNAMIC:
|
||||
default:
|
||||
if (playbackManager != null) playbackManager.load();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -654,6 +645,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||
} else {
|
||||
playQueue.offsetIndex(+1);
|
||||
}
|
||||
playbackManager.load();
|
||||
break;
|
||||
case DISCONTINUITY_REASON_SEEK:
|
||||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||
@ -661,7 +653,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||
default:
|
||||
break;
|
||||
}
|
||||
playbackManager.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -724,8 +715,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||
"], queue index=[" + playQueue.getIndex() + "]");
|
||||
} else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) {
|
||||
final long startPos = info != null ? info.start_position : 0;
|
||||
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
|
||||
" at: " + getTimeString((int)startPos));
|
||||
if (DEBUG) Log.d(TAG, "Rewinding to correct window=[" + currentSourceIndex + "]," +
|
||||
" at=[" + getTimeString((int)startPos) + "]," +
|
||||
" from=[" + simpleExoPlayer.getCurrentPeriodIndex() + "].");
|
||||
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
||||
}
|
||||
|
||||
@ -974,7 +966,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return simpleExoPlayer.getPlaybackState() == Player.STATE_READY && simpleExoPlayer.getPlayWhenReady();
|
||||
final int state = simpleExoPlayer.getPlaybackState();
|
||||
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
|
||||
&& simpleExoPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
public int getRepeatMode() {
|
||||
|
@ -339,11 +339,16 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
switch (streamType) {
|
||||
case AUDIO_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
playbackEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case AUDIO_LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
|
@ -11,7 +11,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES;
|
||||
|
||||
@ -19,6 +18,7 @@ public class LoadController implements LoadControl {
|
||||
|
||||
public static final String TAG = "LoadController";
|
||||
|
||||
private final long initialPlaybackBufferUs;
|
||||
private final LoadControl internalLoadControl;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -26,18 +26,24 @@ public class LoadController implements LoadControl {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public LoadController(final Context context) {
|
||||
this(PlayerHelper.getMinBufferMs(context),
|
||||
PlayerHelper.getMaxBufferMs(context),
|
||||
PlayerHelper.getBufferForPlaybackMs(context));
|
||||
this(PlayerHelper.getPlaybackStartBufferMs(context),
|
||||
PlayerHelper.getPlaybackMinimumBufferMs(context),
|
||||
PlayerHelper.getPlaybackOptimalBufferMs(context));
|
||||
}
|
||||
|
||||
private LoadController(final int minBufferMs, final int maxBufferMs,
|
||||
final int bufferForPlaybackMs) {
|
||||
private LoadController(final int initialPlaybackBufferMs,
|
||||
final int minimumPlaybackbufferMs,
|
||||
final int optimalPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultAllocator allocator = new DefaultAllocator(true,
|
||||
C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||
|
||||
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
|
||||
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
|
||||
internalLoadControl = new DefaultLoadControl(allocator,
|
||||
/*minBufferMs=*/minimumPlaybackbufferMs,
|
||||
/*maxBufferMs=*/optimalPlaybackBufferMs,
|
||||
/*bufferForPlaybackMs=*/initialPlaybackBufferMs,
|
||||
/*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs,
|
||||
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
|
||||
}
|
||||
|
||||
@ -89,7 +95,10 @@ public class LoadController implements LoadControl {
|
||||
@Override
|
||||
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed,
|
||||
boolean rebuffering) {
|
||||
return internalLoadControl.shouldStartPlayback(bufferedDurationUs, playbackSpeed,
|
||||
rebuffering);
|
||||
final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >=
|
||||
this.initialPlaybackBufferUs * playbackSpeed;
|
||||
final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback(
|
||||
bufferedDurationUs, playbackSpeed, rebuffering);
|
||||
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
|
||||
}
|
||||
}
|
||||
|
@ -117,16 +117,27 @@ public class PlayerHelper {
|
||||
return 512 * 1024L;
|
||||
}
|
||||
|
||||
public static int getMinBufferMs(@NonNull final Context context) {
|
||||
return 15000;
|
||||
/**
|
||||
* Returns the number of milliseconds the player buffers for before starting playback.
|
||||
* */
|
||||
public static int getPlaybackStartBufferMs(@NonNull final Context context) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
public static int getMaxBufferMs(@NonNull final Context context) {
|
||||
return 30000;
|
||||
/**
|
||||
* Returns the minimum number of milliseconds the player always buffers to after starting
|
||||
* playback.
|
||||
* */
|
||||
public static int getPlaybackMinimumBufferMs(@NonNull final Context context) {
|
||||
return 25000;
|
||||
}
|
||||
|
||||
public static int getBufferForPlaybackMs(@NonNull final Context context) {
|
||||
return 2500;
|
||||
/**
|
||||
* Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer
|
||||
* hits the point of {@link #getPlaybackMinimumBufferMs(Context)}.
|
||||
* */
|
||||
public static int getPlaybackOptimalBufferMs(@NonNull final Context context) {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
public static boolean isUsingDSP(@NonNull final Context context) {
|
||||
|
@ -12,7 +12,7 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FailedMediaSource implements ManagedMediaSource {
|
||||
private final String TAG = "ManagedMediaSource@" + Integer.toHexString(hashCode());
|
||||
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
|
||||
|
||||
private final PlayQueueItem playQueueItem;
|
||||
private final Throwable error;
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
@ -28,7 +29,6 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.SerialDisposable;
|
||||
@ -38,23 +38,26 @@ import io.reactivex.subjects.PublishSubject;
|
||||
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
|
||||
|
||||
public class MediaSourceManager {
|
||||
private final static String TAG = "MediaSourceManager";
|
||||
@NonNull private final static String TAG = "MediaSourceManager";
|
||||
|
||||
// WINDOW_SIZE determines how many streams AFTER the current stream should be loaded.
|
||||
// The default value (1) ensures seamless playback under typical network settings.
|
||||
private final static int WINDOW_SIZE = 1;
|
||||
|
||||
private final PlaybackListener playbackListener;
|
||||
private final PlayQueue playQueue;
|
||||
@NonNull private final PlaybackListener playbackListener;
|
||||
@NonNull private final PlayQueue playQueue;
|
||||
|
||||
// Once a MediaSource item has been detected to be expired, the manager will immediately
|
||||
// trigger a reload on the associated PlayQueueItem, which may disrupt playback,
|
||||
// if the item is being played
|
||||
private final long expirationTimeMillis;
|
||||
private final TimeUnit expirationTimeUnit;
|
||||
|
||||
// Process only the last load order when receiving a stream of load orders (lessens I/O)
|
||||
// The higher it is, the less loading occurs during rapid noncritical timeline changes
|
||||
// Not recommended to go below 100ms
|
||||
private final long loadDebounceMillis;
|
||||
private final PublishSubject<Long> debouncedLoadSignal;
|
||||
private final Disposable debouncedLoader;
|
||||
@NonNull private final Disposable debouncedLoader;
|
||||
@NonNull private final PublishSubject<Long> debouncedSignal;
|
||||
|
||||
private DynamicConcatenatingMediaSource sources;
|
||||
|
||||
@ -71,23 +74,20 @@ public class MediaSourceManager {
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
this(listener, playQueue,
|
||||
/*loadDebounceMillis=*/400L,
|
||||
/*expirationTimeMillis=*/2,
|
||||
/*expirationTimeUnit=*/TimeUnit.HOURS);
|
||||
/*expirationTimeMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES));
|
||||
}
|
||||
|
||||
private MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final long loadDebounceMillis,
|
||||
final long expirationTimeMillis,
|
||||
@NonNull final TimeUnit expirationTimeUnit) {
|
||||
final long expirationTimeMillis) {
|
||||
this.playbackListener = listener;
|
||||
this.playQueue = playQueue;
|
||||
this.loadDebounceMillis = loadDebounceMillis;
|
||||
this.expirationTimeMillis = expirationTimeMillis;
|
||||
this.expirationTimeUnit = expirationTimeUnit;
|
||||
|
||||
this.loaderReactor = new CompositeDisposable();
|
||||
this.debouncedLoadSignal = PublishSubject.create();
|
||||
this.debouncedSignal = PublishSubject.create();
|
||||
this.debouncedLoader = getDebouncedLoader();
|
||||
|
||||
this.sources = new DynamicConcatenatingMediaSource();
|
||||
@ -109,8 +109,11 @@ public class MediaSourceManager {
|
||||
* Dispose the manager and releases all message buses and loaders.
|
||||
* */
|
||||
public void dispose() {
|
||||
if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
|
||||
if (debouncedLoader != null) debouncedLoader.dispose();
|
||||
if (DEBUG) Log.d(TAG, "dispose() called.");
|
||||
|
||||
debouncedSignal.onComplete();
|
||||
debouncedLoader.dispose();
|
||||
|
||||
if (playQueueReactor != null) playQueueReactor.cancel();
|
||||
if (loaderReactor != null) loaderReactor.dispose();
|
||||
if (syncReactor != null) syncReactor.dispose();
|
||||
@ -129,6 +132,7 @@ public class MediaSourceManager {
|
||||
* Unblocks the player once the item at the current index is loaded.
|
||||
* */
|
||||
public void load() {
|
||||
if (DEBUG) Log.d(TAG, "load() called.");
|
||||
loadDebounced();
|
||||
}
|
||||
|
||||
@ -139,6 +143,8 @@ public class MediaSourceManager {
|
||||
* through {@link #load() load}.
|
||||
* */
|
||||
public void reset() {
|
||||
if (DEBUG) Log.d(TAG, "reset() called.");
|
||||
|
||||
tryBlock();
|
||||
|
||||
syncedItem = null;
|
||||
@ -205,11 +211,11 @@ public class MediaSourceManager {
|
||||
case INIT:
|
||||
case REORDER:
|
||||
case ERROR:
|
||||
case SELECT:
|
||||
loadImmediate(); // low frequency, critical events
|
||||
break;
|
||||
case APPEND:
|
||||
case REMOVE:
|
||||
case SELECT:
|
||||
case MOVE:
|
||||
case RECOVERY:
|
||||
default:
|
||||
@ -229,16 +235,12 @@ public class MediaSourceManager {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private boolean isPlayQueueReady() {
|
||||
if (playQueue == null) return false;
|
||||
|
||||
final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
|
||||
return playQueue.isComplete() || isWindowLoaded;
|
||||
}
|
||||
|
||||
private boolean isPlaybackReady() {
|
||||
if (sources == null || playQueue == null || sources.getSize() != playQueue.size()) {
|
||||
return false;
|
||||
}
|
||||
if (sources == null || sources.getSize() != playQueue.size()) return false;
|
||||
|
||||
final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex());
|
||||
final PlayQueueItem playQueueItem = playQueue.getItem();
|
||||
@ -252,6 +254,8 @@ public class MediaSourceManager {
|
||||
}
|
||||
|
||||
private void tryBlock() {
|
||||
if (DEBUG) Log.d(TAG, "tryBlock() called.");
|
||||
|
||||
if (isBlocked) return;
|
||||
|
||||
playbackListener.block();
|
||||
@ -261,6 +265,8 @@ public class MediaSourceManager {
|
||||
}
|
||||
|
||||
private void tryUnblock() {
|
||||
if (DEBUG) Log.d(TAG, "tryUnblock() called.");
|
||||
|
||||
if (isPlayQueueReady() && isPlaybackReady() && isBlocked && sources != null) {
|
||||
isBlocked = false;
|
||||
playbackListener.unblock(sources);
|
||||
@ -272,6 +278,8 @@ public class MediaSourceManager {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void sync() {
|
||||
if (DEBUG) Log.d(TAG, "sync() called.");
|
||||
|
||||
final PlayQueueItem currentItem = playQueue.getItem();
|
||||
if (isBlocked || currentItem == null) return;
|
||||
|
||||
@ -289,7 +297,6 @@ public class MediaSourceManager {
|
||||
|
||||
private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info) {
|
||||
if (playQueue == null || playbackListener == null) return;
|
||||
// Ensure the current item is up to date with the play queue
|
||||
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
|
||||
playbackListener.sync(syncedItem, info);
|
||||
@ -301,14 +308,14 @@ public class MediaSourceManager {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private Disposable getDebouncedLoader() {
|
||||
return debouncedLoadSignal
|
||||
return debouncedSignal
|
||||
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(timestamp -> loadImmediate());
|
||||
}
|
||||
|
||||
private void loadDebounced() {
|
||||
debouncedLoadSignal.onNext(System.currentTimeMillis());
|
||||
debouncedSignal.onNext(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private void loadImmediate() {
|
||||
@ -316,7 +323,7 @@ public class MediaSourceManager {
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
||||
if (currentItem == null) return;
|
||||
loadItem(currentItem);
|
||||
maybeLoadItem(currentItem);
|
||||
|
||||
// The rest are just for seamless playback
|
||||
final int leftBound = currentIndex + 1;
|
||||
@ -331,10 +338,14 @@ public class MediaSourceManager {
|
||||
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
|
||||
}
|
||||
|
||||
for (final PlayQueueItem item: items) loadItem(item);
|
||||
for (final PlayQueueItem item : items) {
|
||||
maybeLoadItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadItem(@Nullable final PlayQueueItem item) {
|
||||
private void maybeLoadItem(@Nullable final PlayQueueItem item) {
|
||||
if (DEBUG) Log.d(TAG, "maybeLoadItem() called.");
|
||||
|
||||
if (sources == null || item == null) return;
|
||||
|
||||
final int index = playQueue.indexOf(item);
|
||||
@ -368,11 +379,6 @@ public class MediaSourceManager {
|
||||
|
||||
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
|
||||
return stream.getStream().map(streamInfo -> {
|
||||
if (playbackListener == null) {
|
||||
return new FailedMediaSource(stream, new IllegalStateException(
|
||||
"MediaSourceManager playback listener unavailable"));
|
||||
}
|
||||
|
||||
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
|
||||
if (source == null) {
|
||||
final Exception exception = new IllegalStateException(
|
||||
@ -384,21 +390,34 @@ public class MediaSourceManager {
|
||||
return new FailedMediaSource(stream, new IllegalStateException(exception));
|
||||
}
|
||||
|
||||
final long expiration = System.currentTimeMillis() +
|
||||
TimeUnit.MILLISECONDS.convert(expirationTimeMillis, expirationTimeUnit);
|
||||
final long expiration = System.currentTimeMillis() + expirationTimeMillis;
|
||||
return new LoadedMediaSource(source, stream, expiration);
|
||||
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource}
|
||||
* for a given {@link PlayQueueItem} needs replacement, either due to gapless playback
|
||||
* readiness or playlist desynchronization.
|
||||
* <br><br>
|
||||
* If the given {@link PlayQueueItem} is currently being played and is already loaded,
|
||||
* then correction is not only needed if the playlist is desynchronized. Otherwise, the
|
||||
* check depends on the status (e.g. expiration or placeholder) of the
|
||||
* {@link ManagedMediaSource}.
|
||||
* */
|
||||
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
|
||||
if (playQueue == null || sources == null) return false;
|
||||
if (sources == null) return false;
|
||||
|
||||
final int index = playQueue.indexOf(item);
|
||||
if (index == -1 || index >= sources.getSize()) return false;
|
||||
|
||||
final MediaSource mediaSource = sources.getMediaSource(index);
|
||||
return !(mediaSource instanceof ManagedMediaSource) ||
|
||||
((ManagedMediaSource) mediaSource).canReplace(item);
|
||||
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
|
||||
|
||||
if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) {
|
||||
return item != ((LoadedMediaSource) mediaSource).getStream();
|
||||
} else {
|
||||
return mediaSource.canReplace(item);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -406,11 +425,14 @@ public class MediaSourceManager {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void resetSources() {
|
||||
if (DEBUG) Log.d(TAG, "resetSources() called.");
|
||||
|
||||
if (this.sources != null) this.sources.releaseSource();
|
||||
this.sources = new DynamicConcatenatingMediaSource();
|
||||
}
|
||||
|
||||
private void populateSources() {
|
||||
if (DEBUG) Log.d(TAG, "populateSources() called.");
|
||||
if (sources == null || sources.getSize() >= playQueue.size()) return;
|
||||
|
||||
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
|
||||
@ -462,8 +484,12 @@ public class MediaSourceManager {
|
||||
* Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
|
||||
* at the given index with a given {@link MediaSource}. If the index is out of bound,
|
||||
* then the replacement is ignored.
|
||||
* <br><br>
|
||||
* Not recommended to use on indices LESS THAN the currently playing index, since
|
||||
* this will modify the playback timeline prior to the index and cause desynchronization
|
||||
* on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}.
|
||||
* */
|
||||
private void update(final int index, final MediaSource source) {
|
||||
private synchronized void update(final int index, final MediaSource source) {
|
||||
if (sources == null) return;
|
||||
if (index < 0 || index >= sources.getSize()) return;
|
||||
|
||||
|
@ -401,8 +401,8 @@
|
||||
<TextView
|
||||
android:id="@+id/playbackLiveSync"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/live_sync"
|
||||
android:textColor="@android:color/white"
|
||||
android:visibility="gone"
|
||||
|
Loading…
Reference in New Issue
Block a user