跪拜 Guibai
← Back to the summary

Double Buffering Video Switches: No More Black Frames Between Clips

In carousels, product showcases, or background animations, when switching videos you might encounter this: the moment you click to switch, the screen blacks out for a moment, or it freezes in place, and after a while the new video appears. The video itself is fine; the problem lies in the "switch" action.

This article uses a set of comparison demos between a single video and dual videos to explain how this gap arises, and introduces an optimization method that only needs a dozen or so lines of code—double buffering.

Why switching videos "flashes"

Carousels, product showcases, and background animations often need to switch different videos in the same position. The most straightforward way is to obtain the video element and directly change its src:

video.removeAttribute('src')
video.load()
video.src = nextSrc
video.play()

The problem is that this video simultaneously handles two things: displaying the current frame and loading the next resource. Once it starts loading the new resource, the old frame becomes unavailable, and the new video has to go through downloading and decoding before its first frame appears. During that time, the user sees a black screen, a background color, or a "loading" overlay.

This gap—"the old frame is gone, the new frame hasn't arrived yet"—is the source of the stutter during switching.

Look at the single video approach first

Below is the single video implementation on the left side of the demo. To make the gap easier to observe, we show a "loading" overlay during the switch and remove it after the playing event fires:

<template>
  <div class="stage">
    <video ref="video" muted playsinline loop></video>
    <div v-if="loading" class="mask">Loading…</div>
  </div>
</template>

<script setup lang="ts">
const video = useTemplateRef<HTMLVideoElement>('video')
const loading = ref(false)

const play = (src: string) => {
  const el = video.value!
  el.src = src
  el.play().catch(() => {})
}

const switchClip = (index: number) => {
  loading.value = true
  // Directly clear the currently displayed video, the old frame immediately disappears
  video.value!.removeAttribute('src')
  video.value!.load()
  play(clips[index].src)
}

onMounted(() => {
  video.value!.addEventListener('playing', () => (loading.value = false))
  play(clips[0].src)
})
</script>

Here the overlay just "visualizes" the gap. Even if you remove the overlay, the user still sees the background show through or the frame stuck.

Dual video: separate loading from displaying

The idea of double buffering is simple: since one video cannot both "display" and "load" at the same time, use two.

Click switch → foreground stays still → background loads new video → first frame ready → play background, pause foreground → swap display

Throughout the entire process, the user always sees a video with an image; there is no gap.

Step 1: Stack two videos

Stack two video elements in the same container, and use a boolean to record which one is currently displayed.

<template>
  <div class="stage">
    <video ref="videoA" :class="{ show: isAVisible }" muted playsinline loop></video>
    <video ref="videoB" :class="{ show: !isAVisible }" muted playsinline loop></video>
  </div>
</template>

Styling: both videos are absolutely positioned with identical dimensions, and the one without the show class is hidden using visibility: hidden:

.stage {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #000;
}

.stage video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.stage video:not(.show) {
  visibility: hidden;
}

Using visibility instead of v-if here ensures the two video elements always exist and won't lose state due to destroy-and-recreate. muted and playsinline help satisfy the conditions for muted inline autoplay on mobile.

Step 2: Only load in the background, swap after first frame ready

The core logic is only a dozen or so lines:

const videoA = useTemplateRef<HTMLVideoElement>('videoA')
const videoB = useTemplateRef<HTMLVideoElement>('videoB')
// Whether A or B is currently displayed
const isAVisible = ref(true)

const switchClip = (clipIndex: number) => {
  const visibleVideo = isAVisible.value ? videoA.value! : videoB.value! // The one being displayed
  const standbyVideo = isAVisible.value ? videoB.value! : videoA.value! // The hidden background

  // Only load in the background, keep the currently displayed one unchanged
  standbyVideo.src = clips[clipIndex].src
  // After the background's first frame is ready: play it, pause the currently displayed one, swap display
  standbyVideo.onloadeddata = () => {
    standbyVideo.play().catch(() => {})
    visibleVideo.pause()
    isAVisible.value = !isAVisible.value
  }
}

onMounted(() => {
  videoA.value!.src = clips[0].src
  videoA.value!.play().catch(() => {})
})

Key points:

Advantages of dual video

Comparing single video and dual video side by side, the benefits of double buffering mainly include the following.

1. The picture is not interrupted during switching

This is the most direct benefit. Single video switching inevitably goes through the gap of "old frame disappears → new frame appears", while with dual video the foreground always keeps the current frame until the new video's first frame is ready, and then swaps; the user doesn't see a black screen or overlay.

2. The old video can continue playing during the wait

With single video, the old video is cleared immediately upon switching. With dual video, while the background is loading, the foreground video continues playing normally, and only pauses the moment the new video is ready. The wait is nearly imperceptible to the user.

3. A slow load won't leave an empty frame

With a slow network, a new video may take several seconds before its first frame. With single video, the frame is empty during those seconds; with dual video, the old content continues to display, so the experience is "a little delayed" rather than "broken".

4. Low implementation cost

The entire solution only uses Vue's reactivity and the browser's native video element; no player library, no Canvas composition. The core logic is less than twenty lines, easy to understand and maintain.

5. Leaves room for extension

On the skeleton of "load in background, swap when ready", it's natural to extend further:

What it cannot solve

Double buffering optimizes "the experience at the moment of switching", but its boundaries should also be known:

Demo

The demo page is currently under maintenance, will be shown later...

Summary

The stutter when switching videos essentially comes from "one element having to both display the old frame and load the new resource". Double buffering splits these two tasks across two video elements: the foreground handles display, the background handles loading, and they swap after the first frame is ready. It doesn't change the load speed of the resource itself, but it stops that wait from appearing as an empty frame, trading a very small amount of code for a noticeably smoother switching experience.

Comments

Top 2 from juejin.cn, machine-translated. The original thread is authoritative.

Mh

Yes.

卷土的土

Keeping the old frame when loading fails is very practical.