Novità sul prodotto

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

Lettura di 9 minuti
Mayuri Khinvasara Khabya
Developer Relations Engineer

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 nel processo di creazione di esperienze multimediali a bassa latenza e altamente reattive nelle tue app per Android.

  • La parte 1: Introduzione al precaricamento con Media3 ha trattato le nozioni di base. Abbiamo esaminato la distinzione tra PreloadConfiguration per le playlist semplici e DefaultPreloadManager, più potente, per le 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 post del blog esploriamo le funzionalità avanzate di DefaultPreloadManager. Vedremo come ottenere insight con PreloadManagerListener, implementare best practice pronte per la produzione come la condivisione dei componenti principali con ExoPlayer e padroneggiare il pattern della finestra scorrevole per gestire in modo efficace la memoria.
  • Parte 3: la parte finale di questa serie esaminerà l'integrazione di PreloadManager con una cache del disco permanente, consentendoti di ridurre il consumo di dati con la gestione delle risorse e di offrire un'esperienza senza interruzioni.

Se non hai mai utilizzato il precaricamento in Media3, ti consigliamo vivamente di leggere la Parte 1 prima di procedere. Per chi è pronto ad andare oltre le basi, vediamo come migliorare l'implementazione della riproduzione dei contenuti multimediali.

Ascolto: recuperare le analisi con PreloadManagerListener

Quando vuoi lanciare una funzionalità in produzione, in qualità di sviluppatore di app vuoi anche comprendere e acquisire le relative analisi. 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 rendimento. L'interfaccia PreloadManagerListener è il meccanismo principale per raccogliere questi dati.

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

  • onCompleted(MediaItem mediaItem): questo callback viene richiamato al completamento della richiesta di precaricamento, come definito da TargetPreloadStatusControl.
  • onError(errore PreloadException): questo callback può essere utile per il debug e il monitoraggio. Viene richiamato quando un precaricamento non va a buon fine, 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)

Estrazione di insight dall'ascoltatore 

Questi callback degli ascoltatori possono essere collegati alla tua pipeline di analisi. Se inoltri questi eventi al motore di analisi, puoi rispondere a domande chiave come:

  • Qual è la nostra percentuale 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 errori di 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 alla tua esperienza utente. Questi dati possono aiutarti ulteriormente a ottimizzare in modo intelligente la durata del precaricamento e il numero di video che vuoi precaricare, nonché i buffer che allochi.

Oltre al debug: utilizzo di onError per il fallback della UI controllato

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é registrare semplicemente l'errore, puoi adattare la UI. Ad esempio, se il precaricamento del video successivo non va a buon fine, l'applicazione potrebbe disattivare la riproduzione automatica per lo scorrimento 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 un'origine non riuscita dal gestore in base al messaggio di errore o al codice di stato HTTP. L'elemento deve essere rimosso dal flusso dell'interfaccia utente di conseguenza per evitare che i problemi di caricamento influiscano sull'esperienza utente. Puoi anche ottenere dati più granulari da PreloadException, ad esempio HttpDataSourceException, per analizzare ulteriormente gli errori. Scopri di più sulla risoluzione dei problemi di ExoPlayer.

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

DefaultPreloadManager ed ExoPlayer sono progettati per funzionare insieme. Per garantire stabilità ed efficienza, devono condividere diversi componenti di base. Se operano con componenti separati e non coordinati, ciò potrebbe influire sulla sicurezza dei thread e sull'usabilità delle tracce precaricate sul lettore, poiché dobbiamo assicurarci che le tracce precaricate vengano riprodotte sul lettore corretto. I componenti separati potrebbero anche competere per risorse limitate come la larghezza di banda di rete e la memoria, il che potrebbe comportare un peggioramento delle prestazioni. Una parte importante del ciclo di vita è la gestione dello smaltimento appropriato. L'ordine di smaltimento consigliato è il rilascio di 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'istanza del player collegata. Vediamo perché componenti come BandwidthMeter, LoadControl, TrackSelector e Looper devono essere condivisi. Controlla la rappresentazione visiva di come questi componenti interagiscono con la riproduzione di ExoPlayer.

preloadManager2.png

Prevenzione dei conflitti di larghezza di banda con un BandwidthMeter condiviso

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 peggiora e MediaSource di precaricamento 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 l'interruzione della riproduzione del video corrente. Un blocco durante la riproduzione è un grave problema di esperienza utente.

Condividendo un unico 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 fluida.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

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

  • LoadControl: questo componente determina la policy di buffering, ad esempio la quantità di dati da memorizzare nel buffer prima di avviare la riproduzione e quando iniziare 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 un'unica strategia di buffering coordinata sia per i contenuti multimediali precaricati sia per quelli riprodotti attivamente, evitando la contesa delle risorse. Per garantire la coerenza, dovrai allocare in modo intelligente la dimensione del buffer coordinandola con il numero di elementi che precarichi e con la durata. In caso di contesa, il player darà la priorità alla riproduzione dell'elemento corrente visualizzato sullo schermo. Con un LoadControl condiviso, il gestore del precaricamento continua il precaricamento finché i byte del buffer di destinazione allocati per il precaricamento non raggiungono il limite superiore e non attende 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 in modo efficace 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 verranno utilizzate dal player. In questo modo si evita uno scenario dispendioso in cui viene precaricata una traccia video a 480p, solo per farla scartare immediatamente dal player e recuperare una traccia a 720p durante la riproduzione.< br /> Il gestore del precaricamento NON deve condividere la stessa istanza di TrackSelector con il player. Devono invece utilizzare un'istanza TrackSelector diversa, ma della stessa implementazione. Ecco perché abbiamo impostato TrackSelectorFactory anziché TrackSelector in DefaultPreloadManager.Builder.

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 evitare di sprecare larghezza di banda per contenuti che il player non può riprodurre.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Scopri di più sui componenti di ExoPlayer.

La regola d'oro: un Playback Looper 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 deve essere eseguito l'accesso al giocatore può essere interrogato 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 i bug di concorrenza.

Tutte le interazioni tra PreloadManager e il player con le origini media da caricare o precaricare devono avvenire nello stesso thread di riproduzione. La condivisione di Looper è fondamentale 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 dell'interfaccia utente chiama player.setMediaSource(mediaSource), esegui il trasferimento di questo oggetto complesso e stateful da MediaSource di precaricamento al player. In questo scenario, l'intera PreloadMediaSource viene spostata dal gestore al player. Tutte queste interazioni e trasferimenti devono avvenire sullo stesso PlaybackLooper.

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

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Vediamo come puoi condividere tutti i componenti precedenti 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, come DefaultLoadControl e così via, non devi condividerli esplicitamente con DefaultPreloadManager. Quando crei l'istanza di ExoPlayer tramite buildExoPlayer di DefaultPreloadManager.Builder, questi componenti vengono automaticamente referenziati tra loro, se utilizzi le implementazioni predefinite con le configurazioni predefinite. Tuttavia, se utilizzi componenti personalizzati o configurazioni personalizzate, devi comunicarlo esplicitamente 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 i 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 insieme di elementi piccolo e gestibile, 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 entrano nella visualizzazione e rimuovendo quelli che ora sono lontani.

slidingwindow.png

Implementazione del pattern della finestra scorrevole

È essenziale comprendere che PreloadManager non fornisce un metodo setWindowSize() integrato. La finestra scorrevole è un pattern di progettazione che tu, lo sviluppatore, sei responsabile dell'implementazione utilizzando i metodi primitivi add() e remove(). La logica dell'applicazione deve connettere gli eventi dell'interfaccia utente, ad esempio uno scorrimento o un cambio di pagina, a queste chiamate API. Se vuoi un riferimento al codice, abbiamo implementato questo pattern di finestra scorrevole nell'esempio 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 del precaricamento. La chiamata remove() garantisce il rilascio delle risorse che ti aiutano a mantenere la memoria utilizzata dell'app vincolata e stabile.

Perfezionamento di 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 per 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à maggiore di essere riprodotto rispetto a un elemento in posizione +/- 4. Potresti allocare più risorse (rete, CPU, memoria) agli elementi che l'utente ha maggiori probabilità di visualizzare successivamente. In questo modo viene creata 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 approfondimenti di analisi e implementare una gestione degli errori efficace.
  • Utilizza sempre un singolo DefaultPreloadManager.Builder per creare sia le istanze del gestore sia quelle 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 e a più livelli che bilanci prestazioni e consumo di risorse.

Passaggio successivo della 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 dei compromessi. Una volta chiusa l'applicazione o rimossa la risorsa precaricata dal gestore, i dati vengono eliminati. Per ottenere un livello di ottimizzazione più persistente, possiamo combinare il precaricamento con la memorizzazione nella cache del disco. Questa funzionalità è in fase di sviluppo attivo e sarà disponibile tra qualche mese.

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

Continua a seguirci e velocizza la riproduzione dei tuoi video. 🚀

Scritto da:

Continua a leggere