Generate thumbnail from video using react custom hook

Generate thumbnail from video using react custom hook

·

3 min read

Hi there, Good day. Today I will share with you how to create a thumbnail from a video file using the react.js custom hook.

We all do video uploading while developing web applications. Usually, after uploading to the server, we take a cover photo of the video using a transcoder. It increases our server costs. But we can do that on the client side during video upload if we want. So let's see how to do that.

We will do this using ReactJS. First, we will create a custom hook file named useGenerateThumbnailFromVideo.ts. It will receive a video file from us and generate a thumbnail file using HTML's canvas and we'll preview that thumbnail. Here is the full code of the custom hook:

import { useState } from 'react';
import { toast } from 'react-toastify';

export interface IFileProcessing {
  fileName: string;
  thumbFile: any;
  originalFile: any;
}

const useGenerateThumbnailFromVideo = () => {
  const [processedFile, setProcessedFile] = useState<IFileProcessing | null>();
  const [isProcessing, setIsProcessing] = useState<boolean>(false);

  const startFileProcessing = async (videoFile: any) => {
    try {
      if (!videoFile?.name) throw new Error('Please select a valid video file.');

      setIsProcessing(true);

      let item: IFileProcessing = {
        fileName: videoFile?.name,
        thumbFile: null,
        originalFile: videoFile,
      };

      const thumbFile = await createVideoThumbnail(videoFile, 2.0);
      setProcessedFile({ ...item, thumbFile });
    } catch (error) {
      toast.error(`${error}`);
    } finally {
      setIsProcessing(false);
    }
  };

  return [(videoFile: any) => startFileProcessing(videoFile), processedFile, isProcessing] as const;
};

const createVideoThumbnail = async (file: File, seekTo = 0.0, quality = 0.75) => {
  return new Promise((resolve, reject) => {
    const player = document.createElement('video');
    player.setAttribute('src', URL.createObjectURL(file));

    player.load();
    player.addEventListener('error', (err: any) => reject(`${file?.name} is invalid video format.`));

    // load metadata of the video to get video duration and dimensions
    player.addEventListener('loadedmetadata', () => {
      // seek to user defined timestamp (in seconds) if possible
      if (player.duration < seekTo) {
        reject('The video is too short.');
        return;
      }

      // Delay seeking or else 'seeked' event won't fire on Safari
      setTimeout(() => {
        player.currentTime = seekTo;
      }, 500);

      // extract video thumbnail once seeking is complete
      player.addEventListener('seeked', () => {
        // define a canvas to have the same dimension as the video
        const videoCanvas = document.createElement('canvas');
        videoCanvas.width = player.videoWidth;
        videoCanvas.height = player.videoHeight;

        // draw the video frame to canvas
        const videoContext: any = videoCanvas.getContext('2d');
        videoContext.drawImage(player, 0, 0, videoCanvas.width, videoCanvas.height);

        // return the canvas image as a blob
        videoContext.canvas.toBlob((blob: any) => resolve(blob), 'image/jpeg', quality);
      });
    });
  });
};

export default useGenerateThumbnailFromVideo;

Now we will create the user interface, where we will take an input field and an image tag where we will display the thumb file. As soon as the input file is selected, our hook will be called and the hook will return to us a thumb file. Here is the full code of user interface:

import { useEffect, useState } from 'react';

import useGenerateThumbnailFromVideo, { IFileProcessing } from 'hooks/useGenerateThumbnailFromVideo';
import Spinner from 'components/Spinner';

const Feature = () => {
  const [file, setFile] = useState<IFileProcessing | null>(null);

  // File processing
  const [fileProcessingFn, processedFile, isProcessing] = useGenerateThumbnailFromVideo();
  useEffect(() => {
    if (!!processedFile?.thumbFile && !isProcessing) {
      setFile(processedFile);
    }
  }, [processedFile, isProcessing]);

  return (
    <div className="relative px-5 py-2">
      <div className="flex justify-center">
        <div className="w-[50vw] h-full">
          <h2 className="flex justify-between items-center font-semibold text-black mb-2">Select a video file</h2>
          <div className="flex w-full h-[75vh] justify-center items-start border border-dashed border-black text-white p-4">
            <div className="w-full">
              <input
                type="file"
                accept="video/*"
                onChange={(e: any) => fileProcessingFn(e.target.files[0])}
                className="text-black"
              />

              <div className="mt-4">
                {isProcessing && (
                  <div className="flex justify-start items-center space-x-2 text-black">
                    <Spinner className="w-6 h-6" /> <span>Processing...</span>
                  </div>
                )}

                {!!file && !isProcessing && (
                  <div className="flex justify-start items-start space-x-4">
                    <img src={URL.createObjectURL(file?.thumbFile)} alt="thumbnail" className="w-auto h-auto" />
                  </div>
                )}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Feature;

Now, run the project using the following command and see the output.

npm run start

Download full source code from GitHub