Fresh v2 で video.js を動かす

Fresh v2が公開になりました。
github - denoland/fresh - 2.0.0

Fresh v2ではViteが使われており、今までとは趣が変わってきています。
そんな中、Fresh 1と同じ方法ではvideo.jsを動かすことができず、動くようにするまで少し手間取ったので、備忘録として残しておきます。

参考

実装

実行環境

1
2
$  deno -V
deno 2.5.0

Fresh 1

一旦Fresh 1で動かしてみます。

video.js を使用する側のコンポーネント。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// islands/NpmVideoVideoPlayer.tsx
import { useEffect, useRef } from "preact/hooks";
import { IS_BROWSER } from "$fresh/runtime.ts";
import videojs from "video.js";

// Video.jsのプレイヤー型定義
interface VideoJsPlayer {
dispose(): void;
}

interface VideoJsOptions {
autoplay: boolean;
controls: boolean;
preload: string;
fluid: boolean;
sources: Array<{
src: string;
type: string;
}>;
poster?: string;
}

type VideoJsPlayerProps = {
src: string; // m3u8 など
width?: number;
height?: number;
autoPlay?: boolean;
poster?: string;
controls?: boolean;
};

export default function NpmVideoVideoPlayer({
src,
width = 640,
height = 360,
autoPlay = false,
poster,
controls = true,
}: VideoJsPlayerProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const playerRef = useRef<VideoJsPlayer | null>(null);

// videojs初期化
useEffect(() => {
if (!IS_BROWSER || !videoRef.current) return;

if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}

playerRef.current = videojs(videoRef.current, {
autoplay: autoPlay,
controls,
preload: "auto",
fluid: false,
sources: [
{
src,
type: "application/x-mpegURL",
},
],
poster,
});

return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [src, autoPlay, controls, poster]);

if (!IS_BROWSER) {
return (
<div style={{ textAlign: 'center', padding: '2em' }}>
Loading...
</div>
);
}

return (
<div>
<link
href="https://vjs.zencdn.net/8.12.0/video-js.css"
rel="stylesheet"
/>
<video
ref={videoRef}
class="video-js vjs-default-skin"
width={width}
height={height}
playsInline
poster={poster}
/>
</div>
);
}

呼び出し側のコンポーネント。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// routes/index.tsx
import NpmVideoVideoPlayer from "../islands/NpmVideoVideoPlayer.tsx";

export default function Home() {
return (
<div class="px-4 py-8 mx-auto bg-[#86efac]">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<NpmVideoVideoPlayer
src="http://localhost:8000/output.m3u8"
width={400}
controls={true}
autoPlay={true}
/>
</div>
</div>
);
}

動かすと、以下のように動きます。

npm から取得した video.js を素直に動かせました。

Fresh 2

Fresh 2では、npm から読み込んだものを使うコンポーネントを呼び出すと以下のようにエラーになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deno task dev
Task dev vite

VITE v7.1.5 ready in 1431 ms

➜ Local: http://127.0.0.1:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
15:27:46 [vite] (ssr) Error when evaluating SSR module fresh:server_entry: module is not defined

# 省略

info: Deno supports CommonJS modules in .cjs files, or when the closest
package.json has a "type": "commonjs" option.
hint: Rewrite this module to ESM,
or change the file extension to .cjs,
or add package.json next to the file with "type": "commonjs" option,
or pass --unstable-detect-cjs flag to detect CommonJS when loading.
docs: https://docs.deno.com/go/commonjs

参考情報の記載もあるのですが、一通り試してもうまくいきませんでした。

読み込みは、Fresh v2と同様です。

1
import videojs from "video.js";

video.js パッケージには、video.js/dist/video.es.js もあってさもESMであるかのように見えますがこちらも同様のエラーになります。
対応策として video.js のロードをコンポーネントで行い。
globalThisに紐づいた状態を検知した上で video.js を初期化するようにしました。
以下対応版です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// islands/GlobalThisVideoVideoPlayer.tsx
import { useEffect, useRef, useState } from "preact/hooks";
import { IS_BROWSER } from "fresh/runtime";
import { Head } from "fresh/runtime";

// Video.jsのプレイヤー型定義
interface VideoJsPlayer {
dispose(): void;
}

interface VideoJsOptions {
autoplay: boolean;
controls: boolean;
preload: string;
fluid: boolean;
sources: Array<{
src: string;
type: string;
}>;
poster?: string;
}

type VideoJsPlayerProps = {
src: string; // m3u8 など
width?: number;
height?: number;
autoPlay?: boolean;
poster?: string;
controls?: boolean;
};

export default function GlobalThisVideoVideoPlayer({
src,
width = 640,
height = 360,
autoPlay = false,
poster,
controls = true,
}: VideoJsPlayerProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const playerRef = useRef<VideoJsPlayer | null>(null);
const [videojsReady, setVideojsReady] = useState(false);

// videojsのグローバルロード待機
useEffect(() => {
if (!IS_BROWSER) return;

const checkVideojs = () => (globalThis as Record<string, unknown>).videojs;

if (checkVideojs()) {
setVideojsReady(true);
return;
}

const id = setInterval(() => {
if (checkVideojs()) {
setVideojsReady(true);
clearInterval(id);
}
}, 300);
return () => clearInterval(id);
}, []);

useEffect(() => {
if (!IS_BROWSER || !videojsReady || !videoRef.current) return;

if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}

const videojs = (globalThis as Record<string, unknown>).videojs as (
element: HTMLVideoElement,
options: VideoJsOptions
) => VideoJsPlayer;

playerRef.current = videojs(videoRef.current, {
autoplay: autoPlay,
controls,
preload: "auto",
fluid: false,
sources: [
{
src,
type: "application/x-mpegURL",
},
],
poster,
});

return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [src, autoPlay, controls, poster, videojsReady]);

if (!IS_BROWSER && videojsReady) {
return (
<div style={{ textAlign: 'center', padding: '2em' }}>
Loading...
</div>
);
}

return (
<div>
<Head>
<script src="https://vjs.zencdn.net/8.12.0/video.min.js"></script>
<link
href="https://vjs.zencdn.net/8.12.0/video-js.css"
rel="stylesheet"
/>
</Head>
<video
ref={videoRef}
class="video-js vjs-default-skin"
width={width}
height={height}
playsInline
poster={poster}
/>
</div>
);
}

呼び出し側のコンポーネントは、Fresh v2と同様です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { define } from "../utils.ts";
import GlobalThisVideoVideoPlayer from "../islands/GlobalThisVideoVideoPlayer.tsx";

export default define.page(function Home(ctx) {

return (
<div class="px-4 py-8 mx-auto fresh-gradient min-h-screen">
<GlobalThisVideoVideoPlayer
src="http://localhost:5173/output.m3u8"
width={400}
controls={true}
autoPlay={true}
/>
</div>
);
});

動かすと、以下のように動きます。

読み込みの速度だけでいうと、fresh 2版の方が動きがいいです。

Fresh v2 でならできていたことが、Fresh v2でできなくなったので、回避方法を探しました。

では。