Share
Embed
Jetpack Media3로 좋은 콘텐츠 소비 경험 구현하기
이 발표는 안드로이드 미디어 경험 구현을 위한 새로운 라이브러리 컬렉션인 Media3를 다룹니다. ExoPlayer와 같은 Android 지원 라이브러리가 Media3로 이관되었는데 Media3는 어떤 의도를 가지고 개발되었는지, 어떻게 개발자의 어려움을 해결하며 어플리케이션에서 활용될 수 있는지 살펴봅니다.
* 발표에서 생략했던 '스트리밍 개요' 섹션이 포함되어있습니다.
* 맨 뒤 참고자료 링크는 다운로드 후 pdf에서 하이퍼링크를 누르시면 동작합니다.
workspace
September 12, 2023
Transcript
106 slides
-
য়ט ೣԋ ঌইࠅ Ѫ? ❏ झܻ߁ ѐۿ ❏ ExoPlayer (feat.
v1ࠗఠ ॵ ࢎۈ ҃) ❏ Media3 🎉 -
ই ݢ ঢ়զ… ❏ ஹೊఠ۽ ࢚ ࠁӝ दೠ ӝর
2000֙ റ߈ࠗఠ ❏ Ӓ۞ա ೦࢚ ޙઁܳ ѻѱ غחؘ… -
द झܻ߁ ❏ Әࠁ ࢚ਵ۽ ࠗೠ ੋۄ ❏ झܻ߁ਊ ۽షx,
ੌਸ ా૩۽ ۽٘ ߉ ই ࢤ. ❏ (ૐѢ) ܼ റ ۽٘ ؽ or 1%ۄب উ߉ই ݶ ࢤ উغѢա ӵ ❏ ӒѪਸ nݺ… -
2012-2014 োр ݽ߄ੌ ӝӝ / ࢎਊ ࣻ 0 0.45 0.9
1.35 1.8 2012 2013 2014 క࠶݁ ೞ झ݃ಪ ೞ झ݃ಪ ࢎਊ ࣻ : Gartner (Mar 2014), IDC (Jan 2014), eMarketer (Dec 2013) -
2012-2014 োр ݽ߄ੌ ӝӝ / ࢎਊ ࣻ 0 0.45 0.9
1.35 1.8 2012 2013 2014 క࠶݁ ೞ झ݃ಪ ೞ झ݃ಪ ࢎਊ ࣻ : Gartner (Mar 2014), IDC (Jan 2014), eMarketer (Dec 2013) झܻ߁ਸ ਤೠ Ҵઁ ળ ӝࣿ ١ -
DASH ❏ Dynamic Adaptive Streaming over HTTP ❏ নೠ ೧࢚ب৬
࠺ۨ۽ video streamਸ ੋ٬ ❏ োਸ ୭ࣗചೞҊ, উੋ Ҋಿ द ജ҃ਸ ઁҕ ❏ ٣যী ೠ ݺࣁ - Dash Manifest(mpd) ❏ ఋ ۽ష : HLS(Apple), Smooth Streaming(Microsoft) -
ExoPlayer ❏ উ٘۽٘ܳ ਤೠ ٣য ࢤ ۄ࠳۞ܻ ❏ 2014 Google
I/Oীࢲ ١ ❏ DASH, Smooth Streaming١ ࢜۽ ળ ਗ ❏ frameworkױ ۽ਸ applicationױীࢲ ࣻ ೡ ࣻ ب۾ ೞৈ ਬোೞѱ ഛ 🔗 https://youtu.be/92fgcUNCHic?t=1108 -
ExoPlayerо ઁҕೞח Ѫ Extension cast, media session, media2 (androidx) cronet,
okhttp, rtmp (data source) AV1, VP9, FLAC, Opus (decoder) FFMpeg (renderer) Library core dash, hls, smoothstreaming ui -
աীѱ ExoPlayerۆ? ❏ गо ցޖ ݆ও😇 ❏ ਵ۽ ֤ী ଵৈ೧ࠄ
য়ࣗझ ❏ ۽٘ ࣘب ೱ࢚, DRM ࢤ ߡߢ , ߓࣘ ١ ޙઁ ೧Ѿী р ӝৈ ❏ ࣘبח וܻ݅ Բળ ޙઁ ೧Ѿ👍 -
যڃ ۄ࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? Player UI Media Session ❏ Jetpack
Media(MediaCompat), ❏ Media2 ❏ ExoPlayer п ݾ Ҋ, Ҁח ҳഅب ࢚ೠ নೠ Media apiо ઓ. -
যڃ ۄ࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? UI Player Media Session ❏ Jetpack
Media(MediaCompat), ❏ Media2 ❏ ExoPlayer -
যڃ ۄ࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? Player UI Media Session ❏ Jetpack
Media(MediaCompat), ❏ Media2 ❏ ExoPlayer -
যڃ ۄ࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? Player UI Media Session ❏ Jetpack
Media(MediaCompat), ❏ Media2 ❏ ExoPlayer Ӓ ߆ীب ৈ۞о য۰… -
যڃ ۄ࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? Player UI Media Session ❏ Jetpack
Media(MediaCompat), ❏ Media2 ❏ ExoPlayer Media3 👍 -
Foreground Playback (previous api) Activity Player UI Media Session androidx.media
ExoPlayer ExoPlayer Connector -
https://github.com/google/ExoPlayer/blob/release-v2/extensions/mediasession/src/main/java/com/ google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java package com.google.android.exoplayer2.ext.mediasession; public final class MediaSessionConnector { //
media session command -> player @Override public void onPlay(…) { // … player.play(); } // player callback -> media session @Override public void onPlaybackStateChanged(…) { // … mediaSession.setState(newState); } } -
https://github.com/google/ExoPlayer/blob/release-v2/extensions/mediasession/src/main/java/com/ google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java package com.google.android.exoplayer2.ext.mediasession; public final class MediaSessionConnector { //
media session command -> player @Override public void onPlay(…) { // … player.play(); } // player callback -> media session @Override public void onPlaybackStateChanged(…) { // … mediaSession.setState(newState); } } -
https://github.com/google/ExoPlayer/blob/release-v2/extensions/mediasession/src/main/java/com/ google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java package com.google.android.exoplayer2.ext.mediasession; public final class MediaSessionConnector { //
media session command -> player @Override public void onPlay(…) { // … player.play(); } // player callback -> media session @Override public void onPlaybackStateChanged(…) { // … mediaSession.setState(newState); } } -
Foreground Playback (media3) Activity Player UI Media Session // androidx.media3.common
interface Player { … } -
Foreground Playback (media3) Activity ExoPlayer UI Media Session // androidx.media3.common
interface Player { … } Implements Player Takes Player Takes Player Connector ਃ হ! -
// Initialize ExoPlayer as an implementation of Player interface val
player = ExoPlayer.Builder(context).build() -
// Initialize ExoPlayer as an implementation of Player interface val
player = ExoPlayer.Builder(context).build() // Takes the Player instance to initialize MediaSession val mediaSession = MediaSession.Builder(context, player).build() -
// Initialize ExoPlayer as an implementation of Player interface val
player = ExoPlayer.Builder(context).build() // Takes the Player instance to initialize MediaSession val mediaSession = MediaSession.Builder(context, player).build() // Takes the Player instance to configure UI playerView.player = player -
Background Playback (previous api) Service Media Session Player Activity UI
Media Controller androidx.media ExoPlayer ExoPlayer androidx.media -
Background Playback (previous api) Service Media Session Player Activity UI
Media Controller androidx.media ExoPlayer ExoPlayer androidx.media Connector Connector -
Background Playback (media3) Service Media Session ExoPlayer Activity UI Media
Controller Implements Player Takes Player Takes Player Implements Player -
// PlayerService // 1. Initialize ExoPlayer as an implementation of
the Player interface val player = ExoPlayer.Builder(context).build() -
// PlayerService // 1. Initialize ExoPlayer as an implementation of
the Player interface val player = ExoPlayer.Builder(context).build() // 2. Takes the Player instance to initialize the MediaSession val mediaSession = MediaSession.Builder(context, player).build() -
// PlayerService // 1. Initialize ExoPlayer as an implementation of
the Player interface val player = ExoPlayer.Builder(context).build() // 2. Takes the Player instance to initialize the MediaSession val mediaSession = MediaSession.Builder(context, player).build() // PlayerActivity // 1. Create a session token to identify session. val sessionToken = SessionToken(context, ComponentName(context, PlayerService::class.java)) -
// PlayerService // 1. Initialize ExoPlayer as an implementation of
the Player interface val player = ExoPlayer.Builder(context).build() // 2. Takes the Player instance to initialize the MediaSession val mediaSession = MediaSession.Builder(context, player).build() // PlayerActivity // 1. Create a session token to identify session. val sessionToken = SessionToken(context, ComponentName(context, PlayerService::class.java)) // 2. Build mediaController which connects to the session asynchronously. val mediaControllerFuture = MediaController .Builder(context, sessionToken) .buildAsync() -
// PlayerService // 1. Initialize ExoPlayer as an implementation of
the Player interface val player = ExoPlayer.Builder(context).build() // 2. Takes the Player instance to initialize the MediaSession val mediaSession = MediaSession.Builder(context, player).build() // PlayerActivity // 1. Create a session token to identify session. val sessionToken = SessionToken(context, ComponentName(context, PlayerService::class.java)) // 2. Build mediaController which connects to the session asynchronously. val mediaControllerFuture = MediaController .Builder(context, sessionToken) .buildAsync() // 3. Takes the Player instance to configure UI mediaControllerFuture.addListener( { playerView.setPlayer(player = mediaControllerFuture.get())}, MoreExecutors.directExecutor() ) -
Working with other apps MediaBrowserService ❏ Exposing media session ❏
Exposing content library -
Working with other apps MediaBrowserService ❏ Exposing media session ❏
Exposing content library MediaSessionService ❏ Exposing media session -
Working with other apps MediaBrowserService ❏ Exposing media session ❏
Exposing content library MediaSessionService MediaLibraryService ❏ Exposing media session ❏ Exposing content library -
TODO ❏ Video ҙ۲ model ࣻ ❏ SessionDetailScreenী video ਬޖী
ٮܲ ۨয ੑ ઁҕ ❏ ٣য ࢤ ҳഅ (PlayerScreen, PlaybackService) -
data class PlaybackState( val isPlaying: Boolean = false, val hasPrevious:
Boolean = false, val hasNext: Boolean = false, val position: Long = C.TIME_UNSET, val duration: Long = C.TIME_UNSET, val speed: Float = 1F, val aspectRatio: Float = 16F / 9F, val title: String? = null, val artist: String? = null, val artworkUri: Uri? = null, ) -
@Singleton class PlaybackStateManager @Inject constructor() { private val _playbackState =
MutableStateFlow(PlaybackState()) val flow: StateFlow<PlaybackState> get() = _playbackState var playbackState: PlaybackState set(value) { _playbackState.value = value } get() = _playbackState.value } -
internal class PlaybackStateListener @Inject constructor(…) : Player.Listener { private lateinit
var player: Player override fun onPlaybackStateChanged(…) { updatePlayState() } override fun onPlayWhenReadyChanged(…) { … } override fun onPositionDiscontinuity(…) { … } override fun onIsPlayingChanged(…) { … } override fun onPlaybackParametersChanged(…) { … } override fun onVideoSizeChanged(…) { … } } -
internal class PlaybackStateListener @Inject constructor(…) : Player.Listener { // …
private fun updatePlayState() { val playbackState = player.playbackState playbackStateManager.playbackState = PlaybackState( isPlaying = when { playbackState == Player.STATE_ENDED || playbackState == Player.STATE_IDLE -> false player.playWhenReady -> true else -> false }, hasPrevious = player.hasPreviousMediaItem(), hasNext = player.hasNextMediaItem(), position = player.contentPosition, duration = player.duration, speed = player.playbackParameters.speed, aspectRatio = …, title = …, artist = …, artworkUri = … ) } } -
internal class PlaybackStateListener @Inject constructor(…) : Player.Listener { private lateinit
var player: Player fun attachTo(player: Player) { this.player = player player.addListener(this) scope.launch { playbackStateManager.flow .map { it.isPlaying } .collectLatest { isPlaying -> if (isPlaying) { while (true) { updatePlayState() delay(400.milliseconds) } } } } } } -
@AndroidEntryPoint class PlaybackService : MediaLibraryService() { @Inject lateinit var session:
MediaLibrarySession …… override fun onGetSession( controllerInfo: MediaSession.ControllerInfo ): MediaLibrarySession? { … } } -
class PlayerController @Inject constructor(…) { … fun play() = executeAfterPrepare
{ controller -> controller.play() } fun playPause() = executeAfterPrepare { controller -> if (controller.isPlaying) { controller.pause() } else { controller.play() } } fun setPosition(positionMs: Long) = executeAfterPrepare { controller -> controller.seekTo(positionMs) } fun fastForward() = executeAfterPrepare { controller -> controller.seekForward() } fun rewind() = executeAfterPrepare { controller -> controller.seekBack() } fun previous() = executeAfterPrepare { controller -> controller.seekToPrevious() } fun next() = executeAfterPrepare { controller -> controller.seekToNext() } … } -
@Composable fun PlayerView( modifier: Modifier = Modifier ) { var
player: Player? by remember { mutableStateOf(null) } var surfaceView: SurfaceView? by remember { mutableStateOf(null) } } -
@Composable fun PlayerView( modifier: Modifier = Modifier ) { var
player: Player? by remember { mutableStateOf(null) } var surfaceView: SurfaceView? by remember { mutableStateOf(null) } AndroidView( factory = { SurfaceView(it).apply { surfaceView = this } }, modifier = modifier ) } -
@Composable fun PlayerView( modifier: Modifier = Modifier ) { val
context = LocalContext.current var player: Player? by remember { mutableStateOf(null) } var surfaceView: SurfaceView? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { player = MediaController .Builder( context, SessionToken(context, ComponentName(context, PlaybackService::class.java)) ) .buildAsync() .await() } AndroidView( factory = { SurfaceView(it).apply { surfaceView = this } }, modifier = modifier ) } -
@Composable fun PlayerView( modifier: Modifier = Modifier ) { val
context = LocalContext.current var player: Player? by remember { mutableStateOf(null) } var surfaceView: SurfaceView? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { player = MediaController .Builder( context, SessionToken(context, ComponentName(context, PlaybackService::class.java)) ) .buildAsync() .await() } DisposableEffect(player, surfaceView) { if (player?.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE) == true) { player?.setVideoSurfaceView(surfaceView) } onDispose { if (player?.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE) == true) { player?.clearVideoSurfaceView(surfaceView) } } } AndroidView( factory = { SurfaceView(it).apply { surfaceView = this } }, modifier = modifier ) } -
sealed interface PlayerUiState { object Loading : PlayerUiState data class
Success( val isPlaying: Boolean, val hasPrevious: Boolean, val hasNext: Boolean, val position: Long, val duration: Long, val speed: Float, val aspectRatio: Float ) : PlayerUiState } -
@HiltViewModel class PlayerViewModel @Inject constructor( … private val playbackStateManager: PlaybackStateManager,
… ) : ViewModel() { private val _playerUiState = MutableStateFlow<PlayerUiState>(PlayerUiState.Loading) val playerUiState: StateFlow<PlayerUiState> = _playerUiState init { … viewModelScope.launch { playbackStateManager.flow.collect { _playerUiState.value = PlayerUiState.Success( it.isPlaying, it.hasPrevious, it.hasNext, it.position, it.duration, it.speed, it.aspectRatio ) } } } } -
sealed interface PlayerUiState { object Loading : PlayerUiState data class
Success( val isPlaying: Boolean, val hasPrevious: Boolean, val hasNext: Boolean, val position: Long, val duration: Long, val speed: Float, val aspectRatio: Float ) : PlayerUiState } -
sealed interface PlayerUiState { object Loading : PlayerUiState data class
Success( val isPlaying: Boolean, val hasPrevious: Boolean, val hasNext: Boolean, val position: Long, val duration: Long, val speed: Float, val aspectRatio: Float ) : PlayerUiState } -
sealed interface PlayerUiState { object Loading : PlayerUiState data class
Success( val isPlaying: Boolean, val hasPrevious: Boolean, val hasNext: Boolean, val position: Long, val duration: Long, val speed: Float, val aspectRatio: Float ) : PlayerUiState } -
sealed interface PlayerUiState { object Loading : PlayerUiState data class
Success( val isPlaying: Boolean, val hasPrevious: Boolean, val hasNext: Boolean, val position: Long, val duration: Long, val speed: Float, val aspectRatio: Float ) : PlayerUiState } -
sealed interface PlayerUiState { object Loading : PlayerUiState data class
Success( val isPlaying: Boolean, val hasPrevious: Boolean, val hasNext: Boolean, val position: Long, val duration: Long, val speed: Float, val aspectRatio: Float ) : PlayerUiState } -
sealed interface PlayerUiState { object Loading : PlayerUiState data class
Success( val isPlaying: Boolean, val hasPrevious: Boolean, val hasNext: Boolean, val position: Long, val duration: Long, val speed: Float, val aspectRatio: Float ) : PlayerUiState } PlayerView -
@HiltViewModel class PlayerViewModel @Inject constructor( … private val playerController: PlayerController,
) : ViewModel() { … fun playPause() { playerController.playPause() } fun prev() { playerController.previous() } fun next() { playerController.next() } fun setPosition(position: Long) { playerController.setPosition(position) } } -
@HiltViewModel class PlayerViewModel @Inject constructor( … private val playerController: PlayerController,
) : ViewModel() { … fun playPause() { playerController.playPause() } fun prev() { playerController.previous() } fun next() { playerController.next() } fun setPosition(position: Long) { playerController.setPosition(position) } } -
@HiltViewModel class PlayerViewModel @Inject constructor( … private val playerController: PlayerController,
) : ViewModel() { … fun playPause() { playerController.playPause() } fun prev() { playerController.previous() } fun next() { playerController.next() } fun setPosition(position: Long) { playerController.setPosition(position) } } -
@HiltViewModel class PlayerViewModel @Inject constructor( … private val playerController: PlayerController,
) : ViewModel() { … fun playPause() { playerController.playPause() } fun prev() { playerController.previous() } fun next() { playerController.next() } fun setPosition(position: Long) { playerController.setPosition(position) } } -
MediaLibraryService - Android Auto ਗ (1) <service android:name=".session.PlaybackService" android:foregroundServiceType="mediaPlayback" android:exported="true"
tools:ignore="ExportedService"> <intent-filter> <action android:name="androidx.media3.session.MediaLibraryService"/> <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service> -
MediaLibraryService - Android Auto ਗ (2) Root Keynote Keynote Dev
Environment Architecture Session Session Session Session Track 01 Session Session -
core:playback module ഝਊ ❏ Wear OS জ ❏ TV জ
❏ Automotive জ ਤ 3о জ… ߈ա݅ী ٜ݅ ࣻ 😎 -
ਃড ❏ Media3 ۄ࠳۞ܻח ӝઓ ۄ࠳۞ܻܳ ݽف ా! ❏ ࢎਊ
҃ਸ ਤ೧ MediaSession ਃ ❏ Media*Service ҳഅਸ ా೧ ߔӒۄ٘ ࢤ, ӝࠄ ঌܿ ઁҕ, ٣য ۄ࠳ ۞ܻ ઁҕਸ औѱ ೡ ࣻ . ❏ ࢤ ҳഅ ݽٕਸ ഝਊೞৈ wear os, tv, auto ٣য জਸ औѱ ѐߊ -
݄݃ਵ۽ (1) ❏ ޙࢲ/۽ં మ݁ ই ߈ উؽ… ex)
media3۽ Android Autoܳ ਗೞ۰ݶ? old ޙࢲ / template Android Autoܳ ਗೞ۰ݶ MediaBrowserServiceCompatਸ ҳഅ೧ঠೠ. new ޙࢲ media3 MediaLibraryServiceח legacy media API৬ ഐജػ. -
Resource Google I/O 2014 - Building great multi-media experiences on
Android (18:29 ~) Introducing Jetpack Media3 Media3 is ready to play! Introduction to Jetpack Media3 Media3 Github Android Dev Summit 2021 - What’s next for AndroidX Media and ExoPlayer DroidKnight2023 App Github (reference-media3 branch) Using Jetpack Compose on Wear OS Use Jetpack Compose on Android TV Build media apps for cars ⌚ 📺 🚗