ExoPlayer 支援多種容器格式的 HLS。此外,系統也必須支援內含的音訊和影片樣本格式 (詳情請參閱樣本格式一節)。我們強烈建議 HLS 內容製作人按照這篇網誌文章所述,產生高品質的 HLS 串流。
| 功能 | 支援 | 註解 |
|---|---|---|
| 容器 | ||
| MPEG-TS | 是 | |
| FMP4/CMAF | 是 | |
| ADTS (AAC) | 是 | |
| MP3 | 是 | |
| 隱藏式輔助字幕 / 字幕 | ||
| CEA-608 | 是 | |
| CEA-708 | 是 | |
| WebVTT | 是 | |
| 中繼資料 | ||
| ID3 | 是 | |
| SCTE-35 | 否 | |
| 內容保護 | ||
| AES-128 | 是 | |
| 範例 AES-128 | 否 | |
| Widevine | YES | API 19+ (cenc 配置) 和 25+ (cbcs 配置) |
| PlayReady SL2000 | 是 | 僅 Android TV |
| 伺服器控制項 | ||
| 差異更新 | 是 | |
| 封鎖播放清單重新載入 | 是 | |
| 封鎖預先載入提示的載入作業 | YES | 長度未定義的位元組範圍除外 |
| 廣告插播 | ||
| 伺服器導引廣告插播 (插頁式廣告) | 部分支援 | 僅限 X-ASSET-URI 的 VOD。直播和X-ASSET-LIST將於日後新增。 |
| IMA 伺服器端和用戶端廣告 | YES | 廣告插播指南 |
| 即時播放 | ||
| 一般直播播放 | 是 | |
| 低延遲 HTTP 即時串流 (Apple) | 是 | |
| 低延遲 HLS (社群) | 否 | |
| 通用媒體用戶端資料 CMCD | YES | CMCD 整合指南 |
使用 MediaItem
如要播放 HLS 串流,您必須依附 HLS 模組。
Kotlin
implementation("androidx.media3:media3-exoplayer-hls:1.11.1")
Groovy
implementation "androidx.media3:media3-exoplayer-hls:1.11.1"
接著,您可以為 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. } } });
播放含有插頁式廣告的 HTTP 即時串流
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);
完成這項播放器設定後,只要在播放器上設定含有 AdsConfiguration 的媒體項目,即可播放 HLS 插頁式廣告:
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 a 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 生命週期
HlsInterstitialsAdsLoader 或 HlsInterstitialsAdsLoader.AdsMediaSourceFactory 的例項可重複用於多個播放器例項,這些播放器例項會建立多個需要載入廣告的媒體來源。
舉例來說,您可以在 Activity 的 onCreate 方法中建立執行個體,然後在多個播放器執行個體中重複使用。只要同時由單一播放器執行個體使用,就能正常運作。這項功能適用於常見用途,例如應用程式進入背景時,播放器執行個體會遭到刪除,並在應用程式再次進入前景時建立新的執行個體。
在廣告播放狀態下繼續播放
為避免使用者重複觀看廣告,您可以在重新建立播放器時,儲存並還原廣告播放狀態。方法是在播放器即將釋出時呼叫 getAdsResumptionStates,並儲存傳回的 AdsResumptionState 物件。重新建立播放器時,只要在廣告載入器例項上呼叫 addAdResumptionState,即可還原狀態。AdsResumptionState 可繫結,因此可以儲存在 Activity 的 onSaveInstanceState 繫結中。請注意,只有 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 @Suppress("DEPRECATION") // getParcelableArrayList without class type is deprecated in API 33+ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Create the ads loader instance. hlsInterstitialsAdsLoader = HlsInterstitialsAdsLoader(this) // Restore ad resumption states. val bundles = if (Build.VERSION.SDK_INT >= 33) { savedInstanceState?.getParcelableArrayList(ADS_RESUMPTION_STATE_KEY, Bundle::class.java) } else { savedInstanceState?.getParcelableArrayList(ADS_RESUMPTION_STATE_KEY) } adsResumptionStates = bundles?.map(HlsInterstitialsAdsLoader.AdsResumptionState::fromBundle) } 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"; private @Nullable HlsInterstitialsAdsLoader hlsInterstitialsAdsLoader; private @Nullable PlayerView playerView; private @Nullable ExoPlayer player; private List<HlsInterstitialsAdsLoader.AdsResumptionState> adsResumptionStates; @SuppressWarnings( "deprecation") // getParcelableArrayList without class type is deprecated in API 33+ @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; if (Build.VERSION.SDK_INT >= 33) { bundles = savedInstanceState.getParcelableArrayList(ADS_RESUMPTION_STATE_KEY, Bundle.class); } else { 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 內容。如需完整說明,請參閱這篇 Medium 文章,瞭解 ExoPlayer 中的 HLS 播放功能。主要重點如下:
- 使用精確的片段長度。
- 使用連續媒體串流,避免各區隔的媒體結構發生變化。
- 使用
#EXT-X-INDEPENDENT-SEGMENTS標記。 - 建議使用解多工串流,而非同時包含影片和音訊的檔案。
- 在多變化版本播放清單中加入所有資訊。
以下是直播專屬規範:
- 使用
#EXT-X-PROGRAM-DATE-TIME標記。 - 使用
#EXT-X-DISCONTINUITY-SEQUENCE標記。 - 提供較長的直播時間。一分鐘以上最好。