設定全方位服務 DAI

選取平台: HTML5 Android iOS tvOS Cast Roku

本指南說明如何將 IMA DAI SDK 整合至影片播放器應用程式。如要查看或按照完整的整合範例操作,請從 GitHub 下載 BasicExample。

IMA DAI 總覽

如本指南所示,導入 IMA DAI 涉及三個主要 SDK 元件:

  • IMAAdDisplayContainer: 位於影片播放元素上方的容器物件,用於容納廣告 UI 元素。
  • IMAAdsLoader: 這個物件會要求串流,並處理串流要求回應物件觸發的事件。您只需要例項化一個廣告載入器,即可在應用程式的整個生命週期中重複使用。
  • IMAStreamRequest: IMAVODStreamRequest 或 IMALiveStreamRequest:定義串流請求的物件。串流請求可以是隨選影片或直播。直播請求會指定素材資源金鑰,而 VOD 請求則會指定 CMS ID 和影片 ID。這兩種要求類型都可以選擇加入 API 金鑰 (用於存取指定串流),以及 Google Ad Manager 聯播網代碼 (用於讓 IMA SDK 按照 Google Ad Manager 設定處理廣告 ID)。
  • IMAStreamManager: 這個物件會處理動態廣告插播串流,以及與 DAI 後端的互動。串流管理工具也會處理追蹤連線偵測 (ping),並將串流和廣告事件轉送給發布商。

必要條件

開始前,請先備妥下列項目:

建立新的 Xcode 專案

在 Xcode 中,使用 Objective-C 建立新的 tvOS 專案。使用 BasicExample 做為專案名稱。

將 IMA DAI SDK 新增至 Xcode 專案

請選擇偏好的方法安裝 IMA SDK。

建議做法:使用 Swift Package Manager 安裝 SDK

互動式媒體廣告 SDK 自 4.8.2 版起支援 Swift Package Manager。請按照下列步驟匯入 Swift 套件。

  1. 在 Xcode 中,依序前往「File」>「Add Packages」,安裝 GoogleInteractiveMediaAds Swift Package。

  2. 在出現的提示詞框,搜尋「GoogleInteractiveMediaAds Swift Package GitHub repository」:

    https://github.com/googleads/swift-package-manager-google-interactive-media-ads-tvos
    
  3. 選取要使用的 GoogleInteractiveMediaAds Swift Package 版本。新專案建議使用「Up to Next Major Version」。

完成後,Xcode 會解析套件依附元件,並在背景下載。如要進一步瞭解如何新增套件依附元件,請參閱 Apple 的文章。

手動下載及安裝 SDK

如果不想使用 Swift Package Manager,請下載 IMA SDK 並手動新增至專案。

匯入 IMA SDK

使用 import 陳述式新增 IMA 架構:

Objective-C

#import "ViewController.h"
#import <AVKit/AVKit.h>

@import GoogleInteractiveMediaAds;

Swift

import AVFoundation
import GoogleInteractiveMediaAds
import UIKit

建立影片播放器並整合 IMA SDK

以下範例會初始化 IMA SDK:

Objective-C

// Live stream asset key, VOD content source and video IDs, and backup content URL.
static NSString *const kAssetKey = @"c-rArva4ShKVIAkNfy6HUQ";
static NSString *const kContentSourceID = @"2548831";
static NSString *const kVideoID = @"tears-of-steel";
static NSString *const kNetworkCode = @"21775744923";
static NSString *const kBackupStreamURLString =
    @"http://googleimadev-vh.akamaihd.net/i/big_buck_bunny/bbb-,480p,720p,1080p,.mov.csmil/"
    @"master.m3u8";
static const StreamType kDefaultStreamType = StreamTypeLive;

@interface ViewController () <IMAAdsLoaderDelegate,
                              IMAStreamManagerDelegate,
                              AVPlayerViewControllerDelegate>
@property(nonatomic) IMAAdsLoader *adsLoader;
@property(nonatomic) IMAAdDisplayContainer *adDisplayContainer;
@property(nonatomic) UIView *adContainerView;
@property(nonatomic) id<IMAVideoDisplay> videoDisplay;
@property(nonatomic) IMAStreamManager *streamManager;
@property(nonatomic) AVPlayerViewController *playerViewController;
@property(nonatomic, getter=isAdBreakActive) BOOL adBreakActive;
@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.view.backgroundColor = [UIColor blackColor];
  self.streamType = kDefaultStreamType;
  [self setupAdsLoader];
  [self setupPlayer];
  [self setupAdContainer];
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  [self requestStream];
}

- (void)setupPlayer {
  // Create a stream video player.
  AVPlayer *player = [[AVPlayer alloc] init];
  self.playerViewController = [[AVPlayerViewController alloc] init];
  self.playerViewController.player = player;

  // Attach video player to view hierarchy.
  [self addChildViewController:self.playerViewController];
  [self.view addSubview:self.playerViewController.view];
  self.playerViewController.view.frame = self.view.bounds;
  [self.playerViewController didMoveToParentViewController:self];
}

Swift

class ViewController:
  UIViewController,
  IMAAdsLoaderDelegate,
  IMAStreamManagerDelegate,
  AVPlayerViewControllerDelegate
{
  // Live stream asset key, VOD content source and video IDs, Google Ad Manager network code, and
  // backup content URL.
  static let assetKey = "c-rArva4ShKVIAkNfy6HUQ"
  static let contentSourceID = "2548831"
  static let videoID = "tears-of-steel"
  static let networkCode = "21775744923"
  static let backupStreamURLString =
    "http://googleimadev-vh.akamaihd.net/i/big_buck_bunny/bbb-,480p,720p,1080p,.mov.csmil/master.m3u8"

  var adsLoader: IMAAdsLoader?
  var videoDisplay: IMAAVPlayerVideoDisplay!
  var adDisplayContainer: IMAAdDisplayContainer?
  var adContainerView: UIView?
  private var streamManager: IMAStreamManager?
  private var contentPlayhead: IMAAVPlayerContentPlayhead?
  private var playerViewController: AVPlayerViewController!
  private var userSeekTime = 0.0
  private var adBreakActive = false

  private enum StreamType {
    case live
    /// Video on demand.
    case vod
  }

  /// Set the stream type here.
  private let currentStreamType: StreamType = .live

  deinit {
    NotificationCenter.default.removeObserver(self)
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    self.view.backgroundColor = UIColor.black

    setupAdsLoader()
    setupPlayer()
    setupAdContainer()
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    requestStream()
  }

  func setupPlayer() {
    let player = AVPlayer()
    let playerViewController = AVPlayerViewController()
    playerViewController.delegate = self
    playerViewController.player = player

    // Set up our content playhead and contentComplete callback.
    contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: player)
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(ViewController.contentDidFinishPlaying(_:)),
      name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
      object: player.currentItem)

    self.addChild(playerViewController)
    playerViewController.view.frame = self.view.bounds
    self.view.insertSubview(playerViewController.view, at: 0)
    playerViewController.didMove(toParent: self)
    self.playerViewController = playerViewController
  }

在 viewDidLoad() 中,setupAdsLoader() 會建立 IMAAdsLoader,setupPlayer() 會建立 AVPlayerViewController,setupAdContainer() 則會準備 UIView 以顯示廣告。當檢視畫面顯示時,viewDidAppear() 會呼叫 requestStream() 來要求 DAI 串流。

為定義串流請求參數,這個範例使用常數,例如直播的 asset key,或 VOD 串流的 content source ID 和 video ID。範例也使用下列元件管理 IMA SDK:

  • adsLoader:處理傳送至 Google Ad Manager 的串流請求。建議在應用程式生命週期中使用單一執行個體。
  • videoDisplay:IMAVideoDisplay 實作,可讓 IMA 控制影片播放,並使用 AVPlayer 追蹤播放事件。
  • adDisplayContainer:管理用於算繪廣告 UI 元素的檢視畫面,並在廣告插播期間處理 UI 焦點。
  • streamManager:管理廣告和內容合併串流的播放,並使用委派傳送廣告生命週期事件。
  • playerViewController:tvOS 播放器,用於顯示 IMA SDK 管理的影片串流。
  • adBreakActive:布林值標記,指出是否正在播放廣告插播,用於防止跳過廣告及管理 UI 焦點。

導入 IMAAdsLoader

接著,將 IMAAdsLoader 例項化,並將廣告容器檢視區塊附加至檢視區塊階層。

Objective-C

- (void)setupAdsLoader {
  self.adsLoader = [[IMAAdsLoader alloc] init];
  self.adsLoader.delegate = self;
}

- (void)setupAdContainer {
  // Attach the ad container to the view hierarchy on top of the player.
  self.adContainerView = [[UIView alloc] init];
  [self.view addSubview:self.adContainerView];
  self.adContainerView.frame = self.view.bounds;
  // Keep hidden initially, until an ad break.
  self.adContainerView.hidden = YES;
}

Swift

func setupAdsLoader() {
  let adsLoader = IMAAdsLoader(settings: nil)
  adsLoader.delegate = self
  self.adsLoader = adsLoader
}

func setupAdContainer() {
  // Attach the ad container to the view hierarchy on top of the player.
  let adContainerView = UIView()
  self.view.addSubview(adContainerView)
  adContainerView.frame = self.view.bounds
  // Keep hidden initially, until an ad break.
  adContainerView.isHidden = true
  self.adContainerView = adContainerView
}

提出串流要求

建立幾個常數來保存串流資訊,然後實作串流要求函式來提出要求。

Objective-C

- (void)requestStream {
  self.videoDisplay =
      [[IMAAVPlayerVideoDisplay alloc] initWithAVPlayer:self.playerViewController.player];
  self.adDisplayContainer = [[IMAAdDisplayContainer alloc] initWithAdContainer:self.adContainerView
                                                                viewController:self];

  // Use the streamType property to determine which request to create.
  switch (self.streamType) {
    case StreamTypeLive: {
      IMALiveStreamRequest *request =
          [[IMALiveStreamRequest alloc] initWithAssetKey:kAssetKey
                                             networkCode:kNetworkCode
                                      adDisplayContainer:self.adDisplayContainer
                                            videoDisplay:self.videoDisplay
                                             userContext:nil];
      NSLog(@"IMA: Requesting Live Stream with Asset Key: %@.", kAssetKey);
      request.useHLSInterstitials = YES;
      [self.adsLoader requestStreamWithRequest:request];
      break;
    }
    case StreamTypeVOD: {
      IMAVODStreamRequest *request =
          [[IMAVODStreamRequest alloc] initWithContentSourceID:kContentSourceID
                                                       videoID:kVideoID
                                                   networkCode:kNetworkCode
                                            adDisplayContainer:self.adDisplayContainer
                                                  videoDisplay:self.videoDisplay
                                                   userContext:nil];
      NSLog(@"IMA: Requesting VOD Stream with Video ID: %@.", kVideoID);
      [self.adsLoader requestStreamWithRequest:request];
      break;
    }
  }
}

Swift

func requestStream() {
  guard let playerViewController = self.playerViewController else { return }
  guard let adContainerView = self.adContainerView else { return }
  guard let adsLoader = self.adsLoader else { return }

  self.videoDisplay = IMAAVPlayerVideoDisplay(avPlayer: playerViewController.player!)
  let adDisplayContainer = IMAAdDisplayContainer(
    adContainer: adContainerView, viewController: self)
  self.adDisplayContainer = adDisplayContainer

  switch self.currentStreamType {
  case .live:
    // Create a live stream request.
    let request = IMALiveStreamRequest(
      assetKey: ViewController.assetKey,
      networkCode: ViewController.networkCode,
      adDisplayContainer: adDisplayContainer,
      videoDisplay: self.videoDisplay,
      pictureInPictureProxy: nil,
      userContext: nil)
    print("IMA: Requesting Live Stream with asset key \(ViewController.assetKey)")
    request.useHLSInterstitials = true
    adsLoader.requestStream(with: request)

  case .vod:
    // Create a VOD stream request.
    let request = IMAVODStreamRequest(
      contentSourceID: ViewController.contentSourceID,
      videoID: ViewController.videoID,
      networkCode: ViewController.networkCode,
      adDisplayContainer: adDisplayContainer,
      videoDisplay: self.videoDisplay,
      pictureInPictureProxy: nil,
      userContext: nil)
    print(
      "IMA: Requesting VOD Stream with content source ID \(ViewController.contentSourceID) and "
        + "video ID \(ViewController.videoID)")
    adsLoader.requestStream(with: request)
  }
}

處理串流事件

IMAAdsLoader 和 IMAStreamManager 會觸發事件,處理初始化、錯誤和串流狀態變更。這些事件會透過 IMAAdsLoaderDelegate 和 IMAStreamManagerDelegate 通訊協定觸發。監聽廣告載入事件,並初始化串流。如果廣告無法載入,請改為播放備用串流。

Objective-C

- (void)playBackupStream {
  NSURL *backupStreamURL = [NSURL URLWithString:kBackupStreamURLString];
  [self.videoDisplay loadStream:backupStreamURL withSubtitles:@[]];
  [self.videoDisplay play];
  [self startMediaSession];
}

- (void)startMediaSession {
  [[AVAudioSession sharedInstance] setActive:YES error:nil];
  [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
}

#pragma mark - IMAAdsLoaderDelegate

- (void)adsLoader:(IMAAdsLoader *)loader adsLoadedWithData:(IMAAdsLoadedData *)adsLoadedData {
  // Initialize and listen to stream manager's events.
  self.streamManager = adsLoadedData.streamManager;
  self.streamManager.delegate = self;
  [self.streamManager initializeWithAdsRenderingSettings:nil];
  NSLog(@"Stream created with: %@.", self.streamManager.streamId);
}

- (void)adsLoader:(IMAAdsLoader *)loader failedWithErrorData:(IMAAdLoadingErrorData *)adErrorData {
  // Fall back to playing the backup stream.
  NSLog(@"Error loading ads: %@", adErrorData.adError.message);
  [self playBackupStream];
}

Swift

@objc func contentDidFinishPlaying(_ notification: Notification) {
  guard let adsLoader = self.adsLoader else { return }
  adsLoader.contentComplete()
}

func startMediaSession() {
  try? AVAudioSession.sharedInstance().setActive(true, options: [])
  try? AVAudioSession.sharedInstance().setCategory(.playback)
}

// MARK: - IMAAdsLoaderDelegate

func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
  let streamManager = adsLoadedData.streamManager!
  streamManager.delegate = self
  streamManager.initialize(with: nil)
  self.streamManager = streamManager
}

func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
  print("Error loading ads: \(adErrorData.adError.message)")
  let streamUrl = URL(string: ViewController.backupStreamURLString)
  self.videoDisplay.loadStream(streamUrl!, withSubtitles: [])
  self.videoDisplay.play()
  playerViewController.player?.play()
}

處理記錄和錯誤事件

串流管理工具委派物件可處理多個事件,但如果是基本導入作業,最重要的用途是執行事件記錄、防止在廣告播放期間執行跳轉動作,以及處理錯誤。

Objective-C

#pragma mark - IMAStreamManagerDelegate

- (void)streamManager:(IMAStreamManager *)streamManager didReceiveAdEvent:(IMAAdEvent *)event {
  NSLog(@"StreamManager event (%@).", event.typeString);
  switch (event.type) {
    case kIMAAdEvent_STREAM_STARTED: {
      [self startMediaSession];
      break;
    }
    case kIMAAdEvent_STARTED: {
      // Log extended data.
      NSString *extendedAdPodInfo = [[NSString alloc]
          initWithFormat:@"Showing ad %zd/%zd, bumper: %@, title: %@, description: %@, contentType:"
                         @"%@, pod index: %zd, time offset: %lf, max duration: %lf.",
                         event.ad.adPodInfo.adPosition, event.ad.adPodInfo.totalAds,
                         event.ad.adPodInfo.isBumper ? @"YES" : @"NO", event.ad.adTitle,
                         event.ad.adDescription, event.ad.contentType, event.ad.adPodInfo.podIndex,
                         event.ad.adPodInfo.timeOffset, event.ad.adPodInfo.maxDuration];

      NSLog(@"%@", extendedAdPodInfo);
      break;
    }
    case kIMAAdEvent_AD_BREAK_STARTED: {
      self.adContainerView.hidden = NO;
      // Trigger an update to send focus to the ad display container.
      self.adBreakActive = YES;
      [self setNeedsFocusUpdate];
      break;
    }
    case kIMAAdEvent_AD_BREAK_ENDED: {
      self.adContainerView.hidden = YES;
      // Trigger an update to send focus to the content player.
      self.adBreakActive = NO;
      [self setNeedsFocusUpdate];
      break;
    }
    case kIMAAdEvent_ICON_FALLBACK_IMAGE_CLOSED: {
      // Resume playback after the user has closed the dialog.
      [self.videoDisplay play];
      break;
    }
    default:
      break;
  }
}

- (void)streamManager:(IMAStreamManager *)streamManager didReceiveAdError:(IMAAdError *)error {
  // Fall back to playing the backup stream.
  NSLog(@"StreamManager error: %@", error.message);
  [self playBackupStream];
}

Swift

// MARK: - IMAStreamManagerDelegate
func streamManager(_ streamManager: IMAStreamManager, didReceive event: IMAAdEvent) {
  print("StreamManager event \(event.typeString).")
  switch event.type {
  case IMAAdEventType.STREAM_STARTED:
    self.startMediaSession()
  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:
    if let adContainerView = self.adContainerView {
      adContainerView.isHidden = false
    }
    // Trigger an update to send focus to the ad display container.
    adBreakActive = true
    setNeedsFocusUpdate()
    break
  case IMAAdEventType.AD_BREAK_ENDED:
    if let adContainerView = self.adContainerView {
      adContainerView.isHidden = true
    }
    // Trigger an update to send focus to the content player.
    adBreakActive = false
    setNeedsFocusUpdate()
    break
  case IMAAdEventType.ICON_FALLBACK_IMAGE_CLOSED:
    // Resume playback after the user has closed the dialog.
    self.videoDisplay.play()
    break
  default:
    break
  }
}

func streamManager(_ streamManager: IMAStreamManager, didReceive error: IMAAdError) {
  print("StreamManager error: \(error.message ?? "Unknown Error")")
}

大功告成!您現在可以使用 IMA DAI SDK 請求及顯示廣告。如要瞭解更多進階 SDK 功能,請參閱其他指南或 GitHub 上的範例。