media3-ui-compose 라이브러리는 Jetpack Compose에서 미디어 UI를 빌드하기 위한 기본 구성요소를 제공합니다. media3-ui-compose-material3 라이브러리에서 제공하는 것보다 더 많은 맞춤설정이 필요한 개발자를 위해 설계되었습니다. 이 페이지에서는 핵심 구성요소와 상태 홀더를 사용하여 맞춤 미디어 플레이어 UI를 만드는 방법을 설명합니다.
Material3 및 맞춤 Compose 구성요소 혼합
media3-ui-compose-material3 라이브러리는 유연하게 설계되었습니다. 대부분의 UI에 사전 빌드된 구성요소를 사용할 수 있지만 더 세부적인 제어가 필요한 경우 단일 구성요소를 커스텀 구현으로 교체할 수 있습니다. 이때 media3-ui-compose 라이브러리가 사용됩니다.
예를 들어 Material3 라이브러리의 표준 PreviousButton 및 NextButton를 사용하려고 하지만 완전히 맞춤설정된 PlayPauseButton가 필요하다고 가정해 보겠습니다. 핵심 media3-ui-compose 라이브러리의 PlayPauseButton를 사용하여 사전 빌드된 구성요소와 함께 배치하면 됩니다.
Row { // Use prebuilt component from the Media3 UI Compose Material3 library PreviousButton(player) // Use the scaffold component from Media3 UI Compose library PlayPauseButton(player) { // `this` is PlayPauseButtonState FilledTonalButton( onClick = { Log.d("PlayPauseButton", "Clicking on play-pause button") this.onClick() }, enabled = this.isEnabled, ) { Icon( imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (showPlay) "Play" else "Pause", ) } } // Use prebuilt component from the Media3 UI Compose Material3 library NextButton(player) }
사용 가능한 구성요소
media3-ui-compose 라이브러리는 일반적인 재생 컨트롤을 위한 사전 빌드된 컴포저블 집합을 제공합니다. 다음은 앱에서 직접 사용할 수 있는 몇 가지 구성요소입니다.
| 구성요소 | 설명 |
|---|---|
PlayPauseButton |
재생과 일시중지 간에 전환하는 버튼의 상태 컨테이너입니다. |
SeekBackButton |
정의된 증분만큼 뒤로 탐색하는 버튼의 상태 컨테이너입니다. |
SeekForwardButton |
정의된 증분만큼 앞으로 탐색하는 버튼의 상태 컨테이너입니다. |
NextButton |
다음 미디어 항목을 찾는 버튼의 상태 컨테이너입니다. |
PreviousButton |
이전 미디어 항목을 찾는 버튼의 상태 컨테이너입니다. |
RepeatButton |
반복 모드를 순환하는 버튼의 상태 컨테이너입니다. |
ShuffleButton |
셔플 모드를 전환하는 버튼의 상태 컨테이너입니다. |
MuteButton |
플레이어를 음소거 및 음소거 해제하는 버튼의 상태 컨테이너입니다. |
TimeText |
플레이어 진행 상황을 표시하는 컴포저블의 상태 컨테이너입니다. |
ContentFrame |
가로세로 비율 관리, 크기 조절, 셔터, 아트워크, 자막 슬롯을 처리하는 미디어 콘텐츠의 표시 영역입니다. |
PlayerSurface |
AndroidView에서 SurfaceView 및 TextureView를 래핑하는 원시 표면입니다. |
Artwork |
앨범 표지와 같은 현재 미디어 항목의 아트워크를 표시하는 컴포저블입니다. |
Player 컴포저블 맞춤설정
media3-ui-compose-material3 라이브러리를 사용할 때 Player 컴포저블은 완전한 미디어 재생 환경을 제공합니다. 콘텐츠 슬롯에 자체 컴포저블을 제공하여 레이아웃을 맞춤설정하거나 PlayerDefaults 객체를 활용하여 UI의 특정 부분(예: TopControls, CenterControls, BottomControls, ErrorOverlay)을 맞춤설정할 수 있습니다.
그림 1은 Player 컴포저블이 슬롯을 배치하는 위치를 보여줍니다. 그림 2는 레이어를 그리는 순서를 보여줍니다.
Player 컴포저블의 슬롯
중앙 및 하단 컨트롤은 PlayerDefaults.CenterControls 및 PlayerDefaults.BottomControls의 슬롯을 보여줍니다. topControls, above, below 슬롯은 기본적으로 비어 있습니다.Player 컴포저블의 레이어는 그려지는 순서대로 번호가 지정됩니다.예를 들어 PlayerDefaults.CenterControls를 사용하여 중앙 재생/일시중지 버튼만 재정의하고 나머지 중앙 컨트롤은 그대로 둘 수 있습니다.
topControls과 같은 매개변수에 완전히 맞춤설정된 컴포저블을 전달하고 bottomControls과 같은 다른 매개변수는 완전히 생략하여 기본값으로 둘 수도 있습니다.
@Composable fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, showControls = showControls, topControls = { // Fully custom top controls Text("My custom title") }, centerControls = { // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = player, central = { // A custom play/pause button Material3PlayPauseButton(player, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
Player 컴포저블은 자막도 표시하며, 표시할 동영상이 없는 경우 현재 미디어 항목의 아트워크를 표시합니다. 모양을 맞춤설정하려면 자체 컴포저블을 subtitles 및 artwork 슬롯에 전달합니다. 숨기려면 빈 컴포저블을 subtitles에 전달하고 null를 artwork에 전달합니다.
UI 상태 홀더
스캐폴딩 구성요소 중 어느 것도 요구사항을 충족하지 않는 경우 상태 객체를 직접 사용할 수도 있습니다. 일반적으로 상응하는 remember 메서드를 사용하여 리컴포지션 간에 UI 디자인을 유지하는 것이 좋습니다.
컴포저블과 비교하여 UI 상태 홀더의 유연성을 사용하는 방법을 더 잘 이해하려면 Compose가 상태를 관리하는 방법을 참고하세요.
버튼 상태 홀더
일부 UI 상태의 경우 라이브러리는 버튼과 같은 컴포저블에서 사용할 가능성이 높다고 가정합니다.
| 주 | remember*State | 유형 |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle 또는 N-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
상수 |
NextButtonState |
rememberNextButtonState |
상수 |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
메뉴 또는 N-Toggle |
PlayPauseButtonState는 재생 및 일시중지 상태 외에도 showBuffering, showReplay, showRetry를 노출합니다. 이를 사용하여 플레이어가 버퍼링하는 동안 버퍼링 표시기를, 재생이 끝난 후 다시보기 아이콘을, 재생 오류 후 다시 시도 아이콘을 표시합니다. showReplay 및 showRetry는 showPlay를 상세검색합니다. 둘 중 하나가 true이면 showPlay도 true이므로 showPlay 전에 확인합니다. onClick 메서드는 이러한 상태도 처리합니다. 예를 들어 오류가 발생한 후 플레이어를 다시 준비합니다.
표시할 상태를 선택하려면 SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED, SHOW_RETRY_ON_ERROR의 조합을 rememberPlayPauseButtonState의 displayMode 매개변수에 전달합니다. 기본적으로 displayMode는 SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED, SHOW_RETRY_ON_ERROR를 결합하여 세 가지 상태를 모두 표시하며, 재생이 일시중지된 동안에도 버퍼링 상태가 표시됩니다.
PlayPauseButtonState 사용 예시:
val state = rememberPlayPauseButtonState(player) IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) { Icon( imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (state.showPlay) stringResource(R.string.playpause_button_play) else stringResource(R.string.playpause_button_pause), ) }
시각적 출력 상태 홀더
CurrentMediaItemState는 현재 재생 중인 MediaItem에 관한 정보를 제공하고 PlaylistState는 플레이어에 설정된 MediaItems에 관한 정보를 노출합니다. 이는 커스텀 UI에 메타데이터 정보를 표시하는 데 유용합니다.
ErrorState는 플레이어의 현재 오류 상태에 관한 정보를 제공하며, 이 정보는 오류 오버레이를 표시하는 데 사용할 수 있습니다.
PlaybackState는 특정 UI 구성요소와 관계없이 플레이어의 핵심 재생 상태(예: playbackState, isPlaying, playerError)를 노출합니다. 이를 사용하여 오버레이 또는 맞춤 레이아웃을 구동합니다. 예를 들어 플레이어가 버퍼링하는 동안 로드 표시기를 표시합니다. 사전 빌드된 Material3 표시기의 경우 BufferingIndicator를 사용합니다.
PresentationState는 PlayerSurface의 동영상 출력을 표시할 수 있거나 자리표시자 UI 요소로 가려야 하는 경우에 관한 정보를 보유합니다.
ContentFrame 컴포저블은 가로세로 비율 처리와 아직 준비되지 않은 노출 영역에 셔터를 표시하는 처리를 결합합니다.
@Composable fun ContentFrame( player: Player?, modifier: Modifier = Modifier, surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW, contentScale: ContentScale = ContentScale.Fit, keepContentOnReset: Boolean = false, shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) }, ) { val presentationState = rememberPresentationState(player, keepContentOnReset) val scaledModifier = modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio) // Always leave PlayerSurface to be part of the Compose tree because it will be initialized in // the process. If this composable is guarded by some condition, it might never become visible // because the Player won't emit the relevant event, e.g. the first frame being ready. PlayerSurface(player, scaledModifier, surfaceType) if (presentationState.coverSurface) { // Cover the surface that is being prepared with a shutter shutter() } }
여기서는 presentationState.videoAspectRatio를 사용하여 노출 영역을 선택한 가로세로 비율로 조정하고(더 많은 유형은 콘텐츠 배율 참고) presentationState.coverSurface를 사용하여 노출 영역을 표시하기에 적절한 시기가 아닌 경우를 알 수 있습니다. 이 경우 노출 영역 위에 불투명 셔터를 배치할 수 있으며, 노출 영역이 준비되면 이 셔터가 사라집니다.
ContentFrame를 사용하면 셔터를 후행 람다로 맞춤설정할 수 있지만 기본적으로 상위 컨테이너의 크기를 채우는 검은색 @Composable Box입니다.
PresentationState는 showArtwork도 노출합니다. 이는 현재 미디어 항목이 동영상 트랙 없이 재생될 때(예: 오디오 전용 콘텐츠) true입니다. 이 경우 ContentFrame는 항목의 MediaMetadata에서 아트워크를 표시하는 Artwork 컴포저블을 표시합니다. 아트워크를 맞춤설정하거나 숨기려면 자체 컴포저블 또는 null를 ContentFrame의 artwork 슬롯에 전달합니다.
기본적으로 Artwork는 메타데이터에 삽입된 아트워크 데이터 또는 content, file, android.resource URI의 아트워크 데이터만 로드합니다. 원격 URI 또는 다른 소스에서 아트워크를 로드하려면 다음 옵션 중 하나를 사용하세요.
DataSourceBitmapLoader와 같은BitmapLoader를Artwork에 전달합니다.painter를 사용하는Artwork오버로드를 사용하여 원하는 라이브러리로 이미지를 로드합니다.
기본적으로 자막을 렌더링하는 Player 컴포저블과 달리 ContentFrame은 subtitles 슬롯을 비워 둡니다. ContentFrame에서 자막을 렌더링하거나 Player에서 자막 렌더링을 맞춤설정하려면 CuesState를 사용하여 플레이어의 현재 큐를 관찰하고 subtitles 슬롯에서 렌더링합니다. ContentFrame는 조정된 동영상의 경계 내에 이 슬롯을 배치하고 셔터와 아트워크 위에 그립니다. 따라서 동영상이 없을 때 자막이 계속 표시됩니다.
트랙 선택 상태 홀더
TrackSelectionState는 오디오 언어 또는 자막 메뉴와 같은 단일 트랙 유형의 메뉴를 빌드하는 데 도움이 됩니다. rememberTrackSelectionState를 사용하여 트랙 유형과 필터 조합(예: TRACK_FILTER_LANGUAGE 또는 TRACK_FILTER_RESOLUTION)을 전달하여 만듭니다. 이 필터 조합은 옵션을 구분하는 트랙 속성을 결정합니다. hasOffOption 또는 hasAutoOption를 true로 설정하여 트랙 유형을 사용 중지하는 옵션이나 플레이어가 트랙을 자동으로 선택하도록 하는 옵션을 추가합니다.
상태는 사용 가능한 selectionOptions 및 selectedOption를 노출합니다.
각 TrackSelectionOption는 기기에서 지원하는지 여부와 선택되었는지 여부를 나타냅니다. selectOption에 옵션을 전달하면 플레이어의 트랙 선택 매개변수가 그에 따라 업데이트됩니다. 메뉴를 표시하기 전에 canSetTrackSelectionParameters 및 선택할 수 있는 옵션이 충분한지 확인합니다. 예를 들어 다음 자막 메뉴는 각 자막 언어에 대한 옵션 하나와 자막을 사용 중지하는 옵션을 제공합니다.
@Composable fun SubtitleMenu(player: Player, modifier: Modifier = Modifier) { val state = rememberTrackSelectionState( player, trackType = C.TRACK_TYPE_TEXT, trackFilters = TRACK_FILTER_LANGUAGE, hasOffOption = true, ) // Hide the menu if the player can't change tracks or has no subtitles to choose from if (!state.canSetTrackSelectionParameters || state.selectionOptions.size < 2) return Column(modifier.selectableGroup()) { for (option in state.selectionOptions) { Row( Modifier.fillMaxWidth() .heightIn(min = 48.dp) .selectable( selected = option.isSelected, enabled = option.isSupported, role = Role.RadioButton, onClick = { state.selectOption(option) }, ) .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton(selected = option.isSelected, onClick = null, enabled = option.isSupported) Text( text = if (option.onOffState == TRACK_SELECTION_OFF) "Off" else option.language?.let { Locale.forLanguageTag(it).displayName } ?: "Unknown", color = if (option.isSupported) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.38f), ) } } } }
플레이어의 Tracks 및 TrackSelectionParameters에 직접 액세스해야 하는 경우 대신 TrackSelectionParametersState를 사용하세요. 플레이어가 트랙을 선택하는 방법에 관한 자세한 내용은 트랙 선택을 참고하세요.
Flows는 어디에 있나요?
많은 Android 개발자는 Kotlin Flow 객체를 사용하여 끊임없이 변화하는 UI 데이터를 수집하는 데 익숙합니다. 예를 들어 수명 주기를 인식하는 방식으로 collect할 수 있는 Player.isPlaying 흐름을 찾고 있을 수 있습니다. 또는 Player.eventsFlow와 같은 것을 사용하여 원하는 방식으로 filter할 수 있는 Flow<Player.Events>를 제공할 수 있습니다.
하지만 Player UI 상태에 흐름을 사용하면 몇 가지 단점이 있습니다. 주요 우려사항 중 하나는 데이터 전송의 비동기적 특성입니다. Player.Event와 UI 측의 소비 간 지연 시간을 최대한 줄여 Player와 동기화되지 않은 UI 요소를 표시하지 않도록 해야 합니다.
기타 사항은 다음과 같습니다.
- 모든
Player.Events가 있는 흐름은 단일 책임 원칙을 준수하지 않으며 각 소비자는 관련 이벤트를 필터링해야 합니다. - 각
Player.Event의 흐름을 만들려면 각 UI 요소에 대해 흐름을 결합(combine사용)해야 합니다. Player.Event와 UI 요소 변경사항 간에는 다대다 매핑이 있습니다.combine를 사용해야 하면 UI가 잠재적으로 잘못된 상태가 될 수 있습니다.
맞춤 UI 상태 만들기
기존 UI 상태가 요구사항을 충족하지 않는 경우 맞춤 UI 상태를 추가할 수 있습니다. 기존 상태의 소스 코드를 확인하여 패턴을 복사합니다. 일반적인 UI 상태 홀더 클래스는 다음을 실행합니다.
Player객체를 사용합니다.- 코루틴을 사용하여
Player를 구독합니다. 자세한 내용은Player.listen를 참조하세요. - 내부 상태를 업데이트하여 특정
Player.Events에 응답합니다. - 적절한
Player업데이트로 변환될 비즈니스 로직 명령어를 허용합니다. - UI 트리의 여러 위치에서 만들 수 있으며 항상 플레이어 상태의 일관된 뷰를 유지합니다.
- 컴포저블에서 사용하여 변경사항에 동적으로 응답할 수 있는 Compose
State필드를 노출합니다. - 컴포지션 간에 인스턴스를 기억하는
remember*State함수가 함께 제공됩니다.
가입 방법:
class SomeButtonState(private val player: Player) { var isEnabled by mutableStateOf(player.isCommandAvailable(COMMAND_ACTION_A)) private set var someFieldValue by mutableStateOf(someFieldDefault) private set fun onClick() { player.actionA() } suspend fun observe(): Nothing = player.listen { events -> if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { someFieldValue = this.someField isEnabled = this.isCommandAvailable(COMMAND_ACTION_A) } } }
자체 Player.Events에 반응하려면 Player.listen를 사용하여 포착하면 됩니다. 이는 코루틴 세계에 진입하여 Player.Events를 무기한 수신 대기할 수 있는 suspend fun입니다. 다양한 UI 상태의 Media3 구현은 최종 개발자가 Player.Events에 관해 학습하는 데 신경 쓰지 않도록 도와줍니다.