fix(video-widget): hls videos not working (#4015)

This commit is contained in:
Meier Lukas
2025-09-06 12:29:30 +02:00
committed by GitHub
parent b65ec27c0d
commit de33439b22
2 changed files with 57 additions and 20 deletions

View File

@@ -42,9 +42,11 @@ const nextConfig: NextConfig = {
headers: [ headers: [
{ {
key: "Content-Security-Policy", 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: ` value: `
default-src 'self'; default-src 'self';
script-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval';
worker-src * blob:;
base-uri 'self'; base-uri 'self';
connect-src *; connect-src *;
style-src * 'unsafe-inline'; style-src * 'unsafe-inline';
@@ -53,7 +55,7 @@ const nextConfig: NextConfig = {
form-action 'self'; form-action 'self';
img-src * data:; img-src * data:;
font-src * data:; font-src * data:;
media-src * data:; media-src * data: blob:;
` `
.replace(/\s{2,}/g, " ") .replace(/\s{2,}/g, " ")
.trim(), .trim(),

View File

@@ -1,9 +1,8 @@
"use client"; "use client";
import { useEffect, useRef } from "react"; 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 { IconBrandYoutube, IconDeviceCctvOff } from "@tabler/icons-react";
import combineClasses from "clsx";
import videojs from "video.js"; import videojs from "video.js";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
@@ -13,6 +12,8 @@ import classes from "./component.module.css";
import "video.js/dist/video-js.css"; import "video.js/dist/video-js.css";
import type Player from "video.js/dist/types/player";
import { createDocumentationLink } from "@homarr/definitions"; import { createDocumentationLink } from "@homarr/definitions";
export default function VideoWidget({ options }: WidgetComponentProps<"video">) { export default function VideoWidget({ options }: WidgetComponentProps<"video">) {
@@ -55,32 +56,66 @@ const ForYoutubeUseIframe = () => {
}; };
const Feed = ({ options }: Pick<WidgetComponentProps<"video">, "options">) => { const Feed = ({ options }: Pick<WidgetComponentProps<"video">, "options">) => {
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player>(null);
useEffect(() => { useEffect(() => {
if (!videoRef.current) { if (playerRef.current) return;
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 playerRef.current = videojs(videoElement, {
if (!("player" in videoRef.current)) { autoplay: options.hasAutoPlay,
videojs( muted: options.isMuted,
videoRef.current, controls: options.hasControls,
sources: [
{ {
autoplay: options.hasAutoPlay, src: options.feedUrl,
muted: options.isMuted,
controls: options.hasControls,
}, },
() => undefined, ],
); });
} // All other properties are updated with other useEffect
}, [options.hasAutoPlay, options.hasControls, options.isMuted, videoRef]); // 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 ( return (
<Group justify="center" w="100%" h="100%" pos="relative"> <Group justify="center" w="100%" h="100%" pos="relative">
<video className={combineClasses("video-js", classes.video)} ref={videoRef}> <Box w="100%" h="100%" ref={videoRef} />
<source src={options.feedUrl} />
</video>
</Group> </Group>
); );
}; };