해당 프로젝트 관련 글
[Android] 자동 로그인 with DataStore(Kotlin)
이전 글에서는 비디오 타임라인에 대해 시간대를 제공하고, 동영상 시점을 변화시켰습니다.
이번글에서는 갤러리 앱처럼 편집한 구간에 대해 재생할 수 있는 컨트롤러를 적용해 볼 예정입니다.
기능 요구사항
- 클립의 구간을 벗어날 수 없다.
- 재생을 하는 도중 타임라인이 변경될 경우 동영상 재생을 정지한다.
- 동영상이 재생하는 동안 타임라인도 같이 이동한다.
그러기 위해선 동영상의 재생을 관리하는 컨트롤러가 필요합니다.
기본적으로 ExoPlayer에서 컨트롤러를 제공하고, 이를 사용하기 위해선 다음과 같은 코드 설정이 필요합니다.
app:use_controller="true" //XML PlayerView
위 컨트롤러가 Exoplayer의 기본적인 컨트롤러 UI입니다.
편집된 동영상의 재생을 관리하기 위해선 UI가 과도하게 구현되었다고 생각합니다.
재생과 정지 버튼만 있으면 되기 때문이죠. 따라서 커스텀 컨트롤러를 구현해보기로 했습니다.
커스텀 컨트롤러 구현하기
ExoPlayer의 컨트롤러를 커스텀하는 방법은 공식문서에 친절하게 나와있습니다.
https://developer.android.com/codelabs/exoplayer-intro?hl=ko#6
우선 커스텀할 컨트롤러의 UI를 구현해야 합니다.
커스텀 컨트롤러 UI 구현
저는 간단하게 재생버튼과 동영상의 현재 시간/최대 시간을 표현하는 UI를 만들어볼까 합니다.
ExoPlayer에서 커스텀 컨트롤러를 구현하기 위한 편의성을 굉장히 많이 제공해주기 때문에 정말 간단합니다.
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:gravity="center"
android:orientation="horizontal"
android:background="@drawable/rectangle_fill_light_purple_radius_40"
android:layout_marginBottom="5dp"
android:paddingEnd="5dp"
>
<ImageButton
android:id="@id/exo_play_pause"
style="@style/ExoMediaButton.Play"
android:layout_width="20dp"
android:layout_height="20dp"
android:scaleType="fitCenter"
app:tint="@color/black" />
<TextView
android:id="@id/exo_position"
style="@style/Typography.Body03.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"/>
<TextView
android:id="@+id/tv_divide_time"
style="@style/Typography.Body03.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/"
android:textColor="@color/black" />
<TextView
android:id="@id/exo_duration"
style="@style/Typography.Body03.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"/>
</LinearLayout>
구현 과정에서 특이했던 점은 컴포넌트에 대한 id와 style만 지정해주면 제 기능을 한다는 점이다.
- 현재 시간을 의미하는 exo_postion
- 동영상 길이를 의미하는 exo_duration
- 버튼을 눌렀을때 정지, 누르지 않을 경우 재생을 나타내는 버튼은 exo_player_pause
- 버튼 이미지를 의미하는 @style/ExoMediaButton.play
위와 같이 id와 style을 지정하면 각각의 역할을 하고, 값이 매핑됩니다.
정말 신기합니다.(위 TextView에 style은 제가 설정한 font이므로 무시하셔도 됩니다.)
다른 속성도 굉장히 많이 제공되고 있고, 필요에 따라 컨트롤러를 커스텀할 수 있는 것은 ExoPlayer의 장점인 것 같습니다.
ExoPlayer에서 제공되는 것들중 일부입니다.
나 이 UI로 컨트롤러를 쓸거야~라고 UI를 지정해 줍니다.
app:controller_layout_id="@layout/custom_player_controller"
컨트롤러가 적용됨을 확인할 수 있습니다.
저는 재생버튼과 정지버튼을 스위치처럼 사용했는데 만약 따로 두고 싶다면 play_pause가 아닌 play, pause를 따로 배치하면 되겠습니다.
재생 관리
첫 번째 해결해야 할 점은 어떻게 재생을 함과 동시에 타임라인을 이동시킬 수 있는가입니다.
처음엔 2가지 방법을 생각했습니다.
- Slider의 value를 증가시켜, ExoPlayer의 시점을 옮긴다.
- ExoPlayer를 재생시키고, 재생 시점을 Slider의 Value와 일치시킨다.
당연하게도 동영상 재생 관리는 ExoPlayer가 하는 것이 맞기 때문에 1번이 아닌 2번을 택했습니다.
1번 방법을 선택하면 Slider의 변화 요인에 따라 ExoPlayer가 이동하고, Slider의 변화요인은 다양하기에 오류가 생긴다면 특정하기 어렵다고 판단했습니다.
ExoPlayer 재생 상태 관찰
ExoPlayer의 재생 상태를 계속해서 관찰해야 했기에, 다음과 같은 코드를 작성했습니다.
exoPlayer.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
startTracking()
} else {
stopTracking()
}
}
})
위 함수는 exoPlayer의 상태를 관찰하는 함수로, isPlaying이라는 Boolean 값을 통해 재생 중인지, 아닌지를 판단해 줍니다.
각 상태에 따라 타임라인을 이동하는 함수와 그 이동을 중지하는 함수를 구현해 봤습니다.
private fun startTracking() {
trackingJob = lifecycleScope.launch {
while (true) {
trackingVideo()
delay(100)
}
}
}
private fun trackingVideo() {
player?.let { player ->
val currentPosition = player.currentPosition
val duration = player.duration
if (duration > 0) {
val positionRatio = currentPosition.toFloat() / duration * 100
binding.sliderVideoTime.value = positionRatio
}
}
}
private fun stopTracking() {
trackingJob?.cancel()
trackingJob = null
}
동영상이 재생되는 동안은 계속해서 트래킹을 지시합니다.
트래킹의 작업은 동영상의 길이대비 현재 재생시점을 영상 타임라인의 value로 일치화하는 작업입니다.
만약 동영상 재생이 정지된다면 트래킹 작업을 취소하고, null로 해제합니다.
즉 동영상이 재생되면 동영상 재생의 시점을 들고 와서 전체 길이에 대한 포지션을 계산하고 Slider의 Value와 일치화한다고 이해해 주시면 됩니다. [ 동영상 재생 -> 재생 시점 변화 -> 슬라이더 적용 ]
타임라인 Value 겹침 문제 발생
구현을 하고 보니 오류가 발생했다.
기존 작성했던 터치로 인한 타임 라인의 이동과 동영상 재생의 이벤트 처리가 겹쳐서 재생이 되지 않았습니다.
기존 코드는 타임 라인을 터치해서 value가 변화할 때마다 해당 값으로 exoPlayer의 포지션을 옮겨줬습니다.
현재는 동영상이 재생됨에 있어서 타임라인의 value가 변화하니 동영상이 제자리 재생되는 현상이 발생한 것입니다.
즉 동영상 재생 -> 슬라이더 값 변화 -> 동영상 시점 변화의 사이클이 무한 반복이라 동영상이 재생이 되지 않는 것입니다.
따라서 슬라이더의 Value 변화를 구별할 수 있는 flag값이 필요했습니다.
동영상 재생으로 인한 슬라이더의 변화와 유저 터치로 인한 슬라이더 변화를 구별하기 위한 flag값이 필요했는데,
감사하게도 Slider의 onChangedListener에 fromUser라는 flag가 있었습니다.
fromUser -> True라면 유저에 의해 변화한 값이고, false라면 유저가 아닌 다른 요인에 의해 변화한 값입니다.
따라서 fromUser를 기준으로 슬라이더 값 변화를 처리해 봤습니다.
sliderVideoTime.addOnChangeListener { slider, value, fromUser ->
val videoLength = player?.duration ?: 0
val newPosition = (videoLength * value / 100).toLong()
val minRangeValue = sliderVideoThumbnail.values[0]
val maxRangeValue = sliderVideoThumbnail.values[1]
if (fromUser) {
player?.run {
seekTo(newPosition)
pause()
}
if (value < minRangeValue) {
slider.value = minRangeValue
sliderVideoThumbnail.values = mutableListOf(value, maxRangeValue)
} else if (value > maxRangeValue) {
slider.value = maxRangeValue
sliderVideoThumbnail.values = mutableListOf(minRangeValue, value)
}
} else {
if (value < minRangeValue) {
slider.value = minRangeValue
} else if (value >= maxRangeValue) {
player?.run {
val newPosition =
(videoLength * sliderVideoThumbnail.values[0] / 100).toLong()
seekTo(newPosition)
pause()
}
}
}
동영상이 재생됨에 따라 타임라인이 이동하는 것을 확인할 수 있었습니다.
하지만 다른 문제가 발견되었습니다.
동영상 클립 범위 넘어감
동영상의 최소 재생 범위에 대해 코드가 적용되지 않아서 생긴 문제였습니다.
기존 코드에서 fromUser로 인한 분기 처리 코드에서 범위를 제한하는 코드가 빠졌던 것이었습니다.
재생에 대해 범위를 넘어가는 경우는 타임라인의 Handle이 오른쪽 클립을 추월하는 것입니다.
따라서 해당 부분을 중점으로 코드를 작성해 봤습니다.
if (value < minRangeValue) {
slider.value = minRangeValue
} else if (value >= maxRangeValue) {
player?.run {
val newPosition =
(videoLength * sliderVideoThumbnail.values[0] / 100).toLong()
seekTo(newPosition)
pause()
}
}
범위가 넘어간다면 동영상의 포지션을 왼쪽 클립에 맞춰주고, 동영상 재생을 중지했습니다.
최종 구현 결과입니다.
'Skils > Android' 카테고리의 다른 글
[Android] - 멀티 모듈 with Version Catalog [멀티 모듈 적용기(2)] (2) | 2024.07.12 |
---|---|
[Android] - 레이어 분리 [멀티 모듈 적용기(1)] (0) | 2024.07.07 |
[Android] 비디오 타임라인 이벤트 처리 (0) | 2024.06.23 |
[Android] 영상 편집 UI 구현 (0) | 2024.06.23 |
[Android] Flow를 이용한 새로고침 구현 및 개선 (0) | 2024.05.28 |