HLS

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 標記。
  • 提供較長的直播時間。一分鐘以上最好。