ใช้โฆษณาคั่นระหว่างหน้า HLS ฝั่งไคลเอ็นต์สำหรับไลฟ์สด

ข้อกำหนดของโฆษณาวิดีโอคั่นระหว่างหน้า HLS นำเสนอวิธีที่ยืดหยุ่นในการจัดตารางเวลาและ แทรกโฆษณาลงในสตรีมวิดีโอหรือเสียง เมื่อใช้แนวทางฝั่งไคลเอ็นต์ แอปพลิเคชันของคุณจะควบคุมได้อย่างเต็มที่ว่าจะขอและเล่น ช่วงพักโฆษณาเมื่อใดโดยการสร้างคลาส AVPlayerInterstitialEvent วิธีนี้ไม่จำเป็นต้องใช้แท็ก EXT-X-DATERANGE ในไฟล์ Manifest ของสตรีมเนื้อหา โฆษณาคั่นระหว่างหน้า HLS ฝั่งไคลเอ็นต์ช่วยให้คุณแทรกโฆษณาแบบไดนามิกลงในเนื้อหาใดก็ได้โดยไม่ต้องแก้ไขไฟล์ Manifest ของสตรีมหรือไฟล์สื่อ

คู่มือนี้ครอบคลุมการผสานรวม Interactive Media Ads (IMA) SDK เข้ากับแอปวิดีโอเพลเยอร์ที่สร้างเซสชันสตรีมแบบสดของการแทรกโฆษณาที่นำโดยเซิร์ฟเวอร์ (SGAI) และกำหนดเวลาโฆษณาคั่นระหว่างหน้าฝั่งไคลเอ็นต์ ดูข้อมูลเพิ่มเติมได้ที่DAI ที่มีคำแนะนำจากเซิร์ฟเวอร์

ข้อกำหนดเบื้องต้น

ก่อนเริ่มต้น คุณต้องมีสิ่งต่อไปนี้

  • โปรเจ็กต์ Xcode ใหม่ที่ใช้ Storyboard สำหรับอินเทอร์เฟซผู้ใช้ ดูข้อมูลเพิ่มเติมได้ที่การสร้างโปรเจ็กต์ Xcode สำหรับแอป

  • Google IMA SDK ดูข้อมูลเพิ่มเติมได้ที่ตั้งค่า IMA SDK สำหรับ DAI

  • พารามิเตอร์ต่อไปนี้สำหรับคำขอไลฟ์สด DAI

    • NETWORK_CODE: รหัสเครือข่าย Google Ad Manager
    • CUSTOM_ASSET_KEY: สตริงที่กำหนดเองซึ่งระบุเหตุการณ์ไลฟ์สด DAI เหตุการณ์ไลฟ์สดต้องมีประเภท DAI ของไฟล์ Manifest ที่แสดงในพ็อด

กำหนดค่าสตอรีบอร์ด

ในไฟล์ iPhone.storyboard ให้ทำดังนี้

  1. สร้างออบเจ็กต์ UIView เป็นคอนเทนเนอร์สำหรับวิดีโอเพลเยอร์และ UI ของโฆษณา
  2. สร้างพร็อพเพอร์ตี้ adUIView ของคลาส ViewController เพื่อเชื่อมต่อกับ ออบเจ็กต์ UIView
  3. ในออบเจ็กต์ adUIView ให้สร้าง UIButton เพื่อทำหน้าที่เป็นปุ่มเล่น
  4. สร้างพร็อพเพอร์ตี้ playButton ของคลาส ViewController เพื่อเชื่อมต่อกับออบเจ็กต์ UIButton และฟังก์ชัน onPlayButtonTouch เพื่อจัดการการแตะของผู้ใช้

เริ่มต้นเครื่องมือโหลดโฆษณา

ใน viewDidLoad ของตัวควบคุมมุมมองหลัก ให้ทำดังนี้

  1. ตั้งค่าวิดีโอเพลเยอร์โดยใช้คลาส AVPlayer และ AVPlayerLayer
  2. สร้างออบเจ็กต์ IMAAdDisplayContainer และ IMAAVPlayerVideoDisplay คอนเทนเนอร์ที่แสดงโฆษณาระบุ adUIView สำหรับ IMA DAI SDK เพื่อแทรกมุมมองย่อยของ UI โฆษณา ออบเจ็กต์การแสดงวิดีโอทําหน้าที่เป็นตัวกลาง ระหว่างตรรกะโฆษณาของ IMA DAI SDK กับระบบการเล่น AVFoundation เพื่อติดตามการเล่นโฆษณาวิดีโอ
  3. เริ่มต้นออบเจ็กต์ IMAAdsLoader ด้วยการตั้งค่าการเล่นโฆษณาและการแปล UI ของโฆษณา

ตัวอย่างต่อไปนี้จะเริ่มต้นโปรแกรมโหลดโฆษณาด้วยออบเจ็กต์ 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 ปัจจุบัน
  • หากไม่สำเร็จ ให้บันทึกข้อผิดพลาด
ส่งผลเสียต่อเมตริกประสิทธิภาพของโฆษณา

ตัวอย่างต่อไปนี้จะจัดการเหตุการณ์ที่สตรีมโหลดแล้วและบันทึกเหตุการณ์ที่สตรีมโหลดไม่สำเร็จ

// 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 ของไฟล์ Manifest ของพ็อดโฆษณา

หากต้องการสร้าง URL ของไฟล์ Manifest ของพ็อดโฆษณา ให้ทำตามเอกสารประกอบวิธี: ไฟล์ Manifest ของพ็อด 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)"
}

ในแอปเวอร์ชันที่ใช้งานจริง ให้ดึงตัวระบุพ็อดจากแหล่งที่มาซึ่งมี ค่าที่ไม่ซ้ำกันสำหรับช่วงพักโฆษณาแต่ละช่วง โดยซิงค์ค่าดังกล่าวสำหรับผู้ชมการถ่ายทอดสดทุกคน

ตัวอย่างต่อไปนี้กำหนดเวลาให้ช่วงพักโฆษณาเริ่มภายใน 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 ของไฟล์ Manifest ของพ็อดโฆษณา ใช้ 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
  }
}

เรียกใช้แอป หากสำเร็จ คุณจะขอและเล่นโฆษณาคั่นระหว่างหน้าได้โดยใช้ สตรีมไฟล์ Manifest ของการแสดงโฆษณาพ็อด