使用用戶端 HTTP 即時串流插頁式廣告進行直播

HLS 插頁式廣告規格提供彈性的方式,可排定廣告放送時間,並將廣告插入影片或音訊串流。採用用戶端方法時,應用程式會建立 AVPlayerInterstitialEvent 類別,全面掌控何時要求及播放廣告插播。這種做法不需要內容串流資訊清單中的 EXT-X-DATERANGE 標記。使用用戶端 HLS 插播廣告,即可在任何內容中動態插入廣告,不必修改串流資訊清單或媒體檔案。

本指南說明如何將互動式媒體廣告 (IMA) SDK 整合至影片播放器應用程式,建立伺服器導向廣告插播 (SGAI) 直播工作階段,並在用戶端排定中插廣告。詳情請參閱伺服器導向的動態廣告插播

必要條件

開始之前,請先備妥下列項目:

  • 使用 Storyboard 做為使用者介面的新 Xcode 專案。詳情請參閱「為應用程式建立 Xcode 專案」一文。

  • Google IMA SDK。詳情請參閱「為 DAI 設定 IMA SDK」。

  • 動態廣告插播直播要求中的下列參數:

    • NETWORK_CODE:您的 Google Ad Manager 聯播網代碼。
    • CUSTOM_ASSET_KEY:用來識別 DAI 直播活動的自訂字串。直播活動的 DAI 類型必須為「廣告連播放送資訊清單」。

設定分鏡腳本

iPhone.storyboard 檔案中,執行下列操作:

  1. 建立 UIView 物件,做為影片播放器和廣告使用者介面的容器。
  2. 建立 ViewController 類別的 adUIView 屬性,以便與 UIView 物件建立連線。
  3. adUIView 物件中,建立 UIButton 做為播放按鈕。
  4. 建立 ViewController 類別的 playButton 屬性,以便與 UIButton 物件建立連線,並建立 onPlayButtonTouch 函式來處理使用者輕觸事件。

初始化廣告載入器

在主要檢視區塊控制器的 viewDidLoad 事件中,執行下列操作:

  1. 使用 AVPlayerAVPlayerLayer 類別設定影片播放器。
  2. 建立 IMAAdDisplayContainerIMAAVPlayerVideoDisplay 物件。廣告顯示容器會指定 adUIView,供 IMA DAI SDK 插入廣告 UI 子檢視區塊。影片顯示物件可做為 IMA DAI SDK 廣告邏輯與 AVFoundation 播放系統之間的橋樑,追蹤影片廣告的播放情形。
  3. 使用廣告播放和廣告 UI 本地化設定,初始化 IMAAdsLoader 物件。

以下範例會使用空白的 IMASettings 物件初始化廣告載入器:

import AVFoundation
import GoogleInteractiveMediaAds
import UIKit

// The main view controller for the sample app.
class ViewController:
  UIViewController, IMAAdsLoaderDelegate, IMAStreamManagerDelegate
{

  private enum StreamParameters {
    static let contentStream =
      "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"

    // Find your [Google Ad Manager network code](https://support.google.com/admanager/answer/7674889)
    // or use the test network code and custom asset key with the DAI type "Pod serving manifest"
    // from [DAI sample streams](https://developers.google.com/ad-manager/dynamic-ad-insertion/streams#pod_serving_dai).

    /// Google Ad Manager network code.
    static let networkCode = "21775744923"

    /// Google DAI livestream custom asset key.
    static let customAssetKey = "sgai-hls-live"

    // Set your ad break duration.
    static let adBreakDurationMs = 10000
  }

  /// The play button to start the stream.
  /// It is hidden when the stream starts playing.
  @IBOutlet private weak var playButton: UIButton!

  /// The view to display the ad UI elements: countdown, skip button, etc.
  /// It is hidden when the stream starts playing.
  @IBOutlet private weak var adUIView: UIView!

  /// The reference of your ad UI view for the IMA SDK to create the ad's user interface elements.
  private var adDisplayContainer: IMAAdDisplayContainer!

  /// The AVPlayer instance that plays the content and the ads.
  private var player: AVPlayer!

  /// The reference of your video player for the IMA SDK to play and monitor the ad breaks.
  private var videoDisplay: IMAAVPlayerVideoDisplay!

  /// The entry point of the IMA SDK to make stream requests to Google Ad Manager.
  private var adsLoader: IMAAdsLoader!

  /// The reference of the ad stream manager, set when the ad stream is loaded.
  /// The IMA SDK requires a strong reference to the stream manager for the entire duration of
  /// the ad break.
  private var streamManager: IMAStreamManager?

  /// The ad stream session ID, set when the ad stream is loaded.
  private var adStreamSessionId: String?

  override func viewDidLoad() {

    // Initialize the IMA SDK.
    let adLoaderSettings = IMASettings()
    adsLoader = IMAAdsLoader(settings: adLoaderSettings)

    // Set up the video player and the container view.
    player = AVPlayer()
    let playerLayer = AVPlayerLayer(player: player)
    playerLayer.frame = adUIView.bounds
    adUIView.layer.addSublayer(playerLayer)
    playButton.layer.zPosition = CGFloat.greatestFiniteMagnitude

    // Create an object to monitor the stream playback.
    videoDisplay = IMAAVPlayerVideoDisplay(avPlayer: player)

    super.viewDidLoad()

    // Create a container object for ad UI elements.
    // See [example in video ads](https://support.google.com/admanager/answer/2695279#zippy=%2Cexample-in-video-ads)
    adDisplayContainer = IMAAdDisplayContainer(
      adContainer: adUIView, viewController: self, companionSlots: nil)

    // Specify the delegate for hanlding ad events of the stream session.
    adsLoader.delegate = self
  }

提出串流要求

如要為內容串流請求廣告,請建立 IMAPodStreamRequest 物件,並傳遞至 IMAAdsLoader 執行個體。您可以選擇設定 adTagParameters 屬性,為串流提供 DAI 選項和指定目標參數。

這個範例會在 viewDidAppear 事件中呼叫 loadAdStream 方法:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)

  loadAdStream()
  loadContentStream()
}

private func loadContentStream() {
  guard let contentURL = URL(string: StreamParameters.contentStream) else {
    print("Failed to load content stream. The URL is invalid.")
    return
  }
  let item = AVPlayerItem(url: contentURL)
  player.replaceCurrentItem(with: item)
}

/// Makes a stream request to Google Ad Manager.
private func loadAdStream() {
  let streamRequest = IMAPodStreamRequest(
    networkCode: StreamParameters.networkCode,
    customAssetKey: StreamParameters.customAssetKey,
    adDisplayContainer: adDisplayContainer,
    videoDisplay: videoDisplay,
    pictureInPictureProxy: nil,
    userContext: nil)

  // Register a streaming session on Google Ad Manager DAI servers.
  adsLoader.requestStream(with: streamRequest)
}

在正式版應用程式中,使用者選取內容串流後,請呼叫 loadAdStream 方法。

處理串流載入事件

實作 IMAAdsLoaderDelegate 通訊協定,處理串流要求成功或失敗的情況:

  • 成功時,您會收到包含 IMAStreamManagerIMAAdsLoadedData 物件。儲存目前 DAI 工作階段的 streamManager.streamId 值。
  • 如果失敗,請記錄錯誤。

以下範例會處理串流載入事件,並記錄串流載入失敗事件:

// MARK: - IMAAdsLoaderDelegate
func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
  guard let streamManager = adsLoadedData.streamManager else {
    // Report a bug on [IMA SDK forum](https://groups.google.com/g/ima-sdk).
    print("Failed to retrieve stream manager from ads loaded data.")
    return
  }
  // Save the stream manager to handle ad events of the stream session.
  self.streamManager = streamManager
  streamManager.delegate = self
  let adRenderingSettings = IMAAdsRenderingSettings()
  // Uncomment the next line to enable the current view controller to get notified of ad clicks.
  // adRenderingSettings.linkOpenerDelegate = self
  // Initialize the stream manager to create ad UI elements.
  streamManager.initialize(with: adRenderingSettings)

  guard streamManager.streamId != nil else {
    // Report a bug on [IMA SDK forum](https://groups.google.com/g/ima-sdk).
    print("Failed to retrieve stream ID from stream manager.")
    return
  }
  // Save the ad stream session ID to construct ad pod requests.
  adStreamSessionId = streamManager.streamId
}

func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
  guard let errorMessage = adErrorData.adError.message else {
    print("Stream registration failed with unknown error.")
    return
  }
  print("Stream registration failed with error: \(errorMessage)")
}

// MARK: - IMAStreamManagerDelegate
func streamManager(_ streamManager: IMAStreamManager, didReceive error: IMAAdError) {
  guard let errorMessage = error.message else {
    print("Ad stream failed to load with unknown error.")
    return
  }
  print("Ad stream failed to load with error: \(errorMessage)")
}

排定廣告插播時間

如要安排廣告插播時間,請建立 AVPlayerInterstitialEvent 物件。將事件物件的 templateItems 屬性設為 AVPlayerItem 物件的陣列,其中每個項目物件都包含廣告插播資訊清單網址。

如要建構廣告 Pod 資訊清單網址,請參閱「方法:HLS Pod 資訊清單」說明文件。

基於示範用途,以下範例會使用內容直播的目前時間,產生 Pod ID 字串。generatePodIdentifier 函式會以 ad_break_id/mid-roll-{minute} 形式傳回 Pod ID。

/// Generates a pod identifier based on the current time.
///
/// See [HLS pod manifest parameters](https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/reference/live#path_parameters_3).
///
/// - Returns: The pod identifier in either the format of "pod/{integer}" or "ad_break_id/{string}".
private func generatePodIdentifier(from currentSeconds: Int) -> String {
  let minute = Int(currentSeconds / 60) + 1
  return "ad_break_id/mid-roll-\(minute)"
}

在正式版應用程式中,從來源擷取廣告群組 ID,該來源會為每個廣告插播提供不重複的值,並為所有直播觀眾同步處理。

以下範例會排定廣告插播,在使用者點選播放按鈕後的兩分鐘內開始:

/// Schedules ad insertion shortly before ad break starts.
private func scheduleAdInsertion() {

  guard let streamID = self.adStreamSessionId else {
    print("The ad stream ID is not set. Skipping all ad breaks of the current stream session.")
    return
  }

  let currentSeconds = Int(Date().timeIntervalSince1970)
  var secondsToAdBreakStart = 60 - currentSeconds % 60
  // If there is less than 30 seconds remaining in the current minute, schedule the ad insertion
  // for the next minute instead.
  if secondsToAdBreakStart < 30 {
    secondsToAdBreakStart += 60
  }

  guard let primaryPlayerCurrentItem = player.currentItem else {
    print(
      "Failed to get the player item of the content stream. Skipping an ad break in \(secondsToAdBreakStart) seconds."
    )
    return
  }

  let adBreakStartTime = CMTime(
    seconds: CMTimeGetSeconds(player.currentTime())
      + Double(secondsToAdBreakStart), preferredTimescale: 1)

  // Create an identifier to construct the ad pod request for the next ad break.
  let adPodIdentifier = generatePodIdentifier(from: currentSeconds)

  guard
    let adPodManifestUrl = URL(
      string:
        "https://dai.google.com/linear/pods/v1/hls/network/\(StreamParameters.networkCode)/custom_asset/\(StreamParameters.customAssetKey)/\(adPodIdentifier).m3u8?stream_id=\(streamID)&pd=\(StreamParameters.adBreakDurationMs)"
    )
  else {
    print("Failed to generate the ad pod manifest URL. Skipping insertion of \(adPodIdentifier).")
    return
  }

  let interstitialEvent = AVPlayerInterstitialEvent(
    primaryItem: primaryPlayerCurrentItem,
    identifier: adPodIdentifier,
    time: adBreakStartTime,
    templateItems: [AVPlayerItem(url: adPodManifestUrl)],
    restrictions: [],
    resumptionOffset: .zero)
  let interstitialEventController = AVPlayerInterstitialEventController(primaryPlayer: player)
  interstitialEventController.events = [interstitialEvent]
  print(
    "Ad break scheduled to start in \(secondsToAdBreakStart) seconds. Ad break manifest URL: \(adPodManifestUrl)."
  )
}

scheduleAdInsertion 方法會計算廣告插播開始時間,並建構廣告插播資訊清單網址。使用這個網址建立 AVPlayerInterstitialEvent 物件。

您可以選擇使用 AVPlayerInterstitialEvent.Restrictions struct,在廣告播放期間限制使用者略過或倒轉。

處理廣告事件

如要處理廣告事件,請導入 IMAStreamManagerDelegate 通訊協定。這個方法可讓您追蹤廣告插播的開始和結束時間,並取得個別廣告的相關資訊。

func streamManager(_ streamManager: IMAStreamManager, didReceive event: IMAAdEvent) {
  switch event.type {
  case IMAAdEventType.STARTED:
    // Log extended data.
    if let ad = event.ad {
      let extendedAdPodInfo = String(
        format: "Showing ad %zd/%zd, bumper: %@, title: %@, "
          + "description: %@, contentType:%@, pod index: %zd, "
          + "time offset: %lf, max duration: %lf.",
        ad.adPodInfo.adPosition,
        ad.adPodInfo.totalAds,
        ad.adPodInfo.isBumper ? "YES" : "NO",
        ad.adTitle,
        ad.adDescription,
        ad.contentType,
        ad.adPodInfo.podIndex,
        ad.adPodInfo.timeOffset,
        ad.adPodInfo.maxDuration)

      print("\(extendedAdPodInfo)")
    }
    break
  case IMAAdEventType.AD_BREAK_STARTED:
    print("Ad break started.")
    break
  case IMAAdEventType.AD_BREAK_ENDED:
    print("Ad break ended.")
    break
  case IMAAdEventType.AD_PERIOD_STARTED:
    print("Ad period started.")
    break
  case IMAAdEventType.AD_PERIOD_ENDED:
    print("Ad period ended.")
    break
  default:
    break
  }
}

執行應用程式。如果成功,您可以使用 Pod 放送資訊清單串流請求及播放插頁式廣告。