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.
Setup — External Scripts
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.6.11"></script>Code
<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>.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;
}
}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
| Name | Type | Default | Description |
|---|---|---|---|
| data-bunny-background-init | boolean | Marks the player container. The script queries all elements with this attribute and initialises each one independently. | |
| data-player-src | string | The HLS playlist URL (.m3u8) for the video to load. | |
| data-player-status | string | idle | Reflects live playback state. Values: idle, ready, loading, playing, paused. Updated automatically — never set manually. |
| data-player-activated | boolean | false | Becomes true after the first play, resets to false when the video ends. Use to hide the placeholder only after playback has started. |
| data-player-autoplay | boolean | false | Set to "true" to enable autoplay. Autoplay forces the video to be muted and looping. Play/pause is driven by IntersectionObserver. |
| data-player-muted | boolean | false | Reflects and controls the mute state. Updated automatically when mute is toggled. |
| data-player-lazy | boolean | false | Set to "true" to defer all loading until the user presses play. Set to "false" to load the stream eagerly on initialisation. |
| data-player-control | string | Add 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.