diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/AdvancedExample.xcodeproj/project.pbxproj b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/AdvancedExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5bfac8d --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/AdvancedExample.xcodeproj/project.pbxproj @@ -0,0 +1,372 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + D20D1DC82C1652260021D157 /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D20D1DC62C1652260021D157 /* iPhone.storyboard */; }; + D20D1DC92C1652260021D157 /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D20D1DC72C1652260021D157 /* iPad.storyboard */; }; + D21627942C165053004B08EF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21627932C165053004B08EF /* AppDelegate.swift */; }; + D21627982C165053004B08EF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21627972C165053004B08EF /* ViewController.swift */; }; + D24D1B4B2D06224E00CE5E87 /* MockAdSchedulingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24D1B4A2D06224E00CE5E87 /* MockAdSchedulingService.swift */; }; + D24D1B4D2D06CD4800CE5E87 /* HLSInterstitialVideoDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24D1B4C2D06CD3900CE5E87 /* HLSInterstitialVideoDisplay.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D20D1DC62C1652260021D157 /* iPhone.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = iPhone.storyboard; sourceTree = ""; }; + D20D1DC72C1652260021D157 /* iPad.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = iPad.storyboard; sourceTree = ""; }; + D21627902C165053004B08EF /* AdvancedExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdvancedExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D21627932C165053004B08EF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D21627972C165053004B08EF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + D21627A42C165055004B08EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D24D1B4A2D06224E00CE5E87 /* MockAdSchedulingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAdSchedulingService.swift; sourceTree = ""; }; + D24D1B4C2D06CD3900CE5E87 /* HLSInterstitialVideoDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSInterstitialVideoDisplay.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D216278D2C165053004B08EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C3F09BE363D05255D29BD1ED /* Pods */ = { + isa = PBXGroup; + children = ( + ); + path = Pods; + sourceTree = ""; + }; + D21627872C165053004B08EF = { + isa = PBXGroup; + children = ( + D21627922C165053004B08EF /* AdvancedExample */, + D21627912C165053004B08EF /* Products */, + C3F09BE363D05255D29BD1ED /* Pods */, + ); + sourceTree = ""; + }; + D21627912C165053004B08EF /* Products */ = { + isa = PBXGroup; + children = ( + D21627902C165053004B08EF /* AdvancedExample.app */, + ); + name = Products; + sourceTree = ""; + }; + D21627922C165053004B08EF /* AdvancedExample */ = { + isa = PBXGroup; + children = ( + D24D1B4C2D06CD3900CE5E87 /* HLSInterstitialVideoDisplay.swift */, + D21627932C165053004B08EF /* AppDelegate.swift */, + D24D1B4A2D06224E00CE5E87 /* MockAdSchedulingService.swift */, + D21627972C165053004B08EF /* ViewController.swift */, + D20D1DC72C1652260021D157 /* iPad.storyboard */, + D20D1DC62C1652260021D157 /* iPhone.storyboard */, + D21627A42C165055004B08EF /* Info.plist */, + ); + name = AdvancedExample; + path = app; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D216278F2C165053004B08EF /* AdvancedExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = D21627A72C165055004B08EF /* Build configuration list for PBXNativeTarget "AdvancedExample" */; + buildPhases = ( + D216278C2C165053004B08EF /* Sources */, + D216278D2C165053004B08EF /* Frameworks */, + D216278E2C165053004B08EF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AdvancedExample; + productName = AdvancedExample; + productReference = D21627902C165053004B08EF /* AdvancedExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D21627882C165053004B08EF /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + D216278F2C165053004B08EF = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = D216278B2C165053004B08EF /* Build configuration list for PBXProject "AdvancedExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D21627872C165053004B08EF; + productRefGroup = D21627912C165053004B08EF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D216278F2C165053004B08EF /* AdvancedExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D216278E2C165053004B08EF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D20D1DC92C1652260021D157 /* iPad.storyboard in Resources */, + D20D1DC82C1652260021D157 /* iPhone.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D216278C2C165053004B08EF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D24D1B4D2D06CD4800CE5E87 /* HLSInterstitialVideoDisplay.swift in Sources */, + D21627982C165053004B08EF /* ViewController.swift in Sources */, + D24D1B4B2D06224E00CE5E87 /* MockAdSchedulingService.swift in Sources */, + D21627942C165053004B08EF /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D21627A52C165055004B08EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + D21627A62C165055004B08EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + D21627A82C165055004B08EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z95L3YYF93; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = app/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = iPhone.storyboard; + INFOPLIST_KEY_UIMainStoryboardFile = iPhone; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = google.AdvancedExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D21627A92C165055004B08EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z95L3YYF93; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = app/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = iPhone.storyboard; + INFOPLIST_KEY_UIMainStoryboardFile = iPhone; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = google.AdvancedExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D216278B2C165053004B08EF /* Build configuration list for PBXProject "AdvancedExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D21627A52C165055004B08EF /* Debug */, + D21627A62C165055004B08EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D21627A72C165055004B08EF /* Build configuration list for PBXNativeTarget "AdvancedExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D21627A82C165055004B08EF /* Debug */, + D21627A92C165055004B08EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D21627882C165053004B08EF /* Project object */; +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/AdvancedExample.xcodeproj/xcshareddata/xcschemes/AdvancedExample.xcscheme b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/AdvancedExample.xcodeproj/xcshareddata/xcschemes/AdvancedExample.xcscheme new file mode 100644 index 0000000..687bb1d --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/AdvancedExample.xcodeproj/xcshareddata/xcschemes/AdvancedExample.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/Podfile b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/Podfile new file mode 100644 index 0000000..8ba9abf --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' + +platform :ios, '17' +project 'AdvancedExample.xcodeproj' + +target 'AdvancedExample' do + pod 'GoogleAds-IMA-iOS-SDK' +end \ No newline at end of file diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/AppDelegate.swift b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/AppDelegate.swift new file mode 100644 index 0000000..126585f --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/AppDelegate.swift @@ -0,0 +1,29 @@ +// Copyright 2024 Google LLC. All rights reserved. +// +// +// 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 +// +// http://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 CoreData +import UIKit + +@main class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Override point for customization after application launch. + return true + } + +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/HLSInterstitialVideoDisplay.swift b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/HLSInterstitialVideoDisplay.swift new file mode 100644 index 0000000..d6de99b --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/HLSInterstitialVideoDisplay.swift @@ -0,0 +1,296 @@ +// Copyright 2024 Google LLC. All rights reserved. +// +// +// 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 +// +// http://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 AVFoundation +import GoogleInteractiveMediaAds +import UIKit + +class HLSInterstitialVideoDisplay: NSObject, + IMAVideoDisplay, + AVPlayerItemMetadataOutputPushDelegate +{ + + var currentMediaTime: TimeInterval + var totalMediaTime: TimeInterval + var bufferedMediaTime: TimeInterval + + var isPlaying: Bool + + public var delegate: (any IMAVideoDisplayDelegate)? + public var volume: Float + + private var isMainPlayerItem: Bool + private var started: Bool = false + private var player: AVPlayer? + private var controller: AVPlayerViewController? + private var mainPlayerItem: AVPlayerItem? + private var currentPlayerItem: AVPlayerItem? + private var metadataOutputManager: AVPlayerItemMetadataOutput? + private var interstitialEventMonitor: AVPlayerInterstitialEventMonitor? + + init(avPlayer player: AVPlayer) { + self.currentMediaTime = 0 + self.totalMediaTime = 0 + self.bufferedMediaTime = 0 + self.isPlaying = false + self.isMainPlayerItem = true + self.volume = player.volume + super.init() + self.player = player + // add metadata handler + interstitialEventMonitor = AVPlayerInterstitialEventMonitor(primaryPlayer: player) + + // add notification center observers + NotificationCenter.default.addObserver( + self, + selector: #selector(handleInterstitialEvent(_:)), + name: AVPlayerInterstitialEventMonitor.currentEventDidChangeNotification, + object: interstitialEventMonitor + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(playerItemDidPlayToEndTime(_:)), + name: AVPlayerItem.didPlayToEndTimeNotification, + object: nil + ) + } + + func loadStream(_ streamURL: URL, withSubtitles subtitles: [[String: String]]) { + let playerItem = AVPlayerItem(url: streamURL) + self.player!.replaceCurrentItem(with: playerItem) + self.mainPlayerItem = playerItem + self.volume = self.player!.volume + self.started = false + self.currentMediaTime = 0 + self.totalMediaTime = 0 + self.bufferedMediaTime = 0 + self.isPlaying = false + self.isMainPlayerItem = true + self.addPlayerObservers(player: self.player!) + self.addItemObservers(playerItem: playerItem) + } + + func addPlayerObservers(player: AVPlayer) { + // player observers + player.addObserver( + self, + forKeyPath: "status", + options: [.old, .new], + context: nil + ) + player.addObserver( + self, + forKeyPath: "currentItem", + options: [.old, .new], + context: nil + ) + player.addObserver( + self, + forKeyPath: "volume", + options: [.old, .new], + context: nil + ) + // player time observer + let interval = CMTime(seconds: 0.2, preferredTimescale: 600) + player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in + guard let self = self else { return } + self.currentMediaTime = time.seconds + playerTickOccurred() + } + } + + func addItemObservers(playerItem: AVPlayerItem) { + // player item observers + playerItem.addObserver( + self, + forKeyPath: "status", + options: [.old, .new], + context: nil + ) + playerItem.addObserver( + self, + forKeyPath: "loadedTimeRanges", + options: [.old, .new], + context: nil + ) + playerItem.addObserver( + self, + forKeyPath: "playbackBufferEmpty", + options: [.old, .new], + context: nil + ) + playerItem.addObserver( + self, + forKeyPath: "playbackBufferFull", + options: [.old, .new], + context: nil + ) + + self.metadataOutputManager = AVPlayerItemMetadataOutput(identifiers: nil) + self.metadataOutputManager!.setDelegate(self, queue: DispatchQueue.main) + playerItem.add(metadataOutputManager!) + } + + func playerTickOccurred() { + if self.mainPlayerItem!.status != .readyToPlay { + return + } + if !self.started { + self.started = true + self.delegate?.videoDisplayDidStart(_: self) + } + self.currentMediaTime = self.currentPlayerItem!.currentTime().seconds + self.delegate?.videoDisplay( + _: self, + didProgressWithMediaTime: self.currentMediaTime, + totalTime: self.totalMediaTime) + } + + func play() { + self.player!.play() + // todo: call delegate function + } + + func pause() { + self.player!.pause() + //todo: call delegate function + } + + func reset() { + + } + + func seekStream(toTime time: TimeInterval) { + let cmtime = CMTime(seconds: time, preferredTimescale: 1000) + self.player!.seek(to: cmtime) + } + + internal override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer? + ) { + if let player = object as? AVPlayer { + // player events + if keyPath == "status" { + if player.status == .readyToPlay { + // grab duration from current avplayer item + self.currentPlayerItem = player.currentItem + let asset = self.currentPlayerItem!.asset + Task { + let duration = try? await asset.load(.duration) + self.totalMediaTime = duration?.seconds ?? 0 + if self.totalMediaTime.isNaN { + self.totalMediaTime = 0 + } + player.play() + } + } + } else if keyPath == "timeControlStatus" { + if player.timeControlStatus == .playing { + if self.isPlaying == false { + self.isPlaying = true + self.delegate?.videoDisplayDidResume(_: self) + } + } else { + if self.isPlaying == true { + self.isPlaying = false + self.delegate?.videoDisplayDidPause(_: self) + } + } + } else if keyPath == "volume" { + self.volume = player.volume + self.delegate?.videoDisplay( + _: self, + volumeChangedTo: self.volume as NSNumber + ) + } + } else if let playerItem = object as? AVPlayerItem { + // player item events + if keyPath == "status" { + if playerItem.status == .readyToPlay { + self.delegate?.videoDisplayDidLoad(_: self) + } else if playerItem.status == .failed { + self.delegate?.videoDisplay( + _: self, + didReceiveError: playerItem.error!) + } + } else if keyPath == "loadedTimeRanges" { + let currentTime = playerItem.currentTime() + for entry in playerItem.loadedTimeRanges { + if let range = entry as? CMTimeRange { + if range.containsTime(currentTime) || CMTimeCompare(currentTime, range.end) <= 0 { + self.bufferedMediaTime = range.end.seconds + self.delegate?.videoDisplay!( + _: self, + didBufferToMediaTime: self.bufferedMediaTime) + } + } + } + } else if keyPath == "playbackBufferEmpty" { + if playerItem.isPlaybackBufferEmpty { + self.delegate?.videoDisplayDidStartBuffering!(_: self) + } + } else if keyPath == "playbackBufferFull" { + if playerItem.isPlaybackBufferFull { + self.delegate?.videoDisplayIsPlaybackReady!(_: self) + } + } + } + } + + @objc func playerItemDidPlayToEndTime(_ notification: Notification) { + if self.isMainPlayerItem { + self.delegate?.videoDisplayDidComplete(_: self) + } + } + + @objc private func handleInterstitialEvent(_ notification: Notification) { + guard let monitor = interstitialEventMonitor, let currentEvent = monitor.currentEvent else { + print("Video player returned to underlying content.") + // Interstitial has ended + self.currentPlayerItem = self.mainPlayerItem + self.isMainPlayerItem = true + return + } + print("Interstitial Event started.") + // Interstitial has started + self.currentPlayerItem = monitor.interstitialPlayer.currentItem + self.isMainPlayerItem = false + self.addPlayerObservers(player: monitor.interstitialPlayer) + self.addItemObservers(playerItem: self.currentPlayerItem!) + } + + // MARK: AVPlayerItemMetadataOutputPushDelegate + func metadataOutput( + _ output: AVPlayerItemMetadataOutput, + didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], + from track: AVPlayerItemTrack? + ) { + Task { + var metadata: [String: String] = [:] + for group in groups { + for item in group.items { + guard let metadataValue = try await item.load(.stringValue) else { return } + var id = item.identifier?.rawValue + if id!.hasPrefix("id3/") { + id = String(id!.dropFirst(4)) + } + metadata[id!] = metadataValue + } + } + self.delegate!.videoDisplay(_: self, didReceiveTimedMetadata: metadata) + } + } +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/Info.plist b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/Info.plist new file mode 100644 index 0000000..5ff527f --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/Info.plist @@ -0,0 +1,15 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIMainStoryboardFile~ipad + iPad + UIMainStoryboardFile~iphone + iPhone + + diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/MockAdSchedulingService.swift b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/MockAdSchedulingService.swift new file mode 100644 index 0000000..4c99962 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/MockAdSchedulingService.swift @@ -0,0 +1,78 @@ +// Copyright 2024 Google LLC. All rights reserved. +// +// +// 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 +// +// http://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 AVFoundation +import UIKit + +struct AdPodOptions { + var id: Int = 0 + var duration: CMTime = CMTime(seconds: 0, preferredTimescale: 1) + var scte35: String = "" + var params: String = "" +} + +protocol AdSchedulingServiceDelegate { + func insertAdPod(insertAt: Date, options: AdPodOptions) +} + +class MockAdSchedulingService { + private var delegate: AdSchedulingServiceDelegate + private var timer: Timer? + + init(delegate: AdSchedulingServiceDelegate) { + self.delegate = delegate + } + + // Begin polling the `fireTimer` method once per second. + func start() { + // In a real use case, this class would communicate + // with a publisher-owned server to receive information + // about upcoming ad pods. This mock uses a timer instead. + self.timer = Timer.scheduledTimer( + timeInterval: 1.0, + target: self, + selector: #selector(fireTimer), + userInfo: nil, + repeats: true) + } + + // This callback is fired once per second, and will request an ad pod once each minute. + @objc func fireTimer() { + let ts = Int(Date().timeIntervalSince1970) + let delay = 5 + let secondsTill = 60 - ts % 60 + // Simulate receiving a message from the publisher's + // server at the start of each minute + if secondsTill != 60 { + if secondsTill % 5 == 0 { + // courtesy debug message every five seconds + print("Making mock ad break insertion request in " + String(secondsTill) + " seconds.") + } + } else { + print("Requesting ad break to start in " + String(delay) + " seconds.") + // Setting the insertion time for 5 seconds in the future + // to allow for load times, buffer, etc + let insertTime = Date(timeIntervalSince1970: Double(ts + delay)) + let duration = CMTime(seconds: 20, preferredTimescale: 1) + + // in a real use case, the details of the ad break would + // be provided by the publisher-owned scheduling server. + // This mock uses the number of minutes since the unix + // epoch as the ad pod ID, and sets a fixed ad duration. + let options = AdPodOptions(id: Int(ts / 60), duration: duration) + + // initiate the ad pod insertion process + self.delegate.insertAdPod(insertAt: insertTime, options: options) + } + } +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/ViewController.swift b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/ViewController.swift new file mode 100644 index 0000000..8fe3386 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/ViewController.swift @@ -0,0 +1,256 @@ +// Copyright 2024 Google LLC. All rights reserved. +// +// +// 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 +// +// http://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 AVFoundation +import GoogleInteractiveMediaAds +import UIKit + +// The main view controller for the sample app. +class ViewController: + UIViewController, + IMAAdsLoaderDelegate, + IMAStreamManagerDelegate, + AdSchedulingServiceDelegate +{ + /// Google Ad Manager network code. + static let networkCode = "" + /// Livestream custom asset key. + static let customAssetKey = "" + /// URL of the content stream. + static let contentStreamURLString = "" + + private var adsLoader: IMAAdsLoader? + private var interstitialEventController: AVPlayerInterstitialEventController? + private var videoDisplay: IMAVideoDisplay! + private var adDisplayContainer: IMAAdDisplayContainer? + private var streamManager: IMAStreamManager? + private var contentPlayhead: IMAAVPlayerContentPlayhead? + private var playerViewController: AVPlayerViewController! + private var streamID = "" + private var userSeekTime = 0.0 + private var adBreakActive = false + + private var contentPlayer: AVPlayer? + @IBOutlet private weak var playButton: UIButton! + @IBOutlet private weak var videoView: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + + playButton.layer.zPosition = CGFloat(MAXFLOAT) + + setupAdsLoader() + setUpPlayer() + } + + @IBAction func onPlayButtonTouch(_ sender: Any) { + requestStream() + playButton.isHidden = true + } + + // MARK: Content Player Setup + + func setUpPlayer() { + // Load AVPlayer with path to our content. + contentPlayer = AVPlayer() + + // Create a player layer for the player. + let playerLayer = AVPlayerLayer(player: contentPlayer) + + // Size, position, and display the AVPlayer. + playerLayer.frame = videoView.layer.bounds + videoView.layer.addSublayer(playerLayer) + } + + // MARK: SDK Setup + + func setupAdsLoader() { + adsLoader = IMAAdsLoader(settings: nil) + adsLoader?.delegate = self + } + + func requestStream() { + // Create an InterstitialEventController to handle interstitial events, like inserting ad pods. + self.interstitialEventController = AVPlayerInterstitialEventController( + primaryPlayer: contentPlayer!) + // Create an ad display container for ad rendering. + adDisplayContainer = IMAAdDisplayContainer( + adContainer: videoView, + viewController: self, + companionSlots: nil) + // Create an IMAAVPlayerVideoDisplay to give the SDK access to your video player. + self.videoDisplay = HLSInterstitialVideoDisplay(avPlayer: contentPlayer!) + let streamRequest = IMAPodStreamRequest( + networkCode: ViewController.networkCode, + customAssetKey: ViewController.customAssetKey, + adDisplayContainer: adDisplayContainer!, + videoDisplay: self.videoDisplay, + pictureInPictureProxy: nil, + userContext: nil) + adsLoader?.requestStream(with: streamRequest) + } + + func startMediaSession() { + try? AVAudioSession.sharedInstance().setActive(true, options: []) + try? AVAudioSession.sharedInstance().setCategory(.playback) + } + + // MARK: - IMAAdsLoaderDelegate + + func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) { + self.streamManager = adsLoadedData.streamManager! + self.streamManager?.delegate = self + // The stream manager must be initialized before playback for adsRenderingSettings to be + // respected. + self.streamManager?.initialize(with: nil) + // Save the stream ID for later use. + self.streamID = (self.streamManager?.streamId)! + // Load the content stream and start playback. + let streamUrl = URL(string: ViewController.contentStreamURLString) + self.videoDisplay.loadStream(streamUrl!, withSubtitles: []) + self.videoDisplay.play() + } + + func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) { + print("Error loading ads: \(String(describing: adErrorData.adError.message))") + let streamURL = URL(string: ViewController.contentStreamURLString) + videoDisplay.loadStream(streamURL!, withSubtitles: []) + videoDisplay.play() + } + + // MARK: - IMAStreamManagerDelegate + func streamManager(_ streamManager: IMAStreamManager, didReceive event: IMAAdEvent) { + print("StreamManager event \(event.typeString).") + switch event.type { + case IMAAdEventType.STREAM_STARTED: + // Create a mock ad scheduler to simulate ad scheduling. + let adScheduler = MockAdSchedulingService(delegate: self) + // Start the ad scheduler to insert ads once each minute. + adScheduler.start() + + 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: + // Trigger an update to send focus to the ad display container. + adBreakActive = true + break + case IMAAdEventType.AD_BREAK_ENDED: + // Trigger an update to send focus to the content player. + adBreakActive = false + 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")") + } + + // MARK: - AVPlayerViewControllerDelegate + func playerViewController( + _ playerViewController: AVPlayerViewController, + timeToSeekAfterUserNavigatedFrom oldTime: CMTime, + to targetTime: CMTime + ) -> CMTime { + if adBreakActive { + return oldTime + } + return targetTime + } + + // MARK: - AdSchedulingServiceDelegate + func insertAdPod(insertAt: Date, options: AdPodOptions) { + // convert timestamp to livestream player position + let insertTime = getPlayerPosition(timestamp: insertAt, player: self.contentPlayer!) + + let adPodURL = buildAdPodRequest( + networkCode: ViewController.networkCode, + customAssetKey: ViewController.customAssetKey, + streamID: self.streamID, + options: options) + + // create ad pod player item + let interstitialPlayerItem = AVPlayerItem(url: adPodURL) + + // create interstitial event + let interstitialEvent = AVPlayerInterstitialEvent( + primaryItem: (self.contentPlayer?.currentItem!)!, + identifier: String(options.id), + time: insertTime, + templateItems: [interstitialPlayerItem], + restrictions: [], + resumptionOffset: options.duration) + // load event into player + interstitialEventController!.events = [interstitialEvent] + } + + // MARK: - Helper Functions + + // Convert a unix epoch time to a player position, relative to the live playhead. + func getPlayerPosition(timestamp: Date, player: AVPlayer) -> CMTime { + let timestampSecs: Double = timestamp.timeIntervalSince1970 + let now: Double = Double(Date().timeIntervalSince1970) + let secondsUntil = CMTimeMakeWithSeconds(Double(timestampSecs - now), preferredTimescale: 1) + guard let livePosition = player.currentItem?.seekableTimeRanges.last as? CMTimeRange else { + return secondsUntil + } + return CMTimeAdd(CMTimeRangeGetEnd(livePosition), secondsUntil) + } + + // Build the ad pod request URL from the component parameters + func buildAdPodRequest( + networkCode: String, customAssetKey: String, streamID: String, options: AdPodOptions + ) -> URL { + let durationMS = String(Int(CMTimeGetSeconds(options.duration) * 1000)) + var path = "/linear/pods/v1/hls" + path += "/network/" + networkCode + path += "/custom_asset/" + customAssetKey + path += "/pod/" + String(options.id) + ".m3u8" + + var components = URLComponents() + components.scheme = "https" + components.host = "dai.google.com" + components.path = path + components.queryItems = [ + URLQueryItem(name: "stream_id", value: streamID), + URLQueryItem(name: "pd", value: durationMS), + URLQueryItem(name: "scte35", value: options.scte35), + URLQueryItem(name: "cust_params", value: options.params), + ] + return components.url! + } +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/iPad.storyboard b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/iPad.storyboard new file mode 100644 index 0000000..464cfb9 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/iPad.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/iPhone.storyboard b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/iPhone.storyboard new file mode 100644 index 0000000..be46501 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/AdvancedExample/app/iPhone.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/BasicExample.xcodeproj/project.pbxproj b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/BasicExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..66bdd77 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/BasicExample.xcodeproj/project.pbxproj @@ -0,0 +1,370 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + D20D1DC82C1652260021D157 /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D20D1DC62C1652260021D157 /* iPhone.storyboard */; }; + D20D1DC92C1652260021D157 /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D20D1DC72C1652260021D157 /* iPad.storyboard */; }; + D21627942C165053004B08EF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21627932C165053004B08EF /* AppDelegate.swift */; }; + D21627982C165053004B08EF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21627972C165053004B08EF /* ViewController.swift */; }; + D21627A02C165055004B08EF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D216279F2C165055004B08EF /* Assets.xcassets */; }; + D24D1B4B2D06224E00CE5E87 /* MockAdSchedulingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24D1B4A2D06224E00CE5E87 /* MockAdSchedulingService.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D20D1DC62C1652260021D157 /* iPhone.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = iPhone.storyboard; sourceTree = ""; }; + D20D1DC72C1652260021D157 /* iPad.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = iPad.storyboard; sourceTree = ""; }; + D21627902C165053004B08EF /* BasicExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BasicExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D21627932C165053004B08EF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D21627972C165053004B08EF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + D21627A42C165055004B08EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D24D1B4A2D06224E00CE5E87 /* MockAdSchedulingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAdSchedulingService.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D216278D2C165053004B08EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C3F09BE363D05255D29BD1ED /* Pods */ = { + isa = PBXGroup; + children = ( + ); + path = Pods; + sourceTree = ""; + }; + D21627872C165053004B08EF = { + isa = PBXGroup; + children = ( + D21627922C165053004B08EF /* BasicExample */, + D21627912C165053004B08EF /* Products */, + C3F09BE363D05255D29BD1ED /* Pods */, + ); + sourceTree = ""; + }; + D21627912C165053004B08EF /* Products */ = { + isa = PBXGroup; + children = ( + D21627902C165053004B08EF /* BasicExample.app */, + ); + name = Products; + sourceTree = ""; + }; + D21627922C165053004B08EF /* BasicExample */ = { + isa = PBXGroup; + children = ( + D21627932C165053004B08EF /* AppDelegate.swift */, + D24D1B4A2D06224E00CE5E87 /* MockAdSchedulingService.swift */, + D21627972C165053004B08EF /* ViewController.swift */, + D20D1DC72C1652260021D157 /* iPad.storyboard */, + D20D1DC62C1652260021D157 /* iPhone.storyboard */, + D21627A42C165055004B08EF /* Info.plist */, + ); + name = BasicExample; + path = app; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D216278F2C165053004B08EF /* BasicExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = D21627A72C165055004B08EF /* Build configuration list for PBXNativeTarget "BasicExample" */; + buildPhases = ( + D216278C2C165053004B08EF /* Sources */, + D216278D2C165053004B08EF /* Frameworks */, + D216278E2C165053004B08EF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BasicExample; + productName = BasicExample; + productReference = D21627902C165053004B08EF /* BasicExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D21627882C165053004B08EF /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + D216278F2C165053004B08EF = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = D216278B2C165053004B08EF /* Build configuration list for PBXProject "BasicExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D21627872C165053004B08EF; + productRefGroup = D21627912C165053004B08EF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D216278F2C165053004B08EF /* BasicExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D216278E2C165053004B08EF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D21627A02C165055004B08EF /* Assets.xcassets in Resources */, + D20D1DC92C1652260021D157 /* iPad.storyboard in Resources */, + D20D1DC82C1652260021D157 /* iPhone.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D216278C2C165053004B08EF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D21627982C165053004B08EF /* ViewController.swift in Sources */, + D24D1B4B2D06224E00CE5E87 /* MockAdSchedulingService.swift in Sources */, + D21627942C165053004B08EF /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D21627A52C165055004B08EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + D21627A62C165055004B08EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + D21627A82C165055004B08EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z95L3YYF93; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = app/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = iPhone.storyboard; + INFOPLIST_KEY_UIMainStoryboardFile = iPhone; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = google.BasicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D21627A92C165055004B08EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z95L3YYF93; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = app/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = iPhone.storyboard; + INFOPLIST_KEY_UIMainStoryboardFile = iPhone; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = google.BasicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D216278B2C165053004B08EF /* Build configuration list for PBXProject "BasicExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D21627A52C165055004B08EF /* Debug */, + D21627A62C165055004B08EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D21627A72C165055004B08EF /* Build configuration list for PBXNativeTarget "BasicExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D21627A82C165055004B08EF /* Debug */, + D21627A92C165055004B08EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D21627882C165053004B08EF /* Project object */; +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/BasicExample.xcodeproj/xcshareddata/xcschemes/BasicExample.xcscheme b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/BasicExample.xcodeproj/xcshareddata/xcschemes/BasicExample.xcscheme new file mode 100644 index 0000000..f07ee2b --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/BasicExample.xcodeproj/xcshareddata/xcschemes/BasicExample.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/Podfile b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/Podfile new file mode 100644 index 0000000..a77c118 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' + +platform :ios, '17' +project 'BasicExample.xcodeproj' + +target 'BasicExample' do + pod 'GoogleAds-IMA-iOS-SDK' +end \ No newline at end of file diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/AppDelegate.swift b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/AppDelegate.swift new file mode 100644 index 0000000..126585f --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/AppDelegate.swift @@ -0,0 +1,29 @@ +// Copyright 2024 Google LLC. All rights reserved. +// +// +// 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 +// +// http://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 CoreData +import UIKit + +@main class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Override point for customization after application launch. + return true + } + +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/Info.plist b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/Info.plist new file mode 100644 index 0000000..5ff527f --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/Info.plist @@ -0,0 +1,15 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIMainStoryboardFile~ipad + iPad + UIMainStoryboardFile~iphone + iPhone + + diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/MockAdSchedulingService.swift b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/MockAdSchedulingService.swift new file mode 100644 index 0000000..4c99962 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/MockAdSchedulingService.swift @@ -0,0 +1,78 @@ +// Copyright 2024 Google LLC. All rights reserved. +// +// +// 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 +// +// http://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 AVFoundation +import UIKit + +struct AdPodOptions { + var id: Int = 0 + var duration: CMTime = CMTime(seconds: 0, preferredTimescale: 1) + var scte35: String = "" + var params: String = "" +} + +protocol AdSchedulingServiceDelegate { + func insertAdPod(insertAt: Date, options: AdPodOptions) +} + +class MockAdSchedulingService { + private var delegate: AdSchedulingServiceDelegate + private var timer: Timer? + + init(delegate: AdSchedulingServiceDelegate) { + self.delegate = delegate + } + + // Begin polling the `fireTimer` method once per second. + func start() { + // In a real use case, this class would communicate + // with a publisher-owned server to receive information + // about upcoming ad pods. This mock uses a timer instead. + self.timer = Timer.scheduledTimer( + timeInterval: 1.0, + target: self, + selector: #selector(fireTimer), + userInfo: nil, + repeats: true) + } + + // This callback is fired once per second, and will request an ad pod once each minute. + @objc func fireTimer() { + let ts = Int(Date().timeIntervalSince1970) + let delay = 5 + let secondsTill = 60 - ts % 60 + // Simulate receiving a message from the publisher's + // server at the start of each minute + if secondsTill != 60 { + if secondsTill % 5 == 0 { + // courtesy debug message every five seconds + print("Making mock ad break insertion request in " + String(secondsTill) + " seconds.") + } + } else { + print("Requesting ad break to start in " + String(delay) + " seconds.") + // Setting the insertion time for 5 seconds in the future + // to allow for load times, buffer, etc + let insertTime = Date(timeIntervalSince1970: Double(ts + delay)) + let duration = CMTime(seconds: 20, preferredTimescale: 1) + + // in a real use case, the details of the ad break would + // be provided by the publisher-owned scheduling server. + // This mock uses the number of minutes since the unix + // epoch as the ad pod ID, and sets a fixed ad duration. + let options = AdPodOptions(id: Int(ts / 60), duration: duration) + + // initiate the ad pod insertion process + self.delegate.insertAdPod(insertAt: insertTime, options: options) + } + } +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/ViewController.swift b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/ViewController.swift new file mode 100644 index 0000000..6bc74ea --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/ViewController.swift @@ -0,0 +1,256 @@ +// Copyright 2024 Google LLC. All rights reserved. +// +// +// 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 +// +// http://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 AVFoundation +import GoogleInteractiveMediaAds +import UIKit + +// The main view controller for the sample app. +class ViewController: + UIViewController, + IMAAdsLoaderDelegate, + IMAStreamManagerDelegate, + AdSchedulingServiceDelegate +{ + /// Google Ad Manager network code. + static let networkCode = "" + /// Livestream custom asset key. + static let customAssetKey = "" + /// URL of the content stream. + static let contentStreamURLString = "" + + private var adsLoader: IMAAdsLoader? + private var interstitialEventController: AVPlayerInterstitialEventController? + private var videoDisplay: IMAAVPlayerVideoDisplay! + private var adDisplayContainer: IMAAdDisplayContainer? + private var streamManager: IMAStreamManager? + private var contentPlayhead: IMAAVPlayerContentPlayhead? + private var playerViewController: AVPlayerViewController! + private var streamID = "" + private var userSeekTime = 0.0 + private var adBreakActive = false + + private var contentPlayer: AVPlayer? + @IBOutlet private weak var playButton: UIButton! + @IBOutlet private weak var videoView: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + + playButton.layer.zPosition = CGFloat(MAXFLOAT) + + setupAdsLoader() + setUpPlayer() + } + + @IBAction func onPlayButtonTouch(_ sender: Any) { + requestStream() + playButton.isHidden = true + } + + // MARK: Content Player Setup + + func setUpPlayer() { + // Load AVPlayer with path to our content. + contentPlayer = AVPlayer() + + // Create a player layer for the player. + let playerLayer = AVPlayerLayer(player: contentPlayer) + + // Size, position, and display the AVPlayer. + playerLayer.frame = videoView.layer.bounds + videoView.layer.addSublayer(playerLayer) + } + + // MARK: SDK Setup + + func setupAdsLoader() { + adsLoader = IMAAdsLoader(settings: nil) + adsLoader?.delegate = self + } + + func requestStream() { + // Create an InterstitialEventController to handle interstitial events, like inserting ad pods. + self.interstitialEventController = AVPlayerInterstitialEventController( + primaryPlayer: contentPlayer!) + // Create an ad display container for ad rendering. + adDisplayContainer = IMAAdDisplayContainer( + adContainer: videoView, + viewController: self, + companionSlots: nil) + // Create an IMAAVPlayerVideoDisplay to give the SDK access to your video player. + self.videoDisplay = IMAAVPlayerVideoDisplay(avPlayer: contentPlayer!) + let streamRequest = IMAPodStreamRequest( + networkCode: ViewController.networkCode, + customAssetKey: ViewController.customAssetKey, + adDisplayContainer: adDisplayContainer!, + videoDisplay: self.videoDisplay, + pictureInPictureProxy: nil, + userContext: nil) + adsLoader?.requestStream(with: streamRequest) + } + + func startMediaSession() { + try? AVAudioSession.sharedInstance().setActive(true, options: []) + try? AVAudioSession.sharedInstance().setCategory(.playback) + } + + // MARK: - IMAAdsLoaderDelegate + + func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) { + self.streamManager = adsLoadedData.streamManager! + self.streamManager?.delegate = self + // The stream manager must be initialized before playback for adsRenderingSettings to be + // respected. + self.streamManager?.initialize(with: nil) + // Save the stream ID for later use. + self.streamID = (self.streamManager?.streamId)! + // Load the content stream and start playback. + let streamUrl = URL(string: ViewController.contentStreamURLString) + self.videoDisplay.loadStream(streamUrl!, withSubtitles: []) + self.videoDisplay.play() + } + + func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) { + print("Error loading ads: \(String(describing: adErrorData.adError.message))") + let streamURL = URL(string: ViewController.contentStreamURLString) + videoDisplay.loadStream(streamURL!, withSubtitles: []) + videoDisplay.play() + } + + // MARK: - IMAStreamManagerDelegate + func streamManager(_ streamManager: IMAStreamManager, didReceive event: IMAAdEvent) { + print("StreamManager event \(event.typeString).") + switch event.type { + case IMAAdEventType.STREAM_STARTED: + // Create a mock ad scheduler to simulate ad scheduling. + let adScheduler = MockAdSchedulingService(delegate: self) + // Start the ad scheduler to insert ads once each minute. + adScheduler.start() + + 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: + // Trigger an update to send focus to the ad display container. + adBreakActive = true + break + case IMAAdEventType.AD_BREAK_ENDED: + // Trigger an update to send focus to the content player. + adBreakActive = false + 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")") + } + + // MARK: - AVPlayerViewControllerDelegate + func playerViewController( + _ playerViewController: AVPlayerViewController, + timeToSeekAfterUserNavigatedFrom oldTime: CMTime, + to targetTime: CMTime + ) -> CMTime { + if adBreakActive { + return oldTime + } + return targetTime + } + + // MARK: - AdSchedulingServiceDelegate + func insertAdPod(insertAt: Date, options: AdPodOptions) { + // convert timestamp to livestream player position + let insertTime = getPlayerPosition(timestamp: insertAt, player: self.contentPlayer!) + + let adPodURL = buildAdPodRequest( + networkCode: ViewController.networkCode, + customAssetKey: ViewController.customAssetKey, + streamID: self.streamID, + options: options) + + // create ad pod player item + let interstitialPlayerItem = AVPlayerItem(url: adPodURL) + + // create interstitial event + let interstitialEvent = AVPlayerInterstitialEvent( + primaryItem: (self.contentPlayer?.currentItem!)!, + identifier: String(options.id), + time: insertTime, + templateItems: [interstitialPlayerItem], + restrictions: [], + resumptionOffset: options.duration) + // load event into player + interstitialEventController!.events = [interstitialEvent] + } + + // MARK: - Helper Functions + + // Convert a unix epoch time to a player position, relative to the live playhead. + func getPlayerPosition(timestamp: Date, player: AVPlayer) -> CMTime { + let timestampSecs: Double = timestamp.timeIntervalSince1970 + let now: Double = Double(Date().timeIntervalSince1970) + let secondsUntil = CMTimeMakeWithSeconds(Double(timestampSecs - now), preferredTimescale: 1) + guard let livePosition = player.currentItem?.seekableTimeRanges.last as? CMTimeRange else { + return secondsUntil + } + return CMTimeAdd(CMTimeRangeGetEnd(livePosition), secondsUntil) + } + + // Build the ad pod request URL from the component parameters + func buildAdPodRequest( + networkCode: String, customAssetKey: String, streamID: String, options: AdPodOptions + ) -> URL { + let durationMS = String(Int(CMTimeGetSeconds(options.duration) * 1000)) + var path = "/linear/pods/v1/hls" + path += "/network/" + networkCode + path += "/custom_asset/" + customAssetKey + path += "/pod/" + String(options.id) + ".m3u8" + + var components = URLComponents() + components.scheme = "https" + components.host = "dai.google.com" + components.path = path + components.queryItems = [ + URLQueryItem(name: "stream_id", value: streamID), + URLQueryItem(name: "pd", value: durationMS), + URLQueryItem(name: "scte35", value: options.scte35), + URLQueryItem(name: "cust_params", value: options.params), + ] + return components.url! + } +} diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/iPad.storyboard b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/iPad.storyboard new file mode 100644 index 0000000..54010e0 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/iPad.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/iPhone.storyboard b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/iPhone.storyboard new file mode 100644 index 0000000..b74d458 --- /dev/null +++ b/Swift/HLSInterstitialsClientSideInsertionExamples/BasicExample/app/iPhone.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +