استخدام إعلانات بينية وفق بروتوكول HTTP ‏(HLS) من جهة العميل للبث المباشر

توفّر مواصفات الإعلانات البينية في HLS طريقة مرنة لتحديد مواعيد الإعلانات وإدراجها في بث الفيديو أو الصوت. باستخدام طريقة العرض من جهة العميل، يتحكّم تطبيقك بشكل كامل في وقت طلب فواصل الإعلانات وتشغيلها من خلال إنشاء الفئة AVPlayerInterstitialEvent. لا يتطلّب هذا النهج علامات EXT-X-DATERANGE في بيانات وصف المحتوى. تتيح لك الإعلانات البينية من جهة العميل بتنسيق HLS إمكانية إدراج إعلانات بشكل ديناميكي في أي محتوى، بدون الحاجة إلى تعديل بيان البث أو ملفات الوسائط.

يتناول هذا الدليل عملية دمج "حزمة تطوير البرامج لإعلانات الوسائط التفاعلية" (IMA SDK) في تطبيق مشغّل فيديو ينشئ جلسة بث مباشر باستخدام ميزة "إدراج الإعلانات من جهة الخادم" (SGAI) ويجدول الإعلانات البينية من جهة العميل. لمزيد من المعلومات، راجِع مقالة ميزة "إدخال الإعلانات الديناميكية" من جهة الخادم.

المتطلبات الأساسية

قبل البدء، يجب أن يتوفّر لديك ما يلي:

ضبط مخطط قصة

في ملف 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 لتوفير خيارات "إعلانات الفيديو الديناميكية" ومعلَمات الاستهداف للبث.

يستدعي هذا المثال طريقة 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 لجلسة "إعلانات البث المباشر" الحالية.
  • في حال حدوث خطأ، سجِّل الخطأ.

يتناول المثال التالي حدث تحميل البث ويسجّل حدث تعذُّر تحميل البث:

// 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 معرّف الحاوية على شكل 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)"
}

في تطبيقك المباشر، استرجِع معرّف مجموعة الإعلانات من مصدر يوفّر قيمًا فريدة لكل فاصل إعلاني، تتم مزامنتها مع جميع مشاهدي البث المباشر.

يجدول المثال التالي فاصلًا إعلانيًا ليبدأ خلال الدقيقتَين التاليتَين بعد أن ينقر المستخدم على زر التشغيل:

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