I’m developing a Flutter application where I need to play a video with ads using GSPlayer and Google IMA SDK on iOS. The video should start playing automatically without requiring user interaction. However, I am facing issues with autoplay, especially integrating the ads to play seamlessly before the main content starts.
Here’s a summary of my setup:
- Dependencies:
- GSPlayer for video playback.
- GoogleInteractiveMediaAds for handling video ads.
- Objective:
- Load and play ads automatically before the main video content.
- Automatically start video playback after ads are done.
- Ensure smooth transition between ads and main video content.
- Problem:
- The video does not start playing automatically.
- Ads do not play as expected before the main content.
import Foundation
import GoogleInteractiveMediaAds
import GSPlayer
import Flutter
class NativeViewFactory : NSObject, FlutterPlatformViewFactory {
private var messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
return NativeView(
frame: frame,
viewIdentifier: viewId,
arguments: args,
binaryMessenger: messenger)
}
}
public class NativeView : NSObject, FlutterPlatformView,fullScreeenDelegate, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {
deinit{
stopTimer()
playerView.pause()
}
private var _view: UIView
var kTestAppContentUrl_MP4 = " "
var settings = UIButton()
var playerView = VideoPlayerView()
var controlView = GSPlayerControlUIView()
var paybackSlider = UISlider()
var contentPlayhead: IMAAVPlayerContentPlayhead?
var adsLoader: IMAAdsLoader!
var adsManager: IMAAdsManager!
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let controller = UIApplication.topViewController()
var item : AVPlayerItem!
var message : FlutterBinaryMessenger!
weak var timer: Timer?
static let kTestAppAdTagUrl =
"AD TAG URL HERE"
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
binaryMessenger messenger: FlutterBinaryMessenger?
) {
_view = UIView()
super.init()
if let argumentsDictionary = args as? Dictionary<String, Any> {
self.kTestAppContentUrl_MP4 = argumentsDictionary["videoURL"] as! String
print("test URL:", kTestAppContentUrl_MP4)
}
message = messenger
let flutterChannel = FlutterMethodChannel(name: "bms_video_player",
binaryMessenger: messenger!)
flutterChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: FlutterResult) -> Void in
switch call.method {
case "pauseVideo":
self.playerView.pause(reason: .userInteraction)
if(self.adsManager.adPlaybackInfo.isPlaying) {
self.adsManager.pause()
}
return
default:
result(FlutterMethodNotImplemented)
}
})
// iOS views can be created here
setUpContentPlayer(view: _view)
setUpAdsLoader()
createNativeView(view: _view)
startTimer()
}
func startTimer() {
timer?.invalidate() // just in case you had existing `Timer`, `invalidate` it before we lose our reference to it
timer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { [weak self] _ in
self?.controlView.isHidden = true
}
}
func stopTimer() {
timer?.invalidate()
self.playerView.pause()
}
// if appropriate, make sure to stop your timer in `deinit`
func fullScreenTap() {
print("fullScreen tapped!!")
let flutterChannel = FlutterMethodChannel(name: "bms_video_player",
binaryMessenger: message!)
flutterChannel.invokeMethod("fullScreen",arguments: 0)
playerView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.height, height:UIScreen.main.bounds.size.width )
controlView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.height, height: UIScreen.main.bounds.size.width)
}
func normalScreenTap() {
print("normalScreen tapped!!")
let flutterChannel = FlutterMethodChannel(name: "bms_video_player",
binaryMessenger: message!)
flutterChannel.invokeMethod("normalScreen",arguments: 0)
playerView.frame = CGRect(x: 0, y: 0, width:UIScreen.main.bounds.size.height, height: 400)
controlView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.height, height: 400)
}
func backButtonTap() {
print("backButton tapped!!")
// Handle back button action
controlView.onClicked_FullScreen(self)
let flutterChannel = FlutterMethodChannel(name: "bms_video_player", binaryMessenger: message!)
flutterChannel.invokeMethod("onBackButtonClicked", arguments: nil)
}
func setUpContentPlayer(view _view: UIView) {
// Load AVPlayer with path to our content.
print("test URL1:", kTestAppContentUrl_MP4)
guard let contentURL = URL(string: kTestAppContentUrl_MP4) else {
print("ERROR: please use a valid URL for the content URL")
return
}
let controller = AVPlayerViewController()
let player = AVPlayer(url: URL(string: kTestAppContentUrl_MP4)!)
controller.player = player
playerView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 400)
controlView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 400)
playerView.contentMode = .scaleAspectFill
playerView.play(for: contentURL)
controlView.delegate = self
controlView.populate(with: playerView)
// Size, position, and display the AVPlayer.
_view.addSubview(playerView)
_view.addSubview(controlView)
playerView.pause(reason: .userInteraction)
controlView.isHidden = true
controlView.bringSubviewToFront(_view)
let tap = UITapGestureRecognizer(target: self, action: #selector(self.touchHappen(_:)))
playerView.addGestureRecognizer(tap)
playerView.isUserInteractionEnabled = true
//_view.layer.addSublayer(playerLayer!)
// Set up our content playhead and contentComplete callback.
contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: playerView.player!)
}
@objc func touchHappen(_ sender: UITapGestureRecognizer) {
print("touchHappen")
self.controlView.isHidden = false
}
@objc func applicationDidEnterBackground(_ notification: NSNotification) {
playerView.pause(reason: .userInteraction)
}
@objc func contentDidFinishPlaying(_ notification: Notification) {
// Make sure we don't call contentComplete as a result of an ad completing.
// if (notification.object as! AVPlayerItem) == playerView.playerLayer.player?.currentItem {
// adsLoader.contentComplete()
// }
}
func setUpAdsLoader() {
adsLoader = IMAAdsLoader(settings: nil)
adsLoader.delegate = self
}
func requestAds(view _view: UIView) {
// Create ad display container for ad rendering.
let adDisplayContainer = IMAAdDisplayContainer(
adContainer: _view, viewController: controller, companionSlots: nil)
// Create an ad request with our ad tag, display container, and optional user context.
let request = IMAAdsRequest(
adTagUrl: NativeView.kTestAppAdTagUrl,
adDisplayContainer: adDisplayContainer,
contentPlayhead: contentPlayhead,
userContext: controlView)
adsLoader.requestAds(with: request)
}
public func view() -> UIView {
return _view
}
func createNativeView(view _view: UIView){
_view.backgroundColor = UIColor.black
settings.addTarget(self, action: #selector(touchedSet), for: .touchUpInside)
settings.setImage(UIImage(named: "play_48px"), for: .normal)
settings.frame = CGRect(x: 200, y:200 , width: 50, height: 50)
_view.addSubview(settings)
_view.bringSubviewToFront(controlView)
_view.bringSubviewToFront(settings)
}
@objc func touchedSet(sender: UIButton!) {
print("You tapped the button")
requestAds(view: _view)
settings.isHidden = true
}
// MARK: - IMAAdsLoaderDelegate
public func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
// Grab the instance of the IMAAdsManager and set ourselves as the delegate.
adsManager = adsLoadedData.adsManager
adsManager.delegate = self
// Create ads rendering settings and tell the SDK to use the in-app browser.
let adsRenderingSettings = IMAAdsRenderingSettings()
adsRenderingSettings.linkOpenerPresentingController = controller
// Initialize the ads manager.
adsManager.initialize(with: adsRenderingSettings)
//touchedSet(sender: UIButton())
//controlView.onClicked_FullScreen(self)
}
public func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
playerView.resume()
controlView.isHidden = false
}
// MARK: - IMAAdsManagerDelegate
public func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) {
if event.type == IMAAdEventType.LOADED {
// When the SDK notifies us that ads have been loaded, play them.
adsManager.start()
}
if event.type == IMAAdEventType.RESUME {
settings.addTarget(self, action: #selector(touchedSet), for: .touchUpInside)
settings.setImage(UIImage(named: "play_48px"), for: .normal)
settings.frame = CGRect(x: 200, y:200 , width: 50, height: 50)
_view.addSubview(settings)
}
if event.type == IMAAdEventType.PAUSE {
if(adsManager.adPlaybackInfo.isPlaying) {
adsManager.pause()
}
settings.addTarget(self, action: #selector(touchedSet), for: .touchUpInside)
settings.setImage(UIImage(named: "play_48px"), for: .normal)
settings.frame = CGRect(x:200, y:200 , width: 50, height: 50)
_view.addSubview(settings)
}
if event.type == IMAAdEventType.TAPPED {
// You can also add allow the user to tap anywhere on the Ad to resume play
if(!adsManager.adPlaybackInfo.isPlaying) {
adsManager.resume()
}
}
}
public func adsManager(_ adsManager: IMAAdsManager, didReceive error: IMAAdError) {
// Something went wrong with the ads manager after ads were loaded. Log the error and play the
// content.
print("AdsManager error: (error.message ?? "nil")")
playerView.resume()
controlView.isHidden = false
}
public func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) {
// The SDK is going to play ads, so pause the content.
playerView.pause(reason: .userInteraction)
controlView.isHidden = true
}
public func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) {
// The SDK is done playing ads (at least for now), so resume the content.
print("AdsManager resume: ("nil")")
playerView.resume()
controlView.isHidden = false
}
}
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
and
GSPlayerControlUIView.swift:
import UIKit
import CoreMedia
import GSPlayer
import Flutter
import MediaPlayer
protocol fullScreeenDelegate: class {
func fullScreenTap()
func normalScreenTap()
func backButtonTap()
}
@IBDesignable
class GSPlayerControlUIView: UIView {
// MARK: IBOutlet
@IBOutlet weak var play_Button: UIButton!
@IBOutlet weak var duration_Slider: UISlider!
@IBOutlet weak var currentDuration_Label: UILabel!
@IBOutlet weak var totalDuration_Label: UILabel!
@IBOutlet weak var fullscreen_Button : UIButton!
@IBOutlet weak var backButton: UIButton!
@IBOutlet weak var volume_Slider: UISlider!
@IBOutlet weak var brightness_Slider: UISlider!
weak var delegate: fullScreeenDelegate?
var isFullScreen = false
// MARK: Variables
private var videoPlayer: VideoPlayerView!
// MARK: Listeners
var onStateDidChanged: ((VideoPlayerView.State) -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.commonInit()
}
func commonInit() {
guard let view = Bundle(for: GSPlayerControlUIView.self).loadNibNamed("GSPlayerControlUIView", owner: self, options: nil)?.first as? UIView else { return }
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.isUserInteractionEnabled = false
self.addSubview(view)
// Set the image for the back button
backButton.setImage(UIImage(named: "backbutton"), for: .normal)
}
}
// MARK: Functions
extension GSPlayerControlUIView {
func populate(with videoPlayer: VideoPlayerView) {
self.videoPlayer = videoPlayer
self.isUserInteractionEnabled = true
setPeriodicTimer()
setOnClicked_VideoPlayer()
setStateDidChangedListener()
duration_Slider.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapSlider(_:))))
}
private func setStateDidChangedListener() {
videoPlayer.stateDidChanged = { [weak self] state in
guard let self = self else { return }
if case .playing = state {
self.setOnStartPlaying()
}
switch state {
case .playing, .paused: self.duration_Slider.isEnabled = true
default: self.duration_Slider.isEnabled = false
}
self.play_Button.setImage(state == .playing ? UIImage(named: "pause_48px") : UIImage(named: "play_48px"), for: .normal)
if let listener = self.onStateDidChanged { listener(state) }
}
videoPlayer.replay = { [weak self] in
if self != nil
{
self!.currentDuration_Label.text = "00:00"
self!.duration_Slider.setValue(0, animated: false)
}
}
}
private func setOnStartPlaying() {
let totalDuration = videoPlayer.totalDuration
duration_Slider.maximumValue = Float(totalDuration)
totalDuration_Label.text = getTimeString(seconds: Int(totalDuration))
}
private func setPeriodicTimer() {
videoPlayer.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 60), using: { [weak self] _ in
if self != nil
{
let currentDuration = self!.videoPlayer.currentDuration
self!.currentDuration_Label.text = self!.getTimeString(seconds: Int(currentDuration))
self!.duration_Slider.setValue(Float(currentDuration), animated: true)
}
})
}
private func getTimeString(seconds: Int) -> String {
String(format: "%02d:%02d", seconds / 60, seconds % 60)
}
}
// MARK: onClicked
extension GSPlayerControlUIView {
@IBAction func backButtonTapped(_ sender: UIButton) {
delegate?.backButtonTap()
}
@IBAction func onClicked_Play(_ sender: Any) {
if (videoPlayer.state == .playing) {
videoPlayer.pause(reason: .userInteraction)
} else {
videoPlayer.resume()
}
}
@IBAction func onClicked_Backward(_ sender: Any) {
videoPlayer.seek(to: CMTime(seconds: Double(max(videoPlayer.currentDuration - 10, 0)), preferredTimescale: 60))
}
@IBAction func onClicked_Forward(_ sender: Any) {
videoPlayer.seek(to: CMTime(seconds: Double(min(videoPlayer.currentDuration + 10, videoPlayer.totalDuration)), preferredTimescale: 60))
}
@IBAction func onClicked_FullScreen(_ sender: Any) {
if isFullScreen == false{
delegate?.fullScreenTap()
isFullScreen = true
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.myOrientation = .landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UIView.setAnimationsEnabled(true)
self.fullscreen_Button.setImage(UIImage(named: "normal_screen"), for: .normal)
}else{
isFullScreen = false
delegate?.normalScreenTap()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.myOrientation = .portrait
self.fullscreen_Button.setImage(UIImage(named: "full_screen"), for: .normal)
}
}
private func setOnClicked_VideoPlayer() {
videoPlayer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onClicked_Video(_:))))
}
@IBAction func onClicked_Video(_ sender: Any) {
UIView.animate(withDuration: 0.2) {
self.alpha = self.alpha == 0 ? 1 : 0
}
}
@IBAction func tapSlider(_ gestureRecognizer: UIGestureRecognizer) {
let pointTapped: CGPoint = gestureRecognizer.location(in: self)
let positionOfSlider: CGPoint = duration_Slider.frame.origin
let widthOfSlider: CGFloat = duration_Slider.frame.size.width
let newValue = ((pointTapped.x - positionOfSlider.x) * CGFloat(duration_Slider.maximumValue) / widthOfSlider)
duration_Slider.setValue(Float(newValue), animated: false)
onValueChanged_DurationSlider(duration_Slider)
}
@IBAction func onValueChanged_DurationSlider(_ sender: UISlider) {
videoPlayer.seek(to: CMTime(seconds: Double(sender.value), preferredTimescale: 60))
}
}
Issue:
The video does not start playing automatically, and ads do not play as expected before the main content. Though if user press on play button then everything works as expected.
I have tried many thing but none of them is working.
- Ensured the video and ad URLs are valid.
- Verified the adsLoader and adsManager setup.
Suvankar is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.