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 YES API 19 以上 (「cenc」架構) 和 25 以上 (「cbcs」架構)
PlayReady SL2000 僅 Android TV
伺服器控制
差異更新
封鎖播放清單重新載入
封鎖預先載入提示的載入作業 YES 長度未定義的位元組範圍除外
廣告插播
伺服器導向的廣告插入 (插頁式廣告) 部分 僅限 VOD,且必須有 X-ASSET-URI。 直播和X-ASSET-LIST將於日後新增。
IMA 伺服器端和用戶端廣告 YES 廣告插播指南
即時播放
一般直播播放
低延遲 HTTP 即時串流 (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 playlist API 播放 HLS 插頁式廣告。

建構播放器例項時,MediaSource.FactoryExoPlayer 可以注入建構工具:

Kotlin

hlsInterstitialsAdsLoader = HlsInterstitialsAdsLoader(context)
// Create a MediaSource.Factory for HLS streams with interstitials.
var 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(
      AdsConfiguration.Builder(Uri.parse("hls://interstitials"))
        .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.
var hlsMediaSourceFactory =
  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.
val mediaSource =
  hlsMediaSourceFactory.createMediaSource(
    MediaItem.Builder()
      .setUri("https://www.example.com/media.m3u8")
      .setAdsConfiguration(
        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()

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

var 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(Uri.parse("hls://interstitials"))
          .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 播放 HTTP 即時串流文章。主要內容如下:

  • 使用精確的片段時間長度。
  • 使用連續媒體串流,避免在各個區段中變更媒體結構。
  • 使用 #EXT-X-INDEPENDENT-SEGMENTS 標記。
  • 請優先使用解多工串流,而非同時包含影片和音訊的檔案。
  • 請在多變數播放清單中加入所有資訊。

直播須符合下列規範:

  • 使用 #EXT-X-PROGRAM-DATE-TIME 標記。
  • 使用 #EXT-X-DISCONTINUITY-SEQUENCE 標記。
  • 提供較長的直播時間範圍。建議至少一分鐘。