React で video.js を使ってみたときの作業メモです。バージョンは以下の通りです。
"video.js": "^7.11.8",
"videojs-errors": "^4.5.0",
"@types/video.js": "^7.3.27",
"@types/videojs-errors": "^4.5.0"
ひな形
Video.js and ReactJS integration でひな形をつくります。
コントロールバーのカスタマイズ
コントロールバーの各種ボタン(再生ボタンや全画面表示ボタン)の表示/非表示を実現する素朴な方法です。
まずはそれぞれの名前を調べます。player.controlBar.children()
で一覧表示すると便利です。
console.log("player.controlBar.children()::", player.controlBar.children());
独自のボタンも作れます。
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
const Button = videojs.getComponent("Button");
// Bootstrap Icons あたりから取得しました
const iconSVG = `<div style="cursor: pointer"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-square" viewBox="0 0 16 16">
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
</svg></div>`;
interface MyButtonOption extends VideoJsPlayerOptions {
onClick: () => void;
}
export default class MyButton extends Button {
constructor(player: VideoJsPlayer, options: MyButtonOption) {
super(player, options);
this.controlText("My Button");
this.el().innerHTML = iconSVG;
}
handleClick() {
const player = this.player();
player.pause(); // など、このボタンでやりたいことを書く。
}
createEl(): HTMLButtonElement {
return super.createEl("button", {
className: "vjs-custom",
});
}
}
独自のボタンは、videojs.registerComponent で登録する必要があります。
React.useEffect(() => {
// make sure Video.js player is only initialized once
if (!playerRef.current) {
const videoElement = videoRef.current;
if (!videoElement) return;
videojs.registerComponent("Myutton", MyButton); // これです
その後、デフォルトのコントロールバーのコンポーネントをいったん削除し、表示したい順で追加します。
const pictureInPictureToggle = player.controlBar.getChild("PictureInPictureToggle");
const fullscreenToggle = player.controlBar.getChild("FullscreenToggle");
const currentTimeDisplay = player.controlBar.getChild("CurrentTimeDisplay");
const volumePanel = player.controlBar.getChild("VolumePanel");
const durationDisplay = player.controlBar.getChild("DurationDisplay");
const progressControl = player.controlBar.getChild("ProgressControl");
const remainingTimeDisplay = player.controlBar.getChild("RemainingTimeDisplay");
player.controlBar.removeChild(volumePanel);
player.controlBar.removeChild(durationDisplay);
player.controlBar.removeChild(progressControl);
player.controlBar.removeChild(pictureInPictureToggle);
player.controlBar.removeChild(fullscreenToggle);
player.controlBar.removeChild(currentTimeDisplay);
player.controlBar.removeChild(remainingTimeDisplay);
player.controlBar.addChild("MyButton");
player.controlBar.addChild("VolumePanel");
player.controlBar.addChild("DurationDisplay");
player.controlBar.addChild("ProgressControl");
player.controlBar.addChild("CurrentTimeDisplay");
player.controlBar.addChild("FullscreenToggle");
もっと高度なカスタマイズは、公式ドキュメント や、個人ブログ How to merge Video.js buttons into one が参考になるはずです。
TypeError: Cannot read properties of null (reading 'currentTime') 対策
CRA で試しているときは問題なかったのですが、大きめのアプリに組み込むと TypeError: Cannot read properties of null (reading 'currentTime')
というエラーが発生し、悩まされました。
後処理に player.controlBar.progressControl.seekBar.dispose()
を追加することで、解消しました。
useEffect(() => {
const player = playerRef.current;
return () => {
if (player) {
player.dispose();
player.controlBar.progressControl.seekBar.dispose(); // TypeError: Cannot read properties of null (reading 'currentTime') 対策
playerRef.current = null;
}
};
}, [playerRef]);
異常系の実装
video.js の MediaError だけでは、補足できないエラーがあります。videojs-errors というプラグインが便利でした。videojs() の第3引数のコールバック内で設定を行う必要があります。
const player: AppPlayer = (playerRef.current = videojs(
videoElement,
options,
() => {
player.errors(); // videojs-errors の有効化
player.errors.timeout(10 * 1000); // タイムアウトの設定を追加
onReady && onReady(player);
}
));
ただ、VideoJsPlayer の @types/videojs-errors による拡張がうまくいっていないようでした。player.errors() の errors() が生えません。以下のような残念な対応で乗り切りました。
// import * as errors from "videojs-errors";
interface AppPlayer extends VideoJsPlayer {
//errors: typeof errors; // これをワークアラウンドとしたかったが、errors() がない。
errors: any;
}
処理自体は player の error イベントのリスナに書いてゆきます。
player.on("error", (e) => {
console.log("player.error()::", player.error());
});
タイムアウトについては、他に player.tech
の retryplaylist
のリスナとして自前実装する案もありましたが、採用しませんでした。
player.on("loadedmetadata", () => {
let retries = 0;
player
.tech({ IWillNotUseThisInPlugins: true })
.on("retryplaylist", (e) => {
retries++;
if (retries >= 5) {
e.stopImmediatePropagation();
player.dispose()
}
});
});
開発環境構築とデータの準備
フリー動画。
このへんを参考に手元のライブストリーミング環境をつくりました。
次のようなデータもみつけました。
- Free HLS m3u8 URLs for Testing HLS Players [Updated]
- Free MPEG-DASH MPD (manifest) Examples for Testing
ご参考。
- ライブ版 HLS(application/x-mpegURL): https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8
- ライブ版 DASH(application/dash+xml): https://livesim.dashif.org/livesim/chunkdur_1/ato_7/testpic4_8s/Manifest.mpd
- VOD 版 HLS(application/x-mpegURL): https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8
- VOD 版 DASH(application/dash+xml): https://s3.amazonaws.com/_bc_dml/example-content/sintel_dash/sintel_vod.mpd