본문 바로가기

Movie

Movie 프로젝트 VideoPlayer

v0.2.0 기준

 

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 상태를 잘 나타내주는 영상이다.

 

 

 

글에 대한 내용이 담긴 PR

 

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