Usar intersticiais HLS do lado do cliente para transmissão ao vivo

A especificação de anúncios intersticiais HLS apresenta uma maneira flexível de programar e inserir anúncios em um stream de vídeo ou áudio. Com a abordagem do lado do cliente, seu aplicativo assume o controle total de quando solicitar e reproduzir os intervalos de publicidade criando a classe AVPlayerInterstitialEvent. Essa abordagem não exige as tags EXT-X-DATERANGE nos manifestos de fluxo de conteúdo. Com os intersticiais HLS do lado do cliente, é possível inserir anúncios dinamicamente em qualquer conteúdo, sem precisar modificar o manifesto de stream ou os arquivos de mídia.

Este guia aborda a integração do SDK do Interactive Media Ads (IMA) em um app player de vídeo que cria uma sessão de transmissão ao vivo de inserção de anúncios guiada pelo servidor (SGAI) e programa intersticiais do lado do cliente. Para mais informações, consulte DAI guiada pelo servidor.

Pré-requisitos

Antes de começar, os seguintes itens são necessários:

  • Um novo projeto do Xcode, usando Storyboard para a interface do usuário. Para mais informações, consulte Como criar um projeto do Xcode para um app.

  • SDK do IMA do Google. Para mais informações, consulte Configurar o SDK do IMA para DAI.

  • Os seguintes parâmetros para sua solicitação de transmissão ao vivo de DAI:

    • NETWORK_CODE: seu código de rede do Google Ad Manager.
    • CUSTOM_ASSET_KEY: sua string personalizada que identifica o evento de transmissão ao vivo da DAI. O evento de transmissão ao vivo precisa ter o tipo de DAI de manifesto de veiculação de conjunto.

Configurar um storyboard

No arquivo iPhone.storyboard, faça o seguinte:

  1. Crie um objeto UIView como um contêiner para o player de vídeo e a interface do anúncio.
  2. Crie uma propriedade adUIView da classe ViewController para se conectar ao objeto UIView.
  3. No objeto adUIView, crie um UIButton para funcionar como um botão de reprodução.
  4. Crie uma propriedade playButton da classe ViewController para se conectar com o objeto UIButton e uma função onPlayButtonTouch para processar toques do usuário.

Inicializar um carregador de anúncios

No evento viewDidLoad do controlador de visualização principal, faça o seguinte:

  1. Configure um player de vídeo usando as classes AVPlayer e AVPlayerLayer.
  2. Crie objetos IMAAdDisplayContainer e IMAAVPlayerVideoDisplay. O contêiner de exibição de anúncios especifica o adUIView para o SDK de DAI do IMA inserir as subvisualizações da interface do anúncio. O objeto de exibição de vídeo atua como uma ponte entre a lógica de anúncios do SDK do IMA DAI e o sistema de reprodução AVFoundation, rastreando a reprodução de anúncios em vídeo.
  3. Inicialize o objeto IMAAdsLoader com as configurações de reprodução de anúncios e localização da interface do usuário de anúncios.

O exemplo a seguir inicializa um carregador de anúncios com um objeto IMASettings vazio:

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
  }

Fazer uma solicitação de stream

Para solicitar anúncios para um stream de conteúdo, crie um objeto IMAPodStreamRequest e transmita-o para sua instância IMAAdsLoader. Opcionalmente, defina a propriedade adTagParameters para fornecer opções de DAI e parâmetros de segmentação para seu stream.

Este exemplo chama o método loadAdStream no 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)
}

No app de produção, chame o método loadAdStream depois que o usuário selecionar um fluxo de conteúdo.

Processar eventos de carregamento de stream

Implemente o protocolo IMAAdsLoaderDelegate para processar o sucesso ou a falha da solicitação de stream:

  • Se a solicitação for bem-sucedida, você vai receber um objeto IMAAdsLoadedData que contém o IMAStreamManager. Armazena o valor streamManager.streamId para a sessão atual da DAI.
  • Em caso de falha, registre o erro.

O exemplo a seguir processa o evento de carregamento de stream e registra o evento de falha no carregamento do stream:

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

Programar inserções de anúncios

Para programar um intervalo comercial, crie um objeto AVPlayerInterstitialEvent. Defina a propriedade templateItems do objeto de evento como uma matriz de objetos AVPlayerItem, em que cada objeto de item contém um URL de manifesto de pod de anúncio.

Para criar um URL de manifesto de pod de anúncio, siga a documentação Método: manifesto de pod HLS.

Para fins de demonstração, o exemplo a seguir gera uma string de identificador de pod usando o horário atual da transmissão ao vivo de conteúdo. A função generatePodIdentifier retorna o identificador do 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)"
}

No app de produção, recupere o identificador do pod de uma fonte que forneça valores únicos para cada intervalo de anúncio, sincronizados para todos os espectadores da transmissão ao vivo.

O exemplo a seguir programa um intervalo de anúncio para começar nos próximos dois minutos depois que o usuário clicar no botão de reprodução:

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

O método scheduleAdInsertion calcula o horário de início do intervalo de anúncio e cria um URL de manifesto de conjunto de anúncios. Use esse URL para criar um objeto AVPlayerInterstitialEvent.

Se quiser, use a struct AVPlayerInterstitialEvent.Restrictions para restringir a capacidade do usuário de pular ou voltar durante a reprodução do anúncio.

Processar eventos de anúncio

Para processar eventos de anúncios, implemente o protocolo IMAStreamManagerDelegate. Assim, você pode acompanhar quando os intervalos começam e terminam, além de receber informações sobre anúncios individuais.

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

Execute o app. Se tudo der certo, você poderá solicitar e veicular anúncios intersticiais usando um fluxo de manifesto de veiculação de pods.