Release v0.2.0 · jhg3410/Movie
What's Changed double click 방지, 화면 전환 애니메이션 구현 by @jhg3410 in #12 Detail UI 수정 (Cast 추가), Pagination 버그 수정 by @jhg3410 in #13 Detail UI 수정 (Teaser 영상 추가) by @jhg3410 in #14 Full Changelog: v0.1.0...
github.com
lib-videoplayer 모듈에 관련된 모든 코드 존재.
Movie 프로젝트 시작점에서 언급했던 VideoPlayer를 넣었고, 과정을 얘기해 보려 한다.
아직 Custom Controller는 구현하지 않았으며, 추후 구현 후 작성 예정이다.
함께 보면 좋을 Custom Controller 구현에 대한 내용
https://jik3410.tistory.com/10
Movie 프로젝트 VideoPlayer Controller
https://github.com/jhg3410/Movie GitHub - jhg3410/Movie Contribute to jhg3410/Movie development by creating an account on GitHub. github.com lib-videoplayer 모듈에 관련된 모든 코드 존재. 모든 내용을 하나의 포스트로 담기엔 무리
jik3410.tistory.com
먼저 구현 화면부터 보자.
Movie 프로젝트 디자인에 있는 Detail UI 의 썸네일 크기와 상이하다.
대부분의 영상들이 가로였고, 그렇다 보니 api 가 제공하는 backdrop(현재 썸네일) 이미지의 크기로 썸네일과 영상을 보기 좋게 표현할 수 있었다.
어떻게 구현했을까?
사용 기술(라이브러리)
1. exoPlayer - 영상 플레이어
2. pytube - youtube url 로 video stream url 을 추출 (python)
3. chaquopy - python 코드를 android 에서 실행
video stream url 추출하기
TMDB 에서 제공하는 api 는 단순 youtube URL의 key(id) 값이다. 그렇기에 exoPlayer 에서는 사용할 수 없다.
그래서 pytube 와 chaquopy 를 활용해, youtube video 의 stream url 값을 추출해서 사용했다.
suspend fun String.toStreamUrlOfYouTube(context: Context): String {
val videoUrl = "https://www.youtube.com/watch?v=$this"
return withContext(Dispatchers.IO) {
if (!Python.isStarted()) {
Python.start(AndroidPlatform(context))
}
val py = Python.getInstance()
val module = py.getModule("youtube_stream_url")
val result = module.callAttr("getVideoStreamUrl", videoUrl)
result.toString()
}
}
from pytube import YouTube
def getVideoStreamUrl(videoId):
# YouTube URL
youtube_url = "https://www.youtube.com/watch?v=" + videoId
# Create a YouTube object
youtube = YouTube(youtube_url)
# Get the best stream (video) available
video_stream = youtube.streams.get_highest_resolution()
# Get the video URL
return video_stream.url
위의 코드들로 가져올 수 있었다. 자세한 사용법은 pytube 와 chaquopy 의 공식문서와 github 을 찾아보자.
// get Video Stream Url
videoUrl.toStreamUrlOfYouTube(context))
결과적으로 위 코드로 stream url 을 가져왔다.
videoPlayer initialize 와 release
fun initializePlayer() {
if (videoUrl == null) {
videoPlayerState = VideoPlayerState.NoVideo
return
}
coroutineScope.launch {
try {
player = ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videoUrl.toStreamUrlOfYouTube(context)))
addListener(renderListener)
prepare()
}
} catch (e: Exception) {
videoPlayerState = VideoPlayerState.GetError(e.message ?: "Unknown Error")
}
}
}
fun releasePlayer() {
player?.let {
it.removeListener(renderListener)
it.release()
}
player = null
}
initialize 와 release 를 신경을 써야하는 이유는 background 로 가면 영상을 끊어야 한다.
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)
}
}
그렇기에 생명주기를 관찰해 onStart 에서 initialize, onStop 에서 release 하도록 구현했다.
fun renderListener(playVideo: () -> Unit) = object : Player.Listener {
override fun onRenderedFirstFrame() {
playVideo()
super.onRenderedFirstFrame()
}
}
val renderListener = VideoPlayerUtil.renderListener { player?.play() }
// use in initializePlayer
player = ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videoUrl.toStreamUrlOfYouTube(context)))
addListener(renderListener)
prepare()
}
Player 를 초기화할떄 renderListener 라는 listener 가 들어간다.
해당 lisetner 는 영상의 첫 프레임이 렌더링되면 호출되는데 그때 영상을 재생하게 하도록 했다.
이유는 player 에는 playWhenReady 라는 함수가 존재하는데 영상이 준비되면 바로 재생하도록 한다.
하지만 playWhenReady 를 true 로 하면 오디오는 출력이 되지만 영상 ui 가 나타나지 않았다.
그렇기에 frame 이 그려지면 그때 영상을 재생하도록 해서 버그를 해결했다.
videoPlayer 상태에 따른 UI 표시
나타날 수 있는 videoPlayer 의 상태를 본다면
1. INITIAL (초기 상태, 썸네일이 표시)
2. LOADING (재생을 클릭 후 준비가 되기 전 상태)
3. GET_ERROR (영상을 가져오는데 에러가 발생한 상태)
4. NO_VIDEO (재생할 영상이 없는 상태)
5. CAN_PLAY (준비가 되어 재생가능한 상태)
위와 같이 5개의 상태로 나타낼 수 있다.
sealed interface VideoPlayerState {
object Initial : VideoPlayerState
object Loading : VideoPlayerState
class GetError(val errorMessage: String) : VideoPlayerState
object NoVideo : VideoPlayerState
object CanPlay : VideoPlayerState
}
상태에 대한 코드는 위와 같이 구현
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
when (videoPlayerState) {
is VideoPlayerState.Initial -> {
Thumbnail()
ThumbnailPlayIcon {
videoPlayerState = VideoPlayerState.Loading
}
}
is VideoPlayerState.Loading -> {
Thumbnail()
ThumbnailLoadingWheel()
if (player != null) {
videoPlayerState = VideoPlayerState.CanPlay
}
}
is VideoPlayerState.CanPlay -> {
VideoPlayerScreen(player = player ?: return)
}
is VideoPlayerState.GetError -> {
ErrorScreen(
errorMessage = (videoPlayerState as VideoPlayerState.GetError).errorMessage,
onRefreshClick = {
initializePlayer()
videoPlayerState = VideoPlayerState.Loading
}
)
}
is VideoPlayerState.NoVideo -> {
ErrorScreen(errorMessage = "No Video Found")
}
}
}
이제 사용자라고 생각하고 Flow 를 따라가보자.
var videoPlayerState: VideoPlayerState by remember { mutableStateOf(VideoPlayerState.Initial) }
처음 화면을 들어오면 당연히 INITIAL 상태이다.
is VideoPlayerState.Initial -> {
Thumbnail()
ThumbnailPlayIcon {
videoPlayerState = VideoPlayerState.Loading
}
}
INITIAL 에선 썸네일이 보이며, 재생 Icon 을 클릭하면 State LOADING 으로 변경된다.
is VideoPlayerState.Loading -> {
Thumbnail()
ThumbnailLoadingWheel()
if (player != null) {
videoPlayerState = VideoPlayerState.CanPlay
}
}
LOADING 상태에도 썸네일은 보이며, Loadingwheel 이 표시된다.
그리고 player 가 null 이 아니라면 상태를 CAN_PLAY 로 변경한다.
(player == null -> initialze 함수가 완료되지 않았다는 의미)
is VideoPlayerState.CanPlay -> {
VideoPlayerScreen(player = player ?: return)
}
CAN_PLAY 라면 영상 Screen(Player View) 을 UI 에 나타낸다.
그런데 만약 영상을 가져오는데 실패했다면?
try {
player = ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videoUrl.toStreamUrlOfYouTube(context)))
addListener(renderListener)
prepare()
}
} catch (e: Exception) {
videoPlayerState = VideoPlayerState.GetError(e.message ?: "Unknown Error")
}
위와 같이 stream url 을 가져오는데 예외가 던져졌다면 이제 상태를 GET_ERROR 로 변경한다.
is VideoPlayerState.GetError -> {
ErrorScreen(
errorMessage = (videoPlayerState as VideoPlayerState.GetError).errorMessage,
onRefreshClick = {
initializePlayer()
videoPlayerState = VideoPlayerState.Loading
}
)
}
GET_ERROR 상태라면 ErrorScreen 을 화면에 나타낸다.
새로고침 Icon 을 클릭하면 Player 를 다시 initialize , 상태는 LOADING 으로 변경된다.
if (videoUrl == null) {
videoPlayerState = VideoPlayerState.NoVideo
return
}
만약 videoPlayer 에 videoUrl 이 null 로 온다면? -> 재생시킬 영상이 없다는 의미
is VideoPlayerState.NoVideo -> {
ErrorScreen(errorMessage = "No Video Found")
}
errorMessage 를 적절히 배치하고 ErrorScreen 을 화면에 나타냈다.
위 영상이 GET_ERROR 상태를 잘 나타내주는 영상이다.
Detail UI 수정 (Teaser 영상 추가) by jhg3410 · Pull Request #14 · jhg3410/Movie
PR.14.mp4 videoPlayer module 생성 Movie backdrop 을 thumbnail 로 사용 pytube 와 chaquopy 를 사용해 youtube 영상의 streamUrl 을 GET streamUrl 로 videoplayer 재생 no video found 및 각종 에러에 따른 message ...
github.com
'Movie' 카테고리의 다른 글
Movie 프로젝트 VideoPlayer Controller (3) | 2023.09.28 |
---|---|
Movie 프로젝트 edge-to-edge (0) | 2023.07.16 |
Movie 프로젝트 Pagination (0) | 2023.07.15 |
Movie 프로젝트 네트워크 통신 (0) | 2023.07.15 |
Movie 프로젝트 구조 (0) | 2023.07.15 |