ExoPlayer 支援 HLS,並提供多種容器格式。內含的音訊和影片樣本格式也必須受到支援 (詳情請參閱「樣本格式」一節)。我們強烈建議 HLS 內容製作人產生高品質的 HLS 串流,詳情請參閱這篇網誌文章。
| 功能 | 支援 | 留言 |
|---|---|---|
| 容器 | ||
| MPEG-TS | 是 | |
| FMP4/CMAF | 是 | |
| ADTS (AAC) | 是 | |
| MP3 | 是 | |
| 隱藏式輔助字幕 / 字幕 | ||
| CEA-608 | 是 | |
| CEA-708 | 是 | |
| WebVTT | 是 | |
| Metadata | ||
| ID3 | 是 | |
| SCTE-35 | 否 | |
| 內容保護 | ||
| AES-128 | 是 | |
| AES-128 範例 | 否 | |
| Widevine | 是 | API 19 以上 (「cenc」架構) 和 25 以上 (「cbcs」架構) |
| PlayReady SL2000 | 是 | 僅 Android TV |
| 伺服器控制 | ||
| 差異更新 | 是 | |
| 封鎖播放清單重新載入 | 是 | |
| 封鎖預先載入提示的載入作業 | YES | 長度未定義的位元組範圍除外 |
| 廣告插播 | ||
| 伺服器導向廣告插入 (插頁式廣告) | 部分 | 僅限 VOD,且必須有 X-ASSET-URI。
直播和X-ASSET-LIST將於日後新增。 |
| IMA 伺服器端和用戶端廣告 | 是 | 廣告插播指南 |
| 即時播放 | ||
| 一般直播播放 | 是 | |
| 低延遲 HLS (Apple) | 是 | |
| 低延遲 HTTP 即時串流 (社群) | 否 | |
| 通用媒體用戶端資料 CMCD | YES | CMCD 整合指南 |
使用 MediaItem
如要播放 HTTP 即時串流,您必須依附於 HTTP 即時串流模組。
Kotlin
implementation("androidx.media3:media3-exoplayer-hls:1.8.0")
Groovy
implementation "androidx.media3:media3-exoplayer-hls:1.8.0"
接著,您可以為 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, @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 串流
HTTP 即時串流規格定義了 HTTP 即時串流插頁式廣告,可用於在媒體播放清單中加入插頁式廣告資訊。ExoPlayer 預設會忽略這些插頁式廣告。您可以使用 HlsInterstitialsAdsLoader 新增支援。我們不會在一開始就支援規格的所有功能。如果你的串流缺少支援,請在 GitHub 上提出問題,並傳送串流 URI 給我們,我們會為你的串流新增支援。
使用播放清單 API 的 MediaItem
如要以最方便的方式播放含有中插廣告的 HLS 串流,請使用 HlsInterstitialsAdsLoader.AdsMediaSourceFactory 建構 ExoPlayer 執行個體。這樣就能使用 Player 介面的 MediaItem 基礎播放清單 API 播放 HLS 插頁式廣告。
建構播放器例項時,MediaSource.Factory 的 ExoPlayer 可以注入建構工具:
Kotlin
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
player =
ExoPlayer.Builder(context)
.setMediaSourceFactory(hlsMediaSourceFactory)
.build()
// Set the player on the ads loader.
hlsInterstitialsAdsLoader.setPlayer(player)
playerView.setPlayer(player)
Java
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
player =
new ExoPlayer.Builder(context)
.setMediaSourceFactory(hlsMediaSourceFactory)
.build();
// Set the player on the ads loader.
hlsInterstitialsAdsLoader.setPlayer(player);
playerView.setPlayer(player);
完成這類播放器設定後,只要在播放器中設定含有 AdsConfiguration 的媒體項目,即可播放 HLS 插頁式廣告:
Kotlin
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
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 例項,而不覆寫預設的媒體來源 Factory。如要支援插頁式廣告,應用程式隨後可使用 HlsInterstitialsAdsLoader.AdsMediaSourceFactory 直接建立 MediaSource,並透過以媒體來源為基礎的播放清單 API 提供給 ExoPlayer:
Kotlin
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.
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.
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.
exoPlayer.setMediaSource(mediaSource);
exoPlayer.prepare();
exoPlayer.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
private 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 套件中。請注意,廣告續播功能僅支援隨選視訊串流
Kotlin
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?.setPlayer(null)
}
Java
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.
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);
}
}
您也可以使用 removeAdResumptionState(Object adsId) 移除個別續傳狀態,或使用 clearAllAdResumptionStates() 清除所有狀態。
一般來說,請務必先釋出舊的播放器例項,再於廣告載入器上設定下一個播放器例項。廣告載入器本身發布後,就無法再使用廣告載入器。
自訂播放設定
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標記。 - 提供較長的時間範圍。建議至少一分鐘。