Skip to content

Instantly share code, notes, and snippets.

@i-like-robots
Last active January 28, 2021 14:16
Show Gist options
  • Save i-like-robots/4d808f71c5602e0d6dfd320a37b24cb2 to your computer and use it in GitHub Desktop.
Save i-like-robots/4d808f71c5602e0d6dfd320a37b24cb2 to your computer and use it in GitHub Desktop.
Minimum viable IMA implementation for desktop and mobile with support for autoplay when available and basic error handling.
'use strict'
const CLASSNAME_WAITING = 'is-waiting'
const CLASSNAME_LOADING = 'is-loading'
const CLASSNAME_PREROLL = 'is-preroll'
const CLASSNAME_PLAYING = 'is-playing'
const CLASSNAME_PROBLEM = 'is-problem'
// <https://developers.google.com/interactive-media-ads/docs/sdks/html5/sdk-player>
function ads (target) {
let initialised = false;
const videoContent = target.querySelector('.player__video-content')
const videoPreroll = target.querySelector('.player__video-preroll')
const videoProblem = target.querySelector('.player__video-problem')
const videoTrigger = target.querySelector('.player__video-trigger')
// create ad display container - link video element with ad overlay
const adDisplayContainer = new google.ima.AdDisplayContainer(
videoPreroll,
videoContent
)
// Create ads loader
const adsLoader = new google.ima.AdsLoader(adDisplayContainer)
// Add listeners to the ads loader for loaded and error events
adsLoader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
onAdsManagerLoaded,
false)
adsLoader.addEventListener(
google.ima.AdErrorEvent.Type.AD_ERROR,
onAdError,
false)
// Request video ads
const adsRequest = new google.ima.AdsRequest()
// adsRequest.adTagUrl = 'https://pubads.g.doubleclick.net/gampad/ads?'
adsRequest.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%3Dskippablelinear&correlator='
// Specify the linear and nonlinear slot sizes. This helps the SDK to
// select the correct creative if multiple are returned.
// ! AFAIK this isn't actually necessary
// adsRequest.linearAdSlotWidth = 640
// adsRequest.linearAdSlotHeight = 360
// adsRequest.nonLinearAdSlotWidth = 640
// adsRequest.nonLinearAdSlotHeight = 360
// reference to ads manager which will control ad playback
let adsManager
function onAdsManagerLoaded (event) {
const adsRenderingSettings = new google.ima.AdsRenderingSettings()
// enable preloading (will not work on mobile, etc.)
adsRenderingSettings.enablePreloading = true
// you must restore original state for mobile devices that recycle video element
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true
// get the ads manager
adsManager = event.getAdsManager(videoContent, adsRenderingSettings)
// Add listeners to the required events
adsManager.addEventListener(
google.ima.AdErrorEvent.Type.AD_ERROR,
onAdError)
adsManager.addEventListener(
google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
onContentPauseRequested)
adsManager.addEventListener(
google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
onContentResumeRequested)
// ! extra events for tracking
// adsManager.addEventListener(
// google.ima.AdEvent.Type.LOADED,
// onAdTrackingEventHandler)
// adsManager.addEventListener(
// google.ima.AdEvent.Type.STARTED,
// onAdTrackingEventHandler)
// adsManager.addEventListener(
// google.ima.AdEvent.Type.COMPLETE,
// onAdTrackingEventHandler)
// adsManager.addEventListener(
// google.ima.AdEvent.Type.SKIPPED,
// onAdTrackingEventHandler)
try {
// Initialize the ads manager. Ad rules playlist will start at this time.
const w = target.clientWidth
const h = target.clientHeight
adsManager.init(w, h, google.ima.ViewMode.NORMAL)
// Call play to start showing the ad. Single video and overlay ads will
// start at this time; the call will be ignored for ad rules.
adsManager.start()
} catch (err) {
// An error may be thrown if there was a problem with the VAST response.
console.error(err.toString())
}
}
// Handle the error logging and destroy the AdsManager
function onAdError (adErrorEvent) {
const err = adErrorEvent.getError()
console.error(err.getMessage())
if (adsManager) {
adsManager.destroy()
}
// remove loading state if we failed already
if (err.getType() === google.ima.AdError.Type.AD_LOAD) {
target.classList.remove(CLASSNAME_LOADING)
}
// continue with video content playback
onContentResumeRequested()
}
// This function is where you should setup UI for showing ads (e.g.
// display ad timer countdown, disable seeking, etc.)
function onContentPauseRequested() {
target.classList.remove(CLASSNAME_LOADING)
target.classList.add(CLASSNAME_PREROLL)
// disable video UI
videoContent.controls = false
videoContent.pause()
}
// This function is where you should ensure that your UI is ready
// to play content.
function onContentResumeRequested () {
adDisplayContainer.destroy()
target.classList.remove(CLASSNAME_PREROLL)
target.classList.add(CLASSNAME_PLAYING)
// re-enable video UI
videoContent.controls = true
videoContent.play()
}
function play () {
if (initialised) return
adsLoader.requestAds(adsRequest)
// "Call this method as a direct result of a user action before starting the ad playback..."
adDisplayContainer.initialize()
// you must call .load() on mobile devices to trigger `loadedmetadata` event
videoContent.load()
target.classList.remove(CLASSNAME_WAITING)
target.classList.add(CLASSNAME_LOADING)
initialised = true
}
videoTrigger.addEventListener('click', play)
function problem (e) {
target.classList.remove(CLASSNAME_WAITING, CLASSNAME_LOADING, CLASSNAME_PREROLL)
target.classList.add(CLASSNAME_PROBLEM)
const message = e.hasOwnProperty('code') ? `Error #${e.code}` : 'Unknown'
videoProblem.innerHTML = `
<p role="alert">
An error occured when trying to playback this video (${message})
</p>
`
}
videoContent.addEventListener('error', problem, true)
if (videoContent.networkState === videoContent.NETWORK_NO_SOURCE) {
const e = new Error('Network error')
// mock a real MediaError
Object.defineProperty(e, 'code', {
value: MediaError.MEDIA_ERR_NETWORK
})
problem(e)
}
// Play (with sound) can only be initiated by a user gesture on mobile
// <https://developers.google.com/web/updates/2016/07/autoplay>
// <https://webkit.org/blog/6784/new-video-policies-for-ios/>
//
// 1. Detect if autoplay is available (autoplay.js)
// 2. Trigger .load() because not all browsers will without autoplay
// 3. Programatically call .play() only when the video is ready
if (target.hasAttribute('data-autoplay')) {
// Always start with loading state
target.classList.add(CLASSNAME_LOADING)
autoplay().then(function (enabled) { // 1
if (enabled) {
videoContent.load() // 2
if (videoContent.readyState === videoContent.HAVE_ENOUGH_DATA) {
play() // 3
} else {
function onCanplay () {
play()
videoContent.removeEventListener('canplay', onCanplay)
}
videoContent.addEventListener('canplay', onCanplay) // 3
}
} else {
target.classList.remove(CLASSNAME_LOADING)
target.classList.add(CLASSNAME_WAITING)
}
})
} else {
target.classList.add(CLASSNAME_WAITING)
}
}
'use strict'
function autoplay () {
const MP4 = 'data:video/mp4;base64, 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'
return new Promise(function (resolve, reject) {
try {
const video = document.createElement('video')
video.addEventListener('playing', resolve)
video.addEventListener('error', reject)
setTimeout(reject, 1000)
video.src = MP4
video.play()
} catch (err) {
reject(err)
}
})
.then(function () {
return true
})
.catch(function () {
return false
})
}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Video IMA test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="player">
<video class="player__video-content" playsinline>
<!-- broken video to test errors -->
<!--<source src="https://bcsecure04-a.akamaihd.net/34/47628783001/201702/3899/47628783001_5311835884001_5311832015001.mp4?pubId=47628783001&videoId=5311832015001" type="video/mp4">-->
<!-- working video to test playback-->
<source src="https://next-video.ft.com/v2/34/47628783001/201702/3416/47628783001_5334328631001_5334326716001.mp4" type="video/mp4">
</video>
<!-- browsers handle poster attribute differently -->
<img class="player__video-placard" src="https://bcsecure01-a.akamaihd.net/13/47628783001/201702/3416/47628783001_5334326337001_5334326716001-vs.jpg?pubId=47628783001">
<div class="player__video-loading"></div>
<div class="player__video-preroll"></div>
<div class="player__video-problem"></div>
<button class="player__video-trigger" type="button" title="click to play video"></button>
</div>
<!-- only including to test in IE11 and old Android -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?rum=1"></script>
<!-- this could be loaded by our ads script -->
<script src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"></script>
<script src="autoplay.js"></script>
<script src="ads.js"></script>
<script>
ads(document.querySelector('.player'))
</script>
</body>
</html>
.player {
position: relative;
width: 640px;
max-width: 100%;
background: #000;
color: #FFF;
}
.player::before {
/* create a fixed aspect-ratio container with padding hack */
content: '';
display: block;
padding-top: 56.26%;
}
.player__video-content,
.player__video-placard,
.player__video-loading,
.player__video-preroll,
.player__video-problem,
.player__video-trigger {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
/* JS will toggle the state */
display: none;
}
.is-preroll .player__video-content,
.is-playing .player__video-content,
.is-waiting .player__video-placard,
.is-preroll .player__video-preroll,
.is-waiting .player__video-trigger {
display: block;
}
/* don't confuse flex and box containers to avoid browser quirks in FF and Safari */
.is-loading .player__video-loading,
.is-problem .player__video-problem {
display: flex;
justify-content: center; /* main axis (horizontal) */
align-items: center; /* cross axis (vertical) */
}
/* loading */
@keyframes loading-spinner {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.player__video-loading::after {
content: '';
width: 30px;
height: 30px;
border: 4px solid rgba(255, 255, 255, 0.5);
border-left-color: #FFF;
border-radius: 50%;
animation: loading-spinner 1s linear infinite;
}
/* problems */
.player__video-problem {
padding: 20px;
}
.player__video-problem p {
margin: 0;
}
/* trigger */
.player__video-trigger {
border: 0;
padding: 0;
margin: 0;
background: none;
}
.player__video-trigger::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60px;
height: 60px;
background-color: #FFF;
background-image: url('https://www.ft.com/__origami/service/image/v2/images/raw/fticon-v1:play?source=o-icons&format=svg');
background-size: contain;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment