diff --git a/ads/google/ima/ima-video.js b/ads/google/ima/ima-video.js
index 69e7ba0089df..d71be652e1b5 100644
--- a/ads/google/ima/ima-video.js
+++ b/ads/google/ima/ima-video.js
@@ -16,17 +16,15 @@
import {CONSENT_POLICY_STATE} from '../../../src/core/constants/consent-state';
import {ImaPlayerData} from './ima-player-data';
-import {
- camelCaseToTitleCase,
- px,
- setStyle,
- setStyles,
-} from '../../../src/style';
+import {camelCaseToTitleCase, setStyle, toggle} from '../../../src/style';
import {getData} from '../../../src/event-helper';
+import {htmlFor, htmlRefs, svgFor} from '../../../src/static-template';
import {isArray, isObject} from '../../../src/core/types';
import {loadScript} from '../../../3p/3p';
import {throttle} from '../../../src/core/types/function';
import {tryParseJson} from '../../../src/json';
+// Source for this constant is css/amp-ima-video-iframe.css
+import {cssText} from '../../../build/amp-ima-video-iframe.css';
/**
* Possible player states.
@@ -43,74 +41,45 @@ const PlayerStates = {
* https://material.io/tools/icons
*/
const icons = {
- 'play': `
- `,
- 'pause': `
- `,
- 'fullscreen': `
- `,
- 'mute': `
- `,
- 'volume_max': `
- `,
- 'seek': ``,
+ play: (svg) => svg`
+
+ `,
+ pause: (svg) => svg`
+
+ `,
+ fullscreen: (svg) => svg`
+
+ `,
+ muted: (svg) => svg`
+
+ `,
+ volumeMax: (svg) => svg`
+
+ `,
};
-const bigPlayDivDisplayStyle = 'table-cell';
-
-// Div wrapping our entire DOM.
-let wrapperDiv;
-
-// Div containing big play button. Rendered before player starts.
-let bigPlayDiv;
-
-// Div contianing play button. Double-nested for alignment.
-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;
-
-// Div containing player time.
-let timeDiv;
-
-// Node containing the player time text.
-let timeNode;
-
-// Wrapper for progress bar DOM elements.
-let progressBarWrapperDiv;
-
-// Line for progress bar.
-let progressLine;
-
-// Line for total time in progress bar.
-let totalTimeLine;
-
-// Div containing the marker for the progress.
-let progressMarkerDiv;
-
-// Div for fullscreen icon.
-let fullscreenDiv;
-
-// Div for mute/unmute icon.
-let muteUnmuteDiv;
-
-// Div for ad container.
-let adContainerDiv;
-
-// Div for content player.
-let contentDiv;
-
-// Content player.
-let videoPlayer;
+// References to rendered elements. See renderElements().
+let elements;
// Event indicating user interaction.
let interactEvent;
@@ -217,7 +186,7 @@ const playerData = new ImaPlayerData();
// Flag used to track if ads have been requested or not.
let adsRequested;
-// Flag that tracks if the user tapped and dragged on the big play button.
+// Flag that tracks if the user tapped and dragged on the overlay button.
let userTappedAndDragged;
// User consent state.
@@ -226,21 +195,127 @@ let consentState;
// Throttle for the showControls() function
let showControlsThrottled = throttle(window, showControls, 1000);
+/**
+ * @param {!Node} parent
+ * @param {string} css
+ */
+function insertCss(parent, css) {
+ const style = parent.ownerDocument.createElement('style');
+ style./*OK*/ textContent = css;
+ parent.appendChild(style);
+}
+
+/**
+ * @param {string} state
+ * @param {boolean} active
+ */
+function toggleRootDataAttribute(state, active) {
+ const {'root': root} = elements;
+ const attributeName = `data-${state}`;
+ if (active) {
+ root.setAttribute(attributeName, '');
+ } else {
+ root.removeAttribute(attributeName);
+ }
+}
+
+/**
+ * @param {Document} elementOrDoc
+ * @return {!Object}
+ */
+function renderElements(elementOrDoc) {
+ const html = htmlFor(elementOrDoc);
+
+ // Elements annotated with `ref="name"` are referenced as `elements['name']`.
+ // They also have their `ref` copied as a classname, so they can be selected
+ // from CSS using that exact value.
+ const root = html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -:- / 0:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const elements = htmlRefs(root);
+
+ // The root element cannot be referenced by `ref="root"`, so we insert it.
+ elements['root'] = root;
+
+ // For a smaller template, we copy each element's `ref` value as a classname.
+ for (const ref in elements) {
+ elements[ref].classList.add(ref);
+ }
+
+ // Adding SVGs separately because they require a different namespace, and the
+ // HTML template must be static.
+ const svg = svgFor(elementOrDoc);
+
+ elements['overlayButton'].appendChild(icons.play(svg));
+ elements['fullscreenButton'].appendChild(icons.fullscreen(svg));
+
+ // Buttons toggle SVGs by including two each, one is displayed at a time.
+ // See CSS selectors for buttons under .root[data-*].
+ const {'playButton': playButton, 'muteButton': muteButton} = elements;
+
+ playButton.appendChild(icons.play(svg));
+ playButton.appendChild(icons.pause(svg));
+
+ muteButton.appendChild(icons.volumeMax(svg));
+ muteButton.appendChild(icons.muted(svg));
+
+ return elements;
+}
+
/**
* @param {!Document} document
+ * @param {!Element} parent
* @param {?Array>} childrenDef
* an array of [tagName, attributes] items like:
* [
* ['SOURCE', {'src': 'foo.mp4'}],
* ['TRACK', {'src': 'bar.mp4'}],
* ]
- * @return {?Node} Optional DocumentFragment containing created children
*/
-function maybeCreateChildren(document, childrenDef) {
+function maybeAppendChildren(document, parent, childrenDef) {
if (!isArray(childrenDef)) {
- return null;
+ return;
}
- const fragment = document.createDocumentFragment();
childrenDef.forEach((child) => {
const tagName = child[0];
const attributes = child[1];
@@ -257,8 +332,8 @@ function maybeCreateChildren(document, childrenDef) {
for (const attr in attributes) {
element.setAttribute(attr, attributes[attr]);
}
+ parent.appendChild(element);
});
- return fragment;
}
/**
@@ -266,236 +341,40 @@ function maybeCreateChildren(document, childrenDef) {
* @param {!Object} data
*/
export function imaVideo(global, data) {
+ insertCss(global.document.head, cssText);
+
videoWidth = global./*OK*/ innerWidth;
videoHeight = global./*OK*/ innerHeight;
adLabel = data.adLabel || 'Ad (%s of %s)';
- // Wraps *everything*.
- wrapperDiv = global.document.createElement('div');
- wrapperDiv.id = 'ima-wrapper';
- setStyle(wrapperDiv, 'width', px(videoWidth));
- setStyle(wrapperDiv, 'height', px(videoHeight));
- setStyle(wrapperDiv, 'background-color', 'black');
-
- // Wraps the big play button we show before video start.
- bigPlayDiv = global.document.createElement('div');
- bigPlayDiv.id = 'ima-big-play';
- setStyles(bigPlayDiv, {
- 'position': 'relative',
- 'width': px(videoWidth),
- 'height': px(videoHeight),
- 'display': bigPlayDivDisplayStyle,
- 'vertical-align': 'middle',
- 'text-align': 'center',
- 'cursor': 'pointer',
- });
- // Inner div so we can v and h align.
- playButtonDiv = createIcon(global, 'play');
- playButtonDiv.id = 'ima-play-button';
- setStyles(playButtonDiv, {
- 'display': 'inline-block',
- 'max-width': '120px',
- 'max-height': '120px',
- });
- bigPlayDiv.appendChild(playButtonDiv);
-
- // Video controls.
- controlsDiv = global.document.createElement('div');
- controlsDiv.id = 'ima-controls';
- setStyles(controlsDiv, {
- 'position': 'absolute',
- '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%)',
- 'box-sizing': 'border-box',
- 'padding': '10px',
- 'padding-top': '60px',
- 'color': 'white',
- 'display': 'none',
- 'font-family': 'Helvetica, Arial, Sans-serif',
- 'justify-content': 'center',
- 'align-items': 'center',
- 'user-select': 'none',
- 'z-index': '1',
- });
- controlsVisible = false;
+ elements = renderElements(global.document);
- // Play button
- playPauseDiv = createIcon(global, 'play');
- playPauseDiv.id = 'ima-play-pause';
- setStyles(playPauseDiv, {
- 'width': '30px',
- 'height': '30px',
- 'margin-right': '20px',
- 'font-size': '1.25em',
- 'cursor': 'pointer',
- });
- controlsDiv.appendChild(playPauseDiv);
- // 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);
- // Current time and duration.
- timeDiv = global.document.createElement('div');
- timeDiv.id = 'ima-time';
- setStyles(timeDiv, {
- 'margin-right': '20px',
- 'text-align': 'center',
- 'font-size': '14px',
- 'text-shadow': '0px 0px 10px black',
- });
- timeNode = global.document.createTextNode('-:- / 0:00');
- timeDiv.appendChild(timeNode);
- controlsDiv.appendChild(timeDiv);
- // Progress bar.
- progressBarWrapperDiv = global.document.createElement('div');
- progressBarWrapperDiv.id = 'ima-progress-wrapper';
- setStyles(progressBarWrapperDiv, {
- 'height': '30px',
- 'flex-grow': '1',
- 'position': 'relative',
- 'margin-right': '20px',
- });
- progressLine = global.document.createElement('div');
- progressLine.id = 'progress-line';
- setStyles(progressLine, {
- 'background-color': 'rgb(255, 255, 255)',
- 'height': '2px',
- 'margin-top': '14px',
- 'width': '0%',
- 'float': 'left',
- });
- totalTimeLine = global.document.createElement('div');
- totalTimeLine.id = 'total-time-line';
- setStyles(totalTimeLine, {
- 'background-color': 'rgba(255, 255, 255, 0.45)',
- 'height': '2px',
- 'width': '100%',
- 'margin-top': '14px',
- });
- progressMarkerDiv = global.document.createElement('div');
- progressMarkerDiv.id = 'ima-progress-marker';
- setStyles(progressMarkerDiv, {
- 'height': '14px',
- 'width': '14px',
- 'position': 'absolute',
- 'left': '0%',
- 'top': '50%',
- 'margin-top': '-7px',
- 'cursor': 'pointer',
- });
- progressMarkerDiv.appendChild(createIcon(global, 'seek'));
- progressBarWrapperDiv.appendChild(progressLine);
- progressBarWrapperDiv.appendChild(progressMarkerDiv);
- progressBarWrapperDiv.appendChild(totalTimeLine);
- controlsDiv.appendChild(progressBarWrapperDiv);
-
- // Mute/Unmute button
- muteUnmuteDiv = createIcon(global, 'volume_max');
- muteUnmuteDiv.id = 'ima-mute-unmute';
- setStyles(muteUnmuteDiv, {
- 'width': '30px',
- 'height': '30px',
- 'flex-shrink': '0',
- 'margin-right': '20px',
- 'font-size': '1.25em',
- 'cursor': 'pointer',
- });
- controlsDiv.appendChild(muteUnmuteDiv);
-
- // Fullscreen button
- fullscreenDiv = createIcon(global, 'fullscreen');
- fullscreenDiv.id = 'ima-fullscreen';
- setStyles(fullscreenDiv, {
- 'width': '30px',
- 'height': '30px',
- 'flex-shrink': '0',
- 'font-size': '1.25em',
- 'cursor': 'pointer',
- 'text-align': 'center',
- 'font-weight': 'bold',
- 'line-height': '1.4em',
- });
- controlsDiv.appendChild(fullscreenDiv);
-
- // Ad container.
- adContainerDiv = global.document.createElement('div');
- adContainerDiv.id = 'ima-ad-container';
- setStyles(adContainerDiv, {
- 'position': 'absolute',
- 'top': '0px',
- 'left': '0px',
- 'width': '100%',
- 'height': '100%',
- });
+ controlsVisible = false;
- // Wraps our content video.
- contentDiv = global.document.createElement('div');
- contentDiv.id = 'ima-content';
- setStyles(contentDiv, {
- 'position': 'absolute',
- 'top': '0px',
- 'left': '0px',
- 'width': '100%',
- 'height': '100%',
- });
- // The video player
- videoPlayer = global.document.createElement('video');
- videoPlayer.id = 'ima-content-player';
- setStyles(videoPlayer, {
- 'width': '100%',
- 'height': '100%',
- 'background-color': 'black',
- });
- videoPlayer.setAttribute('poster', data.poster);
+ // Propagate settings and video element's children.
+ const {'video': video} = elements;
+ video.setAttribute('poster', data.poster);
if (data['crossorigin'] != null) {
- videoPlayer.setAttribute('crossorigin', data['crossorigin']);
+ video.setAttribute('crossorigin', data['crossorigin']);
}
- videoPlayer.setAttribute('playsinline', true);
- videoPlayer.setAttribute(
- 'controlsList',
- 'nodownload nofullscreen noremoteplayback'
- );
if (data.src) {
const sourceElement = document.createElement('source');
sourceElement.setAttribute('src', data.src);
- videoPlayer.appendChild(sourceElement);
+ video.appendChild(sourceElement);
}
- const childrenFragment = maybeCreateChildren(
+ maybeAppendChildren(
global.document,
- data['_context']?.['sourceChildren']
+ video,
+ tryParseJson(data['sourceChildren'])
);
- if (childrenFragment) {
- videoPlayer.appendChild(childrenFragment);
- }
if (data.imaSettings) {
imaSettings = tryParseJson(data.imaSettings);
}
- contentDiv.appendChild(videoPlayer);
- wrapperDiv.appendChild(contentDiv);
- wrapperDiv.appendChild(adContainerDiv);
- wrapperDiv.appendChild(controlsDiv);
- wrapperDiv.appendChild(bigPlayDiv);
- global.document.getElementById('c').appendChild(wrapperDiv);
+ global.document.getElementById('c').appendChild(elements['root']);
+
+ // Attach events and configure IMA SDK.
window.addEventListener('message', onMessage.bind(null, global));
@@ -508,6 +387,13 @@ export function imaVideo(global, data) {
nativeFullscreen = false;
imaLoadAllowed = true;
+ const {
+ 'playButton': playButton,
+ 'progress': progress,
+ 'muteButton': muteButton,
+ 'fullscreenButton': fullscreenButton,
+ } = elements;
+
let mobileBrowser = false;
interactEvent = 'click';
mouseDownEvent = 'mousedown';
@@ -524,24 +410,25 @@ export function imaVideo(global, data) {
mouseMoveEvent = 'touchmove';
mouseUpEvent = 'touchend';
}
+ const {'overlayButton': overlayButton} = elements;
if (mobileBrowser) {
// Create our own tap listener that ignores tap and drag.
- bigPlayDiv.addEventListener(mouseMoveEvent, onBigPlayTouchMove);
- bigPlayDiv.addEventListener(mouseUpEvent, onBigPlayTouchEnd);
- bigPlayDiv.addEventListener(
+ overlayButton.addEventListener(mouseMoveEvent, onOverlayButtonTouchMove);
+ overlayButton.addEventListener(mouseUpEvent, onOverlayButtonTouchEnd);
+ overlayButton.addEventListener(
'tapwithoutdrag',
- onBigPlayClick.bind(null, global)
+ onOverlayButtonInteract.bind(null, global)
);
} else {
- bigPlayDiv.addEventListener(
+ overlayButton.addEventListener(
interactEvent,
- onBigPlayClick.bind(null, global)
+ onOverlayButtonInteract.bind(null, global)
);
}
- playPauseDiv.addEventListener(interactEvent, onPlayPauseClick);
- progressBarWrapperDiv.addEventListener(mouseDownEvent, onProgressClick);
- muteUnmuteDiv.addEventListener(interactEvent, onMuteUnmuteClick);
- fullscreenDiv.addEventListener(
+ playButton.addEventListener(interactEvent, onPlayPauseClick);
+ progress.addEventListener(mouseDownEvent, onProgressClick);
+ muteButton.addEventListener(interactEvent, onMuteUnmuteClick);
+ fullscreenButton.addEventListener(
interactEvent,
toggleFullscreen.bind(null, global)
);
@@ -627,9 +514,11 @@ function onImaLoadSuccess(global, data) {
}
}
+ const {'adContainer': adContainer, 'video': video} = elements;
+
adDisplayContainer = new global.google.ima.AdDisplayContainer(
- adContainerDiv,
- videoPlayer
+ adContainer,
+ video
);
adsLoader = new global.google.ima.AdsLoader(adDisplayContainer);
@@ -666,7 +555,7 @@ function onImaLoadSuccess(global, data) {
false
);
- videoPlayer.addEventListener('ended', onContentEnded);
+ video.addEventListener('ended', onContentEnded);
adsRequest = new global.google.ima.AdsRequest();
adsRequest.adTagUrl = data.tag;
@@ -690,7 +579,7 @@ function onImaLoadFail() {
// Something blocked ima3.js from loading - ignore all IMA stuff and just play
// content.
addHoverEventToElement(
- /** @type {!Element} */ (videoPlayer),
+ /** @type {!Element} */ (elements['video']),
showControlsThrottled
);
imaLoadAllowed = false;
@@ -698,41 +587,12 @@ function onImaLoadFail() {
}
/**
- * @param {!Object} global
- * @param {string} name
- * @param {string} [fill='#FFFFFF']
- * @return {!Element}
- */
-function createIcon(global, name, fill = '#FFFFFF') {
- const doc = global.document;
- const icon = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
- icon.setAttributeNS(null, 'fill', fill);
- icon.setAttributeNS(null, 'height', '100%');
- icon.setAttributeNS(null, 'width', '100%');
- icon.setAttributeNS(null, 'viewBox', '0 0 24 24');
- setStyle(icon, 'filter', 'drop-shadow(0px 0px 14px rgba(0,0,0,0.4))');
- icon./*OK*/ innerHTML = icons[name];
- return icon;
-}
-
-/**
- * @param {!Element} element
- * @param {string} name
- * @param {string} [fill='#FFFFFF']
- */
-function changeIcon(element, name, fill = '#FFFFFF') {
- element./*OK*/ innerHTML = icons[name];
- if (fill != element.getAttributeNS(null, 'fill')) {
- element.setAttributeNS(null, 'fill', fill);
- }
-}
-
-/**
- * Triggered when the user clicks on the big play button div.
+ * Triggered when the user clicks on the overlay button.
* @param {!Object} global
* @visibleForTesting
*/
-export function onBigPlayClick(global) {
+export function onOverlayButtonInteract(global) {
+ const {'video': video, 'overlayButton': overlayButton} = elements;
if (playbackStarted) {
// Resart the video
playVideo();
@@ -744,30 +604,34 @@ export function onBigPlayClick(global) {
if (adDisplayContainer) {
adDisplayContainer.initialize();
}
- videoPlayer.load();
+ video.load();
playAds(global);
}
- setStyle(bigPlayDiv, 'display', 'none');
+ toggle(overlayButton, false);
}
+// TODO(alanorozco): Update name on test's end.
+export const onBigPlayClick = onOverlayButtonInteract;
+
/**
- * Triggered when the user ends a tap on the big play button.
+ * Triggered when the user ends a tap on the overlay button.
+ * @param {Event} event
*/
-function onBigPlayTouchEnd() {
+function onOverlayButtonTouchEnd(event) {
if (userTappedAndDragged) {
// Reset state and ignore this tap.
userTappedAndDragged = false;
} else {
const tapWithoutDragEvent = new Event('tapwithoutdrag');
- bigPlayDiv.dispatchEvent(tapWithoutDragEvent);
+ event.currentTarget.dispatchEvent(tapWithoutDragEvent);
}
}
/**
- * Triggered when the user moves a tap on the big play button.
+ * Triggered when the user moves a tap on the overlay button.
*/
-function onBigPlayTouchMove() {
+function onOverlayButtonTouchMove() {
userTappedAndDragged = true;
}
@@ -838,9 +702,9 @@ export function onContentEnded() {
}
// If all ads are not completed,
- // onContentResume will show the bigPlayDiv
+ // onContentResume will show the elements['overlayButton']
if (allAdsCompleted) {
- setStyle(bigPlayDiv, 'display', bigPlayDivDisplayStyle);
+ toggle(elements['overlayButton'], true);
}
postMessage({event: VideoEvents.PAUSE});
@@ -857,7 +721,7 @@ export function onAdsManagerLoaded(global, adsManagerLoadedEvent) {
const adsRenderingSettings = new global.google.ima.AdsRenderingSettings();
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
adsManager = adsManagerLoadedEvent.getAdsManager(
- videoPlayer,
+ elements['video'],
adsRenderingSettings
);
adsManager.addEventListener(
@@ -907,7 +771,7 @@ export function onAdsLoaderError() {
// playback is concerned because our content will be ready to play.
postMessage({event: VideoEvents.LOAD});
addHoverEventToElement(
- /** @type {!Element} */ (videoPlayer),
+ /** @type {!Element} */ (elements['video']),
showControlsThrottled
);
if (playbackStarted) {
@@ -927,7 +791,7 @@ export function onAdError() {
adsManager.destroy();
}
addHoverEventToElement(
- /** @type {!Element} */ (videoPlayer),
+ /** @type {!Element} */ (elements['video']),
showControlsThrottled
);
playVideo();
@@ -958,7 +822,8 @@ export function onAdProgress(unusedEvent) {
remainingSeconds = '0' + remainingSeconds;
}
const label = adLabel.replace('%s', adPosition).replace('%s', totalAds);
- countdownDiv.textContent = `${label}: ${remainingMinutes}:${remainingSeconds}`;
+ const {'countdown': countdown} = elements;
+ countdown.textContent = `${label}: ${remainingMinutes}:${remainingSeconds}`;
}
/**
@@ -978,14 +843,16 @@ export function onContentPauseRequested(global) {
}
adsActive = true;
postMessage({event: VideoEvents.AD_START});
+ toggle(elements['adContainer'], true);
+ showAdControls();
+
+ const {'video': video} = elements;
+ video.removeEventListener('ended', onContentEnded);
+ video.pause();
removeHoverEventFromElement(
- /** @type {!Element} */ (videoPlayer),
+ /** @type {!Element} */ (video),
showControlsThrottled
);
- setStyle(adContainerDiv, 'display', 'block');
- videoPlayer.removeEventListener('ended', onContentEnded);
- showAdControls();
- videoPlayer.pause();
}
/**
@@ -994,9 +861,10 @@ export function onContentPauseRequested(global) {
* @visibleForTesting
*/
export function onContentResumeRequested() {
+ const {'video': video, 'overlayButton': overlayButton} = elements;
adsActive = false;
addHoverEventToElement(
- /** @type {!Element} */ (videoPlayer),
+ /** @type {!Element} */ (video),
showControlsThrottled
);
postMessage({event: VideoEvents.AD_END});
@@ -1006,10 +874,10 @@ export function onContentResumeRequested() {
// resume content in that case.
playVideo();
} else {
- setStyle(bigPlayDiv, 'display', bigPlayDivDisplayStyle);
+ toggle(overlayButton, true);
}
- videoPlayer.addEventListener('ended', onContentEnded);
+ video.addEventListener('ended', onContentEnded);
}
/**
@@ -1020,8 +888,7 @@ export function onContentResumeRequested() {
* @visibleForTesting
*/
export function onAdPaused() {
- // show play button while ad is paused
- changeIcon(playPauseDiv, 'play');
+ toggleRootDataAttribute('playing', false);
}
/**
@@ -1032,8 +899,7 @@ export function onAdPaused() {
* @visibleForTesting
*/
export function onAdResumed() {
- // show pause button when ad resumes
- changeIcon(playPauseDiv, 'pause');
+ toggleRootDataAttribute('playing', true);
}
/**
@@ -1050,7 +916,8 @@ export function onAllAdsCompleted() {
* Called when our ui timer goes off. Updates the player UI.
*/
function uiTickerClick() {
- updateUi(videoPlayer.currentTime, videoPlayer.duration);
+ const {currentTime, duration} = elements['video'];
+ updateTime(currentTime, duration);
}
/**
@@ -1061,8 +928,9 @@ function playerDataTick() {
// Skip while ads are active in case of custom playback. No harm done for
// non-custom playback because content won't be progressing while ads are
// playing.
- if (videoPlayer && !adsActive) {
- playerData.update(videoPlayer);
+ const {'video': video} = elements;
+ if (video && !adsActive) {
+ playerData.update(video);
postMessage({
event: ImaPlayerData.IMA_PLAYER_DATA,
data: playerData,
@@ -1071,16 +939,21 @@ function playerDataTick() {
}
/**
- * Updates the video player UI.
+ * Updates the time and progress.
* @param {number} currentTime
* @param {number} duration
* @visibleForTesting
*/
-export function updateUi(currentTime, duration) {
- timeNode.textContent = formatTime(currentTime) + ' / ' + formatTime(duration);
+export function updateTime(currentTime, duration) {
+ const {
+ 'time': time,
+ 'progressLine': progressLine,
+ 'progressMarker': progressMarker,
+ } = elements;
+ time.textContent = formatTime(currentTime) + ' / ' + formatTime(duration);
const progressPercent = Math.floor((currentTime / duration) * 100);
setStyle(progressLine, 'width', progressPercent + '%');
- setStyle(progressMarkerDiv, 'left', progressPercent - 1 + '%');
+ setStyle(progressMarker, 'left', progressPercent - 1 + '%');
}
/**
@@ -1144,7 +1017,8 @@ function onProgressClickEnd() {
document.removeEventListener(mouseMoveEvent, onProgressMove);
document.removeEventListener(mouseUpEvent, onProgressClickEnd);
uiTicker = setInterval(uiTickerClick, 500);
- videoPlayer.currentTime = videoPlayer.duration * seekPercent;
+ const {'video': video} = elements;
+ video.currentTime = video.duration * seekPercent;
// Reset hide controls timeout.
showControls();
}
@@ -1154,9 +1028,10 @@ function onProgressClickEnd() {
* @param {!Event} event
*/
function onProgressMove(event) {
- const progressWrapperPosition = getPagePosition(progressBarWrapperDiv);
+ const {'video': video, 'progress': progress} = elements;
+ const progressWrapperPosition = getPagePosition(progress);
const progressListStart = progressWrapperPosition.x;
- const progressListWidth = progressBarWrapperDiv./*OK*/ offsetWidth;
+ const progressListWidth = progress./*OK*/ offsetWidth;
// Handle Android Chrome touch events.
const eventX = event.clientX || event.touches[0].pageX;
@@ -1167,7 +1042,7 @@ function onProgressMove(event) {
} else if (seekPercent > 1) {
seekPercent = 1;
}
- updateUi(videoPlayer.duration * seekPercent, videoPlayer.duration);
+ updateTime(video.duration * seekPercent, video.duration);
}
/**
@@ -1206,17 +1081,18 @@ export function onPlayPauseClick() {
* @visibleForTesting
*/
export function playVideo() {
+ const {'adContainer': adContainer, 'video': video} = elements;
if (adsActive) {
adsManager.resume();
} else {
- setStyle(adContainerDiv, 'display', 'none');
+ toggle(adContainer, false);
// Kick off the hide controls timer.
showControls();
- videoPlayer.play();
+ video.play();
}
playerState = PlayerStates.PLAYING;
postMessage({event: VideoEvents.PLAYING});
- changeIcon(playPauseDiv, 'pause');
+ toggleRootDataAttribute('playing', true);
}
/**
@@ -1228,26 +1104,27 @@ export function pauseVideo(event = null) {
if (adsActive) {
adsManager.pause();
} else {
- videoPlayer.pause();
+ const {'video': video} = elements;
+ video.pause();
// Show controls and keep them there because we're paused.
clearTimeout(hideControlsTimeout);
showControls();
if (event && event.type == 'webkitendfullscreen') {
// Video was paused because we exited fullscreen.
- videoPlayer.removeEventListener('webkitendfullscreen', pauseVideo);
+ video.removeEventListener('webkitendfullscreen', pauseVideo);
fullscreen = false;
}
}
playerState = PlayerStates.PAUSED;
postMessage({event: VideoEvents.PAUSE});
- changeIcon(playPauseDiv, 'play');
+ toggleRootDataAttribute('playing', false);
}
/**
* Handler when the mute/unmute button is clicked
*/
export function onMuteUnmuteClick() {
- if (videoPlayer.muted) {
+ if (elements['video'].muted) {
unmuteVideo();
} else {
muteVideo();
@@ -1258,34 +1135,35 @@ export function onMuteUnmuteClick() {
* Function to mute the video
*/
export function muteVideo() {
- if (!videoPlayer.muted) {
- videoPlayer.volume = 0;
- videoPlayer.muted = true;
- if (adsManager) {
- adsManager.setVolume(0);
- } else {
- muteAdsManagerOnLoaded = true;
- }
- changeIcon(muteUnmuteDiv, 'mute');
- postMessage({event: VideoEvents.MUTED});
- }
+ toggleMuted(elements['video'], true);
}
/**
* Function to unmute the video
*/
export function unmuteVideo() {
- if (videoPlayer.muted) {
- videoPlayer.volume = 1;
- videoPlayer.muted = false;
- if (adsManager) {
- adsManager.setVolume(1);
- } else {
- muteAdsManagerOnLoaded = false;
- }
- changeIcon(muteUnmuteDiv, 'volume_max');
- postMessage({event: VideoEvents.UNMUTED});
+ toggleMuted(elements['video'], false);
+}
+
+/**
+ * Mutes or unmutes the video.
+ * @param {!HTMLMediaElement} video
+ * @param {boolean} muted
+ */
+export function toggleMuted(video, muted) {
+ if (video.muted == muted) {
+ return;
}
+ const volume = muted ? 0 : 1;
+ video.volume = volume;
+ video.muted = muted;
+ if (adsManager) {
+ adsManager.setVolume(volume);
+ } else {
+ muteAdsManagerOnLoaded = muted;
+ }
+ toggleRootDataAttribute('muted', muted);
+ postMessage({event: muted ? VideoEvents.MUTED : VideoEvents.UNMUTED});
}
/**
@@ -1320,12 +1198,13 @@ function enterFullscreen(global) {
fullscreenHeight = window.screen.height;
requestFullscreen.call(global.document.documentElement);
} else {
+ const {'video': video} = elements;
// Use native fullscreen (iPhone)
- videoPlayer.webkitEnterFullscreen();
+ video.webkitEnterFullscreen();
// Pause the video when we leave fullscreen. iPhone does this
// automatically, but we still use pauseVideo as an event handler to
// sync the UI.
- videoPlayer.addEventListener('webkitendfullscreen', pauseVideo);
+ video.addEventListener('webkitendfullscreen', pauseVideo);
nativeFullscreen = true;
onFullscreenChange(global);
}
@@ -1358,9 +1237,6 @@ function onFullscreenChange(global) {
adsManagerWidthOnLoad = null;
adsManagerHeightOnLoad = null;
}
- // Return the video to its original size and position
- setStyle(wrapperDiv, 'width', px(videoWidth));
- setStyle(wrapperDiv, 'height', px(videoHeight));
fullscreen = false;
} else {
// The user just entered fullscreen
@@ -1375,9 +1251,6 @@ function onFullscreenChange(global) {
adsManagerWidthOnLoad = null;
adsManagerHeightOnLoad = null;
}
- // Make the video take up the entire screen
- setStyle(wrapperDiv, 'width', px(fullscreenWidth));
- setStyle(wrapperDiv, 'height', px(fullscreenHeight));
hideControls();
}
fullscreen = true;
@@ -1387,33 +1260,18 @@ function onFullscreenChange(global) {
/**
* Show a subset of controls when ads are playing.
- * Visible controls are countdownDiv, playPauseDiv, muteUnmuteDiv, and fullscreenDiv
+ * See CSS for selectors affected by -controls-ads and -controls-ads-mini
*
* @visibleForTesting
*/
export function showAdControls() {
- const hasMobileStyles = videoWidth <= 400;
- const isSkippable = currentAd ? currentAd.getSkipTimeOffset() !== -1 : false;
- const miniControls = hasMobileStyles && isSkippable;
- // hide non-ad controls
- [timeDiv, progressBarWrapperDiv].forEach((button) => {
- setStyle(button, 'display', 'none');
- });
- // set ad control styles
- setStyles(controlsDiv, {
- 'height': miniControls ? '20px' : '30px',
- 'justify-content': 'flex-end',
- 'padding': '10px',
- });
- [fullscreenDiv, playPauseDiv, muteUnmuteDiv].forEach((button) => {
- setStyles(button, {'height': miniControls ? '18px' : '22px'});
- });
- setStyles(muteUnmuteDiv, {'margin-right': '10px'});
- // show pause button while ad begins playing
- changeIcon(playPauseDiv, 'pause');
- // show ad controls
- setStyle(countdownWrapperDiv, 'display', 'flex');
showControls(true);
+ toggleRootDataAttribute('playing', true);
+ toggleRootDataAttribute('ad', true);
+ toggleRootDataAttribute(
+ 'skippable',
+ currentAd ? currentAd?.getSkipTimeOffset() !== -1 : false
+ );
}
/**
@@ -1422,22 +1280,8 @@ export function showAdControls() {
* @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',
- });
- [fullscreenDiv, playPauseDiv, muteUnmuteDiv].forEach((button) => {
- setStyles(button, {'height': '30px'});
- });
- setStyles(muteUnmuteDiv, {'margin-right': '20px'});
- // show non-ad controls
- [timeDiv, progressBarWrapperDiv].forEach((button) => {
- setStyle(button, 'display', 'block');
- });
+ toggleRootDataAttribute('ad', false);
+ toggleRootDataAttribute('skippable', false);
}
/**
@@ -1454,7 +1298,7 @@ export function showControls(opt_adsForce) {
hideControlsQueued = false;
return;
}
- setStyle(controlsDiv, 'display', 'flex');
+ toggle(elements['controls'], true);
controlsVisible = true;
}
@@ -1474,7 +1318,7 @@ export function showControls(opt_adsForce) {
*/
export function hideControls() {
if (controlsVisible && !adsActive) {
- setStyle(controlsDiv, 'display', 'none');
+ toggle(elements['controls'], false);
controlsVisible = false;
} else if (!showControlsFirstCalled) {
// showControls has not been called yet,
@@ -1507,7 +1351,7 @@ function onMessage(global, event) {
playVideo();
} else {
// Auto-play support
- onBigPlayClick(global);
+ onOverlayButtonInteract(global);
}
break;
case 'pause':
@@ -1532,14 +1376,6 @@ function onMessage(global, event) {
case 'resize':
const args = msg['args'];
if (args && args.width && args.height) {
- setStyles(wrapperDiv, {
- 'width': px(args.width),
- 'height': px(args.height),
- });
- setStyles(bigPlayDiv, {
- 'width': px(args.width),
- 'height': px(args.height),
- });
if (adsActive && !fullscreen) {
adsManager.resize(
args.width,
@@ -1588,16 +1424,13 @@ function postMessage(data) {
*/
export function getPropertiesForTesting() {
return {
- adContainerDiv,
allAdsCompleted,
adRequestFailed,
adsActive,
adsManagerWidthOnLoad,
adsManagerHeightOnLoad,
adsRequest,
- bigPlayDiv,
contentComplete,
- controlsDiv,
controlsVisible,
hideControlsTimeout,
imaLoadAllowed,
@@ -1605,14 +1438,23 @@ export function getPropertiesForTesting() {
playbackStarted,
playerState,
PlayerStates,
- playPauseDiv,
- progressLine,
- progressMarkerDiv,
- timeNode,
uiTicker,
- videoPlayer,
hideControlsQueued,
icons,
+ // TODO(alanorozco): Update names on test's end to pass `elements` instead.
+ elements,
+ videoPlayer: elements['video'],
+ adContainerDiv: elements['adContainer'],
+ controlsDiv: elements['controls'],
+ playPauseDiv: elements['playButton'],
+ countdownDiv: elements['countdown'],
+ timeDiv: elements['time'],
+ progressBarWrapper: elements['progress'],
+ progressLine: elements['progressLine'],
+ progressMarkerDiv: elements['progressMarker'],
+ muteUnmuteDiv: elements['muteButton'],
+ fullscreenDiv: elements['fullscreenButton'],
+ bigPlayDiv: elements['overlayButton'],
};
}
@@ -1626,12 +1468,12 @@ export function getShowControlsThrottledForTesting() {
}
/**
- * Sets the big play button div.
+ * Sets the overlay button.
* @param {!Element} div
* @visibleForTesting
*/
export function setBigPlayDivForTesting(div) {
- bigPlayDiv = div;
+ elements['overlayButton'] = div;
}
/**
@@ -1660,7 +1502,7 @@ export function setVideoWidthAndHeightForTesting(width, height) {
* @visibleForTesting
*/
export function setVideoPlayerMutedForTesting(shouldMute) {
- videoPlayer.muted = shouldMute;
+ elements['video'].muted = shouldMute;
}
/**
@@ -1743,7 +1585,7 @@ export function setContentCompleteForTesting(newContentComplete) {
* @visibleForTesting
*/
export function setVideoPlayerForTesting(newPlayer) {
- videoPlayer = newPlayer;
+ elements['video'] = newPlayer;
}
/**
diff --git a/build-system/tasks/css/index.js b/build-system/tasks/css/index.js
index 61e7ee77976a..c2153cfe394d 100644
--- a/build-system/tasks/css/index.js
+++ b/build-system/tasks/css/index.js
@@ -74,6 +74,12 @@ const cssEntryPoints = [
outCss: 'amp-story-player-iframe-v0.css',
append: false,
},
+ {
+ path: 'amp-ima-video-iframe.css',
+ outJs: 'amp-ima-video-iframe.css.js',
+ outCss: 'amp-ima-video-iframe-v0.css',
+ append: false,
+ },
];
/**
diff --git a/build-system/test-configs/dep-check-config.js b/build-system/test-configs/dep-check-config.js
index 473f7d4a0d55..ad6ba044a9bb 100644
--- a/build-system/test-configs/dep-check-config.js
+++ b/build-system/test-configs/dep-check-config.js
@@ -129,6 +129,7 @@ exports.rules = [
'ads/**->src/mode.js',
'ads/**->src/url.js',
'ads/**->src/core/types/array.js',
+ 'ads/**->src/static-template.js',
'ads/**->src/style.js',
'ads/**->src/core/constants/consent-state.js',
'ads/**->src/internal-version.js',
diff --git a/css/Z_INDEX.md b/css/Z_INDEX.md
index 62761aa22512..c26b13c4d077 100644
--- a/css/Z_INDEX.md
+++ b/css/Z_INDEX.md
@@ -101,6 +101,7 @@
`.i-amphtml-story-hint-container` | 2 | [extensions/amp-story/1.0/amp-story-hint.css](/extensions/amp-story/1.0/amp-story-hint.css)
`.i-amphtml-story-bookend-active amp-story-page[active]::after` | 2 | [extensions/amp-story/1.0/amp-story.css](/extensions/amp-story/1.0/amp-story.css)
`amp-story-grid-layer` | 2 | [extensions/amp-story/1.0/amp-story.css](/extensions/amp-story/1.0/amp-story.css)
+`.controls` | 1 | [css/amp-ima-video-iframe.css](/css/amp-ima-video-iframe.css)
`.amp-story-player-exit-control-button` | 1 | [css/amp-story-player-iframe.css](/css/amp-story-player-iframe.css)
`.i-amphtml-layout-size-defined > [fallback]` | 1 | [css/ampshared.css](/css/ampshared.css)
`.i-amphtml-layout-size-defined > [placeholder]` | 1 | [css/ampshared.css](/css/ampshared.css)
diff --git a/css/amp-ima-video-iframe.css b/css/amp-ima-video-iframe.css
new file mode 100644
index 000000000000..e1d7920102de
--- /dev/null
+++ b/css/amp-ima-video-iframe.css
@@ -0,0 +1,211 @@
+/**
+ * Copyright 2021 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Styles inserted in iframes rendered by .
+ *
+ * .selectorsInThisFile reject naming convention by using camelCase rather than
+ * dash-case, because they're uniform with Javascript references that are more
+ * useful when destructured as such (imaVideo.js).
+ *
+ * We insert this CSS in a standalone document whose namespace is shared only
+ * with the IMA SDK. This unconventional naming is not worth converting to
+ * dash-case during build or runtime, so we keep it.
+ */
+
+[hidden] {
+ display: none !important;
+}
+
+body,
+.video {
+ background: black;
+}
+
+.video {
+ width: 100%;
+ height: 100%;
+}
+
+.fill {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+button {
+ cursor: pointer;
+ appearance: none;
+ padding: 0;
+ border: none;
+ background: transparent;
+ display: block;
+}
+
+.controls {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ background-color: rgba(7, 20, 30, 0.7);
+ background: linear-gradient(
+ 0,
+ rgba(7, 20, 30, 0.7) 0%,
+ rgba(7, 20, 30, 0) 100%
+ );
+ box-sizing: border-box;
+ padding: 10px;
+ padding-top: 60px;
+ color: white;
+ display: flex;
+ font-family: Helvetica, Arial, Sans-serif;
+ justify-content: center;
+ align-items: center;
+ user-select: none;
+ z-index: 1;
+}
+
+.controls > *:not(:last-child) {
+ margin-right: 20px;
+}
+
+button > svg {
+ width: 100%;
+ height: 100%;
+ filter: drop-shadow(0 0 14px rgba(0, 0, 0, 0.4));
+ fill: #ffffff;
+}
+
+.countdownWrapper {
+ align-items: center;
+ box-sizing: border-box;
+ display: none;
+ flex-grow: 1;
+ font-size: 12px;
+ height: 20px;
+ overflow: hidden;
+ padding: 5px;
+ text-shadow: 0 0 10px black;
+ white-space: nowrap;
+}
+
+.time {
+ margin-right: 20px;
+ text-align: center;
+ font-size: 14px;
+ text-shadow: 0 0 10px black;
+}
+
+.progress {
+ height: 30px;
+ flex-grow: 1;
+ position: relative;
+ margin-right: 20px;
+}
+
+.progress::after {
+ display: block;
+ content: '';
+ background-color: rgba(255, 255, 255, 0.45);
+ height: 2px;
+ width: 100%;
+ margin-top: 14px;
+}
+
+.progressLine {
+ background-color: rgb(255, 255, 255);
+ height: 2px;
+ margin-top: 14px;
+ width: 0%;
+ float: left;
+}
+
+.progressMarker {
+ height: 14px;
+ width: 14px;
+ position: absolute;
+ left: 0%;
+ top: 50%;
+ margin-top: -7px;
+ cursor: pointer;
+ border-radius: 14px;
+ background: white;
+}
+
+.controls > button {
+ width: 30px;
+ height: 30px;
+ flex-shrink: 0;
+}
+
+.overlayButton {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.overlayButton > svg {
+ max-width: 120px;
+ max-height: 120px;
+}
+
+/* Swap button's icons based on root state. */
+
+/* play/pause */
+.root:not([data-playing]) .playButton > svg:last-child,
+.root[data-playing] .playButton > svg:first-child,
+/* mute/unmute */
+.root:not([data-muted]) .muteButton > svg:last-child,
+.root[data-muted] .muteButton > svg:first-child {
+ display: none;
+}
+
+/* Ad controls */
+
+.root[data-ad] > .controls {
+ height: 30px;
+ justify-content: flex-end;
+ padding: 10px;
+}
+
+.root[data-ad] > .controls > button {
+ height: 22px;
+}
+
+.root[data-ad] > .controls .muteButton {
+ margin-right: 10px;
+}
+
+@media screen and (max-width: 400) {
+ .root[data-skippable] > .controls {
+ height: 20px;
+ }
+ .root[data-skippable] > .controls > button {
+ height: 18px;
+ }
+}
+
+/* Show ad controls */
+.root[data-ad] > .controls > .countdownWrapper {
+ display: flex;
+}
+
+/* Hide non-ad controls */
+.root[data-ad] > .controls > .time,
+.root[data-ad] > .controls > .progress {
+ display: none;
+}
diff --git a/extensions/amp-ima-video/0.1/amp-ima-video.js b/extensions/amp-ima-video/0.1/amp-ima-video.js
index 907227a6f1b3..f388b7f19954 100644
--- a/extensions/amp-ima-video/0.1/amp-ima-video.js
+++ b/extensions/amp-ima-video/0.1/amp-ima-video.js
@@ -206,14 +206,18 @@ class AmpImaVideo extends AMP.BaseElement {
const consentPromise = consentPolicyId
? getConsentPolicyState(element, consentPolicyId)
: Promise.resolve(null);
+ element.setAttribute(
+ 'data-source-children',
+ JSON.stringify(this.sourceChildren_)
+ );
return consentPromise.then((initialConsentState) => {
- const iframeContext = {
- initialConsentState,
- sourceChildren: this.sourceChildren_,
- };
- const iframe = getIframe(win, element, TYPE, iframeContext, {
- allowFullscreen: true,
- });
+ const iframe = getIframe(
+ win,
+ element,
+ TYPE,
+ {initialConsentState},
+ {allowFullscreen: true}
+ );
iframe.title = this.element.title || 'IMA video';
this.applyFillContent(iframe);
diff --git a/extensions/amp-ima-video/0.1/test/test-amp-ima-video.js b/extensions/amp-ima-video/0.1/test/test-amp-ima-video.js
index 081ecab00259..b2294c4d67d8 100644
--- a/extensions/amp-ima-video/0.1/test/test-amp-ima-video.js
+++ b/extensions/amp-ima-video/0.1/test/test-amp-ima-video.js
@@ -55,8 +55,9 @@ describes.realWin(
);
const parsedName = JSON.parse(iframe.name);
- const sourceChildren = parsedName?.attributes?._context?.sourceChildren;
- expect(sourceChildren).to.not.be.null;
+ const sourceChildrenSerialized = parsedName?.attributes?.sourceChildren;
+ expect(sourceChildrenSerialized).to.not.be.null;
+ const sourceChildren = JSON.parse(sourceChildrenSerialized);
expect(sourceChildren).to.have.length(2);
expect(sourceChildren[0][0]).to.eql('SOURCE');
expect(sourceChildren[0][1]).to.eql({'data-foo': 'bar', src: 'src'});
diff --git a/extensions/amp-ima-video/0.1/test/test-ima-video-internal.js b/extensions/amp-ima-video/0.1/test/test-ima-video-internal.js
index 737f9c438f53..e3fdd948356b 100644
--- a/extensions/amp-ima-video/0.1/test/test-ima-video-internal.js
+++ b/extensions/amp-ima-video/0.1/test/test-ima-video-internal.js
@@ -100,10 +100,7 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
tag: adTagUrl,
});
const bigPlayDivMock = {
- style: {
- display: '',
- },
- removeEventListener() {},
+ setAttribute: env.sandbox.spy(),
};
const adDisplayContainerMock = {initialize() {}};
const initSpy = env.sandbox.spy(adDisplayContainerMock, 'initialize');
@@ -120,9 +117,10 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.onBigPlayClick();
- expect(imaVideoObj.getPropertiesForTesting().playbackStarted).to.be.true;
- expect(imaVideoObj.getPropertiesForTesting().uiTicker).to.exist;
- expect(bigPlayDivMock.style.display).to.eql('none');
+ const properties = imaVideoObj.getPropertiesForTesting();
+ expect(properties.playbackStarted).to.be.true;
+ expect(properties.uiTicker).to.exist;
+ expect(bigPlayDivMock.setAttribute.withArgs('hidden', '')).to.be.calledOnce;
expect(initSpy).to.be.called;
expect(loadSpy).to.be.called;
// TODO - Fix one I figure out how to spy on internals.
@@ -195,8 +193,7 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
defaults = Object.assign(defaults, {adLabel: label});
}
imaVideoObj.imaVideo(win, defaults);
- const {controlsDiv} = imaVideoObj.getPropertiesForTesting();
- const countdownDiv = controlsDiv.querySelector('#ima-countdown > div');
+ const {countdownDiv} = imaVideoObj.getPropertiesForTesting();
const adsManagerMock = getAdsManagerMock({remainingTime});
adPodInfo.getTotalAds = () => totalAds;
adPodInfo.getAdPosition = () => adPosition;
@@ -467,7 +464,7 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
expect(removeEventListenerSpy).to.have.been.calledWith(
properties.interactEvent
);
- expect(properties.adContainerDiv.style.display).to.eql('block');
+ expect(properties.adContainerDiv).not.to.have.attribute('hidden');
expect(removeEventListenerSpy).to.have.been.calledWith('ended');
// TODO - Fix when I can spy on internals.
//expect(hideControlsSpy).to.have.been.called;
@@ -519,7 +516,7 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
expect(removeEventListenerSpy).to.have.been.calledWith(
properties.interactEvent
);
- expect(properties.adContainerDiv.style.display).to.eql('block');
+ expect(properties.adContainerDiv).not.to.have.attribute('hidden');
expect(removeEventListenerSpy).to.have.been.calledWith('ended');
// TODO - Fix when I can spy on internals.
//expect(hideControlsSpy).to.have.been.called;
@@ -552,33 +549,20 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.setAdsManagerForTesting(adsManagerMock);
const {controlsDiv} = imaVideoObj.getPropertiesForTesting();
expect(controlsDiv).not.to.be.null;
- const countdownWrapperDiv = controlsDiv.querySelector('#ima-countdown');
- expect(countdownWrapperDiv).not.to.be.null;
- const playPauseDiv = controlsDiv.querySelector('#ima-play-pause');
+ const {playPauseDiv} = imaVideoObj.getPropertiesForTesting();
expect(playPauseDiv).not.to.be.null;
- const timeDiv = controlsDiv.querySelector('#ima-time');
+ const {timeDiv} = imaVideoObj.getPropertiesForTesting();
expect(timeDiv).not.to.be.null;
- const progressBarWrapperDiv = controlsDiv.querySelector(
- '#ima-progress-wrapper'
- );
- expect(progressBarWrapperDiv).not.to.be.null;
- const muteUnmuteDiv = controlsDiv.querySelector('#ima-mute-unmute');
+ const {muteUnmuteDiv} = imaVideoObj.getPropertiesForTesting();
expect(muteUnmuteDiv).not.to.be.null;
- const fullscreenDiv = controlsDiv.querySelector('#ima-fullscreen');
+ const {fullscreenDiv} = imaVideoObj.getPropertiesForTesting();
expect(fullscreenDiv).not.to.be.null;
// expect controls to be hidden initially
- expect(controlsDiv.style.display).to.eql('none');
- expect(countdownWrapperDiv.style.display).to.eql('none');
+ expect(controlsDiv).to.have.attribute('hidden');
// call pause function to display ads
imaVideoObj.onContentPauseRequested(mockGlobal);
- // expect a subset of controls to be hidden / displayed
- expect(controlsDiv.style.display).not.to.eql('none');
- expect(countdownWrapperDiv.style.display).not.to.eql('none');
- expect(playPauseDiv.style.display).not.to.eql('none');
- expect(timeDiv.style.display).to.eql('none');
- expect(progressBarWrapperDiv.style.display).to.eql('none');
- expect(muteUnmuteDiv.style.display).not.to.eql('none');
- expect(fullscreenDiv.style.display).not.to.eql('none');
+ // expect controls to now be shown
+ expect(controlsDiv).not.to.have.attribute('hidden');
});
it('resumes content', () => {
@@ -632,27 +616,16 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.onContentResumeRequested();
// verify original
- const {controlsDiv} = imaVideoObj.getPropertiesForTesting();
- const playPauseDiv = controlsDiv.querySelector('#ima-play-pause');
- expect(playPauseDiv).to.not.be.null;
- expect(playPauseDiv.style.display).not.to.eql('none');
- expect(playPauseDiv.innerHTML).equal(
- imaVideoObj.getPropertiesForTesting().icons['pause']
- );
+ const {root} = imaVideoObj.getPropertiesForTesting().elements;
+ expect(root).to.have.attribute('data-playing');
// run test
imaVideoObj.onAdPaused();
- expect(playPauseDiv.style.display).not.to.eql('none');
- expect(playPauseDiv.innerHTML).equal(
- imaVideoObj.getPropertiesForTesting().icons['play']
- );
+ expect(root).not.to.have.attribute('data-playing');
// run test
imaVideoObj.onAdResumed();
- expect(playPauseDiv.style.display).not.to.eql('none');
- expect(playPauseDiv.innerHTML).equal(
- imaVideoObj.getPropertiesForTesting().icons['pause']
- );
+ expect(root).to.have.attribute('data-playing');
});
it('resumes content with content complete', () => {
@@ -699,40 +672,24 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.setVideoPlayerForTesting(getVideoPlayerMock());
imaVideoObj.setContentCompleteForTesting(true);
// expect a subset of controls to be hidden / displayed during ad
- const {controlsDiv} = imaVideoObj.getPropertiesForTesting();
+ const {
+ controlsDiv,
+ playPauseDiv,
+ timeDiv,
+ muteUnmuteDiv,
+ fullscreenDiv,
+ } = imaVideoObj.getPropertiesForTesting();
expect(controlsDiv).not.to.be.null;
- const countdownWrapperDiv = controlsDiv.querySelector('#ima-countdown');
- expect(countdownWrapperDiv).not.to.be.null;
- const playPauseDiv = controlsDiv.querySelector('#ima-play-pause');
expect(playPauseDiv).not.to.be.null;
- const timeDiv = controlsDiv.querySelector('#ima-time');
expect(timeDiv).not.to.be.null;
- const progressBarWrapperDiv = controlsDiv.querySelector(
- '#ima-progress-wrapper'
- );
- expect(progressBarWrapperDiv).not.to.be.null;
- const muteUnmuteDiv = controlsDiv.querySelector('#ima-mute-unmute');
expect(muteUnmuteDiv).not.to.be.null;
- const fullscreenDiv = controlsDiv.querySelector('#ima-fullscreen');
expect(fullscreenDiv).not.to.be.null;
imaVideoObj.showAdControls();
- expect(controlsDiv.style.display).not.to.eql('none');
- expect(countdownWrapperDiv.style.display).not.to.eql('none');
- expect(playPauseDiv.style.display).not.to.eql('none');
- expect(timeDiv.style.display).to.eql('none');
- expect(progressBarWrapperDiv.style.display).to.eql('none');
- expect(muteUnmuteDiv.style.display).not.to.eql('none');
- expect(fullscreenDiv.style.display).not.to.eql('none');
+ expect(fullscreenDiv).not.to.have.attribute('hidden');
// resume content after ad finishes
imaVideoObj.onContentResumeRequested();
// expect default control buttons to be displayed again
- expect(countdownWrapperDiv.style.display).to.eql('none');
- expect(playPauseDiv.style.display).not.to.eql('none');
- expect(timeDiv.style.display).not.to.eql('none');
- expect(progressBarWrapperDiv.style.display).not.to.eql('none');
- expect(muteUnmuteDiv.style.display).not.to.eql('none');
- expect(fullscreenDiv.style.display).not.to.eql('none');
- expect(controlsDiv.style.display).not.to.eql('none');
+ expect(controlsDiv).not.to.have.attribute('hidden');
});
it('ad controls are smaller when skippable on mobile', () => {
@@ -752,41 +709,24 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
skippable: {getSkipTimeOffset: () => 30},
unskippable: {getSkipTimeOffset: () => -1},
};
- const video = {
- hasMobileStyles: 400,
- noMobileStyles: 401,
- };
const tests = [
{
- msg: 'Controls should be small when ad is skippable and mobile',
+ msg: 'Should set data-skippable if ad is skippable',
ad: ad.skippable,
- videoSize: video.hasMobileStyles,
- expected: {heightControls: '20px', heightButtons: '18px'},
+ expected: true,
},
{
- msg: 'Controls should be tall when ad is not skippable',
+ msg: 'Should not set data-skippable if ad is unskippable',
ad: ad.unskippable,
- videoSize: video.hasMobileStyles,
- expected: {heightControls: '30px', heightButtons: '22px'},
- },
- {
- msg: 'Controls should be tall when ad is not mobile',
- ad: ad.skippable,
- videoSize: video.noMobileStyles,
- expected: {heightControls: '30px', heightButtons: '22px'},
+ expected: false,
},
];
- tests.forEach(({ad, videoSize, expected, msg}) => {
- imaVideoObj.setVideoWidthAndHeightForTesting(videoSize, 300);
+ tests.forEach(({ad, expected, msg}) => {
imaVideoObj.onAdLoad({getAd: () => ad});
imaVideoObj.showAdControls();
- const {controlsDiv} = imaVideoObj.getPropertiesForTesting();
- const muteUnmuteDiv = controlsDiv.querySelector('#ima-mute-unmute');
- const fullscreenDiv = controlsDiv.querySelector('#ima-fullscreen');
- expect(controlsDiv.style.height).to.eql(expected.heightControls, msg);
- expect(muteUnmuteDiv.style.height).to.eql(expected.heightButtons, msg);
- expect(fullscreenDiv.style.height).to.eql(expected.heightButtons, msg);
+ const {root} = imaVideoObj.getPropertiesForTesting().elements;
+ expect(root.hasAttribute('data-skippable'), msg).to.eql(expected);
});
});
@@ -821,9 +761,7 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoProperties.interactEvent
);
expect(addEventListenerSpy).to.have.been.calledWith('ended');
- expect(imaVideoProperties.bigPlayDiv.style.display).to.be.equal(
- 'table-cell'
- );
+ expect(imaVideoProperties.bigPlayDiv).not.to.have.attribute('hidden');
}
);
@@ -844,9 +782,7 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
const imaVideoProperties = imaVideoObj.getPropertiesForTesting();
- expect(imaVideoProperties.bigPlayDiv.style.display).to.be.equal(
- 'table-cell'
- );
+ expect(imaVideoProperties.bigPlayDiv).not.to.have.attribute('hidden');
});
it(
@@ -880,11 +816,11 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoProperties.interactEvent
);
expect(addEventListenerSpy).to.have.been.calledWith('ended');
- expect(imaVideoProperties.bigPlayDiv.style.display).to.be.equal('none');
+ expect(imaVideoProperties.bigPlayDiv).to.have.attribute('hidden');
}
);
- it('updates UI', () => {
+ it('updates playing time', () => {
const div = doc.createElement('div');
div.setAttribute('id', 'c');
doc.body.appendChild(div);
@@ -896,8 +832,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
tag: adTagUrl,
});
- imaVideoObj.updateUi(0, 60);
- expect(imaVideoObj.getPropertiesForTesting().timeNode.textContent).to.eql(
+ imaVideoObj.updateTime(0, 60);
+ expect(imaVideoObj.getPropertiesForTesting().timeDiv.textContent).to.eql(
'0:00 / 1:00'
);
expect(
@@ -906,8 +842,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
expect(
imaVideoObj.getPropertiesForTesting().progressMarkerDiv.style.left
).to.eql('-1%');
- imaVideoObj.updateUi(30, 60);
- expect(imaVideoObj.getPropertiesForTesting().timeNode.textContent).to.eql(
+ imaVideoObj.updateTime(30, 60);
+ expect(imaVideoObj.getPropertiesForTesting().timeDiv.textContent).to.eql(
'0:30 / 1:00'
);
expect(
@@ -916,8 +852,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
expect(
imaVideoObj.getPropertiesForTesting().progressMarkerDiv.style.left
).to.eql('49%');
- imaVideoObj.updateUi(60, 60);
- expect(imaVideoObj.getPropertiesForTesting().timeNode.textContent).to.eql(
+ imaVideoObj.updateTime(60, 60);
+ expect(imaVideoObj.getPropertiesForTesting().timeDiv.textContent).to.eql(
'1:00 / 1:00'
);
expect(
@@ -1038,8 +974,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.playVideo();
expect(
- imaVideoObj.getPropertiesForTesting().adContainerDiv.style.display
- ).to.eql('none');
+ imaVideoObj.getPropertiesForTesting().adContainerDiv
+ ).to.have.attribute('hidden');
expect(imaVideoObj.getPropertiesForTesting().playerState).to.eql(
imaVideoObj.getPropertiesForTesting().PlayerStates.PLAYING
);
@@ -1176,8 +1112,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.showControls();
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('flex');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).not.to.have.attribute('hidden');
expect(imaVideoObj.getPropertiesForTesting().hideControlsTimeout).to.be
.null;
});
@@ -1200,8 +1136,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.showControls();
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('flex');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).not.to.have.attribute('hidden');
expect(imaVideoObj.getPropertiesForTesting().hideControlsTimeout).not.to.be
.undefined;
});
@@ -1220,9 +1156,9 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.hideControls();
- expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ expect(imaVideoObj.getPropertiesForTesting().controlsDiv).to.have.attribute(
+ 'hidden'
+ );
});
// Case when autoplay signal is sent before play signal is sent.
@@ -1240,15 +1176,15 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.adsActive = false;
imaVideoObj.hideControls();
- expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ expect(imaVideoObj.getPropertiesForTesting().controlsDiv).to.have.attribute(
+ 'hidden'
+ );
expect(imaVideoObj.getPropertiesForTesting().hideControlsQueued).to.be.true;
imaVideoObj.playVideo();
- expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ expect(imaVideoObj.getPropertiesForTesting().controlsDiv).to.have.attribute(
+ 'hidden'
+ );
expect(imaVideoObj.getPropertiesForTesting().hideControlsQueued).to.be
.false;
});
@@ -1267,16 +1203,16 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.adsActive = false;
imaVideoObj.hideControls();
- expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ expect(imaVideoObj.getPropertiesForTesting().controlsDiv).to.have.attribute(
+ 'hidden'
+ );
expect(imaVideoObj.getPropertiesForTesting().hideControlsQueued).to.be.true;
// Fake the ad starting to play
imaVideoObj.showAdControls();
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('flex');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).not.to.have.attribute('hidden');
expect(imaVideoObj.getPropertiesForTesting().hideControlsQueued).to.be.true;
});
@@ -1298,8 +1234,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
expect(imaVideoObj.getPropertiesForTesting().controlsVisible).to.be.false;
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).to.have.attribute('hidden');
const interactEvent = new Event(hoverEvent);
const videoPlayerElement = imaVideoObj.getPropertiesForTesting()
@@ -1316,8 +1252,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
expect(imaVideoObj.getPropertiesForTesting().controlsVisible).to.be.true;
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('flex');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).not.to.have.attribute('hidden');
expect(imaVideoObj.getPropertiesForTesting().hideControlsTimeout).not.to
.be.undefined;
});
@@ -1337,8 +1273,8 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
imaVideoObj.hideControls();
expect(imaVideoObj.getPropertiesForTesting().controlsVisible).to.be.false;
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).to.have.attribute('hidden');
const interactEvent = new Event(hoverEvent);
const videoPlayerElement = imaVideoObj.getPropertiesForTesting()
@@ -1355,30 +1291,30 @@ describes.realWin('UI loaded in frame by amp-ima-video', {}, (env) => {
expect(imaVideoObj.getPropertiesForTesting().controlsVisible).to.be.true;
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('flex');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).not.to.have.attribute('hidden');
imaVideoObj.hideControls();
expect(imaVideoObj.getPropertiesForTesting().controlsVisible).to.be.false;
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).to.have.attribute('hidden');
clock.tick(100);
videoPlayerElement.dispatchEvent(interactEvent);
expect(imaVideoObj.getPropertiesForTesting().controlsVisible).to.be.false;
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('none');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).to.have.attribute('hidden');
clock.tick(950);
videoPlayerElement.dispatchEvent(interactEvent);
expect(imaVideoObj.getPropertiesForTesting().controlsVisible).to.be.true;
expect(
- imaVideoObj.getPropertiesForTesting().controlsDiv.style.display
- ).to.eql('flex');
+ imaVideoObj.getPropertiesForTesting().controlsDiv
+ ).not.to.have.attribute('hidden');
});
});