Usa anuncios intersticiales de HLS del cliente para la transmisión en vivo

La especificación de Intersticiales de HLS presenta una forma flexible de programar e insertar anuncios en una transmisión de audio o video. Con el enfoque del cliente, tu aplicación tiene el control total sobre cuándo solicitar y reproducir las pausas publicitarias creando la clase AVPlayerInterstitialEvent. Este enfoque no requiere las etiquetas EXT-X-DATERANGE en los manifiestos de la transmisión de contenido. Los anuncios intersticiales HLS del cliente te permiten insertar anuncios de forma dinámica en cualquier contenido, sin necesidad de modificar el manifiesto de la transmisión ni los archivos multimedia.

En esta guía, se explica cómo integrar el SDK de anuncios multimedia interactivos (IMA) en una app de reproductor de video que crea una sesión de transmisión en vivo de inserción de anuncios guiada por el servidor (SGAI) y programa anuncios intersticiales del cliente. Para obtener más información, consulta DAI guiada por el servidor.

Requisitos previos

Antes de comenzar, necesitas lo siguiente:

  • Un nuevo proyecto de Xcode que usa Storyboard para la interfaz de usuario. Para obtener más información, consulta Cómo crear un proyecto de Xcode para una app.

  • SDK de IMA de Google Para obtener más información, consulta Configura el SDK de IMA para la DAI.

  • Los siguientes parámetros para tu solicitud de transmisión en vivo de DAI:

    • NETWORK_CODE: Es tu código de red de Google Ad Manager.
    • CUSTOM_ASSET_KEY: Es la cadena personalizada que identifica el evento de transmisión en vivo de DAI. El evento de transmisión en vivo debe tener el tipo de DAI de manifiesto de publicación de grupos de anuncios.

Cómo configurar un guion gráfico

En tu archivo iPhone.storyboard, haz lo siguiente:

  1. Crea un objeto UIView como contenedor del reproductor de video y la IU del anuncio.
  2. Crea una propiedad adUIView de la clase ViewController para conectarte con el objeto UIView.
  3. En el objeto adUIView, crea un UIButton para que funcione como botón de reproducción.
  4. Crea una propiedad playButton de la clase ViewController para conectarte con el objeto UIButton y una función onPlayButtonTouch para controlar los toques del usuario.

Inicializa un cargador de anuncios

En el evento viewDidLoad del controlador de vista principal, haz lo siguiente:

  1. Configura un reproductor de video con las clases AVPlayer y AVPlayerLayer.
  2. Crea objetos IMAAdDisplayContainer y IMAAVPlayerVideoDisplay. El contenedor de visualización de anuncios especifica el adUIView para que el SDK de DAI de IMA inserte las vistas secundarias de la IU del anuncio. El objeto de visualización de video actúa como un puente entre la lógica de anuncios del SDK de DAI de IMA y el sistema de reproducción de AVFoundation, y hace un seguimiento de la reproducción de los anuncios de video.
  3. Inicializa el objeto IMAAdsLoader con la configuración de reproducción de anuncios y localización de la IU de anuncios.

En el siguiente ejemplo, se inicializa un cargador de anuncios con un objeto IMASettings vacío:

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
  }

Realiza una solicitud de transmisión

Para solicitar anuncios para una transmisión de contenido, crea un objeto IMAPodStreamRequest y pásalo a tu instancia de IMAAdsLoader. De forma opcional, establece la propiedad adTagParameters para proporcionar opciones de DAI y parámetros de segmentación para tu transmisión.

En este ejemplo, se llama al método loadAdStream en el 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)
}

En tu app de producción, llama al método loadAdStream después de que el usuario seleccione una transmisión de contenido.

Cómo controlar eventos de carga de transmisiones

Implementa el protocolo IMAAdsLoaderDelegate para controlar el éxito o el fracaso de la solicitud de transmisión:

  • Si la operación se realiza correctamente, recibirás un objeto IMAAdsLoadedData que contiene el IMAStreamManager. Almacena el valor de streamManager.streamId para la sesión de DAI actual.
  • Si falla, registra el error.

En el siguiente ejemplo, se controla el evento de carga de la transmisión y se registra el evento de error en la carga de la transmisión:

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

Programa inserciones de anuncios

Para programar una pausa publicitaria, crea un objeto AVPlayerInterstitialEvent. Establece la propiedad templateItems del objeto de evento en un array de objetos AVPlayerItem, en el que cada objeto de elemento contiene una URL del manifiesto del pod de anuncios.

Para crear una URL de manifiesto de grupo de anuncios, consulta la documentación de Method: HLS pod manifest.

A modo de demostración, en el siguiente ejemplo, se genera una cadena de identificador de pod con la hora actual de la transmisión en vivo de contenido. La función generatePodIdentifier devuelve el identificador del pod como 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)"
}

En tu app de producción, recupera el identificador del pod de una fuente que proporcione valores únicos para cada pausa publicitaria, sincronizados para todos los usuarios que miran la transmisión en vivo.

En el siguiente ejemplo, se programa una pausa publicitaria para que comience en los próximos dos minutos después de que el usuario haga clic en el botón de reproducción:

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

El método scheduleAdInsertion calcula la hora de inicio de la pausa publicitaria y crea una URL del manifiesto del grupo de anuncios. Usa esta URL para crear un objeto AVPlayerInterstitialEvent.

De manera opcional, usa la estructura AVPlayerInterstitialEvent.Restrictions para restringir el avance o el retroceso del usuario durante la reproducción del anuncio.

Cómo controlar eventos de anuncios

Para controlar eventos de anuncios, implementa el protocolo IMAStreamManagerDelegate. Este enfoque te permite hacer un seguimiento de cuándo comienzan y terminan las pausas publicitarias, y obtener información sobre los anuncios individuales.

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

Ejecuta tu app. Si la operación se realiza correctamente, podrás solicitar y reproducir anuncios intersticiales con una transmisión de manifiesto de entrega de Pod.