Utilizzare gli interstitial HLS lato client per il live streaming

La specifica HLS Interstitials introduce un modo flessibile per pianificare e inserire pubblicità in uno stream video o audio. Con l'approccio lato client, la tua applicazione assume il controllo completo su quando richiedere e riprodurre le interruzioni pubblicitarie creando la classe AVPlayerInterstitialEvent. Questo approccio non richiede i tag EXT-X-DATERANGE nei manifest dei flussi di contenuti. Gli interstitial HLS lato client consentono di inserire dinamicamente annunci in qualsiasi contenuto, senza dover modificare il manifest dello stream o i file multimediali.

Questa guida illustra l'integrazione dell'SDK Interactive Media Ads (IMA) in un'app video player che crea una sessione di live streaming con inserimento di annunci guidato dal server (SGAI) e pianifica gli interstitial lato client. Per ulteriori informazioni, vedi DAI guidata dal server.

Prerequisiti

Prima di iniziare, devi disporre di quanto segue:

  • Un nuovo progetto Xcode, che utilizza Storyboard per l'interfaccia utente. Per ulteriori informazioni, vedi Creazione di un progetto Xcode per un'app.

  • SDK IMA di Google. Per ulteriori informazioni, consulta Configurare l'SDK IMA per DAI.

  • I seguenti parametri per la richiesta di live streaming DAI:

    • NETWORK_CODE: il codice di rete Google Ad Manager.
    • CUSTOM_ASSET_KEY: la stringa personalizzata che identifica l'evento live streaming DAI. L'evento in live streaming deve avere il tipo DAI manifest pubblicazione pod.

Configurare uno storyboard

Nel file iPhone.storyboard:

  1. Crea un oggetto UIView come contenitore per il video player e l'interfaccia utente dell'annuncio.
  2. Crea una proprietà adUIView della classe ViewController per connetterti all'oggetto UIView.
  3. Nell'oggetto adUIView, crea un UIButton che funga da pulsante di riproduzione.
  4. Crea una proprietà playButton della classe ViewController per connetterti all'oggetto UIButton e a una funzione onPlayButtonTouch per gestire i tocchi dell'utente.

Inizializza un caricatore di annunci

Nell'evento viewDidLoad del controller di visualizzazione principale, procedi nel seguente modo:

  1. Configura un video player utilizzando le classi AVPlayer e AVPlayerLayer.
  2. Crea oggetti IMAAdDisplayContainer e IMAAVPlayerVideoDisplay. Il contenitore di visualizzazione dell'annuncio specifica il adUIView per l'SDK IMA DAI per inserire le sottoviste dell'interfaccia utente dell'annuncio. L'oggetto di visualizzazione video funge da ponte tra la logica degli annunci dell'SDK IMA DAI e il sistema di riproduzione AVFoundation, monitorando la riproduzione degli annunci video.
  3. Inizializza l'oggetto IMAAdsLoader con le impostazioni di riproduzione degli annunci e di localizzazione dell'interfaccia utente degli annunci.

L'esempio seguente inizializza un caricatore di annunci con un oggetto IMASettings vuoto:

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
  }

Effettuare una richiesta di stream

Per richiedere annunci per un flusso di contenuti, crea un oggetto IMAPodStreamRequest e passalo all'istanza IMAAdsLoader. (Facoltativo) Imposta la proprietà adTagParameters per fornire opzioni DAI e parametri di targeting per lo stream.

Questo esempio chiama il metodo loadAdStream nell'evento 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)
}

Nella tua app di produzione, chiama il metodo loadAdStream dopo che l'utente seleziona un flusso di contenuti.

Gestire gli eventi di caricamento dello stream

Implementa il protocollo IMAAdsLoaderDelegate per gestire l'esito positivo o negativo della richiesta di stream:

  • In caso di esito positivo, ricevi un oggetto IMAAdsLoadedData contenente IMAStreamManager. Memorizza il valore streamManager.streamId per la sessione DAI corrente.
  • In caso di errore, registra l'errore.

L'esempio seguente gestisce l'evento di caricamento dello stream e registra l'evento di caricamento dello stream non riuscito:

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

Pianificare gli inserimenti di annunci

Per pianificare un'interruzione pubblicitaria, crea un oggetto AVPlayerInterstitialEvent. Imposta la proprietà templateItems dell'oggetto evento su un array di oggetti AVPlayerItem, in cui ogni oggetto elemento contiene un URL manifest del pod di annunci.

Per creare un URL manifest di un pod pubblicitario, segui la documentazione relativa al metodo: manifest del pod HLS.

A scopo dimostrativo, l'esempio seguente genera una stringa identificatore del pod utilizzando l'ora corrente del live streaming dei contenuti. La funzione generatePodIdentifier restituisce l'identificatore del pod come 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)"
}

Nella tua app di produzione, recupera l'identificatore del pod da una sorgente che fornisce valori univoci per ogni interruzione pubblicitaria, sincronizzati per tutti gli spettatori della live streaming.

L'esempio seguente pianifica l'inizio di un'interruzione pubblicitaria entro i due minuti successivi al clic dell'utente sul pulsante di riproduzione:

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

Il metodo scheduleAdInsertion calcola l'ora di inizio dell'interruzione pubblicitaria e crea un URL manifest del pod di annunci. Utilizza questo URL per creare un oggetto AVPlayerInterstitialEvent.

Se vuoi, utilizza la struttura AVPlayerInterstitialEvent.Restrictions per impedire all'utente di saltare o riavvolgere la riproduzione dell'annuncio.

Gestire gli eventi degli annunci

Per gestire gli eventi degli annunci, implementa il protocollo IMAStreamManagerDelegate. Questo approccio ti consente di monitorare l'inizio e la fine delle interruzioni pubblicitarie e di ottenere informazioni sui singoli annunci.

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

Esegui l'app. Se l'operazione va a buon fine, puoi richiedere e riprodurre interstitial utilizzando un flusso di manifest di pubblicazione di pod.