Notícias sobre produtos
Melhorando a reprodução de mídia: um estudo detalhado do PreloadManager da Media3 – Parte 2
Leitura de 9 minutos
Esta é a segunda parte da nossa série de três partes sobre pré-carregamento de mídia com a Media3. Esta série foi criada para orientar você no processo de criação de experiências de mídia altamente responsivas e de baixa latência nos seus apps Android.
- Parte 1: introdução à pré-carga com Media3 abordou os fundamentos. Exploramos a distinção entre PreloadConfiguration para playlists simples e o DefaultPreloadManager mais eficiente para interfaces de usuário dinâmicas. Você aprendeu a implementar o ciclo de vida básico da API: adicionar mídia com add(), recuperar um MediaSource preparado com getMediaSource(), gerenciar prioridades com setCurrentPlayingIndex() e invalidate() e liberar recursos com remove() e release().
- Parte 2 (esta postagem): neste blog, vamos explorar os recursos avançados do DefaultPreloadManager. Vamos abordar como gerar insights com PreloadManagerListener, implementar práticas recomendadas prontas para produção, como compartilhar componentes principais com o ExoPlayer, e dominar o padrão de janela deslizante para gerenciar a memória de maneira eficaz.
- Parte 3: a parte final desta série vai abordar a integração do PreloadManager com um cache em disco persistente, permitindo reduzir o consumo de dados com gerenciamento de recursos e oferecer uma experiência perfeita.
Se você não conhece o pré-carregamento no Media3, recomendamos ler a Parte 1 antes de continuar. Para quem já está pronto para ir além do básico, vamos explorar como melhorar a implementação da reprodução de mídia.
Ouvindo: buscar análises com PreloadManagerListener
Ao lançar um recurso em produção, como desenvolvedor de apps, você também quer entender e capturar as análises relacionadas a ele. Como ter certeza de que sua estratégia de pré-carregamento é eficaz em um ambiente real? Para responder a essa pergunta, são necessários dados sobre taxas de sucesso, falhas e desempenho. A interface PreloadManagerListener é o principal mecanismo para coletar esses dados.
O PreloadManagerListener oferece dois callbacks essenciais que fornecem insights importantes sobre o processo e o status do pré-carregamento.
- onCompleted(MediaItem mediaItem): esse callback é invocado quando uma solicitação de pré-carregamento é concluída com êxito, conforme definido pelo TargetPreloadStatusControl.
- onError(erro PreloadException): esse callback pode ser útil para depuração e monitoramento. Ele é invocado quando um pré-carregamento falha, fornecendo a exceção associada.
É possível registrar um listener com uma única chamada de método, conforme mostrado no exemplo de código a seguir:
val preloadManagerListener = object : PreloadManagerListener { override fun onCompleted(mediaItem: MediaItem) { // Log success for analytics. Log.d("PreloadAnalytics", "Preload completed for $mediaItem") } override fun onError( preloadError: PreloadException) { // Log the specific error for debugging and monitoring. Log.e("PreloadAnalytics", "Preload error ", preloadError) } } preloadManager.addListener(preloadManagerListener)
Extrair insights do listener
Esses callbacks de listener podem ser conectados ao seu pipeline de análise. Ao encaminhar esses eventos para seu mecanismo de análise, você pode responder a perguntas importantes, como:
- Qual é nossa taxa de sucesso de pré-carregamento? (proporção de eventos onCompleted para o total de tentativas de pré-carregamento)
- Quais CDNs ou formatos de vídeo apresentam as maiores taxas de erro? (Ao analisar as exceções de onError)
- Qual é a nossa taxa de erros de pré-carregamento? (proporção de eventos "onError" em relação ao total de tentativas de pré-carregamento)
Esses dados podem fornecer feedback quantitativo sobre sua estratégia de pré-carregamento, permitindo testes A/B e melhorias na experiência do usuário com base em dados. Esses dados podem ajudar você a ajustar de forma inteligente a duração do pré-carregamento, o número de vídeos que você quer pré-carregar e os buffers alocados.
Além da depuração: como usar onError para um retorno de interface elegante
Uma pré-carga com falha é um forte indicador de um evento de buffer iminente para o usuário. O callback onError permite que você responda de forma reativa. Em vez de apenas registrar o erro, você pode adaptar a interface. Por exemplo, se o próximo vídeo não for pré-carregado, o aplicativo poderá desativar a reprodução automática para o próximo deslize, exigindo um toque do usuário para iniciar a reprodução.
Além disso, ao inspecionar o tipo PreloadException, é possível definir uma estratégia de nova tentativa mais inteligente. Um app pode remover imediatamente uma fonte com falha do gerenciador com base na mensagem de erro ou no código de status HTTP. O item precisa ser removido do fluxo da interface de acordo com o problema para não afetar a experiência do usuário. Você também pode receber dados mais detalhados de PreloadException, como HttpDataSourceException, para investigar melhor os erros. Leia mais sobre solução de problemas do ExoPlayer.
Sistema de parceiros: por que é necessário compartilhar componentes com o ExoPlayer?
O DefaultPreloadManager e o ExoPlayer foram criados para funcionar juntos. Para garantir a estabilidade e a eficiência, eles precisam compartilhar vários componentes principais. Se eles operarem com componentes separados e não coordenados, isso poderá afetar a segurança de encadeamento e a usabilidade das faixas pré-carregadas no player, já que precisamos garantir que elas sejam reproduzidas no player correto. Os componentes separados também podem competir por recursos limitados, como largura de banda de rede e memória, o que pode levar à degradação do desempenho. Uma parte importante do ciclo de vida é o descarte adequado. A ordem recomendada é liberar primeiro o PreloadManager e depois o ExoPlayer.
O DefaultPreloadManager.Builder foi projetado para facilitar esse compartilhamento e tem APIs para instanciar o PreloadManager e uma instância de player vinculada. Vamos ver por que componentes como BandwidthMeter, LoadControl, TrackSelector e Looper precisam ser compartilhados. Confira a representação visual de como esses componentes interagem com a reprodução do ExoPlayer.
Como evitar conflitos de largura de banda com um BandwidthMeter compartilhado
O BandwidthMeter fornece uma estimativa da largura de banda de rede disponível com base nas taxas de transferência históricas. Se o PreloadManager e o player usarem instâncias separadas, eles não vão conhecer a atividade de rede um do outro, o que pode levar a falhas. Por exemplo, considere o cenário em que um usuário está assistindo um vídeo, a conexão de rede dele é degradada e o MediaSource de pré-carregamento inicia simultaneamente um download agressivo para um vídeo futuro. A atividade de pré-carregamento do MediaSource consumiria a largura de banda necessária para o player ativo, fazendo com que o vídeo atual parasse. Uma interrupção durante a reprodução é uma falha significativa na experiência do usuário.
Ao compartilhar um único BandwidthMeter, o TrackSelector consegue selecionar faixas da mais alta qualidade, considerando as condições atuais da rede e o estado do buffer, durante o pré-carregamento ou a reprodução. Em seguida, ele pode tomar decisões inteligentes para proteger a sessão de reprodução ativa e garantir uma experiência tranquila.
preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)
Garantir a consistência com componentes compartilhados LoadControl, TrackSelector e Renderer do ExoPlayer
- LoadControl: esse componente determina a política de buffer, como a quantidade de dados a serem armazenados em buffer antes de iniciar a reprodução e quando iniciar ou parar de carregar mais dados. O compartilhamento do LoadControl garante que o consumo de memória do player e do PreloadManager seja orientado por uma única estratégia de buffer coordenada em mídias pré-carregadas e em reprodução ativa, evitando a disputa de recursos. Você precisará alocar o tamanho do buffer de maneira inteligente, coordenando com quantos itens e por quanto tempo você está pré-carregando para garantir a consistência. Em momentos de disputa, o player prioriza a reprodução do item atual exibido na tela. Com um LoadControl compartilhado, o gerenciador de pré-carregamento continua pré-carregando enquanto os bytes de buffer de destino alocados para pré-carregamento não atingem o limite máximo. Ele não espera até que o carregamento para reprodução seja concluído.
Observação: o compartilhamento de LoadControl na versão mais recente do Media3 (1.8) garante que o alocador possa ser compartilhado corretamente com o PreloadManager e o player. Usar o LoadControl para controlar o pré-carregamento de maneira eficaz é um recurso que estará disponível na próxima versão do Media3 1.9.
preloadManagerBuilder.setLoadControl(customLoadControl)
- TrackSelector: esse componente é responsável por selecionar quais faixas (por exemplo, vídeo de uma determinada resolução, áudio em um idioma específico) serão carregadas e reproduzidas. O compartilhamento garante que as faixas selecionadas durante o pré-carregamento sejam as mesmas que o player vai usar. Isso evita um cenário de desperdício em que uma faixa de vídeo de 480p é pré-carregada, apenas para o player descartá-la imediatamente e buscar uma faixa de 720p na reprodução.< br /> O gerenciador de pré-carregamento NÃO deve compartilhar a mesma instância do TrackSelector com o player. Em vez disso, eles precisam usar a instância diferente do TrackSelector, mas da mesma implementação. Por isso, definimos o TrackSelectorFactory em vez de um TrackSelector no DefaultPreloadManager.Builder.
preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)
- Renderizador: esse componente é responsável por entender os recursos do player sem criar os renderizadores completos. Ele verifica esse projeto para saber quais formatos de vídeo, áudio e texto o player final vai aceitar. Isso permite que ele selecione e baixe de forma inteligente apenas a faixa de mídia compatível e evita o desperdício de largura de banda em conteúdo que o player não pode reproduzir.
preloadManagerBuilder.setRenderersFactory(customRenderersFactory)
Leia sobre mais componentes do ExoPlayer.
A regra de ouro: um Reprodutor de loop comum para guiar todos os outros
A linha de execução em que uma instância do ExoPlayer pode ser acessada pode ser especificada explicitamente transmitindo um Looper ao criar o player. O Looper da linha de execução em que o player precisa ser acessado pode ser consultado usando Player.getApplicationLooper. Ao manter um Looper compartilhado entre o player e o PreloadManager, é garantido que todas as operações nesses objetos de mídia compartilhados sejam serializadas na fila de mensagens de uma única linha de execução. Isso pode reduzir os bugs de simultaneidade.
Todas as interações entre o PreloadManager e o player com fontes de mídia a serem carregadas ou pré-carregadas precisam acontecer na mesma linha de execução de reprodução. Compartilhar o Looper é essencial para a segurança de encadeamento. Por isso, precisamos compartilhar o PlaybackLooper entre o PreloadManager e o player.
O PreloadManager prepara um objeto MediaSource com estado em segundo plano. Quando o código da interface chama player.setMediaSource(mediaSource), você está transferindo esse objeto complexo e com estado da MediaSource de pré-carregamento para o player. Nesse cenário, todo o PreloadMediaSource é movido do gerenciador para o player. Todas essas interações e transferências precisam ocorrer no mesmo PlaybackLooper.
Se o PreloadManager e o ExoPlayer estivessem operando em linhas de execução diferentes, uma condição de disputa poderia ocorrer. A linha de execução do PreloadManager pode estar modificando o estado interno do MediaSource (por exemplo, gravando novos dados em um buffer) no momento exato em que a linha de execução do player está tentando ler dele. Isso leva a um comportamento imprevisível e a uma IllegalStateException difícil de depurar.
preloadManagerBuilder.setPreloadLooper(playbackLooper)
Vamos ver como você pode compartilhar todos os componentes acima entre o ExoPlayer e o DefaultPreloadManager na própria configuração.
val preloadManagerBuilder = DefaultPreloadManager.Builder(context, targetPreloadStatusControl) // Optional - Share components between ExoPlayer and DefaultPreloadManager preloadManagerBuilder .setBandwidthMeter(customBandwidthMeter) .setLoadControl(customLoadControl) .setMediaSourceFactory(customMediaSourceFactory) .setTrackSelectorFactory(customTrackSelectorFactory) .setRenderersFactory(customRenderersFactory) .setPreloadLooper(playbackLooper) val preloadManager = val preloadManagerBuilder.build()
Dica: se você usar os componentes padrão no ExoPlayer, como DefaultLoadControl etc., não será necessário compartilhá-los explicitamente com o DefaultPreloadManager. Quando você cria a instância do ExoPlayer usando o buildExoPlayer do DefaultPreloadManager.Builder, esses componentes são referenciados automaticamente entre si se você usar as implementações padrão com configurações padrão. No entanto, se você usar componentes ou configurações personalizadas, notifique explicitamente o DefaultPreloadManager sobre eles usando as APIs acima.
Pré-carregamento pronto para produção: o padrão de janela deslizante
Em um feed dinâmico, o usuário pode rolar uma quantidade praticamente infinita de conteúdo. Se você adicionar vídeos continuamente ao DefaultPreloadManager sem uma estratégia de remoção correspondente, vai causar um OutOfMemoryError. Cada MediaSource pré-carregado mantém uma SampleQueue, que aloca buffers de memória. À medida que se acumulam, eles podem esgotar o espaço de heap do aplicativo. A solução é um algoritmo que talvez você já conheça, chamado de janela deslizante. O padrão de janela deslizante mantém um conjunto pequeno e gerenciável de itens na memória que são logicamente adjacentes à posição atual do usuário no feed. À medida que o usuário rola a tela, essa "janela" de itens gerenciados desliza com ele, adicionando novos itens que aparecem e removendo os que estão distantes.
Implementar o padrão de janela deslizante
É essencial entender que o PreloadManager não oferece um método setWindowSize() integrado. A janela deslizante é um padrão de design que você, o desenvolvedor, é responsável por implementar usando os métodos primitivos add() e remove(). A lógica do aplicativo precisa conectar eventos da interface, como uma rolagem ou mudança de página, a essas chamadas de API. Se você quiser uma referência de código para isso, temos esse padrão de janela deslizante implementado na amostra socialite, que também inclui um PreloadManagerWrapper que imita uma janela deslizante.
Não se esqueça de adicionar preloadManager.remove(mediaItem) à sua implementação quando o item não for mais aparecer em breve na visualização do usuário. Não remover itens que não estão mais próximos do usuário é a principal causa de problemas de memória em implementações de pré-carregamento. A chamada remove() garante que os recursos sejam liberados, o que ajuda a manter o uso da memória do app limitado e estável.
Ajuste de uma estratégia de pré-carregamento categorizada com TargetPreloadStatusControl
Agora que definimos o que pré-carregar (os itens na nossa janela), podemos aplicar uma estratégia bem definida de quanto pré-carregar para cada item. Já vimos como alcançar essa granularidade com a configuração TargetPreloadStatusControl na Parte 1.
Um item na posição +/- 1 tem uma probabilidade maior de ser reproduzido do que um item na posição +/- 4. Você pode alocar mais recursos (rede, CPU, memória) para os itens que o usuário tem mais chances de ver em seguida. Isso cria uma estratégia de "pré-carregamento" com base na proximidade, que é a chave para equilibrar a reprodução imediata com o uso eficiente de recursos.
Você pode usar dados de análise via PreloadManagerListener, conforme discutido nas seções anteriores, para decidir sua estratégia de duração do pré-carregamento.
Conclusão e próximas etapas
Agora você tem o conhecimento avançado para criar feeds de mídia rápidos, estáveis e eficientes em termos de recursos usando o DefaultPreloadManager da Media3.
Vamos recapitular os principais pontos:
- Use PreloadManagerListener para coletar insights de análise e implementar um tratamento de erros robusto.
- Sempre use um único DefaultPreloadManager.Builder para criar instâncias do gerenciador e do player e garantir que componentes importantes sejam compartilhados.
- Implemente o padrão de janela deslizante gerenciando ativamente as chamadas add() e remove() para evitar OutOfMemoryError.
- Use TargetPreloadStatusControl para criar uma estratégia de pré-carregamento inteligente e em camadas que equilibra performance e consumo de recursos.
O que vem a seguir na Parte 3: armazenamento em cache com mídia pré-carregada
O pré-carregamento de dados na memória oferece um benefício imediato de performance, mas pode ter compensações. Depois que o aplicativo é fechado ou a mídia pré-carregada é removida do gerenciador, os dados desaparecem. Para alcançar um nível de otimização mais persistente, podemos combinar o pré-carregamento com o cache de disco. Esse recurso está em desenvolvimento e será lançado em alguns meses.
Quer compartilhar algum feedback? Queremos saber sua opinião.
Fique por dentro e acelere a reprodução dos seus vídeos! 🚀
Continuar lendo
-
Notícias sobre produtos
Nos apps atuais focados em mídia, oferecer uma experiência de reprodução tranquila e ininterrupta é fundamental para uma experiência do usuário agradável. Os usuários esperam que os vídeos comecem instantaneamente e sejam reproduzidos sem problemas e sem pausas.
Mayuri Khinvasara Khabya • Leitura de 8 minutos
-
Notícias sobre produtos
O fluxo de trabalho e as necessidades de IA de cada desenvolvedor são únicos. Por isso, é importante poder escolher como a IA ajuda no desenvolvimento. Em janeiro, lançamos a opção de escolher qualquer modelo de IA local ou remoto para ativar a funcionalidade de IA no Android Studio.
Matthew Warner • Leitura de 2 minutos
-
Notícias sobre produtos
O Android Studio Panda 3 agora está estável e pronto para uso em produção. Com essa versão, você tem ainda mais controle e personalização sobre seus fluxos de trabalho com tecnologia de IA, o que facilita a criação de apps Android de alta qualidade.
Matt Dyor • 3 min de leitura
Fique por dentro
Receba os insights mais recentes sobre desenvolvimento Android na sua caixa de entrada semanalmente.