Al centro della libreria ExoPlayer si trova l'interfaccia Player. Un Player
espone le tradizionali funzionalità di alto livello del media player, come la possibilità di
eseguire il buffering dei contenuti multimediali, riprodurli, metterli in pausa e cercarli. L'implementazione predefinita ExoPlayer è progettata per fare poche ipotesi sul tipo di contenuti multimediali riprodotti, su come e dove vengono archiviati e su come vengono visualizzati (e quindi imporre poche limitazioni). Anziché implementare direttamente il caricamento e il rendering dei contenuti multimediali, le implementazioni di ExoPlayer delegano questo lavoro ai componenti che vengono inseriti quando viene creato un player o quando vengono passate nuove origini multimediali al player.
I componenti comuni a tutte le implementazioni di ExoPlayer sono:
- Istanze
MediaSourceche definiscono i contenuti multimediali da riprodurre, li caricano e da cui è possibile leggerli. Un'istanzaMediaSourceviene creata da unMediaItemda unMediaSource.Factoryall'interno del player. Possono anche essere passati direttamente al player utilizzando l'API per le playlist basate sull'origine multimediale. - Un'istanza
MediaSource.Factoryche converte unMediaItemin unMediaSource. IlMediaSource.Factoryviene inserito quando viene creato il player. Rendererche eseguono il rendering dei singoli componenti dei contenuti multimediali. Questi vengono inseriti al momento della creazione del player.- Un
TrackSelectorche seleziona le tracce fornite daMediaSourceper essere utilizzate da ogniRendererdisponibile. UnTrackSelectorviene inserito quando viene creato il player. - Un
LoadControlche controlla quandoMediaSourcememorizza nel buffer altri contenuti multimediali e quanti contenuti multimediali vengono memorizzati nel buffer. UnLoadControlviene inserito quando viene creato il player. - Un
LivePlaybackSpeedControlche controlla la velocità di riproduzione durante le riproduzioni live per consentire al player di rimanere vicino a un offset live configurato. UnLivePlaybackSpeedControlviene inserito quando viene creato il player.
Il concetto di inserimento di componenti che implementano parti della funzionalità del player è presente in tutta la libreria. Le implementazioni predefinite di alcuni componenti delegano il lavoro ad altri componenti inseriti. In questo modo, molti componenti secondari possono essere sostituiti singolarmente con implementazioni configurate in modo personalizzato.
Personalizzazione del player
Alcuni esempi comuni di personalizzazione del player tramite l'inserimento di componenti sono descritti nelle sezioni seguenti.
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 alla memorizzazione nella cache on-the-fly temporanea e al download di contenuti multimediali.
Personalizzazione delle interazioni con il server
Alcune app potrebbero voler intercettare le richieste e le risposte HTTP. Potresti voler inserire intestazioni della richiesta personalizzate, leggere le intestazioni della 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 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 precedente, l'oggetto HttpDataSource inserito 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 inserire un comportamento just-in-time utilizzando un
ResolvingDataSource. Il seguente snippet di codice mostra come inserire le intestazioni della richiesta 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 un ResolvingDataSource per eseguire modifiche just-in-time dell'URI, come mostrato nel seguente snippet:
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)));
Personalizzazione della 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 anziché riprovare più volte o potrebbe voler personalizzare la logica di backoff che controlla il tempo di attesa del player tra un tentativo e l'altro. Il seguente snippet
mostra come implementare una logica di backoff personalizzata:
Kotlin
val loadErrorHandlingPolicy: LoadErrorHandlingPolicy = object : DefaultLoadErrorHandlingPolicy() { override fun getRetryDelayMsFor( loadErrorInfo: LoadErrorHandlingPolicy.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(LoadErrorHandlingPolicy.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 dell'estrattore
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 su DefaultExtractorsFactory fornito a DefaultMediaSourceFactory. L'esempio seguente passa un flag che attiva la ricerca basata su 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 con velocità in bit costante
Per gli stream MP3, ADTS e AMR, puoi attivare la ricerca approssimativa utilizzando un'ipotesi di velocità in bit costante con i flag FLAG_ENABLE_CONSTANT_BITRATE_SEEKING.
Questi flag possono essere impostati per i singoli estrattori utilizzando i singoli metodi DefaultExtractorsFactory.setXyzExtractorFlags, come descritto in precedenza. Per
attivare la ricerca con 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 utilizzando DefaultMediaSourceFactory, come descritto in precedenza per la personalizzazione dei flag dell'estrattore.
Attivazione della coda di buffer asincrona
La coda di buffer asincrona è un miglioramento della pipeline di rendering di ExoPlayer, che gestisce le istanze MediaCodec in modalità asincrona e utilizza thread aggiuntivi per pianificare la decodifica e il rendering dei dati. L'attivazione di questa opzione può ridurre i frame persi e gli underrun audio.
La coda di buffer asincrona è attiva per impostazione predefinita sui dispositivi con Android 12 (livello API 31) o 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 noti frame persi o underrun audio, in particolare durante la riproduzione di contenuti protetti da DRM o con frequenza fotogrammi elevata.
Nel caso più semplice, devi inserire un DefaultRenderersFactory nel player come segue:
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 crei direttamente le istanze dei renderer, passa new
DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() ai costruttori MediaCodecVideoRenderer e MediaCodecAudioRenderer.
Personalizzazione delle operazioni con ForwardingSimpleBasePlayer
Puoi personalizzare alcuni comportamenti di un'istanza Player inserendola in una sottoclasse di ForwardingSimpleBasePlayer. Questa classe ti consente di intercettare "operazioni" specifiche, anziché dover implementare direttamente i metodi Player. In questo modo, viene garantito un comportamento coerente, ad esempio, di play(), pause()
e setPlayWhenReady(boolean). Inoltre, garantisce che tutte le modifiche di stato vengano propagate correttamente alle istanze Player.Listener registrate. Per la maggior parte dei casi d'uso di personalizzazione, ForwardingSimpleBasePlayer dovrebbe essere preferito al più soggetto a errori ForwardingPlayer a causa di queste garanzie di coerenza.
Ad esempio, per aggiungere una logica personalizzata quando la riproduzione viene avviata o interrotta:
Kotlin
class PlayerWithCustomPlay(player: Player) : ForwardingSimpleBasePlayer(player) { override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> { // Add custom logic return super.handleSetPlayWhenReady(playWhenReady) } }
Java
public static final class PlayerWithCustomPlay extends ForwardingSimpleBasePlayer { public PlayerWithCustomPlay(Player player) { super(player); } @Override protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) { // Add custom logic return super.handleSetPlayWhenReady(playWhenReady); } }
In alternativa, per non consentire il comando SEEK_TO_NEXT (e assicurarti che Player.seekToNext non esegua alcuna operazione):
Kotlin
class PlayerWithoutSeekToNext(player: Player) : ForwardingSimpleBasePlayer(player) { override fun getState(): State { val state = super.getState() return state .buildUpon() .setAvailableCommands( state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build() ) .build() } // We don't need to override handleSeek, because it is guaranteed not to be called for // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable. }
Java
public static final class PlayerWithoutSeekToNext extends ForwardingSimpleBasePlayer { public PlayerWithoutSeekToNext(Player player) { super(player); } @Override protected State getState() { State state = super.getState(); return state .buildUpon() .setAvailableCommands( state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build()) .build(); } // We don't need to override handleSeek, because it is guaranteed not to be called for // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable. }
Personalizzazione di MediaSource
Gli esempi precedenti inseriscono componenti personalizzati da utilizzare durante la riproduzione di tutti gli oggetti MediaItem passati al player. Se è necessaria una personalizzazione granulare, è anche possibile inserire componenti personalizzati in singole istanze MediaSource, che possono essere passate direttamente al player. L'esempio seguente mostra come personalizzare un ProgressiveMediaSource
per 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 i 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: potresti voler implementare unRendererpersonalizzato per gestire un tipo di media non supportato dalle implementazioni predefinite fornite dalla libreria.TrackSelector: l'implementazione di unTrackSelectorpersonalizzato consente a uno sviluppatore di app di modificare il modo in cui le tracce esposte da unMediaSourcevengono selezionate per l'utilizzo da parte di ciascuno deiRendererdisponibili.LoadControl: l'implementazione di unLoadControlpersonalizzato consente a uno sviluppatore di app di modificare la policy di buffering del player.Extractor: se devi supportare un formato contenitore non attualmente supportato dalla libreria, valuta la possibilità di implementare una classeExtractorpersonalizzata.MediaSource: l'implementazione di una classeMediaSourcepersonalizzata potrebbe essere appropriata se vuoi ottenere campioni multimediali da fornire ai renderer in modo personalizzato o se vuoi implementare un comportamento di composizioneMediaSourcepersonalizzato.MediaSource.Factory: l'implementazione di unMediaSource.Factorypersonalizzato consente a un'applicazione di personalizzare il modo in cui viene creato unMediaSourceda unMediaItem.DataSource: il pacchetto upstream di ExoPlayer contiene già una serie di implementazioni diDataSourceper diversi casi d'uso. Potresti voler implementare la tua classeDataSourceper caricare i dati in un altro modo, ad esempio tramite un protocollo personalizzato, utilizzando uno stack HTTP personalizzato o da una cache permanente personalizzata.
Quando crei componenti personalizzati, ti consigliamo di:
- 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
EventDispatchero passando unHandlerinsieme a un listener al costruttore del componente. - Consigliamo che i componenti personalizzati utilizzino 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.Targete ricevere le modifiche alla configurazione nel metodohandleMessage. Il codice dell'applicazione deve passare le modifiche alla configurazione chiamando il metodocreateMessagedi ExoPlayer, configurando il messaggio e inviandolo al componente utilizzandoPlayerMessage.send. L'invio di messaggi da recapitare sul thread di riproduzione garantisce che vengano eseguiti in ordine con qualsiasi altra operazione eseguita sul player.