Menggunakan interstisial HLS sisi klien untuk livestream

Spesifikasi Interstisial HLS memperkenalkan cara fleksibel untuk menjadwalkan dan menyisipkan iklan ke dalam streaming video atau audio. Dengan pendekatan sisi klien, aplikasi Anda memiliki kontrol penuh atas kapan harus meminta dan memutar jeda iklan dengan membuat class AVPlayerInterstitialEvent. Pendekatan ini tidak memerlukan tag EXT-X-DATERANGE dalam manifes aliran konten. Interstisial HLS sisi klien memungkinkan Anda menyisipkan iklan secara dinamis ke dalam konten apa pun, tanpa perlu mengubah manifes stream atau file media.

Panduan ini membahas cara mengintegrasikan Interactive Media Ads (IMA) SDK ke dalam aplikasi pemutar video yang membuat sesi live streaming penyisipan iklan yang dipandu server (SGAI) dan menjadwalkan iklan interstisial sisi klien. Untuk mengetahui informasi selengkapnya, lihat DAI yang dipandu server.

Prasyarat

Sebelum memulai, Anda memerlukan hal berikut:

  • Project Xcode baru, menggunakan Storyboard untuk antarmuka pengguna. Untuk mengetahui informasi selengkapnya, lihat Membuat project Xcode untuk aplikasi.

  • Google IMA SDK. Untuk mengetahui informasi selengkapnya, lihat Menyiapkan IMA SDK untuk DAI.

  • Parameter berikut untuk permintaan live stream DAI Anda:

    • NETWORK_CODE: Kode jaringan Google Ad Manager Anda.
    • CUSTOM_ASSET_KEY: String kustom Anda yang mengidentifikasi peristiwa livestream DAI. Acara live stream harus memiliki jenis DAI manifes penayangan pod.

Mengonfigurasi storyboard

Di file iPhone.storyboard, lakukan hal berikut:

  1. Buat objek UIView sebagai penampung untuk pemutar video dan UI iklan.
  2. Buat properti adUIView dari class ViewController untuk terhubung dengan objek UIView.
  3. Dalam objek adUIView, buat UIButton untuk berfungsi sebagai tombol putar.
  4. Buat properti playButton dari class ViewController untuk terhubung dengan objek UIButton dan fungsi onPlayButtonTouch untuk menangani ketukan pengguna.

Menginisialisasi loader iklan

Dalam peristiwa viewDidLoad pengontrol tampilan utama, lakukan hal berikut:

  1. Siapkan pemutar video menggunakan class AVPlayer dan AVPlayerLayer.
  2. Buat objek IMAAdDisplayContainer dan IMAAVPlayerVideoDisplay. Kontainer tampilan iklan menentukan adUIView untuk IMA DAI SDK guna menyisipkan subview UI iklan. Objek tampilan video berfungsi sebagai jembatan antara logika iklan IMA DAI SDK dan sistem pemutaran AVFoundation, yang melacak pemutaran iklan video.
  3. Lakukan inisialisasi objek IMAAdsLoader dengan setelan pemutaran iklan dan pelokalan UI iklan.

Contoh berikut menginisialisasi pemuat iklan dengan objek IMASettings kosong:

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
  }

Membuat permintaan streaming

Untuk meminta iklan untuk aliran konten, buat objek IMAPodStreamRequest dan teruskan ke instance IMAAdsLoader Anda. Secara opsional, tetapkan properti adTagParameters untuk menyediakan opsi DAI dan parameter penargetan untuk streaming Anda.

Contoh ini memanggil metode loadAdStream dalam peristiwa 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)
}

Di aplikasi produksi Anda, panggil metode loadAdStream setelah pengguna memilih streaming konten.

Menangani peristiwa pemuatan streaming

Terapkan protokol IMAAdsLoaderDelegate untuk menangani keberhasilan atau kegagalan permintaan streaming:

  • Jika berhasil, Anda akan menerima objek IMAAdsLoadedData yang berisi IMAStreamManager. Menyimpan nilai streamManager.streamId untuk sesi DAI saat ini.
  • Jika gagal, catat error.

Contoh berikut menangani peristiwa stream dimuat dan mencatat peristiwa stream gagal dimuat:

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

Menjadwalkan penyisipan iklan

Untuk menjadwalkan jeda iklan, buat objek AVPlayerInterstitialEvent. Tetapkan properti templateItems objek peristiwa ke array objek AVPlayerItem, dengan setiap objek item menyimpan URL manifes pod iklan.

Untuk membuat URL manifes pod iklan, ikuti dokumentasi Metode: Manifes pod HLS.

Untuk tujuan demonstrasi, contoh berikut menghasilkan string ID pod menggunakan waktu saat ini dari livestream konten. Fungsi generatePodIdentifier menampilkan ID pod sebagai 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)"
}

Di aplikasi produksi, ambil ID pod dari sumber yang memberikan nilai unik untuk setiap jeda iklan, yang disinkronkan untuk semua penonton livestream.

Contoh berikut menjadwalkan jeda iklan untuk dimulai dalam dua menit berikutnya setelah pengguna mengklik tombol putar:

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

Metode scheduleAdInsertion menghitung waktu mulai jeda iklan dan membuat URL manifes pod iklan. Gunakan URL ini untuk membuat objek AVPlayerInterstitialEvent.

Secara opsional, gunakan struct AVPlayerInterstitialEvent.Restrictions untuk membatasi pengguna melewati atau memutar ulang selama pemutaran iklan.

Menangani peristiwa iklan

Untuk menangani peristiwa iklan, terapkan protokol IMAStreamManagerDelegate. Dengan pendekatan ini, Anda dapat melacak kapan jeda iklan dimulai dan berakhir, serta mendapatkan informasi tentang setiap iklan.

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

Jalankan aplikasi Anda. Jika berhasil, Anda dapat meminta dan memutar iklan interstisial menggunakan aliran manifes penayangan Pod.