Novità sul prodotto

Migliorare la riproduzione dei contenuti multimediali: un approfondimento su PreloadManager di Media3 - Parte 2

Lettura di 9 minuti
Mayuri Khinvasara Khabya
Ingegnere per le relazioni con gli sviluppatori

Ti diamo il benvenuto alla seconda parte della nostra serie in tre parti sul precaricamento dei contenuti multimediali con Media3. Questa serie è progettata per guidarti nella creazione di esperienze multimediali a bassa latenza e altamente reattive nelle tue app Android.

  • Parte 1: Introduzione al precaricamento con Media3 ha trattato i concetti fondamentali. Abbiamo esplorato la distinzione tra PreloadConfiguration per playlist semplici e il più potente DefaultPreloadManager per interfacce utente dinamiche. Hai imparato a implementare il ciclo di vita di base dell'API: aggiungere contenuti multimediali con add(), recuperare un MediaSource preparato con getMediaSource(), gestire le priorità con setCurrentPlayingIndex() e invalidate() e rilasciare le risorse con remove() e release().
  • Parte 2 (questo post): in questo blog esploreremo le funzionalità avanzate di DefaultPreloadManager. Vedremo come ottenere insight con PreloadManagerListener, implementare le best practice pronte per la produzione, come la condivisione dei componenti principali con ExoPlayer, e padroneggiare il pattern della finestra scorrevole per gestire efficacemente la memoria.
  • Parte 3: la parte finale di questa serie approfondirà l'integrazione di PreloadManager con una cache del disco persistente, che ti consentirà di ridurre il consumo di dati con la gestione delle risorse e di offrire un'esperienza senza interruzioni.

Se non hai familiarità con il precaricamento in Media3, ti consigliiamo vivamente di leggere Parte 1 prima di procedere. Per chi è pronto ad andare oltre le nozioni di base, scopriamo come migliorare l'implementazione della riproduzione dei contenuti multimediali.

Ascoltare: recuperare i dati di analisi con PreloadManagerListener

Quando vuoi lanciare una funzionalità in produzione, in qualità di sviluppatore di app vuoi anche comprendere e acquisire i dati di analisi che la riguardano. Come puoi essere certo che la tua strategia di precaricamento sia efficace in un ambiente reale? Per rispondere a questa domanda, sono necessari dati su tassi di successo, errori e prestazioni. L'interfaccia PreloadManagerListener è il meccanismo principale per raccogliere questi dati.

PreloadManagerListener fornisce due callback essenziali che offrono insight fondamentali sul processo e sullo stato di precaricamento.

  • onCompleted(MediaItem mediaItem): questo callback viene richiamato al completamento di una richiesta di precaricamento, come definito da TargetPreloadStatusControl.
  • onError(PreloadException error): questo callback potrebbe essere utile per il debug e il monitoraggio. Viene richiamato quando un precaricamento non riesce, fornendo l'eccezione associata.

Puoi registrare un listener con una singola chiamata al metodo, come mostrato nel seguente codice di esempio:

  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)

Estrarre insight dal listener

Questi callback del listener possono essere collegati alla pipeline di analisi. Inoltrando questi eventi al motore di analisi, puoi rispondere a domande chiave come:

  • Qual è il nostro tasso di successo del precaricamento? (rapporto tra eventi onCompleted e tentativi di precaricamento totali)
  • Quali CDN o formati video mostrano i tassi di errore più elevati? (analizzando le eccezioni da onError)
  • Qual è il nostro tasso di errore del precaricamento? (rapporto tra eventi onError e tentativi di precaricamento totali)

Questi dati potrebbero fornirti un feedback quantitativo sulla tua strategia di precaricamento, consentendo test A/B e miglioramenti basati sui dati all'esperienza utente. Questi dati possono aiutarti ulteriormente a ottimizzare in modo intelligente le durate del precaricamento e il numero di video che vuoi precaricare, nonché i buffer che allochi.

Oltre al debug: utilizzare onError per il fallback della UI

Un precaricamento non riuscito è un forte indicatore di un evento di buffering imminente per l'utente. Il callback onError ti consente di rispondere in modo reattivo. Anziché semplicemente registrare l'errore, puoi adattare la UI. Ad esempio, se il precaricamento del video imminente non riesce, la tua applicazione potrebbe disattivare la riproduzione automatica per lo swipe successivo, richiedendo un tocco dell'utente per avviare la riproduzione.

Inoltre, esaminando il tipo PreloadException puoi definire una strategia di nuovi tentativi più intelligente. Un'app può scegliere di rimuovere immediatamente una sorgente non riuscita dal gestore in base al messaggio di errore o al codice di stato HTTP. L'elemento deve essere rimosso di conseguenza dal flusso della UI per evitare che i problemi di caricamento influiscano sull'esperienza utente. Puoi anche ottenere dati più granulari da PreloadException, ad esempio HttpDataSourceException, per approfondire gli errori. Scopri di più sulla risoluzione dei problemi di ExoPlayer.

Il sistema di supporto: perché è necessario condividere i componenti con ExoPlayer?

DefaultPreloadManager ed ExoPlayer sono progettati per funzionare insieme. Per garantire stabilità ed efficienza, devono condividere diversi componenti principali components. Se operano con componenti separati e non coordinati, ciò potrebbe influire sulla sicurezza dei thread e sull'usabilità delle tracce precaricate sul player, poiché dobbiamo assicurarci che le tracce precaricate vengano riprodotte sul player corretto. I componenti separati potrebbero anche competere per risorse limitate come la larghezza di banda della rete e la memoria, il che potrebbe portare a un peggioramento delle prestazioni. Una parte importante del ciclo di vita è la gestione dello smaltimento appropriato. L'ordine di smaltimento consigliato è di rilasciare prima PreloadManager, seguito da ExoPlayer.

DefaultPreloadManager.Builder è progettato per facilitare questa condivisione e dispone di API per creare un'istanza sia di PreloadManager sia di un player collegato. Vediamo perché è necessario condividere componenti come BandwidthMeter, LoadControl, TrackSelector, Looper. Controlla la rappresentazione visiva di come questi componenti interagiscono con la riproduzione di ExoPlayer.

preloadManager2.png

Evitare conflitti di larghezza di banda con un BandwidthMeter condiviso

Il BandwidthMeter fornisce una stima della larghezza di banda di rete disponibile in base ai tassi di trasferimento storici. Se PreloadManager e il player utilizzano istanze separate, non sono a conoscenza dell'attività di rete dell'altro, il che può portare a scenari di errore. Ad esempio, considera lo scenario in cui un utente sta guardando un video, la sua connessione di rete si deteriora e il precaricamento di MediaSource avvia contemporaneamente un download aggressivo per un video futuro. L'attività di precaricamento di MediaSource consumerebbe la larghezza di banda necessaria al player attivo, causando il blocco del video corrente. Un blocco durante la riproduzione è un errore significativo dell'esperienza utente.

Condividendo un singolo BandwidthMeter, TrackSelector è in grado di selezionare le tracce di qualità più elevata in base alle condizioni di rete correnti e allo stato del buffer, durante il precaricamento o la riproduzione. Può quindi prendere decisioni intelligenti per proteggere la sessione di riproduzione attiva e garantire un'esperienza senza interruzioni.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Garantire la coerenza con i componenti LoadControl, TrackSelector e Renderer condivisi di ExoPlayer

  • LoadControl: questo componente determina le norme di buffering, ad esempio la quantità di dati da memorizzare nel buffer prima di avviare la riproduzione e quando avviare o interrompere il caricamento di altri dati. La condivisione di LoadControl garantisce che il consumo di memoria del player e di PreloadManager sia guidato da una singola strategia di buffering coordinata sia per i contenuti multimediali precaricati sia per quelli in riproduzione attiva, evitando la contesa delle risorse. Dovrai allocare in modo intelligente le dimensioni del buffer coordinandoti con il numero di elementi che stai precaricando e con la durata, per garantire la coerenza. In caso di contesa, il player darà la priorità alla riproduzione dell'elemento corrente visualizzato sullo schermo. Con un LoadControl condiviso, il gestore di precaricamento continuerà a precaricare finché i byte del buffer di destinazione allocati per il precaricamento non avranno raggiunto il limite massimo, senza attendere il completamento del caricamento per la riproduzione.

Nota: la condivisione di LoadControl nell'ultima versione di Media3 (1.8) garantisce che il relativo Allocator possa essere condiviso correttamente con PreloadManager e il player. L'utilizzo di LoadControl per controllare efficacemente il precaricamento è una funzionalità che sarà disponibile nella prossima release di Media3 1.9.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: questo componente è responsabile della selezione delle tracce (ad esempio, video di una determinata risoluzione, audio in una lingua specifica) da caricare e riprodurre. La condivisione garantisce che le tracce selezionate durante il precaricamento siano le stesse che utilizzerà il player. In questo modo si evita uno scenario dispendioso in cui viene precaricata una traccia video a 480p, ma il player la scarta immediatamente e recupera una traccia a 720p durante la riproduzione.< br /> Il gestore di precaricamento NON deve condividere la stessa istanza di TrackSelector con il player. Devono invece utilizzare un'istanza di TrackSelector diversa, ma della stessa implementazione. Per questo motivo, in DefaultPreloadManager.Builder abbiamo impostato TrackSelectorFactory anziché TrackSelector.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer: questo componente è responsabile della comprensione delle funzionalità del player senza creare i renderer completi. Controlla questo progetto per vedere quali formati video, audio e di testo supporterà il player finale. In questo modo, può selezionare e scaricare in modo intelligente solo la traccia multimediale compatibile ed evita di sprecare larghezza di banda su contenuti che il player non può riprodurre.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Scopri di più sui componenti di ExoPlayer.

La regola d'oro: un Looper di riproduzione comune per domarli tutti

Il thread su cui è possibile accedere a un'istanza di ExoPlayer può essere specificato in modo esplicito passando un Looper durante la creazione del player. Il Looper del thread da cui è necessario accedere al player può essere eseguito una query utilizzando Player.getApplicationLooper. Mantenendo un Looper condiviso tra il player e PreloadManager, è garantito che tutte le operazioni su questi oggetti multimediali condivisi vengano serializzate nella coda di messaggi di un singolo thread. In questo modo è possibile ridurre gli errori di concorrenza.

Tutte le interazioni tra PreloadManager e il player con le sorgenti multimediali da caricare o precaricare devono avvenire sullo stesso thread di riproduzione. La condivisione di Looper è obbligatoria per la sicurezza dei thread, pertanto dobbiamo condividere PlaybackLooper tra PreloadManager e il player.

PreloadManager prepara un oggetto MediaSource stateful in background. Quando il codice della UI chiama player.setMediaSource(mediaSource), stai eseguendo un passaggio di questo oggetto complesso e stateful da MediaSource di precaricamento al player. In questo scenario, l'intero PreloadMediaSource viene spostato dal gestore al player. Tutte queste interazioni e questi passaggi devono avvenire sullo stesso PlaybackLooper.

Se PreloadManager ed ExoPlayer operassero su thread diversi, potrebbe verificarsi una race condition. Il thread di PreloadManager potrebbe modificare lo stato interno di MediaSource (ad es.scrivere nuovi dati in un buffer) esattamente nel momento in cui il thread del player tenta di leggerlo. Ciò comporta un comportamento imprevedibile, IllegalStateException difficile da eseguire il debug.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Vediamo come puoi condividere tutti i componenti sopra indicati tra ExoPlayer e DefaultPreloadManager nella configurazione stessa.

  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()

Suggerimento: se utilizzi i componenti predefiniti in ExoPlayer, ad esempio DefaultLoadControl, ecc., non devi condividerli in modo esplicito con DefaultPreloadManager. Quando crei l'istanza di ExoPlayer tramite buildExoPlayer di DefaultPreloadManager.Builder, questi componenti vengono referenziati automaticamente tra loro, se utilizzi le implementazioni predefinite con configurazioni predefinite. Tuttavia, se utilizzi componenti o configurazioni personalizzate, devi comunicarle in modo esplicito a DefaultPreloadManager tramite le API sopra indicate.

Precaricamento pronto per la produzione: il pattern della finestra scorrevole

In un feed dinamico, un utente può scorrere una quantità di contenuti praticamente infinita. Se aggiungi continuamente video a DefaultPreloadManager senza una strategia di rimozione corrispondente, causerai inevitabilmente un errore OutOfMemoryError. Ogni MediaSource precaricato contiene una SampleQueue che alloca buffer di memoria. Man mano che si accumulano, possono esaurire lo spazio heap dell'applicazione. La soluzione è un algoritmo che potresti già conoscere, chiamato finestra scorrevole. Il pattern della finestra scorrevole mantiene in memoria un piccolo set gestibile di elementi logicamente adiacenti alla posizione corrente dell'utente nel feed. Man mano che l'utente scorre, questa "finestra" di elementi gestiti scorre con lui, aggiungendo nuovi elementi che vengono visualizzati e rimuovendo quelli che ora sono distanti.

slidingwindow.png

Implementare il pattern della finestra scorrevole

È essenziale capire che PreloadManager non fornisce un metodo setWindowSize() integrato. La finestra scorrevole è un pattern di progettazione che lo sviluppatore è responsabile dell'implementazione utilizzando i metodi primitivi add() e remove(). La logica dell'applicazione deve collegare gli eventi della UI, come uno scorrimento o una modifica della pagina, a queste chiamate API. Se vuoi un riferimento al codice, abbiamo implementato questo pattern della finestra scorrevole nell'esempio di socialite, che include anche un PreloadManagerWrapper che imita una finestra scorrevole.

Non dimenticare di aggiungere preloadManager.remove(mediaItem) nell'implementazione quando è improbabile che l'elemento venga visualizzato a breve dall'utente. La mancata rimozione degli elementi che non sono più vicini all'utente è la causa principale dei problemi di memoria nelle implementazioni di precaricamento. La chiamata remove() garantisce che le risorse vengano rilasciate, il che ti aiuta a mantenere la memoria utilizzata dell'app limitata e stabile.

Ottimizzare una strategia di precaricamento categorizzata con TargetPreloadStatusControl

Ora che abbiamo definito cosa precaricare (gli elementi nella nostra finestra), possiamo applicare una strategia ben definita per la quantità di precaricamento di ogni elemento. Abbiamo già visto come ottenere questa granularità con la configurazione di TargetPreloadStatusControl nella Parte 1.

Per ricordare, un elemento in posizione +/- 1 potrebbe avere una probabilità di riproduzione maggiore rispetto a un elemento in posizione +/- 4. Puoi allocare più risorse (rete, CPU, memoria) agli elementi che l'utente ha maggiori probabilità di visualizzare successivamente. In questo modo si crea una strategia di "precaricamento" basata sulla prossimità, che è la chiave per bilanciare la riproduzione immediata con un utilizzo efficiente delle risorse.

Puoi utilizzare i dati di analisi tramite PreloadManagerListener, come descritto nelle sezioni precedenti, per decidere la strategia di durata del precaricamento.

Conclusioni e passaggi successivi

Ora hai le conoscenze avanzate per creare feed multimediali veloci, stabili ed efficienti in termini di risorse utilizzando DefaultPreloadManager di Media3.

Ricapitoliamo i concetti chiave:

  • Utilizza PreloadManagerListener per raccogliere insight di analisi e implementare una gestione degli errori efficace.
  • Utilizza sempre un singolo DefaultPreloadManager.Builder per creare sia il gestore sia le istanze del player per garantire la condivisione dei componenti importanti.
  • Implementa il pattern della finestra scorrevole gestendo attivamente le chiamate add() e remove() per evitare OutOfMemoryError.
  • Utilizza TargetPreloadStatusControl per creare una strategia di precaricamento intelligente a più livelli che bilanci prestazioni e consumo di risorse.

Cosa succederà nella Parte 3: memorizzazione nella cache con contenuti multimediali precaricati

Il precaricamento dei dati in memoria offre un vantaggio immediato in termini di prestazioni, ma può comportare compromessi. Una volta chiusa l'applicazione o rimossi i contenuti multimediali precaricati dal gestore, i dati vengono eliminati. Per ottenere un livello di ottimizzazione più persistente, possiamo combinare il precaricamento con la memorizzazione nella cache su disco. Questa funzionalità è in fase di sviluppo attivo e sarà disponibile a breve, tra qualche mese.

Vuoi condividere un feedback? Non vediamo l'ora di ricevere il tuo feedback.

Continua a seguirci e velocizza la riproduzione dei video. 🚀

Scritto da:

Continua a leggere