How to Implement Video Ads in React Native Apps

Written by cabhara | Published 2022/01/24
Tech Story Tags: react-native | mobile-development | ios | google-video-ads | hackernoon-top-story | programming | mobile-app-development | app-development | web-monetization

TLDRWe needed to add Video Ads to the 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 IMA API in Native iOS and Android instead. See the implementation for iOS.via the TL;DR App

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.

Implementing Video Ads in React Native Apps

Part 1: iOS

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

References

Expo

https://docs.expo.dev/

Using Native Components in React Native

https://reactnative.dev/docs/native-components-ios

https://reactnative.dev/docs/native-components-android

https://teabreak.e-spres-oh.com/swift-in-react-native-the-ultimate-guide-part-2-ui-components-907767123d9e

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


Written by cabhara | Mobile App Developer, React Native, iOS, Android, Unity
Published by HackerNoon on 2022/01/24