Bunny HLS Background Video

A full-cover HLS background video player powered by hls.js. Supports autoplay with IntersectionObserver-based play/pause, lazy loading, mute toggle, and a play/pause button. Plays silently as a section background.

hlsbackgroundbunnyvideoautoplay

Setup — External Scripts

Setup: External Scripts
html
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.6.11"></script>

Code

index.html
html
<section class="demo-section">
  <div class="bunny-bg" data-bunny-background-init="" data-player-activated="false" data-player-src="https://vz-6ed806ff-5e5.b-cdn.net/b6a663de-07c1-4c37-8bb6-0e79fef7fb3c/playlist.m3u8" data-player-status="idle" data-player-lazy="false" data-player-autoplay="true">
    <video class="bunny-bg__video" preload="auto" width="1920" height="1080" playsinline="playsinline" muted=""></video>
    <img src="https://cdn.prod.website-files.com/68d1624803866689f970e4b1/68d165db0007459a86b6cfd9_player-placeholder.jpg" loading="lazy" width="Auto" class="bunny-bg__placeholder">
    <div data-player-control="playpause" class="bunny-bg__playpause">
      <div class="bunny-bg__btn">
        <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none" class="bunny-bg__pause-svg"><path d="M16 5V19" stroke="currentColor" stroke-width="3" stroke-miterlimit="10"></path><path d="M8 5V19" stroke="currentColor" stroke-width="3" stroke-miterlimit="10"></path></svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none" class="bunny-bg__play-svg"><path d="M6 12V5.01109C6 4.05131 7.03685 3.4496 7.87017 3.92579L14 7.42855L20.1007 10.9147C20.9405 11.3945 20.9405 12.6054 20.1007 13.0853L14 16.5714L7.87017 20.0742C7.03685 20.5503 6 19.9486 6 18.9889V12Z" fill="currentColor"></path></svg>
      </div>
    </div>
    <div class="bunny-bg__loading">
      <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="L9" x="0px" y="0px" viewbox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve" width="100%" class="bunny-bg__loading-svg" fill="none"><path fill="currentColor" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"></path><animatetransform attributename="transform" attributetype="XML" type="rotate" dur="1s" from="0 50 50" to="360 50 50" repeatcount="indefinite"></animatetransform></svg>
    </div>
  </div>
  <div class="demo-section__fade-left"></div>
  <div class="demo-section__title">
    <h1 class="demo-section__title-h1">Background<br>Bunny HLS Player</h1>
  </div>
</section>
styles.css
css
.bunny-bg {
  pointer-events: none;
  color: #fff;
  isolation: isolate;
  border-radius: 1em;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  transform: translateX(0);
}

.bunny-bg__video {
  object-fit: cover;
  width: 100%;
  height: 100%;
  padding-bottom: 0;
  padding-right: 0;
  display: block;
  position: absolute;
  top: 0;
  left: 0;
}

/* Animation */
[data-bunny-background-init] :is(.bunny-bg__placeholder, .bunny-bg__loading) {
  transition: opacity 0.3s linear, visibility 0.3s linear;
}

/* Placeholder */
.bunny-bg__placeholder {
  object-fit: cover;
  width: 100%;
  height: 100%;
  position: absolute;
}

[data-bunny-background-init][data-player-status="playing"] .bunny-bg__placeholder,
[data-bunny-background-init][data-player-status="paused"] .bunny-bg__placeholder,
[data-bunny-background-init][data-player-activated="true"][data-player-status="ready"] .bunny-bg__placeholder {
  opacity: 0;
  visibility: hidden;
}

/* Loading */
.bunny-bg__loading {
  opacity: 0;
  visibility: hidden;
  background-color: #00000054;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
}

.bunny-bg__loading-svg {
  width: 6em;
}

[data-bunny-background-init][data-player-status="loading"] .bunny-bg__loading {
  opacity: 1;
  visibility: visible;
}

/* Play/Pause */
.bunny-bg__playpause {
  pointer-events: auto;
  justify-content: center;
  align-items: center;
  display: flex;
  position: absolute;
  bottom: 4vw;
  right: 4vw;
}

.bunny-bg__btn {
  -webkit-backdrop-filter: blur(1em);
  backdrop-filter: blur(1em);
  cursor: pointer;
  background-color: #6464644d;
  border: 1px solid #ffffff1a;
  border-radius: 50%;
  justify-content: center;
  align-items: center;
  width: 3em;
  height: 3em;
  padding: .8125em;
  display: flex;
  position: relative;
}

.bunny-bg__pause-svg {
  display: none;
}

[data-bunny-background-init][data-player-status="playing"] .bunny-bg__play-svg,
[data-bunny-background-init][data-player-status="loading"] .bunny-bg__play-svg {
  display: none;
}

[data-bunny-background-init][data-player-status="playing"] .bunny-bg__pause-svg,
[data-bunny-background-init][data-player-status="loading"] .bunny-bg__pause-svg {
  display: block;
}

/* Demo Section */
.demo-section {
  color: #efeeec;
  background-color: #000;
  flex-flow: column;
  justify-content: flex-end;
  align-items: flex-start;
  min-height: 100svh;
  padding: 4vw;
  display: flex;
  position: relative;
  overflow: hidden;
}

.demo-section__title {
  position: relative;
}

.demo-section__title-h1 {
  max-width: 9em;
  font-size: 8vw;
  font-weight: 500;
  line-height: 1;
}

.demo-section__fade-left {
  pointer-events: none;
  background-image: linear-gradient(45deg, #000, #0000 50%);
  width: 90vw;
  height: 90vw;
  position: absolute;
  bottom: 0;
  left: 0;
}

@media screen and (max-width: 991px) {
  .bunny-bg__playpause {
    bottom: 1em;
    right: 1em;
  }

  .demo-section {
    padding-bottom: 25vw;
  }

  .demo-section__title-h1 {
    font-size: 15vw;
  }
}
script.js
javascript
function initBunnyPlayerBackground() {
  document.querySelectorAll('[data-bunny-background-init]').forEach(function(player) {
    var src = player.getAttribute('data-player-src');
    if (!src) return;

    var video = player.querySelector('video');
    if (!video) return;

    try { video.pause(); } catch(_) {}
    try { video.removeAttribute('src'); video.load(); } catch(_) {}

    // Attribute helpers
    function setStatus(s) {
      if (player.getAttribute('data-player-status') !== s) {
        player.setAttribute('data-player-status', s);
      }
    }
    function setActivated(v) { player.setAttribute('data-player-activated', v ? 'true' : 'false'); }
    if (!player.hasAttribute('data-player-activated')) setActivated(false);

    // Flags
    var lazyMode   = player.getAttribute('data-player-lazy'); // "true" | "false"
    var isLazyTrue = lazyMode === 'true';
    var autoplay   = player.getAttribute('data-player-autoplay') === 'true';
    var initialMuted = player.getAttribute('data-player-muted') === 'true';

    var pendingPlay = false;

    if (autoplay) { video.muted = true; video.loop = true; }
    else { video.muted = initialMuted; }

    video.setAttribute('muted', '');
    video.setAttribute('playsinline', '');
    video.setAttribute('webkit-playsinline', '');
    video.playsInline = true;
    if (typeof video.disableRemotePlayback !== 'undefined') video.disableRemotePlayback = true;
    if (autoplay) video.autoplay = false;

    var isSafariNative = !!video.canPlayType('application/vnd.apple.mpegurl');
    var canUseHlsJs    = !!(window.Hls && Hls.isSupported()) && !isSafariNative;

    var isAttached = false;
    var userInteracted = false;
    var lastPauseBy = ''; // 'io' | 'manual' | ''

    function attachMediaOnce() {
      if (isAttached) return;
      isAttached = true;

      if (player._hls) { try { player._hls.destroy(); } catch(_) {} player._hls = null; }

      if (isSafariNative) {
        video.preload = isLazyTrue ? 'none' : 'auto';
        video.src = src;
        video.addEventListener('loadedmetadata', function() {
          readyIfIdle(player, pendingPlay);
        }, { once: true });
      } else if (canUseHlsJs) {
        var hls = new Hls({ maxBufferLength: 10 });
        hls.attachMedia(video);
        hls.on(Hls.Events.MEDIA_ATTACHED, function() { hls.loadSource(src); });
        hls.on(Hls.Events.MANIFEST_PARSED, function() {
          readyIfIdle(player, pendingPlay);
        });
        player._hls = hls;
      } else {
        video.src = src;
      }
    }

    if (isLazyTrue) {
      video.preload = 'none';
    } else {
      attachMediaOnce();
    }

    function togglePlay() {
      userInteracted = true;
      if (video.paused || video.ended) {
        if (isLazyTrue && !isAttached) attachMediaOnce();
        pendingPlay = true;
        lastPauseBy = '';
        setStatus('loading');
        safePlay(video);
      } else {
        lastPauseBy = 'manual';
        video.pause();
      }
    }

    function toggleMute() {
      video.muted = !video.muted;
      player.setAttribute('data-player-muted', video.muted ? 'true' : 'false');
    }

    player.addEventListener('click', function(e) {
      var btn = e.target.closest('[data-player-control]');
      if (!btn || !player.contains(btn)) return;
      var type = btn.getAttribute('data-player-control');
      if (type === 'play' || type === 'pause' || type === 'playpause') togglePlay();
      else if (type === 'mute') toggleMute();
    });

    video.addEventListener('play', function() { setActivated(true); setStatus('playing'); });
    video.addEventListener('playing', function() { pendingPlay = false; setStatus('playing'); });
    video.addEventListener('pause', function() { pendingPlay = false; setStatus('paused'); });
    video.addEventListener('waiting', function() { setStatus('loading'); });
    video.addEventListener('canplay', function() { readyIfIdle(player, pendingPlay); });
    video.addEventListener('ended', function() { pendingPlay = false; setStatus('paused'); setActivated(false); });

    if (autoplay) {
      if (player._io) { try { player._io.disconnect(); } catch(_) {} }
      var io = new IntersectionObserver(function(entries) {
        entries.forEach(function(entry) {
          var inView = entry.isIntersecting && entry.intersectionRatio > 0;
          if (inView) {
            if (isLazyTrue && !isAttached) attachMediaOnce();
            if ((lastPauseBy === 'io') || (video.paused && lastPauseBy !== 'manual')) {
              setStatus('loading');
              if (video.paused) togglePlay();
              lastPauseBy = '';
            }
          } else {
            if (!video.paused && !video.ended) {
              lastPauseBy = 'io';
              video.pause();
            }
          }
        });
      }, { threshold: 0.1 });
      io.observe(player);
      player._io = io;
    }
  });

  function readyIfIdle(player, pendingPlay) {
    if (!pendingPlay &&
        player.getAttribute('data-player-activated') !== 'true' &&
        player.getAttribute('data-player-status') === 'idle') {
      player.setAttribute('data-player-status', 'ready');
    }
  }

  function safePlay(video) {
    var p = video.play();
    if (p && typeof p.then === 'function') p.catch(function(){});
  }
}

// Initialize Bunny HTML HLS Player (Background)
document.addEventListener('DOMContentLoaded', function() {
  initBunnyPlayerBackground();
});

Attributes

NameTypeDefaultDescription
data-bunny-background-initbooleanMarks the player container. The script queries all elements with this attribute and initialises each one independently.
data-player-srcstringThe HLS playlist URL (.m3u8) for the video to load.
data-player-statusstringidleReflects live playback state. Values: idle, ready, loading, playing, paused. Updated automatically — never set manually.
data-player-activatedbooleanfalseBecomes true after the first play, resets to false when the video ends. Use to hide the placeholder only after playback has started.
data-player-autoplaybooleanfalseSet to "true" to enable autoplay. Autoplay forces the video to be muted and looping. Play/pause is driven by IntersectionObserver.
data-player-mutedbooleanfalseReflects and controls the mute state. Updated automatically when mute is toggled.
data-player-lazybooleanfalseSet to "true" to defer all loading until the user presses play. Set to "false" to load the stream eagerly on initialisation.
data-player-controlstringAdd to UI elements inside the player. Accepted values: playpause, play, pause, mute.

Notes

  • This player only works with HLS (.m3u8) sources. It will not play regular MP4 files.
  • On Safari (macOS and iOS), native HLS support is used automatically instead of hls.js.
  • When autoplay is enabled, the video is always muted to comply with browser autoplay policies.
  • When autoplay is enabled, the video loops automatically and play/pause is driven by IntersectionObserver — it plays when in view and pauses when scrolled out.
  • The HLS.js script must be loaded before the player script runs.
  • Multiple background players on the same page are supported — the script initialises all elements with [data-bunny-background-init].

Guide

Implementation

For hosting .m3u8 HLS files, Bunny.net is recommended. It provides a straightforward interface for uploading and managing videos, automatically generating the HLS streams needed by this player. This player is built exclusively for HLS sources (.m3u8) and will not work with regular MP4 files.

Container

The player container is marked with [data-bunny-background-init]. Set the HLS source on [data-player-src]. This element becomes the root for all other attributes and UI. The video covers its parent element fully using object-fit: cover.

Status

The [data-player-status] attribute reflects live playback state and is never set manually. Use it in CSS to show or hide UI: idle (initialised, no media), ready (metadata loaded), loading (buffering), playing (active), paused (stopped or ended).

Autoplay

Set [data-player-autoplay="true"] to start playback automatically when the element enters the viewport. Autoplay forces the video to be muted and looping. An IntersectionObserver automatically pauses the video when it leaves the viewport and resumes it when it returns — unless the user manually paused it.

Lazy loading

Set [data-player-lazy="true"] to defer all network requests until the user first presses play. Set to "false" (default) to load the stream eagerly on page load.

Play / Pause

Add [data-player-control="playpause"] to a button inside the player to toggle playback. Use [data-player-control="play"] or [data-player-control="pause"] for separate controls.

Mute

Add [data-player-control="mute"] to toggle the mute state. [data-player-muted="true|false"] updates automatically so you can style the icon accordingly.

Multiple players

The script uses querySelectorAll so all elements with [data-bunny-background-init] on the page are initialised independently. Each player manages its own state, HLS instance, and IntersectionObserver.