androidx / media

Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android
https://developer.android.com/media/media3
Apache License 2.0
1.58k stars 375 forks source link

How to play downloaded content with a player in a MediaSession(Service)? #1543

Closed debz-eight closed 2 months ago

debz-eight commented 2 months ago

So, I have implemented a download feature for my audio app following the docs that uses a DownloadUtil and DownloadTracker which in turn uses DownloadManager to do so. Now the download is working fine. After a file is downloaded, I am getting the mediaSource properly.

if (isFromDownloads() == true){
                    val mediaSource = downloadTracker?.getDownloadRequest(Uri.parse(getSongList()[getCurrentSongPosition()].audio))!!.let {
                        DownloadHelper.createMediaSource(
                            it,
                            DemoUtil.getDataSourceFactory(mContext)
                        )
                    }
                } else {
                    val mediaItems = getSongList().map { song ->
                        MediaItem.Builder().setMediaId(song.audio).setMediaMetadata(
                            MediaMetadata.Builder().setFolderType(MediaMetadata.FOLDER_TYPE_ALBUMS)
                                .setArtworkUri(Uri.parse(song.bannerSquare?.md?: song.banner2))
                                .setArtist(getSeriesName()).setTitle(song.name)
                                .setExtras(putMediaItemsData()).build()
                        ).build()
                    }
                    controller.setMediaItems(mediaItems)
                    controller.seekTo(
                        getCurrentSongPosition(),
                        getCurrentPlayingSongProgressDataInMs()
                    )
                }

If isFromDownloads is true then I want to play the downloaded file. This code section is from a Fragment and I have access to MediaController. As you can see, that if internet is present then media is being streamed and I am setting media items using the controller and seeking according to my needs which is being played from inside onAddMediaItems() using player.prepare() and player.play(). But I don't know how to do the same for downloaded content.

I am not really sure where to post this. So, I posted this here. If you need any other info or context related to code, I'll be happy to explain.

marcbaechinger commented 2 months ago

Thanks for your question.

I have implemented a download feature for my audio app

I assume this implies you have downloaded the media of a media item into a Cache, namely into a SimpleCache instance. I respond in the following assuming that your app is running in a single process which is the process architecture in which SimpleCache is supported. Or more precisely I assume that downloading to and reading from a SimpleCache singleton instance is feasible in the current architecture. See this line in the class level JavaDoc of SimpleCache, saying 'only one instance of SimpleCache is allowed for a given directory at a given time'.

With a single cache instance, the player on the session side can create a media source factory that has a read only cache data source, that reads from the cache to which you've downloaded the media. When building your player in the service you'd do something like this to get a CacheDataSource using the download Cache singleton:

public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
    if (dataSourceFactory == null) {
      context = context.getApplicationContext();
      DefaultDataSource.Factory upstreamFactory =
          new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context));
      Cache cache = getDownloadCacheSingleton(context);
      dataSourceFactory = new CacheDataSource.Factory()
          .setCache(cache)
          .setUpstreamDataSourceFactory(upstreamFactory)
          .setCacheWriteDataSinkFactory(null)
          .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
    }
    return dataSourceFactory;
  }

The CacheDataSource checks whether the requested media is in the cache, in case of a cache-miss the upstream datasource is used to load the data from the original source.

Then use the cache data source to create a media source factory with which to build the player:

DefaultMediaSourceFactory defaultMediaSourceFactory =
    new DefaultMediaSourceFactorycontext)
        .setDataSourceFactory(getDataSourceFactory(context));
ExoPlayer exoPlayer =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(defaultMediaSourceFactory)
        .build();

If you setup your player this way, it will read from the cache if media is in the cache, and asks the upstream data source if not and stream the data from the original source. So there is no need for a controller to know whether a media item is downloaded or not, the controller just tells the session to play the media item either way.

See general docs about 'Playing downloaded content'.

If you are having a multi-process architecture that separates downloading and playback in different processes, I think it's worth to try to move this into the same process. The alternative is writing a cross-process capable Cache implementation.

debz-eight commented 2 months ago

Yes, this solution worked.

What I did was once I got the DownloadRequest object, I converted it into a media item using DownloadRequest.toMediaItem(). Then I added my necessary media MediaMetaData by building upon the existing media items and then set the media items using controller.setMediaItems(updatedMediaItems).

After this, as you instructed, I used my existing download cache and create a cacheDataSourceFactory and set it to Exoplayer with setMediaSourceFactory()