ExoPlayer详解(官方文档-入门)

目录

 

ExoPlayer详解系列文章-入门

一、前言

二、优缺点比较

三、概述

ExoPlayer详解——入门(官方文档)

   添加ExoPlayer作为依赖项

     1、添加依赖

2、添加ExoPlayer模块

3.添加Java 8支持

4.使multidex

创建播放器

关于线程的注释

将播放器附加到视图

填充播放列表并准备播放器

控制播放器

释放播放器

Player events

聆听播放事件

播放状态变更

播放错误

播放列表转换onMediaItemTransition()

Seeking

个别回调与onEvents

其他SimpleExoPlayer侦听器

使用EventLogger

在指定的播放位置触发事件

播放清单

修改播放列表

查询播放列表

识别播放列表项

将应用程序数据与播放列表项相关联

检测播放何时过渡到另一个媒体项目

检测播放列表何时更改

设置自定义洗牌顺序

媒体项目

简单媒体项目

处理非标准文件扩展名

Protected content 受保护的内容

Sideloading subtitle tracks(侧载字幕轨道)

剪辑媒体流

广告插入

媒体资源

自定义媒体源创建

基于媒体源的播放列表API

Track selection 曲目选择

UI组件

Player views

选择表面类型

Player control views 播放器控制视图

Customization 定制

覆盖绘图

覆盖布局文件

自定义布局文件

下载媒体

创建一个DownloadService

创建一个DownloadManager

添加下载

删除下载

开始和停止下载

查询下载

收听下载

播放下载的内容

MediaSource配置

下载和播放自适应流

广告插入

客户端广告插入

声明式广告支持

带有广告的播放列表

IMA扩展

使用第三方广告SDK

服务器端广告插入

即时串流

检测和监控实时播放

在直播中寻找

实时播放界面

配置实时播放参数

BehindLiveWindowException

自定义播放速度调整算法

调试日志

Player information

播放状态

媒体轨道

解码器选择

分析工具

使用AnalyticsListener收集事件

使用PlaybackStatsListener处理事件

处理和解释的事件

单次播放分析数据

汇总多个回放的分析数据

计算的摘要指标

进阶主题

将分析数据与回放元数据相关联

报告自定义分析事件


ExoPlayer详解系列文章-入门

一、前言

ExoPlayer是google开源的应用级媒体播放器项目,目前已有1W+的start,并一直在维护。该开源项目包含ExoPlayer库和演示
demo,github地址:https://github.com/google/ExoPlayer。和官方文档 https://exoplayer.dev/hello-world.html

二、优缺点比较

与Android内置的MediaPlayer相比,ExoPlayer具有许多优点:
*支持通过HTTP(DASH)和SmoothStreaming进行动态自适应流,这两种都不受MediaPlayer的支持。还支持许多其他格式
*能够自定义和扩展播放器,以适应各种不同需求。 ExoPlayer专门设计了这一点,大部分组件都可以自己替换
*官网说了很多,其实说到底最主要的就是各个组件可以自定义,还可以接入ffmpeg组件,基本能满足99.9%的需求
与IJKPlayer和Vitamio相比,ExoPlayer具有的优点:
*导入项目之后APK体积增加小
缺点:
*最低支持版本4.4
*实现比较复杂

三、概述

ExoPlayer库的核心是Exoplayer接口,Exoplayer公开了传统的高级媒体播放器功能,例如缓冲媒体、播放、
暂停和seek等功能,ExoPlayer通过组件实现替他高级功能。ExoPlayer公同的组件有:
*MediaSource:定义多媒体数据源,从Uri中读取数据,传入ExoPlayer。
*TrackSelector:轨道提取器,从MediaSource中提取各个轨道的二进制数据,交给Render渲染。
*LoadControl:可以控制MediaSource,比如什么时候开始缓冲,缓冲多少之后暂停缓冲

 

ExoPlayer详解——入门(官方文档)

   添加ExoPlayer作为依赖项


     1、添加依赖

        确保build.gradle项目根目录中的文件中包含Google和JCenter存储库。

    repositories {
        google()
        jcenter()
    }

2、添加ExoPlayer模块

     接下来在app目录下的build.gradle的文件中添加依赖项。以下内容将为完整的ExoPlayer库添加依赖项:

implementation 'com.google.android.exoplayer:exoplayer:2.X.X'

          2.X.X您的首选版本在哪里(可以通过查阅发行说明找到最新版本)。

    你还可以用以下方法代替 ,您可以依赖于实际需要的库模块。例如,以下内容将添加对Core,DASH和UI库模块的依赖关系,这对于播放DASH内容的应用程序可能是必需的:

implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'

以下是关于所有的模块引用作用。完整的ExoPlayer库添加依赖项等同于单独添加了所有库模块的依赖项。

  • exoplayer-core: 核心功能(必需)。
  • exoplayer-dash: 支持DASH内容。
  • exoplayer-hls: 支持HLS内容。
  • exoplayer-smoothstreaming: 支持SmoothStreaming内容。
  • exoplayer-transformer: 媒体转换功能。
  • exoplayer-ui: 用于ExoPlayer的UI组件和资源。

除了库模块,ExoPlayer还有多个扩展模块,它们依赖于外部库来提供附加功能。浏览扩展目录(官方的)及其各自的README以获取详细信息。

3.添加Java 8支持

如果尚未启用,则需要在所有 build.gradle文件中打开Java 8支持,具体取决于ExoPlayer,方法是在以下android部分添加以下内容:

compileOptions {
  targetCompatibility JavaVersion.VERSION_1_8
}

4.使multidex

如果你的Gradle minSdkVersion是20或更低,你应该启用multidex以防止构建错误。

 

创建播放器


您可以ExoPlayer使用SimpleExoPlayer.Builder或 创建实例ExoPlayer.Builder。这些构建器提供了一系列用于创建ExoPlayer实例的定制选项。对于绝大多数用例, SimpleExoPlayer.Builder都应使用。此构建器返回 SimpleExoPlayer,它扩展ExoPlayer为添加其他高级播放器功能。以下代码是创建的示例SimpleExoPlayer

//2.12版本以后推荐
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();2.

 也可以这样创建

// 创建带宽
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
// 创建轨道选择工厂 视频每一这的画面如何渲染,实现默认的实现类
TrackSelection.Factory videoTrackSelectionFactory = new DefaultRenderersFactory(application)
// 创建轨道选择实例 视频的音视频轨道如何加载,使用默认的轨道选择器
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
// 创建播放器实例
SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(this, trackSelector);

关于线程的注释

必须从单个应用程序线程访问ExoPlayer实例。在绝大多数情况下,这应该是应用程序的主线程。使用ExoPlayer的UI组件或IMA扩展时,必须使用应用程序的主线程。

创建播放器时,可以通过传递Looper来显式指定必须在其上访问ExoPlayer实例的线程。如果未指定Looper ,则使用在其上创建播放器的线程的Looper,或者如果该线程不具有Looper,则使用应用程序的主线程的Looper。在所有情况下,播放器线程的Looper可以使用Player.getApplicationLooper来查询

如果看到报错信息“Player is accessed on the wrong thread(“在错误的线程上访问了播放器”)”并 抛出异常IllegalStateException,说明您的应用程序中的某些代码正在错误的线程上访问了 SimpleExoPlayer 实例(异常的堆栈跟踪显示了您的位置)。您可以通过调用SimpleExoPlayer.setThrowsWhenUsingWrongThread(false)暂时不抛出这些抛出的异常,在这种情况下,该问题将被记录为警告。使用此退出选项是不安全的,并且可能导致意外或模糊的错误。它将在ExoPlayer 2.14中删除。

有关ExoPlayer的踩踏模型的更多信息,请参见ExoPlayer Javadoc的 “线程模型”部分

将播放器附加到视图


ExoPlayer库为媒体播放提供了一系列预构建的UI组件。其中包括StyledPlayerView,它封装了 StyledPlayerControlView,,SubtitleView和渲染视频的Surface。一个StyledPlayerView可以包含在应用程序的布局XML。将播放器绑定到视图很简单:

// Bind the player to the view.
playerView.setPlayer(player);

也可以使用StyledPlayerControlView作为一个独立的组件,这对于仅使用音频的用例很有用。

使用ExoPlayer的预先构建的UI组件是可选的对于实现自己的UI视频的应用,可以分别使用SimpleExoPlayer的 setVideoSurfaceViewsetVideoTextureViewsetVideoSurfaceHolder和 setVideoSurface的方法设置自己的SurfaceViewTextureView, SurfaceHolder或者Surface

SimpleExoPlayeraddTextOutput 方法可用于接收在播放过程中应呈现的字幕。

填充播放列表并准备播放器


在ExoPlayer中,每种媒体都由表示MediaItem。要播放媒体,您需要构建相应的媒体MediaItem,将其添加到播放器中,准备播放器,然后调用play以开始播放:

//  创建mediaItem 
MediaItem mediaItem = MediaItem.fromUri(videoUri);
// 设置mediaItem 
player.setMediaItem(mediaItem);
//  准备播放
player.prepare();
// 开始播放
player.play();

ExoPlayer直接支持播放列表,因此可以为播放器准备多个要依次播放的媒体项目:

// 创建mediaItem
MediaItem firstItem = MediaItem.fromUri(firstVideoUri);
MediaItem secondItem = MediaItem.fromUri(secondVideoUri);
// 添加要播放的媒体项目。
player.addMediaItem(firstItem);
player.addMediaItem(secondItem);
//  准备播放
player.prepare();
// 开始播放
player.play();

播放列表可以在播放期间进行更新,而无需再次准备播放器。在“Media items page上了解有关填充和操作播放列表的更多信息 。在“Media items page上阅读有关构建媒体项目时可用的不同选项的更多信息,例如剪辑和附加字幕文件 。

在ExoPlayer 2.12之前,player需要的是一个MediaSource而不是MediaItem。从2.12开始,player内部会将MediaItem转换为需要的 MediaSource实例。在“Media items page"上可以了解有关此过程以及如何对其进行自定义的更多信息。仍然可以使用ExoPlayer.setMediaSource(s)和 ExoPlayer.addMediaSource(s)MediaSource实例直接提供给播放器。

 2.12之前是这样创建的

// 创建加载数据的工厂 (生成加载媒体数据的数据源实例)
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
Util.getUserAgent(context, "yourApplicationName"));

Uri uri = Uri.parse(url);
// 创建资源
ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
//或者
//MediaSource mediaSource= new ProgressiveMediaSource.Factory(dataSourceFactory)
//.createMediaSource(uri);
// 准备播放
player.prepare(mediaSource);
// 开始播放
player.setPlayWhenReady(true);

控制播放器


准备好播放器后,可以通过调用播放器上的方法来控制播放。下面列出了一些最常用的方法。

  • playpause 开始和暂停播放。
  • seekTo 允许在media内搜寻。
  • hasPrevioushasNextpreviousnext允许通过播放列表进行浏览。
  • setRepeatMode 控制media 是否循环以及如何循环。
  • setShuffleModeEnabled 控制播放列表移动。
  • setPlaybackParameters 调整播放速度和音频音高。

如果player绑定到PlayerViewPlayerControlView,则用户与这些组件的交互将导致调用player上的相应方法。

释放播放器


重要的是,在不再需要播放器时将其释放,以释放有限的资源(例如视频解码器)供其他应用程序使用。这可以通过调用来完成ExoPlayer.release

 @Override
    protected void onDestroy() {
        super.onDestroy();
        if (player != null) {
            player.release();
        }
    }

 

Player events


聆听播放事件


Events 如状态更改和播放错误等事件将报告给已注册 Player.EventListener实例。注册一个监听器来接收这样的事件

// 添加一个监听器来接收来自播放器的事件.
player.addListener(eventListener);

Player.EventListener有空的默认方法,因此您只需要实现您所需要的方法即可。有关方法及其调用时间的完整说明,请参见Javadoc。一些最重要的方法将在下面更详细地描述。

Listeners 可以选择实现单个事件回调或者实现在一个或多个事件一起发生后调用的含泛型onEvents回调。有关详细Individual callbacks vs onEvents说明,请参见参考资料。

播放状态变更

Player状态的变化可以通过实现Player.EventListener 中的onPlaybackStateChanged(@State int state)方法中来接收 。Player可能会处于以下四种播放状态之一:

  • Player.STATE_IDLE:这是初始状态,即播放器停止和播放失败时的状态。
  • Player.STATE_BUFFERING:播放器无法立即从当前位置播放。这主要是因为需要加载更多数据。
  • Player.STATE_READY:播放器可以立即从其当前位置播放。
  • Player.STATE_ENDED:播放器完成了所有媒体的播放。

除了这些状态之外,Player 还具有playWhenReady的标记来表明播放的意图。可以通过实现onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason)接收此标志的更改 。

Player正在播放(即,其位置被推进和媒体正在呈现给用户)时,它的Player.STATE_READY状态和 playWhenReadytrue,而且返回结果并不会因为Player.getPlaybackSuppressionReason的原因而被抑制Player.isPlaying也可以调用而不是必须单独检查这些属性。可以通过执行onIsPlayingChanged(boolean isPlaying)以下操作来接收对此状态的更改:

@Override
public void onIsPlayingChanged(boolean isPlaying) {
  if (isPlaying) {
    // Active playback.
  } else {
    // Not playing because playback is paused, ended, suppressed, or the player
    // is buffering, stopped or failed. Check player.getPlayWhenReady,
    // player.getPlaybackState, player.getPlaybackSuppressionReason and
    // player.getPlaybackError for details.
  }
}

播放错误

通过在  Player.EventListener中的onPlayerError(ExoPlaybackException error)方法中 可以接收到导致播放失败的错误信息。发生错误时,将在播放状态转换为Player.STATE_IDLE之前立即调用此方法。可以通过调用ExoPlayer.retry重试或停止的播放。

ExoPlaybackException有一个type字段,以及相应的getter方法,这些方法返回异常原因,以提供有关故障的更多信息。下例显示了如何检测由于HTTP网络问题而导致播放失败的时间。

@Override
public void onPlayerError(ExoPlaybackException error) {
  if (error.type == ExoPlaybackException.TYPE_SOURCE) {
    IOException cause = error.getSourceException();
    if (cause instanceof HttpDataSourceException) {
      // HTTP错误。
      HttpDataSourceException httpError = (HttpDataSourceException) cause;
      // 这是发生错误的请求。
      DataSpec requestDataSpec = httpError.dataSpec;
      // 通过强制类型转换错误查询原因,可以找到更多关于错误的信息。
      if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
        //转换为InvalidResponseCodeException并检索响应代码、消息和报头。
      } else {
        //尝试调用httpError.getCause()来检索底层的原因,尽管注意它可能是空的。
      }
    }
  }
}

播放列表转换onMediaItemTransition()

每当播放器更改播放列表中的新媒体项目时, onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason)就会在注册Player.EventListeners上调用到 。表明原因这是,自动过渡,seek(例如在调用之后player.next()),重复相同项目还是由于播放列表更改(例如,如果当前正在播放的项目被删除)引起的。

Seeking

调用Player.seekTo方法会导致对已注册Player.EventListener实例的一系列回调 :

  1. onPositionDiscontinuityreason=DISCONTINUITY_REASON_SEEK。这是调用Player.seekTo的直接结果。
  2. onPlaybackStateChanged 搜寻相关的任何即时状态更改。请注意,可能没有这样的更改。

如果您使用AnalyticsListener,则会在onSeekStarted之前有一个附加事件 onPositionDiscontinuity,以指示在开始搜索之前的播放位置。

个别回调与onEvents

侦听器可以在实现单独的回调(如onIsPlayingChanged(boolean isPlaying))和通用 onEvents(Player player, Events events)回调之间进行选择 。通用回调提供对Player对象的访问,并指定events一起发生的对象集。它总是在对应于各个事件的回调之后调用。

@Override
public void onEvents(Player player, Events events) {
  if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
      || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
    uiModule.updateUi(player);
  }
}

在以下情况下,应优先考虑单个事件:

  • 侦听器对更改的原因感兴趣。例如,为onPlayWhenReadyChanged或提供的原因onMediaItemTransition
  • 侦听器仅对通过回调参数提供的新值起作用,或触发不依赖于回调参数的其他事件。
  • 侦听器实现更喜欢在方法名称中以清晰可读的方式指示触发事件的原因。
  • 侦听器向需要了解所有单个事件和状态更改的分析系统报告。

onEvents(Player player, Events events)在以下情况下,应首选通用名称:

  • 侦听器希望针对多个事件触发相同的逻辑。例如更新用于两者的UIonPlaybackStateChanged和 onPlayWhenReadyChanged
  • 侦听器需要访问该Player对象以触发其他事件,例如在媒体项转换后进行搜索。
  • 侦听器打算使用通过单独的回调一起报告的多个状态值,或与Playergetter方法组合使用的状态值。例如,仅在回调内部使用Player.getCurrentWindowIndex()with Timeline提供的inonTimelineChanged是安全的 onEvents
  • 侦听器对事件是否在逻辑上一起发生感兴趣。例如onPlaybackStateChanged,以STATE_BUFFERING因媒体项目过渡。

在某些情况下,侦听器可能需要将各个回调与通用onEvents回调结合起来,例如,使用来记录媒体项更改的原因onMediaItemTransition,但只有在所有状态更改可以一起使用时才采取行动onEvents

 

其他SimpleExoPlayer侦听器


使用时SimpleExoPlayer,可以在播放器中注册其他监听器。

  • addAnalyticsListener:收听详细的事件,这些事件可能对分析和日志记录有用。请参阅分析页面以获取更多详细信息。
  • addTextOutput:收听字幕或字幕提示中的更改。
  • addMetadataOutput:收听定时的元数据事件,例如定时的ID3和EMSG数据。
  • addVideoListener:收听与视频渲染有关的事件,这些事件可能对调整UI有用(例如,Surface正在渲染视频的长宽比)。
  • addAudioListener:收听与音频有关的事件,例如音频会话ID更改以及播放器音量更改时。
  • addDeviceListener:收听与设备状态有关的事件。

ExoPlayer的UI组件(例如StyledPlayerView)将自己注册为相应的事件的侦听器。因此,使用上述方法进行手动注册仅对实现自己的播放器UI或需要出于其他目的监听事件的应用程序有用。

使用EventLogger

EventLoggerAnalyticsListener库直接提供的用于日志记录的目的。可以将其添加到中SimpleExoPlayer,以单行启用有用的附加日志记录。

player.addAnalyticsListener(new EventLogger(trackSelector));

传递trackSelector启用将启用其他日志记录,但是它是可选的,因此l可以传 nul。有关更多详细信息,请参见调试日志记录页面

 

在指定的播放位置触发事件


一些用例需要在指定的播放位置触发事件。支持使用PlayerMessage。一个PlayerMessage可以使用创建 ExoPlayer.createMessage。可以使用来设置应执行播放的位置PlayerMessage.setPosition。默认情况下,消息是在播放线程上执行的,但这可以使用进行自定义 PlayerMessage.setLooperPlayerMessage.setDeleteAfterDelivery可用于控制是在每次遇到指定的播放位置时(是由于搜寻和重复模式而发生多次)还是仅在第一次时执行消息。一旦PlayerMessage配置了,就可以使用进行安排PlayerMessage.send

player
    .createMessage(
        (messageType, payload) -> {
          // Do something at the specified playback position.
        })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* windowIndex= */ 0, /* positionMs= */ 120_000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();

     

播放清单


 

播放列表API由Player接口定义,该接口由所有ExoPlayer实现方式实现。它允许顺序播放多个媒体项目。以下示例显示了如何开始播放包含两个视频的播放列表:

// Build the media items.
MediaItem firstItem = MediaItem.fromUri(firstVideoUri);
MediaItem secondItem = MediaItem.fromUri(secondVideoUri);
// Add the media items to be played.
player.addMediaItem(firstItem);
player.addMediaItem(secondItem);
// Prepare the player.
player.prepare();
// Start the playback.
player.play();

播放列表中项目之间的转换是无缝的。并不需要它们具有相同的格式(例如,播放列表中同时包含H264和VP9视频都是可以的)。它们甚至可能具有不同的类型(例如,播放列表同时包含仅视频和音频流就可以了)。允许MediaItem在播放列表中多次使用相同的内容。

修改播放列表


可以通过添加,移动和删除媒体项来动态修改播放列表。可以在播放之前和播放过程中通过调用相应的播放列表API方法来完成此操作:

// 在playlist的position 添加一个MediaItem .
player.addMediaItem(/* index= */ 1, MediaItem.fromUri(thirdUri));
// Moves the third media item from position 2 to the start of the playlist.
//将第三个MediaItem从位置2移动到playlist的开始。
player.moveMediaItem(/* currentIndex= */ 2, /* newIndex= */ 0);
// 从playlist中移除第一项。
player.removeMediaItem(/* index= */ 0);

还支持替换和清除整个播放列表:

// Replaces the playlist with a new one.
List<MediaItem> newItems = ImmutableList.of(
    MediaItem.fromUri(fourthUri),
    MediaItem.fromUri(fifthUri));
player.setMediaItems(newItems, /* resetPosition= */ true);
// Clears the playlist. If prepared, the player transitions to the ended state.
player.clearMediaItems();

播放器会在播放过程中以正确的方式自动处理修改。例如,如果当前播放的媒体项目已移动,则播放不会中断,并且新的后继对象将在完成后播放。如果MediaItem删除了当前正在播放的播放器,则播放器将自动移动到播放剩余的第一个后继播放器;如果不存在该后继播放器,则播放器将过渡到结束状态。

查询播放列表


可以使用Player.getMediaItemCount和 来查询播放列表Player.getMediaItemAt。可以通过致电查询当前播放的媒体项目Player.getCurrentMediaItem

识别播放列表项


要标识播放列表项目,MediaItem.mediaId可以在构建项目时进行设置:

// Build a media item with a media ID.
MediaItem mediaItem =
    new MediaItem.Builder().setUri(uri).setMediaId(mediaId).build();

如果应用程序未明确定义媒体项目的媒体ID,则使用URI的字符串表示形式。

将应用程序数据与播放列表项相关联


除了ID外,每个媒体项目还可以配置一个自定义标签,该标签可以是任何应用程序提供的对象。自定义标签的一种用法是将元数据附加到每个媒体项目:

// Build a media item with a custom tag.
MediaItem mediaItem =
    new MediaItem.Builder().setUri(uri).setTag(metadata).build();

检测播放何时过渡到另一个媒体项目

当播放转换到另一个媒体项目,或开始重复同一媒体项目时,将EventListener.onMediaItemTransition(MediaItem, @MediaItemTransitionReason)被调用。此回调接收新的媒体项,并@MediaItemTransitionReason指示发生过渡的原因。一个常见的用例onMediaItemTransition是为新的媒体项目更新应用程序的UI:

@Override
public void onMediaItemTransition(
    @Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {
  updateUiForPlayingMediaItem(mediaItem);
}

如果使用自定义标签将更新UI所需的元数据附加到每个媒体项,则实现可能如下所示:

@Override
public void onMediaItemTransition(
    @Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {
  @Nullable CustomMetadata metadata = null;
  if (mediaItem != null && mediaItem.playbackProperties != null) {
    metadata = (CustomMetadata) mediaItem.playbackProperties.tag;
  }
  updateUiForPlayingMediaItem(metadata);
}

检测播放列表何时更改

添加,删除或移动媒体项目时, EventListener.onTimelineChanged(Timeline, @TimelineChangeReason)立即使用调用TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED。即使尚未准备好播放器,也会调用此回调。

@Override
public void onTimelineChanged(
    Timeline timeline, @TimelineChangeReason int reason) {
  if (reason == TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
    // Update the UI according to the modified playlist (add, move or remove).
    updateUiForPlaylist(timeline);
  }
}

当诸如播放列表中媒体项目的持续时间之类的信息可用时,Timeline将对其进行更新并使用onTimelineChanged进行调用TIMELINE_CHANGE_REASON_SOURCE_UPDATE。可能导致时间表更新的其他原因包括:

  • 在准备自适应媒体项之后,清单变得可用。
  • 清单在实时流的回放期间被定期更新。

设置自定义洗牌顺序

默认情况下,播放列表支持使用进行改组DefaultShuffleOrder。可以通过提供自定义的洗牌顺序实现来自定义:

// Set the custom shuffle order.
simpleExoPlayer.setShuffleOrder(shuffleOrder);
// Enable shuffle mode.
simpleExoPlayer.setShuffleModeEnabled(/* shuffleModeEnabled= */ true);

如果播放器的重复播放模式设置为REPEAT_MODE_ALL,则自定义随机播放顺序将以无限循环播放。

媒体项目


 

播放列表API是基于MediaItems,这可以使用都方便地内置MediaItem.Builder。玩家里面,媒体项目被转换成可播放MediaSource由A S MediaSourceFactory。没有 自定义配置,此转换将由进行DefaultMediaSourceFactory,该能够构建与媒体项的属性相对应的复杂媒体源。下面概述了可以在媒体项目上设置的一些属性。

简单媒体项目


可以使用fromUri 便捷方法构建仅包含流URI的媒体项:

MediaItem mediaItem = MediaItem.fromUri(videoUri);

对于所有其他情况,MediaItem.Builder可以使用a。在下面的示例中,使用ID和一些附加的元数据构建媒体项目:

MediaItem mediaItem = new MediaItem.Builder()
    .setUri(videoUri)
    .setMediaId(mediaId)
    .setTag(metadata)
    .build();

发生播放列表转换时,附加元数据对于更新应用的用户界面可能很有用 。

处理非标准文件扩展名


ExoPlayer库为DASH,HLS和SmoothStreaming提供了自适应媒体源。如果此类自适应媒体项目的URI以标准文件扩展名结尾,则会自动创建相应的媒体源。如果URI具有非标准扩展名或根本没有扩展名,则可以显式设置MIME类型以指示媒体项的类型:

// Use the explicit MIME type to build an HLS media item.
MediaItem mediaItem = new MediaItem.Builder()
    .setUri(hlsUri)
    .setMimeType(MimeTypes.APPLICATION_M3U8)
    .build();

对于渐进式媒体流,不需要MIME类型。

Protected content 受保护的内容


对于受保护的内容,应设置媒体项目的DRM属性:

MediaItem mediaItem = new MediaItem.Builder()
    .setUri(videoUri)
    .setDrmUuid(C.WIDEVINE_UUID)
    .setDrmLicenseUri(licenseUri)
    .setDrmLicenseRequestHeaders(httpRequestHeaders)
    .setDrmMultiSession(true)
    .build();

本示例为Widevine保护的内容构建媒体项。在播放器内部,DefaultMediaSourceFactory会将这些属性传递给以 DrmSessionManagerProvider获得a DrmSessionManager,然后将其注入到created中MediaSource。DRM行为可以 进一步根据 您的需求进行自定义

Sideloading subtitle tracks(侧载字幕轨道)


要旁载字幕轨道,MediaItem.Subtitle可以在构建媒体项目时添加实例:

MediaItem.Subtitle subtitle =
    new MediaItem.Subtitle(
        subtitleUri,
        MimeTypes.APPLICATION_SUBRIP, // The correct MIME type.
        language, // The subtitle language. May be null.
        selectionFlags); // Selection flags for the track.

MediaItem mediaItem = new MediaItem.Builder()
    .setUri(videoUri)
    .setSubtitles(Lists.newArrayList(subtitle))
    .build();

在内部,DefaultMediaSourceFactory将使用MergingMediaSource来将内容媒体源与SingleSampleMediaSource每个字幕轨道的组合。

剪辑媒体流


通过设置自定义开始和结束位置,可以裁剪媒体项引用的内容:

MediaItem mediaItem = new MediaItem.Builder()
    .setUri(videoUri)
    .setClipStartPositionMs(startPositionMs)
    .setClipEndPositionMs(endPositionMs)
    .build();

在内部,DefaultMediaSourceFactory将使用ClippingMediaSource来包装内容媒体源。还有其他剪辑属性。有关更多详细信息,请参见 MediaItem.BuilderJavadoc

剪辑视频文件的开始时,如果可能,请尝试将开始位置与关键帧对齐。如果起始位置未与关键帧对齐,则播放器将需要解码并丢弃从前一个关键帧直到起始位置的数据,然后才能开始播放。这将在播放开始时引入短暂的延迟,包括播放器过渡到播放剪辑的媒体源(作为播放列表的一部分或由于循环播放)时。

广告插入


要插入广告,应设置媒体项目的广告代码URI属性:

MediaItem mediaItem = new MediaItem.Builder()
    .setUri(videoUri)
    .setAdTagUri(adTagUri)
    .build();

在内部,DefaultMediaSourceFactory将内容媒体源包装在中, AdsMediaSource以插入广告标签所定义的广告。为此,播放器还需要相应地进行DefaultMediaSourceFactory 配置

媒体资源


 

在ExoPlayer中,每种媒体都由表示MediaItem。但是在内部,播放器需要MediaSource实例来播放内容。播放器使用来从媒体项目创建这些内容MediaSourceFactory

默认情况下,播放器使用DefaultMediaSourceFactory,可以创建以下内容MediaSource实现的实例:

DefaultMediaSourceFactory还可以根据相应媒体项目的属性来创建更复杂的媒体源。这在“媒体项目”页面上有更详细的描述。

对于需要播放器默认配置不支持的媒体源设置的应用程序,有几个自定义选项。

自定义媒体源创建


构建播放器时,MediaSourceFactory可以注入a。例如,如果某个应用要插入广告并使用CacheDataSource.Factory来支持缓存,则DefaultMediaSourceFactory可以将的实例配置为符合这些要求并在播放器构建期间注入:

MediaSourceFactory mediaSourceFactory =
    new DefaultMediaSourceFactory(cacheDataSourceFactory)
        .setAdsLoaderProvider(adsLoaderProvider)
        .setAdViewProvider(playerView);
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context)
    .setMediaSourceFactory(mediaSourceFactory)
    .build();

该 DefaultMediaSourceFactoryJavaDoc中 详细描述了可用的选项。

也可以注入自定义MediaSourceFactory实现,例如以支持自定义媒体源类型的创建。createMediaSource(MediaItem)将调用工厂的工厂 为添加到播放列表中的每个媒体项目创建媒体源 。

基于媒体源的播放列表API


ExoPlayer接口定义了其他播放列表方法,它们接受媒体源而不是媒体项。这样就可以绕过播放器的内部,MediaSourceFactory并直接将媒体源实例传递给播放器:

// Set a list of media sources as initial playlist.
exoPlayer.setMediaSources(listOfMediaSources);
// Add a single media source.
exoPlayer.addMediaSource(anotherMediaSource);

// Can be combined with the media item API.
exoPlayer.addMediaItem(/* index= */ 3, MediaItem.fromUri(videoUri));

exoPlayer.prepare();
exoPlayer.play();

Track selection 曲目选择


曲目选择确定播放器播放哪些可用的媒体曲目。音轨选择是a的责任,TrackSelector只要ExoPlayer构建a即可提供其实例。

DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
SimpleExoPlayer player =
    new SimpleExoPlayer.Builder(context)
        .setTrackSelector(trackSelector)
        .build();

DefaultTrackSelectorTrackSelector适用于大多数用例的一种灵活性。使用时DefaultTrackSelector,可以通过修改来控制选择的曲目Parameters。这可以在播放之前或播放期间完成。例如,以下代码告诉选择器将视频轨道选择限制为SD,并选择德语轨道(如果有的话):

trackSelector.setParameters(
    trackSelector
        .buildUponParameters()
        .setMaxVideoSizeSd()
        .setPreferredAudioLanguage("deu"));

这是基于约束的轨道选择的示例,其中在不了解实际可用轨道的情况下指定约束。可以使用来指定许多不同类型的约束ParametersParameters 也可用于从可用曲目中选择特定曲目。有关 更多详细信息DefaultTrackSelector,请参见ParametersParametersBuilder文档。

UI组件


 

应用程序播放媒体需要用于显示媒体和控制播放的用户界面组件。ExoPlayer库包括一个UI模块,其中包含许多UI组件。要依赖UI模块,请添加一个依赖关系,如下所示。

implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'

最重要的成分是StyledPlayerControlViewStyledPlayerView, PlayerControlViewPlayerView。样式化的变体提供了更优美的用户体验,但是更难于自定义。

  • StyledPlayerControlView并且PlayerControlView是用于控制播放。它们显示标准的播放控件,包括播放/暂停按钮,快进和快退按钮以及搜索栏。
  • StyledPlayerView并且PlayerView是用于回放的高级视图。它们在播放期间显示视频,字幕和专辑封面,以及分别使用StyledPlayerControlView或 播放控制PlayerControlView

这四个视图都具有setPlayer一种用于附加和分离(通过传递 null)播放器实例的方法。

Player views


StyledPlayerView并且PlayerView可用于视频和音频播放。它们在视频播放的情况下渲染视频和字幕,并可以显示作为元数据包含在音频文件中的图稿。您可以将它们包括在布局文件中,就像其他任何UI组件一样。例如,aStyledPlayerView 可以包含在以下XML中:

<com.google.android.exoplayer2.ui.StyledPlayerView
    android:id="@+id/player_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:show_buffering="when_playing"
    app:show_shuffle_button="true"/>

上面的代码段说明了StyledPlayerView提供几个属性。这些属性可用于自定义视图的行为以及其外观。这些属性中的大多数都有对应的setter方法,可用于在运行时自定义视图。该 StyledPlayerViewJavadoc中列出了这些属性和setter方法的更多细节。PlayerView定义相似的属性。

一旦在布局文件中声明了视图,就可以onCreate在活动的方法中对其进行查找 :

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  // ...
  playerView = findViewById(R.id.player_view);
}

初始化播放器后,可以通过调用将该播放器附加到视图 setPlayer

// Instantiate the player.
player = new SimpleExoPlayer.Builder(context).build();
// Attach player to the view.
playerView.setPlayer(player);
// Set the media source to be played.
player.setMediaSource(createMediaSource());
// Prepare the player.
player.prepare();

选择表面类型

surface_type属性StyledPlayerViewPlayerView允许您设置用于视频播放表面的类型。除了值 spherical_gl_surface_view(其是用于球形视频播放一个特殊值)和video_decoder_gl_surface_view(其是用于视频使用延长渲染器渲染),所允许的值surface_view, texture_viewnone。如果视图仅用于音频播放,none则应避免创建表面,因为这样做可能会很昂贵。

如果该视图用于常规视频播放surface_viewtexture_view 则应使用或。与视频播放相比,SurfaceView有许多优点TextureView

  • 大大降低了许多设备的功耗。
  • 更加精确的帧定时,从而使视频播放更加流畅。
  • 播放受DRM保护的内容时支持安全输出。
  • 可以在可扩展UI层的Android TV设备上以全分辨率显示视频内容的能力。

SurfaceView因此,应该优先于TextureView可能的情况。 TextureView仅在SurfaceView不满足您的需求时使用。一个示例是在Android N之前需要平滑的动画或视频表面滚动,如下所述。对于这种情况,最好 TextureView仅在SDK_INT小于24(Android N)时使用, SurfaceView否则。 

SurfaceView在Android N之前,渲染无法与视图动画正确同步。在早期版本中,当将aSurfaceView放置到滚动容器中或对其进行动画处理时,这可能会导致不良效果 。这样的效果包括视图的内容似乎稍微滞后于应显示的位置,并且在进行动画处理后视图变为黑色。为了在Android N之前实现流畅的动画或视频滚动,因此必须使用TextureView而不是SurfaceView

某些Android TV设备以低于显示器全分辨率的分辨率运行其UI层,从而将其放大以呈现给用户。例如,UI层可以在具有4K显示屏的Android TV上以1080p的分辨率运行。在此类设备上,SurfaceView必须用于以全分辨率显示内容。可以使用来查询显示器的全分辨率(在其当前的显示模式下)Util.getCurrentDisplayModeSize。可以使用Android的Display.getSizeAPI查询UI层分辨率。

Player control views 播放器控制视图


使用时StyledPlayerViewStyledPlayerControlView内部使用a来提供播放控件。当使用a时PlayerViewPlayerControlView内部使用a 。

对于特定的用例StyledPlayerControlViewPlayerControlView也可以用作独立组件。它们可以照常包含在您的布局文件中。例如:

<com.google.android.exoplayer2.ui.StyledPlayerControlView
    android:id="@+id/player_control_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

StyledPlayerControlViewPlayerControlView的Javadoc列表中可用的属性,并为这些组件setter方法。查找它们并附加播放器类似于上面的示例:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  // ...
  playerControlView = findViewById(R.id.player_control_view);
}

private void initializePlayer() {
  // Instantiate the player.
  player = new SimpleExoPlayer.Builder(context).build();
  // Attach player to the view.
  playerControlView.setPlayer(player);
  // Prepare the player with the dash media source.
  player.prepare(createMediaSource());
}

Customization 定制


在需要大量自定义的地方,我们希望应用程序开发人员将实现自己的UI组件,而不是使用ExoPlayer的UI模块提供的组件。也就是说,所提供的UI组件确实可以通过设置属性(如上所述),覆盖可绘制对象,覆盖布局文件以及指定自定义布局文件来进行自定义。

覆盖绘图

StyledPlayerControlView和使用的可绘制对象PlayerControlView (及其默认布局文件)可以被应用程序中定义的具有相同名称的可绘制对象覆盖。有关可覆盖的可绘制对象的列表,请参见StyledPlayerControlView和 PlayerControlViewJavadoc。需要注意的是,这些覆盖可绘制也会影响外观 PlayerViewStyledPlayerView,因为它们在内部使用这些视图。

覆盖布局文件

所有视图组件都从其Javadoc中指定的相应布局文件中扩展其布局。例如,当PlayerControlView实例化a时 ,它会从扩展其布局 exo_player_control_view.xml。要自定义这些布局,应用程序可以在其自己的res/layout*目录中定义具有相同名称的布局文件。这些布局文件将覆盖ExoPlayer库提供的文件。

例如,假设我们希望播放控件仅由位于视图中心的播放/暂停按钮组成。我们可以通过exo_player_control_view.xml在应用程序的res/layout 目录中创建一个文件来实现此目的,该文件包含:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  <ImageButton android:id="@id/exo_play"
      android:layout_width="100dp"
      android:layout_height="100dp"
      android:layout_gravity="center"
      android:background="#CC000000"
      style="@style/ExoMediaButton.Play"/>

  <ImageButton android:id="@id/exo_pause"
      android:layout_width="100dp"
      android:layout_height="100dp"
      android:layout_gravity="center"
      android:background="#CC000000"
      style="@style/ExoMediaButton.Pause"/>

</FrameLayout>

与标准控件相比,视觉外观的变化如下所示。

将标准播放控件(左)替换为自定义控件(右)

图1.用自定义控件(右)替换标准播放控件(左)

自定义布局文件

覆盖布局文件是在整个应用程序中更改布局的绝佳解决方案,但是如果仅在单个位置需要自定义布局怎么办?为此,首先定义一个布局文件,就像覆盖默认布局之一一样,但是这次给它一个不同的文件名,例如custom_controls.xml。其次,使用属性指示在放大视图时应使用此布局。例如,当使用时 PlayerView,可以使用controller_layout_id属性指定膨胀的布局以提供播放控件:

<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     app:controller_layout_id="@layout/custom_controls"/>

下载媒体

ExoPlayer提供了下载媒体以供脱机播放的功能。在大多数情况下,即使您的应用程序处于后台,也希望继续下载。对于这些用例,您的应用程序应为的子类DownloadService,并将命令发送至服务以添加,删除和控制下载。下图显示了涉及的主要类。图1.下载媒体的类。箭头方向指示数据流。 

  • DownloadService:包装aDownloadManager并将命令转发给它。该服务允许DownloadManager即使应用程序在后台运行,也可以继续运行。
  • DownloadManager:管理多个下载,从a载入(和)a的状态DownloadIndex,并根据网络连接等要求开始和停止下载。要下载内容,管理员通常会从中读取正在下载的数据 HttpDataSource,然后将其写入中Cache
  • DownloadIndex:保留下载状态。

创建一个DownloadService

要创建DownloadService,您需要对其进行子类化并实现其抽象方法:

  • getDownloadManager():返回DownloadManager要使用的。
  • getScheduler():返回可选Scheduler,当满足待完成的下载进度所需的需求时,可以重新启动服务。ExoPlayer提供了以下实现:
  • getForegroundNotification():返回服务在前台运行时要显示的通知。您可以用来 DownloadNotificationHelper.buildProgressNotification以默认样式创建通知。

最后,您需要在AndroidManifest.xml文件中定义服务:

<service android:name="com.myapp.MyDownloadService"
    android:exported="false">
  <!-- This is needed for Scheduler -->
  <intent-filter>
    <action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
    <category android:name="android.intent.category.DEFAULT"/>
  </intent-filter>
</service>

有关具体示例,请参见DemoDownloadServiceAndroidManifest.xml在ExoPlayer演示应用程序中。

创建一个DownloadManager

以下代码段演示了如何实例化DownloadManager,可以getDownloadManager()在中将其返回DownloadService

// Note: This should be a singleton in your app.
databaseProvider = new ExoDatabaseProvider(context);

// A download cache should not evict media, so should use a NoopCacheEvictor.
downloadCache = new SimpleCache(
    downloadDirectory,
    new NoOpCacheEvictor(),
    databaseProvider);

// Create a factory for reading the data from the network.
dataSourceFactory = new DefaultHttpDataSourceFactory();

// Choose an executor for downloading data. Using Runnable::run will cause each download task to
// download data on its own thread. Passing an executor that uses multiple threads will speed up
// download tasks that can be split into smaller parts for parallel execution. Applications that
// already have an executor for background downloads may wish to reuse their existing executor.
Executor downloadExecutor = Runnable::run;

// Create the download manager.
downloadManager = new DownloadManager(
    context,
    databaseProvider,
    downloadCache,
    dataSourceFactory,
    downloadExecutor);

// Optionally, setters can be called to configure the download manager.
downloadManager.setRequirements(requirements);
downloadManager.setMaxParallelDownloads(3);

有关DemoUtil具体示例,请参见演示应用程序。

演示应用程序中的示例还从旧版ActionFile 实例导入下载状态。仅当您的应用ActionFile在ExoPlayer 2.10.0之前使用时,才有必要。

添加下载

要添加下载,您需要创建一个DownloadRequest并将其发送到 DownloadService。对于自适应流,DownloadHelper可以使用它来帮助构建a DownloadRequest,如本页下所述。以下示例显示了如何创建下载请求:

DownloadRequest downloadRequest =
    new DownloadRequest.Builder(contentId, contentUri).build();

其中contentId是内容的唯一标识符。在简单的情况下, contentUri通常可以将用作contentId,但是应用程序可以自由使用最适合其用例的ID方案。DownloadRequest.Builder也有一些可选的二传手。例如,setKeySetIdsetData可用于分别设置应用程序希望与下载关联的DRM和自定义数据。还可以使用来指定内容的MIME类型setMimeType,以作为无法从中推断出内容类型的提示contentUri

创建完成后,可以将请求发送到,DownloadService以添加下载内容:

DownloadService.sendAddDownload(
    context,
    MyDownloadService.class,
    downloadRequest,
    /* foreground= */ false)

MyDownloadService应用程序的DownloadService子类在哪里,该 foreground参数控制服务是否在前台启动。如果您的应用程序已经在前台,则foreground 通常应将该参数设置为false,因为DownloadService如果它确定有工作要做,则会将自己置于前台。

删除下载

一个下载可以通过发送一个删除命令到被移除DownloadService,其中,contentId识别要去除的下载:

DownloadService.sendRemoveDownload(
    context,
    MyDownloadService.class,
    contentId,
    /* foreground= */ false)

您也可以使用删除所有下载的数据 DownloadService.sendRemoveAllDownloads

开始和停止下载

如果满足以下四个条件,则下载将继续进行:

  • 下载没有停止的原因。
  • 下载不会暂停。
  • 满足下载进度的要求。需求可以指定对允许的网络类型的限制,以及设备应处于空闲状态还是应连接至充电器。
  • 不超过并行下载的最大数量。

所有这些条件都可以通过向您的计算机发送命令来控制 DownloadService

设置和清除下载停止原因

可以设置一个或所有下载停止的原因:

// Set the stop reason for a single download.
DownloadService.sendSetStopReason(
    context,
    MyDownloadService.class,
    contentId,
    stopReason,
    /* foreground= */ false);

// Clear the stop reason for a single download.
DownloadService.sendSetStopReason(
    context,
    MyDownloadService.class,
    contentId,
    Download.STOP_REASON_NONE,
    /* foreground= */ false);

其中stopReason可以是任何非零值(这Download.STOP_REASON_NONE = 0是一个特殊值,表示下载不会停止)。导致下载停止的原因多种多样的应用可以使用不同的值来跟踪每次下载被停止的原因。设置和清除所有下载的停止原因与设置和清除单个下载的停止原因的工作方式相同,只是contentId应将设置为null

设置停止原因不会删除下载。部分下载将被保留,清除停止原因将导致下载继续。

当下载具有非零停止原因时,它将处于 Download.STATE_STOPPED状态。停止原因DownloadIndex保留在中,因此,如果应用程序进程被终止并随后重新启动,则保留原因 。

暂停并恢复所有下载

可以按以下方式暂停和恢复所有下载:

// Pause all downloads.
DownloadService.sendPauseDownloads(
    context,
    MyDownloadService.class,
    /* foreground= */ false);

// Resume all downloads.
DownloadService.sendResumeDownloads(
    context,
    MyDownloadService.class,
    /* foreground= */ false);

暂停下载后,它们将处于Download.STATE_QUEUED状态。与设置停止原因不同,此方法不会保留任何状态更改。它只会影响的运行时状态DownloadManager

设置下载进度的要求

Requirements可以用于指定下载必须满足的约束。如上例所示,可以通过DownloadManager.setRequirements()在创建时调用来设置要求 。也可以通过向以下命令发送命令来动态更改它们:DownloadManagerDownloadService

// Set the download requirements.
DownloadService.sendSetRequirements(
    context,
    MyDownloadService.class,
    requirements,
    /* foreground= */ false);

如果由于不满足要求而无法进行下载时,它将处于Download.STATE_QUEUED状态。您可以使用查询未满足的要求DownloadManager.getNotMetRequirements()

设置最大并行下载数

可以通过调用设置最大并行下载数 DownloadManager.setMaxParallelDownloads()DownloadManager如上例所示,通常在创建时完成此操作。

当由于并行下载的最大数量而无法进行下载时,它将处于此Download.STATE_QUEUED状态。

查询下载

DownloadIndex一个DownloadManager可以查询所有下载,包括那些已完成或失败的状态。该DownloadIndex 可以通过调用来获得DownloadManager.getDownloadIndex()。然后,可以通过调用获取遍历所有下载的游标 DownloadIndex.getDownloads()。或者,可以通过调用查询单个下载的状态DownloadIndex.getDownload()

DownloadManager还提供了DownloadManager.getCurrentDownloads(),它仅返回当前(即未完成或失败)下载的状态。此方法对于更新通知和其他显示当前下载进度和状态的UI组件很有用。

收听下载

您可以添加一个侦听器,DownloadManager以在当前下载更改状态时得到通知:

downloadManager.addListener(
    new DownloadManager.Listener() {
      // Override methods of interest here.
    });

有关具体示例,请参见DownloadManagerListener演示应用程序的DownloadTracker类。

下载进度更新不会触发DownloadManager.Listener。要更新显示下载进度的UI组件,您应该DownloadManager以所需的更新速率定期查询。DownloadService 包含一个示例,该示例会定期更新服务前台通知。

播放下载的内容

播放下载的内容类似于播放在线内容,除了从下载中读取数据Cache而不是通过网络读取数据。

重要的是,不要尝试直接从下载目录中读取文件。而是使用ExoPlayer库类,如下所述。

要播放下载的内容,创建一个CacheDataSource.Factory使用相同的 Cache是用于下载实例,并注入到 DefaultMediaSourceFactory建立播放器时:

// Create a read-only cache data source factory using the download cache.
DataSource.Factory cacheDataSourceFactory =
    new CacheDataSource.Factory()
        .setCache(downloadCache)
        .setUpstreamDataSourceFactory(httpDataSourceFactory)
        .setCacheWriteDataSinkFactory(null); // Disable writing.

SimpleExoPlayer player = new SimpleExoPlayer.Builder(context)
    .setMediaSourceFactory(
        new DefaultMediaSourceFactory(cacheDataSourceFactory))
    .build();

如果同一播放器实例也将用于播放未下载的内容,CacheDataSource.Factory则应将其配置为只读,以避免在播放期间也下载该内容。

将播放器配置为时CacheDataSource.Factory,它将可以访问下载的内容以进行播放。播放下载内容就像将对应的内容传递MediaItem给播放器一样简单。AMediaItem 可以从Download使用中获得,也可以Download.request.toMediaItem直接从DownloadRequest使用中获得DownloadRequest.toMediaItem

MediaSource配置

上面的示例使下载缓存可用于所有MediaItems的回放 。还可以使下载缓存可用于各个MediaSource实例,这些实例可以直接传递给播放器:

ProgressiveMediaSource mediaSource =
    new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
        .createMediaSource(MediaItem.fromUri(contentUri));
player.setMediaSource(mediaSource);
player.prepare();

下载和播放自适应流

自适应流(例如DASH,SmoothStreaming和HLS)通常包含多个媒体轨道。通常会有多条轨道包含质量不同的相同内容(例如SD,HD和4K视频轨道)。也可能有多个包含不同内容的相同类型的音轨(例如,使用不同语言的多个音频音轨)。

对于流式播放,可以使用音轨选择器选择播放哪些音轨。同样,对于下载,DownloadHelper可以使用a选择要下载的曲目。a的典型用法DownloadHelper 如下步骤:

  1. DownloadHelper使用一种DownloadHelper.forMediaItem 方法构建。准备帮助程序并等待回调。
    DownloadHelper downloadHelper =
        DownloadHelper.forMediaItem(
            context,
            MediaItem.fromUri(contentUri),
            new DefaultRenderersFactory(context),
            dataSourceFactory);
    downloadHelper.prepare(myCallback);
  2. 可以选择使用检查默认选定的曲目getMappedTrackInfo ,并getTrackSelections和使用进行调整clearTrackSelections, replaceTrackSelectionsaddTrackSelection
  3. DownloadRequest通过调用来为选定的曲目 创建一个getDownloadRequest。如上所述,可以将请求传递给您DownloadService以添加下载。
  4. 使用释放助手release()

MediaItem如上所述,下载的自适应内容的播放需要配置播放器并传递相应的内容。

构建时MediaItemMediaItem.playbackProperties.streamKeys必须将设置为与匹配,DownloadRequest以便播放器仅尝试播放已下载的曲目的子集。使用 Download.request.toMediaItemDownloadRequest.toMediaItem构建the MediaItem将为您解决这个问题。如果要构建一个MediaSource直接传递给播放器的功能,同样重要的是通过调用来配置流键MediaSourceFactory.setStreamKeys

如果您在尝试播放下载的自适应内容时看到从网络请求数据,则最可能的原因是播放器试图适应未下载的曲目。确保正确设置了流密钥。

 

广告插入

ExoPlayer可以用于客户端和服务器端广告插入。

客户端广告插入

在客户端插入广告时,播放器会在播放内容和广告之间转换时,在从不同URL加载媒体之间进行切换。有关广告的信息与媒体分开加载,例如从XML VAST或 VMAP广告代码中加载。这可以包括相对于内容开头的广告提示位置,实际广告媒体URI和元数据(例如给定广告是否可跳过)。

当使用ExoPlayer的AdsMediaSource客户端广告插入时,播放器将具有有关要播放的广告的信息。这有几个好处:

  • 播放器可以通过其API公开与广告相关的元数据和功能。
  • ExoPlayer UI组件可以自动显示广告位置的标记,并根据广告是否在播放来更改其行为。
  • 在内部,播放器可以在广告和内容之间的过渡之间保持一致的缓冲。

在此设置中,播放器负责在广告和内容之间进行切换,这意味着应用程序无需照顾控制广告和内容的多个单独的背景/前景播放器。

准备用于客户端广告插入的内容视频和广告标签时,理想情况下,应将广告放置在内容视频中的同步样本(关键帧)上,以便播放器可以无缝地继续播放内容。

声明式广告支持

构建时,可以指定广告代码URI MediaItem

MediaItem mediaItem =
    new MediaItem.Builder().setUri(videoUri).setAdTagUri(adTagUri).build();

为了使播放器支持指定广告代码的媒体项目,在创建播放器时,有必要构建并注入一个DefaultMediaSourceFactory带有AdsLoaderProvider和的配置文件 AdViewProvider

MediaSourceFactory mediaSourceFactory =
    new DefaultMediaSourceFactory(context)
        .setAdsLoaderProvider(adsLoaderProvider)
        .setAdViewProvider(playerView);
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context)
    .setMediaSourceFactory(mediaSourceFactory)
    .build();

在内部,DefaultMediaSourceFactory将内容媒体源包装在中 AdsMediaSource。该AdsMediaSource会获得AdsLoader来自 AdsLoaderProvider和使用媒体项目的广告代码的定义为插入广告。

ExoPlayerStyledPlayerViewPlayerViewUI组件均实现 AdViewProvider。IMA扩展程序提供了易于使用的功能AdsLoader,如下所述。

带有广告的播放列表

播放包含多个媒体项目的播放列表时,默认行为是请求广告代码,并为每个媒体ID和广告代码URI组合一次存储广告播放状态。这意味着,即使广告代码URI匹配,用户也将看到带有媒体具有不同媒体ID的广告的每个媒体项目的广告。如果重复播放媒体项目,则用户将仅看到一次相应的广告(广告播放状态存储是否播放过广告,因此在首次出现后会被跳过)。

可以通过传递不透明的广告标识符来自定义此行为,该标识符基于对象相等性与给定媒体项目的广告播放状态链接在一起。这是一个示例,其中通过将广告标签URI作为广告标识符传递,从而将广告播放状态仅链接到广告标签URI,而不是媒体ID和广告标签URI的组合。这样的结果是,广告只会加载一次,并且从头到尾播放播放列表时,用户不会在第二项上看到广告。

// Build the media items, passing the same ads identifier for both items,
// which means they share ad playback state so ads play only once.
MediaItem firstItem =
    new MediaItem.Builder()
        .setUri(firstVideoUri)
        .setAdTagUri(adTagUri, /* adsId= */ adTagUri)
        .build();
MediaItem secondItem =
    new MediaItem.Builder()
        .setUri(secondVideoUri)
        .setAdTagUri(adTagUri, /* adsId= */ adTagUri)
        .build();
player.addMediaItem(firstItem);
player.addMediaItem(secondItem);

IMA扩展

ExoPlayer IMA扩展提供ImaAdsLoader,因此很容易客户端广告插入集成到你的应用程序。它包装了客户端IMA SDK的功能,以支持VAST / VMAP广告的插入。有关如何使用扩展程序(包括如何处理背景和恢复播放)的说明,请参阅README(自述文件)

演示应用程序使用IMA扩展程序,并在示例列表中包含几个示例VAST / VMAP广告代码。

用户界面注意事项

StyledPlayerViewPlayerView默认情况下在广告播放期间隐藏控件,但是应用可以通过调用setControllerHideDuringAds在两个视图中定义的来切换此行为 。当广告正在播放时,IMA SDK将在播放器顶部显示其他视图(例如,“更多信息”链接和跳过按钮,如果适用)。

由于广告客户期望跨应用程序获得一致的体验,因此IMA SDK不允许自定义广告播放时显示的视图。因此,不可能删除或重新定位跳过按钮,更改字体或对这些视图的视觉外观进行其他自定义。

IMA SDK可能会报告广告是否被播放器顶部呈现的应用程序提供的视图所遮盖。需要覆盖对控制播放至关重要的视图的应用必须将其注册到IMA SDK,以便可以在可见度计算中将其忽略。当使用StyledPlayerView 或PlayerView作为时AdViewProvider,他们将自动注册其控制覆盖图。使用自定义播放器用户界面的应用必须通过从中返回叠加视图来进行注册AdViewProvider.getAdOverlayInfos

有关叠加视图的详细信息,请参阅 IMA SDK中的Open Measurement

随播广告

一些广告代码包含其他随播广告,这些随播广告可以显示在应用程序用户界面的“广告位”中。这些插槽可以通过传递 ImaAdsLoader.Builder.setCompanionAdSlots(slots)。有关更多信息,请参见 添加随播广告

独立广告

IMA SDK旨在将广告插入媒体内容中,而不是自己播放独立广告。因此,IMA扩展程序不支持播放独立广告。我们建议在这种情况下使用Google Mobile Ads SDK

使用第三方广告SDK

如果您需要通过第三方广告SDK加载广告,则值得检查它是否已经提供了ExoPlayer集成。如果不是这样,建议采用一种AdsLoader包装第三方广告SDK的自定义 方法,因为它具有AdsMediaSource上述优点。 ImaAdsLoader充当示例实现。

另外,您可以使用ExoPlayer的播放列表支持来制作一系列广告和内容剪辑:

// A pre-roll ad.
MediaItem preRollAd = MediaItem.fromUri(preRollAdUri);
// The start of the content.
MediaItem contentStart =
    new MediaItem.Builder()
        .setUri(contentUri)
        .setClipEndPositionMs(120_000)
        .build();
// A mid-roll ad.
MediaItem midRollAd = MediaItem.fromUri(midRollAdUri);
// The rest of the content
MediaItem contentEnd =
    new MediaItem.Builder()
        .setUri(contentUri)
        .setClipStartPositionMs(120_000)
        .build();

// Build the playlist.
player.addMediaItem(preRollAd);
player.addMediaItem(contentStart);
player.addMediaItem(midRollAd);
player.addMediaItem(contentEnd);

服务器端广告插入

在服务器端广告插入(也称为动态广告插入或DAI)中,媒体流同时包含广告和内容。DASH清单可能同时指向内容段和广告段。对于HLS,请参阅有关将广告合并到播放列表中的Apple文档。

使用服务器端广告插入时,客户端可能需要将跟踪事件报告给广告SDK或广告服务器。例如,媒体流可能包括需要由客户端报告的定时事件( 有关ExoPlayer支持哪些定时元数据格式的信息,请参阅支持的格式)。应用可以通过侦听来自播放器的定时元数据事件 SimpleExoPlayer.addMetadataOutput

IMA扩展程序当前仅处理客户端广告插入。它不提供与IMA SDK的DAI部分的任何集成。

即时串流

ExoPlayer即开即用地播放大多数自适应实时流,而无需任何特殊配置。有关更多详细信息,请参见支持的格式页面

自适应实时流提供了一个可用媒体窗口,该窗口会定期更新以与当前实时同步。这意味着播放位置将始终在此窗口中的某个位置,在大多数情况下,该位置接近于生成流的当前实时时间。当前实时位置和播放位置之间的差异称为实时偏移

与自适应直播流不同,渐进式直播流没有直播窗口,只能在一个位置播放。此页面上的文档仅与自适应直播有关。

ExoPlayer通过稍微改变播放速度来调整实时偏移。播放器将尝试匹配用户和媒体首选项,但还将尝试对不断变化的网络状况做出反应。例如,如果在播放过程中发生了重新缓冲,则播放器将远离实时边缘。如果在较长的时间内有足够的可用缓冲区,则播放器将再次移至实时边缘。

检测和监控实时播放

每次更新活动窗口时,注册的Player.EventListener实例都会收到一个onTimelineChanged事件。您可以通过查询各种 方法PlayerTimeline.Window方法来检索有关当前实时回放的详细信息,如下所列和下图所示。

实况窗口

图1.实时窗口

  • Player.isCurrentWindowLive指示当前正在播放的媒体项目是否为实时流。即使直播结束,该值仍为true。
  • Player.isCurrentWindowDynamic指示当前播放的媒体项目是否仍在更新中。对于尚未结束的实时流,通常是这样。请注意,在某些情况下,此标志对非实时流也适用。
  • Player.getCurrentLiveOffset 返回当前实时和播放位置(如果有)之间的偏移量。
  • Player.getDuration 返回当前活动窗口的长度。
  • Player.getCurrentPosition 返回相对于实时窗口开始的播放位置。
  • Player.getCurrentMediaItem返回当前媒体项目,其中 MediaItem.liveConfiguration包含目标实时偏移和实时偏移调整参数的应用程序提供的替代。
  • Player.getCurrentTimeline在中返回当前的媒体结构 TimelineTimeline.Window可以从Timeline usingPlayer.getCurrentWindowIndex和中检索电流Timeline.getWindow。在内 Window
    • Window.liveConfiguration包含目标实时偏移和实时偏移调整参数。这些值基于媒体中的信息以及在中设置的任何应用程序提供的替代值MediaItem.liveConfiguration
    • Window.windowStartTimeMs 从Unix Epoch开始,实时窗口开始的时间。
    • Window.getCurrentUnixTimeMs是从当前的Unix Epoch开始的时间。可以通过服务器和客户端之间的已知时钟差来纠正此值。
    • Window.getDefaultPositionMs 是实时窗口中播放器默认开始播放的位置。

在直播中寻找

您可以使用定位到实时窗口中的任何位置Player.seekTo。传递的查找位置相对于实时窗口的开始。例如, seekTo(0)将寻求到实时窗口的开始。玩家将尝试在寻道之后保持与寻道位置相同的实时偏移。

实时窗口还具有默认位置,应在该位置开始播放。该位置通常靠近活动边缘。您可以通过调用来查找默认位置Player.seekToDefaultPosition

实时播放界面

ExoPlayer的默认UI组件显示实时窗口的持续时间以及当前窗口中的当前播放位置。这意味着,每次更新实时窗口时,该位置都会向后跳。如果您需要不同的行为,例如显示Unix时间或当前的实时偏移量,则可以进行分叉StyledPlayerControlView和修改以适合您的需求。

对于ExoPlayer的默认UI组件,有一个待处理的功能请求(#2213),以在播放实时流时支持其他模式。

配置实时播放参数

默认情况下,ExoPlayer使用媒体定义的实时回放参数。如果要自己配置实时回放参数,则可以MediaItem通过调用MediaItem.Builder.setLiveXXX方法逐个设置它们。如果要为所有项目全局设置这些值,则可以在DefaultMediaSourceFactory提供给播放器的上设置它们 。在这两种情况下,提供的值都将覆盖介质定义的参数。

// Global settings.
SimpleExoPlayer player =
    new SimpleExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context).setLiveTargetOffsetMs(5000))
        .build();

// Per MediaItem settings.
MediaItem mediaItem =
    new MediaItem.Builder()
        .setUri(mediaUri)
        .setLiveMaxPlaybackSpeed(1.02f)
        .build();
player.setMediaItem(mediaItem);

可用的配置值为:

  • targetOffsetMs:目标实时补偿。如果可能,播放器将尝试在播放过程中接近此实时偏移。
  • minOffsetMs:允许的最小实时偏移量。即使将偏移量调整为当前网络条件,播放器也不会在播放期间尝试低于该偏移量。
  • maxOffsetMs:允许的最大实时偏移量。即使将偏移量调整为当前网络条件,播放器在播放过程中也不会尝试超过该偏移量。
  • minPlaybackSpeed:尝试达到目标实时偏移时,播放器可以回退的最低播放速度。
  • maxPlaybackSpeed:尝试达到目标实时偏移时,播放器可以用来赶上的最大播放速度。

如果不需要自动播放速度调节,可以通过将minPlaybackSpeed和设置maxPlaybackSpeed为来禁用它1.0f

BehindLiveWindowException

例如,如果播放器暂停或缓冲了足够长的时间,则播放位置可能会落在实时窗口后面。如果发生这种情况,则播放将失败,并且BehindLiveWindowException将通过报告 Player.EventListener.onPlayerError。应用程序代码可能希望通过在默认位置恢复播放来处理此类错误。演示应用程序的PlayerActivity就是这种方法的例证。

@Override
public void onPlayerError(ExoPlaybackException e) {
  if (isBehindLiveWindow(e)) {
    // Re-initialize player at the current live window default position.
    player.seekToDefaultPosition();
    player.prepare();
  } else {
    // Handle other errors.
  }
}

private static boolean isBehindLiveWindow(ExoPlaybackException e) {
  if (e.type != ExoPlaybackException.TYPE_SOURCE) {
    return false;
  }
  Throwable cause = e.getSourceException();
  while (cause != null) {
    if (cause instanceof BehindLiveWindowException) {
      return true;
    }
    cause = cause.getCause();
  }
  return false;
}

自定义播放速度调整算法

为了保持在目标实时偏移附近,LivePlaybackSpeedControl在实时回放过程中使用a可以调整回放速度。可以实现自定义LivePlaybackSpeedControl,也可以自定义默认实现DefaultLivePlaybackSpeedControl。在这两种情况下,都可以在构建播放器时设置一个实例:

SimpleExoPlayer player =
    new SimpleExoPlayer.Builder(context)
        .setLivePlaybackSpeedControl(
            new DefaultLivePlaybackSpeedControl.Builder()
                .setFallbackMaxPlaybackSpeed(1.04f)
                .build())
        .build();

的相关自定义参数为DefaultLivePlaybackSpeedControl

  • fallbackMinPlaybackSpeedand fallbackMaxPlaybackSpeed:如果媒体或应用程序提供的均未MediaItem定义限制,则可用于调整的最小和最大播放速度。
  • proportionalControlFactor:控制速度调整的平滑程度。较高的值会使调整更加突然和被动,但也更有可能听到声音。较小的值将导致速度之间的平滑过渡,但会降低速度。
  • targetLiveOffsetIncrementOnRebufferMs:每当发生重新缓冲时,此值都会添加到目标实时偏移中,以便更加谨慎地进行。可以通过将该值设置为0来禁用此功能。
  • minPossibleLiveOffsetSmoothingFactor:指数平滑因子,用于根据当前缓冲的媒体跟踪最小可能的实时偏移。值非常接近1表示估算更加谨慎,可能需要更长的时间才能调整到改善的网络条件,而值越低意味着估算值调整得越快,但遇到重新缓存的风险也会更高。

调试日志

默认情况下,ExoPlayer仅记录错误。为了记录玩家的活动中,EventLogger 可以使用类。它提供的其他日志记录有助于理解播放器的操作以及调试播放问题。EventLogger器具AnalyticsListener,所以注册与实例SimpleExoPlayer是容易的:

player.addAnalyticsListener(new EventLogger(trackSelector));

传递trackSelector启用将启用其他日志记录,但是它是可选的,因此 null可以改为传递。

观察日志最简单的方法是使用Android Studio的logcat的标签。您可以通过包名来选择你的应用程序作为调试的过程( com.google.android.exoplayer2.demo如果使用的是演示应用程序),并告诉logcat的标签只记录了该应用通过选择“仅显示选定的应用程序”。这是可能的进一步过滤与表达的记录 EventLogger|ExoPlayerImpl,只得到从记录EventLogger和播放器本身。

使用Android Studio的logcat标签的替代方法是使用控制台。例如:

adb logcat EventLogger:* ExoPlayerImpl:* *:s

Player information

ExoPlayerImpl类提供有关播放器版本,应用程序正在运行的设备和操作系统,并已加载ExoPlayer的模块的两个重要线路:

ExoPlayerImpl: Release 2cd6e65 [ExoPlayerLib/2.12.0] [marlin, Pixel XL, Google, 26] [goog.exo.core, goog.exo.ui, goog.exo.dash]
ExoPlayerImpl: Init 2e5194c [ExoPlayerLib/2.12.0] [marlin, Pixel XL, Google, 26]

播放状态

玩家状态更改记录在下面的行中:

EventLogger: playWhenReady [eventTime=0.00, mediaPos=0.00, window=0, true, USER_REQUEST]
EventLogger: state [eventTime=0.01, mediaPos=0.00, window=0, BUFFERING]
EventLogger: state [eventTime=0.93, mediaPos=0.00, window=0, period=0, READY]
EventLogger: isPlaying [eventTime=0.93, mediaPos=0.00, window=0, period=0, true]
EventLogger: playWhenReady [eventTime=9.40, mediaPos=8.40, window=0, period=0, false, USER_REQUEST]
EventLogger: isPlaying [eventTime=9.40, mediaPos=8.40, window=0, period=0, false]
EventLogger: playWhenReady [eventTime=10.40, mediaPos=8.40, window=0, period=0, true, USER_REQUEST]
EventLogger: isPlaying [eventTime=10.40, mediaPos=8.40, window=0, period=0, true]
EventLogger: state [eventTime=20.40, mediaPos=18.40, window=0, period=0, ENDED]
EventLogger: isPlaying [eventTime=20.40, mediaPos=18.40, window=0, period=0, false]

在这个例子中,准备播放器之后开始播放0.93秒。用户暂停9.4秒后播放,并继续播放一秒钟后为10.4秒。播放结束后十秒20.4秒。方括号内的公共元素是:

  • [eventTime=float]:因为玩家创建的挂钟时间。
  • [mediaPos=float]:当前播放位置。
  • [window=int]:当前窗口索引。
  • [period=int]:在本期该窗口。

每行中的最后元素指示所报告的状态的值。

媒体轨道

跟踪信息将被记录时,可用或选定曲目的变化。这种情况至少一次在回放的开始。下面示出的例子中记录轨道用于自适应流:

EventLogger: tracks [eventTime=0.30, mediaPos=0.00, window=0, period=0,
EventLogger:   MediaCodecVideoRenderer [
EventLogger:     Group:0, adaptive_supported=YES [
EventLogger:       [X] Track:0, id=133, mimeType=video/avc, bitrate=261112, codecs=avc1.4d4015, res=426x240, fps=30.0, supported=YES
EventLogger:       [X] Track:1, id=134, mimeType=video/avc, bitrate=671331, codecs=avc1.4d401e, res=640x360, fps=30.0, supported=YES
EventLogger:       [X] Track:2, id=135, mimeType=video/avc, bitrate=1204535, codecs=avc1.4d401f, res=854x480, fps=30.0, supported=YES
EventLogger:       [X] Track:3, id=160, mimeType=video/avc, bitrate=112329, codecs=avc1.4d400c, res=256x144, fps=30.0, supported=YES
EventLogger:       [ ] Track:4, id=136, mimeType=video/avc, bitrate=2400538, codecs=avc1.4d401f, res=1280x720, fps=30.0, supported=NO_EXCEEDS_CAPABILITIES
EventLogger:     ]
EventLogger:   ]
EventLogger:   MediaCodecAudioRenderer [
EventLogger:     Group:0, adaptive_supported=YES_NOT_SEAMLESS [
EventLogger:       [ ] Track:0, id=139, mimeType=audio/mp4a-latm, bitrate=48582, codecs=mp4a.40.5, channels=2, sample_rate=22050, supported=YES
EventLogger:       [X] Track:1, id=140, mimeType=audio/mp4a-latm, bitrate=127868, codecs=mp4a.40.2, channels=2, sample_rate=44100, supported=YES
EventLogger:     ]
EventLogger:   ]
EventLogger: ]

在这个例子中,玩家已经选择五个可用的视频轨道中的四个。未选择第五视频轨道因为它超过了设备的能力,由所指示的supported=NO_EXCEEDS_CAPABILITIES。玩家将在播放过程中选择的视频轨道之间适应。当从一个轨道到另一个玩家适应,它记录在类似下面的一行:

EventLogger: downstreamFormat [eventTime=3.64, mediaPos=3.00, window=0, period=0, id=134, mimeType=video/avc, bitrate=671331, codecs=avc1.4d401e, res=640x360, fps=30.0]

此日志行表示玩家切换到640×360分辨率的视频,可跟踪三秒钟后进入媒体。

解码器选择

在大多数情况下ExoPlayer呈现使用媒体MediaCodec从底层平台收购。当解码器被初始化时,这个被记录在如下所示的行:

EventLogger: videoDecoderInitialized [0.77, 0.00, window=0, period=0, video, OMX.qcom.video.decoder.avc]
EventLogger: audioDecoderInitialized [0.79, 0.00, window=0, period=0, audio, OMX.google.aac.decoder]

分析工具

ExoPlayer支持广泛的回放分析需求。归根结底,分析是关于从回放中收集,解释,汇总和汇总数据。此数据可以在设备上使用,例如用于记录,调试或通知将来的播放决策,也可以报告给服务器以监视所有设备上的播放。

分析系统通常需要首先收集事件,然后进一步处理它们以使其有意义:

  • 事件收集:这可以通过AnalyticsListenerExoPlayer 实例上注册来完成。注册的分析侦听器会在使用播放器时接收事件。每个事件都与播放列表中的相应媒体项目以及播放位置和时间戳元数据关联。
  • 事件处理:某些分析系统将原始事件上载到服务器,而所有事件处理均在服务器端执行。还可以处理设备上的事件,这样做可能更简单或减少需要上载的信息量。ExoPlayer提供了PlaybackStatsListener,它使您可以执行以下处理步骤:
    1. 事件解释:为了对分析有用,需要在单个回放的上下文中解释事件。例如,玩家状态更改为的原始事件STATE_BUFFERING可能对应于初始缓冲,重新缓冲或在搜索之后发生的缓冲。
    2. 状态跟踪:此步骤将事件转换为计数器。例如,状态更改事件可以转换为跟踪在每个播放状态中花费多少时间的计数器。结果是一次回放的一组基本分析数据值。
    3. 汇总:此步骤通常通过添加计数器来组合多个回放中的分析数据。
    4. 汇总指标的计算:许多最有用的指标是那些计算平均值或以其他方式组合基本分析数据值的指标。可以为单个或多个回放计算摘要指标。

使用AnalyticsListener收集事件

来自播放器的原始播放事件会报告给AnalyticsListener 实现。您可以轻松添加自己的侦听器,并仅覆盖您感兴趣的方法:

simpleExoPlayer.addAnalyticsListener(new AnalyticsListener() {
    @Override
    public void onPlaybackStateChanged(
        EventTime eventTime, @Player.State int state) {
    }

    @Override
    public void onDroppedVideoFrames(
        EventTime eventTime, int droppedFrames, long elapsedMs) {
    }
});

EventTime则传递到每个回调关联的事件,在播放列表中的媒体项目,以及播放位置和时间戳元数据:

  • realtimeMs:事件的挂钟时间。
  • timelinewindowIndexmediaPeriodId:定义播放列表以及事件所属的播放列表中的项目。所述mediaPeriodId 包含任选的另外的信息,例如指示事件是否属于该项目中的广告。
  • eventPlaybackPositionMs:事件发生时项目中的播放位置。
  • currentTimelinecurrentWindowIndexcurrentMediaPeriodId和 currentPlaybackPositionMs:同上,但对于当前正在播放的项目。当前播放的项目可能与事件所属的项目不同,例如,如果事件对应于要播放的下一个项目的预缓冲。

使用PlaybackStatsListener处理事件

PlaybackStatsListenerAnalyticsListener在设备事件处理上实现的。它PlaybackStats使用计数器和派生指标来计算,包括:

  • 摘要指标,例如总播放时间。
  • 自适应播放质量指标,例如平均视频分辨率。
  • 渲染质量指标,例如丢帧率。
  • 资源使用率指标,例如,通过网络读取的字节数。

您将在PlaybackStatsJavadoc中找到可用计数和派生指标的完整列表 。

PlaybackStatsListener分开计算PlaybackStats为播放列表中的每个媒体项目,以及这些项目中插入每个客户端广告。您可以提供一个回调以PlaybackStatsListener通知已完成的播放,并使用EventTime传递给回调的标识完成播放。可以汇总多个回放的分析数据。您也可以随时使用来查询PlaybackStats当前播放会话的 PlaybackStatsListener.getPlaybackStats()

simpleExoPlayer.addAnalyticsListener(
    new PlaybackStatsListener(
        /* keepHistory= */ true, (eventTime, playbackStats) -> {
          // Analytics data for the session started at `eventTime` is ready.
        }));

的构造函数PlaybackStatsListener提供了保留已处理事件的完整历史记录的选项。请注意,这可能会导致未知的内存开销,具体取决于播放的长度和事件的数量。因此,仅在需要访问已处理事件的完整历史记录而不是仅访问最终分析数据的情况下,才应将其打开。

请注意,PlaybackStats使用扩展的状态集不仅可以指示媒体的状态,还可以指示用户播放的意图以及更详细的信息,例如为什么中断播放或结束播放:

播放状态用户玩意无意玩
播放前JOINING_FOREGROUNDNOT_STARTED, JOINING_BACKGROUND
主动播放PLAYING 
播放中断BUFFERING, SEEKINGPAUSEDPAUSED_BUFFERINGSUPPRESSEDSUPPRESSED_BUFFERINGINTERRUPTED_BY_AD
结束状态 ENDEDSTOPPEDFAILEDABANDONED

用户的播放意图对于区分用户正在主动等待播放继续的时间与被动等待时间是很重要的。例如, PlaybackStats.getTotalWaitTimeMs返回在花费的总时间 JOINING_FOREGROUNDBUFFERING以及SEEKING美国,而不是在播放暂停的时间。同样,PlaybackStats.getTotalPlayAndWaitTimeMs将返回具有用户意图播放的总时间,即总的有效等待时间和在该PLAYING状态下花费的总时间。

处理和解释的事件

您可以使用PlaybackStatsListener 与记录已处理和已解释的事件keepHistory=true。结果PlaybackStats将包含以下事件列表:

  • playbackStateHistory:扩展播放状态的有序列表及其EventTime开始应用的位置。您还可以 PlaybackStats.getPlaybackStateAtTime用来在给定的墙上时钟时间查询状态。
  • mediaTimeHistory:壁钟时间和媒体时间对的历史记录,使您可以重构在哪个时间播放了媒体的哪些部分。您也可以PlaybackStats.getMediaTimeMsAtRealtimeMs用来在给定的墙上时钟时间查找播放位置。
  • videoFormatHistoryaudioFormatHistory:播放时使用的视频和音频格式的有序列表,以及EventTime开始使用的位置。
  • fatalErrorHistorynonFatalErrorHistory:致命错误和非致命错误的有序列表及其EventTime发生的位置。致命错误是指那些已结束播放的错误,而非致命错误可能已经可以恢复。

单次播放分析数据

如果使用PlaybackStatsListener,即使使用,也会自动收集此数据keepHistory=false。最终值是您可以在PlaybackStatsJavadoc中找到的公共字段以及所返回的播放状态持续时间getPlaybackStateDurationMs。为了方便起见,您还将找到getTotalPlayTimeMsgetTotalWaitTimeMs这样的方法,它们返回特定播放状态组合的持续时间。

Log.d("DEBUG", "Playback summary: "
    + "play time = " + playbackStats.getTotalPlayTimeMs()
    + ", rebuffers = " + playbackStats.totalRebufferCount);

有些值totalVideoFormatHeightTimeProduct仅在计算派生的摘要指标(例如平均视频高度)时才有用,但需要将它们正确组合PlaybackStats在一起。

汇总多个回放的分析数据

您可以PlaybackStats通过调用将多个组合在一起 PlaybackStats.merge。结果PlaybackStats将包含所有合并回放的聚合数据。请注意,它不会包含各个播放事件的历史记录,因为这些事件无法汇总。

PlaybackStatsListener.getCombinedPlaybackStats可用于获取的生命周期内收集的所有分析数据的汇总视图 PlaybackStatsListener

计算的摘要指标

除了基本的分析数据外,PlaybackStats还提供了许多计算汇总指标的方法。

Log.d("DEBUG", "Additional calculated summary metrics: "
    + "average video bitrate = " + playbackStats.getMeanVideoFormatBitrate()
    + ", mean time between rebuffers = "
        + playbackStats.getMeanTimeBetweenRebuffers());

进阶主题

将分析数据与回放元数据相关联

在收集单个回放的分析数据时,您可能希望将回放分析数据与有关正在播放的媒体的元数据相关联。

建议使用设置特定于媒体的元数据MediaItem.Builder.setTag。媒体标签是EventTime原始事件报告的一部分,何时 PlaybackStats结束,因此在处理相应的分析数据时可以很容易地检索到它:

new PlaybackStatsListener(
    /* keepHistory= */ false, (eventTime, playbackStats) -> {
      Object mediaTag =
          eventTime.timeline.getWindow(eventTime.windowIndex, new Window())
              .mediaItem.playbackProperties.tag;
      // Report playbackStats with mediaTag metadata.
    });

报告自定义分析事件

如果需要将自定义事件添加到分析数据中,则需要将这些事件保存在自己的数据结构中,并将其与PlaybackStats以后报告的数据合并 。如果有帮助,您可以扩展AnalyticsCollector为能够EventTime为您的自定义事件生成实例,并将其发送给已注册的侦听器,如以下示例所示。

interface ExtendedListener extends AnalyticsListener {
  void onCustomEvent(EventTime eventTime);
}

class ExtendedCollector extends AnalyticsCollector {
 public void customEvent() {
   EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
   sendEvent(eventTime, CUSTOM_EVENT_ID, listener -> {
     if (listener instanceof ExtendedListener) {
       ((ExtendedListener) listener).onCustomEvent(eventTime);
     }
   });
 }
}

// Usage - Setup and listener registration.
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context)
    .setAnalyticsCollector(new ExtendedCollector())
    .build();
player.addAnalyticsListener(new ExtendedListener() {
  @Override
  public void onCustomEvent(EventTime eventTime) {
    // Save custom event for analytics data.
  }
});
// Usage - Triggering the custom event.
((ExtendedCollector) player.getAnalyticsCollector()).customEvent();

 

 


 

  • 24
    点赞
  • 150
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

申小东001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值