ExoPlayer详解——高级主题(官方文档)

一、数字版权管理

ExoPlayer使用Android的MediaDrmAPI来支持受DRM保护的播放。不同支持的DRM方案所需的最低Android版本以及支持它们的流式格式为:

DRM格式Android
版本号
Android
API级别
支持的格式
Widevine“cenc”4.419DASH,HLS(仅限FMP4)
Widevine“cbcs”,“cbc1”和“cens”7.125DASH,HLS(仅限FMP4)
ClearKey521DASH
PlayReady SL2000AndroidTVAndroidTVDASH,SmoothStreaming,HLS(仅限FMP4)

为了使用ExoPlayer播放受DRM保护的内容,您的应用必须在实例化播放器时注入DrmSessionManagerExoPlayerFactory提供允许这样做的方法。

该库提供了一个名为DrmSessionManager的默认实现, DefaultDrmSessionManager适用于大多数用例。在主应用程序PlayerActivity中演示如何``在实例化播放器时创建和注入DefaultDrmSessionManager。

对于某些用例,可能需要进行其他配置,如以下部分所述。

1、Key rotation

要使用Key rotation(个人觉得是“转换密钥”的意思)播放流,必须在实例化DefaultDrmSessionManager时将multiSession构造函数参数设置为true

已知问题#4133 - 当key rotation发生时,播放可能会有轻微的暂停。

已知问题#3561 - 在API级别22及以下,当Key rotation发生时,输出表面可能会闪烁。

2、Multi-key content

多密钥内容由多个流组成,其中一些流使用与其他流不同的密钥。可以通过两种方式之一播放多密钥内容,具体取决于许可证服务器的配置方式。

  • 案例1:许可证服务器响应内容的所有密钥

在这种情况下,许可证服务器配置为当它收到对一个密钥的请求时,它会响应内容的所有密钥。这种情况由ExoPlayer处理,无需任何特殊配置。流之间的适应(例如SD和HD视频)即使使用不同的密钥也是无缝的。

在可能的情况下,我们建议您将许可证服务器配置为以这种方式运行。它是支持多密钥内容回放的最有效和最强大的方式,因为它不需要客户端发出多个许可证请求来访问不同的流。

  • 案例2:许可证服务器仅响应请求的密钥

在这种情况下,许可证服务器配置为仅响应请求中指定的密钥。通过在实例化DefaultDrmSessionManager时将multiSession构造函数参数设置为true可以使用此许可证服务器配置来播放多密钥内容。

我们不建议您将许可证服务器配置为以这种方式运行。它需要额外的许可证请求来播放多密钥内容,这比上述替代方案效率低且稳健。

已知问题#4133 - 使用此许可证服务器配置时,在使用不同密钥的流之间进行调整时,播放可能会略有暂停。

3、Offline keys

DefaultDrmSessionManager可以通过设置offlineLicenseKeySetId构造函数参数来加载脱机密钥集合。这允许使用存储在具有指定id的离线密钥集合中的密钥进行回放。

已知问题#3872 - 每次播放只能指定一个离线密钥集合。因此,仅当如上面的情况1中所述配置许可证服务器时,才支持多密钥内容的离线回放。



二、故障排除

  • 为什么某些媒体文件不可搜索?

默认情况下,ExoPlayer不支持在媒体中搜索,其中执行准确搜索操作的唯一方法是播放器扫描并索引整个文件。ExoPlayer认为此类文件不可见。大多数现代媒体容器格式包括用于搜索的元数据(例如,样本索引),具有良好定义的搜索算法(例如,针对Ogg的内插二分搜索),或指示其内容是恒定比特率。在这些情况下,ExoPlayer可以实现高效的搜索操作并提供支持。

如果您需要搜索但有不可搜索的媒体,我们建议您将内容转换为使用更合适的容器格式。对于MP3,ADTS和AMR文件,还可以使假设文件具有恒定比特率下求,如所描述 这里

  • 为什么有些MPEG-TS文件无法播放?

某些MPEG-TS文件不包含访问单元分隔符(AUD)。默认情况下,ExoPlayer依靠AUD来便宜地检测帧边界。同样,某些MPEG-TS文件不包含IDR关键帧。默认情况下,这些是ExoPlayer考虑的唯一关键帧类型。

当被要求播放缺少AUD或IDR关键帧的MPEG-TS文件时,ExoPlayer似乎会陷入缓冲状态。如果需要播放此类文件,可以分别使用FLAG_DETECT_ACCESS_UNITSFLAG_ALLOW_NON_IDR_KEYFRAMES。这些标志可以使用setTsExtractorFlags设置DefaultExtractorsFactory 。除了相对于基于AUD的帧边界检测而言计算上昂贵之外,使用 FLAG_DETECT_ACCESS_UNITS没有副作用。使用FLAG_ALLOW_NON_IDR_KEYFRAMES可能会在播放开始时以及在播放某些MPEG-TS文件时的搜索后立即导致暂时的视觉损坏。

  • 为什么有些MP4 / FMP4文件播放不正确?

某些MP4 / FMP4文件包含编辑列表,可通过跳过,移动或重复样本列表来重写媒体时间轴。ExoPlayer部分支持应用编辑列表。例如,它可以从同步样本开始延迟或重复样本组,但它不会截断音频样本或预卷媒体以进行不在同步样本上启动的编辑。

如果您看到部分媒体意外丢失或重复,请尝试设置Mp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTSFragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS,这将使提取器完全忽略编辑列表。这些文件可以使用setMp4ExtractorFlagssetFragmentedMp4ExtractorFlags设置DefaultExtractorsFactory

  • 为什么有些流会因HTTP响应代码301或302而失败?

HTTP响应代码301和302都指示重定向。可以在维基百科上找到简要描述。当ExoPlayer发出请求并收到状态码为301或302的响应时,它通常会遵循重定向并正常开始播放。默认情况下不会发生这种情况的一种情况是跨协议重定向。跨协议重定向是从HTTPS重定向到HTTP或反之亦然(或者不太常见,在另一对协议之间)。您可以使用wget命令行工具测试URL是否导致跨协议重定向,如下所示:

    wget "https://yourserver.com/test.mp3" 2>&1  | grep Location

输出应该如下所示:

    $ wget "https://yourserver.com/test.mp3" 2>&1  | grep Location
    Location: https://second.com/test.mp3 [following]
    Location: http://third.com/test.mp3 [following]

在此示例中,有两个重定向。第一个重定向是从 https://yourserver.com/test.mp3到https://second.com/test.mp3。两者都是HTTPS,因此这不是跨协议重定向。第二重定向是从 https://second.com/test.mp3http://third.com/test.mp3。这会从HTTPS重定向到HTTP,因此是跨协议重定向。ExoPlayer在默认配置中不会遵循此重定向,这意味着播放将失败。

如果需要,可以在实例化ExoPlayer应用程序中HttpDataSource.Factory使用的实例时,将ExoPlayer配置为遵循跨协议重定向。DefaultHttpDataSourceFactory具有为此目的接受参数的构造函数allowCrossProtocolRedirects,其他 HttpDataSource.Factory实现也是如此。将这些参数设置为true以启用跨协议重定向。

  • 为什么有些流因UnrecognizedInputFormatException而失败?

此问题与表单的播放失败有关:

    UnrecognizedInputFormatException: None of the available extractors
    (MatroskaExtractor, FragmentedMp4Extractor, ...) could read the stream.

这种失败有两种可能的原因。最常见的原因是您尝试使用ProgressiveMediaSource播放DASH(mpd),HLS(m3u8)或SmoothStreaming(ism,isml)内容。要播放这些流,你必须使用正确MediaSource的实现,这分别是DashMediaSourceHlsMediaSourceSsMediaSource。如果您不知道媒体的类型,那么经常可以使用Util.inferContentType,如 演示应用程序ExoPlayer中PlayerActivity所示。

第二个不常见的原因是,ExoPlayer不支持您尝试播放的媒体的容器格式。在这种情况下,故障按预期工作,但随意向我们的问题跟踪器提交功能请求 ,包括容器格式和测试流的详细信息。请在提交新功能之前搜索现有功能请求。

  • 为什么setPlaybackParameters在某些设备上无法正常工作?

在Android M及更早版本上运行应用程序的调试版本时,使用setPlaybackParameters API 时可能会遇到性能不稳定,可听见的工件和高CPU利用率。这是因为对于在这些Android版本上运行的调试版本,禁用了对此API很重要的优化。

请务必注意,此问题仅影响调试版本。它并不会影响发布版本,使这种优化始终启用。因此,您向最终用户提供的版本不应受此问题的影响。

  • “在错误的线程上访问播放器”警告意味着什么?

如果您看到此警告,则应用程序中的某些代码正在错误的线程上访问 SimpleExoPlayer(请检查报告的堆栈跟踪!)。只需从单个线程访问ExoPlayer实例。在大多数情况下,这应该是应用程序的主线程。有关详细信息,请阅读ExoPlayer Javadoc的“线程模型”部分

  • 如何修复“意外状态行:ICY 200 OK”?

如果服务器响应包含ICY状态行,而不是符合HTTP的状态行,则可能会出现此问题。不推荐使用ICY状态行,因此如果您控制服务器,则应更新它以提供符合HTTP的响应。如果你不能这样做,那么使用 OkHttp扩展将解决问题,因为它能够正确处理ICY状态行。

  • 如何查询正在播放的流是否为直播流?

您可以查询ExoPlayer的isCurrentWindowDynamic方法。动态窗口意味着正在播放的流是直播流。

  • 当我的应用程序背景化时,如何保持音频播放?

当您的应用在后台时,您需要采取一些步骤来确保继续播放音频:

您需要有一个正在运行的前台服务。这可以防止系统终止您的进程以释放资源。
您需要持有WifiLockWakeLock。这些确保系统保持WiFi无线电和CPU唤醒。
一旦音频不再播放,停止服务并释放锁定非常重要。



三、定制

ExoPlayer库的核心是ExoPlayer界面。一个ExoPlayer公开的传统高层次的媒体播放器功能,比如缓冲介质,播放,暂停和寻求的能力。实现的目的是对正在播放的媒体类型,存储方式和位置以及如何呈现媒体类型做出一些假设(并因此施加很少的限制)。实现不是直接ExoPlayer实现媒体的加载和呈现,而是将此工作委托给创建播放器或准备播放时注入的组件。所有ExoPlayer实现共有的组件是:

  • MediaSource定义要播放的媒体,加载媒体,并从中读取加载的媒体。 MediaSource在播放开始时被ExoPlayer.prepare注入。
  • Renderers渲染媒体的各个组成部分。在创建播放器时注入Renderer
  • TrackSelector选择由每个MediaSource可用Renderer提供的轨道。创建播放器时注入TrackSelector.
  • LoadControl控制MediaSource何时缓冲更多媒体,以及缓冲多少媒体。创建播放器时注入LoadControl.

该库为常见用例提供了这些组件的默认实现。一个ExoPlayer可以使用这些组件,但也可以使用自定义的实现是否需要非标准行为建造。自定义实现的一些用例是:

  • Renderer——您可能希望实现自定义Renderer来处理库提供的默认实现不支持的媒体类型。
  • TrackSelector——实现自定义TrackSelector允许应用程序开发人员更改选择由每个MediaSource可用Renderer消耗的轨道的方式。
  • LoadControl——实施自定义LoadControl允许应用开发者更改播放器的缓冲策略。
  • Extractor——如果您需要实现支持库当前不支持的容器格式,请考虑实现一个自定义Extractor类,然后可以将该类用于ProgressiveMediaSource播放该类型的媒体。
  • MediaSource——如果您希望获得以自定义MediaSource方式提供给渲染器的媒体示例,或者您希望实现自定义MediaSource合成行为,则实现自定义类可能是合适的。
  • DataSource——ExoPlayer的上游包已经包含了许多DataSource针对不同用例的 实现。您可能希望实现自己的DataSource类以另一种方式加载数据,例如通过自定义协议,使用自定义HTTP堆栈或自定义持久缓存。

整个库中都存在注入实现播放器功能的组件的概念。组件的默认实现委托工作以进一步注入组件。这允许使用自定义实现单独替换许多子组件。例如,默认MediaSource实现需要通过自己的工厂注入一个或多个DataSource工厂。通过提供自定义DataSource工厂,可以从非标准源或通过不同的网络堆栈加载数据。

构建自定义组件时,建议执行以下操作:

  • 如果自定义组件需要将事件报告回应用程序,我们建议您使用与现有ExoPlayer组件相同的模型执行此操作,其中事件侦听器与Handler一起传递给组件的构造函数。
  • 我们建议自定义组件使用与现有ExoPlayer组件相同的模型,以允许应用程序在播放期间重新配置,如下面的部分所述。为此,自定义组件应在handleMessage方法中实现 PlayerMessage.Target和接收配置更改。应用程序代码应通过调用ExoPlayer的createMessage方法,配置消息并使用PlayerMessage.send发送到组件来传递配置更改。

1、将消息发送到组件

可以向ExoPlayer组件发送消息。这些可以使用ExoPlayer.createMessage创建,然后使用PlayerMessage.send发送。默认情况下,消息尽快在回放线程上传递,但可以通过设置另一个回调线程(使用 PlayerMessage.setHandler)或指定传送回放位置(使用PlayerMessage.setPosition)来自定义。发送要在回放线程上传递的消息确保它们按照在播放器上执行的任何其他操作的顺序执行。

大多数ExoPlayer的开箱即用渲染器都支持允许在播放期间更改其配置的消息。例如,音频渲染器接受消息来设置音量,视频渲染器接受消息来设置曲面。这些消息应在回放线程上传递,以确保线程安全。



四、电池消耗

1、媒体播放导致电池消耗有多重要?

避免不必要的电池消耗是开发优秀Android应用程序的一个重要方面。媒体播放可能是电池耗尽的主要原因,但其对特定应用的重要性在很大程度上取决于其使用模式。如果应用程序每天仅用于播放少量媒体,则相应的电池消耗将仅占设备总消耗的一小部分。在这种情况下,在选择使用哪个播放器时,优先考虑功能集和可靠性优于电池是有意义的。另一方面,如果应用程序通常每天用于播放大量媒体,那么在选择多种可行选项时,应优先考虑优化电池消耗。

2、ExoPlayer的功效如何?

Android设备和媒体内容生态系统的多样性意味着很难对ExoPlayer的电池消耗做出广泛适用的陈述,特别是它与Android的 MediaPlayer API的比较。绝对和相对性能都因硬件,Android版本和正在播放的媒体而异。因此,下面提供的信息应仅作为指导。

(1)、视频回放

对于视频播放,我们的测量显示ExoPlayer和MediaPlayer消耗的功率相近。在两种情况下,显示和解码视频流所需的功率是相同的,并且这些功能占回放期间消耗的大部分功率。

无论使用哪种媒体播放器,在SurfaceView输出和TextureView输出之间进行选择都会对功耗产生重大影响。 SurfaceView更高效,TextureView在视频播放过程中总功耗增加了30%。因此,应尽可能优先考虑SurfaceView。了解更多关于SurfaceViewTextureView之间进行选择 ,并在这里

以下是在Pixel 2上播放1080p和480p视频时的一些功耗测量,使用Monsoon功率监视器测量。如上所述,这些数字不应用于得出有关Android设备和媒体内容生态系统功耗的一般结论。

媒体播放器ExoPlayer
SurfaceView1080p202毫安
TextureView1080p219毫安
SurfaceView480p194毫安
TextureView480p212毫安

(2)、音频播放

对于音频播放,我们的测量表明 ExoPlayer 可以比 MediaPlayer 消耗更多的功率。对于支持音频转移的设备尤其如此,它允许将音频处理从CPU转移到专用信号处理器。MediaPlayer能够利用音频转移来降低功耗,而ExoPlayer则不能,因为Android框架中没有用于启用它的公共API。请注意,这也意味着任何其他应用程序级别的媒体播放器或AudioTrack直接使用的应用程序都无法使用音频转移。

是否值得增加ExoPlayer的稳健性,灵活性和功能集超过MediaPlayer仅用于音频的用例的功耗是应用程序开发人员必须决定的,同时考虑他们的要求和应用程序使用模式。

Android Q中的新公共API将使ExoPlayer以及其他应用程序级媒体播放器能够利用音频转移功能。我们计划在未来的ExoPlayer版本中使用这些API。



五、APK收缩

最小化APK大小是开发优秀Android应用程序的一个重要方面。在针对发展中市场以及开发Android Instant App时尤其如此。对于这种情况,可能需要最小化APK中包含的ExoPlayer库的大小。本页概述了一些有助于实现此目的的简单步骤。

1、使用模块化依赖项

使用ExoPlayer最方便的方法是向完整库添加依赖项:

    implementation 'com.google.android.exoplayer:exoplayer: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'

1、使用ProGuard并缩小资源

通过在应用模块的build.gradle文件中启用ProGuard,可以删除应用未使用的类:

    buildTypes {
       release {
           minifyEnabled true
           shrinkResources true
           useProguard true
           proguardFiles = [
               getDefaultProguardFile('proguard-android.txt'),
               'proguard-rules.pro'
           ]
       }
    }

ExoPlayer的结构允许ProGuard删除未使用的功能。例如,对于播放DASH内容的应用,ExoPlayer对APK大小的贡献可以减少大约40%。

shrinkResources在app模块的build.gradle文件中启用可以进一步减小尺寸。

2、指定您的应用需要哪些提取器

如果您的应用使用ProgressiveMediaSource,请注意默认情况下会使用 DefaultExtractorsFactory。DefaultExtractorsFactory取决于ExoPlayer库中提供的所有Extractor\实现,因此ProGuard不会删除它们。如果您知道您的应用只需要播放少量容器格式,则可以指定自己的容器格式ExtractorsFactory。例如,只需要播放mp4文件的应用程序可以定义如下工厂:

    private class Mp4ExtractorsFactory implements ExtractorsFactory {
      @Override
      public Extractor[] createExtractors() {
          return new Extractor[] {new Mp4Extractor()};
      }
    }

并在实例化ProgressiveMediaSource实例时使用它,例如:

    new ProgressiveMediaSource.Factory(
            mediaDataSourceFactory, new Mp4ExtractorsFactory())
        .createMediaSource(uri);

这将允许ExtractorProGuard删除其他实现,这可以显着减小大小。

3、指定您的应用需要哪些渲染器

如果您的应用使用SimpleExoPlayer,请注意默认情况下将使用播放器的渲染器创建DefaultRenderersFactoryDefaultRenderersFactory取决于ExoPlayer库中提供的所有Renderer实现,因此ProGuard不会删除它们。如果您知道您的应用只需要渲染器的子集,则可以指定自己的渲染器RenderersFactory。例如,只播放音频的应用可以定义工厂,如:

    private class AudioOnlyRenderersFactory implements RenderersFactory {

      private final Context context;

      public AudioOnlyRenderersFactory(Context context) {
        this.context = context;
      }

      @Override
      public Renderer[] createRenderers(
          Handler eventHandler,
          VideoRendererEventListener videoRendererEventListener,
          AudioRendererEventListener audioRendererEventListener,
          TextOutput textRendererOutput,
          MetadataOutput metadataRendererOutput,
          DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
        return new Renderer[] {new MediaCodecAudioRenderer(
            MediaCodecSelector.DEFAULT, eventHandler, audioRendererEventListener)};
      }

    }

并在实例化SimpleExoPlayer实例时使用它,例如:

    SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(
        context, new AudioOnlyRenderersFactory(context), trackSelector);

这将允许ProGuard删除其Renderer他实现。在此特定示例中,视频,文本和元数据渲染器将被删除。



六、OEM测试

ExoPlayer被大量Android应用程序使用。作为OEM,确保ExoPlayer在新设备和现有设备的新平台构建上都能正常工作非常重要。此页面介绍了我们建议在运送设备或平台OTA之前运行的兼容性测试,以及运行它们时遇到的一些常见故障模式。

1、运行测试

要运行ExoPlayer的回放测试,首先从GitHub查看最新版本的ExoPlayer 。然后,您可以从命令行或Android Studio运行测试。

(1)、命令行

从根目录,构建并安装回放测试:

    ./gradlew :playbacktests:installDebug

接下来,在GTS包中运行回放测试:

adb shell am instrument -w -r -e debug false \
  -e package com.google.android.exoplayer2.playbacktests.gts \
  com.google.android.exoplayer2.playbacktests.test/android.test.InstrumentationTestRunner

测试结果显示在STDOUT中。

(2)、Android Studio

打开ExoPlayer项目,导航到该playbacktests模块,右键单击该gts文件夹并运行测试。测试结果显示在Android Studio的“运行”窗口中。

2、常见故障模式

下面介绍运行ExoPlayer播放测试时遇到的一些常见故障模式,以及每种情况下可能的根本原因。我们将添加到此列表中,因为会发现更多故障模式。

(1)、意外的视频缓冲区演示时间戳

Logcat将包含类似于的错误:

    Caused by: java.lang.IllegalStateException: Expected to dequeue video buffer
    with presentation timestamp: 134766000. Instead got: 134733000 (Processed
    buffers since last flush: 2242).

这种故障通常是由于被测视频解码器错误地丢弃,插入或重新排序缓冲区引起的。在上面的示例中,测试期望134766000从MediaCodec.dequeueOutputBuffer具有显示时间戳的缓冲区出列 ,但发现它使用呈现时间戳使缓冲区出列134733000。我们建议您在遇到此故障时检查解码器实现,特别是它正确处理自适应分辨率开关而不丢弃任何缓冲区。

(2)、丢失的缓冲区太多了

Logcat将包含类似于的错误:

    junit.framework.AssertionFailedError: Codec(DashTest:Video) was late decoding:
    200 buffers. Limit: 25.

这种故障是性能问题,其中被测视频解码器对大量缓冲器进行后期解码。在上面的示例中,ExoPlayer丢弃了200个缓冲区,因为它们在出列时已经迟到了,对于一个限制为25的测试。最明显的原因是视频解码器解码缓冲区太慢。如果故障仅发生在播放Widevine受保护内容的测试子集中,则缓冲区解密的平台操作可能太慢。我们建议检查这些组件的性能,并查看是否可以进行任何优化以加快它们的速度。

(3)、无法验证本机窗口

Logcat将包含类似于的错误:

    SurfaceUtils: native window could not be authenticated
    ExoPlayerImplInternal: Internal runtime error.
    ExoPlayerImplInternal: android.media.MediaCodec$CodecException: Error 0xffffffff

此失败表示平台未能正确设置安全位标志。

(4)、测试超时

Logcat将包含类似于的错误:

AssertionFailedError: Test timed out after 300000 ms.

此故障通常是由测试运行期间网络连接不良引起的。如果设备出现有良好的网络连接,然后它可能是在测试卡住调用到一个平台组件(例如 MediaCodecMediaDrmAudioTrack等)。检查测试过程中线程的调用堆栈以确定是否是这种情况。



七、设计RFC状态的文档


评论区

  目录