---
title: "Media3 ExoPlayer com Jetpack Compose e Kotlin em 2026"
url: "https://kotlin.dev.br/blog/media3-exoplayer-compose-kotlin-2026/"
markdown_url: "https://kotlin.dev.br/blog/media3-exoplayer-compose-kotlin-2026.MD"
description: "Implemente Media3 (ExoPlayer) com Kotlin e Jetpack Compose: dependências, PlayerView, lifecycle, playlists, cache, áudio em background e erros comuns."
date: "2026-08-11"
author: "Karina Melo"
---

# Media3 ExoPlayer com Jetpack Compose e Kotlin em 2026

Implemente Media3 (ExoPlayer) com Kotlin e Jetpack Compose: dependências, PlayerView, lifecycle, playlists, cache, áudio em background e erros comuns.


**Resposta rápida:** em 2026, o caminho oficial para reproduzir vídeo e áudio no Android com Kotlin é **AndroidX Media3**, a evolução do ExoPlayer. Use o artefato `media3-exoplayer` para o motor, `media3-ui` (ou UI Compose quando o projeto já adotar o módulo estável do time) para a superfície de vídeo, e amarre o **ciclo de vida** da `Activity`/`Composable` para liberar o player. O padrão prático com Jetpack Compose é: criar o `ExoPlayer` fora da recomposição, embutir `PlayerView`/`PlayerView` via `AndroidView`, observar estados com `Player.Listener` ou wrappers, e liberar com `player.release()` no `onDispose`/`onCleared`. Media3 não é “só um `VideoView` moderno”: ele cobre adaptive streaming (HLS/DASH), playlists, cache, faixas de legenda, áudio em background e integração com `MediaSession`.

Apps de curso, fintech com vídeo KYC, marketplaces com demo de produto, podcasts e redes sociais internas pedem player confiável. O erro clássico em Compose é recriar o `ExoPlayer` a cada recomposição, vazar superfície ao rotacionar a tela ou esquecer `release()` — e o app vira um sumidouro de memória e bateria. Este guia mostra a base de produção em Kotlin: dependências, player único, UI Compose, lifecycle, playlist, cache, background e checklist de QA. Conecte com [permissões no Android](/blog/permissoes-android-kotlin-2026/), [WorkManager](/blog/workmanager-kotlin-android-2026/) para pré-download, [Coil no Compose](/blog/coil-jetpack-compose-kotlin-2026/) para thumbnails e [offline-first](/blog/android-offline-first-kotlin-2026/) quando o conteúdo precisa sobreviver sem rede.

## Media3 ou “ainda ExoPlayer”?

**Media3** é o pacote AndroidX que unifica o antigo ExoPlayer, as UIs de mídia e as APIs de sessão. O motor de playback continua sendo o ExoPlayer, agora no namespace `androidx.media3.exoplayer`. Em código e vagas no Brasil, as pessoas ainda dizem “ExoPlayer”; em dependências Gradle, o correto é `androidx.media3:*`.

| Necessidade | Caminho em 2026 |
|---|---|
| Vídeo/áudio local ou remoto | `media3-exoplayer` |
| Controles e superfície de vídeo | `media3-ui` (+ `AndroidView` no Compose) |
| Notificação / fones / Android Auto | `media3-session` |
| Transformações, concat, clipping | `media3-transformer` / editores de `MediaItem` |
| Apenas escolher arquivo de mídia do usuário | Photo Picker / `OpenDocument` — sem player embutido |

Se o produto só precisa “abrir um MP4 externo”, um intent ou o player do sistema pode bastar. Media3 vale a pena quando o app controla buffer, qualidade, legenda, analytics de playback ou UX própria.

## Dependências base

No módulo Android, com Version Catalog ou coordenadas explícitas:

```kotlin
// build.gradle.kts (módulo app)
dependencies {
    val media3 = "1.5.1" // confira a estável do seu BOM/AGP

    implementation("androidx.media3:media3-exoplayer:$media3")
    implementation("androidx.media3:media3-ui:$media3")
    implementation("androidx.media3:media3-session:$media3") // se for background / notificação
    implementation("androidx.media3:media3-exoplayer-hls:$media3") // se usar HLS
    implementation("androidx.media3:media3-datasource-okhttp:$media3") // opcional, HTTP custom
}
```

Alinhe a versão ao `compileSdk` e ao restante do AndroidX. Em times com vários módulos, centralize no [Gradle Version Catalog](/blog/gradle-version-catalog-kotlin-2026/) e no [Compose BOM](/blog/compose-bom-android-kotlin-guia-2026/) para a UI.

No `AndroidManifest.xml`, declare só o que o fluxo usa:

```xml
<uses-permission android:name="android.permission.INTERNET" />
<!-- Só se for tocar com tela desligada / serviço em foreground -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
```

Não peça armazenamento amplo se o conteúdo vem de URL HTTPS ou de um URI já concedido pelo sistema de arquivos.

## Criando o ExoPlayer uma vez

O player é um objeto **caro** e com estado. Trate-o como dependência de `ViewModel` ou de um holder com escopo de tela — nunca como `remember { ExoPlayer.Builder(...).build() }` recriado sem controle.

```kotlin
class VideoPlayerViewModel(
    application: Application,
) : AndroidViewModel(application) {

    val player: ExoPlayer = ExoPlayer.Builder(application).build().apply {
        playWhenReady = true
        repeatMode = Player.REPEAT_MODE_OFF
    }

    fun setMedia(url: String) {
        val item = MediaItem.fromUri(url)
        player.setMediaItem(item)
        player.prepare()
    }

    fun setPlaylist(urls: List<String>, startIndex: Int = 0) {
        player.setMediaItems(urls.map(MediaItem::fromUri), startIndex, /* startPositionMs */ 0L)
        player.prepare()
    }

    override fun onCleared() {
        player.release()
        super.onCleared()
    }
}
```

Regras que evitam 80% dos bugs:

1. **Um player por fluxo de UI** (ou um player de app com fila única, se o produto for “um áudio por vez”).
2. **`prepare()` depois** de configurar `MediaItem`s, não o contrário em ordem confusa.
3. **`release()` exatamente uma vez** no fim do escopo — `onCleared` do ViewModel ou `DisposableEffect` se o player for local à tela.
4. **Não chame `player.release()` e continue usando** a referência em listeners.

## Compose: PlayerView com AndroidView

Compose ainda se apoia na `PlayerView` (View system) para a superfície de vídeo na maioria dos apps de produção. O padrão estável:

```kotlin
@Composable
fun Media3Player(
    player: ExoPlayer,
    modifier: Modifier = Modifier,
    useController: Boolean = true,
) {
    AndroidView(
        modifier = modifier.fillMaxWidth().aspectRatio(16f / 9f),
        factory = { context ->
            PlayerView(context).apply {
                this.player = player
                this.useController = useController
                // layoutParams padrão do PlayerView já cobre a superfície
            }
        },
        update = { playerView ->
            if (playerView.player != player) {
                playerView.player = player
            }
        },
    )
}
```

Pontos de atenção:

- No `update`, **não** recrie a `PlayerView` — só reatribua o player se mudar.
- Em navegação Compose, ao sair da tela, ou você mantém o player no `ViewModel` (continua bufferizando) ou pausa explicitamente (`player.pause()`) no `DisposableEffect` de saída.
- Para tela cheia, combine com edge-to-edge e orientação; veja o guia de [Android 16 edge-to-edge](/blog/android-16-edge-to-edge-compose-kotlin-2026/) e o de [Predictive Back](/blog/predictive-back-android-compose-kotlin-2026/) se a tela de player interceptar o gesto de voltar.

```kotlin
@Composable
fun AulaVideoScreen(
    viewModel: VideoPlayerViewModel = viewModel(),
    url: String,
) {
    LaunchedEffect(url) {
        viewModel.setMedia(url)
    }

    DisposableEffect(Unit) {
        onDispose {
            // Se o ViewModel sobrevive à tela, só pause:
            viewModel.player.pause()
        }
    }

    Column {
        Media3Player(player = viewModel.player)
        // UI de título, capítulos, velocidade etc. em Compose puro
    }
}
```

Controles customizados (botões Compose em cima do vídeo) funcionam bem: deixe `useController = false` e chame `player.play()`, `pause()`, `seekTo`, `setPlaybackSpeed` a partir da UI.

## Observando estado sem recomposição selvagem

Evite ler `player.isPlaying` direto no corpo do `@Composable` sem assinatura — a UI não recompõe sozinha. Use um listener ou um `State` derivado:

```kotlin
@Composable
fun rememberIsPlaying(player: Player): State<Boolean> {
    return produceState(initialValue = player.isPlaying, player) {
        val listener = object : Player.Listener {
            override fun onIsPlayingChanged(isPlaying: Boolean) {
                value = isPlaying
            }
        }
        player.addListener(listener)
        awaitDispose { player.removeListener(listener) }
    }
}
```

O mesmo padrão serve para `playbackState`, `mediaItemIndex`, erros e faixas de legenda. Em telas ricas, prefira expor um `StateFlow` no ViewModel atualizado pelo listener — fica testável e alinhado a [MVVM](/guias/guia-arquitetura-mvvm-kotlin/).

## Adaptive streaming, legendas e erros

Para HLS:

```kotlin
val item = MediaItem.Builder()
    .setUri("https://cdn.exemplo.com/aula/master.m3u8")
    .setMimeType(MimeTypes.APPLICATION_M3U8)
    .build()
player.setMediaItem(item)
player.prepare()
```

Para legenda lateral (WebVTT/TTML), use `MediaItem.SubtitleConfiguration` no builder do item. Sempre trate `PlaybackException` no listener: rede instável, 403 de CDN, certificado, formato não suportado e DRM têm mensagens e UX diferentes. Mostre retry com backoff e, se o conteúdo for pago, diferencie “sem internet” de “sem permissão”.

| Sintoma | Causa comum | Ação |
|---|---|---|
| Tela preta, áudio ok | Superfície não anexada / `player` trocado tarde | Reatribuir `PlayerView.player` no `update` |
| Travou ao rotacionar | Player recriado na recomposição | Mover player para ViewModel |
| Estoura memória | Vários ExoPlayers vivos | Um por fluxo + `release()` |
| 403 no CDN | Header/token ausente | `DataSource.Factory` com OkHttp + interceptor |
| Não pausa em ligação | Falta `AudioAttributes` / focus | Configurar foco de áudio no builder |

## Cache e pré-download

Para cursos e catálogos, cache HTTP do Media3 reduz dados móveis:

```kotlin
val cache = SimpleCache(
    File(context.cacheDir, "media3"),
    LeastRecentlyUsedCacheEvictor(200L * 1024 * 1024),
    StandaloneDatabaseProvider(context),
)

val cacheDataSourceFactory = CacheDataSource.Factory()
    .setCache(cache)
    .setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory())

val player = ExoPlayer.Builder(context)
    .setMediaSourceFactory(DefaultMediaSourceFactory(cacheDataSourceFactory))
    .build()
```

Pré-download grande (pacote offline de aula) costuma combinar Media3 DownloadManager com [WorkManager](/blog/workmanager-kotlin-android-2026/) e restrição de Wi-Fi. Não misture “cache oportunista” com “download explícito do usuário” na mesma UX — o usuário precisa saber o que ocupa armazenamento.

## Áudio em background e MediaSession

Vídeo com tela ligada é uma Activity. Podcast/música com tela desligada precisa de **serviço de playback** + `MediaSession` (Media3 Session) + notificação de mídia + foco de áudio. O esqueleto mental:

1. `ExoPlayer` vive no `Service` (ou em um holder process-scoped gerenciado pelo serviço).
2. `MediaSession` expõe play/pause/seek para fones, notificação e assistentes.
3. Foreground service com tipo `mediaPlayback` no manifesto (Android 14+).
4. Trate `AUDIOFOCUS_LOSS` e fones desconectados.

Se o produto é “vídeo em tela cheia e pronto”, não carregue session/serviço só por hábito — complexidade sobe rápido e vagas de Android seniores cobram exatamente essa distinção.

## Testes e QA

- **Robolectric/instrumented:** prefira fakes de `Player` para ViewModel; teste de buffer real é flaky em CI.
- **Dispositivo físico:** valide HLS, rotação, picture-in-picture (se houver), gestos de brightness/seek e multitasking.
- **Rede:** throttle 3G, airplane mode no meio do play, troca Wi-Fi → 4G.
- **Acessibilidade:** controles com content description, legendas legíveis, não dependa só de cor no estado “ao vivo”.
- **Bateria:** deixe o app em background com áudio; confira se o serviço para quando o usuário manda stop.

Para fluxos de UI Compose em geral, o guia de [testes Android com Compose e Maestro](/guias/testes-android-compose-maestro/) complementa o que o Media3 sozinho não cobre.

## Checklist de produção

1. Dependências `androidx.media3:*` alinhadas — zero `com.google.android.exoplayer2` legado no mesmo app sem ponte clara.
2. Player criado uma vez; `release()` no fim do escopo.
3. Compose via `AndroidView` + `PlayerView`; UI chrome em Compose.
4. Listener para estado, erro e troca de item.
5. `AudioAttributes` e foco de áudio configurados.
6. Cache ou download com política de tamanho e limpeza.
7. HTTPS, tokens de CDN e tratamento de 4xx/5xx.
8. Manifesto de foreground service só se houver background real.
9. Métricas: tempo até first frame, rebuffer count, taxa de erro (útil em [observabilidade](/blog/opentelemetry-kotlin-spring-ktor-2026/) se o backend correlacionar sessões).
10. Documente no README do módulo qual formato (MP4/HLS/DASH) o app suporta de verdade.

## Quando não usar Media3

- Preview de câmera ao vivo → [CameraX](/blog/camerax-compose-kotlin-2026/).
- Só escolher foto/vídeo da galeria → Photo Picker / `PickVisualMedia`.
- Animação de UI, não mídia → [animações no Compose](/blog/animacoes-jetpack-compose-kotlin-2026/).
- Upload de vídeo em background sem playback → WorkManager + API de storage.

## Carreira: por que isso aparece em vagas

Descrições de vagas Android no Brasil ainda listam “ExoPlayer” ao lado de Kotlin, Coroutines e Compose — especialmente em edtech, streaming, fintech e marketplaces. Saber Media3 na prática (lifecycle, HLS, session) diferencia de quem só colou um snippet. Explore [vagas Kotlin](/vagas/) e o hub de [carreira](/carreira/) para cruzar stack com salário e senioridade.

## Perguntas frequentes

### Media3 substitui ExoPlayer?

O motor continua sendo ExoPlayer, agora distribuído como AndroidX Media3. Novos projetos devem usar `androidx.media3`, não o pacote antigo `com.google.android.exoplayer2`.

### Dá para usar Media3 100% em Compose sem PlayerView?

Há experimentos e módulos de UI Compose no ecossistema, mas o padrão maduro de produção em 2026 ainda embute a superfície View (`PlayerView`) e compõe o restante da interface em Compose. Avalie estabilidade da lib Compose de mídia do seu time antes de abandonar `AndroidView`.

### Como pausar quando o usuário sai da tela?

Se o `ViewModel` sobrevive (Navigation com back stack), chame `player.pause()` no `DisposableEffect` de saída ou em `onStop` da Activity hospedeira. Se a tela “é” o player e não volta, `release()` no `onCleared`.

### Preciso de coroutines dentro do ExoPlayer?

O player tem callbacks próprios. Use coroutines no ViewModel para carregar a URL (API, Room, DataStore) e então chamar `setMediaItem`. Não envolva `player.play()` em `withContext(Dispatchers.IO)` — o ExoPlayer já gerencia threads internas.

### E DRM / Widevine?

Conteúdo premium com DRM exige `MediaItem` com configuração DRM, licenças e, em geral, suporte do provedor de streaming. Só adicione essa complexidade quando o catálogo exigir; para MP4/HLS abertos, o caminho simples acima basta.

## Conclusão

**Media3 (ExoPlayer) com Kotlin e Jetpack Compose** é o stack padrão de playback no Android moderno: um player de longa vida, UI híbrida Compose + `PlayerView`, listeners de estado, cache consciente e session só quando há background de verdade. Domine lifecycle e `release()`, e o restante — playlist, HLS, legendas, métricas — encaixa em cima dessa base.

Continue com [Jetpack Compose](/guias/guia-jetpack-compose/), [Navigation Compose](/blog/navigation-compose-navhost-navcontroller-kotlin-2026/), [permissões](/blog/permissoes-android-kotlin-2026/) e [modularização Android](/blog/modularizacao-android-kotlin-compose-2026/) se o player for um feature module. Para oportunidades que pedem essa stack, veja as [vagas Kotlin e Android](/vagas/) atualizadas no Kotlin Brasil.
