22
33import static org .schabi .newpipe .MainActivity .DEBUG ;
44
5+ import android .net .Uri ;
56import android .os .Bundle ;
6- import android .support . v4 . media . MediaBrowserCompat ;
7+ import android .os . ResultReceiver ;
78import android .support .v4 .media .MediaBrowserCompat .MediaItem ;
9+ import android .support .v4 .media .MediaDescriptionCompat ;
810import android .support .v4 .media .session .MediaSessionCompat ;
11+ import android .support .v4 .media .session .PlaybackStateCompat ;
912import android .util .Log ;
1013
1114import androidx .annotation .NonNull ;
1215import androidx .annotation .Nullable ;
16+ import androidx .annotation .StringRes ;
1317import androidx .media .MediaBrowserServiceCompat ;
18+ import androidx .media .utils .MediaConstants ;
1419
20+ import com .google .android .exoplayer2 .Player ;
1521import com .google .android .exoplayer2 .ext .mediasession .MediaSessionConnector ;
1622
23+ import org .schabi .newpipe .NewPipeDatabase ;
24+ import org .schabi .newpipe .R ;
25+ import org .schabi .newpipe .database .AppDatabase ;
26+ import org .schabi .newpipe .database .playlist .PlaylistMetadataEntry ;
27+ import org .schabi .newpipe .database .playlist .PlaylistStreamEntry ;
28+ import org .schabi .newpipe .local .playlist .LocalPlaylistManager ;
1729import org .schabi .newpipe .player .PlayerService ;
30+ import org .schabi .newpipe .player .playqueue .PlayQueue ;
31+ import org .schabi .newpipe .player .playqueue .SinglePlayQueue ;
32+ import org .schabi .newpipe .util .NavigationHelper ;
1833
1934import java .util .ArrayList ;
2035import java .util .List ;
36+ import java .util .stream .Collectors ;
2137
38+ import io .reactivex .rxjava3 .android .schedulers .AndroidSchedulers ;
2239import io .reactivex .rxjava3 .core .Single ;
40+ import io .reactivex .rxjava3 .disposables .Disposable ;
2341
24- public class MediaBrowserConnector {
42+ public class MediaBrowserConnector implements MediaSessionConnector . PlaybackPreparer {
2543 private static final String TAG = MediaBrowserConnector .class .getSimpleName ();
2644
2745 private final PlayerService playerService ;
2846 private final @ NonNull MediaSessionConnector sessionConnector ;
2947 private final @ NonNull MediaSessionCompat mediaSession ;
3048
49+ private AppDatabase database ;
50+ private LocalPlaylistManager localPlaylistManager ;
51+ private Disposable prepareOrPlayDisposable ;
52+
3153 public MediaBrowserConnector (@ NonNull final PlayerService playerService ) {
3254 this .playerService = playerService ;
3355 mediaSession = new MediaSessionCompat (playerService , TAG );
3456 sessionConnector = new MediaSessionConnector (mediaSession );
3557 sessionConnector .setMetadataDeduplicationEnabled (true );
58+ sessionConnector .setPlaybackPreparer (this );
3659 playerService .setSessionToken (mediaSession .getSessionToken ());
3760 }
3861
@@ -41,11 +64,58 @@ public MediaBrowserConnector(@NonNull final PlayerService playerService) {
4164 }
4265
4366 public void release () {
67+ disposePrepareOrPlayCommands ();
4468 mediaSession .release ();
4569 }
4670
4771 @ NonNull
48- private static final String MY_MEDIA_ROOT_ID = "media_root_id" ;
72+ private static final String ID_ROOT = "//${BuildConfig.APPLICATION_ID}/r" ;
73+ @ NonNull
74+ private static final String ID_BOOKMARKS = ID_ROOT + "/playlists" ;
75+
76+ private MediaItem createRootMediaItem (final String mediaId , final String folderName ) {
77+ final var builder = new MediaDescriptionCompat .Builder ();
78+ builder .setMediaId (mediaId );
79+ builder .setTitle (folderName );
80+
81+ final var extras = new Bundle ();
82+ extras .putString (MediaConstants .DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE ,
83+ playerService .getString (R .string .app_name ));
84+ builder .setExtras (extras );
85+ return new MediaItem (builder .build (), MediaItem .FLAG_BROWSABLE );
86+ }
87+
88+ private MediaItem createPlaylistMediaItem (final PlaylistMetadataEntry playlist ) {
89+ final var builder = new MediaDescriptionCompat .Builder ();
90+ builder .setMediaId (createMediaIdForPlaylist (playlist .uid ))
91+ .setTitle (playlist .name )
92+ .setIconUri (Uri .parse (playlist .thumbnailUrl ));
93+
94+ final var extras = new Bundle ();
95+ extras .putString (MediaConstants .DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE ,
96+ playerService .getResources ().getString (R .string .tab_bookmarks ));
97+ builder .setExtras (extras );
98+ return new MediaItem (builder .build (), MediaItem .FLAG_BROWSABLE );
99+ }
100+
101+ private String createMediaIdForPlaylist (final long playlistId ) {
102+ return ID_BOOKMARKS + '/' + playlistId ;
103+ }
104+
105+ private MediaItem createPlaylistStreamMediaItem (final long playlistId ,
106+ final PlaylistStreamEntry item ,
107+ final int index ) {
108+ final var builder = new MediaDescriptionCompat .Builder ();
109+ builder .setMediaId (createMediaIdForPlaylistIndex (playlistId , index ))
110+ .setTitle (item .getStreamEntity ().getTitle ())
111+ .setIconUri (Uri .parse (item .getStreamEntity ().getThumbnailUrl ()));
112+
113+ return new MediaItem (builder .build (), MediaItem .FLAG_PLAYABLE );
114+ }
115+
116+ private String createMediaIdForPlaylistIndex (final long playlistId , final int index ) {
117+ return createMediaIdForPlaylist (playlistId ) + '/' + index ;
118+ }
49119
50120 @ Nullable
51121 public MediaBrowserServiceCompat .BrowserRoot onGetRoot (@ NonNull final String clientPackageName ,
@@ -56,16 +126,153 @@ public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String cli
56126 clientPackageName , clientUid , rootHints ));
57127 }
58128
59- return new MediaBrowserServiceCompat .BrowserRoot (MY_MEDIA_ROOT_ID , null );
129+ return new MediaBrowserServiceCompat .BrowserRoot (ID_ROOT , null );
60130 }
61131
62132 public Single <List <MediaItem >> onLoadChildren (@ NonNull final String parentId ) {
63133 if (DEBUG ) {
64134 Log .d (TAG , String .format ("MediaBrowserService.onLoadChildren(%s)" , parentId ));
65135 }
66136
67- final List <MediaBrowserCompat .MediaItem > mediaItems = new ArrayList <>();
137+ final List <MediaItem > mediaItems = new ArrayList <>();
138+ final var parentIdUri = Uri .parse (parentId );
68139
140+ if (parentId .equals (ID_ROOT )) {
141+ mediaItems .add (
142+ createRootMediaItem (ID_BOOKMARKS ,
143+ playerService .getResources ().getString (R .string .tab_bookmarks )));
144+
145+ } else if (parentId .startsWith (ID_BOOKMARKS )) {
146+ final var path = parentIdUri .getPathSegments ();
147+ if (path .size () == 2 ) {
148+ return populateBookmarks ();
149+ } else if (path .size () == 3 ) {
150+ final var playlistId = Long .parseLong (path .get (2 ));
151+ return populatePlaylist (playlistId );
152+ } else {
153+ Log .w (TAG , "Unknown playlist uri " + parentId );
154+ }
155+ }
69156 return Single .just (mediaItems );
70157 }
158+
159+ private LocalPlaylistManager getPlaylistManager () {
160+ if (database == null ) {
161+ database = NewPipeDatabase .getInstance (playerService );
162+ }
163+ if (localPlaylistManager == null ) {
164+ localPlaylistManager = new LocalPlaylistManager (database );
165+ }
166+ return localPlaylistManager ;
167+ }
168+
169+ private Single <List <MediaItem >> populateBookmarks () {
170+ final var playlists = getPlaylistManager ().getPlaylists ().firstOrError ();
171+ return playlists .map (playlist ->
172+ playlist .stream ().map (this ::createPlaylistMediaItem ).collect (Collectors .toList ()));
173+ }
174+
175+ private Single <List <MediaItem >> populatePlaylist (final long playlistId ) {
176+ final var playlist = getPlaylistManager ().getPlaylistStreams (playlistId ).firstOrError ();
177+ return playlist .map (items -> {
178+ final List <MediaItem > results = new ArrayList <>();
179+ int index = 0 ;
180+ for (final var item : items ) {
181+ results .add (createPlaylistStreamMediaItem (playlistId , item , index ));
182+ ++index ;
183+ }
184+ return results ;
185+ });
186+ }
187+
188+ private void playbackError (@ StringRes final int resId , final int code ) {
189+ playerService .stopForImmediateReusing ();
190+ sessionConnector .setCustomErrorMessage (playerService .getString (resId ), code );
191+ }
192+
193+ private Single <PlayQueue > extractPlayQueueFromMediaId (final String mediaId ) {
194+ final Uri mediaIdUri = Uri .parse (mediaId );
195+ if (mediaIdUri == null ) {
196+ return Single .error (new NullPointerException ());
197+ }
198+ if (mediaId .startsWith (ID_BOOKMARKS )) {
199+ final var path = mediaIdUri .getPathSegments ();
200+ if (path .size () == 4 ) {
201+ final long playlistId = Long .parseLong (path .get (2 ));
202+ final int index = Integer .parseInt (path .get (3 ));
203+
204+ return getPlaylistManager ()
205+ .getPlaylistStreams (playlistId )
206+ .firstOrError ()
207+ .map (items -> {
208+ final var infoItems = items .stream ()
209+ .map (PlaylistStreamEntry ::toStreamInfoItem )
210+ .collect (Collectors .toList ());
211+ return new SinglePlayQueue (infoItems , index );
212+ });
213+ }
214+ }
215+
216+ return Single .error (new NullPointerException ());
217+ }
218+
219+ @ Override
220+ public long getSupportedPrepareActions () {
221+ return PlaybackStateCompat .ACTION_PLAY_FROM_MEDIA_ID ;
222+ }
223+
224+ private void disposePrepareOrPlayCommands () {
225+ if (prepareOrPlayDisposable != null ) {
226+ prepareOrPlayDisposable .dispose ();
227+ prepareOrPlayDisposable = null ;
228+ }
229+ }
230+
231+ @ Override
232+ public void onPrepare (final boolean playWhenReady ) {
233+ disposePrepareOrPlayCommands ();
234+ // No need to prepare
235+ }
236+
237+ @ Override
238+ public void onPrepareFromMediaId (@ NonNull final String mediaId , final boolean playWhenReady ,
239+ @ Nullable final Bundle extras ) {
240+ if (DEBUG ) {
241+ Log .d (TAG , String .format ("MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)" ,
242+ mediaId , playWhenReady , extras ));
243+ }
244+
245+ disposePrepareOrPlayCommands ();
246+ prepareOrPlayDisposable = extractPlayQueueFromMediaId (mediaId )
247+ .observeOn (AndroidSchedulers .mainThread ())
248+ .subscribe (
249+ playQueue -> {
250+ sessionConnector .setCustomErrorMessage (null );
251+ NavigationHelper .playOnBackgroundPlayer (playerService , playQueue ,
252+ playWhenReady );
253+ },
254+ throwable -> playbackError (R .string .error_http_not_found ,
255+ PlaybackStateCompat .ERROR_CODE_NOT_SUPPORTED )
256+ );
257+ }
258+
259+ @ Override
260+ public void onPrepareFromSearch (@ NonNull final String query , final boolean playWhenReady ,
261+ @ Nullable final Bundle extras ) {
262+ disposePrepareOrPlayCommands ();
263+ playbackError (R .string .content_not_supported , PlaybackStateCompat .ERROR_CODE_NOT_SUPPORTED );
264+ }
265+
266+ @ Override
267+ public void onPrepareFromUri (@ NonNull final Uri uri , final boolean playWhenReady ,
268+ @ Nullable final Bundle extras ) {
269+ disposePrepareOrPlayCommands ();
270+ playbackError (R .string .content_not_supported , PlaybackStateCompat .ERROR_CODE_NOT_SUPPORTED );
271+ }
272+
273+ @ Override
274+ public boolean onCommand (@ NonNull final Player player , @ NonNull final String command ,
275+ @ Nullable final Bundle extras , @ Nullable final ResultReceiver cb ) {
276+ return false ;
277+ }
71278}
0 commit comments