I've noticed this log in my crashlytics:
Caused by java.lang.NullPointerException: Attempt to invoke interface method 'android.media.session.ISessionController android.media.session.ISession.getController()' on a null object reference
at android.media.session.MediaSession.<init>(MediaSession.java:199)
at android.support.v4.media.session.MediaSessionCompat$MediaSessionImplApi29.createFwkMediaSession(MediaSessionCompat.java:4457)
at android.support.v4.media.session.MediaSessionCompat$MediaSessionImplApi21.<init>(MediaSessionCompat.java:3821)
at android.support.v4.media.session.MediaSessionCompat$MediaSessionImplApi22.<init>(MediaSessionCompat.java:4405)
at android.support.v4.media.session.MediaSessionCompat$MediaSessionImplApi28.<init>(MediaSessionCompat.java:4422)
at android.support.v4.media.session.MediaSessionCompat$MediaSessionImplApi29.<init>(MediaSessionCompat.java:4447)
at android.support.v4.media.session.MediaSessionCompat.<init>(MediaSessionCompat.java:576)
at android.support.v4.media.session.MediaSessionCompat.<init>(MediaSessionCompat.java:539)
at android.support.v4.media.session.MediaSessionCompat.<init>(MediaSessionCompat.java:503)
at android.support.v4.media.session.MediaSessionCompat.<init>(MediaSessionCompat.java:477)
at com.mypackage.API21VersionFactory.getMediaSessionCompat(API21VersionFactory.kt:16)
at com.mypackage.AudioPlayerService.onCreate(AudioPlayerService.kt:97)
at android.app.ActivityThread.handleCreateService(ActivityThread.java:4319)
at android.app.ActivityThread.access$1500(ActivityThread.java:263)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2010)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:240)
at android.app.ActivityThread.main(ActivityThread.java:8000)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:603)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
When trying create Audio notification style within my AudioService (an Android Service which plays audio files).
I'm building that notification in this way:
NotificationCompat.Builder(this, versionFactory.getAudioPlayerNotificationChannelId(this))
.setSmallIcon(R.drawable.ic_icon3)
.setColor(ContextCompat.getColor(this, R.color.color_2))
.setColorized(true)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.cover_art))
.setSubText(getString(R.string.audio))
.apply {
mediaSessionCompat?.let {
setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(it.sessionToken)
.setShowActionsInCompactView(0)
)
}
}
In order to get my mediaSessionCompat, I've declared a factory of android versions, so I get it this way:
open class API21VersionFactory : DefaultAndroidVersionFactory() {
...
override fun getMediaSessionCompat(context: Context) =
MediaSessionCompat(context, "TAG")
.apply {
setMetadata(
MediaMetadataCompat.fromMediaMetadata(
MediaMetadata.Builder()
.putLong(MediaMetadata.METADATA_KEY_DURATION, -1)
.build()
)
)
}
...
I'm not being able to replicate it because in every test I do I can play audios and notification is visible (and AudioService has been created successfully).
What's wrong in my code? What is the right way to prevent this and play audios within a Services in versions above API 21?
Thanks in advance!
Related
I am using the latest Android Media3 library, but I found a problem in using it...
I created a MediaSessionService, and then got the MediaController in the Activity, and then when I tried to call the media controller and add some MediaItems, an error occurred:
java.lang.NullPointerException
at androidx.media3.common.util.Assertions.checkNotNull(Assertions.java:155)
at androidx.media3.exoplayer.source.DefaultMediaSourceFactory.createMediaSource(DefaultMediaSourceFactory.java:338)
at androidx.media3.exoplayer.ExoPlayerImpl.createMediaSources(ExoPlayerImpl.java:1164)
at androidx.media3.exoplayer.ExoPlayerImpl.addMediaItems(ExoPlayerImpl.java:463)
at androidx.media3.exoplayer.SimpleExoPlayer.addMediaItems(SimpleExoPlayer.java:1146)
at androidx.media3.common.BasePlayer.addMediaItems(BasePlayer.java:69)
at androidx.media3.common.BasePlayer.addMediaItem(BasePlayer.java:64)
at androidx.media3.common.ForwardingPlayer.addMediaItem(ForwardingPlayer.java:90)
at androidx.media3.session.PlayerWrapper.addMediaItem(PlayerWrapper.java:346)
at androidx.media3.session.MediaSessionStub.lambda$addMediaItem$28(MediaSessionStub.java:1052)
at androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda8.run(Unknown Source:2)
at androidx.media3.session.MediaSessionStub.lambda$getSessionTaskWithPlayerCommandRunnable$2$androidx-media3-session-MediaSessionStub(MediaSessionStub.java:234)
at androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda52.run(Unknown Source:14)
at androidx.media3.session.MediaSessionStub.lambda$flushCommandQueue$50(MediaSessionStub.java:1479)
at androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda58.run(Unknown Source:2)
at androidx.media3.common.util.Util.postOrRun(Util.java:517)
at androidx.media3.session.MediaSessionStub.flushCommandQueue(MediaSessionStub.java:1473)
at androidx.media3.session.MediaControllerImplBase$FlushCommandQueueHandler.handleMessage(MediaControllerImplBase.java:3035)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7813)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
So I checked the createMediaSource function of DefaultMediaSourceFactory and found that it is checking whether the localConfiguration of MediaItem is null:
#Override
public MediaSource createMediaSource(MediaItem mediaItem) {
checkNotNull(mediaItem.localConfiguration);
...
}
And this is localConfiguration:
/**
* Optional configuration for local playback. May be {#code null} if shared over process
* boundaries.
*/
#Nullable public final LocalConfiguration localConfiguration;
I am pretty sure that there is no problem with the way I created the MediaItem, and it works well inside the Service, but when I try to insert the MediaItem in the Activity, an error occurs. According to the comments, I guess this may be a cross-process communication problem, but I don't have any clue about this. Does anyone have experience with Media3?
When you add/set MediaItems from a controller, the localConfiguration (uri, mimeType, drm config, etc) of MediaItem is removed for security/privacy reasons. Without localConfiguration the player can't play the media item. We need to add the missing information back to the MediaItem.
Updated answer (media3 1.0.0-beta01 or higher)
Open the Callback you defined when creating the MediaLibrarySession in your Service.
// My MediaLibraryService
// onCreate()
mediaLibrarySession = MediaLibrarySession.Builder(
this,
player,
librarySessionCallback // <--
).build()
// NOTE: If you are using MediaSessionService instead of MediaLibraryService,
// use `setCallback(librarySessionCallback)` from the MediaSession.Builder.
Override onAddMediaItems inside your MediaLibrarySession.Callback. Every time you use setMediaItem/addMediaItem from a controller, your onAddMediaItems will be called and the MediaItems returned there are the ones that will be played.
class CustomMediaLibrarySessionCallback : MediaLibraryService.MediaLibrarySession.Callback {
// [...]
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: MutableList<MediaItem>
): ListenableFuture<List<MediaItem>> {
// NOTE: You can use the id from the mediaItems to look up missing
// information (e.g. get URI from a database) and return a Future with
// a list of playable MediaItems.
// If your use case is really simple and the security/privacy reasons
// mentioned earlier don't apply to you, you can add the URI to the
// MediaItem request metadata in your activity/fragment and use it
// to rebuild the playable MediaItem.
val updatedMediaItems = mediaItems.map { mediaItem ->
mediaItem.buildUpon()
.setUri(mediaItem.requestMetadata.mediaUri)
.build()
}
return Futures.immediateFuture(updatedMediaItems)
}
}
Create and play your MediaItem from the activity/fragment.
// My Activity
val mmd = MediaMetadata.Builder()
.setTitle("Example")
.setArtist("Artist name")
.build()
// Request metadata. New in (1.0.0-beta01)
// This is optional. I'm adding a RequestMetadata to the MediaItem so I
// can get the mediaUri from my `onAddMediaItems` simple use case (see
// onAddMediaItems for more info).
// If you are going to get the final URI from a database, you can move your
// query to your `MediaLibrarySession.Callback#onAddMediaItems` and skip this.
val rmd = RequestMetadata.Builder()
.setMediaUri("...".toUri())
.build()
val mediaItem = MediaItem.Builder()
.setMediaId("123")
.setMediaMetadata(mmd)
.setRequestMetadata(rmd)
.build()
browser.setMediaItem(mediaItem)
browser.prepare()
browser.play()
Old answer (media3 1.0.0-alpha)
When you create the MediaLibrarySession inside your MediaLibraryService, you can add a MediaItemFiller. This MediaItemFiller has a fillInLocalConfiguration method that will be "Called to fill in the MediaItem.localConfiguration of the media item from controllers."
Knowing this, you need to:
Add a MediaItemFiller to your MediaLibrarySession builder inside your service.
// My MediaLibraryService
// onCreate()
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller()) // <--
.setSessionActivity(pendingIntent)
.build()
Create a custom MediaSession.MediaItemFiller. Any time you use a setMediaItem/addMediaItem from a controller this will be called and the MediaItem returned here will be the one played.
class CustomMediaItemFiller : MediaSession.MediaItemFiller {
override fun fillInLocalConfiguration(
session: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItem: MediaItem
): MediaItem {
// Return the media item to be played
return mediaItem.buildUpon()
// Use the metadata values to fill our media item
.setUri(mediaItem.mediaMetadata.mediaUri)
.build()
}
}
And finally, create and play your MediaItem from the activity.
// My Activity
// Fill some metadata that the MediaItemFiller
// will use to create the new MediaItem
val mmd = MediaMetadata.Builder()
.setTitle("Example")
.setArtist("Artist name")
.setMediaUri("...".toUri())
.build()
val mediaItem: MediaItem =
MediaItem.Builder()
.setMediaMetadata(mmd)
.build()
browser.setMediaItem(mediaItem)
browser.prepare()
browser.play()
I don't know why it has to be this awkward, but if you have a look to the CustomMediaItemFiller they use in the official repo, you will see that they use the mediaItem.mediaId to fetch a valid MediaItem from a media catalog. That's why their demo works when they use setMediaItem from an activity.
Also, as far as I know, anything you do inside fillInLocalConfiguration has to block the main thread (I believe setMediaItem has to be called from main) so, if you can, try to move any heavy work (ie, get media info from your database) to your Activity/ViewModel where you have more control, fill all the metadata you need there, and use your MediaSession.MediaItemFiller to do a simple transformation. Or move everything to your service and forget about everything.
I hope the flow is understood. I don't have much experience with media3 and maybe I'm missing something, but with the limitations of MediaItemFiller I found it a bit useless and I would really like to know more about its purpose.
I am trying to create a Podcast player. So following the Android Universal Media Player source code. The problem I am facing is, I do not know my playlist ahead of time, and the playlist creation is upon the user interaction. So I need to add/ remove Media Items from my Activity/ Fragment. My code is very alike the UMP sample app, also I have modified the MusicService's mediaSession
from
// Create a new MediaSession.
mediaSession = MediaSessionCompat(this, "MusicService")
.apply {
setSessionActivity(sessionActivityPendingIntent)
isActive = true
}
to
// Create a new MediaSession.
mediaSession = MediaSessionCompat(this, "MusicService")
.apply {
setSessionActivity(sessionActivityPendingIntent)
isActive = true
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
or MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS
or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
}
And I am trying to Add an item to the queue like below
fun addRandomMedia() {
if (mediaSessionConnection.isConnected.value == true) {
val mediaDescription = MediaDescriptionCompat.Builder()
.setMediaId("wake_up_01")
.setTitle("Intro - The Way Of Waking Up (feat. Alan Watts)")
.setMediaUri(Uri.parse("https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3"))
.build()
mediaSessionConnection.mediaController.addQueueItem(mediaDescription)
}
}
But I am always getting this below exception
Process: com.example.android.uamp.next, PID: 8446
java.lang.UnsupportedOperationException: This session doesn't support queue management operations
at android.support.v4.media.session.MediaControllerCompat$MediaControllerImplApi21.addQueueItem(MediaControllerCompat.java:1988)
at android.support.v4.media.session.MediaControllerCompat.addQueueItem(MediaControllerCompat.java:316)
at com.example.android.uamp.viewmodels.MainActivityViewModel.addItemToQueue(MainActivityViewModel.kt:118)
at com.example.android.uamp.MainActivity$onCreate$3.onClick(MainActivity.kt:68)
at android.view.View.performClick(View.java:6294)
at android.view.View$PerformClick.run(View.java:24770)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
Here is the addQueueItem code form support v4 media package
#Override
public void addQueueItem(MediaDescriptionCompat description) {
long flags = getFlags();
if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
throw new UnsupportedOperationException(
"This session doesn't support queue management operations");
}
Bundle params = new Bundle();
params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
sendCommand(COMMAND_ADD_QUEUE_ITEM, params, null);
}
So, I feel must be issues with getFlags method or I am missing something here, as the if check is always true.
To reproduce this issue, I have forked the sample and added the above code in the app. Here is the full code link
Here is the code to reproduce
Are u using MediaSessionConnector?
The problem is the flags are overridden in constructor of MediaSessionConnector.
So u need to call mediaSession.setFlags(flags) after creating an instance of MediaSessionConnector.
mediaSessionConnector = MediaSessionConnector(mediaSession).also {
...
...
it.mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
or MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS
or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
}
Starting Android Pie (API 28), Google isn't allowing using a single WebView instance in 2 different processes.
Documentation: https://developer.android.com/reference/android/webkit/WebView.html#setDataDirectorySuffix(java.lang.String)
As required, I called WebView.setDataDirectorySuffix("dir_name_no_separator") but unfortunately, I get an exception.
I tried to call this method inside the 2nd process Service onCreate().
java.lang.RuntimeException: Unable to create service com.myapp.service.MyService: java.lang.IllegalStateException: Can't set data directory suffix: WebView already initialized
at android.app.ActivityThread.handleCreateService(ActivityThread.java:3544)
at android.app.ActivityThread.access$1300(ActivityThread.java:199)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1666)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Caused by: java.lang.IllegalStateException: Can't set data directory suffix: WebView already initialized
at android.webkit.WebViewFactory.setDataDirectorySuffix(WebViewFactory.java:136)
at android.webkit.WebView.setDataDirectorySuffix(WebView.java:2165)
at com.myapp.service.MyService.onCreate(MyService.java:134)
I couldn't find any reason for that exception. I didn't call this method twice nor I called it in my main process. Any ideas?
Solved.
My project hosts AdMob ads and I call the MobileAds.initialize() method inside my Application class onCreate(). The ads initializer loads a WebView which is now forbidden to do in a new process before you call the WebView.setDataDirectorySuffix("dir_name_no_separator") method.
When the second process is created, it also goes through the same application create flow, meaning it calls the same onCreate() inside the Application class, which calls the MobileAds.initialize() that tries to create a new WebView instance and by that causes the crash.
IllegalStateException: Can't set data directory suffix: WebView already initialized
How I solved this?
I get the process name using the below method and check if it's my main process - call the MobileAds.initialize() method and if it's my second process, call the
WebView.setDataDirectorySuffix("dir_name_no_separator") method.
Get process name:
public static String getProcessName(Context context) {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == android.os.Process.myPid()) {
return processInfo.processName;
}
}
return null;
}
Application class onCreate():
if (!Utils.getProcessName(this).equals("YOUR_SECOND_PROCESS_NAME")) {
MobileAds.initialize(this);
} else {
WebView.setDataDirectorySuffix("dir_name_no_separator")
}
To summarize the fix with all the improvements, this is the code in Kotlin:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (packageName != Application.getProcessName()) {
WebView.setDataDirectorySuffix(Application.getProcessName())
}
}
Add it to your Application class to onCreate() method.
Note this is will only fix problem with maximum 2 processes. If your app is using more, you have to provide different WebView suffix for each of them.
when error due to ads, then in application class
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
MobileAds.initialize(this)
AudienceNetworkAds.initialize(this)
} catch (e: Error) {
Timber.e(e)
} catch (e: Exception) {
Timber.e(e)
}
I am using ExoPlayer (https://github.com/google/ExoPlayer) and custom notifications.
I want to access my music player from lock screen and headphone like in google play music and wync.
Please help me.
For playback controls on the lock screen you need to do a MediaStyle notification.
If you want to have an artwork as the lockscreen background you need to support MediaSession and maintain the metadata of the session properly:
new MediaMetadata.Builder(track)
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
.putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, bitmap)
.build();
I am also using Exoplayer with PlayerNotificationManager, And I used MediaSessionConnector and TimelineQueueNavigator to build notifications for the lock screen and background image for the lock screen.
Here, is my question with the Exoplayer team which is resolved for android 11 and above, regarding how to use MediaSessionConnector:
Why PlayerNotificationManager not showing Notification on startForeground in Android 11(R)?
Now, I just added a bitmap (using putParcelable()) for the current session track for MediaDescriptionCompact which will be set to the current MediaSession internally.
Here is the code for that:
val mediaSession = MediaSessionCompat(serviceContext, "DPS_APP")
mediaSession.isActive = true
mediaSessionConnector = MediaSessionConnector(mediaSession).also {
it.setQueueNavigator(
object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(
player: Player,
windowIndex: Int
): MediaDescriptionCompat {
val data: MediaMetaData =
getEmptyOfNullMedia(player)
isBitmapAvailable(getCurrentMediaArt(data))
val extras = Bundle().apply {
putString(
MediaMetadataCompat.METADATA_KEY_TITLE,
getCurrentTitle(data)
)
putString(
MediaMetadataCompat.METADATA_KEY_ARTIST,
getMediaTitle(data)
)
putParcelable(
MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
sessionCurrentBitmap
)
}
return MediaDescriptionCompat.Builder()
.setIconBitmap(sessionCurrentBitmap)
.setExtras(extras)
.build()
}
it.setPlayer(mPlayer)
}
Here is a small brief on that how to refresh or invalidate MediaSession in the given below issue on GitHub:
https://github.com/google/ExoPlayer/issues/5494
How to fully integrate Google Cast v3 with ExoPlayer v2? The activity will contain a FrameLayout with a com.google.android.exoplayer2.ui.SimpleExoPlayerView in it. The Google tutorial only covers integration with VideoView.
The code below is available in a Kotlin class in this Gist that should help people trying to set up their CastPlayer for the first time:
https://gist.github.com/stefan-zh/fd52e0ee06088ac4086d2ea3fb7d7f3e
Also, going through this tutorial from Google will help you: https://codelabs.developers.google.com/codelabs/cast-videos-android/index.html#0
I also used this tutorial to get started: https://android.jlelse.eu/sending-media-to-chromecast-has-never-been-easier-c331eeef1e0a
Here is a breakdown how to achieve this using ExoPlayer and its Cast extension.
1. You will need these dependencies:
// ExoPlayer is an advanced media player for playing media files
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
implementation "com.google.android.exoplayer:extension-cast:$exoplayer_version"
2. You will need the Cast button
The Cast button can be added in the options menu for the activities. This is the recommended way to do it.
Add the following to res/menu/browse.xml (in my case the menu file is called browse.xml):
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item
android:id="#+id/media_route_menu_item"
android:title="#string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always"/>
</menu>
Then add the following code to your Activity to enable the castButton:
/**
* We need to populate the Cast button across all activities as suggested by Google Cast Guide:
* https://developers.google.com/cast/docs/design_checklist/cast-button#sender-cast-icon-available
*/
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val result = super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.browse, menu)
castButton = CastButtonFactory.setUpMediaRouteButton(applicationContext, menu, R.id.media_route_menu_item)
return result
}
3. Declare your Options Provider for the Cast context
You need this so that you get the options dialog with the list of devices that you can cast to. Add this to your AndroidManifest.xml in the application tag:
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" />
4. Your Activity needs to implement ExoPlayer's Cast Extension interface SessionAvailabilityListener
This interface will allow you to listen for changes in the Cast session availability. Based on whether a Cast session is available you can direct playback to the local player or the remote player.
override fun onCastSessionAvailable() {
playOnPlayer(castPlayer)
}
override fun onCastSessionUnavailable() {
playOnPlayer(exoPlayer)
}
5. You will need logic to initialize the players:
Notice how we are calling castPlayer?.setSessionAvailabilityListener(this) where this refers to your Activity that implements the SessionAvailabilityListener interface. The listener's methods will be called when the Cast session availability changes.
private fun initializePlayers() {
exoPlayer = SimpleExoPlayer.Builder(this).build()
playerView.player = exoPlayer
if (castPlayer == null) {
castPlayer = CastPlayer(castContext)
castPlayer?.setSessionAvailabilityListener(this)
}
// start the playback
if (castPlayer?.isCastSessionAvailable == true) {
playOnPlayer(castPlayer)
} else {
playOnPlayer(exoPlayer)
}
}
6. You need logic to play on the selected player:
This method allows you to store the playback state (playbackPosition, playWhenReady, or windowIndex)
Create the correct media type for the local or remote players
Select which player should start playback
playOnPlayer() method:
private fun playOnPlayer(player: Player?) {
if (currentPlayer == player) {
return
}
// save state from the existing player
currentPlayer?.let {
if (it.playbackState != Player.STATE_ENDED) {
it.rememberState()
}
it.stop(true)
}
// set the new player
currentPlayer = player
// set up the playback
// if the current player is the ExoPlayer, play from it
if (currentPlayer == exoPlayer) {
// build the MediaSource from the URI
val uri = Uri.parse(videoClipUrl)
val dataSourceFactory = DefaultDataSourceFactory(this#SampleCastingPlayerActivity, "exoplayer-agent")
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
// use stored state (if any) to resume (or start) playback
exoPlayer?.playWhenReady = playWhenReady
exoPlayer?.seekTo(currentWindow, playbackPosition)
exoPlayer?.prepare(mediaSource, false, false)
}
// if the current player is the CastPlayer, play from it
if (currentPlayer == castPlayer) {
val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
metadata.putString(MediaMetadata.KEY_TITLE, "Title")
metadata.putString(MediaMetadata.KEY_SUBTITLE, "Subtitle")
metadata.addImage(WebImage(Uri.parse("any-image-url")))
val mediaInfo = MediaInfo.Builder(videoClipUrl)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(MimeTypes.VIDEO_MP4)
.setMetadata(metadata)
.build()
val mediaItem = MediaQueueItem.Builder(mediaInfo).build()
castPlayer?.loadItem(mediaItem, playbackPosition)
}
}
7. Remember state and clean up resources
Each time you switch your application between background or foreground you would need to release or request resources. Each time you release the Player's resources back to the system you would need to save its state.
/**
* Remembers the state of the playback of this Player.
*/
private fun Player.rememberState() {
this#SampleCastingPlayerActivity.playWhenReady = playWhenReady
this#SampleCastingPlayerActivity.playbackPosition = currentPosition
this#SampleCastingPlayerActivity.currentWindow = currentWindowIndex
}
/**
* Releases the resources of the local player back to the system.
*/
private fun releaseLocalPlayer() {
exoPlayer?.release()
exoPlayer = null
playerView.player = null
}
/**
* Releases the resources of the remote player back to the system.
*/
private fun releaseRemotePlayer() {
castPlayer?.setSessionAvailabilityListener(null)
castPlayer?.release()
castPlayer = null
}
Google Cast SDK is independent of Local Player, you can use ExoPlayer or MediaPlayer ( VideoView )
Once your APP has an active session, place the url in MediaInfo
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
movieMetadata.putString(MediaMetadata.KEY_TITLE, "Title")
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, "Sub")
val mediaLoadOptions = MediaInfo.Builder( < URL > )
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(< Content Type of Media>)
.setMetadata(movieMetadata)
.setStreamDuration(<Media Duration >)
.build()
mCastSession.remoteMediaClient.load(buildMediaInfo(url), mediaLoadOptions)
If you need to stream a local media, you will need to stream it for yourself, using NanoHttpd or another of your choice, and also implement a Cast Receiver