Skip to content

Commit

Permalink
feat: add video casting
Browse files Browse the repository at this point in the history
  • Loading branch information
wakieu committed Jan 26, 2024
1 parent dee75c5 commit 2012046
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 54 deletions.
2 changes: 2 additions & 0 deletions androidenhancedvideoplayer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ dependencies {
// exoplayer
implementation "androidx.media3:media3-exoplayer:$mediaVersion"
implementation "androidx.media3:media3-ui:$mediaVersion"
implementation "androidx.media3:media3-cast:$mediaVersion"
implementation 'androidx.appcompat:appcompat:1.3.1'
}

afterEvaluate {
Expand Down
5 changes: 5 additions & 0 deletions androidenhancedvideoplayer/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.profusion.androidenhancedvideoplayer.CastOptionsProvider"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.profusion.androidenhancedvideoplayer

import android.content.Context
import com.google.android.gms.cast.CastMediaControlIntent
import com.google.android.gms.cast.LaunchOptions
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.SessionProvider

class CastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions {
val launchOptions = LaunchOptions.Builder()
.setAndroidReceiverCompatible(true)
.build()
return CastOptions.Builder()
.setLaunchOptions(launchOptions)
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
.setStopReceiverApplicationWhenEndingSession(true)
.build()
}

override fun getAdditionalSessionProviders(p0: Context): MutableList<SessionProvider>? {
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
Expand All @@ -22,13 +20,19 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.BasePlayer
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import com.google.android.gms.cast.framework.CastContext
import com.profusion.androidenhancedvideoplayer.R
import com.profusion.androidenhancedvideoplayer.components.playerOverlay.ControlsCustomization
import com.profusion.androidenhancedvideoplayer.components.playerOverlay.PlayerControls
Expand Down Expand Up @@ -74,6 +78,7 @@ fun EnhancedVideoPlayer(
zoomToFit: Boolean = true,
enableImmersiveMode: Boolean = true,
disableControls: Boolean = false,
disableCast: Boolean = false,
currentTimeTickInMs: Long = CURRENT_TIME_TICK_IN_MS,
controlsVisibilityDurationInMs: Long = PLAYER_CONTROLS_VISIBILITY_DURATION_IN_MS,
controlsCustomization: ControlsCustomization = ControlsCustomization(),
Expand All @@ -86,19 +91,25 @@ fun EnhancedVideoPlayer(
val orientation = configuration.orientation
val volumeController = remember { VolumeController(context) }

var isPlaying by remember { mutableStateOf(exoPlayer.isPlaying) }
val castContext = CastContext.getSharedInstance()
val castPlayer = castContext?.let { CastPlayer(it) }
val mediaItems: List<MediaItem> = listOfNotNull(exoPlayer.currentMediaItem)
var currentPlayer: Player by remember { mutableStateOf(exoPlayer) }
var isCasting by remember { mutableStateOf(false) }

var isPlaying by remember { mutableStateOf(currentPlayer.isPlaying) }
var isBuffering by remember {
mutableStateOf(exoPlayer.playbackState == ExoPlayer.STATE_BUFFERING)
mutableStateOf(currentPlayer.playbackState == BasePlayer.STATE_BUFFERING)
}
var hasEnded by remember { mutableStateOf(exoPlayer.playbackState == ExoPlayer.STATE_ENDED) }
var hasEnded by remember { mutableStateOf(currentPlayer.playbackState == BasePlayer.STATE_ENDED) }
var isControlsVisible by remember { mutableStateOf(false) }
var speed by remember { mutableStateOf(exoPlayer.playbackParameters.speed) }
var loop by remember { mutableStateOf(exoPlayer.repeatMode == ExoPlayer.REPEAT_MODE_ALL) }
var currentTime by remember { mutableStateOf(exoPlayer.contentPosition) }
var bufferedPosition by remember { mutableStateOf(exoPlayer.bufferedPosition) }
var totalDuration by remember { mutableStateOf(exoPlayer.duration) }
var speed by remember { mutableStateOf(currentPlayer.playbackParameters.speed) }
var loop by remember { mutableStateOf(currentPlayer.repeatMode == BasePlayer.REPEAT_MODE_ALL) }
var currentTime by remember { mutableStateOf(currentPlayer.contentPosition) }
var bufferedPosition by remember { mutableStateOf(currentPlayer.bufferedPosition) }
var totalDuration by remember { mutableStateOf(currentPlayer.duration) }
var title by remember {
mutableStateOf(exoPlayer.currentMediaItem?.mediaMetadata?.title?.toString())
mutableStateOf(currentPlayer.currentMediaItem?.mediaMetadata?.title?.toString())
}
var currentImagePreview by remember {
mutableStateOf(ImageBitmap(DEFAULT_THUMBNAIL_WIDTH, DEFAULT_THUMBNAIL_HEIGHT))
Expand Down Expand Up @@ -129,23 +140,47 @@ fun EnhancedVideoPlayer(
) {
mutableStateOf(
generateTrackQualityOptions(
exoPlayer.currentTracks,
currentPlayer.currentTracks,
autoQualityTrack
)
)
}

fun setPlayer(newPlayer: Player) {
if (castContext != null && !disableCast && currentPlayer !== newPlayer) {
var mediaIndex = C.INDEX_UNSET
var playbackPositionMs = C.TIME_UNSET
var playWhenReady = false
val previousPlayer = currentPlayer

val playbackState = previousPlayer.playbackState
if (playbackState != Player.STATE_ENDED) {
mediaIndex = previousPlayer.currentMediaItemIndex
playbackPositionMs = previousPlayer.currentPosition
playWhenReady = previousPlayer.playWhenReady

previousPlayer.stop()
}

newPlayer.setMediaItems(mediaItems, mediaIndex, playbackPositionMs)
newPlayer.playWhenReady = playWhenReady
newPlayer.prepare()

currentPlayer = newPlayer
}
}

DisposableEffect(context) {
val listener = object : Player.Listener {
val listener = object : Player.Listener, SessionAvailabilityListener {
override fun onEvents(player: Player, events: Player.Events) {
isPlaying = player.isPlaying
isBuffering = player.playbackState == ExoPlayer.STATE_BUFFERING
hasEnded = player.playbackState == ExoPlayer.STATE_ENDED
isBuffering = player.playbackState == BasePlayer.STATE_BUFFERING
hasEnded = player.playbackState == BasePlayer.STATE_ENDED
speed = player.playbackParameters.speed
title = player.mediaMetadata.title?.toString()
currentTime = player.contentPosition
totalDuration = player.duration
loop = player.repeatMode == ExoPlayer.REPEAT_MODE_ALL
loop = player.repeatMode == BasePlayer.REPEAT_MODE_ALL
deviceVolume = player.deviceVolume
super.onEvents(player, events)
}
Expand All @@ -164,20 +199,35 @@ fun EnhancedVideoPlayer(
)
super.onTrackSelectionParametersChanged(parameters)
}

override fun onCastSessionAvailable() {
setPlayer(castPlayer ?: exoPlayer)
isCasting = true
isControlsVisible = true
}

override fun onCastSessionUnavailable() {
setPlayer(exoPlayer)
isCasting = false
}
}
exoPlayer.addListener(listener)
castPlayer?.addListener(listener)
castPlayer?.setSessionAvailabilityListener(listener)

onDispose {
exoPlayer.removeListener(listener)
castPlayer?.removeListener(listener)
castPlayer?.setSessionAvailabilityListener(null)
}
}

TimeoutEffect(
timeoutInMs = currentTimeTickInMs,
enabled = isControlsVisible
) {
currentTime = exoPlayer.currentPosition
bufferedPosition = exoPlayer.bufferedPosition
currentTime = currentPlayer.currentPosition
bufferedPosition = currentPlayer.bufferedPosition
}

LaunchedEffect(isFullScreen) {
Expand All @@ -198,15 +248,17 @@ fun EnhancedVideoPlayer(
isPlaying,
isBrightnessSliderDragged,
isVolumeSliderDragged,
isTimeBarDragged
isTimeBarDragged,
isCasting
) {
if (
isControlsVisible &&
isPlaying &&
controlsVisibilityDurationInMs > 0 &&
!isBrightnessSliderDragged &&
!isTimeBarDragged &&
!isVolumeSliderDragged
!isVolumeSliderDragged &&
!isCasting
) {
delay(controlsVisibilityDurationInMs)
isControlsVisible = false
Expand Down Expand Up @@ -258,22 +310,25 @@ fun EnhancedVideoPlayer(
} else {
AspectRatioFrameLayout.RESIZE_MODE_FIT
}
playerView.player = currentPlayer
}
)
if (!disableControls) {
Box(modifier = Modifier.matchParentSize()) {
SeekHandler(
seekIncrement = exoPlayer::seekIncrement,
seekIncrement = currentPlayer::seekIncrement,
disableSeekForward = hasEnded,
isCasting = isCasting,
controlsCustomization = controlsCustomization,
toggleControlsVisibility = {
setControlsVisibility(!isControlsVisible)
setControlsVisibility(!isControlsVisible || isCasting)
},
setControlsVisibility = ::setControlsVisibility,
transformSeekIncrementRatio = transformSeekIncrementRatio
)
PlayerControls(
title = title,
disableCast = disableCast,
isVisible = isControlsVisible,
isPlaying = isPlaying,
isBuffering = isBuffering,
Expand All @@ -285,15 +340,15 @@ fun EnhancedVideoPlayer(
brightnessMutableInteractionSource = brightnessMutableInteractionSource,
volumeMutableInteractionSource = volumeMutableInteractionSource,
timeBarMutableInteractionSource = timeBarMutableInteractionSource,
totalDuration = totalDuration,
totalDuration = if (totalDuration > 0) totalDuration else 0,
currentTime = { currentTime },
bufferedPosition = { bufferedPosition },
onPreviousClick = exoPlayer::seekToPrevious,
onNextClick = exoPlayer::seekToNext,
onPreviousClick = currentPlayer::seekToPrevious,
onNextClick = currentPlayer::seekToNext,
onPauseToggle = when {
hasEnded -> exoPlayer::seekToDefaultPosition
isPlaying -> exoPlayer::pause
else -> exoPlayer::play
hasEnded -> currentPlayer::seekToDefaultPosition
isPlaying -> currentPlayer::pause
else -> currentPlayer::play
},
onFullScreenToggle = {
when (isFullScreen) {
Expand All @@ -304,7 +359,7 @@ fun EnhancedVideoPlayer(
onSettingsToggle = { isSettingsOpen = !isSettingsOpen },
onSeekBarValueFinished = { value ->
currentTime = value
exoPlayer.seekTo(value)
currentPlayer.seekTo(value)
},
onSeekBarValueChange = {
if (previewThumbnailBuilder != null) {
Expand All @@ -324,12 +379,12 @@ fun EnhancedVideoPlayer(
onDismissRequest = { isSettingsOpen = false },
speed = speed,
isLoopEnabled = loop,
onSpeedSelected = exoPlayer::setPlaybackSpeed,
onSpeedSelected = currentPlayer::setPlaybackSpeed,
onIsLoopEnabledSelected = { value ->
exoPlayer.repeatMode = if (value) {
ExoPlayer.REPEAT_MODE_ALL
currentPlayer.repeatMode = if (value) {
BasePlayer.REPEAT_MODE_ALL
} else {
ExoPlayer.REPEAT_MODE_OFF
BasePlayer.REPEAT_MODE_OFF
}
},
selectedQualityTrack = {
Expand All @@ -339,7 +394,7 @@ fun EnhancedVideoPlayer(
trackQualityOptions
},
onQualityChanged = { selectedQualityTrack ->
exoPlayer.setVideoQuality(selectedQualityTrack)
currentPlayer.setVideoQuality(selectedQualityTrack)
},
customization = settingsControlsCustomization
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.profusion.androidenhancedvideoplayer.components.mediaRouter

import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.gms.cast.framework.CastContext
import com.profusion.androidenhancedvideoplayer.styling.Dimensions

@Composable
fun MediaRouter(context: Context, modifier: Modifier = Modifier) {
val viewModel = MediaRouterViewModel(context)
val castContext = CastContext.getSharedInstance()

if (castContext != null) {
Box(modifier = modifier) {
AndroidView(
factory = { viewModel.mediaRouteButton() },
modifier = Modifier.size(Dimensions.xlarge)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.profusion.androidenhancedvideoplayer.components.mediaRouter

import android.content.Context
import android.view.View
import androidx.lifecycle.ViewModel
import androidx.mediarouter.app.MediaRouteActionProvider
import androidx.mediarouter.app.MediaRouteButton
import androidx.mediarouter.media.MediaControlIntent
import androidx.mediarouter.media.MediaRouteSelector

class MediaRouterViewModel(context: Context) : ViewModel() {
private val mediaRouteActionProvider = MediaRouteActionProvider(context)

init {
mediaRouteActionProvider.onCreateMediaRouteButton()
mediaRouteActionProvider.routeSelector = MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build()
}

fun mediaRouteButton(): View {
return mediaRouteActionProvider.onCreateActionView()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ data class ControlsCustomization(
fun PlayerControls(
modifier: Modifier = Modifier,
title: String? = null,
disableCast: Boolean = false,
isVisible: Boolean,
isPlaying: Boolean,
isBuffering: Boolean,
Expand Down Expand Up @@ -70,6 +71,7 @@ fun PlayerControls(
TopControls(
modifier = it,
title = title,
disableCast = disableCast,
shouldShowContent = shouldShowContent
)
},
Expand Down
Loading

0 comments on commit 2012046

Please sign in to comment.