Personalizzazione

L'interfaccia Player è al centro della libreria ExoPlayer. Un Player espone le funzionalità tradizionali dei lettori multimediali di alto livello, come la possibilità di bufferare i contenuti multimediali, riprodurli, metterli in pausa e cercare. L'implementazione predefinita ExoPlayer è progettata per fare poche ipotesi (e quindi per imporre alcune restrizioni) sul tipo di contenuti multimediali riprodotti, su come e dove vengono archiviati e su come vengono visualizzati. Anziché implementare direttamente il caricamento e il rendering dei contenuti multimediali, le implementazioni di ExoPlayer deleganno questo compito ai componenti che vengono inseriti quando viene creato un player o quando nuove origini multimediali vengono passate al player. I componenti comuni a tutte le implementazioni di ExoPlayer sono:

  • Istanze MediaSource che definiscono i contenuti multimediali da riprodurre, li caricano e da cui possono essere letti. Un'istanza MediaSource viene creata da un MediaItem da un MediaSource.Factory all'interno del player. Possono anche essere trasmessi direttamente al player utilizzando l'API Playlist basata su origini media.
  • Un'istanza MediaSource.Factory che converte un MediaItem in MediaSource. MediaSource.Factory viene inserito al momento della creazione del player.
  • Istanze Renderer che visualizzano i singoli componenti dei contenuti multimediali. Vengono inserito al momento della creazione del player.
  • Un TrackSelector che seleziona i canali forniti dal MediaSource da consumare da ogni Renderer disponibile. Viene inserito un TrackSelector quando viene creato il player.
  • Un LoadControl che controlla quando MediaSource memorizza nella cache più contenuti multimediali e quanto. Un LoadControl viene inserito al momento della creazione del player.
  • Un LivePlaybackSpeedControl che controlla la velocità di riproduzione durante le riproduzioni dal vivo per consentire al player di rimanere vicino a un offset dal vivo configurato. Un LivePlaybackSpeedControl viene inserito al momento della creazione del player.

Il concetto di inserimento di componenti che implementano le funzionalità del player è presente in tutta la raccolta. Le implementazioni predefinite di alcuni componenti delegano il lavoro a ulteriori componenti inseriti. In questo modo, molti subcomponenti possono essere sostituiti singolarmente con implementazioni configurate in modo personalizzato.

Personalizzazione del giocatore

Di seguito sono descritti alcuni esempi comuni di personalizzazione del player tramite l'inserimento di componenti.

Configurazione dello stack di rete

Abbiamo una pagina sulla personalizzazione dello stack di rete utilizzato da ExoPlayer.

Memorizzazione nella cache dei dati caricati dalla rete

Consulta le guide sulla cache temporanea dinamica e sul download dei contenuti multimediali.

Personalizzare le interazioni con il server

Alcune app potrebbero voler intercettare le richieste e le risposte HTTP. Potresti voler inserire intestazioni di richiesta personalizzate, leggere le intestazioni di risposta del server, modificare gli URI delle richieste e così via. Ad esempio, la tua app potrebbe autenticarsi inserendo un token come intestazione quando richiede i segmenti multimediali.

L'esempio seguente mostra come implementare questi comportamenti inserendo un valore DataSource.Factory personalizzato in DefaultMediaSourceFactory:

Kotlin

val dataSourceFactory =
  DataSource.Factory {
    val dataSource = httpDataSourceFactory.createDataSource()
    // Set a custom authentication request header.
    dataSource.setRequestProperty("Header", "Value")
    dataSource
  }
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
    )
    .build()

Java

DataSource.Factory dataSourceFactory =
    () -> {
      HttpDataSource dataSource = httpDataSourceFactory.createDataSource();
      // Set a custom authentication request header.
      dataSource.setRequestProperty("Header", "Value");
      return dataSource;
    };

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory))
        .build();

Nello snippet di codice riportato sopra, il parametro HttpDataSource iniettato include l'intestazione "Header: Value" in ogni richiesta HTTP. Questo comportamento è fisso per ogni interazione con un'origine HTTP.

Per un approccio più granulare, puoi iniettare il comportamento just-in-time utilizzando un ResolvingDataSource. Il seguente snippet di codice mostra come iniettare le intestazioni delle richieste appena prima di interagire con un'origine HTTP:

Kotlin

val dataSourceFactory: DataSource.Factory =
  ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec ->
    // Provide just-in-time request headers.
    dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri))
  }

Java

    DataSource.Factory dataSourceFactory =
        new ResolvingDataSource.Factory(
            httpDataSourceFactory,
            // Provide just-in-time request headers.
            dataSpec -> dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri)));

Puoi anche utilizzare ResolvingDataSource per eseguire modifiche just-in-time dell'URI, come mostrato nello snippet seguente:

Kotlin

val dataSourceFactory: DataSource.Factory =
  ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec ->
    // Provide just-in-time URI resolution logic.
    dataSpec.withUri(resolveUri(dataSpec.uri))
  }

Java

DataSource.Factory dataSourceFactory =
    new ResolvingDataSource.Factory(
        httpDataSourceFactory,
        // Provide just-in-time URI resolution logic.
        dataSpec -> dataSpec.withUri(resolveUri(dataSpec.uri)));

Personalizzare la gestione degli errori

L'implementazione di un LoadErrorHandlingPolicy personalizzato consente alle app di personalizzare il modo in cui ExoPlayer reagisce agli errori di caricamento. Ad esempio, un'app potrebbe voler fallire rapidamente invece di riprovare molte volte o potrebbe voler personalizzare la logica di back-off che controlla il tempo di attesa del player tra ogni nuovo tentativo. Lo snippet seguente mostra come implementare una logica di backoff personalizzata:

Kotlin

val loadErrorHandlingPolicy: LoadErrorHandlingPolicy =
  object : DefaultLoadErrorHandlingPolicy() {
    override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorInfo): Long {
      // Implement custom back-off logic here.
      return 0
    }
  }
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
    )
    .build()

Java

LoadErrorHandlingPolicy loadErrorHandlingPolicy =
    new DefaultLoadErrorHandlingPolicy() {
      @Override
      public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
        // Implement custom back-off logic here.
        return 0;
      }
    };

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context)
                .setLoadErrorHandlingPolicy(loadErrorHandlingPolicy))
        .build();

L'argomento LoadErrorInfo contiene ulteriori informazioni sul caricamento non riuscito per personalizzare la logica in base al tipo di errore o alla richiesta non riuscita.

Personalizzazione dei flag degli estrattori

I flag dell'estrattore possono essere utilizzati per personalizzare il modo in cui i singoli formati vengono estratti dai contenuti multimediali progressivi. Possono essere impostati sul DefaultExtractorsFactory fornito al DefaultMediaSourceFactory. L'esempio seguente passa un flag che attiva la ricerca basata sull'indice per gli stream MP3.

Kotlin

val extractorsFactory =
  DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING)
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(DefaultMediaSourceFactory(context, extractorsFactory))
    .build()

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING);

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(new DefaultMediaSourceFactory(context, extractorsFactory))
        .build();

Attivazione della ricerca di una velocità in bit costante

Per gli stream MP3, ADTS e AMR, puoi attivare la ricerca approssimativa utilizzando un presupposto di velocità in bit costante con flag FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Questi flag possono essere impostati per i singoli estrattori utilizzando i singoli metodiDefaultExtractorsFactory.setXyzExtractorFlags come descritto sopra. Per attivare la ricerca della velocità in bit costante per tutti gli estrattori che la supportano, utilizza DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);

ExtractorsFactory può quindi essere inserito tramite DefaultMediaSourceFactory come описано sopra per la personalizzazione dei flag dell'estrattore.

Attivazione dell'accodamento del buffer asincrono

La coda del buffer asincrono è un miglioramento della pipeline di rendering di ExoPlayer, che gestisce istanze MediaCodec in modalità asincrona e utilizza thread aggiuntivi per pianificare la decodifica e il rendering dei dati. La sua attivazione può ridurre i frame persi e gli underrun audio.

La coda del buffer asincrona è attiva per impostazione predefinita sui dispositivi con Android 12 (livello API 31) e versioni successive e può essere attivata manualmente a partire da Android 6.0 (livello API 23). Valuta la possibilità di attivare la funzionalità per dispositivi specifici su cui riscontri frame persi o sottocorrenti audio, in particolare durante la riproduzione di contenuti protetti da DRM o ad alta frequenza fotogrammi.

Nel caso più semplice, devi inserire un DefaultRenderersFactory nel player nel seguente modo:

Kotlin

val renderersFactory = 
  DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing()
val exoPlayer = ExoPlayer.Builder(context, renderersFactory).build()

Java

DefaultRenderersFactory renderersFactory =
    new DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing();
ExoPlayer exoPlayer = new ExoPlayer.Builder(context, renderersFactory).build();

Se stai creando direttamente i renderer, passa un AsynchronousMediaCodecAdapter.Factory ai costruttori MediaCodecVideoRenderer e MediaCodecAudioRenderer.

Intercettare le chiamate ai metodi con ForwardingPlayer

Puoi personalizzare parte del comportamento di un'istanza Player racchiudendola in una sottoclasse di ForwardingPlayer e sostituendo i metodi per eseguire una delle seguenti operazioni:

  • Accedi ai parametri prima di trasmetterli al delegato Player.
  • Accedi al valore restituito dal delegato Player prima di restituirlo.
  • Implementa di nuovo completamente il metodo.

Quando sostituisci i metodi ForwardingPlayer, è importante assicurarti che l'implementazione rimanga coerente e conforme all'interfaccia Player, soprattutto quando hai a che fare con metodi che devono avere un comportamento identico o correlato. Ad esempio:

  • Se vuoi sostituire ogni operazione di "riproduzione", devi sostituire sia ForwardingPlayer.play sia ForwardingPlayer.setPlayWhenReady, perché un chiamante si aspetta che il comportamento di questi metodi sia identico quando playWhenReady = true.
  • Se vuoi modificare l'incremento di ricerca in avanti, devi sostituire sia ForwardingPlayer.seekForward per eseguire una ricerca con l'incremento personalizzato sia ForwardingPlayer.getSeekForwardIncrement per segnalare nuovamente all'autore della chiamata l'incremento personalizzato corretto.
  • Se vuoi controllare quali Player.Commands vengono pubblicizzati da un'istanza del player, devi eseguire l'override sia di Player.getAvailableCommands() sia di Player.isCommandAvailable(), nonché ascoltare il callback Player.Listener.onAvailableCommandsChanged() per ricevere una notifica delle modifiche provenienti dal player sottostante.

Personalizzazione di MediaSource

Negli esempi precedenti vengono inseriti componenti personalizzati da utilizzare durante la riproduzione di tutti gli oggetti MediaItem trasmessi al player. Quando è necessaria una personalizzazione dettagliata, puoi anche inserire componenti personalizzati in singole istanze MediaSource, che possono essere trasmesse direttamente al player. L'esempio riportato di seguito mostra come personalizzare un ProgressiveMediaSource in modo da utilizzare DataSource.Factory, ExtractorsFactory e LoadErrorHandlingPolicy personalizzati:

Kotlin

val mediaSource =
  ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory)
    .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy)
    .createMediaSource(MediaItem.fromUri(streamUri))

Java

ProgressiveMediaSource mediaSource =
    new ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory)
        .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy)
        .createMediaSource(MediaItem.fromUri(streamUri));

Creazione di componenti personalizzati

La libreria fornisce implementazioni predefinite dei componenti elencati nella parte superiore di questa pagina per casi d'uso comuni. Un ExoPlayer può utilizzare questi componenti, ma può anche essere creato per utilizzare implementazioni personalizzate se sono richiesti comportamenti non standard. Ecco alcuni casi d'uso per le implementazioni personalizzate:

  • Renderer: ti consigliamo di implementare un Renderer personalizzato per gestire un tipo di media non supportato dalle implementazioni predefinite fornite dalla raccolta.
  • TrackSelector: l'implementazione di un valore TrackSelector personalizzato consente a uno sviluppatore di app di modificare il modo in cui le tracce esposte da un MediaSource vengono selezionate per l'utilizzo da parte di ciascuno dei Renderer disponibili.
  • LoadControl: l'implementazione di un LoadControl personalizzato consente a un sviluppatore di app di modificare il criterio di buffering del player.
  • Extractor - Se devi supportare un formato container non attualmente supportato dalla libreria, valuta la possibilità di implementare una classe Extractor personalizzata.
  • MediaSource – L'implementazione di una classe MediaSource personalizzata può essere appropriata se desideri ottenere campioni multimediali da inviare ai renderer in modo personalizzato o per implementare un comportamento di composizione personalizzato di MediaSource.
  • MediaSource.Factory: l'implementazione di un MediaSource.Factory personalizzato consente a un'applicazione di personalizzare il modo in cui viene creato un MediaSource da un MediaItem.
  • DataSource - Il pacchetto upstream di ExoPlayer contiene già una serie di implementazioni DataSource per diversi casi d'uso. Ti consigliamo di implementare la tua classe DataSource per caricare i dati in un altro modo, ad esempio tramite un protocollo personalizzato, utilizzando uno stack HTTP personalizzato o da una cache persistente personalizzata.

Quando crei componenti personalizzati, ti consigliamo quanto segue:

  • Se un componente personalizzato deve segnalare eventi all'app, ti consigliamo di farlo utilizzando lo stesso modello dei componenti ExoPlayer esistenti, ad esempio utilizzando le classi EventDispatcher o passando un Handler insieme a un ascoltatore al costruttore del componente.
  • Consigliamo per i componenti personalizzati di utilizzare lo stesso modello dei componenti ExoPlayer esistenti per consentire la riconfigurazione da parte dell'app durante la riproduzione. Per farlo, i componenti personalizzati devono implementare PlayerMessage.Target e ricevere le modifiche alla configurazione nel metodo handleMessage. Il codice dell'applicazione deve passare le modifiche alla configurazione chiamando il metodo createMessage di ExoPlayer, configurando il messaggio e inviandolo al componente utilizzando PlayerMessage.send. L'invio di messaggi da consegnare nel thread di riproduzione garantisce che vengano eseguiti in ordine con le altre operazioni eseguite sul player.