使用客户端 HLS 插播广告进行直播

HLS 插页式广告规范提供了一种灵活的方式来安排广告并将其插入视频或音频流中。采用客户端方法时,您的应用可以通过创建 AVPlayerInterstitialEvent 类来完全控制何时请求和播放广告插播。此方法不需要在内容流清单中添加 EXT-X-DATERANGE 标记。借助客户端 HLS 插播广告,您可以将广告动态插入到任何内容中,而无需修改流清单或媒体文件。

本指南介绍了如何将互动式媒体广告 (IMA) SDK 集成到视频播放器应用中,该应用可创建服务器引导的广告插播 (SGAI) 直播会话,并在客户端安排插页式广告。如需了解详情,请参阅服务器引导的 DAI

前提条件

在开始之前,您需要做好以下准备:

  • 一个使用 Storyboard 作为用户界面的新 Xcode 项目。如需了解详情,请参阅为应用创建 Xcode 项目

  • Google IMA SDK。如需了解详情,请参阅为 DAI 设置 IMA SDK

  • DAI 直播请求的以下参数:

    • 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 插入广告界面子视图。视频显示对象充当 IMA DAI SDK 的广告逻辑与 AVFoundation 播放系统之间的桥梁,用于跟踪视频广告的播放情况。
  3. 使用广告播放和广告界面本地化设置初始化 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 对象数组,其中每个商品对象都包含一个广告插播清单网址。

如需构建广告插播清单网址,请参阅方法:HLS 插播清单文档。

出于演示目的,以下示例使用内容直播的当前时间生成 pod 标识符字符串。generatePodIdentifier 函数以 ad_break_id/mid-roll-{minute} 形式返回 pod 标识符。

/// 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)"
}

在正式版应用中,从一个为每个广告插播时间点提供唯一值的来源检索 pod 标识符,该标识符会针对直播的所有观看者进行同步。

以下示例安排了一个广告插播,该插播将在用户点击播放按钮后的两分钟内开始:

/// 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 结构体来限制用户在广告播放期间跳过或快退。

处理广告事件

如需处理广告事件,请实现 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 服务清单流请求和播放插页式广告。