diff --git a/package.json b/package.json index 549fab8..9b5899e 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "strftime": "0.10.1", "typescript": "^4.4.2", "validator": "^13.9.0", + "video.js": "^8.10.0", "web-vitals": "^2.1.0", "zod": "^3.21.4" }, @@ -74,6 +75,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@types/strftime": "^0.9.8", "@types/uuid": "^9.0.2", + "@types/video.js": "^7.3.56", "uuid": "^9.0.0" } } diff --git a/src/pages/MainBody.tsx b/src/pages/MainBody.tsx index ae3ead3..ae450bc 100644 --- a/src/pages/MainBody.tsx +++ b/src/pages/MainBody.tsx @@ -35,6 +35,7 @@ const MainBody = observer(() => { if (isError) return const cards = () => { + // return cameras.filter(cam => cam.frigateHost?.host.includes('5001')).slice(0,1).map(camera => ( return cameras.map(camera => ( import('../shared/components/accordion/CameraAccordion')); + + +const RecordingsPage = observer(() => { + const { sideBarsStore } = useContext(Context) + const filterData: OneSelectItem[] = [ + { value: 'dsfgdfg', label: 'fasdfsdf' }, + { value: 'dsfgsfgnjcv', label: 'frteh' }, + { value: 'rthsdfgh', label: 'dftghdfgjn' }, + ] + const hostSelector = () => ( + + ) + useEffect(() => { + sideBarsStore.rightVisible = true + sideBarsStore.setRightChildren(hostSelector()) + }, []) + + + + const location = useLocation() + const queryParams = new URLSearchParams(location.search) + const cameraId = queryParams.get('cameraId'); + const hostId = queryParams.get('hostId') + const date = queryParams.get('date'); + const time = queryParams.get('time'); + + const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({ + queryKey: [frigateQueryKeys.getCameraWHost, cameraId], + queryFn: async () => { + if (cameraId) { + return frigateApi.getCameraWHost(cameraId) + } + return null + } + }) + const { data: host, isPending: hostPending, isError: hostError, refetch: hostRefetch } = useQuery({ + queryKey: [frigateQueryKeys.getFrigateHost, hostId], + queryFn: async () => { + if (hostId) { + return frigateApi.getHost(hostId) + } + return null + } + }) + + const [openCameraId, setOpenCameraId] = useState(null) + + const handleOnChange = (cameraId: string | null) => { + console.log('Camera id', cameraId) + setOpenCameraId(openCameraId === cameraId ? null : cameraId) + } + + const handleRetry = () => { + if (cameraId) cameraRefetch() + else if (hostId) hostRefetch() + } + + if (hostPending || cameraPending) return + if (hostError || cameraError) return + + + // Camera selected + if (camera && camera.frigateHost) { + return ( + + {camera.frigateHost.name} + + + + + ) + } + // Host selected + if (host && host.cameras.length > 0) { + + const cameras = host.cameras.map(camera => { + return ( + + {camera.name} + + {openCameraId === camera.id && ( + + + + )} + + + ) + }) + + return ( + + {host.name} + handleOnChange(value)}> + {cameras} + + + ) + } + + return ( + + Please select host + + ) + + + // const videoUrl = proxyApi.eventURL('localhost:5000', event) + // const recordUrl = 'http://127.0.0.1:5000/vod/2024-02/22/18/Buhgalteria/Asia,Krasnoyarsk/master.m3u8' + // const recordUrl = proxyApi.recordingURL('localhost:5000', 'Buhgalteria', 'Asia,Krasnoyarsk', '2024-02/22/18') + // console.log(recordUrl) + + // return ( + // + // {/* */} + // + // ) + + // seekOptions={{ forward: 10, backward: 5 }} + // onReady={handlePlayerReady} + // onDispose={onDispose} + {/* {eventOverlay ? ( + + ) : null} */} return (
) -} +}) + +export default RecordingsPage // const API_LIMIT = 25; @@ -45,7 +188,7 @@ export default function EventsPage() { // export default function Events({ path, ...props }) { // // const apiHost = useApiHost(); // // const { data: config } = useSWR('config'); -// const timezone = useMemo(() => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]); +// const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone // const [searchParams, setSearchParams] = useState({ // before: null, // after: null, @@ -284,12 +427,12 @@ export default function EventsPage() { // [path, searchParams, setSearchParams] // ); -// const onClickFilterSubmitted = useCallback(() => { -// if (++searchParams.is_submitted > 1) { -// searchParams.is_submitted = -1; -// } -// onFilter('is_submitted', searchParams.is_submitted); -// }, [searchParams, onFilter]); +// // const onClickFilterSubmitted = useCallback(() => { +// // if (++searchParams.is_submitted > 1) { +// // searchParams.is_submitted = -1; +// // } +// // onFilter('is_submitted', searchParams.is_submitted); +// // }, [searchParams, onFilter]); // const isDone = (eventPages?.[eventPages.length - 1]?.length ?? 0) < API_LIMIT; @@ -405,14 +548,14 @@ export default function EventsPage() { // )} //
-// {config.plus.enabled && ( +// {/* {config.plus.enabled && ( // onClickFilterSubmitted()} // inner_fill={searchParams.is_submitted == 1 ? 'currentColor' : 'gray'} // outer_stroke={searchParams.is_submitted >= 0 ? 'currentColor' : 'gray'} // /> -// )} +// )} */} // -// onSave(e, event.id, !event.retain_indefinitely)} +// fill={event.retain_indefinitely ? 'yellow' : 'none'} /> +// {/* onSave(e, event.id, !event.retain_indefinitely)} // fill={event.retain_indefinitely ? 'currentColor' : 'none'} -// /> +// /> */} // {event.end_time ? null : ( //
// In progress @@ -825,7 +971,7 @@ export default function EventsPage() { // : `, ${event.sub_label}: ${(event?.data?.sub_label_score * 100).toFixed(0)}%`} //
//
-// //
diff --git a/src/router/routes.path.ts b/src/router/routes.path.ts index 12012d8..e153fc7 100644 --- a/src/router/routes.path.ts +++ b/src/router/routes.path.ts @@ -1,7 +1,6 @@ export const routesPath = { MAIN_PATH: '/', BIRDSEYE_PATH: '/birdseye', - EVENTS_PATH: '/events', RECORDINGS_PATH: '/recordings', SETTINGS_PATH: '/settings', HOSTS_PATH: '/hosts', diff --git a/src/router/routes.tsx b/src/router/routes.tsx index f2d547e..43cca09 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -11,6 +11,7 @@ import HostConfigPage from "../pages/HostConfigPage"; import HostSystemPage from "../pages/HostSystemPage"; import HostStoragePage from "../pages/HostStoragePage"; import LiveCameraPage from "../pages/LiveCameraPage"; +import RecordingsPage from "../pages/RecordingsPage"; interface IRoute { path: string, @@ -30,6 +31,10 @@ export const routes: IRoute[] = [ path: routesPath.HOSTS_PATH, component: , }, + { + path: routesPath.RECORDINGS_PATH, + component: , + }, { path: routesPath.HOST_CONFIG_PATH, component: , diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index fabd53f..13aa183 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -4,6 +4,7 @@ import { z } from "zod" import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost, GetFrigateHostWithCameras, GetCameraWHost, GetCameraWHostWConfig } from "./frigate.schema"; import { FrigateConfig } from "../../types/frigateConfig"; import { url } from "inspector"; +import { RecordSummary } from "../../types/record"; const instanceApi = axios.create({ @@ -17,7 +18,7 @@ export const frigateApi = { getHosts: () => instanceApi.get('apiv1/frigate-hosts').then(res => { return res.data }), - getHostWithCameras: () => instanceApi.get('apiv1/frigate-hosts', { params: { include: 'cameras' } }).then(res => { + getHostsWithCameras: () => instanceApi.get('apiv1/frigate-hosts', { params: { include: 'cameras' } }).then(res => { return res.data }), getHost: (id: string) => instanceApi.get(`apiv1/frigate-hosts/${id}`).then(res => { @@ -34,8 +35,8 @@ export const frigateApi = { } export const proxyApi = { - getHostConfigRaw: (hostName: string) => instanceApi.get('proxy/api/config/raw', { params: { hostName: hostName } }).then(res => res.data), - getHostConfig: (hostName: string) => instanceApi.get('proxy/api/config', { params: { hostName: hostName } }).then(res => res.data), + getHostConfigRaw: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/config/raw`).then(res => res.data), + getHostConfig: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/config`).then(res => res.data), getImageFrigate: async (imageUrl: string) => { const response = await fetch(imageUrl); if (!response.ok) { @@ -43,11 +44,59 @@ export const proxyApi = { } return response.blob(); }, - cameraWsURL: (hostName: string, cameraName: string) => { - return `ws://${proxyURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${hostName}` - }, - cameraImageURL: (hostName: string, cameraName: string) => { - return `http://${proxyURL.host}/proxy/api/${cameraName}/latest.jpg?hostName=${hostName}` + getHostRestart: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/restart`).then(res => res.data), + + getRecordings: ( + hostName: string, + cameraName: string, + after: number, + before: number + ) => + instanceApi.get(`proxy/${hostName}/api/${cameraName}/recordings?after=${after}&before=${before}`).then(res => res.data), + + getRecordingsSummary: ( + hostName: string, + cameraName: string, + timezone: string, + ) => + instanceApi.get(`proxy/${hostName}/api/${cameraName}/recordings/summary`, {params: { timezone}}).then(res => res.data), + + getEvents: ( + hostName: string, + camerasName: string[], + timezone: string, + minScore?: number, + maxScore?: number, + after?: number, + before?: number, + labels?: string[], + ) => + instanceApi.get(`proxy/${hostName}/api/events`, { + params: { + cameras: camerasName, + after: after, + timezone: timezone, + before: before, // @before the last event start_time in list + labels: labels, + min_score: minScore, + max_score: maxScore, + } + }).then(res => res.data), + + getEventsSummary: (hostName: string, cameraName: string) => + instanceApi.get(`proxy/${hostName}/api/${cameraName}/events/summary`).then(res => res.data), + getEventsInProgress: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/events?in_progress=1&include_thumbnails=0`), + cameraWsURL: (hostName: string, cameraName: string) => + `ws://${proxyURL.host}/proxy-ws/${hostName}/live/jsmpeg/${cameraName}`, + cameraImageURL: (hostName: string, cameraName: string) => + `${proxyURL.protocol}//${proxyURL.host}/proxy/${hostName}/api/${cameraName}/latest.jpg`, + eventURL: (hostName: string, event: string) => + `${proxyURL.protocol}//${proxyURL.host}/proxy/${hostName}/vod/event/${event}/master.m3u8`, + // http://127.0.0.1:5000/vod/2024-02/23/19/CameraName/Asia,Krasnoyarsk/master.m3u8 + recordingURL: (hostName: string, cameraName: string, timezone: string, day: string, hour: string) => {// day:2024-02-23 hour:19 + const parts = day.split('-') + const date = `${parts[0]}-${parts[1]}/${parts[2]}/${hour}` + return `${proxyURL.protocol}//${proxyURL.host}/proxy/${hostName}/vod/${date}/${cameraName}/${timezone}/master.m3u8` // todo add Date/Time }, } @@ -69,4 +118,6 @@ export const frigateQueryKeys = { getCamerasWHost: 'cameras-frigate-host', getCameraWHost: 'camera-frigate-host', getHostConfig: 'host-config', + getRecordingsSummary: 'recordings-frigate-summary', + getRecordings: 'recordings-frigate', } diff --git a/src/shared/components/CameraCard.tsx b/src/shared/components/CameraCard.tsx index fc047c2..acbc99a 100644 --- a/src/shared/components/CameraCard.tsx +++ b/src/shared/components/CameraCard.tsx @@ -52,9 +52,6 @@ const CameraCard = ({ const handleOpenRecordings = () => { throw Error('Not yet implemented') } - const handleOpenEvents = () => { - throw Error('Not yet implemented') - } return ( @@ -66,7 +63,6 @@ const CameraCard = ({ className={classes.bottomGroup}> - diff --git a/src/shared/components/accordion/CameraAccordion.tsx b/src/shared/components/accordion/CameraAccordion.tsx new file mode 100644 index 0000000..c708365 --- /dev/null +++ b/src/shared/components/accordion/CameraAccordion.tsx @@ -0,0 +1,72 @@ +import { Accordion, Center, Text } from '@mantine/core'; +import React, { useContext, useEffect, useState } from 'react'; +import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema'; +import { useQuery } from '@tanstack/react-query'; +import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api'; +import CogwheelLoader from '../CogwheelLoader'; +import DayAccordion from './DayAccordion'; +import { observer } from 'mobx-react-lite'; +import { Context } from '../../..'; + +interface CameraAccordionProps { + camera: GetCameraWHostWConfig, + host: GetFrigateHost +} + +const CameraAccordion = observer(({ + camera, + host +}: CameraAccordionProps) => { + const { recordingsStore } = useContext(Context) + + const { data, isPending, isError } = useQuery({ + queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id], + queryFn: () => { + if (camera && host) { + const hostName = mapHostToHostname(host) + return proxyApi.getRecordingsSummary(hostName, camera.name, 'Asia/Krasnoyarsk') + } + return null + } + }) + + const [openedDay, setOpenedDay] = useState() + + useEffect(() => { + if (openedDay) { + recordingsStore.playedRecord.cameraName = camera.name + const hostName = mapHostToHostname(host) + recordingsStore.playedRecord.hostName = hostName + } + }, [openedDay]) + + const handleClick = (value: string | null) => { + setOpenedDay(value) + } + + if (isPending) return ( +
+ Loading... +
+ ) + if (isError) return Loading error + + if (!data || !camera) return null + + const days = data.map(rec => ( + + {rec.day} + + + + + + )) + return ( + + {days} + + ) +}) + +export default CameraAccordion; \ No newline at end of file diff --git a/src/shared/components/accordion/DayAccordion.tsx b/src/shared/components/accordion/DayAccordion.tsx new file mode 100644 index 0000000..783165e --- /dev/null +++ b/src/shared/components/accordion/DayAccordion.tsx @@ -0,0 +1,94 @@ +import { Accordion, Flex, Group, Text } from '@mantine/core'; +import React, { useContext, useEffect, useState } from 'react'; +import { RecordHour, RecordSummary, Recording } from '../../../types/record'; +import Button from '../frigate/Button'; +import { IconPlayerPause, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; +import { observer } from 'mobx-react-lite'; +import PlayControl from './PlayControl'; +import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api'; +import { Context } from '../../..'; +import VideoPlayer from '../frigate/VideoPlayer'; + +interface RecordingAccordionProps { + recordSummary?: RecordSummary +} + +const DayAccordion = observer(({ + recordSummary +}: RecordingAccordionProps) => { + const { recordingsStore } = useContext(Context) + const [openVideoPlayer, setOpenVideoPlayer] = useState() + const [openedValue, setOpenedValue] = useState() + const [playerUrl, setPlayerUrl] = useState() + + useEffect(() => { + if (openVideoPlayer) { + console.log('openVideoPlayer', openVideoPlayer) + if (openVideoPlayer) { + recordingsStore.playedRecord.day = recordSummary?.day + recordingsStore.playedRecord.hour = openVideoPlayer + recordingsStore.playedRecord.timezone = 'Asia,Krasnoyarsk' + const parsed = recordingsStore.getFullPlayedRecord(recordingsStore.playedRecord) + console.log('recordingsStore.playedRecord: ', recordingsStore.playedRecord) + if (parsed.success) { + const url = proxyApi.recordingURL( + parsed.data.hostName, + parsed.data.cameraName, + parsed.data.timezone, + parsed.data.day, + parsed.data.hour + ) + console.log('GET URL: ', url) + setPlayerUrl(url) + } + } + }else { + setPlayerUrl(undefined) + } + }, [openVideoPlayer]) + + if (!recordSummary || recordSummary.hours.length < 1) return (Not have record at that day) + + const handleOpenPlayer = (hour: string) => { + // console.log(`openVideoPlayer day:${recordSummary.day} hour:${hour}`) + if (openVideoPlayer !== hour) { + setOpenedValue(hour) + setOpenVideoPlayer(hour) + } else if (openedValue === hour && openVideoPlayer === hour) { + setOpenVideoPlayer(undefined) + } + } + + const handleClick = (value: string) => { + if (openedValue === value) { + setOpenedValue(undefined) + } else { + setOpenedValue(value) + } + setOpenVideoPlayer(undefined) + } + + return ( + + {recordSummary.hours.map(hour => ( + + + + + + {openVideoPlayer === hour.hour ? : <>} + Events + + + ))} + + ) +}) + +export default DayAccordion; \ No newline at end of file diff --git a/src/shared/components/accordion/PlayControl.tsx b/src/shared/components/accordion/PlayControl.tsx new file mode 100644 index 0000000..d5ea6ae --- /dev/null +++ b/src/shared/components/accordion/PlayControl.tsx @@ -0,0 +1,46 @@ +import { Flex, Group, Text } from '@mantine/core'; +import { IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; +import React from 'react'; + +interface PlayControlProps { + hour: string, + openVideoPlayer?: string, + onClick?: (value: string) => void +} + +const PlayControl = ({ + hour, + openVideoPlayer, + onClick +}: PlayControlProps) => { + const handleClick = (value: string) => { + if (onClick) onClick(value) + } + return ( + + Hour: {hour} + + { + event.stopPropagation() + handleClick(hour) + }}> + {openVideoPlayer === hour ? 'Stop Video' : 'Play Video'} + + {openVideoPlayer === hour ? + { + event.stopPropagation() + handleClick(hour) + }} /> + : + { + event.stopPropagation() + handleClick(hour) + }} /> + + } + + + ) +} + +export default PlayControl; \ No newline at end of file diff --git a/src/shared/components/accordion/TestAccordion.tsx b/src/shared/components/accordion/TestAccordion.tsx new file mode 100644 index 0000000..a690bda --- /dev/null +++ b/src/shared/components/accordion/TestAccordion.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const TestAccordion = ({ camera }: { camera: any }) => { + return ( +
+ TEST ACCORDION {camera.name} +
+ ); +}; + +export default TestAccordion; \ No newline at end of file diff --git a/src/shared/components/filters.aps/OneSelectFilter.tsx b/src/shared/components/filters.aps/OneSelectFilter.tsx index a293f17..4e9b29b 100644 --- a/src/shared/components/filters.aps/OneSelectFilter.tsx +++ b/src/shared/components/filters.aps/OneSelectFilter.tsx @@ -2,9 +2,18 @@ import { SelectItem, SystemProp, SpacingValue, SelectProps, Box, Flex, CloseButt import React, { CSSProperties } from 'react'; import CloseWithTooltip from '../CloseWithTooltip'; import { strings } from '../../strings/strings'; + + +export interface OneSelectItem { + value: string; + label: string; + selected?: boolean; + disabled?: boolean; +} + interface OneSelectFilterProps { id: string - data: SelectItem[] + data: OneSelectItem[] spaceBetween?: SystemProp label?: string defaultValue?: string diff --git a/src/shared/components/frigate/VideoPlayer.jsx b/src/shared/components/frigate/VideoPlayer.jsx deleted file mode 100644 index 638cba4..0000000 --- a/src/shared/components/frigate/VideoPlayer.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useRef, useEffect } from 'react'; -import videojs from 'video.js'; -import 'videojs-playlist'; -import 'video.js/dist/video-js.css'; - -export default function VideoPlayer({ - children, - options, - seekOptions = { forward: 30, backward: 10 }, onReady = () => { }, onDispose = () => { } -}) { - const playerRef = useRef(); - - useEffect(() => { - const defaultOptions = { - controls: true, - controlBar: { - skipButtons: seekOptions, - }, - playbackRates: [0.5, 1, 2, 4, 8], - fluid: true, - }; - - - if (!videojs.browser.IS_FIREFOX) { - defaultOptions.playbackRates.push(16); - } - - const player = videojs(playerRef.current, { ...defaultOptions, ...options }, () => { - onReady(player); - }); - - // Allows player to continue on error - player.reloadSourceOnError(); - - // Disable fullscreen on iOS if we have children - if ( - children && - videojs.browser.IS_IOS && - videojs.browser.IOS_VERSION > 9 && - !player.el_.ownerDocument.querySelector('.bc-iframe') - ) { - player.tech_.el_.setAttribute('playsinline', 'playsinline'); - player.tech_.supportsFullScreen = function () { - return false; - }; - } - - const screen = window.screen; - - const angle = () => { - // iOS - if (typeof window.orientation === 'number') { - return window.orientation; - } - // Android - if (screen && screen.orientation && screen.orientation.angle) { - return window.orientation; - } - videojs.log('angle unknown'); - return 0; - }; - - const rotationHandler = () => { - const currentAngle = angle(); - - if (currentAngle === 90 || currentAngle === 270 || currentAngle === -90) { - if (player.paused() === false) { - player.requestFullscreen(); - } - } - - if ((currentAngle === 0 || currentAngle === 180) && player.isFullscreen()) { - player.exitFullscreen(); - } - }; - - if (videojs.browser.IS_IOS) { - window.addEventListener('orientationchange', rotationHandler); - } else if (videojs.browser.IS_ANDROID && screen.orientation) { - // addEventListener('orientationchange') is not a user interaction on Android - screen.orientation.onchange = rotationHandler; - } - - return () => { - if (videojs.browser.IS_IOS) { - window.removeEventListener('orientationchange', rotationHandler); - } - player.dispose(); - onDispose(); - }; - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - return ( -
- {/* Setting an empty data-setup is required to override the default values and allow video to be fit the size of its parent */} -
- ); -} \ No newline at end of file diff --git a/src/shared/components/frigate/VideoPlayer.tsx b/src/shared/components/frigate/VideoPlayer.tsx new file mode 100644 index 0000000..1c551a6 --- /dev/null +++ b/src/shared/components/frigate/VideoPlayer.tsx @@ -0,0 +1,72 @@ +import React, { useRef, useEffect } from 'react'; +import videojs from 'video.js'; +import Player from 'video.js/dist/types/player'; +import 'video.js/dist/video-js.css' + +interface VideoPlayerProps { + videoUrl?: string +} + +const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => { + const videoRef = useRef(null); + const playerRef = useRef(null); + + useEffect(() => { + const defaultOptions = { + preload: 'auto', + autoplay: true, + sources: [ + { + src: videoUrl, + type: 'application/vnd.apple.mpegurl', + }, + ], + controls: true, + controlBar: { + skipButtons: { forward: 10, backward: 5 }, + }, + playbackRates: [0.5, 1, 2, 4, 8], + fluid: true, + }; + + if (!videojs.browser.IS_FIREFOX) { + defaultOptions.playbackRates.push(16); + } + + //TODO add rotations on IOS and android devices + + console.log('playerRef.current', playerRef.current) + + if (videoRef.current) { + console.log('mount new player') + playerRef.current = videojs(videoRef.current, { ...defaultOptions }, () => { + console.log('player is ready'); + }); + } + console.log('VideoPlayer rendered') + return () => { + if (playerRef.current !== null) { + playerRef.current.dispose(); + playerRef.current = null; + console.log('unmount player') + } + }; + }, []); + + + useEffect(() => { + if (playerRef.current) { + playerRef.current.src(videoUrl); + console.log('player change src') + } + }, [videoUrl]); + + return ( +
+ {/* Setting an empty data-setup is required to override the default values and allow video to be fit the size of its parent */} +
+ ); +}; + +export default VideoPlayer; \ No newline at end of file diff --git a/src/shared/components/frigate/icons/Download.jsx b/src/shared/components/frigate/icons/Download.jsx index cd6227b..7ed2c57 100644 --- a/src/shared/components/frigate/icons/Download.jsx +++ b/src/shared/components/frigate/icons/Download.jsx @@ -1,7 +1,9 @@ -import { h } from 'preact'; -import { memo } from 'preact/compat'; +import { memo } from 'react'; -export function Download({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) { + + + +export function Download({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => void }) { return ( =5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-json-parse@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-4.0.0.tgz#7c0f578cfccd12d33a71c0e05413e2eca171eaac" + integrity sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ== + dependencies: + rust-result "^1.0.0" + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -9554,6 +9715,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +url-toolkit@^2.2.1: + version "2.2.5" + resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.5.tgz#58406b18e12c58803e14624df5e374f638b0f607" + integrity sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg== + use-callback-ref@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" @@ -9640,6 +9806,45 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +"video.js@^7 || ^8", video.js@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/video.js/-/video.js-8.10.0.tgz#603a49909ef33f839264da8b73513f9daf592b57" + integrity sha512-7UeG/flj/pp8tNGW8WKPP1VJb3x2FgLoqUWzpZqkoq5YIyf6MNzmIrKtxprl438T5RVkcj+OzV8IX4jYSAn4Sw== + dependencies: + "@babel/runtime" "^7.12.5" + "@videojs/http-streaming" "3.10.0" + "@videojs/vhs-utils" "^4.0.0" + "@videojs/xhr" "2.6.0" + aes-decrypter "^4.0.1" + global "4.4.0" + keycode "2.2.0" + m3u8-parser "^7.1.0" + mpd-parser "^1.2.2" + mux.js "^7.0.1" + safe-json-parse "4.0.0" + videojs-contrib-quality-levels "4.0.0" + videojs-font "4.1.0" + videojs-vtt.js "0.15.5" + +videojs-contrib-quality-levels@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.0.0.tgz#faa8096594cdbfc3ccbefe8572fc20531ba23f3d" + integrity sha512-u5rmd8BjLwANp7XwuQ0Q/me34bMe6zg9PQdHfTS7aXgiVRbNTb4djcmfG7aeSrkpZjg+XCLezFNenlJaCjBHKw== + dependencies: + global "^4.4.0" + +videojs-font@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-4.1.0.tgz#3ae1dbaac60b4f0f1c4e6f7ff9662a89df176015" + integrity sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w== + +videojs-vtt.js@0.15.5: + version "0.15.5" + resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz#567776eaf2a7a928d88b148a8b401ade2406f2ca" + integrity sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ== + dependencies: + global "^4.3.1" + vscode-jsonrpc@8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"