Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Media3로 좋은 콘텐츠 소비 경험 구현하기

workspace
September 12, 2023

Jetpack Media3로 좋은 콘텐츠 소비 경험 구현하기

이 발표는 안드로이드 미디어 경험 구현을 위한 새로운 라이브러리 컬렉션인 Media3를 다룹니다. ExoPlayer와 같은 Android 지원 라이브러리가 Media3로 이관되었는데 Media3는 어떤 의도를 가지고 개발되었는지, 어떻게 개발자의 어려움을 해결하며 어플리케이션에서 활용될 수 있는지 살펴봅니다.

* 발표에서 생략했던 '스트리밍 개요' 섹션이 포함되어있습니다.
* 맨 뒤 참고자료 링크는 다운로드 후 pdf에서 하이퍼링크를 누르시면 동작합니다.

workspace

September 12, 2023
Tweet

More Decks by workspace

Other Decks in Programming

Transcript

  1. য়ט ೣԋ ঌইࠅ Ѫ਷? ❏ झ౟ܻ߁ ѐۿ ❏ ExoPlayer (feat.

    v1ࠗఠ ॵ ࢎۈ੄ ҃೷) ❏ Media3 🎉
  2. ই઱ ݢ ঢ়զ… ❏ ஹೊఠ۽ ৔࢚ ࠁӝ द੘ೠ ୐ ӝর

    ਷ 2000֙ ઺റ߈ࠗఠ ❏ Ӓ۞ա ೦࢚ ޙઁܳ ѻѱ غחؘ…
  3. ׼द੄ झ౟ܻ߁ ❏ ૑Әࠁ׮ ࢚؀੸ਵ۽ ࠗ઒ೠ ੋ೐ۄ ❏ झ౟ܻ߁ਊ ೐۽ష௒x,

    ౵ੌਸ ా૩۽ ׮਍۽٘ ߉ ই ੤ࢤ. ❏ (ૐѢ) ਋௿ܼ റ ׮਍۽٘ ؽ or 1%ۄب উ߉ই ૑ݶ ੤ࢤ উغѢա ӵ૗ ❏ ӒѪਸ nݺ੉…
  4. 2012-2014 োр ݽ߄ੌ ӝӝ / ࢎਊ੗ ࣻ 0 0.45 0.9

    1.35 1.8 2012 2013 2014 క࠶݁ ୹ೞ۝ झ݃౟ಪ ୹ೞ۝ झ݃౟ಪ ࢎਊ੗ ࣻ ୹୊: Gartner (Mar 2014), IDC (Jan 2014), eMarketer (Dec 2013)
  5. 2012-2014 োр ݽ߄ੌ ӝӝ / ࢎਊ੗ ࣻ 0 0.45 0.9

    1.35 1.8 2012 2013 2014 క࠶݁ ୹ೞ۝ झ݃౟ಪ ୹ೞ۝ झ݃౟ಪ ࢎਊ੗ ࣻ ୹୊: Gartner (Mar 2014), IDC (Jan 2014), eMarketer (Dec 2013) झ౟ܻ߁ਸ ਤೠ Ҵઁ ಴ળ ӝࣿ੄ ١੢
  6. DASH ❏ Dynamic Adaptive Streaming over HTTP ❏ ׮নೠ ೧࢚ب৬

    ࠺౟ۨ੉౟۽ video streamਸ ੋ௏٬ ❏ ૑োਸ ୭ࣗചೞҊ, উ੿੸ੋ Ҋಿ૕ द୒ ജ҃ਸ ઁҕ ❏ ޷٣যী ؀ೠ ݺࣁ - Dash Manifest(mpd) ❏ ఋ ೐۽ష௒ : HLS(Apple), Smooth Streaming(Microsoft)
  7. ExoPlayer ❏ উ٘۽੉٘ܳ ਤೠ ޷٣য ੤ࢤ ۄ੉࠳۞ܻ ❏ 2014 Google

    I/Oীࢲ ١੢ ❏ DASH, Smooth Streaming١ ࢜۽਍ ಴ળ ૑ਗ ❏ frameworkױ੄ ۽૒ਸ applicationױীࢲ ࣻ ੿ೡ ࣻ ੓ب۾ ೞৈ ਬোೞѱ ഛ੢ 🔗 https://youtu.be/92fgcUNCHic?t=1108
  8. 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
  9. աীѱ ExoPlayerۆ? ❏ ੉गо ցޖ ݆ও਺😇 ❏ ୊਺ਵ۽ ֤੄ী ଵৈ೧ࠄ

    য়೑ࣗझ ❏ ׮਍۽٘ ࣘب ೱ࢚, DRM ੤ࢤ ߡߢ ੐, ߓࣘ ١ ޙઁ ೧Ѿী р੽ ӝৈ ❏ ࣘبח וܻ૑݅ Բળ൤ ޙઁ ೧Ѿ👍
  10. যڃ ۄ੉࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? Player UI Media Session ❏ Jetpack

    Media(MediaCompat), ❏ Media2 ❏ ExoPlayer п੗ ݾ੸੉ ੓Ҋ, Ҁ஖ח ҳഅب ࢚׼ೠ ׮নೠ Media apiо ઓ੤.
  11. যڃ ۄ੉࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? Player UI Media Session ❏ Jetpack

    Media(MediaCompat), ❏ Media2 ❏ ExoPlayer Ӓ ߆ীب ৈ۞о૑ য۰਑੉…
  12. যڃ ۄ੉࠳۞ܻܳ ࢎਊ೧ঠ ೞחо? Player UI Media Session ❏ Jetpack

    Media(MediaCompat), ❏ Media2 ❏ ExoPlayer Media3 👍
  13. 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); } }
  14. 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); } }
  15. 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); } }
  16. Foreground Playback (media3) Activity ExoPlayer UI Media Session // androidx.media3.common

    interface Player { … } Implements Player Takes Player Takes Player Connector ೙ਃ হ਺!
  17. // Initialize ExoPlayer as an implementation of Player interface val

    player = ExoPlayer.Builder(context).build()
  18. // 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()
  19. // 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
  20. Background Playback (previous api) Service Media Session Player Activity UI

    Media Controller androidx.media ExoPlayer ExoPlayer androidx.media
  21. Background Playback (previous api) Service Media Session Player Activity UI

    Media Controller androidx.media ExoPlayer ExoPlayer androidx.media Connector Connector
  22. Background Playback (media3) Service Media Session ExoPlayer Activity UI Media

    Controller Implements Player Takes Player Takes Player Implements Player
  23. // PlayerService // 1. Initialize ExoPlayer as an implementation of

    the Player interface val player = ExoPlayer.Builder(context).build()
  24. // 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()
  25. // 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))
  26. // 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()
  27. // 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() )
  28. Working with other apps MediaBrowserService ❏ Exposing media session ❏

    Exposing content library MediaSessionService ❏ Exposing media session
  29. Working with other apps MediaBrowserService ❏ Exposing media session ❏

    Exposing content library MediaSessionService MediaLibraryService ❏ Exposing media session ❏ Exposing content library
  30. TODO ❏ Video ҙ۲ model ࣻ੿ ❏ SessionDetailScreenী video ਬޖী

    ٮܲ ೒ۨ੉য ૓ੑ੼ ઁҕ ❏ ޷٣য ੤ࢤ ҳഅ (PlayerScreen, PlaybackService)
  31. 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, )
  32. @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 }
  33. 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(…) { … } }
  34. 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 = … ) } }
  35. 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) } } } } } }
  36. @AndroidEntryPoint class PlaybackService : MediaLibraryService() { @Inject lateinit var session:

    MediaLibrarySession …… override fun onGetSession( controllerInfo: MediaSession.ControllerInfo ): MediaLibrarySession? { … } }
  37. 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() } … }
  38. @Composable fun PlayerView( modifier: Modifier = Modifier ) { var

    player: Player? by remember { mutableStateOf(null) } var surfaceView: SurfaceView? by remember { mutableStateOf(null) } }
  39. @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 ) }
  40. @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 ) }
  41. @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 ) }
  42. 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 }
  43. @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 ) } } } }
  44. 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 }
  45. 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 }
  46. 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 }
  47. 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 }
  48. 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 }
  49. 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
  50. @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) } }
  51. @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) } }
  52. @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) } }
  53. @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) } }
  54. 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>
  55. MediaLibraryService - Android Auto ૑ਗ (2) Root Keynote Keynote Dev

    Environment Architecture Session Session Session Session Track 01 Session Session
  56. core:playback module੄ ੤ഝਊ ❏ Wear OS জ ❏ TV জ

    ❏ Automotive জ ਤ 3о૑ জ… ߈ա੺݅ী ׮ ٜ݅ ࣻ ੓঻਺😎
  57. ਃড ❏ Media3 ۄ੉࠳۞ܻח ӝઓ ۄ੉࠳۞ܻܳ ݽف ా೤! ❏ ࢎਊ੗

    ҃೷ਸ ਤ೧ MediaSession੉ ઺ਃ ❏ Media*Service ҳഅਸ ా೧ ߔӒۄ਍٘ ੤ࢤ, ӝࠄ ঌܿ ઁҕ, ޷٣য ۄ੉࠳ ۞ܻ ઁҕਸ औѱ ೡ ࣻ ੓਺. ❏ ੤ࢤ ҳഅ ݽٕਸ ੤ഝਊೞৈ wear os, tv, auto ޷٣য জਸ औѱ ѐߊ
  58. ݃૑݄ਵ۽ ઱੄੼ (1) ❏ ޙࢲ/೐۽ં౟ మ೒݁਷ ই૒ ߈৔ উؽ… ex)

    media3۽ Android Autoܳ ૑ਗೞ۰ݶ? old ޙࢲ / template Android Autoܳ ૑ਗೞ۰ݶ MediaBrowserServiceCompatਸ ҳഅ೧ঠೠ׮. new ޙࢲ media3 MediaLibraryServiceח legacy media API৬ ഐജػ׮.
  59. 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 ⌚ 📺 🚗