本指南說明如何將 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 13 以上版本
- IMA SDK 安裝方法:
- Swift Package Manager (建議)
- 下載 tvOS 專用 IMA SDK
建立新的 Xcode 專案
在 Xcode 中,使用 Objective-C 建立新的 tvOS 專案。使用 BasicExample 做為專案名稱。
在 Xcode 專案中加入 IMA DAI SDK
請選擇偏好的方法來安裝 IMA SDK。
建議做法:使用 Swift Package Manager 安裝 SDK
互動式媒體廣告 SDK 自 4.8.2 版起支援 Swift Package Manager。請按照下列步驟匯入 Swift 套件。
在 Xcode 中,依序前往「File」>「Add Packages」,安裝 GoogleInteractiveMediaAds Swift Package。
在出現的提示詞框,搜尋「GoogleInteractiveMediaAds Swift Package GitHub repository」:
https://github.com/googleads/swift-package-manager-google-interactive-media-ads-tvos選取要使用的 GoogleInteractiveMediaAds Swift Package 版本。新專案建議使用「Up to Next Major Version」。
完成後,Xcode 會解析套件依附元件,並在背景下載。如要進一步瞭解如何新增套件依附元件,請參閱 Apple 的文章。
手動下載及安裝 SDK
如果不想使用 Swift Package Manager,請下載 IMA SDK 並手動新增至專案。
匯入 IMA SDK
使用匯入陳述式新增 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 上的範例。