4747import static org .schabi .newpipe .util .Localization .assureCorrectAppLanguage ;
4848import static java .util .concurrent .TimeUnit .MILLISECONDS ;
4949
50+ import android .app .AlertDialog ;
5051import android .content .BroadcastReceiver ;
5152import android .content .Context ;
5253import android .content .Intent ;
8687import org .schabi .newpipe .R ;
8788import org .schabi .newpipe .databinding .PlayerBinding ;
8889import org .schabi .newpipe .error .ErrorInfo ;
90+ import org .schabi .newpipe .error .ErrorPanelHelper ;
8991import org .schabi .newpipe .error .ErrorUtil ;
9092import org .schabi .newpipe .error .UserAction ;
9193import org .schabi .newpipe .extractor .stream .AudioStream ;
109111import org .schabi .newpipe .player .playback .PlaybackListener ;
110112import org .schabi .newpipe .player .playqueue .PlayQueue ;
111113import org .schabi .newpipe .player .playqueue .PlayQueueItem ;
114+ import org .schabi .newpipe .player .playqueue .SinglePlayQueue ;
112115import org .schabi .newpipe .player .resolver .AudioPlaybackResolver ;
113116import org .schabi .newpipe .player .resolver .VideoPlaybackResolver ;
114117import org .schabi .newpipe .player .resolver .VideoPlaybackResolver .SourceType ;
118121import org .schabi .newpipe .player .ui .PopupPlayerUi ;
119122import org .schabi .newpipe .player .ui .VideoPlayerUi ;
120123import org .schabi .newpipe .util .DependentPreferenceHelper ;
124+ import org .schabi .newpipe .util .ExtractorHelper ;
121125import org .schabi .newpipe .util .ListHelper ;
122126import org .schabi .newpipe .util .NavigationHelper ;
127+ import org .schabi .newpipe .util .PermissionHelper ;
123128import org .schabi .newpipe .util .image .PicassoHelper ;
124129import org .schabi .newpipe .util .SerializedCache ;
125130import org .schabi .newpipe .util .StreamTypeUtil ;
130135
131136import io .reactivex .rxjava3 .android .schedulers .AndroidSchedulers ;
132137import io .reactivex .rxjava3 .core .Observable ;
138+ import io .reactivex .rxjava3 .core .Single ;
133139import io .reactivex .rxjava3 .disposables .CompositeDisposable ;
134140import io .reactivex .rxjava3 .disposables .Disposable ;
135141import io .reactivex .rxjava3 .disposables .SerialDisposable ;
142+ import io .reactivex .rxjava3 .schedulers .Schedulers ;
136143
137144public final class Player implements PlaybackListener , Listener {
138145 public static final boolean DEBUG = MainActivity .DEBUG ;
@@ -160,6 +167,7 @@ public final class Player implements PlaybackListener, Listener {
160167 public static final String PLAY_WHEN_READY = "play_when_ready" ;
161168 public static final String PLAYER_TYPE = "player_type" ;
162169 public static final String PLAYER_INTENT_TYPE = "player_intent_type" ;
170+ public static final String PLAYER_INTENT_DATA = "player_intent_data" ;
163171
164172 /*//////////////////////////////////////////////////////////////////////////
165173 // Time constants
@@ -244,6 +252,8 @@ public final class Player implements PlaybackListener, Listener {
244252 private final SerialDisposable progressUpdateDisposable = new SerialDisposable ();
245253 @ NonNull
246254 private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable ();
255+ @ NonNull
256+ private final CompositeDisposable streamItemDisposable = new CompositeDisposable ();
247257
248258 // This is the only listener we need for thumbnail loading, since there is always at most only
249259 // one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
@@ -344,18 +354,31 @@ public int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
344354
345355 @ SuppressWarnings ("MethodLength" )
346356 public void handleIntent (@ NonNull final Intent intent ) {
347- // fail fast if no play queue was provided
348- final String queueCache = intent .getStringExtra ( PLAY_QUEUE_KEY );
349- if (queueCache == null ) {
357+
358+ final PlayerIntentType playerIntentType = intent .getParcelableExtra ( PLAYER_INTENT_TYPE );
359+ if (playerIntentType == null ) {
350360 return ;
351361 }
352- final PlayQueue newQueue = SerializedCache .getInstance ().take (queueCache , PlayQueue .class );
353- if (newQueue == null ) {
354- return ;
362+ final PlayerType newPlayerType ;
363+ // TODO: this should be in the second switch below, but I’m not sure whether I
364+ // can move the initUIs stuff without breaking the setup for edge cases somehow.
365+ switch (playerIntentType ) {
366+ case TimestampChange -> {
367+ // TODO: this breaks out of the pattern of asking for the permission before
368+ // sending the PlayerIntent, but I’m not sure yet how to combine the permissions
369+ // with the new enum approach. Maybe it’s better that the player asks anyway?
370+ if (!PermissionHelper .isPopupEnabledElseAsk (context )) {
371+ return ;
372+ }
373+ newPlayerType = PlayerType .POPUP ;
374+ }
375+ default -> {
376+ newPlayerType = PlayerType .retrieveFromIntent (intent );
377+ }
355378 }
356379
357380 final PlayerType oldPlayerType = playerType ;
358- playerType = PlayerType . retrieveFromIntent ( intent ) ;
381+ playerType = newPlayerType ;
359382 initUIsForCurrentPlayerType ();
360383 // TODO: what does the following comment mean? Is that a relict?
361384 // We need to setup audioOnly before super(), see "sourceOf"
@@ -365,29 +388,66 @@ public void handleIntent(@NonNull final Intent intent) {
365388 videoResolver .setPlaybackQuality (intent .getStringExtra (PLAYBACK_QUALITY ));
366389 }
367390
368- final PlayerIntentType playerIntentType = intent .getParcelableExtra ( PLAYER_INTENT_TYPE );
391+ final boolean playWhenReady = intent .getBooleanExtra ( PLAY_WHEN_READY , true );
369392
370393 switch (playerIntentType ) {
371394 case Enqueue -> {
372395 if (playQueue != null ) {
396+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
397+ if (newQueue == null ) {
398+ return ;
399+ }
373400 playQueue .append (newQueue .getStreams ());
374401 }
375402 return ;
376403 }
377404 case EnqueueNext -> {
378405 if (playQueue != null ) {
406+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
407+ if (newQueue == null ) {
408+ return ;
409+ }
379410 final int currentIndex = playQueue .getIndex ();
380411 playQueue .append (newQueue .getStreams ());
381412 playQueue .move (playQueue .size () - 1 , currentIndex + 1 );
382413 }
383414 return ;
384415 }
416+ case TimestampChange -> {
417+ final TimestampChangeData dat = intent .getParcelableExtra (PLAYER_INTENT_DATA );
418+ assert dat != null ;
419+ final Single <StreamInfo > single =
420+ ExtractorHelper .getStreamInfo (dat .getServiceId (), dat .getUrl (), false );
421+ streamItemDisposable .add (single .subscribeOn (Schedulers .io ())
422+ .observeOn (AndroidSchedulers .mainThread ())
423+ .subscribe (info -> {
424+ final PlayQueue newPlayQueue =
425+ new SinglePlayQueue (info , dat .getSeconds () * 1000L );
426+ // TODO: add back the “already playing stream” optimization here
427+ initPlayback (newPlayQueue , playWhenReady );
428+ handleIntentPost (oldPlayerType );
429+ }, throwable -> {
430+ if (DEBUG ) {
431+ Log .e (TAG , "Could not play on popup: " + dat .getUrl (), throwable );
432+ }
433+ new AlertDialog .Builder (context )
434+ .setTitle (R .string .player_stream_failure )
435+ .setMessage (
436+ ErrorPanelHelper .Companion .getExceptionDescription (throwable ))
437+ .setPositiveButton (R .string .ok , null )
438+ .show ();
439+ }));
440+ return ;
441+ }
385442 case AllOthers -> {
386443 // fallthrough; TODO: put other intent data in separate cases
387444 }
388445 }
389446
390- final boolean playWhenReady = intent .getBooleanExtra (PLAY_WHEN_READY , true );
447+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
448+ if (newQueue == null ) {
449+ return ;
450+ }
391451
392452 // branching parameters for below
393453 final boolean samePlayQueue = playQueue != null && playQueue .equalStreamsAndIndex (newQueue );
@@ -468,6 +528,10 @@ public void handleIntent(@NonNull final Intent intent) {
468528 initPlayback (samePlayQueue ? playQueue : newQueue , playWhenReady );
469529 }
470530
531+ handleIntentPost (oldPlayerType );
532+ }
533+
534+ private void handleIntentPost (final PlayerType oldPlayerType ) {
471535 if (oldPlayerType != playerType && playQueue != null ) {
472536 // If playerType changes from one to another we should reload the player
473537 // (to disable/enable video stream or to set quality)
@@ -478,6 +542,19 @@ public void handleIntent(@NonNull final Intent intent) {
478542 NavigationHelper .sendPlayerStartedEvent (context );
479543 }
480544
545+ @ Nullable
546+ private static PlayQueue getPlayQueueFromCache (@ NonNull final Intent intent ) {
547+ final String queueCache = intent .getStringExtra (PLAY_QUEUE_KEY );
548+ if (queueCache == null ) {
549+ return null ;
550+ }
551+ final PlayQueue newQueue = SerializedCache .getInstance ().take (queueCache , PlayQueue .class );
552+ if (newQueue == null ) {
553+ return null ;
554+ }
555+ return newQueue ;
556+ }
557+
481558 private void initUIsForCurrentPlayerType () {
482559 if ((UIs .get (MainPlayerUi .class ).isPresent () && playerType == PlayerType .MAIN )
483560 || (UIs .get (PopupPlayerUi .class ).isPresent () && playerType == PlayerType .POPUP )) {
@@ -607,6 +684,7 @@ public void destroy() {
607684
608685 databaseUpdateDisposable .clear ();
609686 progressUpdateDisposable .set (null );
687+ streamItemDisposable .clear ();
610688 cancelLoadingCurrentThumbnail ();
611689
612690 UIs .destroyAll (Object .class ); // destroy every UI: obviously every UI extends Object
0 commit comments