As part of our Mux.com refresh, we wanted to demo our API experience via a React-based animation. At the end of it we wanted to show a video playing across multiple devices, which starts to get into weirder territory than you might expect.
It'd be easy to jump to using multiple video elements across the devices. On top of loading the same video multiple times (and the bandwidth that entails), synchronizing the playback gets problematic. Starting them all at the same time is a good start, but what if any of the players is slow to start or rebuffers at any point?
Instead, we decided to keep playing with canvas
. We made a React component that plays video in a <video>
tag...but actually never displays that video. Instead, it distributes that video content to the array of canvas refs
passed to it.
function CanvasPlayer (props) {
const player = useRef(null);
const canvases = props.canvases.map((c) => {
const canvas = c.current;
const ctx = canvas.getContext('2d');
return [canvas, ctx];
});
const updateCanvases = () => {
// If the player is empty, we probably reset!
// In that case, let's clear out the canvases
if (!player.current) {
canvases.map(([canvas, ctx]) => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
});
}
// I don't know how we'd get to this point without
// player being defined, but... yeah. Here we check
// to see if the video is actually playing before
// continuing to paint to the canvases
if (!player.current || player.current.paused || player.current.ended) {
return;
}
// Paint! Map over each canvas and draw what's currently
// in the video element.
canvases.map(([canvas, ctx]) => {
ctx.drawImage(player.current, 0, 0, canvas.width, canvas.height));
}
// Loop that thing.
window.requestAnimationFrame(updateCanvases);
};
// Fired whenever the video element starts playing
const onPlay = () => {
updateCanvases();
};
useEffect(() => {
// We're using HLS, so this is just to make sure the player
// can support it. This isn't necessary if you're just using
// an mp4 or something.
let hls;
if (player.current.canPlayType('application/vnd.apple.mpegurl')) {
player.current.src = props.src;
player.current.addEventListener('loadedmetadata', () => {
player.current.play();
});
} else {
hls = new Hls();
hls.loadSource(props.src);
hls.attachMedia(player.current);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
player.current.play();
});
}
return () => hls && hls.destroy();
}, []);
/* eslint-disable jsx-a11y/media-has-caption */
return <video style={{ display: 'none' }} ref={player} onPlay={onPlay} {...props} />;
}
All the magic is in the updateCanvases
function. While the video is playing it maps over each canvas ref and draws whatever is in the video tag to it.
How it ends up looking
function FunComponent(props) {
const canvasOne = useRef(null);
const canvasTwo = useRef(null);
return (
<div>
<SomeComponent>
<canvas ref={canvasOne} />
</SomeComponent>
<OtherComponent>
<canvas ref={canvasTwo} />
</OtherComponent>
<CanvasPlayer
src={`https://stream.mux.com/${props.playbackID}.m3u8`}
muted
canvases={[canvasOne, canvasTwo]}
loop
/>
</div>
)
}
The CanvasPlayer
won't actually play anything itself, but it'll distribute the video image around to each of the refs passed to it. This means you could sprinkle a video all around a page if you want but only have to download it once!
Top comments (0)