Merge branch 'qol-follow-ups' of https://github.com/karyogamy/NewPipe into test

This commit is contained in:
Christian Schabesberger 2018-04-14 10:53:09 +02:00
commit 3cbd2057e3
23 changed files with 548 additions and 303 deletions

View File

@ -42,7 +42,7 @@ android {
ext {
supportLibVersion = '27.1.0'
exoPlayerLibVersion = '2.7.1'
exoPlayerLibVersion = '2.7.3'
roomDbLibVersion = '1.0.0'
leakCanaryLibVersion = '1.5.4'
okHttpLibVersion = '1.5.0'
@ -73,6 +73,7 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:4.2.1'
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"

View File

@ -42,7 +42,11 @@
<service
android:name=".player.BackgroundPlayer"
android:exported="false"/>
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<activity
android:name=".player.BackgroundPlayerActivity"

View File

@ -56,7 +56,8 @@ public final class BookmarkFragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final AppDatabase database = NewPipeDatabase.getInstance(getContext());
if (activity == null) return;
final AppDatabase database = NewPipeDatabase.getInstance(activity);
localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable();

View File

@ -118,8 +118,12 @@ public final class BackgroundPlayer extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent +
"], flags = [" + flags + "], startId = [" + startId + "]");
basePlayerImpl.handleIntent(intent);
if (basePlayerImpl.mediaSessionManager != null) {
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
@ -160,6 +164,11 @@ public final class BackgroundPlayer extends Service {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate();
if (on) {
basePlayerImpl.startProgressLoop();
} else {
basePlayerImpl.stopProgressLoop();
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -545,7 +554,6 @@ public final class BackgroundPlayer extends Service {
super.onPaused();
updateNotification(R.drawable.ic_play_arrow_white);
if (isProgressLoopRunning()) stopProgressLoop();
lockManager.releaseWifiAndCpu();
}

View File

@ -46,7 +46,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Util;
@ -64,6 +64,7 @@ import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.helper.MediaSessionManager;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager;
@ -124,7 +125,6 @@ public abstract class BasePlayer implements
//////////////////////////////////////////////////////////////////////////*/
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f};
protected PlayQueue playQueue;
protected PlayQueueAdapter playQueueAdapter;
@ -140,10 +140,10 @@ public abstract class BasePlayer implements
// Player
//////////////////////////////////////////////////////////////////////////*/
protected final static int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds
protected final static int PLAY_PREV_ACTIVATION_LIMIT = 5000; // 5 seconds
protected final static int PROGRESS_LOOP_INTERVAL = 500;
protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds
protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds
protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
protected CustomTrackSelector trackSelector;
protected PlayerDataSource dataSource;
@ -177,11 +177,11 @@ public abstract class BasePlayer implements
}
public void setup() {
if (simpleExoPlayer == null) initPlayer();
if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true);
initListeners();
}
public void initPlayer() {
public void initPlayer(final boolean playOnReady) {
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
@ -191,15 +191,15 @@ public abstract class BasePlayer implements
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
final AdaptiveTrackSelection.Factory trackSelectionFactory =
new AdaptiveTrackSelection.Factory(bandwidthMeter);
final TrackSelection.Factory trackSelectionFactory =
PlayerHelper.getQualitySelector(context, bandwidthMeter);
trackSelector = new CustomTrackSelector(trackSelectionFactory);
final LoadControl loadControl = new LoadController(context);
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(true);
simpleExoPlayer.setPlayWhenReady(playOnReady);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
audioReactor = new AudioReactor(context, simpleExoPlayer);
@ -237,15 +237,16 @@ public abstract class BasePlayer implements
final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch());
// Good to go...
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*playOnInit=*/true);
}
protected void initPlayback(@NonNull final PlayQueue queue,
@Player.RepeatMode final int repeatMode,
final float playbackSpeed,
final float playbackPitch) {
final float playbackPitch,
final boolean playOnReady) {
destroyPlayer();
initPlayer();
initPlayer(playOnReady);
setRepeatMode(repeatMode);
setPlaybackParameters(playbackSpeed, playbackPitch);
@ -518,15 +519,16 @@ public abstract class BasePlayer implements
}
public void triggerProgressUpdate() {
if (simpleExoPlayer == null) return;
onUpdateProgress(
(int) simpleExoPlayer.getCurrentPosition(),
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
(int) simpleExoPlayer.getDuration(),
simpleExoPlayer.getBufferedPercentage()
);
}
private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> triggerProgressUpdate());
}
@ -553,8 +555,8 @@ public abstract class BasePlayer implements
// Ensure dynamic/livestream timeline changes does not cause negative position
if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
"clamping position to 0ms.");
seekTo(/*clampToTime=*/0);
"clamping to default position.");
seekToDefault();
}
break;
}
@ -640,12 +642,12 @@ public abstract class BasePlayer implements
seekTo(recoveryPositionMillis);
playQueue.unsetRecovery(currentSourceIndex);
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
} else if (isSynchronizing && isLive()) {
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
// Is still synchronizing?
seekToDefault();
} else if (isSynchronizing && presetStartPositionMillis != 0L) {
} else if (isSynchronizing && presetStartPositionMillis > 0L) {
if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
"position=[" + presetStartPositionMillis + "]");
// Has another start position?
@ -700,41 +702,23 @@ public abstract class BasePlayer implements
}
}
/**
* Processes {@link ExoPlaybackException} tagged with {@link ExoPlaybackException#TYPE_SOURCE}.
* <br><br>
* If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid,
* then we know the error is produced by transitioning into a bad window, therefore we report
* an error to the play queue based on if the current error can be skipped.
* <br><br>
* This is done because ExoPlayer reports the source exceptions before window is
* transitioned on seamless playback. Because player error causes ExoPlayer to go
* back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source
* again to resume playback.
* <br><br>
* In the event that this error is produced during a valid stream playback, we save the
* current position so the playback may be recovered and resumed manually by the user. This
* happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete.
* <br><br>
* In the event of livestreaming being lagged behind for any reason, most notably pausing for
* too long, a {@link BehindLiveWindowException} will be produced. This will trigger a reload
* instead of skipping or removal.
* */
private void processSourceError(final IOException error) {
if (simpleExoPlayer == null || playQueue == null) return;
if (simpleExoPlayer.getCurrentPosition() <
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
setRecovery();
}
setRecovery();
final Throwable cause = error.getCause();
if (cause instanceof BehindLiveWindowException) {
reload();
} else if (cause instanceof UnknownHostException) {
playQueue.error(/*isNetworkProblem=*/true);
} else if (isCurrentWindowValid()) {
playQueue.error(/*isTransitioningToBadStream=*/true);
} else if (cause instanceof FailedMediaSource.MediaSourceResolutionException) {
playQueue.error(/*recoverableWithNoAvailableStream=*/false);
} else if (cause instanceof FailedMediaSource.StreamInfoLoadException) {
playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false);
} else {
playQueue.error(isCurrentWindowValid());
playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true);
}
}
@ -787,9 +771,10 @@ public abstract class BasePlayer implements
//////////////////////////////////////////////////////////////////////////*/
@Override
public boolean isNearPlaybackEdge(final long timeToEndMillis) {
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
// If live, then not near playback edge
if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false;
// If not playing, then not approaching playback edge
if (simpleExoPlayer == null || isLive() || !isPlaying()) return false;
final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
final long currentDurationMillis = simpleExoPlayer.getDuration();
@ -985,22 +970,22 @@ public abstract class BasePlayer implements
public void onFastRewind() {
if (DEBUG) Log.d(TAG, "onFastRewind() called");
seekBy(-FAST_FORWARD_REWIND_AMOUNT);
seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS);
}
public void onFastForward() {
if (DEBUG) Log.d(TAG, "onFastForward() called");
seekBy(FAST_FORWARD_REWIND_AMOUNT);
seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS);
}
public void onPlayPrevious() {
if (simpleExoPlayer == null || playQueue == null) return;
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds,
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds,
* restart current track. Also restart the track if the current track
* is the first in a queue.*/
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT ||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS ||
playQueue.getIndex() == 0) {
seekToDefault();
playQueue.offsetIndex(0);
@ -1050,7 +1035,9 @@ public abstract class BasePlayer implements
}
public void seekToDefault() {
if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition();
if (simpleExoPlayer != null) {
simpleExoPlayer.seekToDefaultPosition();
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -1091,9 +1078,9 @@ public abstract class BasePlayer implements
private void savePlaybackState() {
if (simpleExoPlayer == null || currentInfo == null) return;
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD &&
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
simpleExoPlayer.getCurrentPosition() <
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
}
}
@ -1127,9 +1114,7 @@ public abstract class BasePlayer implements
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
public boolean isLiveEdge() {
if (simpleExoPlayer == null) return false;
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
if (!isLive) return false;
if (simpleExoPlayer == null || !isLive()) return false;
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
@ -1143,6 +1128,16 @@ public abstract class BasePlayer implements
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
}
public boolean isLive() {
if (simpleExoPlayer == null) return false;
try {
return simpleExoPlayer.isCurrentWindowDynamic();
} catch (@NonNull IndexOutOfBoundsException ignored) {
// Why would this even happen =(
return false;
}
}
public boolean isPlaying() {
final int state = simpleExoPlayer.getPlaybackState();
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
@ -1170,10 +1165,6 @@ public abstract class BasePlayer implements
setPlaybackParameters(speed, getPlaybackPitch());
}
public void setPlaybackPitch(float pitch) {
setPlaybackParameters(getPlaybackSpeed(), pitch);
}
public PlaybackParameters getPlaybackParameters() {
final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f);
if (simpleExoPlayer == null) return defaultParameters;

View File

@ -30,8 +30,10 @@ import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
@ -59,7 +61,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
@ -95,12 +96,12 @@ public final class MainVideoPlayer extends AppCompatActivity
private GestureDetector gestureDetector;
private boolean activityPaused;
private VideoPlayerImpl playerImpl;
private SharedPreferences defaultPreferences;
@Nullable private StateSaver.SavedState savedState;
@Nullable private PlayerState playerState;
private boolean isInMultiWindow;
/*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle
@ -135,8 +136,9 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called");
super.onRestoreInstanceState(bundle);
savedState = StateSaver.tryToRestore(bundle, this);
StateSaver.tryToRestore(bundle, this);
}
@Override
@ -148,26 +150,28 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override
protected void onResume() {
super.onResume();
if (DEBUG) Log.d(TAG, "onResume() called");
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying()
&& !playerImpl.isPlaying()) {
playerImpl.onPlay();
}
activityPaused = false;
super.onResume();
if(globalScreenOrientationLocked()) {
boolean lastOrientationWasLandscape
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false);
if (globalScreenOrientationLocked()) {
boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
getString(R.string.last_orientation_landscape_key), false);
setLandscape(lastOrientationWasLandscape);
}
}
@Override
public void onBackPressed() {
if (DEBUG) Log.d(TAG, "onBackPressed() called");
super.onBackPressed();
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false);
// Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
// since the first onResume needs to restore the player.
// Subsequent onResume calls while multiwindow mode remains the same and the player is
// prepared should be ignored.
if (isInMultiWindow) return;
isInMultiWindow = isInMultiWindow();
if (playerState != null) {
playerImpl.setPlaybackQuality(playerState.getPlaybackQuality());
playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(),
playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(),
playerState.wasPlaying());
}
}
@Override
@ -180,33 +184,24 @@ public final class MainVideoPlayer extends AppCompatActivity
}
}
@Override
protected void onPause() {
super.onPause();
if (DEBUG) Log.d(TAG, "onPause() called");
if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) {
playerImpl.wasPlaying = playerImpl.isPlaying();
playerImpl.onPause();
}
activityPaused = true;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
super.onSaveInstanceState(outState);
if (playerImpl == null) return;
playerImpl.setRecovery();
savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState,
outState, this);
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
playerImpl.getPlaybackQuality(), playerImpl.isPlaying());
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (DEBUG) Log.d(TAG, "onDestroy() called");
if (playerImpl != null) playerImpl.destroy();
protected void onStop() {
if (DEBUG) Log.d(TAG, "onStop() called");
super.onStop();
playerImpl.destroy();
}
/*//////////////////////////////////////////////////////////////////////////
@ -221,48 +216,19 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override
public void writeTo(Queue<Object> objectsToSave) {
if (objectsToSave == null) return;
objectsToSave.add(playerImpl.getPlayQueue());
objectsToSave.add(playerImpl.getRepeatMode());
objectsToSave.add(playerImpl.getPlaybackSpeed());
objectsToSave.add(playerImpl.getPlaybackPitch());
objectsToSave.add(playerImpl.getPlaybackQuality());
objectsToSave.add(playerState);
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
@NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll();
final int repeatMode = (int) savedObjects.poll();
final float playbackSpeed = (float) savedObjects.poll();
final float playbackPitch = (float) savedObjects.poll();
@NonNull final String playbackQuality = (String) savedObjects.poll();
playerImpl.setPlaybackQuality(playbackQuality);
playerImpl.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
StateSaver.onDestroy(savedState);
public void readFrom(@NonNull Queue<Object> savedObjects) {
playerState = (PlayerState) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
// View
//////////////////////////////////////////////////////////////////////////*/
/**
* Prior to Kitkat, hiding system ui causes the player view to be overlaid and require two
* clicks to get rid of that invisible overlay. By showing the system UI on actions/events,
* that overlay is removed and the player view is put to the foreground.
*
* Post Kitkat, navbar and status bar can be pulled out by swiping the edge of
* screen, therefore, we can do nothing or hide the UI on actions/events.
* */
private void changeSystemUi() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
showSystemUi();
} else {
hideSystemUi();
}
}
private void showSystemUi() {
if (DEBUG) Log.d(TAG, "showSystemUi() called");
if (playerImpl != null && playerImpl.queueVisible) return;
@ -275,6 +241,14 @@ public final class MainVideoPlayer extends AppCompatActivity
} else {
visibility = View.STATUS_BAR_VISIBLE;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@ColorInt final int systenUiColor =
ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color);
getWindow().setStatusBarColor(systenUiColor);
getWindow().setNavigationBarColor(systenUiColor);
}
getWindow().getDecorView().setSystemUiVisibility(visibility);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
@ -342,6 +316,10 @@ public final class MainVideoPlayer extends AppCompatActivity
}
}
private boolean isInMultiWindow() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode();
}
////////////////////////////////////////////////////////////////////////////
// Playback Parameters Listener
////////////////////////////////////////////////////////////////////////////
@ -411,15 +389,6 @@ public final class MainVideoPlayer extends AppCompatActivity
this.itemsListCloseButton = findViewById(R.id.playQueueClose);
this.itemsList = findViewById(R.id.playQueue);
this.windowRootLayout = rootView.findViewById(R.id.playbackWindowRoot);
// Prior to Kitkat, there is no way of setting translucent navbar programmatically.
// Thus, fit system windows is opted instead.
// See https://stackoverflow.com/questions/29069070/completely-transparent-status-bar-and-navigation-bar-on-lollipop
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
windowRootLayout.setFitsSystemWindows(false);
windowRootLayout.invalidate();
}
titleTextView.setSelected(true);
channelTextView.setSelected(true);
@ -727,7 +696,7 @@ public final class MainVideoPlayer extends AppCompatActivity
animatePlayButtons(true, 200);
});
changeSystemUi();
showSystemUi();
getRootView().setKeepScreenOn(false);
}
@ -900,7 +869,7 @@ public final class MainVideoPlayer extends AppCompatActivity
playerImpl.hideControls(150, 0);
} else {
playerImpl.showControlsThenHide();
changeSystemUi();
showSystemUi();
}
return true;
}

View File

@ -0,0 +1,88 @@
package org.schabi.newpipe.player;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import org.schabi.newpipe.playlist.PlayQueue;
import java.io.Serializable;
public class PlayerState implements Serializable {
private final static String TAG = "PlayerState";
@NonNull private final PlayQueue playQueue;
private final int repeatMode;
private final float playbackSpeed;
private final float playbackPitch;
@Nullable private final String playbackQuality;
private final boolean wasPlaying;
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) {
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying);
}
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch,
@Nullable final String playbackQuality, final boolean wasPlaying) {
this.playQueue = playQueue;
this.repeatMode = repeatMode;
this.playbackSpeed = playbackSpeed;
this.playbackPitch = playbackPitch;
this.playbackQuality = playbackQuality;
this.wasPlaying = wasPlaying;
}
/*//////////////////////////////////////////////////////////////////////////
// Serdes
//////////////////////////////////////////////////////////////////////////*/
@Nullable
public static PlayerState fromJson(@NonNull final String json) {
try {
return new Gson().fromJson(json, PlayerState.class);
} catch (JsonSyntaxException error) {
Log.e(TAG, "Failed to deserialize PlayerState from json=[" + json + "]", error);
return null;
}
}
@NonNull
public String toJson() {
return new Gson().toJson(this);
}
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
@NonNull
public PlayQueue getPlayQueue() {
return playQueue;
}
public int getRepeatMode() {
return repeatMode;
}
public float getPlaybackSpeed() {
return playbackSpeed;
}
public float getPlaybackPitch() {
return playbackPitch;
}
@Nullable
public String getPlaybackQuality() {
return playbackQuality;
}
public boolean wasPlaying() {
return wasPlaying;
}
}

View File

@ -32,6 +32,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueAdapter;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
@ -40,6 +41,9 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
@ -151,7 +155,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
finish();
return true;
case R.id.action_append_playlist:
appendToPlaylist();
appendAllToPlaylist();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
@ -187,13 +191,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
private void appendToPlaylist() {
if (this.player != null && this.player.getPlayQueue() != null) {
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
.show(getSupportFragmentManager(), getTag());
}
}
////////////////////////////////////////////////////////////////////////////
// Service Connection
////////////////////////////////////////////////////////////////////////////
@ -319,7 +316,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, R.string.play_queue_remove);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/0,
Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
@ -328,12 +326,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
});
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail);
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> {
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
return true;
});
final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2,
Menu.NONE, R.string.append_playlist);
append.setOnMenuItemClickListener(menuItem -> {
openPlaylistAppendDialog(Collections.singletonList(item));
return true;
});
menu.show();
}
@ -488,6 +494,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
seeking = false;
}
////////////////////////////////////////////////////////////////////////////
// Playlist append
////////////////////////////////////////////////////////////////////////////
private void appendAllToPlaylist() {
if (player != null && player.getPlayQueue() != null) {
openPlaylistAppendDialog(player.getPlayQueue().getStreams());
}
}
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
PlaylistAppendDialog.fromPlayQueueItems(playlist)
.show(getSupportFragmentManager(), getTag());
}
////////////////////////////////////////////////////////////////////////////
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@ -497,6 +518,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
}
@Override
@ -609,4 +631,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
playbackPitchButton.setText(formatPitch(parameters.pitch));
}
}
private void onMaybePlaybackAdapterChanged() {
if (itemsList == null || player == null) return;
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
itemsList.setAdapter(maybeNewAdapter);
}
}
}

View File

@ -228,8 +228,8 @@ public abstract class VideoPlayer extends BasePlayer
}
@Override
public void initPlayer() {
super.initPlayer();
public void initPlayer(final boolean playOnReady) {
super.initPlayer(playOnReady);
// Setup video view
simpleExoPlayer.setVideoSurfaceView(surfaceView);

View File

@ -1,8 +1,12 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.session.MediaButtonReceiver;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
@ -15,8 +19,8 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager {
private static final String TAG = "MediaSessionManager";
private final MediaSessionCompat mediaSession;
private final MediaSessionConnector sessionConnector;
@NonNull private final MediaSessionCompat mediaSession;
@NonNull private final MediaSessionConnector sessionConnector;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@ -28,11 +32,9 @@ public class MediaSessionManager {
this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer());
}
public MediaSessionCompat getMediaSession() {
return mediaSession;
}
public MediaSessionConnector getSessionConnector() {
return sessionConnector;
@Nullable
@SuppressWarnings("UnusedReturnValue")
public KeyEvent handleMediaButtonIntent(final Intent intent) {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
}

View File

@ -7,7 +7,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
@ -203,6 +207,16 @@ public class PlayerHelper {
return 60000;
}
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
@NonNull final BandwidthMeter meter) {
return new AdaptiveTrackSelection.Factory(meter,
AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE,
/*bufferDurationRequiredForQualityIncrease=*/1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
}
public static boolean isUsingDSP(@NonNull final Context context) {
return true;
}

View File

@ -14,13 +14,35 @@ import java.io.IOException;
public class FailedMediaSource implements ManagedMediaSource {
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
public static class FailedMediaSourceException extends Exception {
FailedMediaSourceException(String message) {
super(message);
}
FailedMediaSourceException(Throwable cause) {
super(cause);
}
}
public static final class MediaSourceResolutionException extends FailedMediaSourceException {
public MediaSourceResolutionException(String message) {
super(message);
}
}
public static final class StreamInfoLoadException extends FailedMediaSourceException {
public StreamInfoLoadException(Throwable cause) {
super(cause);
}
}
private final PlayQueueItem playQueueItem;
private final Throwable error;
private final FailedMediaSourceException error;
private final long retryTimestamp;
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error,
@NonNull final FailedMediaSourceException error,
final long retryTimestamp) {
this.playQueueItem = playQueueItem;
this.error = error;
@ -32,7 +54,7 @@ public class FailedMediaSource implements ManagedMediaSource {
* The error will always be propagated to ExoPlayer.
* */
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final Throwable error) {
@NonNull final FailedMediaSourceException error) {
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = Long.MAX_VALUE;
@ -42,7 +64,7 @@ public class FailedMediaSource implements ManagedMediaSource {
return playQueueItem;
}
public Throwable getError() {
public FailedMediaSourceException getError() {
return error;
}

View File

@ -0,0 +1,135 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
public class ManagedMediaSourcePlaylist {
@NonNull private final DynamicConcatenatingMediaSource internalSource;
public ManagedMediaSourcePlaylist() {
internalSource = new DynamicConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
new ShuffleOrder.UnshuffledShuffleOrder(0));
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Delegations
//////////////////////////////////////////////////////////////////////////*/
public int size() {
return internalSource.getSize();
}
/**
* Returns the {@link ManagedMediaSource} at the given index of the playlist.
* If the index is invalid, then null is returned.
* */
@Nullable
public ManagedMediaSource get(final int index) {
return (index < 0 || index >= size()) ?
null : (ManagedMediaSource) internalSource.getMediaSource(index);
}
public void dispose() {
internalSource.releaseSource();
}
@NonNull
public DynamicConcatenatingMediaSource getParentMediaSource() {
return internalSource;
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist Manipulation
//////////////////////////////////////////////////////////////////////////*/
/**
* Expands the {@link DynamicConcatenatingMediaSource} by appending it with a
* {@link PlaceholderMediaSource}.
*
* @see #append(ManagedMediaSource)
* */
public synchronized void expand() {
append(new PlaceholderMediaSource());
}
/**
* Appends a {@link ManagedMediaSource} to the end of {@link DynamicConcatenatingMediaSource}.
* @see DynamicConcatenatingMediaSource#addMediaSource
* */
public synchronized void append(@NonNull final ManagedMediaSource source) {
internalSource.addMediaSource(source);
}
/**
* Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource}
* at the given index. If this index is out of bound, then the removal is ignored.
* @see DynamicConcatenatingMediaSource#removeMediaSource(int)
* */
public synchronized void remove(final int index) {
if (index < 0 || index > internalSource.getSize()) return;
internalSource.removeMediaSource(index);
}
/**
* Moves a {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
* @see DynamicConcatenatingMediaSource#moveMediaSource(int, int)
* */
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= internalSource.getSize() || target >= internalSource.getSize()) return;
internalSource.moveMediaSource(source, target);
}
/**
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
* with a {@link PlaceholderMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* */
public synchronized void invalidate(final int index,
@Nullable final Runnable finalizingAction) {
if (get(index) instanceof PlaceholderMediaSource) return;
update(index, new PlaceholderMediaSource(), finalizingAction);
}
/**
* Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}.
* @see #update(int, ManagedMediaSource, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source) {
update(index, source, /*doNothing=*/null);
}
/**
* Updates the {@link ManagedMediaSource} in {@link DynamicConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
* then the replacement is ignored.
* @see DynamicConcatenatingMediaSource#addMediaSource
* @see DynamicConcatenatingMediaSource#removeMediaSource(int, Runnable)
* */
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= internalSource.getSize()) return;
// Add and remove are sequential on the same thread, therefore here, the exoplayer
// message queue must receive and process add before remove, effectively treating them
// as atomic.
// Since the finalizing action occurs strictly after the timeline has completed
// all its changes on the playback thread, thus, it is possible, in the meantime,
// other calls that modifies the playlist media source occur in between. This makes
// it unsafe to call remove as the finalizing action of add.
internalSource.addMediaSource(index + 1, source);
// Because of the above race condition, it is thus only safe to synchronize the player
// in the finalizing action AFTER the removal is complete and the timeline has changed.
internalSource.removeMediaSource(index, finalizingAction);
}
}

View File

@ -2,11 +2,11 @@ package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@ -14,6 +14,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
@ -23,8 +24,10 @@ import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@ -37,8 +40,11 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager {
@ -52,7 +58,6 @@ public class MediaSourceManager {
* streams before will only be cached for future usage.
*
* @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource)
* @see #update(int, MediaSource, Runnable)
* */
private final static int WINDOW_SIZE = 1;
@ -103,7 +108,7 @@ public class MediaSourceManager {
@NonNull private final AtomicBoolean isBlocked;
@NonNull private DynamicConcatenatingMediaSource sources;
@NonNull private ManagedMediaSourcePlaylist playlist;
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
@ -143,9 +148,9 @@ public class MediaSourceManager {
this.isBlocked = new AtomicBoolean(false);
this.sources = new DynamicConcatenatingMediaSource();
this.playlist = new ManagedMediaSourcePlaylist();
this.loadingItems = Collections.synchronizedSet(new HashSet<>());
this.loadingItems = Collections.synchronizedSet(new ArraySet<>());
playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread())
@ -167,7 +172,7 @@ public class MediaSourceManager {
playQueueReactor.cancel();
loaderReactor.dispose();
syncReactor.dispose();
sources.releaseSource();
playlist.dispose();
}
/*//////////////////////////////////////////////////////////////////////////
@ -215,17 +220,18 @@ public class MediaSourceManager {
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex());
playlist.remove(removeEvent.getRemoveIndex());
break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) event;
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
playlist.move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case REORDER:
// Need to move to ensure the playing index from play queue matches that of
// the source timeline, and then window correction can take care of the rest
final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
playlist.move(reorderEvent.getFromSelectedIndex(),
reorderEvent.getToSelectedIndex());
break;
case RECOVERY:
default:
@ -266,10 +272,11 @@ public class MediaSourceManager {
}
private boolean isPlaybackReady() {
if (sources.getSize() != playQueue.size()) return false;
if (playlist.size() != playQueue.size()) return false;
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
if (mediaSource == null) return false;
final ManagedMediaSource mediaSource =
(ManagedMediaSource) sources.getMediaSource(playQueue.getIndex());
final PlayQueueItem playQueueItem = playQueue.getItem();
return mediaSource.isStreamEqual(playQueueItem);
}
@ -288,9 +295,9 @@ public class MediaSourceManager {
private void maybeUnblock() {
if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) {
if (isBlocked.get()) {
isBlocked.set(false);
playbackListener.onPlaybackUnblock(sources);
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
}
}
@ -299,10 +306,10 @@ public class MediaSourceManager {
//////////////////////////////////////////////////////////////////////////*/
private void maybeSync() {
if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called.");
if (DEBUG) Log.d(TAG, "maybeSync() called.");
final PlayQueueItem currentItem = playQueue.getItem();
if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return;
if (isBlocked.get() || currentItem == null) return;
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
@ -321,9 +328,11 @@ public class MediaSourceManager {
}
}
private void maybeSynchronizePlayer() {
maybeUnblock();
maybeSync();
private synchronized void maybeSynchronizePlayer() {
if (isPlayQueueReady() && isPlaybackReady()) {
maybeUnblock();
maybeSync();
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -332,12 +341,14 @@ public class MediaSourceManager {
private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
.filter(ignored ->
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
}
private Disposable getDebouncedLoader() {
return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate());
}
@ -348,42 +359,21 @@ public class MediaSourceManager {
private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE);
if (itemsToLoad == null) return;
// Evict the items being loaded to free up memory
if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
}
maybeLoadItem(currentItem);
// Evict the previous items being loaded to free up memory, before start loading new ones
maybeClearLoaders();
// The rest are just for seamless playback
// Although timeline is not updated prior to the current index, these sources are still
// loaded into the cache for faster retrieval at a potentially later time.
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 Set<PlayQueueItem> items = new HashSet<>(
playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin
final int excess = rightLimit - playQueue.size();
if (excess >= 0) {
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
}
items.remove(currentItem);
for (final PlayQueueItem item : items) {
maybeLoadItem(itemsToLoad.center);
for (final PlayQueueItem item : itemsToLoad.neighbors) {
maybeLoadItem(item);
}
}
private void maybeLoadItem(@NonNull final PlayQueueItem item) {
if (DEBUG) Log.d(TAG, "maybeLoadItem() called.");
if (playQueue.indexOf(item) >= sources.getSize()) return;
if (playQueue.indexOf(item) >= playlist.size()) return;
if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() +
@ -402,19 +392,19 @@ public class MediaSourceManager {
return stream.getStream().map(streamInfo -> {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) {
final Exception exception = new IllegalStateException(
"Unable to resolve source from stream info." +
" URL: " + stream.getUrl() +
", audio count: " + streamInfo.getAudioStreams().size() +
", video count: " + streamInfo.getVideoOnlyStreams().size() +
streamInfo.getVideoStreams().size());
return new FailedMediaSource(stream, exception);
final String message = "Unable to resolve source from stream info." +
" URL: " + stream.getUrl() +
", audio count: " + streamInfo.getAudioStreams().size() +
", video count: " + streamInfo.getVideoOnlyStreams().size() +
streamInfo.getVideoStreams().size();
return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
}
final long expiration = System.currentTimeMillis() +
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
}).onErrorReturn(throwable -> new FailedMediaSource(stream,
new StreamInfoLoadException(throwable)));
}
private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
@ -426,10 +416,10 @@ public class MediaSourceManager {
final int itemIndex = playQueue.indexOf(item);
// Only update the playlist timeline for items at the current index or after.
if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) {
if (isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
}
}
@ -445,10 +435,8 @@ public class MediaSourceManager {
* */
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
final int index = playQueue.indexOf(item);
if (index == -1 || index >= sources.getSize()) return false;
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
return mediaSource.shouldBeReplacedWith(item,
final ManagedMediaSource mediaSource = playlist.get(index);
return mediaSource != null && mediaSource.shouldBeReplacedWith(item,
/*mightBeInProgress=*/index != playQueue.getIndex());
}
@ -465,10 +453,9 @@ public class MediaSourceManager {
* */
private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex();
if (sources.getSize() <= currentIndex) return;
final ManagedMediaSource currentSource = playlist.get(currentIndex);
if (currentSource == null) return;
final ManagedMediaSource currentSource =
(ManagedMediaSource) sources.getMediaSource(currentIndex);
final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
maybeSynchronizePlayer();
@ -477,7 +464,16 @@ public class MediaSourceManager {
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate);
playlist.invalidate(currentIndex, this::loadImmediate);
}
private void maybeClearLoaders() {
if (DEBUG) Log.d(TAG, "MediaSource - maybeClearLoaders() called.");
if (!loadingItems.contains(playQueue.getItem()) &&
loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
}
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers
@ -486,72 +482,55 @@ public class MediaSourceManager {
private void resetSources() {
if (DEBUG) Log.d(TAG, "resetSources() called.");
this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource(false,
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0));
playlist.dispose();
playlist = new ManagedMediaSourcePlaylist();
}
private void populateSources() {
if (DEBUG) Log.d(TAG, "populateSources() called.");
if (sources.getSize() >= playQueue.size()) return;
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
emplace(index, new PlaceholderMediaSource());
while (playlist.size() < playQueue.size()) {
playlist.expand();
}
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Manipulation
// Manager Helpers
//////////////////////////////////////////////////////////////////////////*/
@Nullable
private static ItemsToLoad getItemsToLoad(@NonNull final PlayQueue playQueue,
final int windowSize) {
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return null;
/**
* Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource}
* with position in respect to the play queue only if no {@link MediaSource}
* already exists at the given index.
* */
private synchronized void emplace(final int index, @NonNull final MediaSource source) {
if (index < sources.getSize()) return;
// The rest are just for seamless playback
// Although timeline is not updated prior to the current index, these sources are still
// loaded into the cache for faster retrieval at a potentially later time.
final int leftBound = Math.max(0, currentIndex - windowSize);
final int rightLimit = currentIndex + windowSize + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final Set<PlayQueueItem> neighbors = new ArraySet<>(
playQueue.getStreams().subList(leftBound,rightBound));
sources.addMediaSource(index, source);
// Do a round robin
final int excess = rightLimit - playQueue.size();
if (excess >= 0) {
neighbors.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
}
neighbors.remove(currentItem);
return new ItemsToLoad(currentItem, neighbors);
}
/**
* Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource}
* at the given index. If this index is out of bound, then the removal is ignored.
* */
private synchronized void remove(final int index) {
if (index < 0 || index > sources.getSize()) return;
private static class ItemsToLoad {
@NonNull final private PlayQueueItem center;
@NonNull final private Collection<PlayQueueItem> neighbors;
sources.removeMediaSource(index);
}
/**
* Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
* */
private synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
sources.moveMediaSource(source, target);
}
/**
* 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 may cause desynchronization
* on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}.
* */
private synchronized void update(final int index, @NonNull final MediaSource source,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= sources.getSize()) return;
sources.addMediaSource(index + 1, source, () ->
sources.removeMediaSource(index, finalizingAction));
ItemsToLoad(@NonNull final PlayQueueItem center,
@NonNull final Collection<PlayQueueItem> neighbors) {
this.center = center;
this.neighbors = neighbors;
}
}
}

View File

@ -13,13 +13,13 @@ import java.util.List;
public interface PlaybackListener {
/**
* Called to check if the currently playing stream is close to the end of its playback.
* Implementation should return true when the current playback position is within
* timeToEndMillis or less until its playback completes or transitions.
* Called to check if the currently playing stream is approaching the end of its playback.
* Implementation should return true when the current playback position is progressing within
* timeToEndMillis or less to its playback during.
*
* May be called at any time.
* */
boolean isNearPlaybackEdge(final long timeToEndMillis);
boolean isApproachingPlaybackEdge(final long timeToEndMillis);
/**
* Called when the stream at the current queue index is not ready yet.

View File

@ -3,5 +3,5 @@
<gradient
android:angle="90"
android:endColor="#00000000"
android:startColor="#8c000000"/>
android:startColor="@color/video_overlay_color"/>
</shape>

View File

@ -3,5 +3,5 @@
<gradient
android:angle="-90"
android:endColor="#00000000"
android:startColor="#8c000000"/>
android:startColor="@color/video_overlay_color"/>
</shape>

View File

@ -304,7 +304,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center"
android:text="@string/duration_live"
android:text="@string/duration_live_button"
android:textAllCaps="true"
android:textColor="?attr/colorAccent"
android:maxLength="4"

View File

@ -129,7 +129,7 @@
android:id="@+id/playbackControlRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#64000000"
android:background="@color/video_overlay_color"
android:visibility="gone"
tools:visibility="visible">
@ -406,7 +406,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center"
android:text="@string/duration_live"
android:text="@string/duration_live_button"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:maxLength="4"

View File

@ -154,7 +154,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center"
android:text="@string/duration_live"
android:text="@string/duration_live_button"
android:textAllCaps="true"
android:textColor="?attr/colorAccent"
android:maxLength="4"

View File

@ -198,7 +198,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center_vertical"
android:text="@string/duration_live"
android:text="@string/duration_live_button"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:maxLength="4"

View File

@ -40,7 +40,7 @@
<color name="playlist_stream_count_background_color">#e6000000</color>
<color name="duration_text_color">#EEFFFFFF</color>
<color name="playlist_stream_count_text_color">#ffffff</color>
<color name="video_overlay_color">#66000000</color>
<color name="video_overlay_color">#64000000</color>
<color name="background_notification_color">#323232</color>
<color name="background_title_color">#ffffff</color>

View File

@ -119,6 +119,7 @@
<string name="show_age_restricted_content_title">Show age restricted content</string>
<string name="video_is_age_restricted">Age Restricted Video. Allowing such material is possible from Settings.</string>
<string name="duration_live">live</string>
<string name="duration_live_button" translatable="false">LIVE</string>
<string name="downloads">Downloads</string>
<string name="downloads_title">Downloads</string>
<string name="error_report_title">Error report</string>