We needed to add Video Ads to our broadcasts in our React Native (Expo) app. After trying out a few (mostly outdated) implementations for React Native, I decided to implement the ads with the Google IMA API in Native iOS and Android instead.
Hopefully, the code examples can be helpful if you are trying to do something similar.
The video ads are being delivered by Google Ad Manager. For our sample application, we will use Google’s sample vast tags.
Let’s get started with our sample app. In your terminal, run
expo init Media
I’m selecting the blank template. For more information on Expo and settings needed, check out https://docs.expo.dev/.
To run our project, type:
cd Media
yarn ios
You should now see the iOS app running inside the Expo Go Client.
Let’s add React Navigation so we can navigate between screens.
expo install @react-navigation/native
expo install react-native-screens react-native-safe-area-context
To add a Stack Navigator, install
expo install @react-navigation/native-stack
Replace the App.js file with
import * as React from 'react';
import { View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
Create a folder “screens” and a new JS File “VideoScreen.js” in it:
import * as React from "react";
import { View, Text } from "react-native";
export default function VideoScreen() {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Text>Video Screen</Text>
</View>
);
}
In App.js, add a button to the Home Screen. Import the VideoScreen and add it to the stack.
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import VideoScreen from './screens/VideoScreen';
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button
title="Video"
onPress={() => navigation.navigate('Video')}
/>
</View>
);
}
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Video" component={VideoScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
Let’s add Video and IMA for iOS now.
First, let’s create the native iOS folder by running:
expo run:ios
Add the following to the Podfile in the ios folder:
source 'https://github.com/CocoaPods/Specs.git'
...
# (below target 'Media' do)
pod 'GoogleAds-IMA-iOS-SDK'
...
Inside the ios folder, run pod install to install the Google IMA library.
pod install --repo-update
Create the Objective C file RNTVideoViewManager.m . We are defining 2 parameters that will be passed from React Native to iOS and a view that will be returned to React native.
#import <Foundation/Foundation.h>
#import <React/RCTViewManager.h>
#import "Media-Swift.h"
@interface RNTVideoViewManager : RCTViewManager
@end
@implementation RNTVideoViewManager
RCT_EXPORT_VIEW_PROPERTY(videoUrl, NSString);
RCT_EXPORT_VIEW_PROPERTY(adTagUrl, NSString);
RCT_EXPORT_MODULE()
- (UIView *)view {
return [VideoView new];
}
@end
Create the VideoView.swift file.
import UIKit
class VideoView: UIView {
weak var videoViewController: VideoViewController?
@objc var videoUrl: String? {
didSet {
setNeedsLayout()
}
}
@objc var adTagUrl: String? {
didSet {
setNeedsLayout()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) { fatalError("nope") }
override func layoutSubviews() {
super.layoutSubviews()
if (self.videoViewController==nil) {
embed()
} else {
videoViewController?.view.frame = bounds
}
}
private func embed() {
guard
let parentVC = parentViewController
else {
return
}
let vc = VideoViewController()
vc.videoUrl = videoUrl ?? ""
vc.adTagUrl = adTagUrl ?? ""
vc.videoView = self
parentVC.addChild(vc)
addSubview(vc.view)
vc.view.frame = bounds
vc.didMove(toParent: parentVC)
self.videoViewController = vc
}
}
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
Create VideoViewController.swift
import AVFoundation
import AVKit
import UIKit
import GoogleInteractiveMediaAds
class VideoViewController: UIViewController, IMAAdsLoaderDelegate,IMAAdsManagerDelegate {
var videoUrl:String = ""
var adTagUrl:String = ""
var videoView:VideoView?
var adsLoader: IMAAdsLoader!
var adsManager: IMAAdsManager!
var contentPlayhead: IMAAVPlayerContentPlayhead?
var playerViewController: AVPlayerViewController!
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.black;
setUpContentPlayer()
setUpAdsLoader()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated);
requestAds()
}
func setUpContentPlayer() {
// Load AVPlayer with path to your content.
do{
// Configure and activate the AVAudioSession
// so the video sound will play even when the iPhone is muted
try AVAudioSession.sharedInstance().setCategory(
AVAudioSession.Category.playback
)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
//error setting audio
}
let contentURL = URL(string: videoUrl)
let player = AVPlayer(url: contentURL!)
playerViewController = AVPlayerViewController()
playerViewController.player = player
// Set up your content playhead and contentComplete callback.
contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: player)
NotificationCenter.default.addObserver(
self,
selector: #selector(VideoViewController.contentDidFinishPlaying(_:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: player.currentItem);
showContentPlayer()
}
func showContentPlayer() {
self.addChild(playerViewController)
playerViewController.view.frame = self.view.bounds
self.view.insertSubview(playerViewController.view, at: 0)
playerViewController.didMove(toParent:self)
}
func hideContentPlayer() {
// The whole controller needs to be detached so that it doesn't capture events from the remote.
playerViewController.willMove(toParent:nil)
playerViewController.view.removeFromSuperview()
playerViewController.removeFromParent()
}
func setUpAdsLoader() {
adsLoader = IMAAdsLoader(settings: nil)
adsLoader.delegate = self
}
func requestAds() {
// Create ad display container for ad rendering.
let adDisplayContainer = IMAAdDisplayContainer(adContainer: self.view, viewController: self)
// Create an ad request with our ad tag, display container, and optional user context.
let request = IMAAdsRequest(
adTagUrl: adTagUrl,
adDisplayContainer: adDisplayContainer,
contentPlayhead: contentPlayhead,
userContext: nil)
adsLoader.requestAds(with: request)
}
@objc func contentDidFinishPlaying(_ notification: Notification) {
adsLoader.contentComplete()
}
// MARK: - IMAAdsLoaderDelegate
func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
adsManager = adsLoadedData.adsManager
adsManager.delegate = self
adsManager.initialize(with: nil)
}
func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
showContentPlayer()
playerViewController.player?.play()
}
// MARK: - IMAAdsManagerDelegate
func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) {
// Play each ad once it has been loaded
if event.type == IMAAdEventType.LOADED {
adsManager.start()
}
}
func adsManager(_ adsManager: IMAAdsManager, didReceive error: IMAAdError) {
// Fall back to playing content
showContentPlayer()
playerViewController.player?.play()
}
func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) {
// Pause the content for the SDK to play ads.
playerViewController.player?.pause()
hideContentPlayer()
}
func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) {
// Resume the content since the SDK is done playing ads (at least for now).
showContentPlayer()
playerViewController.player?.play()
}
}
Let’s use our native component in React Native now. Create a folder “components” and inside a file VideoViewIos.js.
// requireNativeComponent automatically resolves 'RNTVideoView' to 'RNTVideoViewManager'
import { requireNativeComponent } from "react-native";
module.exports = requireNativeComponent("RNTVideoView");
In the VideoScreen.js, add the new VideoViewIos.
import * as React from "react";
import { View, Text, Dimensions } from "react-native";
import Constants from "expo-constants";
import VideoViewIos from "../components/VideoViewIos"
const videoLink = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8";
const adTagUrl = "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=";
const width=Dimensions.get("window").width;
export default function VideoScreen() {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
{Constants.platform.ios && (
<VideoViewIos
videoUrl={videoLink}
adTagUrl={adTagUrl}
style={{
height: width * 0.56,
width: width,
backgroundColor: "grey",
}}
/>
)}
</View>
);
}
Now when we click on Video, we will see the sample preroll and our video afterwards.
Please remember, when using a production ad tag url, you also need to add your GAD application identifier - either in your app.json:
"ios": {
...
"config": {
"googleMobileAdsAppId": "ca-app-pub-XXXXX~XXXXX"
}
},
or in your Info.plist in your Xcode project
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-XXXXX~XXXXX</string>
To check out the full implementation, see the repository on Github. It also includes the Audio implementation, including a way to stop and resume audio from the React Native side.
https://github.com/cabhara/ReactNativeVideoAds
Expo
Using Native Components in React Native
https://reactnative.dev/docs/native-components-ios
https://reactnative.dev/docs/native-components-android
React Navigation
https://reactnavigation.org/docs
Google IMA
https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side
https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side
https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags