Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener;
Expand Down Expand Up @@ -1166,8 +1167,12 @@ private void openMainPlayer() {
final PlayQueue queue = setupPlayQueueForIntent(false);
tryAddVideoPlayerView();

final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
PlayerService.class, queue, true, autoPlayEnabled);
final Context context = requireContext();
final Intent playerIntent =
NavigationHelper.getPlayerIntent(context, PlayerService.class, queue,
PlayerIntentType.AllOthers)
.putExtra(Player.PLAY_WHEN_READY, autoPlayEnabled)
.putExtra(Player.RESUME_PLAYBACK, true);
ContextCompat.startForegroundService(activity, playerIntent);
}

Expand Down
217 changes: 161 additions & 56 deletions app/src/main/java/org/schabi/newpipe/player/Player.java

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions app/src/main/java/org/schabi/newpipe/player/PlayerIntentType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.schabi.newpipe.player

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

// We model this as an enum class plus one struct for each enum value
// so we can consume it from Java properly. After converting to Kotlin,
// we could switch to a sealed enum class & a proper Kotlin `when` match.

@Parcelize
enum class PlayerIntentType : Parcelable {
Enqueue,
EnqueueNext,
TimestampChange,
AllOthers
}
Comment on lines +10 to +16
Copy link
Member

@Isira-Seneviratne Isira-Seneviratne Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enums are serializable, so they can be passed to/retrieved from intents/bundles as serializable objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what’s the difference? I don’t see any.

Copy link
Member

@Isira-Seneviratne Isira-Seneviratne Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some suggestions with the relevant changes, so the Parcelable implementation can be removed for this enum.

Copy link
Member

@Isira-Seneviratne Isira-Seneviratne Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Parcelize
enum class PlayerIntentType : Parcelable {
Enqueue,
EnqueueNext,
TimestampChange,
AllOthers
}
enum class PlayerIntentType {
Enqueue,
EnqueueNext,
TimestampChange,
AllOthers
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but why? I don’t see a big difference and like Parcellable more than Serializable


/**
* A timestamp on the given was clicked and we should switch the playing stream to it.
*/
@Parcelize
data class TimestampChangeData(
val serviceId: Int,
val url: String,
val seconds: Int
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ public int onStartCommand(final Intent intent, final int flags, final int startI
}

if (player != null) {
final PlayerType oldPlayerType = player.getPlayerType();
player.handleIntent(intent);
player.handleIntentPost(oldPlayerType);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package org.schabi.newpipe.player.helper;

import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
Expand All @@ -25,7 +22,6 @@
import androidx.preference.PreferenceManager;

import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
Expand Down Expand Up @@ -410,23 +406,9 @@ private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
return singlePlayQueue;
}


// endregion
// region Utils used by player

@RepeatMode
public static int nextRepeatMode(@RepeatMode final int repeatMode) {
switch (repeatMode) {
case REPEAT_MODE_OFF:
return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL:
default:
return REPEAT_MODE_OFF;
}
}

@ResizeMode
public static int retrieveResizeModeFromPrefs(final Player player) {
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper;

Expand Down Expand Up @@ -254,7 +255,9 @@ private Intent getIntentForNotification() {
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
final Intent intent = NavigationHelper.getPlayerIntent(
player.getContext(), MainActivity.class, null, true);
player.getContext(), MainActivity.class, null,
PlayerIntentType.AllOthers);
intent.putExtra(Player.RESUME_PLAYBACK, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,22 @@ public synchronized void append(@NonNull final List<PlayQueueItem> items) {
broadcast(new AppendEvent(itemList.size()));
}

/**
* Add the given item after the current stream.
*
* @param item item to add.
* @param skipIfSame if set, skip adding if the next stream is the same stream.
*/
public void enqueueNext(@NonNull final PlayQueueItem item, final boolean skipIfSame) {
final int currentIndex = getIndex();
// if the next item is the same item as the one we want to enqueue, skip if flag is true
if (skipIfSame && item.isSameItem(getItem(currentIndex + 1))) {
return;
}
append(List.of(item));
move(size() - 1, currentIndex + 1);
}

/**
* Removes the item at the given index from the play queue.
* <p>
Expand Down Expand Up @@ -529,8 +545,7 @@ public boolean equalStreams(@Nullable final PlayQueue other) {
final PlayQueueItem stream = streams.get(i);
final PlayQueueItem otherStream = other.streams.get(i);
// Check is based on serviceId and URL
if (stream.getServiceId() != otherStream.getServiceId()
|| !stream.getUrl().equals(otherStream.getUrl())) {
if (!stream.isSameItem(otherStream)) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class PlayQueueItem implements Serializable {
private long recoveryPosition;
private Throwable error;

PlayQueueItem(@NonNull final StreamInfo info) {
public PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnails(), info.getUploaderName(),
info.getUploaderUrl(), info.getStreamType());
Expand Down Expand Up @@ -71,6 +71,22 @@ private PlayQueueItem(@Nullable final String name, @Nullable final String url,
this.recoveryPosition = RECOVERY_UNSET;
}

/** Whether these two items should be treated as the same stream
* for the sake of keeping the same player running when e.g. jumping between timestamps.
*
* @param other the {@link PlayQueueItem} to compare against.
* @return whether the two items are the same so the stream can be re-used.
*/
public boolean isSameItem(@Nullable final PlayQueueItem other) {
if (other == null) {
return false;
}
// We assume that the same service & URL uniquely determines
// that we can keep the same stream running.
return serviceId == other.serviceId
&& url.equals(other.url);
}

@NonNull
public String getTitle() {
return title;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public SinglePlayQueue(final StreamInfoItem item) {
public SinglePlayQueue(final StreamInfo info) {
super(0, List.of(new PlayQueueItem(info)));
}

public SinglePlayQueue(final PlayQueueItem item) {
super(0, List.of(item));
}
public SinglePlayQueue(final StreamInfo info, final long startPosition) {
super(0, List.of(new PlayQueueItem(info)));
getItem().setRecoveryPosition(startPosition);
Expand Down
64 changes: 34 additions & 30 deletions app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Parcelable;
import android.util.Log;
import android.widget.Toast;

Expand Down Expand Up @@ -57,8 +58,10 @@
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.PlayQueueActivity;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.TimestampChangeData;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
Expand All @@ -85,7 +88,7 @@ private NavigationHelper() {
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
final boolean resumePlayback) {
@NonNull final PlayerIntentType playerIntentType) {
final Intent intent = new Intent(context, targetClazz);

if (playQueue != null) {
Expand All @@ -95,44 +98,31 @@ public static <T> Intent getPlayerIntent(@NonNull final Context context,
}
}
intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent());
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true);
intent.putExtra(Player.PLAYER_INTENT_TYPE, (Parcelable) playerIntentType);

return intent;
}

@NonNull
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
final boolean resumePlayback,
final boolean playWhenReady) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(Player.PLAY_WHEN_READY, playWhenReady);
}
public static Intent getPlayerTimestampIntent(@NonNull final Context context,
@NonNull final TimestampChangeData
timestampChangeData) {
final Intent intent = new Intent(context, PlayerService.class);

@NonNull
public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue) {
// when enqueueing `resumePlayback` is always `false` since:
// - if there is a video already playing, the value of `resumePlayback` just doesn't make
// any difference.
// - if there is nothing already playing, it is useful for the enqueue action to have a
// slightly different behaviour than the normal play action: the latter resumes playback,
// the former doesn't. (note that enqueue can be triggered when nothing is playing only
// by long pressing the video detail fragment, playlist or channel controls
return getPlayerIntent(context, targetClazz, playQueue, false)
.putExtra(Player.ENQUEUE, true);
intent.putExtra(Player.PLAYER_INTENT_TYPE, (Parcelable) PlayerIntentType.TimestampChange);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
intent.putExtra(Player.PLAYER_INTENT_TYPE, (Parcelable) PlayerIntentType.TimestampChange);
intent.putExtra(Player.PLAYER_INTENT_TYPE, PlayerIntentType.TimestampChange);

intent.putExtra(Player.PLAYER_INTENT_DATA, timestampChangeData);

return intent;
}

@NonNull
public static <T> Intent getPlayerEnqueueNextIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue) {
// see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false
return getPlayerIntent(context, targetClazz, playQueue, false)
.putExtra(Player.ENQUEUE_NEXT, true);
return getPlayerIntent(context, targetClazz, playQueue, PlayerIntentType.EnqueueNext)
// see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false
.putExtra(Player.RESUME_PLAYBACK, false);
}

/* PLAY */
Expand Down Expand Up @@ -166,8 +156,10 @@ public static void playOnPopupPlayer(final Context context,

Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();

final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent());
final Intent intent = getPlayerIntent(context, PlayerService.class, queue,
PlayerIntentType.AllOthers);
intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent())
.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
ContextCompat.startForegroundService(context, intent);
}

Expand All @@ -177,8 +169,10 @@ public static void playOnBackgroundPlayer(final Context context,
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show();

final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
final Intent intent = getPlayerIntent(context, PlayerService.class, queue,
PlayerIntentType.AllOthers);
intent.putExtra(Player.PLAYER_TYPE, PlayerType.AUDIO.valueForIntent());
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
ContextCompat.startForegroundService(context, intent);
}

Expand All @@ -191,7 +185,17 @@ public static void enqueueOnPlayer(final Context context,
}

Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent(context, PlayerService.class, queue);

// when enqueueing `resumePlayback` is always `false` since:
// - if there is a video already playing, the value of `resumePlayback` just doesn't make
// any difference.
// - if there is nothing already playing, it is useful for the enqueue action to have a
// slightly different behaviour than the normal play action: the latter resumes playback,
// the former doesn't. (note that enqueue can be triggered when nothing is playing only
// by long pressing the video detail fragment, playlist or channel controls
final Intent intent = getPlayerIntent(context, PlayerService.class, queue,
PlayerIntentType.Enqueue)
.putExtra(Player.RESUME_PLAYBACK, false);

intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
ContextCompat.startForegroundService(context, intent);
Expand Down
Loading