-
Notifications
You must be signed in to change notification settings - Fork 1
Ios ad + remove pip #168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Ios ad + remove pip #168
Changes from all commits
a92384d
6511fac
4422e15
efafe7d
27f148f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ import GoogleInteractiveMediaAds | |
| import MediaPlayer | ||
| import UIKit | ||
|
|
||
| class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, AVPictureInPictureControllerDelegate { | ||
| class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { | ||
| var channel: FlutterMethodChannel? | ||
|
|
||
| // Video objects | ||
|
|
@@ -16,10 +16,6 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| var adsManager: IMAAdsManager? | ||
| var companionSlot: IMACompanionAdSlot? | ||
|
|
||
| // PiP objects. | ||
| var pictureInPictureController: AVPictureInPictureController? | ||
| var pictureInPictureProxy: IMAPictureInPictureProxy? | ||
|
|
||
| var isVideo: Bool = true | ||
| var adUrl: String! | ||
| var contentUrl: String! | ||
|
|
@@ -31,10 +27,10 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| var sentOnComplete = false | ||
| var ppID: String? = nil | ||
| var isRegistered = false | ||
| private var offScreenHostWindow: UIWindow? | ||
|
|
||
| @IBOutlet weak var videoView: UIView! | ||
| @IBOutlet weak var companionView: UIView! | ||
| @IBOutlet weak var pictureInPictureButton: UIButton! | ||
|
|
||
| override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { | ||
| super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) | ||
|
|
@@ -55,43 +51,105 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| override func viewDidAppear(_ animated: Bool) { | ||
| super.viewDidAppear(animated) | ||
| print("AD: viewDidAppear at \(Date())") | ||
| if isRegistered { | ||
| beginAdSessionIfNeeded() | ||
| } | ||
|
|
||
| func setup(channel: FlutterMethodChannel?, adUrl: String?, contentUrl: String?, screen: Screen, args: [String: Any]?) { | ||
| print("[PREROLL] SETUP IS CALLED") | ||
|
|
||
| self.channel = channel | ||
| self.adUrl = adUrl | ||
| self.contentUrl = contentUrl | ||
| self.screen = screen | ||
| self.args = args | ||
|
|
||
| resetSessionState() | ||
| scheduleBeginAdSessionIfNeeded() | ||
| } | ||
|
|
||
| private func scheduleBeginAdSessionIfNeeded() { | ||
| DispatchQueue.main.async { [weak self] in | ||
| self?.beginAdSessionIfNeeded() | ||
| } | ||
| } | ||
|
|
||
| private func beginAdSessionIfNeeded() { | ||
| guard !isRegistered else { return } | ||
|
|
||
| loadViewIfNeeded() | ||
|
|
||
| let isOffScreenAd = isOffScreenAdRequest() | ||
| let viewInWindow = viewIfLoaded?.window != nil | ||
| guard viewInWindow || isOffScreenAd else { | ||
| return | ||
| } | ||
|
|
||
| if isOffScreenAd && !viewInWindow { | ||
| attachToOffScreenWindowIfNeeded() | ||
| } | ||
|
|
||
| setUpContentPlayer() | ||
| setUpAdsLoader() | ||
| setupAudioSession() | ||
|
|
||
| requestAds() | ||
|
|
||
| if self.videoView.frame.width > self.view.frame.width { | ||
| self.videoView.frame = CGRect( | ||
| if let videoView = videoView, videoView.frame.width > view.frame.width { | ||
| videoView.frame = CGRect( | ||
| x: 0, | ||
| y: 0, | ||
| width: self.view.frame.width, | ||
| height: self.view.frame.width / 1.333333333 | ||
| width: view.frame.width, | ||
| height: view.frame.width / 1.333333333 | ||
| ) | ||
| } | ||
|
|
||
| isRegistered = true | ||
| } | ||
|
|
||
| func setup(channel: FlutterMethodChannel?, adUrl: String?, contentUrl: String?, screen: Screen, args: [String: Any]?) { | ||
| print("[PREROLL] SETUP IS CALLED") | ||
| private func isOffScreenAdRequest() -> Bool { | ||
| args?["__OFF_SCREEN__"] as? String == "1" | ||
| } | ||
|
|
||
| self.channel = channel | ||
| self.adUrl = adUrl | ||
| self.contentUrl = contentUrl | ||
| self.screen = screen | ||
| self.args = args | ||
| private func attachToOffScreenWindowIfNeeded() { | ||
| loadViewIfNeeded() | ||
| guard isOffScreenAdRequest(), view.window == nil else { return } | ||
|
|
||
| detachFromOffScreenHost() | ||
|
|
||
| let adFrame = CGRect(x: 0, y: 0, width: 640, height: 480) | ||
| let window: UIWindow | ||
|
|
||
| let scene = UIApplication.shared.connectedScenes | ||
| .compactMap { $0 as? UIWindowScene } | ||
| .first { $0.screen == UIScreen.main } | ||
| ?? UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first | ||
| if let scene = scene { | ||
| window = UIWindow(windowScene: scene) | ||
| } else { | ||
| window = UIWindow(frame: adFrame) | ||
| } | ||
|
|
||
| window.frame = adFrame | ||
| window.windowLevel = UIWindow.Level(rawValue: UIWindow.Level.normal.rawValue - 1) | ||
| window.rootViewController = self | ||
| view.frame = adFrame | ||
| window.isHidden = false | ||
| offScreenHostWindow = window | ||
|
|
||
| resetSessionState() | ||
| } | ||
|
|
||
| private func detachFromOffScreenHost() { | ||
| guard offScreenHostWindow != nil else { return } | ||
|
|
||
| offScreenHostWindow?.isHidden = true | ||
| offScreenHostWindow?.rootViewController = nil | ||
| offScreenHostWindow = nil | ||
| } | ||
|
|
||
| private func resetSessionState() { | ||
| loadViewIfNeeded() | ||
|
|
||
| detachFromOffScreenHost() | ||
|
|
||
| sentOnComplete = false | ||
| playing = false | ||
| active = true | ||
|
|
@@ -106,6 +164,7 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
|
|
||
| private func cleanupPlaybackResources() { | ||
| removePlaybackObservers() | ||
| detachFromOffScreenHost() | ||
|
|
||
| adsManager?.delegate = nil | ||
| adsManager?.destroy() | ||
|
|
@@ -120,10 +179,6 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| contentPlayer = nil | ||
| contentPlayhead = nil | ||
|
|
||
| pictureInPictureController?.delegate = nil | ||
| pictureInPictureController = nil | ||
| pictureInPictureProxy = nil | ||
|
|
||
| contentPlayerLayer?.removeFromSuperlayer() | ||
| contentPlayerLayer = nil | ||
|
|
||
|
|
@@ -155,12 +210,10 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
|
|
||
| func setupAudioSession() { | ||
| do { | ||
| let options: AVAudioSession.CategoryOptions = [.allowAirPlay, .allowBluetoothA2DP] | ||
| let mode: AVAudioSession.Mode = isOffScreenAdRequest() ? .default : .moviePlayback | ||
| try AVAudioSession.sharedInstance().setCategory(.playback, mode: mode, options: options) | ||
| try AVAudioSession.sharedInstance().setActive(true) | ||
| if #available(iOS 10.0, *) { | ||
| try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback, options: []) | ||
| } else { | ||
| try AVAudioSession.sharedInstance().setCategory(.playback, options: []) | ||
| } | ||
| } catch let error { | ||
| print("AD: \(error.localizedDescription)") | ||
| } | ||
|
|
@@ -218,23 +271,6 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| // Set up our content playhead and contentComplete callback. | ||
| contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: contentPlayer) | ||
|
|
||
| // Set ourselves up for PiP. | ||
| pictureInPictureProxy = IMAPictureInPictureProxy(avPictureInPictureControllerDelegate: self) | ||
|
|
||
| if let contentPlayerLayer { | ||
| pictureInPictureController = AVPictureInPictureController(playerLayer: contentPlayerLayer) | ||
| } | ||
|
|
||
| if pictureInPictureController != nil { | ||
| if #available(iOS 14.0, *) { | ||
| pictureInPictureController!.requiresLinearPlayback = true | ||
| } | ||
| pictureInPictureController!.delegate = pictureInPictureProxy | ||
| } | ||
| if !AVPictureInPictureController.isPictureInPictureSupported() && pictureInPictureButton != nil { | ||
| pictureInPictureButton.isHidden = true | ||
| } | ||
|
|
||
| if let currentItem = contentPlayhead?.player.currentItem { | ||
| NotificationCenter.default.addObserver( | ||
| self, | ||
|
|
@@ -257,6 +293,36 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| name: UIApplication.didEnterBackgroundNotification, | ||
| object: nil | ||
| ) | ||
|
|
||
| NotificationCenter.default.addObserver( | ||
| self, | ||
| selector: #selector(AdsViewController.handleAudioSessionInterruption(notification:)), | ||
| name: AVAudioSession.interruptionNotification, | ||
| object: AVAudioSession.sharedInstance() | ||
| ) | ||
| } | ||
|
|
||
| @objc func handleAudioSessionInterruption(notification: Notification) { | ||
| guard let userInfo = notification.userInfo, | ||
| let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, | ||
| let type = AVAudioSession.InterruptionType(rawValue: typeValue) | ||
| else { return } | ||
|
|
||
| switch type { | ||
| case .began: | ||
| print("AD: audio session interruption began") | ||
| case .ended: | ||
| print("AD: audio session interruption ended") | ||
| if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { | ||
| let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) | ||
| if options.contains(.shouldResume) { | ||
| setupAudioSession() | ||
| adsManager?.resume() | ||
| } | ||
| } | ||
| @unknown default: | ||
| break | ||
| } | ||
| } | ||
|
|
||
| // Initialize ad display container. | ||
|
|
@@ -300,7 +366,10 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
|
|
||
| @objc func applicationDidEnterBackground(notification: NSNotification) { | ||
| print("AD: applicationDidEnterBackground") | ||
| self.active = false | ||
| // Off-screen audio ads must keep playing in background. | ||
| if !isOffScreenAdRequest() { | ||
| self.active = false | ||
| } | ||
|
Comment on lines
+395
to
+398
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
echo '--- line count ---'
wc -l "$FILE"
echo '--- around 360-470 ---'
sed -n '360,470p' "$FILE"
echo '--- search background-playback references ---'
rg -n "adsManager\.resume|resume\(\)|isOffScreenAdRequest|applicationState|background|Audio|AirPlay|requestAds\(with:" packages/ads/ios/smads/Sources/smads/AdsViewController.swift packages/ads/ios/smads/Sources -g '*.swift' -g '*.md' -g '*.txt'Repository: SuaMusica/flutter_plugins Length of output: 12589 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
nl -ba "$FILE" | sed -n '380,455p'Repository: SuaMusica/flutter_plugins Length of output: 203 🌐 Web query:
💡 Result: To enable background ad playback in the Google IMA SDK for iOS, you must strictly follow specific configuration and implementation requirements [1]. Google does not recommend playing video ads in the background; this functionality is intended for audio ads [1]. Key Requirements: 1. Enable Background Modes: In your Xcode project target, navigate to Capabilities > Background Modes and enable Audio and AirPlay [1]. Without this, audio playback stops when the app moves to the background, and the app cannot launch audio while backgrounded [1]. 2. Configure AVAudioSession: You must activate the AVAudioSession and set it to a category that supports background audio, such as AVAudioSessionCategoryPlayback [1]. 3. Enable Background Playback in IMA SDK: When creating your IMAAdsLoader, you must pass an IMASettings object with enableBackgroundPlayback set to YES [1][2]. 4. Content Playback Requirement: To make ad requests while your app is in the background, your content must already be playing [1]. If the app enters the background, you may need to manually ensure content playback is active before calling [IMAAdsLoader requestAds:] [1]. 5. Resume Ad Playback: Backgrounding an iOS app automatically pauses ad playback [1]. If an ad is playing when the app moves to the background, you must call [IMAAdsManager resume] to continue ad playback [1]. These steps allow the app to both request ads in the background and continue playing an ad that was already in progress when the app was backgrounded [1]. Failure to follow these steps, particularly the Audio and AirPlay capability and the IMASettings configuration, will result in ad playback halting upon backgrounding [1][3]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
echo '--- 340-375 ---'
sed -n '340,375p' "$FILE"
echo '--- 100-125 ---'
sed -n '100,125p' "$FILE"
echo '--- 755-780 ---'
sed -n '755,780p' "$FILE"Repository: SuaMusica/flutter_plugins Length of output: 2671 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
echo '--- lifecycle handlers ---'
rg -n "applicationDidBecomeActive|applicationWillEnterForeground|applicationDidEnterBackground|resume\(\)|pause\(\)|active =" "$FILE"
echo '--- around 300-370 ---'
sed -n '300,370p' "$FILE"Repository: SuaMusica/flutter_plugins Length of output: 3771 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
echo '--- requestAds call sites ---'
rg -n "\brequestAds\(\)" "$FILE"
echo '--- contentPlayer play references ---'
rg -n "contentPlayer.*play|\.play\(\)" "$FILE"Repository: SuaMusica/flutter_plugins Length of output: 455 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
echo '--- 70-100 ---'
sed -n '70,100p' "$FILE"
echo '--- 460-490 ---'
sed -n '460,490p' "$FILE"
echo '--- 585-610 ---'
sed -n '585,610p' "$FILE"
echo '--- 635-655 ---'
sed -n '635,655p' "$FILE"Repository: SuaMusica/flutter_plugins Length of output: 3563 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
echo '--- 140-185 ---'
sed -n '140,185p' "$FILE"Repository: SuaMusica/flutter_plugins Length of output: 1326 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE='packages/ads/ios/smads/Sources/smads/AdsViewController.swift'
echo '--- 185-260 ---'
sed -n '185,260p' "$FILE"
echo '--- 260-340 ---'
sed -n '260,340p' "$FILE"Repository: SuaMusica/flutter_plugins Length of output: 6193 Complete the off-screen resume flow. 📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @objc func contentDidFinishPlaying(_ notification: Notification) { | ||
|
|
@@ -342,14 +411,12 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| } | ||
|
|
||
| func requestAds() { | ||
| guard self.active else { | ||
| guard self.active || isOffScreenAdRequest() else { | ||
| onComplete() | ||
| return | ||
| } | ||
|
|
||
| guard let contentPlayer, | ||
| let pictureInPictureProxy | ||
| else { | ||
| guard let contentPlayer else { | ||
| print("AD: ERROR: player is not ready to request ads") | ||
| onComplete() | ||
| return | ||
|
|
@@ -359,8 +426,7 @@ class AdsViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDe | |
| let request = IMAAdsRequest( | ||
| adTagUrl: getAdTagUrl(), | ||
| adDisplayContainer: createAdDisplayContainer(), | ||
| avPlayerVideoDisplay: IMAAVPlayerVideoDisplay(avPlayer: contentPlayer), | ||
| pictureInPictureProxy: pictureInPictureProxy, | ||
| contentPlayhead: contentPlayhead, // Passamos o playhead em vez do avPlayerVideoDisplay | ||
| userContext: nil | ||
| ) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: SuaMusica/flutter_plugins
Length of output: 1956
🌐 Web query:
AVAudioSession.CategoryOptions.allowAirPlay playback category allowed documentation💡 Result:
The AVAudioSession.CategoryOptions.allowAirPlay option determines whether an audio session is permitted to stream audio to AirPlay devices [1][2]. Key documentation points regarding its usage: 1. Explicit vs. Implicit Setting: You can only explicitly set the allowAirPlay option when your audio session category is set to.playAndRecord [1][2]. For most other categories that support AirPlay, such as.playback,.ambient, and.soloAmbient, the system handles this option implicitly [1][2][3]. 2. Limitations: Audio sessions using the.multiRoute or.record categories implicitly clear (disable) this option [1][2]. 3. Interaction: Attempting to explicitly set allowAirPlay with categories that do not support it—or when it is otherwise restricted—may result in an error when configuring the audio session (e.g., OSStatus error -50) [4]. For most standard playback applications, you do not need to set this option manually, as the system enables it automatically for appropriate categories [1][2].
Citations:
🌐 Web query:
AVAudioSession.CategoryOptions allowBluetoothA2DP playback category valid documentation💡 Result:
The AVAudioSession.CategoryOptions.allowBluetoothA2DP option is a valid and supported option for the AVAudioSession.Category.playback category [1][2]. Official Apple documentation specifies that the allowBluetoothA2DP option determines whether you can stream audio from a session to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP) [1][2]. Key details regarding its usage include: 1. Automatic Routing: The system automatically routes audio to A2DP ports when you configure an audio session to use the playback category [2]. 2. Compatibility: While enabled by default for playback, specifying this option explicitly ensures the availability of A2DP devices for high-quality, stereo output [2]. 3. Other Categories: Starting with iOS 10.0, this option is also valid when using the playAndRecord category [2]. 4. Conflicts: If both allowBluetoothA2DP and allowBluetooth (which enables the Hands-Free Profile/HFP) are set, the system prioritizes HFP ports for routing when a device supports both profiles [2]. You can apply this option by passing it to the options parameter when calling the setCategory(_:mode:options:) method on your AVAudioSession instance [3][4].
Citations:
Drop
.allowAirPlayfrom this playback session..playbackalready supports AirPlay routing implicitly; keep.allowBluetoothA2DP, but remove the AirPlay option to avoidsetCategoryerrors.🤖 Prompt for AI Agents