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