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) => {
   ...