Utiliser des interstitiels HLS côté client pour les diffusions en direct

La spécification HLS Interstitials offre un moyen flexible de planifier et d'insérer des annonces dans un flux vidéo ou audio. Avec l'approche côté client, votre application contrôle entièrement le moment où les pauses publicitaires doivent être demandées et lues en créant la classe AVPlayerInterstitialEvent. Cette approche ne nécessite pas les tags EXT-X-DATERANGE dans les fichiers manifeste du flux de contenu. Les interstitiels HLS côté client vous permettent d'insérer dynamiquement des annonces dans n'importe quel contenu, sans avoir à modifier le fichier manifeste du flux ni les fichiers multimédias.

Ce guide explique comment intégrer le SDK Interactive Media Ads (IMA) dans une application de lecteur vidéo qui crée une session de diffusion en direct avec insertion d'annonces guidée par le serveur (SGAI) et planifie des interstitiels côté client. Pour en savoir plus, consultez DAI guidée par le serveur.

Prérequis

Avant de commencer, vous avez besoin des éléments suivants :

  • Un nouveau projet Xcode utilisant Storyboard pour l'interface utilisateur. Pour en savoir plus, consultez Créer un projet Xcode pour une application.

  • SDK IMA de Google. Pour en savoir plus, consultez Configurer le SDK IMA pour DAI.

  • Les paramètres suivants pour votre demande de diffusion en direct DAI :

    • NETWORK_CODE : code de réseau Google Ad Manager.
    • CUSTOM_ASSET_KEY : chaîne personnalisée identifiant l'événement de diffusion en direct DAI. L'événement de diffusion en direct doit être de type "Fichier manifeste de la diffusion de séries d'annonces" pour l'insertion dynamique d'annonces.

Configurer un storyboard

Dans votre fichier iPhone.storyboard, procédez comme suit :

  1. Créez un objet UIView en tant que conteneur pour le lecteur vidéo et l'UI de l'annonce.
  2. Créez une propriété adUIView de la classe ViewController pour vous connecter à l'objet UIView.
  3. Dans l'objet adUIView, créez un UIButton qui servira de bouton de lecture.
  4. Créez une propriété playButton de la classe ViewController pour vous connecter à l'objet UIButton et une fonction onPlayButtonTouch pour gérer les appuis de l'utilisateur.

Initialiser un chargeur d'annonces

Dans l'événement viewDidLoad du contrôleur de vue principal, procédez comme suit :

  1. Configurez un lecteur vidéo à l'aide des classes AVPlayer et AVPlayerLayer.
  2. Créez des objets IMAAdDisplayContainer et IMAAVPlayerVideoDisplay. Le conteneur d'affichage des annonces spécifie le adUIView pour que le SDK IMA DAI insère les sous-vues de l'UI des annonces. L'objet d'affichage vidéo sert de pont entre la logique publicitaire du SDK IMA DAI et le système de lecture AVFoundation, en suivant la lecture des annonces vidéo.
  3. Initialisez l'objet IMAAdsLoader avec les paramètres de lecture et de localisation de l'UI des annonces.

L'exemple suivant initialise un chargeur d'annonces avec un objet IMASettings vide :

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
  }

Envoyer une requête de flux

Pour demander des annonces pour un flux de contenu, créez un objet IMAPodStreamRequest et transmettez-le à votre instance IMAAdsLoader. Vous pouvez également définir la propriété adTagParameters pour fournir des options d'insertion dynamique d'annonces et des paramètres de ciblage pour votre flux.

Cet exemple appelle la méthode loadAdStream dans l'événement 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)
}

Dans votre application de production, appelez la méthode loadAdStream une fois que l'utilisateur a sélectionné un flux de contenu.

Gérer les événements de chargement de flux

Implémentez le protocole IMAAdsLoaderDelegate pour gérer la réussite ou l'échec de la requête de flux :

  • En cas de réussite, vous recevez un objet IMAAdsLoadedData contenant le IMAStreamManager. Stockez la valeur streamManager.streamId pour la session DAI actuelle.
  • En cas d'échec, enregistrez l'erreur.

L'exemple suivant gère l'événement de flux chargé et consigne l'événement d'échec du chargement du flux :

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

Planifier les insertions d'annonces

Pour planifier une coupure publicitaire, créez un objet AVPlayerInterstitialEvent. Définissez la propriété templateItems de l'objet d'événement sur un tableau d'objets AVPlayerItem, où chaque objet d'élément contient une URL de fichier manifeste de bloc d'annonces.

Pour créer une URL de fichier manifeste de bloc d'annonces, consultez la documentation Méthode : fichier manifeste de bloc HLS.

À des fins de démonstration, l'exemple suivant génère une chaîne d'identifiant de pod à l'aide de l'heure actuelle du flux de contenu en direct. La fonction generatePodIdentifier renvoie l'identifiant du pod sous la forme 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)"
}

Dans votre application de production, récupérez l'identifiant du pod à partir d'une source qui fournit des valeurs uniques pour chaque coupure publicitaire, synchronisées pour tous les spectateurs du flux en direct.

L'exemple suivant planifie une coupure publicitaire qui doit commencer dans les deux minutes suivant le clic de l'utilisateur sur le bouton de lecture :

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

La méthode scheduleAdInsertion calcule l'heure de début de la coupure publicitaire et crée une URL de fichier manifeste de série d'annonces. Utilisez cette URL pour créer un objet AVPlayerInterstitialEvent.

Vous pouvez également utiliser la structure AVPlayerInterstitialEvent.Restrictions pour empêcher l'utilisateur de passer ou de rembobiner les annonces pendant leur lecture.

Gérer les événements d'annonces

Pour gérer les événements publicitaires, implémentez le protocole IMAStreamManagerDelegate. Cette approche vous permet de suivre le début et la fin des pauses publicitaires, et d'obtenir des informations sur les annonces individuelles.

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

Exécutez votre application. Si tout se passe bien, vous pouvez demander et lire des interstitiels à l'aide d'un flux de fichier manifeste de diffusion de pods.