Skip to content

Commit

Permalink
Merge pull request #15 from jhg3410/videoplayer-controller
Browse files Browse the repository at this point in the history
videoplayer controller 구현(Detail UI)
  • Loading branch information
jhg3410 authored Sep 25, 2023
2 parents 31dafe5 + 5ec0737 commit 6a66958
Show file tree
Hide file tree
Showing 23 changed files with 681 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.jik.core.designsystem.component.LoadingWheel
Expand All @@ -32,7 +33,7 @@ import com.jik.core.designsystem.icon.MovieIcons
import com.jik.core.model.MovieInfo
import com.jik.core.ui.state.UiState
import com.jik.core.ui.util.MovieGenreUtils
import com.jik.lib.videoplayer.ui.VideoPlayer
import com.jik.lib.videoplayer.VideoPlayer


@Composable
Expand All @@ -42,7 +43,9 @@ fun DetailScreen(
navigateUp: () -> Unit
) {

val detailUiState = viewModel.detailUiState.collectAsStateWithLifecycle().value
val detailUiState = viewModel.detailUiState.collectAsStateWithLifecycle(
minActiveState = Lifecycle.State.CREATED
).value

when (detailUiState) {
is UiState.Loading -> {
Expand Down Expand Up @@ -100,7 +103,7 @@ private fun Content(
Column(modifier = modifier) {
VideoPlayer(
modifier = Modifier.aspectRatio(500f / 281f),
Thumbnail = {
thumbnail = {
PosterCard(
posterPath = movieInfo.getBackdropUrl(),
modifier = Modifier.fillMaxSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class DetailViewModel @Inject constructor(
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
started = SharingStarted.WhileSubscribed(),
initialValue = UiState.Loading
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -29,7 +30,7 @@ fun HomeScreen(
onPosterClick: (Long) -> Unit,
) {

val mainMovie = homeViewModel.mainMovie.collectAsStateWithLifecycle().value
val mainMovie by homeViewModel.mainMovie.collectAsStateWithLifecycle()

Box(modifier = modifier) {
Content(
Expand Down
213 changes: 213 additions & 0 deletions lib-videoplayer/src/main/java/com/jik/lib/videoplayer/VideoPlayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package com.jik.lib.videoplayer

import androidx.compose.foundation.layout.Box
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import com.jik.lib.videoplayer.component.player.PlayerLoadingWheel
import com.jik.lib.videoplayer.component.player.PlayerPlayIcon
import com.jik.lib.videoplayer.error.ErrorScreen
import com.jik.lib.videoplayer.error.VideoPlayerControllerError.setErrorMessage
import com.jik.lib.videoplayer.error.VideoPlayerError.toMovieErrorMessage
import com.jik.lib.videoplayer.state.VideoPlayerControllerState
import com.jik.lib.videoplayer.state.VideoPlayerState
import com.jik.lib.videoplayer.state.getControllerState
import com.jik.lib.videoplayer.ui.VideoPlayerController
import com.jik.lib.videoplayer.ui.VideoPlayerScreen
import com.jik.lib.videoplayer.util.VideoPlayerControllerUtil.VISIBLE_DURATION
import com.jik.lib.videoplayer.util.VideoPlayerListener.stateChangedListener
import com.jik.lib.videoplayer.util.VideoPlayerUtil.toStreamUrlOfYouTube
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds


@Composable
fun VideoPlayer(
modifier: Modifier = Modifier,
thumbnail: @Composable () -> Unit,
videoUrl: String?
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var streamUrl: String? = remember { null }

var player: ExoPlayer? by remember { mutableStateOf(null) }
var videoPlayerState: VideoPlayerState by remember { mutableStateOf(VideoPlayerState.Initial) }

var controllerVisible by remember { mutableStateOf(true) }
var isPlaying by remember { mutableStateOf(false) }
var currentPosition by remember { mutableStateOf(0L) }
var controllerState: VideoPlayerControllerState by remember {
mutableStateOf(VideoPlayerControllerState.INITIAL)
}

val stateChangedListener = stateChangedListener { changedPlayer ->
isPlaying = changedPlayer.isPlaying
currentPosition = changedPlayer.currentPosition
controllerState = getControllerState(
isPlaying = isPlaying,
playbackState = changedPlayer.playbackState
)
}

LaunchedEffect(key1 = controllerState, key2 = controllerVisible) {
if (controllerState == VideoPlayerControllerState.PLAYING && controllerVisible) {
delay(VISIBLE_DURATION)
controllerVisible = false
}
}

if (isPlaying) {
LaunchedEffect(key1 = Unit) {
while (player != null) {
currentPosition = player?.currentPosition ?: 0L
delay(1.seconds / 30)
}
}
}

fun initializePlayer() {
if (videoUrl == null) {
videoPlayerState = VideoPlayerState.NoVideo
return
}
controllerVisible = true
videoPlayerState = VideoPlayerState.Initial

coroutineScope.launch {
try {
player = ExoPlayer.Builder(context).build().apply {
setMediaItem(
MediaItem.fromUri(streamUrl ?: videoUrl.toStreamUrlOfYouTube(context).also {
streamUrl = it
}),
currentPosition
)
playWhenReady = false
addListener(stateChangedListener)
prepare()
}
} catch (e: Exception) {
videoPlayerState = VideoPlayerState.GetError(e.toMovieErrorMessage())
}
}
}

fun releasePlayer() {
player?.let {
it.removeListener(stateChangedListener)
it.release()
}
player = null
}

val lifeCycleOwner by rememberUpdatedState(newValue = LocalLifecycleOwner.current)

DisposableEffect(key1 = lifeCycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> {
initializePlayer()
}

Lifecycle.Event.ON_STOP -> {
releasePlayer()
}

else -> Unit
}
}

lifeCycleOwner.lifecycle.addObserver(observer)

onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
}
}

Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
when (videoPlayerState) {
is VideoPlayerState.Initial -> {
thumbnail()
PlayerPlayIcon {
videoPlayerState = VideoPlayerState.Loading
}
}

is VideoPlayerState.Loading -> {
thumbnail()
PlayerLoadingWheel()
player?.let {
videoPlayerState = VideoPlayerState.CanPlay
it.play()
}
}

is VideoPlayerState.CanPlay -> {
val moviePlayer = player ?: return

VideoPlayerScreen(
player = moviePlayer,
onScreenClick = { controllerVisible = !controllerVisible }
)

VideoPlayerController(
modifier = Modifier.fillMaxSize(),
visible = controllerVisible,
controllerState = controllerState.apply {
if (this is VideoPlayerControllerState.ERROR) {
this.setErrorMessage(errorCode = moviePlayer.playerError?.errorCode)
}
},
onRefresh = {
moviePlayer.prepare()
moviePlayer.play()
},
onPlay = moviePlayer::play,
onPause = moviePlayer::pause,
onReplay = moviePlayer::seekTo,
onForward = moviePlayer::seekTo,
onBackward = moviePlayer::seekTo,
getCurrentPosition = moviePlayer::getCurrentPosition,
currentPosition = currentPosition,
duration = moviePlayer.contentDuration,
bufferedPercentage = moviePlayer.bufferedPercentage,
onSlide = moviePlayer::seekTo
)
}

is VideoPlayerState.GetError -> {
ErrorScreen(
errorMessage = (videoPlayerState as VideoPlayerState.GetError).errorMessage,
onRefresh = {
initializePlayer()
videoPlayerState = VideoPlayerState.Loading
}
)
}

is VideoPlayerState.NoVideo -> {
ErrorScreen(errorMessage = "No Video Found")
}
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
package com.jik.lib.videoplayer.component

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Forward5
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Replay5
import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.unit.dp

internal object VideoPlayerIcons {

val Play = Icons.Filled.PlayArrow
val Pause = Icons.Rounded.Pause
val Replay = Icons.Rounded.Refresh
val Refresh = Icons.Rounded.Refresh
}

val Forward5 = Icons.Filled.Forward5
val Backward5 = Icons.Filled.Replay5
}

internal val iconSize = 40.dp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.jik.lib.videoplayer.component.controller

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.jik.lib.videoplayer.component.iconSize

@Composable
fun ControllerLoadingWheel(
modifier: Modifier = Modifier
) {
Box(modifier = modifier.size(iconSize)) {
CircularProgressIndicator(
color = Color.White,
modifier = modifier.fillMaxSize()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.jik.lib.videoplayer.component.controller

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.jik.lib.videoplayer.component.VideoPlayerIcons.Pause
import com.jik.lib.videoplayer.component.iconSize

@Composable
fun ControllerPauseIcon(
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
IconButton(onClick = onClick, modifier = modifier.size(iconSize)) {
Icon(
modifier = modifier.fillMaxSize(),
imageVector = Pause,
contentDescription = "Pause",
tint = Color.White,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.jik.lib.videoplayer.component.controller

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.jik.lib.videoplayer.component.VideoPlayerIcons.Play
import com.jik.lib.videoplayer.component.iconSize

@Composable
fun ControllerPlayIcon(
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
IconButton(onClick = onClick, modifier = modifier.size(iconSize)) {
Icon(
modifier = modifier.fillMaxSize(),
imageVector = Play,
contentDescription = "Pause",
tint = Color.White,
)
}
}
Loading

0 comments on commit 6a66958

Please sign in to comment.