ExoPlayer 支持采用多种容器格式的 HLS。所含音频和视频样本格式也必须受支持(有关详情,请参阅样本格式部分)。我们强烈建议 HLS 内容制作方生成高质量的 HLS 流,如这篇博文中所述。
| 功能 | 支持 | 评论 |
|---|---|---|
| 容器 | ||
| MPEG-TS | 是 | |
| FMP4/CMAF | 是 | |
| ADTS (AAC) | 是 | |
| MP3 | 是 | |
| 字幕 | ||
| CEA-608 | 是 | |
| CEA-708 | 是 | |
| WebVTT | 是 | |
| 元数据 | ||
| ID3 | 是 | |
| SCTE-35 | 否 | |
| 内容保护 | ||
| AES-128 | 是 | |
| Sample AES-128 | 否 | |
| Widevine | 是 | API 19 及更高版本(“cenc”方案)和 25 及更高版本(“cbcs”方案) |
| PlayReady SL2000 | 是 | 仅分发至 Android TV |
| 服务器控制 | ||
| 增量更新 | 是 | |
| 阻止播放列表重新加载 | 是 | |
| 阻止加载预加载提示 | 是 | 长度未定义的字节范围除外 |
| 广告插播 | ||
| 服务器引导的广告插播(插页式广告) | 部分支持 | 仅限包含 X-ASSET-URI 的 VOD。
直播和X-ASSET-LIST将在稍后添加。 |
| IMA 服务器端广告和客户端广告 | 是 | 广告插播指南 |
| 直播 | ||
| 常规直播播放 | 是 | |
| 低延迟 HLS (Apple) | 是 | |
| 低延迟 HLS(社区) | 否 | |
| 通用媒体客户端数据 CMCD | 是 | CMCD 集成指南 |
使用 MediaItem
如需播放 HLS 直播,您需要依赖于 HLS 模块。
Kotlin
implementation("androidx.media3:media3-exoplayer-hls:1.9.2")
Groovy
implementation "androidx.media3:media3-exoplayer-hls:1.9.2"
然后,您可以为 HLS 播放列表 URI 创建一个 MediaItem 并将其传递给播放器。
Kotlin
// Create a player instance. val player = ExoPlayer.Builder(context).build() // Set the media item to be played. player.setMediaItem(MediaItem.fromUri(hlsUri)) // Prepare the player. player.prepare()
Java
// Create a player instance. ExoPlayer player = new ExoPlayer.Builder(context).build(); // Set the media item to be played. player.setMediaItem(MediaItem.fromUri(hlsUri)); // Prepare the player. player.prepare();
如果您的 URI 不以 .m3u8 结尾,您可以将 MimeTypes.APPLICATION_M3U8 传递给 MediaItem.Builder 的 setMimeType,以明确指示内容类型。
媒体项的 URI 可以指向媒体播放列表,也可以指向多变体播放列表。如果 URI 指向声明了多个 #EXT-X-STREAM-INF 标记的多变体播放列表,则 ExoPlayer 会自动在变体之间进行调整,同时考虑可用带宽和设备功能。
使用 HlsMediaSource
如需更多自定义选项,您可以创建 HlsMediaSource 并将其直接传递给播放器,而不是传递 MediaItem。
Kotlin
// Create a data source factory. val dataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory() // Create a HLS media source pointing to a playlist uri. val hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(hlsUri)) // Create a player instance. val player = ExoPlayer.Builder(context).build() // Set the HLS media source as the playlist with a single media item. player.setMediaSource(hlsMediaSource) // Prepare the player. player.prepare()
Java
// Create a data source factory. DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory(); // Create a HLS media source pointing to a playlist uri. HlsMediaSource hlsMediaSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(hlsUri)); // Create a player instance. ExoPlayer player = new ExoPlayer.Builder(context).build(); // Set the HLS media source as the playlist with a single media item. player.setMediaSource(hlsMediaSource); // Prepare the player. player.prepare();
访问清单
您可以通过调用 Player.getCurrentManifest 来检索当前清单。
对于 HLS,您应将返回的对象转换为 HlsManifest。每当加载清单时,系统也会调用 Player.Listener 的 onTimelineChanged 回调。对于点播内容,这种情况只会发生一次;对于直播内容,这种情况可能会发生多次。以下代码段展示了应用如何在每次加载清单时执行某些操作。
Kotlin
player.addListener( object : Player.Listener { override fun onTimelineChanged( timeline: Timeline, @Player.TimelineChangeReason reason: Int, ) { val manifest = player.currentManifest if (manifest is HlsManifest) { // Do something with the manifest. } } } )
Java
player.addListener( new Player.Listener() { @Override public void onTimelineChanged( Timeline timeline, @Player.TimelineChangeReason int reason) { Object manifest = player.getCurrentManifest(); if (manifest != null) { HlsManifest hlsManifest = (HlsManifest) manifest; // Do something with the manifest. } } });
播放包含插页式广告的 HLS 流
HLS 规范定义了 HLS 插播广告,可用于在媒体播放列表中添加插播广告信息。ExoPlayer 默认会忽略这些插播广告。您可以使用 HlsInterstitialsAdsLoader 添加支持。我们不会从一开始就支持规范的所有功能。如果您发现自己的直播缺少支持,请在 GitHub 上提交问题并向我们发送您的直播 URI,以便我们为您的直播添加支持。
将 MediaItem 与 Playlist API 搭配使用
使用插播广告播放 HLS 直播的最便捷方式是使用 HlsInterstitialsAdsLoader.AdsMediaSourceFactory 构建 ExoPlayer 实例。
这样一来,便可以使用 Player 接口基于 MediaItem 的播放列表 API 来播放 HLS 插播广告。
构建播放器实例时,可以将 ExoPlayer 的 MediaSource.Factory 注入到构建器中:
Kotlin
val hlsInterstitialsAdsLoader = HlsInterstitialsAdsLoader(context) // Create a MediaSource.Factory for HLS streams with interstitials. val hlsMediaSourceFactory = HlsInterstitialsAdsLoader.AdsMediaSourceFactory( hlsInterstitialsAdsLoader, playerView, DefaultMediaSourceFactory(context), ) // Build player with interstitials media source factory val player = ExoPlayer.Builder(context).setMediaSourceFactory(hlsMediaSourceFactory).build() // Set the player on the ads loader. hlsInterstitialsAdsLoader.setPlayer(player) playerView.setPlayer(player)
Java
HlsInterstitialsAdsLoader hlsInterstitialsAdsLoader = new HlsInterstitialsAdsLoader(context); // Create a MediaSource.Factory for HLS streams with interstitials. MediaSource.Factory hlsMediaSourceFactory = new HlsInterstitialsAdsLoader.AdsMediaSourceFactory( hlsInterstitialsAdsLoader, playerView, new DefaultMediaSourceFactory(context)); // Build player with interstitials media source factory ExoPlayer player = new ExoPlayer.Builder(context).setMediaSourceFactory(hlsMediaSourceFactory).build(); // Set the player on the ads loader. hlsInterstitialsAdsLoader.setPlayer(player); playerView.setPlayer(player);
通过这种播放器设置,播放 HLS 插页式广告只需在播放器上设置一个带有 AdsConfiguration 的媒体项:
Kotlin
// Build an HLS media item with ads configuration to be played. player.setMediaItem( MediaItem.Builder() .setUri("https://www.example.com/media.m3u8") .setAdsConfiguration( MediaItem.AdsConfiguration.Builder("hls://interstitials".toUri()) .setAdsId("ad-tag-0") // must be unique within playlist .build() ) .build() ) player.prepare() player.play()
Java
// Build an HLS media item with ads configuration to be played. player.setMediaItem( new MediaItem.Builder() .setUri("https://www.example.com/media.m3u8") .setAdsConfiguration( new AdsConfiguration.Builder(Uri.parse("hls://interstitials")) .setAdsId("ad-tag-0") // must be unique within playlist .build()) .build()); player.prepare(); player.play();
使用基于媒体源的 API
或者,也可以在不替换默认媒体源工厂的情况下构建 ExoPlayer 实例。为了支持插页式广告,应用随后可以使用 HlsInterstitialsAdsLoader.AdsMediaSourceFactory 直接创建 MediaSource,并使用基于媒体源的播放列表 API 将其提供给 ExoPlayer:
Kotlin
val hlsInterstitialsAdsLoader = HlsInterstitialsAdsLoader(context) // Create a MediaSource.Factory for HLS streams with interstitials. val hlsMediaSourceFactory = HlsInterstitialsAdsLoader.AdsMediaSourceFactory( hlsInterstitialsAdsLoader, playerView, context, ) // Build player with default media source factory. val player = ExoPlayer.Builder(context).build() // Create an media source from an HLS media item with ads configuration. val mediaSource = hlsMediaSourceFactory.createMediaSource( MediaItem.Builder() .setUri("https://www.example.com/media.m3u8") .setAdsConfiguration( MediaItem.AdsConfiguration.Builder("hls://interstitials".toUri()) .setAdsId("ad-tag-0") .build() ) .build() ) // Set the media source on the player. player.setMediaSource(mediaSource) player.prepare() player.play()
Java
HlsInterstitialsAdsLoader hlsInterstitialsAdsLoader = new HlsInterstitialsAdsLoader(context); // Create a MediaSource.Factory for HLS streams with interstitials. MediaSource.Factory hlsMediaSourceFactory = new HlsInterstitialsAdsLoader.AdsMediaSourceFactory( hlsInterstitialsAdsLoader, playerView, context); // Build player with default media source factory. ExoPlayer player = new ExoPlayer.Builder(context).build(); // Create an media source from an HLS media item with ads configuration. MediaSource mediaSource = hlsMediaSourceFactory.createMediaSource( new MediaItem.Builder() .setUri("https://www.example.com/media.m3u8") .setAdsConfiguration( new MediaItem.AdsConfiguration.Builder(Uri.parse("hls://interstitials")) .setAdsId("ad-tag-0") .build()) .build()); // Set the media source on the player. player.setMediaSource(mediaSource); player.prepare(); player.play();
监听广告事件
可以将 Listener 添加到 HlsInterstitialsAdsLoader,以监控与 HLS 插播广告播放相关的状态更改事件。这可让应用或 SDK 跟踪已播放的广告、正在加载的素材资源列表、正在准备的广告媒体来源,或检测素材资源列表加载和广告准备错误。此外,还可以接收广告媒体来源发出的元数据,以进行精细的广告播放验证或跟踪广告播放进度。
Kotlin
class AdsLoaderListener : HlsInterstitialsAdsLoader.Listener { override fun onStart(mediaItem: MediaItem, adsId: Any, adViewProvider: AdViewProvider) { // Do something when HLS media item with interstitials is started. } override fun onMetadata( mediaItem: MediaItem, adsId: Any, adGroupIndex: Int, adIndexInAdGroup: Int, metadata: Metadata, ) { // Do something with metadata that is emitted by the ad media source of the given ad. } override fun onAdCompleted( mediaItem: MediaItem, adsId: Any, adGroupIndex: Int, adIndexInAdGroup: Int, ) { // Do something when ad completed playback. } // ... See JavaDoc for further callbacks of HlsInterstitialsAdsLoader.Listener. override fun onStop(mediaItem: MediaItem, adsId: Any, adPlaybackState: AdPlaybackState) { // Do something with the resulting ad playback state when stopped. } }
Java
@OptIn(markerClass = UnstableApi.class) private static class AdsLoaderListener implements HlsInterstitialsAdsLoader.Listener { // implement HlsInterstitialsAdsLoader.Listener @Override public void onStart(MediaItem mediaItem, Object adsId, AdViewProvider adViewProvider) { // Do something when HLS media item with interstitials is started. } @Override public void onMetadata( MediaItem mediaItem, Object adsId, int adGroupIndex, int adIndexInAdGroup, Metadata metadata) { // Do something with metadata that is emitted by the ad media source of the given ad. } @Override public void onAdCompleted( MediaItem mediaItem, Object adsId, int adGroupIndex, int adIndexInAdGroup) { // Do something when ad completed playback. } // ... See JavaDoc for further callbacks @Override public void onStop(MediaItem mediaItem, Object adsId, AdPlaybackState adPlaybackState) { // Do something with the resulting ad playback state when stopped. } }
如需查看所有可用回调的详细文档,请参阅 HlsInterstitialsAdsLoader.Listener 的 JavaDoc。
然后,可以将监听器添加到广告加载器:
Kotlin
val listener = AdsLoaderListener() // Add the listener to the ads loader to receive ad loader events. hlsInterstitialsAdsLoader.addListener(listener)
Java
AdsLoaderListener listener = new AdsLoaderListener(); // Add the listener to the ads loader to receive ad loader events. hlsInterstitialsAdsLoader.addListener(listener);
HlsInterstitialsAdsLoader lifecycle
HlsInterstitialsAdsLoader 或 HlsInterstitialsAdsLoader.AdsMediaSourceFactory 的实例可重复用于多个播放器实例,这些实例会创建多个媒体源,必须为这些媒体源加载广告。
例如,可以在 Activity 的 onCreate 方法中创建一个实例,然后将其重复用于多个播放器实例。只要同时只有一个播放器实例在使用它,它就可以正常工作。当应用进入后台时,播放器实例会被销毁,然后当应用再次进入前台时,会创建一个新实例,这对于常见的使用情形非常有用。
通过广告播放状态继续播放
为避免用户必须重新观看广告,可以在重新创建播放器时保存并恢复广告播放状态。具体方法是,在即将释放播放器时调用 getAdsResumptionStates(),并存储返回的 AdsResumptionState 对象。当播放器重新创建时,可以通过对广告加载器实例调用 addAdResumptionState() 来恢复状态。AdsResumptionState 可打包,因此可以存储在 Activity 的 onSaveInstanceState bundle 中。请注意,广告恢复功能仅适用于 VOD 流
Kotlin
class HlsInterstitialsActivity : Activity() { companion object { const val ADS_RESUMPTION_STATE_KEY = "ads_resumption_state" } private var hlsInterstitialsAdsLoader: HlsInterstitialsAdsLoader? = null private var playerView: PlayerView? = null private var player: ExoPlayer? = null private var adsResumptionStates: List<HlsInterstitialsAdsLoader.AdsResumptionState>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Create the ads loader instance. hlsInterstitialsAdsLoader = HlsInterstitialsAdsLoader(this) // Restore ad resumption states. savedInstanceState?.getParcelableArrayList<Bundle>(ADS_RESUMPTION_STATE_KEY)?.let { bundles -> adsResumptionStates = bundles.map { HlsInterstitialsAdsLoader.AdsResumptionState.fromBundle(it) } } } override fun onStart() { super.onStart() // Build a player and set it on the ads loader. initializePlayer() hlsInterstitialsAdsLoader?.setPlayer(player) // Add any stored ad resumption states to the ads loader. adsResumptionStates?.forEach { hlsInterstitialsAdsLoader?.addAdResumptionState(it) } adsResumptionStates = null // Consume the states } override fun onStop() { super.onStop() // Get ad resumption states. adsResumptionStates = hlsInterstitialsAdsLoader?.adsResumptionStates releasePlayer() } override fun onDestroy() { // Release the ads loader when not used anymore. hlsInterstitialsAdsLoader?.release() hlsInterstitialsAdsLoader = null super.onDestroy() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) // Store the ad resumption states. adsResumptionStates?.let { outState.putParcelableArrayList( ADS_RESUMPTION_STATE_KEY, ArrayList(it.map(HlsInterstitialsAdsLoader.AdsResumptionState::toBundle)), ) } } fun initializePlayer() { if (player == null) { // Create a media source factory for HLS streams. val hlsMediaSourceFactory = HlsInterstitialsAdsLoader.AdsMediaSourceFactory( checkNotNull(hlsInterstitialsAdsLoader), playerView!!, /* context= */ this, ) // Build player with interstitials media source player = ExoPlayer.Builder(/* context= */ this) .setMediaSourceFactory(hlsMediaSourceFactory) .build() // Set the player on the ads loader. hlsInterstitialsAdsLoader?.setPlayer(player) playerView?.player = player } // Use a media item with an HLS stream URI, an ad tag URI and ads ID. player?.setMediaItem( MediaItem.Builder() .setUri("https://www.example.com/media.m3u8") .setMimeType(MimeTypes.APPLICATION_M3U8) .setAdsConfiguration( MediaItem.AdsConfiguration.Builder("hls://interstitials".toUri()) .setAdsId("ad-tag-0") // must be unique within ExoPlayer playlist .build() ) .build() ) player?.prepare() player?.play() } fun releasePlayer() { player?.release() player = null hlsInterstitialsAdsLoader?.setPlayer(null) playerView?.player = null } }
Java
@OptIn(markerClass = UnstableApi.class) public static class HlsInterstitialsActivity extends Activity { public static final String ADS_RESUMPTION_STATE_KEY = "ads_resumption_state"; @Nullable private HlsInterstitialsAdsLoader hlsInterstitialsAdsLoader; @Nullable private PlayerView playerView; @Nullable private ExoPlayer player; private List<HlsInterstitialsAdsLoader.AdsResumptionState> adsResumptionStates; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Create the ads loader instance. hlsInterstitialsAdsLoader = new HlsInterstitialsAdsLoader(this); // Restore ad resumption states. if (savedInstanceState != null) { ArrayList<Bundle> bundles = savedInstanceState.getParcelableArrayList(ADS_RESUMPTION_STATE_KEY); if (bundles != null) { adsResumptionStates = new ArrayList<>(); for (Bundle bundle : bundles) { adsResumptionStates.add( HlsInterstitialsAdsLoader.AdsResumptionState.fromBundle(bundle)); } } } } @Override protected void onStart() { super.onStart(); // Build a player and set it on the ads loader. initializePlayer(); // Add any stored ad resumption states to the ads loader. if (adsResumptionStates != null) { for (HlsInterstitialsAdsLoader.AdsResumptionState state : adsResumptionStates) { hlsInterstitialsAdsLoader.addAdResumptionState(state); } adsResumptionStates = null; // Consume the states } } @Override protected void onStop() { super.onStop(); // Get ad resumption states before releasing the player. if (hlsInterstitialsAdsLoader != null) { adsResumptionStates = hlsInterstitialsAdsLoader.getAdsResumptionStates(); } releasePlayer(); } @Override protected void onDestroy() { // Release the ads loader when not used anymore. if (hlsInterstitialsAdsLoader != null) { hlsInterstitialsAdsLoader.release(); hlsInterstitialsAdsLoader = null; } super.onDestroy(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Store the ad resumption states. if (adsResumptionStates != null) { ArrayList<Bundle> bundles = new ArrayList<>(); for (HlsInterstitialsAdsLoader.AdsResumptionState state : adsResumptionStates) { bundles.add(state.toBundle()); } outState.putParcelableArrayList(ADS_RESUMPTION_STATE_KEY, bundles); } } private void initializePlayer() { if (player == null) { // Create a media source factory for HLS streams. MediaSource.Factory hlsMediaSourceFactory = new HlsInterstitialsAdsLoader.AdsMediaSourceFactory( checkNotNull(hlsInterstitialsAdsLoader), playerView, /* context= */ this); // Build player with interstitials media source player = new ExoPlayer.Builder(/* context= */ this) .setMediaSourceFactory(hlsMediaSourceFactory) .build(); // Set the player on the ads loader. hlsInterstitialsAdsLoader.setPlayer(player); playerView.setPlayer(player); } // Use a media item with an HLS stream URI, an ad tag URI and ads ID. player.setMediaItem( new MediaItem.Builder() .setUri("https://www.example.com/media.m3u8") .setMimeType(MimeTypes.APPLICATION_M3U8) .setAdsConfiguration( new MediaItem.AdsConfiguration.Builder(Uri.parse("hls://interstitials")) .setAdsId("ad-tag-0") // must be unique within ExoPlayer playlist .build()) .build()); player.prepare(); player.play(); } private void releasePlayer() { if (player != null) { player.release(); player = null; } if (hlsInterstitialsAdsLoader != null) { hlsInterstitialsAdsLoader.setPlayer(null); } if (playerView != null) { playerView.setPlayer(null); } } }
自定义播放
ExoPlayer 提供了多种方法,可让您根据应用的需求量身定制播放体验。如需查看示例,请参阅自定义页面。
停用无块准备
默认情况下,ExoPlayer 将使用无块准备。这意味着 ExoPlayer 将仅使用多变体播放列表中的信息来准备视频流,如果 #EXT-X-STREAM-INF 标记包含 CODECS 属性,则此方法有效。
如果您的媒体片段包含未在多变体播放列表中使用 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS 标记声明的混合字幕轨道,您可能需要停用此功能。否则,系统将无法检测到这些字幕轨道,也无法播放。您可以在 HlsMediaSource.Factory 中停用无块准备,如以下代码段所示。请注意,这会增加启动时间,因为 ExoPlayer 需要下载媒体片段才能发现这些额外的轨道,因此最好在多变体播放列表中声明字幕轨道。
Kotlin
val hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory) .setAllowChunklessPreparation(false) .createMediaSource(MediaItem.fromUri(hlsUri))
Java
HlsMediaSource hlsMediaSource = new HlsMediaSource.Factory(dataSourceFactory) .setAllowChunklessPreparation(false) .createMediaSource(MediaItem.fromUri(hlsUri));
创建高品质 HLS 内容
为了充分利用 ExoPlayer,您可以遵循某些准则来改进 HLS 内容。如需了解完整说明,请参阅我们关于 ExoPlayer 中 HLS 播放的 Medium 博文。要点如下:
- 使用精确的细分时长。
- 使用连续的媒体流;避免在各个片段之间更改媒体结构。
- 使用
#EXT-X-INDEPENDENT-SEGMENTS标记。 - 偏好于解复用后的流,而不是同时包含视频和音频的文件。
- 在多变体播放列表中尽可能包含所有信息。
以下准则专门针对直播活动:
- 使用
#EXT-X-PROGRAM-DATE-TIME标记。 - 使用
#EXT-X-DISCONTINUITY-SEQUENCE标记。 - 提供较长的直播窗口。一分钟或更长时间效果更佳。