Skip to content

Commit

Permalink
Add Player Controls; Make Player LandScape
Browse files Browse the repository at this point in the history
  • Loading branch information
khaled-0 committed Mar 8, 2024
1 parent 24c6d6f commit 16518e6
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 14 deletions.
59 changes: 45 additions & 14 deletions app/src/main/java/dev/khaled/leanstream/player/Player.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package dev.khaled.leanstream.player

import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
Expand All @@ -12,32 +13,62 @@ import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import dev.khaled.leanstream.channels.Channel
import dev.khaled.leanstream.player.controller.PlayerController


@Composable
fun Player(channel: Channel, navigateBack: () -> Unit) {
val exoPlayer = ExoPlayer.Builder(LocalContext.current).build()
val exoPlayer = rememberExoPlayer(LocalContext.current)
val mediaSource = remember { MediaItem.fromUri(channel.url) }

LaunchedEffect(Unit) {
exoPlayer.setMediaItem(mediaSource)
exoPlayer.prepare()
exoPlayer.playWhenReady = true
}

DisposableEffect(Unit) {
onDispose { exoPlayer.release() }
}
BackHandler(onBack = navigateBack)

AndroidView(modifier = Modifier.fillMaxSize(), factory = {
PlayerView(it).apply {
player = exoPlayer
useController = false
}
}, update = { it.player = exoPlayer }, onRelease = { exoPlayer.release() })


AndroidView(
PlayerController(
modifier = Modifier.fillMaxSize(),
factory = {
PlayerView(it).apply {
player = exoPlayer
useController = false
}
},
channel = channel,
player = exoPlayer,
backHandler = navigateBack
)
}


@Composable
private fun rememberExoPlayer(context: Context) = remember {
ExoPlayer.Builder(context).build().apply {
playWhenReady = true
}
}

}
//private fun Modifier.dPadEvents(
// exoPlayer: ExoPlayer,
// videoPlayerState: VideoPlayerState,
// pulseState: VideoPlayerPulseState
//): Modifier = this.handleDPadKeyEvents(
// onLeft = {
// exoPlayer.seekBack()
// pulseState.setType(BACK)
// },
// onRight = {
// exoPlayer.seekForward()
// pulseState.setType(FORWARD)
// },
// onUp = { videoPlayerState.showControls() },
// onDown = { videoPlayerState.showControls() },
// onEnter = {
// exoPlayer.pause()
// videoPlayerState.showControls()
// }
//)
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dev.khaled.leanstream.player.controller

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BrokenImage
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import dev.khaled.leanstream.channels.Channel

@Composable
fun ChannelInfo(channel: Channel) {
Card {
Row(Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
text = channel.title ?: channel.url,
style = MaterialTheme.typography.titleLarge,
)
Spacer(modifier = Modifier.width(8.dp))
AsyncImage(
model = channel.icon,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.border(2.dp, MaterialTheme.colorScheme.primary, CircleShape),
contentDescription = null,
contentScale = ContentScale.FillBounds,
error = rememberVectorPainter(Icons.Rounded.BrokenImage),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.khaled.leanstream.player.controller

import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable

@Composable
fun ExtraControls(
backHandler: () -> Unit,
// toggleFullScreen: () -> Unit,
) {
Row {
FilledTonalIconButton(onClick = backHandler) {
Icon(Icons.Rounded.ArrowBackIosNew, contentDescription = null)
}
// FilledTonalIconButton(onClick = toggleFullScreen) {
// Icon(Icons.Rounded.Fullscreen, contentDescription = null)
// }
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.khaled.leanstream.player.controller

import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun PlayPauseToggle(
modifier: Modifier = Modifier, isPlaying: Boolean, error: String?, onToggle: () -> Unit,
) {
//TODO Show error message somehow??
FilledTonalIconButton(modifier = modifier.size(80.dp), onClick = onToggle) {
Icon(
when {
error != null -> Icons.Rounded.ErrorOutline
isPlaying -> Icons.Rounded.Pause
else -> Icons.Rounded.PlayArrow
}, contentDescription = null, modifier = Modifier.size(80.dp)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package dev.khaled.leanstream.player.controller

import android.content.pm.ActivityInfo
import android.os.Handler
import android.os.Looper
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_BUFFERING
import androidx.media3.exoplayer.ExoPlayer
import dev.khaled.leanstream.channels.Channel
import dev.khaled.leanstream.conditional
import dev.khaled.leanstream.isRunningOnTV
import dev.khaled.leanstream.ui.ScreenOrientation

@Composable
fun PlayerController(
modifier: Modifier = Modifier,
channel: Channel,
player: ExoPlayer,
backHandler: () -> Unit,
) {


val isRunningOnTV = isRunningOnTV(LocalContext.current)

var controllerVisible by remember { mutableStateOf(true) }

var isPlaying by remember { mutableStateOf(true) }
var isBuffering by remember { mutableStateOf(true) }
var error by remember { mutableStateOf<String?>(null) }


val handler = remember { Handler(Looper.getMainLooper()) }
val controllerVisibilityRunnable =
remember { Runnable { if (controllerVisible) controllerVisible = false } }

fun triggerHideController() = run {
handler.removeCallbacks(controllerVisibilityRunnable)
handler.postDelayed(controllerVisibilityRunnable, 3000)
}

DisposableEffect(Unit) {
val listener = object : Player.Listener {
override fun onIsPlayingChanged(playing: Boolean) {
super.onIsPlayingChanged(playing)
isPlaying = playing
if (isPlaying) error = null
if (error == null) triggerHideController()
}

override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
isBuffering = playbackState == STATE_BUFFERING
controllerVisible = true
}

override fun onPlayerError(exception: PlaybackException) {
super.onPlayerError(exception)
error = exception.message
controllerVisible = true
}
}

player.addListener(listener)
onDispose {
player.removeListener(listener)
}
}


ScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)

Box(modifier = Modifier
.fillMaxSize()
.conditional(!controllerVisible) {
clickable(
interactionSource = MutableInteractionSource(),
indication = null,
onClick = {
controllerVisible = true
triggerHideController()
})
})



AnimatedVisibility(
modifier = modifier.fillMaxSize(), visible = controllerVisible,
) {
Box(modifier = Modifier
.fillMaxSize()
.conditional(!isRunningOnTV) {
clickable { controllerVisible = false }
}
.background(Color.Black.copy(alpha = 0.3f))) {

Box(modifier = Modifier.align(Alignment.Center)) {
if (isBuffering) CircularProgressIndicator()
else PlayPauseToggle(isPlaying = isPlaying, error = error) {
if (error != null) {
error = null
return@PlayPauseToggle player.prepare()
}

if (isPlaying) player.pause()
else player.play()
triggerHideController()
}
}

Row(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
) {
if (!isRunningOnTV) ExtraControls(backHandler)

Spacer(modifier = Modifier.weight(1f))

ChannelInfo(channel = channel)
}
}
}
}


28 changes: 28 additions & 0 deletions app/src/main/java/dev/khaled/leanstream/ui/ScreenOrientation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.khaled.leanstream.ui

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext

@Composable
fun ScreenOrientation(orientation: Int) {
val context = LocalContext.current
DisposableEffect(orientation) {
val activity = context.findActivity() ?: return@DisposableEffect onDispose {}
val originalOrientation = activity.requestedOrientation
activity.requestedOrientation = orientation
onDispose {
// restore original orientation when view disappears
activity.requestedOrientation = originalOrientation
}
}
}

fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}

0 comments on commit 16518e6

Please sign in to comment.