Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃悰 amp-ima-video: Show sound and fullscreen buttons during ads #18954

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions ads/ads.extern.js
Expand Up @@ -198,6 +198,8 @@ data.slot;
// imaVideo.js
var google;
google.ima;
google.ima.Ad;
google.ima.Ad.getSkipTimeOffset;
google.ima.AdDisplayContainer;
google.ima.AdDisplayContainer.initialize;
google.ima.ImaSdkSettings;
Expand All @@ -222,12 +224,20 @@ google.ima.UiElements;
google.ima.UiElements.AD_ATTRIBUTION;
google.ima.UiElements.COUNTDOWN;
google.ima.AdEvent;
google.ima.AdEvent.getAd;
google.ima.AdEvent.getAdData;
google.ima.AdEvent.Type;
google.ima.AdEvent.Type.AD_PROGRESS;
google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED;
google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED;
google.ima.AdEvent.Type.LOADED;
google.ima.AdEvent.Type.ALL_ADS_COMPLETED;
google.ima.AdsManager;
google.ima.AdsManager.getRemainingTime;
google.ima.AdsManager.setVolume;
google.ima.AdProgressData;
google.ima.AdProgressData.adPosition;
google.ima.AdProgressData.totalAds;
google.ima.settings;
google.ima.settings.setLocale;
google.ima.settings.setVpaidMode;
Expand Down
146 changes: 135 additions & 11 deletions ads/google/imaVideo.js
Expand Up @@ -56,7 +56,6 @@ const icons = {
`<circle cx="12" cy="12" r="12" />`
};

const controlsBg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAABkCAQAAADtJZLrAAAAQklEQVQY03WOwQoAIAxC1fX/v1yHaCgVeHg6wWFCAEABJl7glgZtmVaHZYmDjpxblVCfZPPIhHl9NntovBaZnf12LeWZAm6dMYNCAAAAAElFTkSuQmCC';
/*eslint-enable */

const bigPlayDivDisplayStyle = 'table-cell';
Expand All @@ -73,6 +72,12 @@ let playButtonDiv;
// Div containing player controls.
let controlsDiv;

// Wrapper for ad countdown element.
let countdownWrapperDiv;

// Div containing ad countdown timer.
let countdownDiv;

// Div containing play or pause button.
let playPauseDiv;

Expand Down Expand Up @@ -134,6 +139,9 @@ let allAdsCompleted;
// Flag tracking if an ad request has failed.
let adRequestFailed;

// IMA SDK Ad object
let currentAd;

// IMA SDK AdDisplayContainer object.
let adDisplayContainer;

Expand Down Expand Up @@ -161,6 +169,9 @@ let fullscreenWidth;
// Height the player should be in fullscreen mode.
let fullscreenHeight;

// "Ad" label used in ad controls.
let adLabel;

// Flag tracking if ads are currently active.
let adsActive;

Expand Down Expand Up @@ -210,6 +221,7 @@ export function imaVideo(global, data) {

videoWidth = global./*OK*/innerWidth;
videoHeight = global./*OK*/innerHeight;
adLabel = data.adLabel || 'Ad (%s of %s)';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure to document later on that this will be the default 馃憤


// Wraps *everything*.
wrapperDiv = global.document.createElement('div');
Expand Down Expand Up @@ -248,17 +260,38 @@ export function imaVideo(global, data) {
'bottom': '0px',
'width': '100%',
'height': '100px',
'background-color': 'rgba(7, 20, 30, .7)',
'background':
'linear-gradient(0, rgba(7, 20, 30, .7) 0%, rgba(7, 20, 30, 0) 100%)',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh I like this approach, thank you! 馃槃

'box-sizing': 'border-box',
'padding': '10px',
'padding-top': '60px',
'background-image': `url(${controlsBg})`,
'background-position': 'bottom',
'color': 'white',
'display': 'none',
'font-family': 'Helvetica, Arial, Sans-serif',
'justify-content': 'center',
'align-items': 'center',
'user-select': 'none',
'z-index': '1',
});
// Ad progress
countdownWrapperDiv = global.document.createElement('div');
countdownWrapperDiv.id = 'ima-countdown';
setStyles(countdownWrapperDiv, {
'align-items': 'center',
'box-sizing': 'border-box',
'display': 'none',
'flex-grow': '1',
'font-size': '12px',
'height': '20px',
'overflow': 'hidden',
'padding': '5px',
'text-shadow': '0px 0px 10px black',
'white-space': 'nowrap',
});
countdownDiv = global.document.createElement('div');
countdownWrapperDiv.appendChild(countdownDiv);
controlsDiv.appendChild(countdownWrapperDiv);
// Play button
playPauseDiv = createIcon(global, 'play');
playPauseDiv.id = 'ima-play-pause';
Expand All @@ -276,7 +309,6 @@ export function imaVideo(global, data) {
setStyles(timeDiv, {
'margin-right': '20px',
'text-align': 'center',
'font-family': 'Helvetica, Arial, Sans-serif',
'font-size': '14px',
'text-shadow': '0px 0px 10px black',
});
Expand Down Expand Up @@ -332,6 +364,7 @@ export function imaVideo(global, data) {
setStyles(muteUnmuteDiv, {
'width': '30px',
'height': '30px',
'flex-shrink': '0',
'margin-right': '20px',
'font-size': '1.25em',
'cursor': 'pointer',
Expand All @@ -344,6 +377,7 @@ export function imaVideo(global, data) {
setStyles(fullscreenDiv, {
'width': '30px',
'height': '30px',
'flex-shrink': '0',
'font-size': '1.25em',
'cursor': 'pointer',
'text-align': 'center',
Expand Down Expand Up @@ -716,13 +750,16 @@ export function onContentEnded() {
export function onAdsManagerLoaded(global, adsManagerLoadedEvent) {
const adsRenderingSettings = new global.google.ima.AdsRenderingSettings();
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
adsRenderingSettings.uiElements =
[global.google.ima.UiElements.AD_ATTRIBUTION,
global.google.ima.UiElements.COUNTDOWN];
adsManager = adsManagerLoadedEvent.getAdsManager(videoPlayer,
adsRenderingSettings);
adsManager.addEventListener(global.google.ima.AdErrorEvent.Type.AD_ERROR,
onAdError);
adsManager.addEventListener(
global.google.ima.AdEvent.Type.LOADED,
onAdLoad);
adsManager.addEventListener(
global.google.ima.AdEvent.Type.AD_PROGRESS,
onAdProgress);
adsManager.addEventListener(
global.google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
onContentPauseRequested.bind(null, global));
Expand Down Expand Up @@ -762,13 +799,41 @@ export function onAdsLoaderError() {
*/
export function onAdError() {
postMessage({event: VideoEvents.AD_END});
currentAd = null;
if (adsManager) {
adsManager.destroy();
}
videoPlayer.addEventListener(interactEvent, showControls);
playVideo();
}

/**
* Called each time a new ad loads. Sets currentAd
* @param {!Object} global
* @visibleForTesting
*/
export function onAdLoad(global) {
currentAd = global.getAd();
}

/**
* Called intermittently as the ad plays, allowing us to display ad counter.
* @param {!Object} global
* @visibleForTesting
*/
export function onAdProgress(global) {
const {adPosition, totalAds} = global.getAdData();
const remainingTime = adsManager.getRemainingTime();
const remainingMinutes = Math.floor(remainingTime / 60);
let remainingSeconds = Math.floor(remainingTime % 60);
if (remainingSeconds.toString().length < 2) {
remainingSeconds = '0' + remainingSeconds;
}
const label = adLabel.replace('%s', adPosition).replace('%s', totalAds);
countdownDiv.textContent
= `${label}: ${remainingMinutes}:${remainingSeconds}`;
}

/**
* Called by the IMA SDK. Pauses the content and readies the player for ads.
* @param {!Object} global
Expand All @@ -788,7 +853,7 @@ export function onContentPauseRequested(global) {
videoPlayer.removeEventListener(interactEvent, showControls);
setStyle(adContainerDiv, 'display', 'block');
videoPlayer.removeEventListener('ended', onContentEnded);
hideControls();
showAdControls();
videoPlayer.pause();
}

Expand All @@ -801,6 +866,7 @@ export function onContentResumeRequested() {
adsActive = false;
videoPlayer.addEventListener(interactEvent, showControls);
postMessage({event: VideoEvents.AD_END});
resetControlsAfterAd();
if (!contentComplete) {
// CONTENT_RESUME will fire after post-rolls as well, and we don't want to
// resume content in that case.
Expand All @@ -818,6 +884,7 @@ export function onContentResumeRequested() {
* @visibleForTesting
*/
export function onAllAdsCompleted() {
currentAd = null;
allAdsCompleted = true;
}

Expand Down Expand Up @@ -1150,6 +1217,61 @@ function onFullscreenChange(global) {
postMessage({event: 'fullscreenchange', isFullscreen: fullscreen});
}

/**
* Show a subset of controls when ads are playing.
* Visible controls are countdownDiv, muteUnmuteDiv, and fullscreenDiv
*
* @visibleForTesting
*/
export function showAdControls() {
const hasMobileStyles = videoWidth <= 400;
const isSkippable = currentAd ? currentAd.getSkipTimeOffset() !== -1 : false;
const miniControls = hasMobileStyles && isSkippable;
// hide non-ad controls
const hideElement = button => setStyle(button, 'display', 'none');
[playPauseDiv, timeDiv, progressBarWrapperDiv].forEach(hideElement);
// set ad control styles
setStyles(controlsDiv, {
'height': miniControls ? '20px' : '30px',
'justify-content': 'flex-end',
'padding': '10px',
});
const buttonDefaults = {
'height': miniControls ? '18px' : '22px',
};
setStyles(fullscreenDiv, buttonDefaults);
setStyles(muteUnmuteDiv, Object.assign(buttonDefaults, {
'margin-right': '10px',
}));
// show ad controls
setStyle(countdownWrapperDiv, 'display', 'flex');
showControls();
}

/**
* Reinstate access to all controls when ads have ended.
*
* @visibleForTesting
*/
export function resetControlsAfterAd() {
// hide ad controls
setStyle(countdownWrapperDiv, 'display', 'none');
// set non-ad control styles
setStyles(controlsDiv, {
'justify-content': 'center',
'height': '100px',
'padding': '60px 10px 10px',
});
const buttonDefaults = {'height': '30px'};
setStyles(fullscreenDiv, buttonDefaults);
setStyles(muteUnmuteDiv, Object.assign(buttonDefaults, {
'margin-right': '20px',
}));
// show non-ad controls
const showElement = button => setStyle(button, 'display', 'block');
[playPauseDiv, timeDiv, progressBarWrapperDiv].forEach(showElement);
}

/**
* Show video controls and reset hide controls timeout.
*
Expand All @@ -1166,12 +1288,14 @@ export function showControls() {
}

/**
* Hide video controls.
* Hide video controls, except when ads are active.
*
* @visibleForTesting
*/
export function hideControls() {
setStyle(controlsDiv, 'display', 'none');
if (!adsActive) {
setStyle(controlsDiv, 'display', 'none');
}
}

/**
Expand Down Expand Up @@ -1233,7 +1357,7 @@ function onMessage(global, event) {
'width': px(msg.args.width),
'height': px(msg.args.height),
});
if (adsActive) {
if (adsActive && !fullscreen) {
adsManager.resize(
msg.args.width, msg.args.height,
global.google.ima.ViewMode.NORMAL);
Expand Down
29 changes: 26 additions & 3 deletions examples/ima-video.amp.html
Expand Up @@ -36,12 +36,13 @@ <h3>Resources</h3>
</ul>
<h2>Video1</h2>
<div class="description">
Uses the Pre roll + Bumper IMA Sample Tag.
Uses the Pre roll + Bumper IMA Sample Tag. Also, with custom ad label in Spanish
</div>
<div id="player-wrapper1" class="player-wrapper">
<amp-ima-video id="Video1"
width="640" height="360" layout="responsive"
data-tag="https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator="
data-ad-label="Publicidad (%s de %s)"
data-poster="/examples/img/ima-poster.png">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
</amp-ima-video>
Expand Down Expand Up @@ -79,11 +80,10 @@ <h3>Video2 Actions</h3>

<h2>Video3</h2>
<div class="description">
Uses the Pre-, Mid-, and Post Roll IMA Sample Tag. Also, this video with autoplay
Uses the Pre-, Mid-, and Post Roll IMA Sample Tag.
</div>
<div id="player-wrapper3" class="player-wrapper">
<amp-ima-video id="Video3"
autoplay
width="640" height="360" layout="responsive"
data-tag="https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
data-poster="/examples/img/ima-poster.png">
Expand All @@ -96,5 +96,28 @@ <h3>Video3 Actions</h3>
<button on="tap:Video3.mute">Mute</button>
<button on="tap:Video3.unmute">Unmute</button>
<button on="tap:Video3.fullscreen">Fullscreen</button>

<div class="spacer"></div>


<h2>Video4</h2>
<div class="description">
Uses the Single Skippable Inline IMA Sample Tag. Also, this video with autoplay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Example, thank you!

</div>
<div id="player-wrapper4" class="player-wrapper">
<amp-ima-video id="Video4"
autoplay
width="640" height="360" layout="responsive"
data-tag="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="
data-poster="/examples/img/ima-poster.png">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
</amp-ima-video>
</div>
<h3>Video4 Actions</h3>
<button on="tap:Video4.play">Play</button>
<button on="tap:Video4.pause">Pause</button>
<button on="tap:Video4.mute">Mute</button>
<button on="tap:Video4.unmute">Unmute</button>
<button on="tap:Video4.fullscreen">Fullscreen</button>
</body>
</html>