<div class="Wrapper">
  <h1>React - Simple video buffering loader</h1>
  <p>
    Exampe of showing a buffering loader in React. Click on the button below to jump to random time to make the video buffer. You can also use network throtling in your browser's develop tools to force buffering. I also wrote a short <a href="https://muffinman.io/react-simple-video-buffering-loader/" target="_parent">blog post</a> about it.
  </p>
  <div id="app"></div>
</div>
$button-size: 60px;
$blue: #3498db;

@keyframes spin {
  from { transform: rotate(0deg) }
  to { transform: rotate(360deg) }
}

*, *:after, *:before {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: Helvetica, Arial, sans-serif;
  padding: 22px;
}

h1 {
  font-size: 20px;
  line-height: 1.2em;
  margin-bottom: 20px;
  color: #111;
  font-weight: normal;
}

p {
  margin-bottom: 20px;
  line-height: 1.4em;
  max-width: 400px;
  color: #888;
}

a {
  color: #333;
  text-decoration: none;
  border-bottom: 1px solid #ddd;
  transition: 300ms all;
  outline: none;
  
  &:focus,
  &:hover {
    color: $blue;
    border-color: #bbb;
  }
}

.Wrapper {
  max-width: 800px;
  margin: 0 auto;
  position: relative;
}

.SimpleVideo {
  position: relative;
}

.SimpleVideo-video {
  width: 100%;
  display: block;
}

.SimpleVideo-playPause {
  background: rgba(0, 0, 0, 0.5);
  border-radius: 50%;
  border: none;
  bottom: 20px;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
  color: white;
  font-size: 12px;
  height: $button-size;
  left: 20px;
  line-height: 1;
  position: absolute;
  width: $button-size;
  transition: all 300ms;
  outline: none;
  cursor: pointer;
  
  &:hover {
    background: rgba(0, 0, 0, 0.8);
  }
  
  &:active {
    transform: translateY(1px);
  }
  
  &:focus {
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8);
  }
}

.SimpleVideo-loader {
  animation: spin 1s linear infinite;
  border-radius: 50%;
  border: $button-size * 0.05 solid rgba(255, 255, 255, 0.8);
  border-left-color: transparent;
  color: transparent;
  content: '';
  display: block;
  height: $button-size * 0.9;
  left: $button-size * 0.05;
  position: absolute;
  top: $button-size * 0.05;
  width: $button-size * 0.9;
}


.SimpleVideo-jump {
  margin: 40px 0 20px;
  display: block;
  border: 1px solid #ddd;
  background: #fff;
  font-size: 16px;
  border-radius: 50px;
  padding: 8px 22px;
  transition: all 300ms;
  outline: none;

  &:focus {
    box-shadow: 0 0 0 2px #ddd;
  }
  
  &:active {
    transform: translateY(1px);
  }

  &:hover {
    color: $blue;
    cursor: pointer;
  }
}
View Compiled
const { Component, useEffect, useState, useRef } = React;
// You probably want to use modules in the real app
// import React, { useEffect, useState, useRef } from 'react';

const PLAYING_DEBOUNCE_TIME = 50;
const WAITING_DEBOUNCE_TIME = 200;

const Video = ({ src, ...props }) => {
  const [isPlaying, setIsPlaying] = useState(false);
  const [isWaiting, setIsWaiting] = useState(false);

  const isWaitingTimeout = useRef(null);
  const isPlayingTimeout = useRef(null);

  const videoElementRef = useRef();

  useEffect(() => {
    if (!videoElementRef.current) {
      return;
    }

    const waitingHandler = () => {
      clearTimeout(isWaitingTimeout.current);

      isWaitingTimeout.current = setTimeout(() => {
        setIsWaiting(true);
      }, WAITING_DEBOUNCE_TIME);
    };

    const playHandler = () => {
      clearTimeout(isWaitingTimeout.current);
      clearTimeout(isPlayingTimeout.current);

      isPlayingTimeout.current = setTimeout(() => {
        setIsPlaying(true);
        setIsWaiting(false);
      }, PLAYING_DEBOUNCE_TIME);
    };

    const pauseHandler = () => {
      clearTimeout(isWaitingTimeout.current);
      clearTimeout(isPlayingTimeout.current);

      isPlayingTimeout.current = setTimeout(() => {
        setIsPlaying(false);
        setIsWaiting(false);
      }, PLAYING_DEBOUNCE_TIME);
    };

    const element = videoElementRef.current;

    element.addEventListener("waiting", waitingHandler);
    element.addEventListener("play", playHandler);
    element.addEventListener("playing", playHandler);
    element.addEventListener("pause", pauseHandler);

    // clean up
    return () => {
      clearTimeout(isWaitingTimeout.current);
      clearTimeout(isPlayingTimeout.current);

      element.removeEventListener("waiting", waitingHandler);
      element.removeEventListener("play", playHandler);
      element.removeEventListener("playing", playHandler);
      element.removeEventListener("pause", pauseHandler);
    };
  }, [videoElementRef]);

  const handlePlayPauseClick = () => {
    if (videoElementRef.current) {
      if (isPlaying) {
        videoElementRef.current.pause();
      } else {
        videoElementRef.current.play();
      }
    }
  };

  return (
    <div className="SimpleVideo">
      {/* demo jump button, you want to remove this block in the real app */}
      <button 
        onClick={() => {
          const video = videoElementRef.current;
          if (video) {
            video.currentTime = video.duration * Math.random();
            !isPlaying && video.play();
          }
        }} 
        className="SimpleVideo-jump"  
      >
        Jump to the random time
      </button>
      {/* end of demo jump button */}
      
      <video {...props} ref={videoElementRef} src={src} className="SimpleVideo-video" />
      <button onClick={handlePlayPauseClick} className="SimpleVideo-playPause">
        {isPlaying ? "Pause" : "Play"}
        {isWaiting && <span className="SimpleVideo-loader">Buffering</span>}
      </button> 
    </div>
  );
};

ReactDOM.render(
  <Video muted src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" />,
  document.getElementById("app")
);
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/react/umd/react.development.js
  2. https://unpkg.com/react-dom/umd/react-dom.development.js