diff --git a/app/build.gradle b/app/build.gradle
index e20535ee4..a0b4da510 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7447c81ed..1e55270be 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -42,7 +42,11 @@
+ android:exported="false">
+
+
+
+
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}.
- *
- * 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.
- *
- * 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.
- *
- * 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.
- *
- * 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;
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
index dbc34b11a..19621593c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
@@ -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 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 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 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;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
new file mode 100644
index 000000000..6f38ce835
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
@@ -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;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
index 239c9c8d3..ccaa6f225 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -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 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);
+ }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
index b019ea91e..0e0dca983 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -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);
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
index 8405e45fd..2611705a8 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
@@ -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);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index 63ac7e8a1..84c7a619b 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -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;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
index 5f029cc50..878d7c711 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
@@ -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;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java
new file mode 100644
index 000000000..310f1062b
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java
@@ -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);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
index 477358113..b4236d3c5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
@@ -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 onSuccess = info -> syncInternal(currentItem, info);
final Consumer 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 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 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 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 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.
- *
- * 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 neighbors) {
+ this.center = center;
+ this.neighbors = neighbors;
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
index 34c7702bc..daf58d5dd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
@@ -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.
diff --git a/app/src/main/res/drawable/player_controls_bg.xml b/app/src/main/res/drawable/player_controls_bg.xml
index 7e1981347..f250e3558 100644
--- a/app/src/main/res/drawable/player_controls_bg.xml
+++ b/app/src/main/res/drawable/player_controls_bg.xml
@@ -3,5 +3,5 @@
+ android:startColor="@color/video_overlay_color"/>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/player_top_controls_bg.xml b/app/src/main/res/drawable/player_top_controls_bg.xml
index f1e8b98fc..ba62ce863 100644
--- a/app/src/main/res/drawable/player_top_controls_bg.xml
+++ b/app/src/main/res/drawable/player_top_controls_bg.xml
@@ -3,5 +3,5 @@
+ android:startColor="@color/video_overlay_color"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml
index 11765f901..72f673ffc 100644
--- a/app/src/main/res/layout-land/activity_player_queue_control.xml
+++ b/app/src/main/res/layout-land/activity_player_queue_control.xml
@@ -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"
diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml
index c581c3203..616f93536 100644
--- a/app/src/main/res/layout/activity_main_player.xml
+++ b/app/src/main/res/layout/activity_main_player.xml
@@ -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"
diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml
index 7f649e382..e81a19553 100644
--- a/app/src/main/res/layout/activity_player_queue_control.xml
+++ b/app/src/main/res/layout/activity_player_queue_control.xml
@@ -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"
diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml
index 0c3ea77df..5e8ed664e 100644
--- a/app/src/main/res/layout/player_popup.xml
+++ b/app/src/main/res/layout/player_popup.xml
@@ -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"
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 6b43999a0..df2c73ec8 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -40,7 +40,7 @@
#e6000000
#EEFFFFFF
#ffffff
- #66000000
+ #64000000
#323232
#ffffff
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f22f42e95..33ce46412 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -119,6 +119,7 @@
Show age restricted content
Age Restricted Video. Allowing such material is possible from Settings.
live
+ LIVE
Downloads
Downloads
Error report