ライブ配信にクライアントサイドの HLS インタースティシャルを使用する

HLS インタースティシャル仕様では、動画ストリームまたは音声ストリームに広告を柔軟にスケジュールして挿入する方法が導入されています。クライアントサイドのアプローチでは、AVPlayerInterstitialEvent クラスを作成することで、広告ブレークのリクエストと再生のタイミングをアプリケーションで完全に制御できます。このアプローチでは、コンテンツ ストリーム マニフェストに EXT-X-DATERANGE タグは必要ありません。クライアントサイド HLS インタースティシャルを使用すると、ストリーム マニフェストやメディア ファイルを変更することなく、あらゆるコンテンツに広告を動的に挿入できます。

このガイドでは、インタラクティブ メディア広告(IMA)SDK を、サーバーガイド広告挿入(SGAI)ライブ ストリーム セッションを作成し、クライアントサイドでインタースティシャルをスケジュールする動画プレーヤー アプリに統合する方法について説明します。詳しくは、サーバーガイド付き DAI をご覧ください。

前提条件

始める前に、次のものが必要になります。

  • ユーザー インターフェースに Storyboard を使用する新しい Xcode プロジェクト。詳細については、アプリの Xcode プロジェクトを作成するをご覧ください。

  • Google IMA SDK。詳しくは、DAI 用 IMA SDK を設定するをご覧ください。

  • ダイナミック広告挿入ライブ ストリーム リクエストの次のパラメータ:

    • NETWORK_CODE: Google アド マネージャーのネットワーク コード。
    • CUSTOM_ASSET_KEY: DAI ライブ配信イベントを識別するカスタム文字列。ライブ ストリーム イベントの DAI タイプが連続広告配信マニフェストである必要があります。

ストーリーボードを構成する

iPhone.storyboard ファイルで、次の操作を行います。

  1. 動画プレーヤーと広告 UI のコンテナとして UIView オブジェクトを作成します。
  2. ViewController クラスの adUIView プロパティを作成して、UIView オブジェクトと接続します。
  3. adUIView オブジェクトに、再生ボタンとして機能する UIButton を作成します。
  4. ViewController クラスの playButton プロパティを作成して、UIButton オブジェクトと接続し、onPlayButtonTouch 関数を作成してユーザーのタップを処理します。

広告ローダを初期化する

メインビュー コントローラの viewDidLoad イベントで、次の操作を行います。

  1. AVPlayer クラスと AVPlayerLayer クラスを使用して動画プレーヤーを設定します。
  2. IMAAdDisplayContainer オブジェクトと IMAAVPlayerVideoDisplay オブジェクトを作成します。広告表示コンテナは、IMA DAI SDK が広告 UI サブビューを挿入するための adUIView を指定します。動画表示オブジェクトは、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 プロトコルを実装して、ストリーム リクエストの成功または失敗を処理します。

  • 成功すると、IMAStreamManager を含む IMAAdsLoadedData オブジェクトが返されます。現在の 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 オブジェクトの配列に設定します。各アイテム オブジェクトには広告ポッド マニフェスト URL が含まれます。

連続配信広告のマニフェスト URL を作成するには、メソッド: HLS 連続配信広告のマニフェストのドキュメントをご覧ください。

デモ用に、次の例では、コンテンツ ライブストリームの現在時刻を使用して Pod 識別子文字列を生成します。generatePodIdentifier 関数は、Pod 識別子を ad_break_id/mid-roll-{minute} として返します。

/// 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 ID を取得します。

次の例では、ユーザーが再生ボタンをクリックしてから 2 分以内に広告ブレークが開始されるようにスケジュールを設定しています。

/// 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 メソッドは、広告ブレークの開始時間を計算し、広告ポッドのマニフェスト URL を作成します。この URL を使用して 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 サービング マニフェスト ストリームを使用してインタースティシャルをリクエストして再生できます。