-
const src = 'https://images.unsplash.com/photo-1512851685250-bfa0aa982a1c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=889&q=80'
const srcset = 'https://images.unsplash.com/photo-1512851685250-bfa0aa982a1c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=889&q=80 889w, https://images.unsplash.com/photo-1512851685250-bfa0aa982a1c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1189&q=80 1189w, https://images.unsplash.com/photo-1512851685250-bfa0aa982a1c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1489&q=80 1489w, https://images.unsplash.com/photo-1512851685250-bfa0aa982a1c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1778&q=80 1778w, https://images.unsplash.com/photo-1512851685250-bfa0aa982a1c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1789&q=80 1789w, https://images.unsplash.com/photo-1512851685250-bfa0aa982a1c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2089&q=80 2089w'
.cp-wrapper
.aspect-ratio-container.aspect-ratio-container--16x9.anim-hover-zoom.js-inline-video-container
video.video-container__video.video-container__video--cover.js-video-element(
src="https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8"
playsinline
)
.video-container__image.js-video-tile-image(
aria-hidden="true"
)
img.js-inline-player-image.anim-hover-zoom__zooms(
src=src
srcset=srcset
sizes="(min-width: 800px) 800px, 100vw"
)
small This pen accompanies the article “Our Journey to Native HTML Video” on the <a href="https://engineering.kitchenstories.io/">Kitchen Stories Engineering Blog</a>
small <b>Media:</b> <a href="https://unsplash.com/photos/AvV5rJl1vcU">Image by ShareGrid</a> on Unsplash. Video courtesy of Apple, <a href="https://videojs.github.io/videojs-contrib-hls/">HLS manifest from VideoJS</a>
View Compiled
/**
* Video & Preview Image
*/
.video-container {
background-color: transparent;
cursor: pointer;
position: relative;
}
.video-container__video,
.video-container__image {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
.video-container__video {
background-color: #ddd;
cursor: pointer;
object-position: center;
// Helper to avoid clipping videos
// Square/portrait videos in landscape container element need to be contained
&--contain {
object-fit: contain;
}
&--cover {
object-fit: cover;
}
}
.video-container__image {
pointer-events: none;
transition: opacity 0.3s cubic-bezier(0, 0.1, 0.3, 1);
// Add play icon
&::after {
background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/413808/icon_play_big.svg');
background-position: center;
// Play icon in large containers should be larger than in small ones
background-size: calc(45px + 5%);
background-repeat: no-repeat;
content: '';
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: 10;
}
& > img {
height: inherit;
left: inherit;
object-fit: cover;
position: inherit;
top: inherit;
width: inherit;
}
&--hidden {
opacity: 0;
// Ensure nothing inside hidden container is clickable
& * {
pointer-events: none !important;
}
}
}
/**
* Aspect ratio
*/
.aspect-ratio-container {
position: relative;
background-color: #ddd;
border-radius: 0.5em;
overflow: hidden;
//z-index: 2; // ensure rounded corners for safari
&::before {
content: '';
display: inline-block;
width: 1px;
height: 0px;
}
& > img {
border-radius: inherit;
display: block;
left: 0;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
width: 100%;
}
&--1x1 {
&::before {
padding-bottom: 100%;
}
}
&--2x3 {
padding-bottom: calc(100% / (2/3));
}
&--4x3 {
&::before {
padding-bottom: calc(100% / (4/3));
}
}
&--3x4 {
&::before {
padding-bottom: 133.3333333%;
}
}
&--16x9 {
&::before {
padding-bottom: calc(100% / (16/9));
}
}
}
/**
* Zoom FX
*/
.anim-hover-zoom {
.anim-hover-zoom__zooms {
backface-visibility: hidden;
transition: transform 0.25s ease-out;
transform-origin: center;
will-change: transform;
}
&:hover {
.anim-hover-zoom__zooms {
overflow: hidden;
transform: scale(1.1);
}
}
}
/**
* Demo styles
*/
body {
background-color: #eee;
font-family: sans-serif;
}
.cp-wrapper {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
small {
display: block;
margin-top: 2rem;
}
a {
color: currentColor;
font-weight: bold;
}
View Compiled
class VideoPlayerController {
constructor(element) {
this.$container = element
if (!this.$container) {
console.error('Video container not found. Aborting initialization.')
return
}
this.$imgContainer = this.$container.querySelector('.js-video-tile-image')
this.$vidContainer = this.$container.querySelector('.js-video-element')
this.userControls = false
if (!this.$imgContainer || !this.$vidContainer) {
console.error('Video element and/or preview image not found. Aborting.')
return
}
this.handlePlay = this.handlePlay.bind(this)
this.parsed = false
this.init()
}
init() {
this.$vidContainer.addEventListener('click', (e) => {
/**
* Having script interaction interfers with playing/pausing as well
* as toggling native UI elements
*/
if (!this.userControls) {
// Prevent default to avoid play/pause duplication
e.preventDefault()
this.handlePlay()
}
})
this.$vidContainer.addEventListener('ended', () => {
VideoPlayerController.showImage(this.$imgContainer)
// We need to remove the controls again to make the event listener
// work properly in Firefox
this.$vidContainer.removeAttribute('controls')
// Hand back control to this class
this.userControls = false
})
}
handlePlay() {
// Make the script return if interactions occur while playing the video
this.userControls = true
// Attributes had to be missing from the rendered source to register
// the click in Firefox
this.$vidContainer.setAttribute('controls', 'true')
// Set focus() to allow toggling the play state via keyboard in Chrome
// Safari does not allow focussing the video event with tabindex, but
// native keyboard multimedia controls work
// Firefox sets the focus automatically
this.$vidContainer.focus()
try {
const hlsSupported = VideoPlayerController.hlsSupported()
if (hlsSupported || this.parsed) {
this.playVideo()
} else {
const { Hls } = window
if (Hls) {
this.parseHlsManifest()
} else {
VideoPlayerController.loadLibrary().then(() => {
this.parseHlsManifest()
})
}
}
} catch (error) {
console.error(error)
}
}
playVideo() {
console.log('HLS – Playing video')
VideoPlayerController.hideImage(this.$imgContainer)
this.$vidContainer.play().then(() => {
console.log('HLS - Video played')
})
}
parseHlsManifest() {
console.log('HLS – Parsing video manifest')
const { Hls } = window
const hls = new Hls()
const { src } = this.$vidContainer
hls.loadSource(src)
hls.attachMedia(this.$vidContainer)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.parsed = true
this.playVideo()
})
}
static loadLibrary() {
return new Promise((resolve, reject) => {
const $s = document.createElement('script')
$s.src =
'https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.light.min.js'
$s.onload = () => {
resolve()
}
$s.onerror = () => {
reject('Failed to load HLS library')
}
document.head.appendChild($s)
})
}
static hideImage($img) {
$img.classList.add('video-container__image--hidden')
}
static showImage($img) {
$img.classList.remove('video-container__image--hidden')
}
static hlsSupported() {
const $vid = document.createElement('video')
return (
$vid.canPlayType('application/x-mpegURL codecs="avc1.42E01E"') ||
$vid.canPlayType('application/vnd.apple.mpegurl')
)
}
}
document.addEventListener('DOMContentLoaded', () => {
const $videoContainers = document.querySelectorAll(
'.js-inline-video-container'
)
if ($videoContainers) {
Array.from($videoContainers).forEach(($container) => {
new VideoPlayerController($container)
})
}
})
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.