Jak vytvořit přehrávač videa v Reactu

Jedna z věcí, která mě v poslední době zaujala, je vytvoření plně přizpůsobeného přehrávače videa. Je zřejmé, že v současné době máme služby, které poskytují widgety, které lze použít na našich webových stránkách.

Nebo na druhou stranu již máte závislosti, které můžete nainstalovat a začít používat. Tato zařízení však mají svou cenu, kterou je v tomto případě absence nebo obtížnost přizpůsobení.

Proto mě napadlo vytvořit si vlastní videopřehrávač a zjevně to není tak těžké, jak jsem si myslel, a nakonec mě to bavilo.

Přesně z tohoto důvodu mě napadlo napsat tento článek, abych krok za krokem vysvětlil, jak vytvořit jednoduchý přehrávač videa, ale se stejnou logikou můžete jít mnohem dál.

V dnešním příkladu použijeme toto video, má zvuk a je zcela zdarma.

Pojďme kódovat

Dnes nebudeme používat žádné externí závislosti, takže budete se vším plně obeznámeni.

Pokud jde o styling, na konci uvedu kód CSS, protože cílem tohoto článku je naučit logiku fungování přehrávače videa.

První věc, o kterou vás požádám, je stáhnout si výše uvedené video a poté soubor přejmenovat na video.mp4 . Nakonec vytvořte v projektu složku s názvem assets a přetáhněte soubor do této složky.

Abychom neměli kód v jediném souboru, vytvořme si vlastní háček, který bude mít na starosti ovládání celého chodu našeho videopřehrávače.

// @src/hooks/useVideoPlayer.js

const useVideoPlayer = () => {
  // ...
};

export default useVideoPlayer;

V našem háku budeme používat pouze dva háky React, useState() a useEffect() .

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = () => {
  // ...
};

export default useVideoPlayer;

Nyní můžeme začít vytvářet náš stav, který budeme nazývat playerState . Tento náš stav bude mít čtyři vlastnosti, isPlaying, isMuted, progress a speed.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = () => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });
  // ...
};

export default useVideoPlayer;

Jednu věc, kterou chci, abyste měli na paměti, je, že náš hák musí přijmout jediný argument, kterým v tomto případě bude odkaz na naše video, které pojmenujeme videoElement.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });
  // ...
};

export default useVideoPlayer;

Nyní můžeme vytvořit naši funkci, která bude diktovat, zda je přehrávač pozastaven nebo ne. Za tímto účelem zachováme hodnoty všech ostatních vlastností našeho playerState a pouze řekneme, že kdykoli je funkce spuštěna, poskytuje inverzní hodnotu aktuálního stavu isPlaying.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };
  // ...
};

export default useVideoPlayer;

Nyní musíme použít useEffect() pozastavit nebo nepozastavit video prostřednictvím hodnoty vlastnosti isPlaying.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);
  // ...
};

export default useVideoPlayer;

Nyní musíme vytvořit funkci, která nám pomůže zjistit průběh videa, tj. podle délky trvání videa chceme, aby ukazatel průběhu ukazoval, kolik z videa jsme viděli.

K tomu vytvoříme funkci nazvanou handleOnTimeUpdate() abychom mohli spočítat, kolik jsme toho videa viděli, s tím, co ještě zbývá vidět. Poté zachováme hodnoty všech ostatních vlastností našeho stavu a aktualizujeme pouze hodnotu progress.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };
  // ...
};

export default useVideoPlayer;

Jedna z věcí, kterou budeme chtít implementovat, je možnost, že můžeme přetáhnout ukazatel průběhu, abychom si mohli vybrat, kde chceme video sledovat.

Tímto způsobem vytvoříme funkci s názvem handleVideoProgress() který bude mít jediný argument, kterým v tomto případě bude událost.

Potom převedeme naši hodnotu události z řetězce na číslo. Je to proto, že pak chceme našemu videoElementu přímo sdělit, že aktuální doba sledování se rovná hodnotě naší ruční změny. Nakonec pouze zachováme hodnoty všech ostatních vlastností našeho stavu a aktualizujeme pouze průběh.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };
  // ...
};

export default useVideoPlayer;

Další funkcí, kterou budeme chtít implementovat, je rychlost přehrávání videa, protože věřím, že ne každý je 1,0násobným fanouškem a že existují lidé, kteří sledují videa rychlostí 1,25x.

K tomu vytvoříme funkci nazvanou handleVideoSpeed() který obdrží událost jako jeden argument, pak se hodnota této události převede na číslo a nakonec sdělíme prvku videoElement, že rychlost přehrávání se rovná hodnotě události.

V našem stavu zachováváme hodnoty všech vlastností kromě rychlosti.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };

  const handleVideoSpeed = (event) => {
    const speed = Number(event.target.value);
    videoElement.current.playbackRate = speed;
    setPlayerState({
      ...playerState,
      speed,
    });
  };
  // ...
};

export default useVideoPlayer;

Poslední funkcí, kterou chci přidat, je možnost ztlumit a zapnout video. A jak byste měli vypočítat logiku, je velmi podobné přehrávání/pauze.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };

  const handleVideoSpeed = (event) => {
    const speed = Number(event.target.value);
    videoElement.current.playbackRate = speed;
    setPlayerState({
      ...playerState,
      speed,
    });
  };

  const toggleMute = () => {
    setPlayerState({
      ...playerState,
      isMuted: !playerState.isMuted,
    });
  };

  useEffect(() => {
    playerState.isMuted
      ? (videoElement.current.muted = true)
      : (videoElement.current.muted = false);
  }, [playerState.isMuted, videoElement]);
  // ...
};

export default useVideoPlayer;

Nakonec stačí vrátit náš stav a všechny funkce, které byly vytvořeny.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };

  const handleVideoSpeed = (event) => {
    const speed = Number(event.target.value);
    videoElement.current.playbackRate = speed;
    setPlayerState({
      ...playerState,
      speed,
    });
  };

  const toggleMute = () => {
    setPlayerState({
      ...playerState,
      isMuted: !playerState.isMuted,
    });
  };

  useEffect(() => {
    playerState.isMuted
      ? (videoElement.current.muted = true)
      : (videoElement.current.muted = false);
  }, [playerState.isMuted, videoElement]);

  return {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  };
};

export default useVideoPlayer;

Nyní můžeme začít pracovat na našem App.jsx komponenta a pro záznam použitá knihovna ikon byla Boxicons a typografie byla DM Sans.

Nejprve dám css kód našeho App.css .

body {
  background: #EEEEEE;
}

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

h1 {
  color: white;
}

video {
  width: 100%;
}

.video-wrapper {
  width: 100%;
  max-width: 700px;
  position: relative;
  display: flex;
  justify-content: center;
  overflow: hidden;
  border-radius: 10px;
}

.video-wrapper:hover .controls {
  transform: translateY(0%);
}

.controls {
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  position: absolute;
  bottom: 30px;
  padding: 14px;
  width: 100%;
  max-width: 500px;
  flex-wrap: wrap;
  background: rgba(255, 255, 255, 0.25);
  box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  transform: translateY(150%);
  transition: all 0.3s ease-in-out;
}

.actions button {
  background: none;
  border: none;
  outline: none;
  cursor: pointer;
}

.actions button i {
  background-color: none;
  color: white;
  font-size: 30px;
}

input[type="range"] {
  -webkit-appearance: none !important;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 20px;
  height: 4px;
  width: 350px;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none !important;
  cursor: pointer;
  height: 6px;
}

input[type="range"]::-moz-range-progress {
  background: white;
}

.velocity {
  appearance: none;
  background: none;
  color: white;
  outline: none;
  border: none;
  text-align: center;
  font-size: 16px;
}

.mute-btn {
  background: none;
  border: none;
  outline: none;
  cursor: pointer;
}

.mute-btn i {
  background-color: none;
  color: white;
  font-size: 20px;
}

Nyní můžeme začít pracovat na naší komponentě a k tomu importujeme vše, co potřebujeme, v tomto případě je to náš styling, naše video a náš háček.

// @src/App.jsx

import React from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  // ...
};

export default App;

Poté naimportujeme useRef() háček k vytvoření reference našeho videoElementu. Takhle:

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  // ...
};

export default App;

Pak můžeme získat náš stav hráče a každou z našich funkcí z našeho háku. Takhle:

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  // ...
};

export default App;

Nyní můžeme konečně začít pracovat na naší šabloně, tímto způsobem začneme pracovat na našem video prvku, který bude mít tři rekvizity, zdrojem bude naše video a stále budeme předávat naši referenci a naše handleOnTimeUpdate() funkce.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        // ...
      </div>
    </div>
  );
};

export default App;

Nyní můžeme začít pracovat na ovládání videa, začněme tlačítkem pro přehrávání a pozastavení. Kterému předáme togglePlay() a provedeme podmíněné vykreslení tak, aby zobrazovalo naznačené ikony podle hodnoty vlastnosti isPlaying.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          // ...
        </div>
      </div>
    </div>
  );
};

export default App;

Nyní můžeme začít prací na našem vstupu, který bude typu range, který bude mít minimální hodnotu nula a maximální hodnotu sto. Stejným způsobem předáme handleVideoProgress() funkce a hodnota vlastnosti progress.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          <input
            type="range"
            min="0"
            max="100"
            value={playerState.progress}
            onChange={(e) => handleVideoProgress(e)}
          />
          // ...
        </div>
      </div>
    </div>
  );
};

export default App;

Nyní budeme pracovat na prvku, abychom vybrali rychlost přehrávání videa. Kterým předáme hodnotu vlastnosti speed a handleVideoSpeed() funkce.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          <input
            type="range"
            min="0"
            max="100"
            value={playerState.progress}
            onChange={(e) => handleVideoProgress(e)}
          />
          <select
            className="velocity"
            value={playerState.speed}
            onChange={(e) => handleVideoSpeed(e)}
          >
            <option value="0.50">0.50x</option>
            <option value="1">1x</option>
            <option value="1.25">1.25x</option>
            <option value="2">2x</option>
          </select>
          // ...
        </div>
      </div>
    </div>
  );
};

export default App;

V neposlední řadě budeme mít tlačítko, které bude zodpovídat za ztlumení a zapnutí zvuku videa. Kterému předáme toggleMute() a provedeme podmíněné vykreslení, abychom zobrazili naznačené ikony podle vlastnosti isMuted.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          <input
            type="range"
            min="0"
            max="100"
            value={playerState.progress}
            onChange={(e) => handleVideoProgress(e)}
          />
          <select
            className="velocity"
            value={playerState.speed}
            onChange={(e) => handleVideoSpeed(e)}
          >
            <option value="0.50">0.50x</option>
            <option value="1">1x</option>
            <option value="1.25">1.25x</option>
            <option value="2">2x</option>
          </select>
          <button className="mute-btn" onClick={toggleMute}>
            {!playerState.isMuted ? (
              <i className="bx bxs-volume-full"></i>
            ) : (
              <i className="bx bxs-volume-mute"></i>
            )}
          </button>
        </div>
      </div>
    </div>
  );
};

export default App;

Konečný výsledek by měl vypadat takto:

Závěr

Jako vždy doufám, že vás to zaujalo. Pokud jste si v tomto článku všimli nějaké chyby, uveďte je prosím v komentářích. 🥳

Přeji vám hezký den! 🙌