Add video-stream widget (#685)

This commit is contained in:
Yossi Hillali
2023-02-08 23:23:53 +02:00
committed by GitHub
parent bb010ff54a
commit fab018a10e
9 changed files with 452 additions and 51 deletions

View File

@@ -5,6 +5,7 @@ import usenet from './useNet/UseNetTile';
import weather from './weather/WeatherTile';
import torrent from './torrent/TorrentTile';
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
import videoStream from './video/VideoStreamTile';
export default {
calendar,
@@ -14,4 +15,5 @@ export default {
'torrents-status': torrent,
dlspeed: torrentNetworkTraffic,
date,
'video-stream': videoStream,
};

View File

@@ -0,0 +1,68 @@
import { LoadingOverlay } from '@mantine/core';
import { createStyles } from '@mantine/styles';
import { useEffect, useRef, useState } from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
interface VideoFeedProps {
source: string;
muted: boolean;
autoPlay: boolean;
controls: boolean;
}
const VideoFeed = ({ source, controls, autoPlay, muted }: VideoFeedProps) => {
const videoRef = useRef(null);
const [player, setPlayer] = useState<ReturnType<typeof videojs>>();
const { classes, cx } = useStyles();
useEffect(() => {
// make sure Video.js player is only initialized once
if (player) {
return;
}
const videoElement = videoRef.current;
if (!videoElement) {
return;
}
setPlayer(videojs(videoElement, { autoplay: autoPlay, muted, controls }, () => {}));
}, [videoRef]);
useEffect(
() => () => {
if (!player) {
return;
}
if (player.isDisposed()) {
return;
}
player.dispose();
},
[player]
);
return (
<>
<LoadingOverlay visible={player === undefined} />
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video className={cx('video-js', classes.video)} ref={videoRef}>
<source src={source} type="video/mp4" />
</video>
</>
);
};
const useStyles = createStyles(({ radius }) => ({
video: {
height: '100%',
borderRadius: radius.md,
overflow: 'hidden',
},
}));
export default VideoFeed;

View File

@@ -0,0 +1,68 @@
import { Center, Group, Stack, Title } from '@mantine/core';
import { IconDeviceCctv, IconHeartBroken } from '@tabler/icons';
import { useTranslation } from 'react-i18next';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import VideoFeed from './VideoFeed';
const definition = defineWidget({
id: 'video-stream',
icon: IconDeviceCctv,
options: {
cameraFeedUrl: {
type: 'text',
defaultValue: '',
},
autoPlay: {
type: 'switch',
defaultValue: true,
},
muted: {
type: 'switch',
defaultValue: true,
},
controls: {
type: 'switch',
defaultValue: false,
},
},
gridstack: {
minWidth: 3,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
component: VideoStreamWidget,
});
export type VideoStreamWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface VideoStreamWidgetProps {
widget: VideoStreamWidget;
}
function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
const { t } = useTranslation('modules/video-stream');
if (!widget.properties.cameraFeedUrl) {
return (
<Center h="100%">
<Stack align="center">
<IconHeartBroken />
<Title order={4}>{t('errors.invalidStream')}</Title>
</Stack>
</Center>
);
}
return (
<Group position="center" w="100%" h="100%">
<VideoFeed
source={widget?.properties.cameraFeedUrl}
muted={widget?.properties.muted}
autoPlay={widget?.properties.autoPlay}
controls={widget?.properties.controls}
/>
</Group>
);
}
export default definition;