-
  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)
    })
  }
})
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.