Skip to content

Instantly share code, notes, and snippets.

@stefan-zh
Created April 27, 2020 12:22
Show Gist options
  • Save stefan-zh/fd52e0ee06088ac4086d2ea3fb7d7f3e to your computer and use it in GitHub Desktop.
Save stefan-zh/fd52e0ee06088ac4086d2ea3fb7d7f3e to your computer and use it in GitHub Desktop.
Create Cast-enabled Activity with ExoPlayer
package com.exoplayer.cast
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.cast.CastPlayer
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.exoplayer2.util.Util
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.common.images.WebImage
import com.stefanzh.beetvplus.R
class SampleCastingPlayerActivity : AppCompatActivity(), SessionAvailabilityListener {
private lateinit var videoClipUrl: String
// the local and remote players
private var exoPlayer: SimpleExoPlayer? = null
private var castPlayer: CastPlayer? = null
private var currentPlayer: Player? = null
// views associated with the players
private lateinit var playerView: PlayerView
// the Cast context
private lateinit var castContext: CastContext
private lateinit var castButton: MenuItem
// Player state params
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
castContext = CastContext.getSharedInstance(this)
videoClipUrl = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"
// set the view to the clip display activity
setContentView(R.layout.activity_display_clip)
playerView = findViewById(R.id.local_player_view)
}
/**
* Starting with API level 24 Android supports multiple windows. As our app can be visible but
* not active in split window mode, we need to initialize the player in onStart. Before API level
* 24 we wait as long as possible until we grab resources, so we wait until onResume before
* initializing the player.
*/
override fun onStart() {
super.onStart()
if (Util.SDK_INT >= 24) {
initializePlayers()
}
}
override fun onResume() {
super.onResume()
if (Util.SDK_INT < 24 || exoPlayer == null) {
initializePlayers()
}
}
/**
* Before API Level 24 there is no guarantee of onStop being called. So we have to release the
* player as early as possible in onPause. Starting with API Level 24 (which brought multi and
* split window mode) onStop is guaranteed to be called. In the paused state our activity is still
* visible so we wait to release the player until onStop.
*/
override fun onPause() {
super.onPause()
if (Util.SDK_INT < 24) {
currentPlayer?.rememberState()
releaseLocalPlayer()
}
}
override fun onStop() {
super.onStop()
if (Util.SDK_INT >= 24) {
currentPlayer?.rememberState()
releaseLocalPlayer()
}
}
/**
* We release the remote player when activity is destroyed
*/
override fun onDestroy() {
releaseRemotePlayer()
currentPlayer = null
super.onDestroy()
}
/**
* 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
}
/**
* When back button is pressed, close this activity, which will go back to previous screen
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
/**
* CastPlayer [SessionAvailabilityListener] implementation.
*/
override fun onCastSessionAvailable() {
playOnPlayer(castPlayer)
}
override fun onCastSessionUnavailable() {
playOnPlayer(exoPlayer)
}
/**
* Prepares the local and remote players for playback.
*/
private fun initializePlayers() {
// first thing to do is set up the player to avoid the double initialization that happens
// sometimes if onStart() runs and then onResume() checks if the player is null
exoPlayer = SimpleExoPlayer.Builder(this).build()
playerView.player = exoPlayer
// create the CastPlayer that communicates with receiver app
// but because we don't release the CastPlayer on each onPause()/onStop(), we don't have
// to recreate it if it exists and the Activity wakes up
if (castPlayer == null) {
castPlayer = CastPlayer(castContext)
castPlayer?.setSessionAvailabilityListener(this)
}
// start the playback
if (castPlayer?.isCastSessionAvailable == true) {
playOnPlayer(castPlayer)
} else {
playOnPlayer(exoPlayer)
}
}
/**
* Sets the video on the current player (local or remote), whichever is active.
*/
private fun startPlayback() {
// 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)
}
}
/**
* Sets the current player to the selected player and starts playback.
*/
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
startPlayback()
}
/**
* Remembers the state of the playback of this Player.
*/
private fun Player.rememberState() {
[email protected] = playWhenReady
[email protected] = currentPosition
[email protected] = 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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment