Sử dụng quảng cáo xen kẽ HLS phía máy khách cho sự kiện phát trực tiếp

Quy cách Quảng cáo xen kẽ HLS giới thiệu một cách linh hoạt để lập lịch và chèn quảng cáo vào luồng video hoặc âm thanh. Với phương pháp phía máy khách, ứng dụng của bạn có toàn quyền kiểm soát thời điểm yêu cầu và phát các điểm chèn quảng cáo bằng cách tạo lớp AVPlayerInterstitialEvent. Phương pháp này không yêu cầu thẻ EXT-X-DATERANGE trong tệp kê khai luồng nội dung. Quảng cáo xen kẽ HLS phía ứng dụng cho phép bạn chèn quảng cáo một cách linh động vào mọi nội dung mà không cần sửa đổi tệp kê khai luồng hoặc tệp đa phương tiện.

Hướng dẫn này trình bày cách tích hợp SDK Quảng cáo trên phương tiện truyền thông tương tác (IMA) vào một ứng dụng trình phát video tạo phiên phát trực tiếp Chèn quảng cáo do máy chủ hướng dẫn (SGAI) và lên lịch quảng cáo xen kẽ phía máy khách. Để biết thêm thông tin, hãy xem bài viết DAI do máy chủ hướng dẫn.

Điều kiện tiên quyết

Trước khi bắt đầu, bạn cần có những thông tin sau:

  • Một dự án Xcode mới, sử dụng Storyboard cho giao diện người dùng. Để biết thêm thông tin, hãy xem bài viết Tạo dự án Xcode cho một ứng dụng.

  • Google IMA SDK. Để biết thêm thông tin, hãy xem bài viết Thiết lập SDK IMA cho DAI.

  • Các thông số sau đây cho yêu cầu phát trực tiếp DAI:

    • NETWORK_CODE: Mã mạng Google Ad Manager của bạn.
    • CUSTOM_ASSET_KEY: Chuỗi tuỳ chỉnh của bạn xác định sự kiện phát trực tiếp DAI. Sự kiện phát trực tiếp phải có loại DAI Tệp kê khai phân phát nhóm.

Định cấu hình bảng phân cảnh

Trong tệp iPhone.storyboard, hãy làm như sau:

  1. Tạo một đối tượng UIView làm vùng chứa cho trình phát video và giao diện người dùng quảng cáo.
  2. Tạo một thuộc tính adUIView của lớp ViewController để kết nối với đối tượng UIView.
  3. Trong đối tượng adUIView, hãy tạo một UIButton để hoạt động như một nút phát.
  4. Tạo một thuộc tính playButton của lớp ViewController để kết nối với đối tượng UIButton và một hàm onPlayButtonTouch để xử lý các thao tác nhấn của người dùng.

Khởi chạy một trình tải quảng cáo

Trong sự kiện viewDidLoad của bộ điều khiển chế độ xem chính, hãy làm như sau:

  1. Thiết lập trình phát video bằng các lớp AVPlayerAVPlayerLayer.
  2. Tạo các đối tượng IMAAdDisplayContainerIMAAVPlayerVideoDisplay. Vùng chứa hiển thị quảng cáo chỉ định adUIView để IMA DAI SDK chèn các khung hiển thị phụ của giao diện người dùng quảng cáo. Đối tượng hiển thị video đóng vai trò là cầu nối giữa logic quảng cáo của SDK DAI IMA và hệ thống phát lại AVFoundation, theo dõi quá trình phát quảng cáo dạng video.
  3. Khởi động đối tượng IMAAdsLoader bằng chế độ phát quảng cáo và chế độ cài đặt bản địa hoá giao diện người dùng quảng cáo.

Ví dụ sau đây khởi chạy một trình tải quảng cáo bằng một đối tượng IMASettings trống:

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
  }

Đưa ra yêu cầu phát trực tuyến

Để yêu cầu quảng cáo cho một luồng nội dung, hãy tạo một đối tượng IMAPodStreamRequest rồi truyền đối tượng đó đến phiên bản IMAAdsLoader. Bạn có thể đặt thuộc tính adTagParameters để cung cấp các lựa chọn DAI và thông số nhắm mục tiêu cho luồng phát.

Ví dụ này gọi phương thức loadAdStream trong sự kiện 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)
}

Trong ứng dụng phát hành công khai, hãy gọi phương thức loadAdStream sau khi người dùng chọn một luồng nội dung.

Xử lý sự kiện tải luồng phát

Triển khai giao thức IMAAdsLoaderDelegate để xử lý trạng thái thành công hoặc không thành công của yêu cầu truyền phát:

  • Khi thành công, bạn sẽ nhận được một đối tượng IMAAdsLoadedData chứa IMAStreamManager. Lưu trữ giá trị streamManager.streamId cho phiên DAI hiện tại.
  • Khi thất bại, hãy ghi lại lỗi.

Ví dụ sau đây xử lý sự kiện luồng được tải và ghi lại sự kiện luồng không tải được:

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

Lên lịch chèn quảng cáo

Để lên lịch cho một điểm chèn quảng cáo, hãy tạo đối tượng AVPlayerInterstitialEvent. Đặt thuộc tính templateItems của đối tượng sự kiện thành một mảng gồm các đối tượng AVPlayerItem, trong đó mỗi đối tượng mục giữ một URL tệp kê khai nhóm quảng cáo.

Để tạo URL tệp kê khai nhóm quảng cáo, hãy làm theo tài liệu Phương thức: Tệp kê khai nhóm HLS.

Để minh hoạ, ví dụ sau đây sẽ tạo một chuỗi mã nhận dạng nhóm quảng cáo bằng cách sử dụng thời gian hiện tại của luồng phát trực tiếp nội dung. Hàm generatePodIdentifier trả về giá trị nhận dạng nhóm dưới dạng 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)"
}

Trong ứng dụng phát hành công khai, hãy truy xuất mã nhận dạng nhóm từ một nguồn cung cấp các giá trị riêng biệt cho từng điểm chèn quảng cáo, được đồng bộ hoá cho tất cả người xem sự kiện phát trực tiếp.

Ví dụ sau đây lên lịch cho một điểm chèn quảng cáo bắt đầu trong vòng 2 phút tiếp theo sau khi người dùng nhấp vào nút phát:

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

Phương thức scheduleAdInsertion sẽ tính toán thời gian bắt đầu điểm chèn quảng cáo và tạo URL tệp kê khai nhóm quảng cáo. Dùng URL này để tạo một đối tượng AVPlayerInterstitialEvent.

Nếu muốn, hãy dùng cấu trúc AVPlayerInterstitialEvent.Restrictions để hạn chế người dùng tua qua hoặc tua lại trong quá trình phát quảng cáo.

Xử lý sự kiện quảng cáo

Để xử lý các sự kiện quảng cáo, hãy triển khai giao thức IMAStreamManagerDelegate. Phương pháp này cho phép bạn theo dõi thời điểm bắt đầu và kết thúc các điểm chèn quảng cáo, đồng thời nhận thông tin về từng quảng cáo.

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

Chạy ứng dụng của bạn. Nếu thành công, bạn có thể yêu cầu và phát quảng cáo xen kẽ bằng cách sử dụng luồng tệp kê khai phân phát Pod.