Używanie reklam pełnoekranowych HLS po stronie klienta w przypadku transmisji na żywo

Specyfikacja pełnoekranowych reklam HLS wprowadza elastyczny sposób planowania i wstawiania reklam do strumienia wideo lub audio. W przypadku podejścia po stronie klienta aplikacja ma pełną kontrolę nad tym, kiedy wysyłać żądania dotyczące przerw na reklamy i kiedy je odtwarzać. W tym celu tworzy klasę AVPlayerInterstitialEvent. To podejście nie wymaga tagów EXT-X-DATERANGE w plikach manifestu strumienia treści. Reklamy pełnoekranowe HLS po stronie klienta umożliwiają dynamiczne wstawianie reklam do dowolnych treści bez konieczności modyfikowania pliku manifestu strumienia ani plików multimedialnych.

Ten przewodnik zawiera informacje o integracji pakietu Interactive Media Ads (IMA) SDK z aplikacją odtwarzacza wideo, która tworzy sesję transmisji na żywo z wstawianiem reklam sterowanym przez serwer (SGAI) i planuje reklamy pełnoekranowe po stronie klienta. Więcej informacji znajdziesz w artykule DAI sterowane przez serwer.

Wymagania wstępne

Zanim zaczniesz, musisz mieć:

  • Nowy projekt Xcode z interfejsem użytkownika Storyboard. Więcej informacji znajdziesz w artykule Tworzenie projektu Xcode dla aplikacji.

  • Google IMA SDK. Więcej informacji znajdziesz w artykule Konfigurowanie pakietu IMA SDK na potrzeby DAI.

  • Te parametry żądania transmisji na żywo z dynamicznym wstawianiem reklam:

    • NETWORK_CODE: kod sieci Google Ad Managera.
    • CUSTOM_ASSET_KEY: Twój ciąg niestandardowy identyfikujący zdarzenie transmisji na żywo DAI. Zdarzenie transmisji na żywo musi mieć typ dynamicznego wstawiania reklam Blok reklamowy z plikiem manifestu.

Konfigurowanie scenorysu

W pliku iPhone.storyboard wykonaj te czynności:

  1. Utwórz obiekt UIView jako kontener odtwarzacza wideo i interfejsu reklamy.
  2. Utwórz właściwość adUIView klasy ViewController, aby połączyć ją z obiektem UIView.
  3. W obiekcie adUIView utwórz UIButton, który będzie pełnił funkcję przycisku odtwarzania.
  4. Utwórz playButton usługę klasy ViewController, aby połączyć się z obiektem UIButton, oraz funkcję onPlayButtonTouch do obsługi kliknięć użytkownika.

Inicjowanie narzędzia do wczytywania reklam

W zdarzeniu viewDidLoad głównego kontrolera widoku wykonaj te czynności:

  1. Skonfiguruj odtwarzacz wideo za pomocą klas AVPlayer i AVPlayerLayer.
  2. Tworzenie obiektów IMAAdDisplayContainerIMAAVPlayerVideoDisplay. Kontener wyświetlania reklamy określa adUIView, w którym pakiet IMA DAI SDK ma wstawiać podrzędne widoki interfejsu reklamy. Obiekt wyświetlania wideo pełni funkcję pomostu między logiką reklamową pakietu IMA DAI SDK a systemem odtwarzania AVFoundation, śledząc odtwarzanie reklam wideo.
  3. Zainicjuj obiekt IMAAdsLoader za pomocą ustawień odtwarzania reklam i lokalizacji interfejsu reklam.

Poniższy przykład pokazuje, jak zainicjować moduł wczytywania reklam za pomocą pustego obiektu 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
  }

Tworzenie żądania strumienia

Aby wysłać żądanie wyświetlenia reklam w strumieniu treści, utwórz obiekt IMAPodStreamRequest i przekaż go do instancji IMAAdsLoader. Opcjonalnie ustaw właściwość adTagParameters, aby podać opcje dynamicznego wstawiania reklam i parametry kierowania dla strumienia.

W tym przykładzie wywołujemy metodę loadAdStream w zdarzeniu 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)
}

W aplikacji w wersji produkcyjnej wywołaj metodę loadAdStream po wybraniu przez użytkownika strumienia treści.

Obsługa zdarzeń wczytywania strumienia

Zaimplementuj protokół IMAAdsLoaderDelegate, aby obsługiwać powodzenie lub niepowodzenie żądania strumienia:

  • Jeśli operacja się uda, otrzymasz obiekt IMAAdsLoadedData zawierający IMAStreamManager. Zapisz wartość streamManager.streamId dla bieżącej sesji DAI.
  • W przypadku niepowodzenia zarejestruj błąd.

W tym przykładzie obsługiwane jest zdarzenie wczytania strumienia i rejestrowane jest zdarzenie nieudanego wczytania strumienia:

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

Planowanie wstawiania reklam

Aby zaplanować przerwę na reklamę, utwórz obiekt AVPlayerInterstitialEvent. Ustaw właściwość templateItems obiektu zdarzenia na tablicę obiektów AVPlayerItem, gdzie każdy obiekt elementu zawiera adres URL manifestu bloku reklamowego.

Aby utworzyć adres URL pliku manifestu bloku reklamowego, postępuj zgodnie z instrukcjami w dokumentacji Metoda: plik manifestu bloku reklamowego HLS.

Na potrzeby demonstracji poniższy przykład generuje ciąg identyfikatora zasobu za pomocą bieżącego czasu transmisji na żywo. Funkcja generatePodIdentifier zwraca identyfikator zasobnika jako 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)"
}

W aplikacji produkcyjnej pobierz identyfikator bloku reklamowego ze źródła, które zapewnia unikalne wartości dla każdej przerwy na reklamę, zsynchronizowane dla wszystkich widzów transmisji na żywo.

W tym przykładzie przerwa na reklamę jest zaplanowana tak, aby rozpoczęła się w ciągu 2 minut od kliknięcia przez użytkownika przycisku odtwarzania:

/// 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 metoda oblicza czas rozpoczęcia przerwy na reklamę i tworzy adres URL pliku manifestu bloku reklamowego. Użyj tego adresu URL, aby utworzyć obiekt AVPlayerInterstitialEvent.

Opcjonalnie możesz użyć struktury AVPlayerInterstitialEvent.Restrictions, aby ograniczyć możliwość pomijania lub przewijania przez użytkownika podczas odtwarzania reklamy.

Obsługa zdarzeń związanych z reklamami

Aby obsługiwać zdarzenia związane z reklamami, zaimplementuj protokół IMAStreamManagerDelegate. Dzięki temu możesz śledzić początek i koniec przerw na reklamy oraz uzyskiwać informacje o poszczególnych reklamach.

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

Uruchom aplikację. Jeśli się to uda, możesz wysyłać żądania reklam pełnoekranowych i je odtwarzać za pomocą strumienia manifestu wyświetlania w podzie.