1
0
Fork 0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2025-10-03 01:39:38 +02:00

Compare commits

...

15 commits

Author SHA1 Message Date
Profpatsch
eb277fe14b Player/handleIntent: call handleIntentPost unconditionally
We always need to handleIntentPost otherwise the VideoDetailFragment
is not setup correctly.
2025-09-06 15:31:14 +02:00
Profpatsch
d77771a60c Player/handleIntent: fix enqueue if player not running
In 063dcd41e5 I falsely claimed that the
fallthrough case is always degenerate, but it kinda somehow still
worked because if you long-click on e.g. the popup button, it would
call enqueue, but if nothing was running yet it would fallthrough to
the very last case and start the player with the video.

So let’s return to that and add a TODO for further refactoring in the
future.
2025-09-06 15:09:11 +02:00
Profpatsch
01f9a3de33 Fix Checkstyle & remove unused fields 2025-09-06 15:09:11 +02:00
Profpatsch
150649aea9 Player/handleIntent: Don’t delete queue when clicking on timestamp
Fixes https://github.com/TeamNewPipe/NewPipe/issues/11013

We finally are at the point where we can have good logic around
clicking on timestamps.

This is pretty straightforward:

1) if we are already playing the stream (usual case), we skip to the
   correct second directly
2) If we don’t have a queue yet, create a trivial one with the stream
3) If we have a queue, we insert the video as next item and start
  playing it.

The skipping logic in 1) is similar to the one further down in the old
optimization block, but will always correctly fire for timestamps now.
I copied it because it’s not quite the same code, and moving into a
separate method at this stage would complicate the code too much.
2025-09-06 15:09:11 +02:00
Profpatsch
3803d49489 Player/handleIntent: separate out the timestamp request into enum
Instead of implicitely reconstructing whether the intent was
intended (lol) to be a timestamp change, we create a new kind of
intent that *only* sets the data we need to switch to a new timestamp.

This means that the logic of what to do (opening a popup player) gets
moved from `InternalUrlsHandler.playOnPopup` to the
`Player.handleIntent` method, we only pass that we want to jump to a
new timestamp. Thus, the stream is now loaded *after* sending the
intent instead of before sending.

This is somewhat messy right now and still does not fix the issue of
queue deletion, but from now on the queue logic should get more
straightforward to implement.

In the end, everything should be a giant switch. Thus we don’t
fall-through anymore, but run the post-setup code manually by calling
`handeIntentPost` and then returning.
2025-09-06 15:06:53 +02:00
Profpatsch
25a4a9a253 Player/handleIntent: move prefs parameters into initPlayback
They are just read from the player preferences and don’t influence the
branching, no need to read them in the intent parsing logic.
2025-09-06 15:04:06 +02:00
Profpatsch
d534946550 Player: inline repeat mode cycling 2025-09-06 15:04:06 +02:00
Profpatsch
8fb3e90fe1 Player: remove unused REPEAT_MODE intent key 2025-09-06 15:04:06 +02:00
Profpatsch
5750ef6aa8 Player/handleIntent: start converting intent data to enum
The goal here is to convert all player intents to use a single enum
with extra data for each case. The queue ones are pretty easy, they
don’t carry any extra data. We fall through for everything else for
now.
2025-09-06 15:04:06 +02:00
Profpatsch
ab7d1377e5 Player/handleIntent: always early return on ENQUEUE an ENQUEUE_NEXT
We can do this, because:

1. if `playQueue` is not null, we return early
2. if `playQueue` is null and we need to enqueue:
  - the only “proper” case that could be triggered is
    the `RESUME_PLAYBACK` case, which is never `true` for the queuing
    intents, see the comment in `NavigationHelper.enqueueOnPlayer`
  - the generic `else` case is degenerate, because it would crash on
  `playQueue` being `null`.

This makes some sense, because there is no way to trigger the
enqueueing logic via the UI currently if there is no video playing
yet, in which case `playQueue` is not `null`.

So we need to transform this whole if desaster into a big switch.
2025-09-06 15:04:06 +02:00
Profpatsch
fd24c08529 Player/handleIntent: de morgan samePlayQueue
Okay, so this is the … only? branch in this if-chain that will
conditionally fire if `playQueue` *is* `null`, sometimes.

This is why the unconditional `initPlayback` in `else` is not passed a
`null` in many cases … because `RESUME_PLAYBACK` is `true` and
`playQueue` is `null`.

It’s gonna be hard to figure out which parts of that are intentional,
I say.
2025-09-06 15:04:06 +02:00
Profpatsch
e14ec3a4f9 NavigationHelper: inline trivial getPlayerIntent use 2025-09-06 15:04:06 +02:00
Profpatsch
b592403a66 NavigationHelper: push out resumePlayback one layer 2025-09-06 15:04:06 +02:00
Profpatsch
90e1ac56ef NavigationHelper: inline getPlayerEnqueueIntent
Funnily enough, I’m pretty sure that whole comment will be not
necessary, because we never check `resumePlayback` on handling the
intent anyway.
2025-09-06 15:04:06 +02:00
Profpatsch
32eb3afe16 Player/handleIntent: a few comments 2025-09-06 15:04:06 +02:00
14 changed files with 294 additions and 199 deletions

View file

@ -93,6 +93,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
@ -1166,8 +1167,12 @@ public final class VideoDetailFragment
final PlayQueue queue = setupPlayQueueForIntent(false); final PlayQueue queue = setupPlayQueueForIntent(false);
tryAddVideoPlayerView(); tryAddVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), final Context context = requireContext();
PlayerService.class, queue, true, autoPlayEnabled); 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); ContextCompat.startForegroundService(activity, playerIntent);
} }

View file

@ -24,12 +24,12 @@ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJ
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
import static com.google.android.exoplayer2.Player.DiscontinuityReason; import static com.google.android.exoplayer2.Player.DiscontinuityReason;
import static com.google.android.exoplayer2.Player.Listener; import static com.google.android.exoplayer2.Player.Listener;
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_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static com.google.android.exoplayer2.Player.RepeatMode; import static com.google.android.exoplayer2.Player.RepeatMode;
import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
@ -109,6 +109,7 @@ import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
@ -118,8 +119,10 @@ import org.schabi.newpipe.player.ui.PlayerUiList;
import org.schabi.newpipe.player.ui.PopupPlayerUi; import org.schabi.newpipe.player.ui.PopupPlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
@ -130,9 +133,11 @@ import java.util.stream.IntStream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.SerialDisposable; import io.reactivex.rxjava3.disposables.SerialDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class Player implements PlaybackListener, Listener { public final class Player implements PlaybackListener, Listener {
public static final boolean DEBUG = MainActivity.DEBUG; public static final boolean DEBUG = MainActivity.DEBUG;
@ -154,14 +159,13 @@ public final class Player implements PlaybackListener, Listener {
// Intent // Intent
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final String REPEAT_MODE = "repeat_mode";
public static final String PLAYBACK_QUALITY = "playback_quality"; public static final String PLAYBACK_QUALITY = "playback_quality";
public static final String PLAY_QUEUE_KEY = "play_queue_key"; public static final String PLAY_QUEUE_KEY = "play_queue_key";
public static final String ENQUEUE = "enqueue";
public static final String ENQUEUE_NEXT = "enqueue_next";
public static final String RESUME_PLAYBACK = "resume_playback"; public static final String RESUME_PLAYBACK = "resume_playback";
public static final String PLAY_WHEN_READY = "play_when_ready"; public static final String PLAY_WHEN_READY = "play_when_ready";
public static final String PLAYER_TYPE = "player_type"; public static final String PLAYER_TYPE = "player_type";
public static final String PLAYER_INTENT_TYPE = "player_intent_type";
public static final String PLAYER_INTENT_DATA = "player_intent_data";
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Time constants // Time constants
@ -246,6 +250,8 @@ public final class Player implements PlaybackListener, Listener {
private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
@NonNull @NonNull
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
@NonNull
private final CompositeDisposable streamItemDisposable = new CompositeDisposable();
// This is the only listener we need for thumbnail loading, since there is always at most only // This is the only listener we need for thumbnail loading, since there is always at most only
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference, // one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
@ -346,48 +352,134 @@ public final class Player implements PlaybackListener, Listener {
@SuppressWarnings("MethodLength") @SuppressWarnings("MethodLength")
public void handleIntent(@NonNull final Intent intent) { public void handleIntent(@NonNull final Intent intent) {
// fail fast if no play queue was provided
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY); final PlayerIntentType playerIntentType = intent.getParcelableExtra(PLAYER_INTENT_TYPE);
if (queueCache == null) { if (playerIntentType == null) {
return; return;
} }
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class); final PlayerType newPlayerType;
if (newQueue == null) { // TODO: this should be in the second switch below, but Im not sure whether I
return; // can move the initUIs stuff without breaking the setup for edge cases somehow.
switch (playerIntentType) {
case TimestampChange -> {
// TODO: this breaks out of the pattern of asking for the permission before
// sending the PlayerIntent, but Im not sure yet how to combine the permissions
// with the new enum approach. Maybe its better that the player asks anyway?
if (!PermissionHelper.isPopupEnabledElseAsk(context)) {
return;
}
newPlayerType = PlayerType.POPUP;
}
default -> {
newPlayerType = PlayerType.retrieveFromIntent(intent);
}
} }
final PlayerType oldPlayerType = playerType; playerType = newPlayerType;
playerType = PlayerType.retrieveFromIntent(intent);
initUIsForCurrentPlayerType(); initUIsForCurrentPlayerType();
// We need to setup audioOnly before super(), see "sourceOf"
isAudioOnly = audioPlayerSelected(); isAudioOnly = audioPlayerSelected();
if (intent.hasExtra(PLAYBACK_QUALITY)) { if (intent.hasExtra(PLAYBACK_QUALITY)) {
videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
} }
// Resolve enqueue intents final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
if (intent.getBooleanExtra(ENQUEUE, false) && playQueue != null) {
playQueue.append(newQueue.getStreams());
return;
// Resolve enqueue next intents switch (playerIntentType) {
} else if (intent.getBooleanExtra(ENQUEUE_NEXT, false) && playQueue != null) { case Enqueue -> {
final int currentIndex = playQueue.getIndex(); if (playQueue != null) {
playQueue.append(newQueue.getStreams()); final PlayQueue newQueue = getPlayQueueFromCache(intent);
playQueue.move(playQueue.size() - 1, currentIndex + 1); if (newQueue == null) {
return;
}
playQueue.append(newQueue.getStreams());
return;
}
// TODO: This falls through to the old logic, there was no playQueue
// yet so we should start the player and add the new video
break;
}
case EnqueueNext -> {
if (playQueue != null) {
final PlayQueue newQueue = getPlayQueueFromCache(intent);
if (newQueue == null) {
return;
}
final PlayQueueItem newItem = newQueue.getStreams().get(0);
newQueue.enqueueNext(newItem, false);
return;
}
// TODO: This falls through to the old logic, there was no playQueue
// yet so we should start the player and add the new video
break;
}
case TimestampChange -> {
final TimestampChangeData dat = intent.getParcelableExtra(PLAYER_INTENT_DATA);
assert dat != null;
final Single<StreamInfo> single =
ExtractorHelper.getStreamInfo(dat.getServiceId(), dat.getUrl(), false);
streamItemDisposable.add(single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
final @Nullable PlayQueue oldPlayQueue = playQueue;
info.setStartPosition(dat.getSeconds());
final PlayQueueItem playQueueItem = new PlayQueueItem(info);
// If the stream is already playing,
// we can just seek to the appropriate timestamp
if (oldPlayQueue != null
&& playQueueItem.isSameItem(oldPlayQueue.getItem())) {
// Player can have state = IDLE when playback is stopped or failed
// and we should retry in this case
if (simpleExoPlayer.getPlaybackState()
== com.google.android.exoplayer2.Player.STATE_IDLE) {
simpleExoPlayer.prepare();
}
simpleExoPlayer.seekTo(oldPlayQueue.getIndex(),
dat.getSeconds() * 1000L);
simpleExoPlayer.setPlayWhenReady(playWhenReady);
} else {
final PlayQueue newPlayQueue;
// If there is no queue yet, just add our item
if (oldPlayQueue == null) {
newPlayQueue = new SinglePlayQueue(playQueueItem);
// else we add the timestamped stream behind the current video
// and start playing it.
} else {
oldPlayQueue.enqueueNext(playQueueItem, true);
oldPlayQueue.offsetIndex(1);
newPlayQueue = oldPlayQueue;
}
initPlayback(newPlayQueue, playWhenReady);
}
}, throwable -> {
// This will only show a snackbar if the passed context has a root view:
// otherwise it will resort to showing a notification, so we are safe
// here.
ErrorUtil.createNotification(context,
new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, dat.getUrl(),
null, dat.getUrl()));
}));
return;
}
case AllOthers -> {
// fallthrough; TODO: put other intent data in separate cases
}
}
final PlayQueue newQueue = getPlayQueueFromCache(intent);
if (newQueue == null) {
return; return;
} }
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); // branching parameters for below
final float playbackSpeed = savedParameters.speed;
final float playbackPitch = savedParameters.pitch;
final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString(
R.string.playback_skip_silence_key), getPlaybackSkipSilence());
final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue); final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue);
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
/* /*
* TODO As seen in #7427 this does not work: * TODO As seen in #7427 this does not work:
@ -402,7 +494,7 @@ public final class Player implements PlaybackListener, Listener {
if (!exoPlayerIsNull() if (!exoPlayerIsNull()
&& newQueue.size() == 1 && newQueue.getItem() != null && newQueue.size() == 1 && newQueue.getItem() != null
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl()) && newQueue.getItem().isSameItem(playQueue.getItem())
&& newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { && newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
// Player can have state = IDLE when playback is stopped or failed // Player can have state = IDLE when playback is stopped or failed
// and we should retry in this case // and we should retry in this case
@ -428,7 +520,8 @@ public final class Player implements PlaybackListener, Listener {
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
&& DependentPreferenceHelper.getResumePlaybackEnabled(context) && DependentPreferenceHelper.getResumePlaybackEnabled(context)
&& !samePlayQueue // !samePlayQueue
&& (playQueue == null || !playQueue.equalStreamsAndIndex(newQueue))
&& !newQueue.isEmpty() && !newQueue.isEmpty()
&& newQueue.getItem() != null && newQueue.getItem() != null
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
@ -444,30 +537,30 @@ public final class Player implements PlaybackListener, Listener {
newQueue.setRecovery(newQueue.getIndex(), newQueue.setRecovery(newQueue.getIndex(),
state.getProgressMillis()); state.getProgressMillis());
} }
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, initPlayback(newQueue, playWhenReady);
playbackSkipSilence, playWhenReady);
}, },
error -> { error -> {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "Failed to start playback", error); Log.w(TAG, "Failed to start playback", error);
} }
// In case any error we can start playback without history // In case any error we can start playback without history
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, initPlayback(newQueue, playWhenReady);
playbackSkipSilence, playWhenReady);
}, },
() -> { () -> {
// Completed but not found in history // Completed but not found in history
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, initPlayback(newQueue, playWhenReady);
playbackSkipSilence, playWhenReady);
} }
)); ));
} else { } else {
// Good to go... // Good to go...
// In a case of equal PlayQueues we can re-init old one but only when it is disposed // In a case of equal PlayQueues we can re-init old one but only when it is disposed
initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed, initPlayback(samePlayQueue ? playQueue : newQueue, playWhenReady);
playbackPitch, playbackSkipSilence, playWhenReady);
} }
}
public void handleIntentPost(final PlayerType oldPlayerType) {
if (oldPlayerType != playerType && playQueue != null) { if (oldPlayerType != playerType && playQueue != null) {
// If playerType changes from one to another we should reload the player // If playerType changes from one to another we should reload the player
// (to disable/enable video stream or to set quality) // (to disable/enable video stream or to set quality)
@ -478,6 +571,19 @@ public final class Player implements PlaybackListener, Listener {
NavigationHelper.sendPlayerStartedEvent(context); NavigationHelper.sendPlayerStartedEvent(context);
} }
@Nullable
private static PlayQueue getPlayQueueFromCache(@NonNull final Intent intent) {
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
if (queueCache == null) {
return null;
}
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
if (newQueue == null) {
return null;
}
return newQueue;
}
private void initUIsForCurrentPlayerType() { private void initUIsForCurrentPlayerType() {
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) { || (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
@ -511,15 +617,13 @@ public final class Player implements PlaybackListener, Listener {
} }
private void initPlayback(@NonNull final PlayQueue queue, private void initPlayback(@NonNull final PlayQueue queue,
@RepeatMode final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
final boolean playbackSkipSilence,
final boolean playOnReady) { final boolean playOnReady) {
destroyPlayer(); destroyPlayer();
initPlayer(playOnReady); initPlayer(playOnReady);
setRepeatMode(repeatMode); final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString(
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); R.string.playback_skip_silence_key), getPlaybackSkipSilence());
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
setPlaybackParameters(savedParameters.speed, savedParameters.pitch, playbackSkipSilence);
playQueue = queue; playQueue = queue;
playQueue.init(); playQueue.init();
@ -609,6 +713,7 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear(); databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null); progressUpdateDisposable.set(null);
streamItemDisposable.clear();
cancelLoadingCurrentThumbnail(); cancelLoadingCurrentThumbnail();
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
@ -1175,16 +1280,25 @@ public final class Player implements PlaybackListener, Listener {
return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
} }
public void setRepeatMode(@RepeatMode final int repeatMode) { public void cycleNextRepeatMode() {
if (!exoPlayerIsNull()) { if (!exoPlayerIsNull()) {
@RepeatMode final int repeatMode;
switch (simpleExoPlayer.getRepeatMode()) {
case REPEAT_MODE_OFF:
repeatMode = REPEAT_MODE_ONE;
break;
case REPEAT_MODE_ONE:
repeatMode = REPEAT_MODE_ALL;
break;
case REPEAT_MODE_ALL:
default:
repeatMode = REPEAT_MODE_OFF;
break;
}
simpleExoPlayer.setRepeatMode(repeatMode); simpleExoPlayer.setRepeatMode(repeatMode);
} }
} }
public void cycleNextRepeatMode() {
setRepeatMode(nextRepeatMode(getRepeatMode()));
}
@Override @Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) { public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
if (DEBUG) { if (DEBUG) {

View file

@ -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
}
/**
* 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

View file

@ -169,7 +169,9 @@ public final class PlayerService extends MediaBrowserServiceCompat {
} }
if (player != null) { if (player != null) {
final PlayerType oldPlayerType = player.getPlayerType();
player.handleIntent(intent); player.handleIntent(intent);
player.handleIntentPost(oldPlayerType);
player.UIs().get(MediaSessionPlayerUi.class) player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent)); .ifPresent(ui -> ui.handleMediaButtonIntent(intent));
} }

View file

@ -1,8 +1,5 @@
package org.schabi.newpipe.player.helper; 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_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
@ -25,7 +22,6 @@ import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
@ -410,23 +406,9 @@ public final class PlayerHelper {
return singlePlayQueue; return singlePlayQueue;
} }
// endregion // endregion
// region Utils used by player // 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 @ResizeMode
public static int retrieveResizeModeFromPrefs(final Player player) { public static int retrieveResizeModeFromPrefs(final Player player) {
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode), return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),

View file

@ -23,6 +23,7 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -254,7 +255,9 @@ public final class NotificationUtil {
} else { } else {
// We are playing in fragment. Don't open another activity just show fragment. That's it // We are playing in fragment. Don't open another activity just show fragment. That's it
final Intent intent = NavigationHelper.getPlayerIntent( 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.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN); intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.addCategory(Intent.CATEGORY_LAUNCHER);

View file

@ -291,6 +291,22 @@ public abstract class PlayQueue implements Serializable {
broadcast(new AppendEvent(itemList.size())); 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. * Removes the item at the given index from the play queue.
* <p> * <p>
@ -529,8 +545,7 @@ public abstract class PlayQueue implements Serializable {
final PlayQueueItem stream = streams.get(i); final PlayQueueItem stream = streams.get(i);
final PlayQueueItem otherStream = other.streams.get(i); final PlayQueueItem otherStream = other.streams.get(i);
// Check is based on serviceId and URL // Check is based on serviceId and URL
if (stream.getServiceId() != otherStream.getServiceId() if (!stream.isSameItem(otherStream)) {
|| !stream.getUrl().equals(otherStream.getUrl())) {
return false; return false;
} }
} }

View file

@ -38,7 +38,7 @@ public class PlayQueueItem implements Serializable {
private long recoveryPosition; private long recoveryPosition;
private Throwable error; private Throwable error;
PlayQueueItem(@NonNull final StreamInfo info) { public PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnails(), info.getUploaderName(), info.getThumbnails(), info.getUploaderName(),
info.getUploaderUrl(), info.getStreamType()); info.getUploaderUrl(), info.getStreamType());
@ -71,6 +71,22 @@ public class PlayQueueItem implements Serializable {
this.recoveryPosition = RECOVERY_UNSET; 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 @NonNull
public String getTitle() { public String getTitle() {
return title; return title;

View file

@ -16,7 +16,9 @@ public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfo info) { public SinglePlayQueue(final StreamInfo info) {
super(0, List.of(new PlayQueueItem(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) { public SinglePlayQueue(final StreamInfo info, final long startPosition) {
super(0, List.of(new PlayQueueItem(info))); super(0, List.of(new PlayQueueItem(info)));
getItem().setRecoveryPosition(startPosition); getItem().setRecoveryPosition(startPosition);

View file

@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Parcelable;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@ -57,8 +58,10 @@ import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.PlayQueueActivity; import org.schabi.newpipe.player.PlayQueueActivity;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType; 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.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -85,7 +88,7 @@ public final class NavigationHelper {
public static <T> Intent getPlayerIntent(@NonNull final Context context, public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz, @NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue, @Nullable final PlayQueue playQueue,
final boolean resumePlayback) { @NonNull final PlayerIntentType playerIntentType) {
final Intent intent = new Intent(context, targetClazz); final Intent intent = new Intent(context, targetClazz);
if (playQueue != null) { if (playQueue != null) {
@ -95,44 +98,31 @@ public final class NavigationHelper {
} }
} }
intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent()); intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent());
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true); intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true);
intent.putExtra(Player.PLAYER_INTENT_TYPE, (Parcelable) playerIntentType);
return intent; return intent;
} }
@NonNull @NonNull
public static <T> Intent getPlayerIntent(@NonNull final Context context, public static Intent getPlayerTimestampIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz, @NonNull final TimestampChangeData
@Nullable final PlayQueue playQueue, timestampChangeData) {
final boolean resumePlayback, final Intent intent = new Intent(context, PlayerService.class);
final boolean playWhenReady) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(Player.PLAY_WHEN_READY, playWhenReady);
}
@NonNull intent.putExtra(Player.PLAYER_INTENT_TYPE, (Parcelable) PlayerIntentType.TimestampChange);
public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context, intent.putExtra(Player.PLAYER_INTENT_DATA, timestampChangeData);
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue) { return intent;
// 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);
} }
@NonNull @NonNull
public static <T> Intent getPlayerEnqueueNextIntent(@NonNull final Context context, public static <T> Intent getPlayerEnqueueNextIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz, @NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue) { @Nullable final PlayQueue playQueue) {
// see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false return getPlayerIntent(context, targetClazz, playQueue, PlayerIntentType.EnqueueNext)
return getPlayerIntent(context, targetClazz, playQueue, false) // see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false
.putExtra(Player.ENQUEUE_NEXT, true); .putExtra(Player.RESUME_PLAYBACK, false);
} }
/* PLAY */ /* PLAY */
@ -166,8 +156,10 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback); final Intent intent = getPlayerIntent(context, PlayerService.class, queue,
intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent()); PlayerIntentType.AllOthers);
intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent())
.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -177,8 +169,10 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show(); .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.PLAYER_TYPE, PlayerType.AUDIO.valueForIntent());
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -191,7 +185,17 @@ public final class NavigationHelper {
} }
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); 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()); intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);

View file

@ -1,63 +1,27 @@
package org.schabi.newpipe.util.text; package org.schabi.newpipe.util.text;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import androidx.core.content.ContextCompat;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.TimestampChangeData;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class InternalUrlsHandler { public final class InternalUrlsHandler {
private static final String TAG = InternalUrlsHandler.class.getSimpleName();
private static final boolean DEBUG = MainActivity.DEBUG;
private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)"); private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
Pattern.compile("(.*)#timestamp=(\\d+)");
private InternalUrlsHandler() { private InternalUrlsHandler() {
} }
/**
* Handle a YouTube timestamp comment URL in NewPipe.
* <p>
* This method will check if the provided url is a YouTube comment description URL ({@code
* https://www.youtube.com/watch?v=}video_id{@code #timestamp=}time_in_seconds). If yes, the
* popup player will be opened when the user will click on the timestamp in the comment,
* at the time and for the video indicated in the timestamp.
*
* @param disposables a field of the Activity/Fragment class that calls this method
* @param context the context to use
* @param url the URL to check if it can be handled
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
public static boolean handleUrlCommentsTimestamp(@NonNull final CompositeDisposable
disposables,
final Context context,
@NonNull final String url) {
return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN, disposables);
}
/** /**
* Handle a YouTube timestamp description URL in NewPipe. * Handle a YouTube timestamp description URL in NewPipe.
* <p> * <p>
@ -66,36 +30,13 @@ public final class InternalUrlsHandler {
* player will be opened when the user will click on the timestamp in the video description, * player will be opened when the user will click on the timestamp in the video description,
* at the time and for the video indicated in the timestamp. * at the time and for the video indicated in the timestamp.
* *
* @param disposables a field of the Activity/Fragment class that calls this method
* @param context the context to use * @param context the context to use
* @param url the URL to check if it can be handled * @param url the URL to check if it can be handled
* @return true if the URL can be handled by NewPipe, false if it cannot * @return true if the URL can be handled by NewPipe, false if it cannot
*/ */
public static boolean handleUrlDescriptionTimestamp(@NonNull final CompositeDisposable public static boolean handleUrlDescriptionTimestamp(final Context context,
disposables,
final Context context,
@NonNull final String url) { @NonNull final String url) {
return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN, disposables); final Matcher matcher = AMPERSAND_TIMESTAMP_PATTERN.matcher(url);
}
/**
* Handle an URL in NewPipe.
* <p>
* This method will check if the provided url can be handled in NewPipe or not. If this is a
* service URL with a timestamp, the popup player will be opened and true will be returned;
* else, false will be returned.
*
* @param context the context to use
* @param url the URL to check if it can be handled
* @param pattern the pattern to use
* @param disposables a field of the Activity/Fragment class that calls this method
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
private static boolean handleUrl(final Context context,
@NonNull final String url,
@NonNull final Pattern pattern,
@NonNull final CompositeDisposable disposables) {
final Matcher matcher = pattern.matcher(url);
if (!matcher.matches()) { if (!matcher.matches()) {
return false; return false;
} }
@ -120,7 +61,7 @@ public final class InternalUrlsHandler {
} }
if (linkType == StreamingService.LinkType.STREAM && seconds != -1) { if (linkType == StreamingService.LinkType.STREAM && seconds != -1) {
return playOnPopup(context, matchedUrl, service, seconds, disposables); return playOnPopup(context, matchedUrl, service, seconds);
} else { } else {
NavigationHelper.openRouterActivity(context, matchedUrl); NavigationHelper.openRouterActivity(context, matchedUrl);
return true; return true;
@ -134,15 +75,12 @@ public final class InternalUrlsHandler {
* @param url the URL of the content * @param url the URL of the content
* @param service the service of the content * @param service the service of the content
* @param seconds the position in seconds at which the floating player will start * @param seconds the position in seconds at which the floating player will start
* @param disposables disposables created by the method are added here and their lifecycle
* should be handled by the calling class
* @return true if the playback of the content has successfully started or false if not * @return true if the playback of the content has successfully started or false if not
*/ */
public static boolean playOnPopup(final Context context, public static boolean playOnPopup(final Context context,
final String url, final String url,
@NonNull final StreamingService service, @NonNull final StreamingService service,
final int seconds, final int seconds) {
@NonNull final CompositeDisposable disposables) {
final LinkHandlerFactory factory = service.getStreamLHFactory(); final LinkHandlerFactory factory = service.getStreamLHFactory();
final String cleanUrl; final String cleanUrl;
@ -152,19 +90,14 @@ public final class InternalUrlsHandler {
return false; return false;
} }
final Single<StreamInfo> single = final Intent intent = NavigationHelper.getPlayerTimestampIntent(context,
ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); new TimestampChangeData(
disposables.add(single.subscribeOn(Schedulers.io()) service.getServiceId(),
.observeOn(AndroidSchedulers.mainThread()) cleanUrl,
.subscribe(info -> { seconds
final PlayQueue playQueue = new SinglePlayQueue(info, seconds * 1000L); ));
NavigationHelper.playOnPopupPlayer(context, playQueue, false); ContextCompat.startForegroundService(context, intent);
}, throwable -> {
// This will only show a snackbar if the passed context has a root view:
// otherwise it will resort to showing a notification, so we are safe here.
ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, url, null, url));
}));
return true; return true;
} }
} }

View file

@ -192,7 +192,7 @@ public final class TextLinkifier {
* <p> * <p>
* Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of * Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of
* a content, this method will parse the {@link CharSequence} and replace all current web links * a content, this method will parse the {@link CharSequence} and replace all current web links
* with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}. * with {@link ShareUtils#openUrlInBrowser(Context, String)}.
* </p> * </p>
* *
* <p> * <p>
@ -240,7 +240,7 @@ public final class TextLinkifier {
for (final URLSpan span : urls) { for (final URLSpan span : urls) {
final String url = span.getURL(); final String url = span.getURL();
final LongPressClickableSpan longPressClickableSpan = final LongPressClickableSpan longPressClickableSpan =
new UrlLongPressClickableSpan(context, disposables, url); new UrlLongPressClickableSpan(context, url);
textBlockLinked.setSpan(longPressClickableSpan, textBlockLinked.setSpan(longPressClickableSpan,
textBlockLinked.getSpanStart(span), textBlockLinked.getSpanStart(span),

View file

@ -46,7 +46,7 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan {
@Override @Override
public void onClick(@NonNull final View view) { public void onClick(@NonNull final View view) {
playOnPopup(context, relatedStreamUrl, relatedInfoService, playOnPopup(context, relatedStreamUrl, relatedInfoService,
timestampMatchDTO.seconds(), disposables); timestampMatchDTO.seconds());
} }
@Override @Override

View file

@ -7,29 +7,22 @@ import androidx.annotation.NonNull;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
final class UrlLongPressClickableSpan extends LongPressClickableSpan { final class UrlLongPressClickableSpan extends LongPressClickableSpan {
@NonNull @NonNull
private final Context context; private final Context context;
@NonNull @NonNull
private final CompositeDisposable disposables;
@NonNull
private final String url; private final String url;
UrlLongPressClickableSpan(@NonNull final Context context, UrlLongPressClickableSpan(@NonNull final Context context,
@NonNull final CompositeDisposable disposables,
@NonNull final String url) { @NonNull final String url) {
this.context = context; this.context = context;
this.disposables = disposables;
this.url = url; this.url = url;
} }
@Override @Override
public void onClick(@NonNull final View view) { public void onClick(@NonNull final View view) {
if (!InternalUrlsHandler.handleUrlDescriptionTimestamp( if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(context, url)) {
disposables, context, url)) {
ShareUtils.openUrlInApp(context, url); ShareUtils.openUrlInApp(context, url);
} }
} }