Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save chibaye/2eda6ddaa08f81be0d71048b9d989cb5 to your computer and use it in GitHub Desktop.
Save chibaye/2eda6ddaa08f81be0d71048b9d989cb5 to your computer and use it in GitHub Desktop.

Revisions

  1. @rubenquadros rubenquadros revised this gist Aug 7, 2022. 1 changed file with 35 additions and 12 deletions.
    47 changes: 35 additions & 12 deletions CustomExoPlayerControls.kt
    Original file line number Diff line number Diff line change
    @@ -40,6 +40,8 @@ private fun VideoPlayer(modifier: Modifier = Modifier) {

    var bufferedPercentage by remember { mutableStateOf(0) }

    var playbackState by remember { mutableStateOf(exoPlayer.playbackState) }

    Box(modifier = modifier) {
    DisposableEffect(key1 = Unit) {
    val listener =
    @@ -53,6 +55,7 @@ private fun VideoPlayer(modifier: Modifier = Modifier) {
    currentTime = player.currentPosition.coerceAtLeast(0L)
    bufferedPercentage = player.bufferedPercentage
    isPlaying = player.isPlaying
    playbackState = player.playbackState
    }
    }

    @@ -87,16 +90,25 @@ private fun VideoPlayer(modifier: Modifier = Modifier) {
    isVisible = { shouldShowControls },
    isPlaying = { isPlaying },
    title = { exoPlayer.mediaMetadata.displayTitle.toString() },
    playbackState = { playbackState },
    onReplayClick = { exoPlayer.seekBack() },
    onForwardClick = { exoPlayer.seekForward() },
    onPauseToggle = {
    if (exoPlayer.isPlaying) {
    // pause the video
    exoPlayer.pause()
    } else {
    // play the video
    // it's already paused
    exoPlayer.play()
    when {
    exoPlayer.isPlaying -> {
    // pause the video
    exoPlayer.pause()
    }
    exoPlayer.isPlaying.not() &&
    playbackState == STATE_ENDED -> {
    exoPlayer.seekTo(0)
    exoPlayer.playWhenReady = true
    }
    else -> {
    // play the video
    // it's already paused
    exoPlayer.play()
    }
    }
    isPlaying = isPlaying.not()
    },
    @@ -123,6 +135,7 @@ private fun PlayerControls(
    totalDuration: () -> Long,
    currentTime: () -> Long,
    bufferedPercentage: () -> Int,
    playbackState: () -> Int,
    onSeekChanged: (timeMs: Float) -> Unit
    ) {

    @@ -145,7 +158,8 @@ private fun PlayerControls(
    isPlaying = isPlaying,
    onReplayClick = onReplayClick,
    onForwardClick = onForwardClick,
    onPauseToggle = onPauseToggle
    onPauseToggle = onPauseToggle,
    playbackState = playbackState
    )

    BottomControls(
    @@ -191,12 +205,15 @@ private fun TopControl(modifier: Modifier = Modifier, title: () -> String) {
    private fun CenterControls(
    modifier: Modifier = Modifier,
    isPlaying: () -> Boolean,
    playbackState: () -> Int,
    onReplayClick: () -> Unit,
    onPauseToggle: () -> Unit,
    onForwardClick: () -> Unit
    ) {
    val isVideoPlaying = remember(isPlaying()) { isPlaying() }

    val playerState = remember(playbackState()) { playbackState() }

    Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceEvenly) {
    IconButton(modifier = Modifier.size(40.dp), onClick = onReplayClick) {
    Image(
    @@ -212,10 +229,16 @@ private fun CenterControls(
    modifier = Modifier.fillMaxSize(),
    contentScale = ContentScale.Crop,
    painter =
    if (isVideoPlaying) {
    painterResource(id = R.drawable.ic_pause)
    } else {
    painterResource(id = R.drawable.ic_play)
    when {
    isVideoPlaying -> {
    painterResource(id = R.drawable.ic_pause)
    }
    isVideoPlaying.not() && playerState == STATE_ENDED -> {
    painterResource(id = R.drawable.ic_replay)
    }
    else -> {
    painterResource(id = R.drawable.ic_play)
    }
    },
    contentDescription = "Play/Pause"
    )
  2. @rubenquadros rubenquadros revised this gist Aug 7, 2022. 1 changed file with 12 additions and 11 deletions.
    23 changes: 12 additions & 11 deletions CustomExoPlayerControls.kt
    Original file line number Diff line number Diff line change
    @@ -1,18 +1,8 @@
    @Composable
    fun VideoPlayer(modifier: Modifier = Modifier) {
    private fun VideoPlayer(modifier: Modifier = Modifier) {

    val context = LocalContext.current

    var shouldShowControls by remember { mutableStateOf(false) }

    var isPlaying by remember { mutableStateOf(true) }

    var totalDuration by remember { mutableStateOf(0L) }

    var currentTime by remember { mutableStateOf(0L) }

    var bufferedPercentage by remember { mutableStateOf(0) }

    val exoPlayer = remember {
    ExoPlayer.Builder(context)
    .apply {
    @@ -40,6 +30,16 @@ fun VideoPlayer(modifier: Modifier = Modifier) {
    }
    }

    var shouldShowControls by remember { mutableStateOf(false) }

    var isPlaying by remember { mutableStateOf(exoPlayer.isPlaying) }

    var totalDuration by remember { mutableStateOf(0L) }

    var currentTime by remember { mutableStateOf(0L) }

    var bufferedPercentage by remember { mutableStateOf(0) }

    Box(modifier = modifier) {
    DisposableEffect(key1 = Unit) {
    val listener =
    @@ -52,6 +52,7 @@ fun VideoPlayer(modifier: Modifier = Modifier) {
    totalDuration = player.duration.coerceAtLeast(0L)
    currentTime = player.currentPosition.coerceAtLeast(0L)
    bufferedPercentage = player.bufferedPercentage
    isPlaying = player.isPlaying
    }
    }

  3. @rubenquadros rubenquadros revised this gist Jul 31, 2022. 1 changed file with 34 additions and 11 deletions.
    45 changes: 34 additions & 11 deletions CustomExoPlayerControls.kt
    Original file line number Diff line number Diff line change
    @@ -11,6 +11,8 @@ fun VideoPlayer(modifier: Modifier = Modifier) {

    var currentTime by remember { mutableStateOf(0L) }

    var bufferedPercentage by remember { mutableStateOf(0) }

    val exoPlayer = remember {
    ExoPlayer.Builder(context)
    .apply {
    @@ -49,6 +51,7 @@ fun VideoPlayer(modifier: Modifier = Modifier) {
    super.onEvents(player, events)
    totalDuration = player.duration.coerceAtLeast(0L)
    currentTime = player.currentPosition.coerceAtLeast(0L)
    bufferedPercentage = player.bufferedPercentage
    }
    }

    @@ -98,6 +101,7 @@ fun VideoPlayer(modifier: Modifier = Modifier) {
    },
    totalDuration = { totalDuration },
    currentTime = { currentTime },
    bufferedPercentage = { bufferedPercentage },
    onSeekChanged = { timeMs: Float ->
    exoPlayer.seekTo(timeMs.toLong())
    }
    @@ -117,6 +121,7 @@ private fun PlayerControls(
    onPauseToggle: () -> Unit,
    totalDuration: () -> Long,
    currentTime: () -> Long,
    bufferedPercentage: () -> Int,
    onSeekChanged: (timeMs: Float) -> Unit
    ) {

    @@ -162,6 +167,7 @@ private fun PlayerControls(
    ),
    totalDuration = totalDuration,
    currentTime = currentTime,
    bufferedPercentage = bufferedPercentage,
    onSeekChanged = onSeekChanged
    )
    }
    @@ -230,25 +236,42 @@ private fun BottomControls(
    modifier: Modifier = Modifier,
    totalDuration: () -> Long,
    currentTime: () -> Long,
    bufferedPercentage: () -> Int,
    onSeekChanged: (timeMs: Float) -> Unit
    ) {

    val duration = remember(totalDuration()) { totalDuration() }

    val videoTime = remember(currentTime()) { currentTime() }

    val buffer = remember(bufferedPercentage()) { bufferedPercentage() }

    Column(modifier = modifier.padding(bottom = 32.dp)) {
    Slider(
    modifier = Modifier.fillMaxWidth(),
    value = videoTime.toFloat(),
    onValueChange = onSeekChanged,
    valueRange = 0f..duration.toFloat(),
    colors =
    SliderDefaults.colors(
    thumbColor = Purple200,
    activeTickColor = Purple200
    )
    )
    Box(modifier = Modifier.fillMaxWidth()) {
    Slider(
    value = buffer.toFloat(),
    enabled = false,
    onValueChange = { /*do nothing*/},
    valueRange = 0f..100f,
    colors =
    SliderDefaults.colors(
    disabledThumbColor = Color.Transparent,
    disabledActiveTrackColor = Color.Gray
    )
    )

    Slider(
    modifier = Modifier.fillMaxWidth(),
    value = videoTime.toFloat(),
    onValueChange = onSeekChanged,
    valueRange = 0f..duration.toFloat(),
    colors =
    SliderDefaults.colors(
    thumbColor = Purple200,
    activeTickColor = Purple200
    )
    )
    }

    Row(
    modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
  4. @rubenquadros rubenquadros created this gist Jul 31, 2022.
    293 changes: 293 additions & 0 deletions CustomExoPlayerControls.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,293 @@
    @Composable
    fun VideoPlayer(modifier: Modifier = Modifier) {

    val context = LocalContext.current

    var shouldShowControls by remember { mutableStateOf(false) }

    var isPlaying by remember { mutableStateOf(true) }

    var totalDuration by remember { mutableStateOf(0L) }

    var currentTime by remember { mutableStateOf(0L) }

    val exoPlayer = remember {
    ExoPlayer.Builder(context)
    .apply {
    setSeekBackIncrementMs(PLAYER_SEEK_BACK_INCREMENT)
    setSeekForwardIncrementMs(PLAYER_SEEK_FORWARD_INCREMENT)
    }
    .build()
    .apply {
    setMediaItem(
    MediaItem.Builder()
    .apply {
    setUri(
    "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4"
    )
    setMediaMetadata(
    MediaMetadata.Builder()
    .setDisplayTitle("My Video")
    .build()
    )
    }
    .build()
    )
    prepare()
    playWhenReady = true
    }
    }

    Box(modifier = modifier) {
    DisposableEffect(key1 = Unit) {
    val listener =
    object : Player.Listener {
    override fun onEvents(
    player: Player,
    events: Player.Events
    ) {
    super.onEvents(player, events)
    totalDuration = player.duration.coerceAtLeast(0L)
    currentTime = player.currentPosition.coerceAtLeast(0L)
    }
    }

    exoPlayer.addListener(listener)

    onDispose {
    exoPlayer.removeListener(listener)
    exoPlayer.release()
    }
    }

    AndroidView(
    modifier =
    Modifier.clickable {
    shouldShowControls = shouldShowControls.not()
    },
    factory = {
    StyledPlayerView(context).apply {
    player = exoPlayer
    useController = false
    layoutParams =
    FrameLayout.LayoutParams(
    ViewGroup.LayoutParams.MATCH_PARENT,
    ViewGroup.LayoutParams.MATCH_PARENT
    )
    }
    }
    )

    PlayerControls(
    modifier = Modifier.fillMaxSize(),
    isVisible = { shouldShowControls },
    isPlaying = { isPlaying },
    title = { exoPlayer.mediaMetadata.displayTitle.toString() },
    onReplayClick = { exoPlayer.seekBack() },
    onForwardClick = { exoPlayer.seekForward() },
    onPauseToggle = {
    if (exoPlayer.isPlaying) {
    // pause the video
    exoPlayer.pause()
    } else {
    // play the video
    // it's already paused
    exoPlayer.play()
    }
    isPlaying = isPlaying.not()
    },
    totalDuration = { totalDuration },
    currentTime = { currentTime },
    onSeekChanged = { timeMs: Float ->
    exoPlayer.seekTo(timeMs.toLong())
    }
    )
    }
    }

    @OptIn(ExperimentalAnimationApi::class)
    @Composable
    private fun PlayerControls(
    modifier: Modifier = Modifier,
    isVisible: () -> Boolean,
    isPlaying: () -> Boolean,
    title: () -> String,
    onReplayClick: () -> Unit,
    onForwardClick: () -> Unit,
    onPauseToggle: () -> Unit,
    totalDuration: () -> Long,
    currentTime: () -> Long,
    onSeekChanged: (timeMs: Float) -> Unit
    ) {

    val visible = remember(isVisible()) { isVisible() }

    AnimatedVisibility(
    modifier = modifier,
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
    ) {
    Box(modifier = Modifier.background(Color.Black.copy(alpha = 0.6f))) {
    TopControl(
    modifier = Modifier.align(Alignment.TopStart).fillMaxWidth(),
    title = title
    )

    CenterControls(
    modifier = Modifier.align(Alignment.Center).fillMaxWidth(),
    isPlaying = isPlaying,
    onReplayClick = onReplayClick,
    onForwardClick = onForwardClick,
    onPauseToggle = onPauseToggle
    )

    BottomControls(
    modifier =
    Modifier.align(Alignment.BottomCenter)
    .fillMaxWidth()
    .animateEnterExit(
    enter =
    slideInVertically(
    initialOffsetY = { fullHeight: Int ->
    fullHeight
    }
    ),
    exit =
    slideOutVertically(
    targetOffsetY = { fullHeight: Int ->
    fullHeight
    }
    )
    ),
    totalDuration = totalDuration,
    currentTime = currentTime,
    onSeekChanged = onSeekChanged
    )
    }
    }
    }

    @Composable
    private fun TopControl(modifier: Modifier = Modifier, title: () -> String) {
    val videoTitle = remember(title()) { title() }

    Text(
    modifier = modifier.padding(16.dp),
    text = videoTitle,
    style = MaterialTheme.typography.h6,
    color = Purple200
    )
    }

    @Composable
    private fun CenterControls(
    modifier: Modifier = Modifier,
    isPlaying: () -> Boolean,
    onReplayClick: () -> Unit,
    onPauseToggle: () -> Unit,
    onForwardClick: () -> Unit
    ) {
    val isVideoPlaying = remember(isPlaying()) { isPlaying() }

    Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceEvenly) {
    IconButton(modifier = Modifier.size(40.dp), onClick = onReplayClick) {
    Image(
    modifier = Modifier.fillMaxSize(),
    contentScale = ContentScale.Crop,
    painter = painterResource(id = R.drawable.ic_replay_5),
    contentDescription = "Replay 5 seconds"
    )
    }

    IconButton(modifier = Modifier.size(40.dp), onClick = onPauseToggle) {
    Image(
    modifier = Modifier.fillMaxSize(),
    contentScale = ContentScale.Crop,
    painter =
    if (isVideoPlaying) {
    painterResource(id = R.drawable.ic_pause)
    } else {
    painterResource(id = R.drawable.ic_play)
    },
    contentDescription = "Play/Pause"
    )
    }

    IconButton(modifier = Modifier.size(40.dp), onClick = onForwardClick) {
    Image(
    modifier = Modifier.fillMaxSize(),
    contentScale = ContentScale.Crop,
    painter = painterResource(id = R.drawable.ic_forward_10),
    contentDescription = "Forward 10 seconds"
    )
    }
    }
    }

    @Composable
    private fun BottomControls(
    modifier: Modifier = Modifier,
    totalDuration: () -> Long,
    currentTime: () -> Long,
    onSeekChanged: (timeMs: Float) -> Unit
    ) {

    val duration = remember(totalDuration()) { totalDuration() }

    val videoTime = remember(currentTime()) { currentTime() }

    Column(modifier = modifier.padding(bottom = 32.dp)) {
    Slider(
    modifier = Modifier.fillMaxWidth(),
    value = videoTime.toFloat(),
    onValueChange = onSeekChanged,
    valueRange = 0f..duration.toFloat(),
    colors =
    SliderDefaults.colors(
    thumbColor = Purple200,
    activeTickColor = Purple200
    )
    )

    Row(
    modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
    horizontalArrangement = Arrangement.SpaceBetween
    ) {
    Text(
    modifier = Modifier.padding(horizontal = 16.dp),
    text = duration.formatMinSec(),
    color = Purple200
    )

    IconButton(
    modifier = Modifier.padding(horizontal = 16.dp),
    onClick = {}
    ) {
    Image(
    contentScale = ContentScale.Crop,
    painter = painterResource(id = R.drawable.ic_fullscreen),
    contentDescription = "Enter/Exit fullscreen"
    )
    }
    }
    }
    }

    fun Long.formatMinSec(): String {
    return if (this == 0L) {
    "..."
    } else {
    String.format(
    "%02d:%02d",
    TimeUnit.MILLISECONDS.toMinutes(this),
    TimeUnit.MILLISECONDS.toSeconds(this) -
    TimeUnit.MINUTES.toSeconds(
    TimeUnit.MILLISECONDS.toMinutes(this)
    )
    )
    }
    }

    private const val PLAYER_SEEK_BACK_INCREMENT = 5 * 1000L // 5 seconds
    private const val PLAYER_SEEK_FORWARD_INCREMENT = 10 * 1000L // 10 seconds