Используйте клиентские HLS-вставки для прямых трансляций

Спецификация HLS Interstitials представляет собой гибкий способ планирования и вставки рекламы в видео- или аудиопоток. При использовании клиентского подхода ваше приложение полностью контролирует время запроса и воспроизведения рекламных пауз, создавая класс AVPlayerInterstitialEvent . Этот подход не требует добавления тегов EXT-X-DATERANGE в манифесты потока контента. Клиентские HLS Interstitials позволяют динамически вставлять рекламу в любой контент без необходимости изменения манифеста потока или медиафайлов.

В этом руководстве рассматривается интеграция SDK Interactive Media Ads (IMA) в приложение видеоплеера, которое создаёт сеанс прямой трансляции с использованием вставки рекламы на сервере (SGAI) и планирует показ полноэкранных объявлений на стороне клиента. Подробнее см. в разделе «DSAI на сервере» .

Предпосылки

Прежде чем начать, вам понадобится следующее:

  • Новый проект Xcode, использующий Storyboard для пользовательского интерфейса. Подробнее см. в статье Создание проекта Xcode для приложения .

  • Google IMA SDK. Подробнее см. в статье «Настройка IMA SDK для DAI» .

  • Следующие параметры для вашего запроса прямой трансляции DAI:

    • NETWORK_CODE : Ваш сетевой код Google Ad Manager.
    • CUSTOM_ASSET_KEY : Ваша пользовательская строка, идентифицирующая событие прямой трансляции DAI. Событие прямой трансляции должно иметь тип DAI «Pod, обслуживающий манифест».

Настройте раскадровку

В файле iPhone.storyboard выполните следующие действия:

  1. Создайте объект UIView как контейнер для видеоплеера и рекламного пользовательского интерфейса.
  2. Создайте свойство adUIView класса ViewController для подключения к объекту UIView .
  3. В объекте adUIView создайте UIButton , который будет функционировать как кнопка воспроизведения.
  4. Создайте свойство playButton класса ViewController для подключения к объекту UIButton и функцию onPlayButtonTouch для обработки нажатий пользователя.

Инициализировать загрузчик рекламы

В событии viewDidLoad основного контроллера представления выполните следующие действия:

  1. Настройте видеоплеер с использованием классов AVPlayer и AVPlayerLayer .
  2. Создайте объекты IMAAdDisplayContainer и IMAAVPlayerVideoDisplay . Контейнер для показа рекламы определяет adUIView для IMA DAI SDK для вставки подвидов пользовательского интерфейса рекламы. Объект для показа видео служит мостом между логикой рекламы IMA DAI SDK и системой воспроизведения AVFoundation, отслеживая воспроизведение видеорекламы.
  3. Инициализируйте объект IMAAdsLoader с настройками воспроизведения рекламы и локализации пользовательского интерфейса рекламы.

В следующем примере загрузчик рекламы инициализируется с пустым объектом IMASettings :

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
  }

Подать заявку на трансляцию

Чтобы запросить рекламу для потока контента, создайте объект IMAPodStreamRequest и передайте его экземпляру IMAAdsLoader . При желании задайте свойство adTagParameters , чтобы указать параметры DAI и таргетинга для вашего потока.

В этом примере вызывается метод loadAdStream в событии 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)
}

В вашем производственном приложении вызовите метод loadAdStream после того, как пользователь выберет поток контента.

Обработка событий загрузки потока

Реализуйте протокол IMAAdsLoaderDelegate для обработки успешного или неудачного запроса потока:

  • В случае успешного выполнения вы получите объект IMAAdsLoadedData , содержащий IMAStreamManager . Сохраните значение streamManager.streamId для текущего сеанса DAI.
  • В случае неудачи зарегистрируйте ошибку.

В следующем примере обрабатывается событие загрузки потока и регистрируется событие «Не удалось загрузить поток»:

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

Расписание показов рекламы

Чтобы запланировать рекламную паузу, создайте объект AVPlayerInterstitialEvent . Присвойте свойству templateItems объекта события массив объектов AVPlayerItem , где каждый объект элемента содержит URL-адрес манифеста рекламного модуля.

Чтобы создать URL-адрес манифеста рекламного модуля, следуйте методу: документация по манифесту модуля HLS .

Для демонстрации следующий пример генерирует строку идентификатора модуля, используя текущее время прямой трансляции контента. Функция generatePodIdentifier возвращает идентификатор модуля в виде 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)"
}

В своем производственном приложении извлеките идентификатор модуля из источника, который предоставляет уникальные значения для каждой рекламной паузы, синхронизированные для всех зрителей прямой трансляции.

В следующем примере рекламная пауза планируется к запуску в течение следующих двух минут после того, как пользователь нажмет кнопку воспроизведения:

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

Метод scheduleAdInsertion рассчитывает время начала рекламной паузы и формирует URL-адрес манифеста рекламного модуля. Используйте этот URL-адрес для создания объекта AVPlayerInterstitialEvent .

При желании можно использовать структуру AVPlayerInterstitialEvent.Restrictions , чтобы ограничить возможность пользователя пропускать или перематывать рекламу во время воспроизведения.

Обработка рекламных событий

Для обработки рекламных событий реализуйте протокол IMAStreamManagerDelegate . Этот подход позволяет отслеживать начало и конец рекламных пауз, а также получать информацию об отдельных рекламных объявлениях.

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

Запустите приложение. В случае успеха вы сможете запрашивать и воспроизводить полноэкранную рекламу, используя поток манифеста Pod.