Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Swift/SGAIClientSideExample/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
source 'https://github.com/CocoaPods/Specs.git'

platform :tvos, '17'
project 'SGAIClientSideExample.xcodeproj'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
objects = {

/* Begin PBXBuildFile section */
128B5F3B2FB1396E00BC9E0C /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128B5F3A2FB1396E00BC9E0C /* Secrets.swift */; };
C7B0262FB6644A3ABE00B648 /* libPods-SGAIClientSideExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A7EDE97B02BE38F086895B4E /* libPods-SGAIClientSideExample.a */; };
D21414D725755FA90019940F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21414D625755FA90019940F /* AppDelegate.swift */; };
D21414D925755FA90019940F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21414D825755FA90019940F /* ViewController.swift */; };
D21414DE25755FAA0019940F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D21414DD25755FAA0019940F /* Assets.xcassets */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
128B5F3A2FB1396E00BC9E0C /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
A7EDE97B02BE38F086895B4E /* libPods-SGAIClientSideExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SGAIClientSideExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
B8F57A6FE0CFC97F17FEE6EB /* Pods-SGAIClientSideExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SGAIClientSideExample.debug.xcconfig"; path = "Target Support Files/Pods-SGAIClientSideExample/Pods-SGAIClientSideExample.debug.xcconfig"; sourceTree = "<group>"; };
D21414D325755FA90019940F /* SGAIClientSideExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SGAIClientSideExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -74,6 +76,7 @@
D21414D525755FA90019940F /* app */ = {
isa = PBXGroup;
children = (
128B5F3A2FB1396E00BC9E0C /* Secrets.swift */,
D21414D625755FA90019940F /* AppDelegate.swift */,
D21414D825755FA90019940F /* ViewController.swift */,
D21414DD25755FAA0019940F /* Assets.xcassets */,
Expand Down Expand Up @@ -112,7 +115,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1210;
LastUpgradeCheck = 1620;
LastUpgradeCheck = 2600;
TargetAttributes = {
D21414D225755FA90019940F = {
CreatedOnToolsVersion = 12.1;
Expand Down Expand Up @@ -157,10 +160,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-SGAIClientSideExample/Pods-SGAIClientSideExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-SGAIClientSideExample/Pods-SGAIClientSideExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SGAIClientSideExample/Pods-SGAIClientSideExample-frameworks.sh\"\n";
Expand Down Expand Up @@ -196,6 +203,7 @@
buildActionMask = 2147483647;
files = (
D21414D925755FA90019940F /* ViewController.swift in Sources */,
128B5F3B2FB1396E00BC9E0C /* Secrets.swift in Sources */,
D21414D725755FA90019940F /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -239,6 +247,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = Z95L3YYF93;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
Expand All @@ -260,6 +269,7 @@
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = appletvos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TVOS_DEPLOYMENT_TARGET = 18.0;
Expand Down Expand Up @@ -302,6 +312,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = Z95L3YYF93;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
Expand All @@ -316,6 +327,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = appletvos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TVOS_DEPLOYMENT_TARGET = 18.0;
Expand All @@ -330,7 +342,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = Z95L3YYF93;
INFOPLIST_FILE = app/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -351,7 +362,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = Z95L3YYF93;
INFOPLIST_FILE = app/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down
26 changes: 26 additions & 0 deletions Swift/SGAIClientSideExample/app/Secrets.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

// Secrets provided by your Google Ad Manager account.
enum Secrets {
// Add your Stream Create Authentication Key from Google Ad Manager.
// See https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/live/stream-session-requests
static let streamCreateSecret = ""

// Add your Pod Resource Authentication Key from Google Ad Manager.
// See https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/live/pod-manifest-requests
static let manifestSecret = ""
}
119 changes: 100 additions & 19 deletions Swift/SGAIClientSideExample/app/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

// [START set_up_view_controller]
import AVFoundation
import CryptoKit
import GoogleInteractiveMediaAds
import UIKit

Expand All @@ -35,11 +36,11 @@ class ViewController:
// 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"
/// Replace with your Google Ad Manager network code.
static let networkCode = "YOUR_NETWORK_CODE"

/// Google DAI livestream custom asset key.
static let customAssetKey = "sgai-hls-live"
/// Replace with your Google Ad Manager custom asset key.
static let customAssetKey = "YOUR_CUSTOM_ASSET_KEY"

// Set your ad break duration.
static let adBreakDurationMs = 10000
Expand Down Expand Up @@ -68,6 +69,10 @@ class ViewController:
/// The ad stream session ID, set when the ad stream is loaded.
private var adStreamSessionId: String?

// Set the duration for the auth token to be valid, in seconds. This is set to 60 seconds but
// can be set to any value depending on your use case.
private var expirationDuration: TimeInterval = 60

private var playerViewController: AVPlayerViewController!

deinit {
Expand Down Expand Up @@ -137,7 +142,7 @@ class ViewController:
player.play()
}

/// Makes a stream request to Google Ad Manager.
// Makes a stream request to Google Ad Manager.
private func loadAdStream() {
let streamRequest = IMAPodStreamRequest(
networkCode: StreamParameters.networkCode,
Expand All @@ -147,8 +152,30 @@ class ViewController:
pictureInPictureProxy: nil,
userContext: nil)

// Register a streaming session on Google Ad Manager DAI servers.
adsLoader.requestStream(with: streamRequest)
// Generate and set the authToken using the secret from Secrets.swift.
if !Secrets.streamCreateSecret.isEmpty {
let expirationTime = Int(Date().timeIntervalSince1970) + Int(expirationDuration)
let params: [String: Any] = [
"custom_asset_key": StreamParameters.customAssetKey,
"exp": expirationTime,
"network_code": StreamParameters.networkCode,
]
if let authToken = generateAuthToken(
params: params,
secret: Secrets.streamCreateSecret
) {
streamRequest.authToken = authToken
print("AdsManager: Making PodStreamRequest with auth.")
// Register a streaming session on Google Ad Manager DAI servers.
adsLoader.requestStream(with: streamRequest)
} else {
print("AdsManager: Failed to generate auth token. Stream request aborted.")
}
} else {
print("AdsManager: Making PodStreamRequest without auth (streamCreateSecret is empty).")
// Register a streaming session on Google Ad Manager DAI servers.
adsLoader.requestStream(with: streamRequest)
}
}
// [END make_stream_request]

Expand Down Expand Up @@ -179,13 +206,40 @@ class ViewController:
seconds: player.currentTime().seconds + Double(secondsToAdBreakStart), preferredTimescale: 1)

// Create an identifier to construct the ad pod request for the next ad break.
let adPodIdentifier = generatePodIdentifier(from: currentSeconds)
let adPodIdentifier = generateAdBreakID()

// Generate HMAC token for the manifest URL using the secret from Secrets.swift.
let manifestSecret = Secrets.manifestSecret
guard !manifestSecret.isEmpty else {
print("Manifest secret is empty. Cannot generate auth token for ad pod manifest.")
return
}

let expirationTime = Int(
Date.now.addingTimeInterval(TimeInterval(expirationDuration)).timeIntervalSince1970)
let params: [String: Any] = [
"ad_break_id": adPodIdentifier.replacingOccurrences(of: "ad_break_id/", with: ""),
"custom_asset_key": StreamParameters.customAssetKey,
"exp": expirationTime,
"network_code": StreamParameters.networkCode,
"pd": StreamParameters.adBreakDurationMs,
]

guard let authToken = generateAuthToken(params: params, secret: manifestSecret) else {
print(
"Failed to generate auth token for ad pod manifest. Skipping insertion of \(adPodIdentifier)."
)
return
}

var components = CharacterSet.alphanumerics
components.insert(charactersIn: "-._~")
let encodedAuthToken = authToken.addingPercentEncoding(withAllowedCharacters: components) ?? ""

// Construct the ad pod request URL for the next ad break.
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)"
"https://dai.google.com/linear/pods/v1/hls/network/\(StreamParameters.networkCode)/custom_asset/\(StreamParameters.customAssetKey)/\(adPodIdentifier).m3u8?stream_id=\(streamID)&pd=\(StreamParameters.adBreakDurationMs)&auth-token=\(encodedAuthToken)"
)
else {
// If URL creation fails, verify that StreamParameters and streamID are not empty.
Expand All @@ -208,16 +262,43 @@ class ViewController:
}
// [END schedule_ad_insertion]

// Generates an HMAC-SHA256 token based on the provided parameters and secret.
// [START generate_auth_token]
private func generateAuthToken(params: [String: Any], secret: String) -> String? {
let sortedKeys = params.keys.sorted()
var tokenPairs: [String] = []

for key in sortedKeys {
if let value = params[key] {
tokenPairs.append("\(key)=\(value)")
}
}

let tokenString = tokenPairs.joined(separator: "~")

guard let secretData = secret.data(using: .utf8),
let tokenData = tokenString.data(using: .utf8)
else {
print("Error: Could not convert secret or tokenString to Data.")
return nil
}

let key = SymmetricKey(data: secretData)
let hmac = HMAC<SHA256>.authenticationCode(for: tokenData, using: key)
let hexHash = hmac.map { String(format: "%02x", $0) }.joined()

return "\(tokenString)~hmac=\(hexHash)"
}
// [END generate_auth_token]

// Helper Function to get an unused ad break ID.
// Ad Break ID is an alpha numeric string.
// In production the Ad Break ID is determined by your ad signalling stack,
// propagated through a SCTE35 or other cue markers.
// [START generate_pod_id]
/// 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)"
private func generateAdBreakID() -> String {
let minutesSinceEpoch = Int(Date().timeIntervalSince1970 / expirationDuration)
return "ab\(minutesSinceEpoch)"
}
// [END generate_pod_id]

Expand Down
Loading