HLS

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.BuildersetMimeType,明確指出內容類型。

媒體項目的 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.ListeneronTimelineChanged 回呼。隨選內容只會發生一次,但直播內容可能會發生多次。下列程式碼片段說明應用程式如何在資訊清單載入時執行某些動作。

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.FactoryExoPlayer 可以注入建構工具:

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.ListenerJavaDoc。

接著,即可將監聽器新增至廣告載入器:

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 生命週期

HlsInterstitialsAdsLoaderHlsInterstitialsAdsLoader.AdsMediaSourceFactory 的執行個體可重複用於多個播放器執行個體,這些執行個體會建立多個媒體來源,必須載入廣告。

舉例來說,您可以在 ActivityonCreate 方法中建立執行個體,然後重複用於多個播放器執行個體。只要同時只有一個播放器執行個體使用,這項功能就能正常運作。如果應用程式進入背景,播放器例項就會遭到刪除,然後在應用程式再次進入前景時建立新例項,這時這個方法就非常實用。

使用廣告播放狀態繼續播放

為避免使用者必須重新觀看廣告,系統可在重新建立播放器時儲存並還原廣告播放狀態。方法是在即將發布播放器時呼叫 getAdsResumptionStates(),並儲存傳回的 AdsResumptionState 物件。重新建立播放器時,可以呼叫廣告載入器例項上的 addAdResumptionState(),還原狀態。AdsResumptionState 可打包,因此可以儲存在 ActivityonSaveInstanceState 套件中。請注意,廣告續播功能僅支援隨選視訊串流

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 標記。
  • 提供較長的時間範圍。建議至少一分鐘。