They Will Make You Hate It: Getting to Know VAST and VPAID, or How to Integrate Ads into a Web Video Player

Doubletapp
19 min readDec 12, 2023

Hello! My name is Valeria, and I’m a front-end developer at Doubletapp. I dedicated a year of my life to developing a video player with advertising integrations. At the start, there was only speculation, documentation, a single article on Habr explaining ads, and the client’s strong desire to implement it in the video player. Now it’s time to acquaint you more closely with advertising.

This article is for those who want to understand the specifics of how VAST and VPAID work, configure custom ad management, place video ads as a separate block on the site (out-stream), or integrate ad segments into video content (in-stream).

Toolkit, or main ingredients

Video Ad Serving Template or VAST is a unified standard for exchanging video advertisements, essentially an XML file. It is structured according to specifications developed by the Interactive Advertising Bureau (IAB).

With VAST, ad providers create integrations in a universal format, gather analytics on clicks, views, and other viewer interactions. They can provide relevant information based on age, location, and interests, building a more quality marketing campaign.

However, we are interested in the technical side of the matter. To run VAST, you need a video player and an XML parser. This article focuses on advertising, so I won’t delve into creating an HLS player or working with MSE; we’ll stick to the standard <video> tag. But when it comes to ad parsing, we’ll go hardcore. Google IMA SDK is probably the only tool that provides full control for working with ads and is open for use.

Just for reference: there are ready-made players with ad playback capabilities (both paid and free) — Video.js, JW Player, plyr, etc. If you dig deeper, you will see they use the same Google IMA SDK, but these libraries provide limited access to it and their custom interfaces. There is also Yandex’s Video Ads SDK, but unfortunately, the tool only works with its advertising partners.

In summary, IMA SDK will give us the flexibility to manage VAST, set up side effects, and, if necessary, change the playback logic according to the client’s requirements.

More about VAST

The simplest VAST has the following structure. It is unmistakable because the body is wrapped in a <VAST> node. Inside it can be AD nodes or an ERROR node if the server didn’t return an ad.

The <VAST> node specifies the version of the template. The latest version at the time of this article is 4.3. During player development, I encountered VASTs of versions 3.0 and 2.0. Don’t let this confuse you; the implementations do not differ much.

Starting from version 3.0, there can be multiple AD nodes inside VAST. They will play in the order specified in the sequence attribute. If an ad in the first AD node fails, the next one in the sequence is loaded.

<VAST version=”3.0”>
<AD id=”jDGmdD” sequence=”1”> … </AD>
<AD id=”hrYhfK” sequence=”2”> … </AD>

<AD id=”hrYhfK” sequence=”14”> … </AD>
</VAST>

Inside AD, there can be:

  1. The actual ad — an InLine node.
  2. Information about the intermediary in ad delivery — a Wrapper node.

Let’s first look at an example with the InLine node.

<VAST version="3.0">
<Ad id="1234567">
<InLine>
<AdSystem>Your Ad System</AdSystem>
<AdTitle>Name of your VAST</AdTitle>
<Description>Linear Video Ad</Description>
<Error>https://www.example.com/error</Error>
<Impression>https://www.example.com/impression</Impression>
<Creatives>
<Creative sequence="1">
<Linear skipoffset=”00:00:05”>
<Duration>00:01:55</Duration>
<TrackingEvents>
<Tracking event="start">https://www.example.com/start</Tracking>
<Tracking event="firstQuartile">https://www.example.com/firstQuartile</Tracking>
<Tracking event="midpoint">https://www.example.com/midpoint</Tracking>
<Tracking event="thirdQuartile">https://www.example.com/thirdQuartile</Tracking>
<Tracking event="complete">https://www.example.com/complete</Tracking>
<Tracking event="mute">https://www.example.com/mute</Tracking>
<Tracking event="unmute">https://www.example.com/unmute</Tracking>
<Tracking event="rewind">https://www.example.com/rewind</Tracking>
<Tracking event="pause">https://www.example.com/pause</Tracking>
<Tracking event="resume">https://www.example.com/resume</Tracking>
<Tracking event="fullscreen">https://www.example.com/fullscreen</Tracking>
</TrackingEvents>
<VideoClicks>
<ClickThrough id="123">https://google.com</ClickThrough>
<ClickTracking id="123">https://www.example.com/click</ClickTracking>
</VideoClicks>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4"> https://example.com/example.mp4 </MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>

*This is a file example. We do not guarantee that a link to an existing video file will be inside VAST.

AdSystem — the name of the advertising service providing the ad
AdTitle, Description — the title and description of the ad
ErrorURL that will be called in case of an error
ImpressionURL that will be called after displaying the first frame of the creative
Creatives — a set of creatives for display. One creative equals one media file for playback
Creative — a creative also has a sequence attribute similar to the AD node. There can be several, but the specification asks not to confuse Creative sequence with Ad sequence. In this case, sequence is the components of one ad
Linear — the type of ad. It can have a skipoffset attribute that sets the time after which the ad can be skipped

IAB provides three types of ads: Linear, NonLinearAds, and CompanionAds. I hope the illustration will help understand the difference between them.

In this article, we will take a closer look at the Linear type of ad:

Duration — the duration of the ad
TrackingEvents — a container for Tracking
Tracking
— contains a URL that will be called when the event specified in the event attribute occurs
VideoClicks — can contain nodes ClickThrough, ClickTracking, and CustomClick. It defines the behavior when clicking on the ad block
ClickThrough — contains a link that will be followed when interacting with the ad block (landing page, website)
ClickTracking — tracks click statistics described in the VideoClicks node (website visits or custom clicks)
MediaFiles — a container for MediaFile
MediaFile — contains a link to the ad. There can be several with different qualities, specifying width and height attributes. It is used to select resolution on different devices. The delivery attribute indicates the protocol for downloading the file — streaming or progressive.

Earlier, I mentioned that there are integrations wrapped in a Wrapper. This means that the original VAST has an intermediary who also added events to monitor impressions. In this wrapper, the link to the true VAST is in the VASTAdTagURI node. If you follow the link in it, you will see the same InLine XML file, similar to the example above.

<VAST xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vast.xsd" version="3.0">
<Ad id="5368884870">
<Wrapper>
<AdSystem>GDFP</AdSystem>
<VASTAdTagURI>
<![CDATA[ https://lerok007.ams3.digitaloceanspaces.com/vast%20(1).xml ]]>
</VASTAdTagURI>
<Error>https://www.example.com/error</Error>
<Impression>https://www.example.com/impression</Impression>
<Creatives>
<Creative id="138311401271" sequence="1">
<Linear>
<TrackingEvents>

</TrackingEvents>
<VideoClicks>

</VideoClicks>
</Linear>
</Creative>
</Creatives>
<Extensions>
<Extension type="geo">
<Country>RU</Country>
</Extension>
</Extensions>
</Wrapper>
</Ad>
</VAST>

*This is a file example taken from the IAB repository https://github.com/InteractiveAdvertisingBureau/VAST_Samples. Inside VAST, there may be a link to a non-existent video file.

Overall, this knowledge is sufficient to start development. However, if you crave details, you can refer to the documentation on the IAB website.

Launching an ad block on the page (out-stream)

I’ve published all the examples in this repository. Here you can check the functionality of the examples, experiment with settings, or get inspired by the code. Get your popcorn ready and don’t forget to turn off AdBlock!

Please navigate to the simpleExampleVast directory. I recommend simultaneously looking at the code to see the big picture and not get lost in the code parts. This could also be a nice workshop.

Let’s create a basic index.html and styles for it. IMA recommends placing the video element and the ad container at the same level. The VAST specification is designed to deliver ad content to video players, so we will select ads based on the original dimensions of the <video> tag. The adContainer is positioned absolutely and overlays the entire video element block.

/* ./src/simpleExampleVast/index.html **/
<div id="videoContainer">
<video id="videoElement"></video>
<div id="adContainer"></div>
</div>
/* ./src/simpleExampleVast/styles.html **/
#videoContainer {
position: relative;
width: 800px;
height: 450px;
background-color: black;
}

#videoElement {
width: 100%;
height: 100%;
}

#adContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

Connecting IMA SDK

In the context of this article, I hosted a working VAST for you at this link. You can compare it with the examples above. It doesn’t have event tracking, but it should be enough for practice.

We only need the IMA SDK from the tools (here is the documentation).

The IMA SDK can be connected directly via a script, but we will use the @alugha/ima package. This gives us several useful features:

  1. Typing
  2. Dynamic loading of IMA SDK
  3. Check for an active ad blocker

Let’s load the IMA SDK

import {loadImaSdk} from "@alugha/ima";

async function loadIma() {
try {
const ima = await loadImaSdk()
console.info('IMA SDK successfully loaded. Ima version: ' + ima.VERSION);
} catch (err) {
console.error("IMA SDK could not be loaded. Check your ad blocker");
}
}

loadIma()

The loadImaSdk function checks whether an ad blocker is currently active on the client. If there are no blockers, it loads the ima instance; otherwise, it triggers an error.

After loading, ad management will be handled through the ima entity returned by the loadImaSdk function. It will also be available in the global variables window.google.ima.

To ensure proper typing, let’s update the global interface.

/* ./src/types.d.ts **/
import { ImaSdk } from '@alugha/ima'

declare global {
interface Window {
google: { ima: ImaSdk }
}
}

Based on this knowledge, let’s write a universal function for loading the IMA SDK. We’ll need it for further development.

Our utility will take a callback that will be invoked upon successful ima loading.

/* ./src/imaLoader.ts **/
import { loadImaSdk } from '@alugha/ima'

export async function loadIma(onImaLoaded: () => void) {
const ima = await loadImaSdk().catch((err) =>
console.error('IMA SDK could not be loaded. Check your ad blocker.')
)

if (ima) {
console.info('IMA SDK successfully loaded.')
onImaLoaded()
}
}

Initialization

Details about configuring IMA are extensively covered in the documentation, so I’ll focus on what’s beyond the Get Started section. I’ll summarize the knowledge gained from developing a similar player, hoping it will help you save time in understanding the tool and its features.

With IMA, you can:

  • track ad events
  • describe side effects
  • obtain information about the ad
  • control sound, pause, rewind, etc.
  • load new ads at any time

and much more.

We will manipulate two elements

/* ./src/simpleExampleVast/index.ts **/
const videoElement = document.getElementById('videoContainer')
const adContainer = document.getElementById('adContainer')

I will describe all methods of interacting with ads in a custom class called ImaManager. If you have multiple ad blocks on your page (multiple adContainer), a new imaManager instance is created for each new container.

/* ./src/simpleExampleVast/index.ts **/
const imaManager = new ImaManager(videoElement, adContainer)

Initialization involves passing information about our adContainer to ima. This is the element through which IMA will inject ad blocks. They can be html with a media file in <iframe> or <video>. IMA SDK also provides additional controls, such as for skipping ads or timers.

Initialize the ad container in the ImaManager class

/* ./src/simpleExampleVast/ImaManager.ts **/
init() {
const ima = window.google.ima

const adDisplayContainer = new ima.AdDisplayContainer(
this.adContainer
)

adDisplayContainer.initialize()
}

and create the ad loader

/* ./src/simpleExampleVast/ImaManager.ts **/
init() {
if (this.adsLoader) return

const ima = window.google.ima

const adDisplayContainer = new ima.AdDisplayContainer(this.adContainer)
adDisplayContainer.initialize()

this.adsLoader = new ima.AdsLoader(adDisplayContainer)

this.adsLoader.addEventListener(
ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
this.onAdsManagerLoaded
)

this.adsLoader.addEventListener(
ima.AdErrorEvent.Type.AD_ERROR,
this.onAdError
)
}

Since we can declare the adsLoader once for each ImaManager, I added a check for its existence at the beginning of the method.

The adsLoader requests the XML file and signals its reception in the ADS_MANAGER_LOADED event or reports an error with AD_ERROR.

Handling errors

IMA has critical and non-critical errors. You can see a detailed description of errors and their codes here.

Let’s create a method to handle errors.

/* ./src/simpleExampleVast/ImaManager.ts **/
private onAdError = (adErrorEvent: google.ima.AdErrorEvent) => {
console.error(
adErrorEvent.getError().getErrorCode(),
adErrorEvent.getError().getMessage()
)
}

Handling the ADS_MANAGER_LOADED event

/* ./src/simpleExampleVast/ImaManager.ts **/
this.adsLoader.addEventListener(
ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
this.onAdsManagerLoaded
)

The ADS_MANAGER_LOADED event triggers when the XML file is received. At this point, we can interact with the adsManager, which is used for direct communication with the ad. The adsManager instance allows us to:

  • retrieve information about the ad
  • subscribe to events of the current ad
  • control ad playback — stop, rewind, or skip integration

Optionally, settings can be passed during the adsManager initialization — see the list here.

As an example, let’s change the media file loading timeout to 9 seconds (default is 8 seconds). This means that if the video or banner does not load within 9 seconds, IMA will skip it and load the next one or signal the end of ad playback (more on this in the ad event handling).

/* ./src/simpleExampleVast/ImaManager.ts **/
onAdsManagerLoaded = (
adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
) => {
console.info('Ads manager loaded')

const ima = window.google.ima

const adsRenderingSettings = new ima.AdsRenderingSettings()

adsRenderingSettings.loadVideoTimeout = 9000

this.adsManager = adsManagerLoadedEvent.getAdsManager(
this.videoElement,
adsRenderingSettings
)
}

Something can also go wrong during ad playback, so we add error handling and initialize the adsManager.

/* ./src/simpleExampleVast/ImaManager.ts **/
onAdsManagerLoaded = (
adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
) => {
// . . .

this.adsManager.addEventListener(
ima.AdErrorEvent.Type.AD_ERROR,
this.onAdError
)

this.adsManager.init(
this.videoElement.clientWidth,
this.videoElement.clientHeight,
ima.ViewMode.NORMAL
)
}

When initializing the adsManager, three mandatory arguments need to be passed — width and height of the ad block at mounting, and the display mode (“NORMAL” or “FULLSCREEN”).

Requesting and playing ads

In the ImaManager class, let’s create another method, requestAds, which will take a link to the ad and request its loading.

/* ./src/simpleExampleVast/ImaManager.ts **/
requestAds(adTagUrl: string) {
if (!this.adsLoader) return

console.info('Start to request ad')

const adsRequest = new window.google.ima.AdsRequest()

adsRequest.adTagUrl = adTagUrl

this.adsLoader.requestAds(adsRequest)
}

After calling this function, the adsLoader will start loading the ad and then dispatch the ADS_MANAGER_LOADED event. In this event, we get the adsManager, whose task is to find the .mp4 file in the XML and play it in the specified container. And… the ad still won’t play.

To finally display the ad on the page, we need to wait for it to load and call the adsManager.start() method.

/* ./src/simpleExampleVast/ImaManager.ts **/
private onAdsManagerLoaded = (
adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
) => {
// . . .

this.adsManager.addEventListener(
ima.AdEvent.Type.LOADED,
this.adsManager.start
)

this.adsManager.addEventListener(
ima.AdErrorEvent.Type.AD_ERROR,
this.onAdError
)

// . . .
}

Here is the final script with imaManager initialization. In subsequent examples, I’ll move the init function into the class constructor

/* ./src/simpleExampleVast/index.ts **/
import { loadIma } from '../imaLoader'

function createAdService() {
// Get elements and init ad's manager
const videoElement = document.getElementById(
'videoElement'
) as HTMLVideoElement

const adContainer = document.getElementById('adContainer') as HTMLDivElement

if (videoElement === null || adContainer === null)
throw Error('VideoElement or AdContainer not included in DOM')

const imaManager = new ImaManager(videoElement, adContainer)

// Init ima instance
imaManager.init()

// Activate controls
const startBtn = document.getElementById('startButton')

startBtn?.addEventListener('click', () => {
// Start to load current ad when start ad button clicked
imaManager.requestAds(
'https://lerok007.ams3.digitaloceanspaces.com/vast%20(1).xml'
)
})
}

document.addEventListener('DOMContentLoaded', () => loadIma(createAdService))

The ImaManager class for this example is in the simpleExampleVast folder in the repository. You can run the code with the command yarn simple-vast. Don’t forget to install dependencies first.

P. S. adsManager has many methods for working with ads and ad blocks (as there can be several of them). If you’re working with TypeScript, analyzing them won’t be difficult. For example, I added play, pause, and skip ad methods. It’s important to note: the time to skip ads is regulated in the skipoffset attribute of the VAST file.

Advanced control (diving into ad event handling)

Now, when the ad is launched, we see the following picture. Everything looks pretty good. But there are a couple of things I don’t like. Firstly, localization. Secondly, if I try to change the size of the videoContainer, the ad won’t adapt to it.

Default view
Increasing the container size during playback

All further changes in the repository will be based on the previous example but will be in the advancedExampleVast directory.

Localization

We can set additional ima settings globally for all adConteiners and specifically for each one (settings).

The example below illustrates local and global settings. An exception is localization — it can only be set for the entire project.

// global change

ima.settings.setLocale('ru')

ima.settings.setAutoPlayAdBreaks(false)
// local change for manual managing current adContainer

this.adsLoader.getSettings().setAutoPlayAdBreaks(false)

We will discuss setAutoPlayAdBreaks later when working with VMAP.

Adaptability

I’ll monitor the dimensions of the adaptive block using ResizeObserver. By the way, don’t forget to install a polyfill for it to work correctly in browsers.

/* ./src/advancedExampleVast/ImaManager.ts **/
private initResizeObserver(): void {
const resizeObserver = new ResizeObserver(() => {
this.adsManager?.resize(
this.videoElement.clientWidth,
this.videoElement.clientHeight,
window.google.ima.ViewMode.NORMAL
)
})

resizeObserver.observe(this.videoElement)
}

Event handling

You can find a full list of events for VAST here.

Let’s subscribe to some of them and log them.

/* ./src/advancedExampleVast/ImaManager.ts **/
private onAdsManagerLoaded = (
adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
) => {
// . . .

const imaEvents: google.ima.AdEvent.Type[] = [
window.google.ima.AdEvent.Type.LOADED,
window.google.ima.AdEvent.Type.STARTED,
window.google.ima.AdEvent.Type.AD_PROGRESS,
window.google.ima.AdEvent.Type.PAUSED,
window.google.ima.AdEvent.Type.RESUMED,
window.google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED,
window.google.ima.AdEvent.Type.SKIPPED,
window.google.ima.AdEvent.Type.CLICK,
window.google.ima.AdEvent.Type.VOLUME_CHANGED,
window.google.ima.AdEvent.Type.COMPLETE,
window.google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
]

imaEvents.forEach((imaEvent) =>
this.adsManager?.addEventListener(imaEvent, this.onAdEvent)
)
/* ./src/advancedExampleVast/ImaManager.ts **/
onAdEvent = (
adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
) => {
console.info('EVENT: ', adEvent.type)

switch (adEvent.type) {
case window.google.ima.AdEvent.Type.LOADED:
this.adsManager?.start()
break
}
}

In events, you can get data about the ad (adEvent.getAd() or adEvent.getAdData()). The returned object may vary depending on the event in which you requested it. For example, in the AD_PROGRESS event, we can get information about the ad’s duration, progress, etc.

For variety, let’s add a couple of side effects suggested by my imaginary client

  1. “I click on the ad, go to the site, then back, and the ad is paused. I want to be able to resume it.”

Here you go:

/* ./src/advancedExampleVast/ImaManager.ts **/
onAdEvent = (
adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
) => {
console.info('EVENT: ', adEvent.type)

switch (adEvent.type) {
case window.google.ima.AdEvent.Type.LOADED:
this.adsManager?.start()
break
case window.google.ima.AdEvent.Type.PAUSED:
this.resumeButton?.classList.remove('hidden')
break
case window.google.ima.AdEvent.Type.RESUMED:
this.resumeButton?.classList.add('hidden')
break
}
}

2. "I don’t like this emptiness after showing the ad. Let another ad play — endlessly.”

/* ./src/advancedExampleVast/ImaManager.ts **/
onAdEvent = (
adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
) => {
console.info('EVENT: ', adEvent.type)

switch (adEvent.type) {
// . . .
case window.google.ima.AdEvent.Type.SKIPPED:
case window.google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
this.requestAds(
'https://lerok007.ams3.digitaloceanspaces.com/vast%20(1).xml'
)
break
}
}

The ImaManager class for this example is in the advancedExampleVast folder in the repository. You can run the code with the command “yarn advanced-vast”.

P. S. This way, you can handle and launch both VAST and VMAP. But it’s essential to note that in the case of VMAP, only media files marked as preroll will be launched (more details in the VMAP description).

Breaking a video with ad breaks (in-stream, VMAP)

Scenario: Let’s imagine our client is a media service with a paid subscription, but it also provides resources for free viewing. To cover the costs of hosting such a large volume of files and ensure good bandwidth, substantial expenses are required, not to mention the licensing fees for broadcasting the content. Most similar services try to monetize the content they provide to non-subscribers by embedding ad blocks. These can be at the beginning, end, and middle of the content.

If we were to solve this problem with a simple VAST, we would have to track the video timestamp and launch ads at the right intervals. Fortunately, all of this can be automated using the VMAP specification.

As far as I know, ad services usually provide VAST files, but there’s nothing stopping us from packaging them into VMAP.

More about VMAP

You can skip this section if you are already familiar with the VMAP file template and proceed directly to the implementation section.

Video Multiple Ad Playlist or VMAP is a unified standard for exchanging sets of video ads. It is an XML file formatted according to the specification developed by the Interactive Advertising Bureau (IAB). At the time of writing this article, it has one version.

In simpler terms, VMAP is a schedule of ad breaks for a video player playing some content.

Below is the structure of a VMAP file:

<vmap:VMAP xmlns:vmap="http://www.iab.net/videosuite/vmap" version="1.0">
<vmap:AdBreak timeOffset="start" breakType="linear" breakId="preroll">
<vmap:AdSource id="preroll" allowMultipleAds="false" followRedirects="true">
<vmap:AdTagURI templateType="vast3">
<![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
</vmap:AdTagURI>
</vmap:AdSource>
</vmap:AdBreak>
<vmap:AdBreak timeOffset="00:00:30.000" breakType="linear" breakId="midroll-1">
<vmap:AdSource id="midroll-ad-1" allowMultipleAds="true" followRedirects="true">
<vmap:AdTagURI templateType="vast3">
<![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
</vmap:AdTagURI>
</vmap:AdSource>
</vmap:AdBreak>
<vmap:AdBreak timeOffset="00:01:00.000" breakType="linear" breakId="midroll-1">
<vmap:AdSource id="midroll-ad-2" allowMultipleAds="true" followRedirects="true">
(*1) <vmap:VASTData>
<VAST version=”3.0”>
. . .
</VAST>
</vmap:VASTData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event=”breakStart”>
https://www.example.com/breakStart
</vmap:Tracking>
(*2)</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak timeOffset="00:01:30.000" breakType="linear" breakId="midroll-1">
<vmap:AdSource id="midroll-ad-3" allowMultipleAds="false" followRedirects="true">
<vmap:AdTagURI templateType="vast3">
<![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
</vmap:AdTagURI>
</vmap:AdSource>
</vmap:AdBreak>
<vmap:AdBreak timeOffset="end" breakType="linear" breakId="postroll">
<vmap:AdSource id="postroll" allowMultipleAds="false" followRedirects="true">
<vmap:AdTagURI templateType="vast3">
<![CDATA[ https://raw.githubusercontent.com/InteractiveAdvertisingBureau/VAST_Samples/master/VAST%203.0%20Samples/Inline_Linear_Tag-test.xml ]]>
</vmap:AdTagURI>
</vmap:AdSource>
</vmap:AdBreak>
</vmap:VMAP>

*This is an example file. We do not guarantee that there will be a link to an existing video file inside VAST.

AdBreak: an ad break, a wrapper for individual VASTs.
Attributes:
timeOffset: specifies when the ad will start playing during the main video playback. Allowed values for IMA SDK are “start,” “end,” or a time value in milliseconds.
breakType: sets the type of ad.

AdSource: stores information about the VAST to be played. It can contain a link to VAST (AdTagURI) or an entire file ((1*) VASTData).
Attributes:
allowMultipleAds: allows or prohibits the presence of multiple Ad nodes in VAST (to form an ad block),
followRedirects: allows or prohibits the presence of a VAST with redirection to another VAST (e.g., Wrapper).

(2*) You can also subscribe to events in TrackingEvents

Connecting VMAP to the video player

In the repository, you can find the exampleVmap directory. I will describe all further actions in it.

I wouldn’t be me if I didn’t host a media file and a working VMAP (link for it) for you.

Open this file in your browser. It contains links to 4 ad blocks — at the beginning (preroll) and end (postroll) of playback, as well as two midrolls at the 5th and 10th seconds.

Here, we will load ads not by clicking a button but when starting the content video.

/* ./src/exampleVmap/index.ts **/
function createAdService() {
// . . .

videoElement.addEventListener('play', onVideoPlay)

function onVideoPlay() {
imaManager.requestAds(
'https://lerok007.ams3.digitaloceanspaces.com/vmap%20(2).xml'
)
videoElement.removeEventListener('play', onVideoPlay)
}
}

After these changes, ad breaks will play according to the specified schedule, but along with the main video, and also cover clickable areas after playback. For such side effects, IMA provides two events: CONTENT_PAUSE_REQUESTED and CONTENT_RESUME_REQUESTED. They indicate when the ad block plans to start playback and when it finishes.

/* ./src/exampleVmap/ImaManager.ts **/
onAdEvent = (
adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
) => {
console.info('EVENT: ', adEvent.type)

switch (adEvent.type) {
case window.google.ima.AdEvent.Type.PAUSED:
this.resumeButton?.classList.remove('hidden')
break
case window.google.ima.AdEvent.Type.RESUMED:
this.resumeButton?.classList.add('hidden')
break
case window.google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
this.videoElement.pause()
this.adContainer.classList.remove('backwards')
break
case window.google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
this.videoElement.play()
this.adContainer.classList.add('backwards')
break
}
}

The backwards class changes the z-index of the element to -1.

Note that in this example, we no longer launch ads on the LOADED event. This happens automatically since specific intervals for integrating ads are set in vast. Therefore, calling adsManager?.start() is necessary only once after initialization. IMA will prefetch the next ad before playback for minimal delay.

/* ./src/exampleVmap/ImaManager.ts **/
private onAdsManagerLoaded = (
adsManagerLoadedEvent: google.ima.AdsManagerLoadedEvent
) => {
// . . .

this.adsManager.init(
this.videoElement.clientWidth,
this.videoElement.clientHeight,
this.ima.ViewMode.NORMAL
)

this.adsManager?.start()
}

At this stage, all ads are played except the last one. This happens because IMA does not know when the content video ends. Let’s help it.

Check for the presence of postroll inside VMAP, and if it’s there, notify adsLoader.contentComplete() when the content video ends.

/* ./src/exampleVmap/ImaManager.ts **/
private addContentCompletedHandler(adsManager: google.ima.AdsManager) {
const hasPostroll = adsManager.getCuePoints().slice(-1)[0] === -1

if (!hasPostroll) return

const onContentEnded = () => {
if (hasPostroll) {
this.adsLoader?.contentComplete()
}
this.videoElement.removeEventListener('ended', onContentEnded)
}

this.videoElement.addEventListener('ended', onContentEnded)
}

Call this function after getting adsManager in the onAdsManagerLoaded function.

Manual control of ad start

Suppose we want to start loading in advance to avoid waiting for the media file and have the ability to display it when the player is turned on.

I placed this example in the advancedExampleVmap directory in the repository.

Disable automatic playback in the adsLoader settings

/* ./src/advancedExampleVmap/ImaManager.ts **/
init() {
// . . .

this.adsLoader = new this.ima.AdsLoader(adDisplayContainer)

this.adsLoader.getSettings().setAutoPlayAdBreaks(false)
}

Now, when the ad is ready to play, we will receive the AD_BREAK_READ event. Only after this event will we launch the integration.

When starting the main video playback, call the imaManager.startAd() method,

/* ./src/advancedExampleVmap/index.ts **/
function onVideoPlay() {

imaManager.startAd()

videoElement.removeEventListener('play', onVideoPlay)

}

which will start the ad block,

/* ./src/advancedExampleVmap/ImaManager.ts **/
startAd() {
if (this.adsReady) {
this.adsManager?.start()
}

this.adCanPlay = true
}

or wait until it loads.

/* ./src/advancedExampleVmap/ImaManager.ts **/
onAdEvent = (
adEvent: google.ima.AdEvent & { type?: google.ima.AdEvent.Type }
) => {
console.info('EVENT: ', adEvent.type)

switch (adEvent.type) {
case window.google.ima.AdEvent.Type.AD_BREAK_READY:
if (this.adCanPlay) {
this.adsManager?.start()
} else {
this.adsReady = true
}
break

// . . .
}

P. S. However, with these settings, be cautious if you are not working with a regular VAST file but with VPAID. Its code may have a waiting time for startup. In other words, after loading, some VPAID developers set a timeout, after which we must call the adsManager.start() command; otherwise, the ad will fail with an error, and IMA will skip it.

Have you reached this section? Congratulations. I was just about to tell you about VPAID.

Features of VPAID

Let’s recall what VAST looks like.

And compare it with VPAID below

<VAST version="3.0">
<Ad id="1234567">
<InLine>
<AdSystem>Your Ad System</AdSystem>
<AdTitle>Name of your VAST</AdTitle>
<Description>Linear Video Ad</Description>
<Error>https://www.example.com/error</Error>
<Impression>https://www.example.com/impression</Impression>
<Creatives>
<Creative sequence="1">
<Linear skipoffset=”00:00:05”>
<Duration>00:01:55</Duration>
<TrackingEvents>
. . .
</TrackingEvents>
<VideoClicks>
. . .
</VideoClicks>
<MediaFiles>
<MediaFile maintainAspectRatio="true" scalable="true" delivery="progressive" apiFramework="VPAID" type="application/javascript"> <![CDATA[ https://example.com/vpaidVideoAd.js ]]>
</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>

*This is a file example. We do not guarantee that there will be a link to an existing video file inside VAST

I’ll help you, the only difference is that here, in MediaFile, it contains not a media file .mp4 but a script.

Video Player-Ad Interface Definition (VPAID) is a specification for interactive advertising. Developers can place any script in it, render any elements within the container. In the end, it may not even be a video file but a banner with a survey. My advice: never try to handle the behavior of this ad and leave it exactly as the VPAID developer described it, following the advertiser’s wishes.

Our task is only to launch VPAID, and this is done by adding just one line

 // for local adContainer
this.adsLoader
.getSettings()
.setVpaidMode(window.google.ima.ImaSdkSettings.VpaidMode.INSECURE)

// global for all project
this.ima.settings.setVpaidMode(
window.google.ima.ImaSdkSettings.VpaidMode.INSECURE
)

You can find out that VPAID has come to you after loading the ad in the LOADED event

case window.google.ima.AdEvent.Type.LOADED:
this.isVPAID = adEvent.getAdData().apiFramework === 'VPAID'
break

VPAID works by describing methods for various ad events — they are listed in the specification, which you can find here.

At this point, I would like to finish my “short” story about advertising. But a couple more points that might be useful to you:

  • autoplay

VAST is ultimately a media file, and like all media elements in browsers, there are restrictions on playback. If the video plays after user interaction, the ad may play with sound. It’s better to autoplay ads without sound; otherwise, in some browsers, it may not play. With VAST, it’s enough to set

adsManager.setVolume(0)

immediately after getting adsManager

However, there may be issues with VPAID because its script may also override sound settings. In this case, it’s ideal to find a way to negotiate with representatives of ad services to configure VPAID for soundless playback if it’s not possible in their admin panels.

  • fullscreen

You can take ready-made solutions together with the player — such as video.js or plyr. If you use the IMA SDK, you need to handle fullscreen on the parent videoElement and adContainer.

  • seeking

During seeking of the main video, IMA plays an ad block if it falls within the seek interval, but not more than one.

  • VAST preloading

IMA tries to preload VASTs before the start of the ad block inside the player.

That’s definitely all; you can turn on AdBlock again! Thank you for reading this article, and I hope you found it interesting!

--

--