라이브 스트림에 클라이언트 측 HLS 인터스티셜 사용

HLS 전면 광고 사양은 동영상 또는 오디오 스트림에 광고를 예약하고 삽입하는 유연한 방법을 도입합니다. 클라이언트 측 접근 방식을 사용하면 애플리케이션에서 AVPlayerInterstitialEvent 클래스를 만들어 광고 시점을 요청하고 재생하는 시점을 완전히 제어할 수 있습니다. 이 접근 방식에서는 콘텐츠 스트림 매니페스트에 EXT-X-DATERANGE 태그가 필요하지 않습니다. 클라이언트 측 HLS 광고를 사용하면 스트림 매니페스트나 미디어 파일을 수정하지 않고도 콘텐츠에 광고를 동적으로 삽입할 수 있습니다.

이 가이드에서는 서버 안내 광고 삽입 (SGAI) 라이브 스트림 세션을 만들고 클라이언트 측에서 전면 광고를 예약하는 동영상 플레이어 앱에 양방향 미디어 광고 (IMA) SDK를 통합하는 방법을 설명합니다. 자세한 내용은 서버 안내 DAI를 참고하세요.

기본 요건

시작하기 전에 다음이 필요합니다.

  • 사용자 인터페이스에 Storyboard를 사용하는 새 Xcode 프로젝트 자세한 내용은 앱용 Xcode 프로젝트 만들기를 참고하세요.

  • Google IMA SDK 자세한 내용은 DAI용 IMA SDK 설정을 참고하세요.

  • DAI 라이브 스트림 요청의 다음 매개변수:

    • NETWORK_CODE: Google Ad Manager 네트워크 코드입니다.
    • CUSTOM_ASSET_KEY: DAI 라이브 스트림 이벤트를 식별하는 맞춤 문자열입니다. 라이브 스트림 이벤트의 DAI 유형은 광고 모음 게재 매니페스트여야 합니다.

스토리보드 구성

iPhone.storyboard 파일에서 다음을 실행합니다.

  1. 동영상 플레이어와 광고 UI의 컨테이너로 UIView 객체를 만듭니다.
  2. UIView 객체와 연결하기 위해 ViewController 클래스의 adUIView 속성을 만듭니다.
  3. adUIView 객체에서 재생 버튼으로 작동하는 UIButton을 만듭니다.
  4. UIButton 객체와 연결하고 사용자 탭을 처리하는 onPlayButtonTouch 함수를 ViewController 클래스의 playButton 속성으로 만듭니다.

광고 로더 초기화

기본 뷰 컨트롤러의 viewDidLoad 이벤트에서 다음을 실행합니다.

  1. AVPlayerAVPlayerLayer 클래스를 사용하여 동영상 플레이어를 설정합니다.
  2. IMAAdDisplayContainerIMAAVPlayerVideoDisplay 객체를 만듭니다. 광고 표시 컨테이너는 IMA DAI SDK가 광고 UI 하위 뷰를 삽입할 adUIView를 지정합니다. 동영상 디스플레이 객체는 IMA DAI SDK의 광고 로직과 AVFoundation 재생 시스템 간의 브리지 역할을 하며 동영상 광고 재생을 추적합니다.
  3. 광고 재생 및 광고 UI 현지화 설정으로 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 옵션과 타겟팅 매개변수를 제공합니다.

이 예에서는 viewDidAppear 이벤트에서 loadAdStream 메서드를 호출합니다.

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 프로토콜을 구현하여 스트림 요청의 성공 또는 실패를 처리합니다.

  • 성공하면 IMAStreamManager이 포함된 IMAAdsLoadedData 객체가 수신됩니다. 현재 DAI 세션의 streamManager.streamId 값을 저장합니다.
  • 실패 시 오류를 로깅합니다.

다음 예에서는 스트림 로드 이벤트를 처리하고 스트림 로드 실패 이벤트를 로깅합니다.

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

프로덕션 앱에서 라이브 스트림의 모든 시청자에 대해 동기화된 각 광고 시점에 고유한 값을 제공하는 소스에서 포드 식별자를 가져옵니다.

다음 예에서는 사용자가 재생 버튼을 클릭한 후 2분 이내에 시작되도록 광고 시간을 예약합니다.

/// 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 제공 매니페스트 스트림을 사용하여 전면 광고를 요청하고 재생할 수 있습니다.