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` +
+
+ +
+ + + + + + +
+ `; + + 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'); }); });