שימוש במודעות מעבר מסוג HLS בצד הלקוח לשידור חי

במפרט של מעברונים ב-HLS מוצגת דרך גמישה לתזמון ולהוספה של מודעות לסטרימינג של וידאו או אודיו. בגישה בצד הלקוח, האפליקציה שלכם מקבלת שליטה מלאה על מתי לבקש הפסקות פרסום ולהפעיל אותן, על ידי יצירת הסיווג AVPlayerInterstitialEvent. הגישה הזו לא דורשת את התגים EXT-X-DATERANGE במניפסטים של פיד התוכן. בעזרת מודעות מעברון HLS בצד הלקוח אפשר להוסיף מודעות באופן דינמי לכל תוכן, בלי צורך לשנות את מניפסט הסטרימינג או את קובצי המדיה.

במדריך הזה מוסבר איך להטמיע את ערכת פיתוח התוכנה (SDK) של מודעות מדיה אינטראקטיביות (IMA) באפליקציית נגן וידאו שיוצרת סשן של שידור חי עם הטמעה של מודעות בהנחיית השרת (SGAI) ומתזמנת מודעות מעבריות בצד הלקוח. מידע נוסף זמין במאמר DAI בהנחיית השרת.

דרישות מוקדמות

לפני שמתחילים, צריך:

  • פרויקט חדש ב-Xcode, עם Storyboard לממשק המשתמש. מידע נוסף זמין במאמר יצירת פרויקט Xcode לאפליקציה.

  • ‫Google IMA SDK. מידע נוסף זמין במאמר בנושא הגדרת IMA SDK ל-DAI.

  • הפרמטרים הבאים של בקשת שידור חי ל-DAI:

    • NETWORK_CODE: קוד הרשת שלכם ב-Google Ad Manager.
    • CUSTOM_ASSET_KEY: מחרוזת מותאמת אישית שמזהה את אירוע השידור החי של DAI. סוג ה-DAI של מניפסט ההצגה של הפוד חייב להיות מוגדר לאירוע השידור החי.

הגדרת סטוריבורד

בתוך הקובץ iPhone.storyboard, מבצעים את הפעולות הבאות:

  1. יוצרים אובייקט UIView כקונטיינר לנגן הווידאו ולממשק המשתמש של המודעה.
  2. יוצרים נכס adUIView מסוג ViewController כדי להתחבר לאובייקט UIView.
  3. באובייקט adUIView, יוצרים UIButton שישמש כלחצן הפעלה.
  4. יוצרים מאפיין playButton של המחלקה ViewController כדי להתחבר לאובייקט UIButton ופונקציה onPlayButtonTouch לטיפול בהקשות של המשתמשים.

אתחול של כלי לטעינת מודעות

באירוע viewDidLoad של בקר התצוגה הראשי, מבצעים את הפעולות הבאות:

  1. הגדרת נגן וידאו באמצעות מחלקות AVPlayer ו-AVPlayerLayer.
  2. יצירת אובייקטים של IMAAdDisplayContainer ושל IMAAVPlayerVideoDisplay. הקונטיינר של הצגת המודעות מציין את 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 ופרמטרים של טירגוט לסטרימינג.

בדוגמה הזו, מתבצעת קריאה למתודה loadAdStream באירוע viewDidAppear:

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 כדי לטפל בהצלחה או בכישלון של בקשת הסטרימינג:

  • אם הפעולה בוצעה ללא שגיאות, תקבלו אובייקט IMAAdsLoadedData שמכיל את IMAStreamManager. אחסון הערך streamManager.streamId בסשן ה-DAI הנוכחי.
  • אם הפעולה נכשלת, השגיאה נרשמת ביומן.

בדוגמה הבאה מוצג טיפול באירוע stream loaded ורישום ביומן של האירוע stream failed to load:

// 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.

לצורך הדגמה, בדוגמה הבאה נוצר מחרוזת של מזהה פוד באמצעות השעה הנוכחית של השידור החי של התוכן. הפונקציה 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 ממקור שמספק ערכים ייחודיים לכל הפסקה לפרסום, שמסונכרנים לכל הצופים בשידור החי.

בדוגמה הבאה מתוזמנת הפסקה לפרסום שתתחיל תוך שתי הדקות הבאות אחרי שהמשתמש ילחץ על לחצן ההפעלה:

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

מריצים את האפליקציה. אם הפעולה מצליחה, אפשר לשלוח בקשה למודעות מעברון ולהפעיל אותן באמצעות סטרימינג של קובץ מניפסט להצגת מודעות בבלוק.