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 ;
8485import org .schabi .newpipe .R ;
8586import org .schabi .newpipe .databinding .PlayerBinding ;
8687import org .schabi .newpipe .error .ErrorInfo ;
88+ import org .schabi .newpipe .error .ErrorPanelHelper ;
8789import org .schabi .newpipe .error .ErrorUtil ;
8890import org .schabi .newpipe .error .UserAction ;
8991import org .schabi .newpipe .extractor .stream .AudioStream ;
107109import org .schabi .newpipe .player .playback .PlaybackListener ;
108110import org .schabi .newpipe .player .playqueue .PlayQueue ;
109111import org .schabi .newpipe .player .playqueue .PlayQueueItem ;
112+ import org .schabi .newpipe .player .playqueue .SinglePlayQueue ;
110113import org .schabi .newpipe .player .resolver .AudioPlaybackResolver ;
111114import org .schabi .newpipe .player .resolver .VideoPlaybackResolver ;
112115import org .schabi .newpipe .player .resolver .VideoPlaybackResolver .SourceType ;
116119import org .schabi .newpipe .player .ui .PopupPlayerUi ;
117120import org .schabi .newpipe .player .ui .VideoPlayerUi ;
118121import org .schabi .newpipe .util .DependentPreferenceHelper ;
122+ import org .schabi .newpipe .util .ExtractorHelper ;
119123import org .schabi .newpipe .util .ListHelper ;
120124import org .schabi .newpipe .util .NavigationHelper ;
125+ import org .schabi .newpipe .util .PermissionHelper ;
121126import org .schabi .newpipe .util .image .PicassoHelper ;
122127import org .schabi .newpipe .util .SerializedCache ;
123128import org .schabi .newpipe .util .StreamTypeUtil ;
128133
129134import io .reactivex .rxjava3 .android .schedulers .AndroidSchedulers ;
130135import io .reactivex .rxjava3 .core .Observable ;
136+ import io .reactivex .rxjava3 .core .Single ;
131137import io .reactivex .rxjava3 .disposables .CompositeDisposable ;
132138import io .reactivex .rxjava3 .disposables .Disposable ;
133139import io .reactivex .rxjava3 .disposables .SerialDisposable ;
140+ import io .reactivex .rxjava3 .schedulers .Schedulers ;
134141
135142public final class Player implements PlaybackListener , Listener {
136143 public static final boolean DEBUG = MainActivity .DEBUG ;
@@ -158,6 +165,7 @@ public final class Player implements PlaybackListener, Listener {
158165 public static final String PLAY_WHEN_READY = "play_when_ready" ;
159166 public static final String PLAYER_TYPE = "player_type" ;
160167 public static final String PLAYER_INTENT_TYPE = "player_intent_type" ;
168+ public static final String PLAYER_INTENT_DATA = "player_intent_data" ;
161169
162170 /*//////////////////////////////////////////////////////////////////////////
163171 // Time constants
@@ -242,6 +250,8 @@ public final class Player implements PlaybackListener, Listener {
242250 private final SerialDisposable progressUpdateDisposable = new SerialDisposable ();
243251 @ NonNull
244252 private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable ();
253+ @ NonNull
254+ private final CompositeDisposable streamItemDisposable = new CompositeDisposable ();
245255
246256 // This is the only listener we need for thumbnail loading, since there is always at most only
247257 // one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
@@ -333,18 +343,31 @@ public int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
333343
334344 @ SuppressWarnings ("MethodLength" )
335345 public void handleIntent (@ NonNull final Intent intent ) {
336- // fail fast if no play queue was provided
337- final String queueCache = intent .getStringExtra ( PLAY_QUEUE_KEY );
338- if (queueCache == null ) {
346+
347+ final PlayerIntentType playerIntentType = intent .getParcelableExtra ( PLAYER_INTENT_TYPE );
348+ if (playerIntentType == null ) {
339349 return ;
340350 }
341- final PlayQueue newQueue = SerializedCache .getInstance ().take (queueCache , PlayQueue .class );
342- if (newQueue == null ) {
343- return ;
351+ final PlayerType newPlayerType ;
352+ // TODO: this should be in the second switch below, but I’m not sure whether I
353+ // can move the initUIs stuff without breaking the setup for edge cases somehow.
354+ switch (playerIntentType ) {
355+ case TimestampChange -> {
356+ // TODO: this breaks out of the pattern of asking for the permission before
357+ // sending the PlayerIntent, but I’m not sure yet how to combine the permissions
358+ // with the new enum approach. Maybe it’s better that the player asks anyway?
359+ if (!PermissionHelper .isPopupEnabledElseAsk (context )) {
360+ return ;
361+ }
362+ newPlayerType = PlayerType .POPUP ;
363+ }
364+ default -> {
365+ newPlayerType = PlayerType .retrieveFromIntent (intent );
366+ }
344367 }
345368
346369 final PlayerType oldPlayerType = playerType ;
347- playerType = PlayerType . retrieveFromIntent ( intent ) ;
370+ playerType = newPlayerType ;
348371 initUIsForCurrentPlayerType ();
349372 // TODO: what does the following comment mean? Is that a relict?
350373 // We need to setup audioOnly before super(), see "sourceOf"
@@ -354,29 +377,66 @@ public void handleIntent(@NonNull final Intent intent) {
354377 videoResolver .setPlaybackQuality (intent .getStringExtra (PLAYBACK_QUALITY ));
355378 }
356379
357- final PlayerIntentType playerIntentType = intent .getParcelableExtra ( PLAYER_INTENT_TYPE );
380+ final boolean playWhenReady = intent .getBooleanExtra ( PLAY_WHEN_READY , true );
358381
359382 switch (playerIntentType ) {
360383 case Enqueue -> {
361384 if (playQueue != null ) {
385+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
386+ if (newQueue == null ) {
387+ return ;
388+ }
362389 playQueue .append (newQueue .getStreams ());
363390 }
364391 return ;
365392 }
366393 case EnqueueNext -> {
367394 if (playQueue != null ) {
395+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
396+ if (newQueue == null ) {
397+ return ;
398+ }
368399 final int currentIndex = playQueue .getIndex ();
369400 playQueue .append (newQueue .getStreams ());
370401 playQueue .move (playQueue .size () - 1 , currentIndex + 1 );
371402 }
372403 return ;
373404 }
405+ case TimestampChange -> {
406+ final TimestampChangeData dat = intent .getParcelableExtra (PLAYER_INTENT_DATA );
407+ assert dat != null ;
408+ final Single <StreamInfo > single =
409+ ExtractorHelper .getStreamInfo (dat .getServiceId (), dat .getUrl (), false );
410+ streamItemDisposable .add (single .subscribeOn (Schedulers .io ())
411+ .observeOn (AndroidSchedulers .mainThread ())
412+ .subscribe (info -> {
413+ final PlayQueue newPlayQueue =
414+ new SinglePlayQueue (info , dat .getSeconds () * 1000L );
415+ // TODO: add back the “already playing stream” optimization here
416+ initPlayback (newPlayQueue , playWhenReady );
417+ handleIntentPost (oldPlayerType );
418+ }, throwable -> {
419+ if (DEBUG ) {
420+ Log .e (TAG , "Could not play on popup: " + dat .getUrl (), throwable );
421+ }
422+ new AlertDialog .Builder (context )
423+ .setTitle (R .string .player_stream_failure )
424+ .setMessage (
425+ ErrorPanelHelper .Companion .getExceptionDescription (throwable ))
426+ .setPositiveButton (R .string .ok , null )
427+ .show ();
428+ }));
429+ return ;
430+ }
374431 case AllOthers -> {
375432 // fallthrough; TODO: put other intent data in separate cases
376433 }
377434 }
378435
379- final boolean playWhenReady = intent .getBooleanExtra (PLAY_WHEN_READY , true );
436+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
437+ if (newQueue == null ) {
438+ return ;
439+ }
380440
381441 // branching parameters for below
382442 final boolean samePlayQueue = playQueue != null && playQueue .equalStreamsAndIndex (newQueue );
@@ -457,6 +517,10 @@ public void handleIntent(@NonNull final Intent intent) {
457517 initPlayback (samePlayQueue ? playQueue : newQueue , playWhenReady );
458518 }
459519
520+ handleIntentPost (oldPlayerType );
521+ }
522+
523+ private void handleIntentPost (final PlayerType oldPlayerType ) {
460524 if (oldPlayerType != playerType && playQueue != null ) {
461525 // If playerType changes from one to another we should reload the player
462526 // (to disable/enable video stream or to set quality)
@@ -467,6 +531,19 @@ public void handleIntent(@NonNull final Intent intent) {
467531 NavigationHelper .sendPlayerStartedEvent (context );
468532 }
469533
534+ @ Nullable
535+ private static PlayQueue getPlayQueueFromCache (@ NonNull final Intent intent ) {
536+ final String queueCache = intent .getStringExtra (PLAY_QUEUE_KEY );
537+ if (queueCache == null ) {
538+ return null ;
539+ }
540+ final PlayQueue newQueue = SerializedCache .getInstance ().take (queueCache , PlayQueue .class );
541+ if (newQueue == null ) {
542+ return null ;
543+ }
544+ return newQueue ;
545+ }
546+
470547 private void initUIsForCurrentPlayerType () {
471548 if ((UIs .get (MainPlayerUi .class ).isPresent () && playerType == PlayerType .MAIN )
472549 || (UIs .get (PopupPlayerUi .class ).isPresent () && playerType == PlayerType .POPUP )) {
@@ -596,6 +673,7 @@ public void destroy() {
596673
597674 databaseUpdateDisposable .clear ();
598675 progressUpdateDisposable .set (null );
676+ streamItemDisposable .clear ();
599677 cancelLoadingCurrentThumbnail ();
600678
601679 UIs .destroyAll (Object .class ); // destroy every UI: obviously every UI extends Object
0 commit comments