🍿

Youtube's Ambient Mode

ReactTailwindcssHooksCanvas API

Recreating Youtube's ambient mode feature using React & Canvas API

Introduction

Youtube recently launched a new feature called Ambient Mode, it uses a lighting effect to make watching videos in Dark theme more immersive, by casting gentle colors from the video, into your screen’s background.

Youtube has made the switch to a darker shade of black for its dark theme in order to optimize the Ambient Mode effect for viewers.

open movie - Spring by blender studios.

How it works

  1. A canvas element is created and appended to the DOM
  2. The canvas element is resized to the size of the video and placed behind the video.
  3. The video is drawn on the canvas
  4. The canvas is blurred

Basic component setup

AmbientVideo.tsx
// AmbientVideo.tsx
import React, { useState, useRef } from 'react'
 
const AmbientVideo = () => {
  const [isPlaying, setIsplaying] = useState(false)
  const videoRef = useRef(null)
  const canvasRef = useRef(null)
  const ctx = canvasRef.current?.getContext('2d')
 
  return (
    <div id='cinematics' className='aspect-video relative inline-block '>
      <canvas
        ref={canvasRef}
        id='canvas'
        style={{
          width: `${videoRef.current?.clientWidth}px`,
          height: `${videoRef.current?.clientHeight}px`,
        }}
        className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 scale-110 opacity-80 blur-[40px]'
      ></canvas>
      <video
        poster='/assets/spring-poster.jpg'
        preload='metadata'
        ref={videoRef}
        id='video'
        src='/assets/spring.mp4'
        className='relative h-full w-auto rounded-lg object-cover focus:outline-none'
        controls
      ></video>
    </div>
  )
}

Drawing the video on the canvas

useInterval hook by Dan Abramov is used to efficiently draw the video on to canvas. Read more about useInterval hook

//UseInterval hook
import React, { useState, useEffect, useRef } from 'react'
 
function useInterval(callback, delay) {
  const savedCallback = useRef()
 
  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])
 
  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current()
    }
    if (delay !== null) {
      let id = setInterval(tick, delay)
      return () => clearInterval(id)
    }
  }, [delay])
}
// AmbientVideo.tsx
React.useEffect(() => {
  const videoInstance = videoRef.current
  // Add event listeners to the video element to update the isPlaying state, so that the canvas is only drawn when the video is playing.
  videoInstance?.addEventListener('play', () => setIsplaying(true))
  videoInstance?.addEventListener('ended', () => setIsplaying(false))
  videoInstance?.addEventListener('pause', () => setIsplaying(false))
 
  return () => {
    videoInstance?.removeEventListener('play', () => setIsplaying(true))
    videoInstance?.removeEventListener('ended', () => setIsplaying(false))
    videoInstance?.removeEventListener('pause', () => setIsplaying(false))
  }
}, [])
 
// useInterval hook
useInterval(
  () => {
    if (!videoRef.current || !canvasRef.current) {
      return
    }
    ctx?.drawImage(videoRef.current, 0, 0, canvasRef.current?.width, canvasRef.current?.height)
  },
  isPlaying ? 1000 / 15 : null // runs at 15fps when video is playing, and stops when video is paused
)

Stackblitz demo

Here is a demo of the component in action, you can play around with the code in the editor.

Next Steps

Goal of this project was just to implement a basic version of the feature, there are a lot of improvements that can be made to make it more performant and user friendly. Here are some ideas that I have in mind.

  • Youtube generates ambient mode effects less frequently, as drawing onto canvas is a very expensive operation, they might even store the generated ambient mode images in the database and serve them when needed.
  • Add a button to toggle the ambient mode on and off
  • Adjust the canvas size on window resize

Share your thoughts or questions with me on my social media handles, I would love to hear from you.