mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-15 09:46:19 +01:00
✨ Add video-stream widget (#685)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
68
src/widgets/video/VideoFeed.tsx
Normal file
68
src/widgets/video/VideoFeed.tsx
Normal 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;
|
||||
68
src/widgets/video/VideoStreamTile.tsx
Normal file
68
src/widgets/video/VideoStreamTile.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user