Untitled fiddle
by Julien Etienne
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Video Crossfade</title>
<style>
html, body {
margin: 0;
height: 100%;
background: #000;
}
.stage {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.stage video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 1s ease-in-out;
}
.stage video.active {
opacity: 1;
}
</style>
</head>
<body>
<div class="stage">
<video id="videoA" class="active" muted playsinline></video>
<video id="videoB" muted playsinline></video>
</div>
<script>
const playlist = [
'https://samplelib.com/mp4/sample-5s.mp4',
'https://samplelib.com/mp4/sample-10s-h265.mp4', // HEVC — Safari only, will error/skip on Chrome & Firefox
'https://filesamples.com/samples/video/mp4/sample_960x400_ocean_with_audio.mp4',
'https://www.w3schools.com/html/mov_bbb.mp4'
]
const fadeSeconds = 1 // must match the CSS transition duration above
const loop = true // set to false to stop after the last clip
const players = [document.getElementById('videoA'), document.getElementById('videoB')]
let activeSlot = 0 // which element in players[] is currently visible
let clipIndex = 0 // which playlist entry is currently visible
let transitioning = false // lock so rapid-fire timeupdate events can't double-trigger
const nextClipIndex = current => {
const next = current + 1
if (next < playlist.length) return next
return loop ? 0 : -1
}
const showSlot = slot => {
players.forEach((p, i) => p.classList.toggle('active', i === slot))
}
// Forces a real reload via .load() and waits for canplay/error rather than
// trusting loadedmetadata to refire on an already-used <video> element.
const loadClip = (player, idx) => new Promise((resolve, reject) => {
...