diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 5f21d74e4..be0798daa 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -42,9 +42,11 @@ const nextConfig: NextConfig = { headers: [ { key: "Content-Security-Policy", + // worker-src / media-src with blob: is necessary for video.js, see https://github.com/homarr-labs/homarr/issues/3912 and https://stackoverflow.com/questions/65792855/problem-with-video-js-and-content-security-policy-csp value: ` default-src 'self'; script-src * 'unsafe-inline' 'unsafe-eval'; + worker-src * blob:; base-uri 'self'; connect-src *; style-src * 'unsafe-inline'; @@ -53,7 +55,7 @@ const nextConfig: NextConfig = { form-action 'self'; img-src * data:; font-src * data:; - media-src * data:; + media-src * data: blob:; ` .replace(/\s{2,}/g, " ") .trim(), diff --git a/packages/widgets/src/video/component.tsx b/packages/widgets/src/video/component.tsx index 3527246db..68038744e 100644 --- a/packages/widgets/src/video/component.tsx +++ b/packages/widgets/src/video/component.tsx @@ -1,9 +1,8 @@ "use client"; import { useEffect, useRef } from "react"; -import { Anchor, Center, Group, Stack, Title } from "@mantine/core"; +import { Anchor, Box, Center, Group, Stack, Title } from "@mantine/core"; import { IconBrandYoutube, IconDeviceCctvOff } from "@tabler/icons-react"; -import combineClasses from "clsx"; import videojs from "video.js"; import { useI18n } from "@homarr/translation/client"; @@ -13,6 +12,8 @@ import classes from "./component.module.css"; import "video.js/dist/video-js.css"; +import type Player from "video.js/dist/types/player"; + import { createDocumentationLink } from "@homarr/definitions"; export default function VideoWidget({ options }: WidgetComponentProps<"video">) { @@ -55,32 +56,66 @@ const ForYoutubeUseIframe = () => { }; const Feed = ({ options }: Pick, "options">) => { - const videoRef = useRef(null); + const videoRef = useRef(null); + const playerRef = useRef(null); useEffect(() => { - if (!videoRef.current) { - return; + if (playerRef.current) return; + const videoElement = document.createElement("video-js"); + videoElement.classList.add("vjs-big-play-centered"); + if (classes.video) { + videoElement.classList.add(classes.video); } + videoRef.current?.appendChild(videoElement); - // Initialize Video.js player if it's not already initialized - if (!("player" in videoRef.current)) { - videojs( - videoRef.current, + playerRef.current = videojs(videoElement, { + autoplay: options.hasAutoPlay, + muted: options.isMuted, + controls: options.hasControls, + sources: [ { - autoplay: options.hasAutoPlay, - muted: options.isMuted, - controls: options.hasControls, + src: options.feedUrl, }, - () => undefined, - ); - } - }, [options.hasAutoPlay, options.hasControls, options.isMuted, videoRef]); + ], + }); + // All other properties are updated with other useEffect + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [videoRef]); + + useEffect(() => { + if (!playerRef.current) return; + playerRef.current.src(options.feedUrl); + }, [options.feedUrl]); + + useEffect(() => { + if (!playerRef.current) return; + playerRef.current.autoplay(options.hasAutoPlay); + }, [options.hasAutoPlay]); + + useEffect(() => { + if (!playerRef.current) return; + playerRef.current.muted(options.isMuted); + }, [options.isMuted]); + + useEffect(() => { + if (!playerRef.current) return; + playerRef.current.controls(options.hasControls); + }, [options.hasControls]); + + useEffect(() => { + const player = playerRef.current; + + return () => { + if (player && !player.isDisposed()) { + player.dispose(); + playerRef.current = null; + } + }; + }, [playerRef]); return ( - + ); };