--- id: android-exoplayer-patterns title: Android Media3 ExoPlayer — 비디오 / 오디오 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [android, media3, exoplayer, vibe-coding] tech_stack: { language: "Kotlin / androidx.media3", applicable_to: ["Android"] } applied_in: [] aliases: [ExoPlayer, MediaItem, DASH, HLS, MediaSession] --- # Android Media3 ExoPlayer > 표준 미디어 player. **Adaptive streaming (HLS/DASH)** + DRM + offline + cast 지원. 옛 `com.google.android.exoplayer2` 대신 `androidx.media3` (Media3) 사용. ## 📖 핵심 개념 - ExoPlayer: 단일 player. - MediaItem: 재생 항목 (URI + metadata). - MediaSource: HLS / DASH / 일반. - MediaSession: 시스템 (잠금화면 / Bluetooth) 통합. ## 💻 코드 패턴 ### 기본 셋업 ```kotlin implementation("androidx.media3:media3-exoplayer:1.4.0") implementation("androidx.media3:media3-ui:1.4.0") implementation("androidx.media3:media3-exoplayer-hls:1.4.0") ``` ```kotlin class PlayerViewModel : ViewModel() { val player: ExoPlayer = ExoPlayer.Builder(context).build() init { player.setMediaItem(MediaItem.fromUri("https://example.com/stream.m3u8")) player.prepare() player.playWhenReady = true } override fun onCleared() { player.release() } } ``` ### Compose ```kotlin @Composable fun VideoPlayer(player: ExoPlayer) { AndroidView(factory = { ctx -> PlayerView(ctx).apply { this.player = player useController = true } }) } DisposableEffect(Unit) { onDispose { player.release() } } ``` ### HLS / DASH ```kotlin val mediaItem = MediaItem.Builder() .setUri("https://example.com/stream.m3u8") .setMimeType(MimeTypes.APPLICATION_M3U8) .build() val source = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()) .createMediaSource(mediaItem) player.setMediaSource(source) ``` ### MediaSession (잠금화면) ```kotlin class PlaybackService : MediaSessionService() { private lateinit var session: MediaSession override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() session = MediaSession.Builder(this, player).build() } override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = session override fun onDestroy() { session.release(); super.onDestroy() } } ``` ### Listener — buffering / error ```kotlin player.addListener(object : Player.Listener { override fun onPlaybackStateChanged(state: Int) { when (state) { Player.STATE_BUFFERING -> showSpinner() Player.STATE_READY -> hideSpinner() Player.STATE_ENDED -> onEnded() } } override fun onPlayerError(error: PlaybackException) { log.error("Playback error", error) } }) ``` ### DRM (Widevine) ```kotlin val drmConfig = MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) .setLicenseUri(licenseUrl) .setMultiSession(true) .build() val mediaItem = MediaItem.Builder() .setUri(streamUrl) .setDrmConfiguration(drmConfig) .build() ``` ## 🤔 의사결정 기준 | 상황 | 도구 | |---|---| | 영상 재생 (mp4) | ExoPlayer Builder | | Live streaming | HLS / DASH MediaSource | | Background audio | MediaSessionService | | Cast | Cast extension | | DRM | DrmConfiguration | | 짧은 효과음 | SoundPool 또는 MediaPlayer (가벼움) | ## ❌ 안티패턴 - **player.release() 누락**: 메모리 / surface leak. - **여러 화면이 같은 player 인스턴스 + 미관리**: surface 충돌. - **lifecycle 안 맞춤**: 백그라운드에서도 영상 디코딩 → 배터리 / 데이터. - **error 무시**: 사용자 멈춤 화면. retry button. - **너무 큰 buffer**: 메모리 폭발. LoadControl 조정. - **DRM 토큰 만료 후 재시도 X**: 영상 멈춤. ## 🤖 LLM 활용 힌트 - 신규 = androidx.media3 (옛 exoplayer2 X). - 백그라운드 audio = MediaSessionService. ## 🔗 관련 문서 - [[Android_Lifecycle_Aware_Components]] - [[Android_Compose_State_Hoisting]]