From 717e5e633be5471b3d3b9f476b7834cc8a433bcc Mon Sep 17 00:00:00 2001 From: NlightN22 Date: Thu, 22 Feb 2024 22:27:29 +0700 Subject: [PATCH] fix live player --- src/hooks/use-camera-activity.ts | 4 +- src/index.tsx | 4 +- src/pages/LiveCameraPage.tsx | 47 +++++++ src/pages/MainBody.tsx | 62 +++------- src/router/routes.path.ts | 2 +- src/router/routes.tsx | 5 + src/services/frigate.proxy/frigate.api.ts | 11 +- src/services/frigate.proxy/frigate.schema.ts | 8 +- src/shared/components/CameraCard.tsx | 29 ++--- .../frigate/AutoUpdatingCameraImage.tsx | 4 +- src/shared/components/frigate/CameraImage.tsx | 12 +- .../components/frigate/JSMpegPlayer.tsx | 117 ++++++------------ src/shared/components/frigate/Player.tsx | 37 +++--- .../components/menu/HostSettingsMenu.tsx | 1 + src/types/frigateConfig.ts | 2 +- 15 files changed, 162 insertions(+), 183 deletions(-) create mode 100644 src/pages/LiveCameraPage.tsx diff --git a/src/hooks/use-camera-activity.ts b/src/hooks/use-camera-activity.ts index 5732bba..b546663 100644 --- a/src/hooks/use-camera-activity.ts +++ b/src/hooks/use-camera-activity.ts @@ -58,8 +58,8 @@ export default function useCameraActivity( return { activeTracking: hasActiveObjects, activeMotion: detectingMotion == "ON", - activeAudio: camera.audio.enabled_in_config - ? audioRms >= camera.audio.min_volume + activeAudio: camera.audio?.enabled_in_config ? + audioRms >= camera.audio.min_volume : false, }; } diff --git a/src/index.tsx b/src/index.tsx index f3cdbf1..2e49846 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -31,9 +31,9 @@ export const Context = createContext(rootStore) root.render( - + {/* */} - + {/* */} ); diff --git a/src/pages/LiveCameraPage.tsx b/src/pages/LiveCameraPage.tsx new file mode 100644 index 0000000..35f2bfb --- /dev/null +++ b/src/pages/LiveCameraPage.tsx @@ -0,0 +1,47 @@ +import React, { Fragment, useContext, useEffect } from 'react'; +import { Context } from '..'; +import { observer } from 'mobx-react-lite'; +import { useParams } from 'react-router-dom'; +import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; +import { useQuery } from '@tanstack/react-query'; +import CenterLoader from '../shared/components/CenterLoader'; +import RetryError from './RetryError'; +import Player from '../shared/components/frigate/Player'; +import { Flex } from '@mantine/core'; +import JSMpegPlayer from '../shared/components/frigate/JSMpegPlayer'; + +const LiveCameraPage = observer(() => { + let { id: cameraId } = useParams<'id'>() + if (!cameraId) throw Error('Camera id does not exist') + + const { data: camera, isPending, isError, refetch } = useQuery({ + queryKey: [frigateQueryKeys.getCameraWHost, cameraId], + queryFn: () => frigateApi.getCameraWHost(cameraId!) + }) + + const { sideBarsStore } = useContext(Context) + useEffect(() => { + sideBarsStore.rightVisible = false + sideBarsStore.setLeftChildren(null) + sideBarsStore.setRightChildren(null) + }, []) + + + if (isPending) return + + if (isError) return + + // const hostNameWPort = camera.frigateHost ? new URL(camera.frigateHost.host).host : '' + // const wsUrl = frigateApi.cameraWsURL(hostNameWPort, camera.name) + + return ( + + + {/* */} + {/* {JSON.stringify(camera)} */} + {/* {cameraWsURL} */} + + ); +}) + +export default LiveCameraPage; \ No newline at end of file diff --git a/src/pages/MainBody.tsx b/src/pages/MainBody.tsx index 6c008d7..ecd8dbc 100644 --- a/src/pages/MainBody.tsx +++ b/src/pages/MainBody.tsx @@ -1,25 +1,18 @@ -import { Container, Flex, Grid, Group, Skeleton, Text } from '@mantine/core'; -import ProductTable, { TableAdapter } from '../widgets/ProductTable'; +import { Flex, Grid, Group } from '@mantine/core'; import HeadSearch from '../shared/components/HeadSearch'; import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector'; import { useContext, useState, useEffect } from 'react'; -import ProductGrid, { GridAdapter } from '../shared/components/grid.aps/ProductGrid'; import { getCookie, setCookie } from 'cookies-next'; import { Context } from '..'; import { observer } from 'mobx-react-lite' import CenterLoader from '../shared/components/CenterLoader'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { frigateApi, frigateQueryKeys, mapHostToHostname } from '../services/frigate.proxy/frigate.api'; import RetryError from './RetryError'; -import { CameraConfig, FrigateConfig } from '../types/frigateConfig'; +import { FrigateConfig } from '../types/frigateConfig'; import { GetFrigateHostWConfig } from '../services/frigate.proxy/frigate.schema'; -import { host } from '../shared/env.const'; -import AutoUpdatingCameraImage from '../shared/components/frigate/AutoUpdatingCameraImage'; import CameraCard from '../shared/components/CameraCard'; - - - const MainBody = observer(() => { const { sideBarsStore } = useContext(Context) useEffect(() => { @@ -34,43 +27,21 @@ const MainBody = observer(() => { setTableState(state) } - const { data: hosts, isPending, isError, refetch } = useQuery({ - queryKey: [frigateQueryKeys.getFrigateHostsConfigs], - queryFn: async () => { - const hosts = await frigateApi.getHosts() - let fetchedConfigs = [] - for (const host of hosts) { - if (host.enabled) { - const hostName = mapHostToHostname(host) - const config: FrigateConfig = await frigateApi.getHostConfig(hostName) - if (config) { - const hostWConfig: GetFrigateHostWConfig = { config: config, ...host } - fetchedConfigs.push(hostWConfig) - } - } - } - return fetchedConfigs - } + const { data: cameras, isPending, isError, refetch } = useQuery({ + queryKey: [frigateQueryKeys.getCamerasWHost], + queryFn: frigateApi.getCamerasWHost }) if (isPending) return if (isError) return - - // const child = () => { - // return ( ) - // } - - const cards = (host: GetFrigateHostWConfig) => { - return Object.entries(host.config.cameras).map( - ([cameraName, cameraConfig]) => ( - )) + const cards = () => { + return cameras.map(camera => ( + )) } return ( @@ -93,11 +64,10 @@ const MainBody = observer(() => { - {hosts.map(host => ( - - {cards(host)} - - ))} + + {cards()} + + )) ); diff --git a/src/router/routes.path.ts b/src/router/routes.path.ts index e87a45a..12012d8 100644 --- a/src/router/routes.path.ts +++ b/src/router/routes.path.ts @@ -9,7 +9,7 @@ export const routesPath = { HOST_SYSTEM_PATH: '/hosts/:id/system', HOST_STORAGE_PATH: '/hosts/:id/storage', ROLES_PATH: '/roles', - LIVE_PATH: '/live', + LIVE_PATH: '/cameras/:id/', THANKS_PATH: '/thanks', USER_DETAILED_PATH: '/user', RETRY_ERROR_PATH: '/retry_error', diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 9f8f82d..f2d547e 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -10,6 +10,7 @@ import FrigateHostsPage from "../pages/FrigateHostsPage"; import HostConfigPage from "../pages/HostConfigPage"; import HostSystemPage from "../pages/HostSystemPage"; import HostStoragePage from "../pages/HostStoragePage"; +import LiveCameraPage from "../pages/LiveCameraPage"; interface IRoute { path: string, @@ -41,6 +42,10 @@ export const routes: IRoute[] = [ path: routesPath.HOST_STORAGE_PATH, component: , }, + { + path: routesPath.LIVE_PATH, + component: , + }, { path: routesPath.MAIN_PATH, component: , diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index a9f1bb6..5ae67e5 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -1,7 +1,7 @@ import axios from "axios" import { proxyURL } from "../../shared/env.const" import { z } from "zod" -import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost, GetFrigateHostWithCameras } from "./frigate.schema"; +import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost, GetFrigateHostWithCameras, GetCameraWHost, GetCameraWHostWConfig } from "./frigate.schema"; import { FrigateConfig } from "../../types/frigateConfig"; @@ -19,9 +19,11 @@ export const frigateApi = { getHostWithCameras: () => instance.get('apiv1/frigate-hosts', { params: { include: 'cameras'}}).then(res => { return res.data }), - getHost: (id: string) => instance.get(`apiv1/frigate-hosts/${id}`).then(res => { + getHost: (id: string) => instance.get(`apiv1/frigate-hosts/${id}`).then(res => { return res.data }), + getCamerasWHost: () => instance.get(`apiv1/cameras`).then(res => {return res.data}), + getCameraWHost: (id: string) => instance.get(`apiv1/cameras/${id}`).then(res => {return res.data}), putHosts: (hosts: PutFrigateHost[]) => instance.put('apiv1/frigate-hosts', hosts).then(res => { return res.data }), @@ -42,19 +44,18 @@ export const mapCamerasFromConfig = (config: FrigateConfig): string[] => { return Object.keys(config.cameras) } - - export const mapHostToHostname = (host: GetFrigateHost): string => { const url = new URL(host.host) const hostName = url.host return hostName } - export const frigateQueryKeys = { getConfig: 'config', getFrigateHosts: 'frigate-hosts', getFrigateHostsConfigs: 'frigate-hosts-configs', getFrigateHost: 'frigate-host', + getCamerasWHost: 'cameras-frigate-host', + getCameraWHost: 'camera-frigate-host', getHostConfig: 'host-config', } diff --git a/src/services/frigate.proxy/frigate.schema.ts b/src/services/frigate.proxy/frigate.schema.ts index 3a0e71f..814c5d1 100644 --- a/src/services/frigate.proxy/frigate.schema.ts +++ b/src/services/frigate.proxy/frigate.schema.ts @@ -1,5 +1,5 @@ import { any, z } from "zod"; -import { FrigateConfig } from "../../types/frigateConfig"; +import { CameraConfig, FrigateConfig } from "../../types/frigateConfig"; export const putConfigSchema = z.object({ key: z.string(), @@ -41,6 +41,10 @@ const getCameraSchema = z.object({ state: z.boolean().nullable(), }); +const getCameraWithHostSchema = getCameraSchema.merge(z.object({ + frigateHost: getFrigateHostSchema.optional() +})) + export const getFrigateHostWithCamerasSchema = getFrigateHostSchema.merge(z.object({ cameras: z.array(getCameraSchema), })) @@ -59,5 +63,7 @@ export type PutConfig = z.infer export type GetFrigateHost = z.infer export type GetFrigateHostWithCameras = z.infer export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig} +export type GetCameraWHost = z.infer +export type GetCameraWHostWConfig = GetCameraWHost & { config?: CameraConfig } export type PutFrigateHost = z.infer export type DeleteFrigateHost = z.infer \ No newline at end of file diff --git a/src/shared/components/CameraCard.tsx b/src/shared/components/CameraCard.tsx index 45a526c..921f412 100644 --- a/src/shared/components/CameraCard.tsx +++ b/src/shared/components/CameraCard.tsx @@ -2,13 +2,10 @@ import React from 'react'; import { CameraConfig } from '../../types/frigateConfig'; import { AspectRatio, Button, Card, Flex, Grid, Group, Space, Text, createStyles, useMantineTheme } from '@mantine/core'; import AutoUpdatingCameraImage from './frigate/AutoUpdatingCameraImage'; - -interface CameraCardProps { - cameraName: string, - hostName: string, - cameraConfig: CameraConfig, - imageUrl: string -} +import { useNavigate } from 'react-router-dom'; +import { routesPath } from '../../router/routes.path'; +import { GetCameraWHostWConfig, GetFrigateHost } from '../../services/frigate.proxy/frigate.schema'; +import { frigateApi, mapHostToHostname } from '../../services/frigate.proxy/frigate.api'; const useStyles = createStyles((theme) => ({ @@ -30,17 +27,21 @@ const useStyles = createStyles((theme) => ({ } })) +interface CameraCardProps { + camera: GetCameraWHostWConfig +} const CameraCard = ({ - cameraName, - hostName, - cameraConfig, - imageUrl, + camera }: CameraCardProps) => { const { classes } = useStyles(); + const navigate = useNavigate() + const imageUrl = camera.frigateHost ? frigateApi.cameraImageURL(mapHostToHostname(camera.frigateHost), camera.name) : '' //todo implement get URL from live cameras + const handleOpenLiveView = () => { - throw Error('Not yet implemented') + const url = routesPath.LIVE_PATH.replace(':id', camera.id) + navigate(url) } const handleOpenRecordings = () => { throw Error('Not yet implemented') @@ -52,10 +53,10 @@ const CameraCard = ({ {/* */} - {cameraName} / {hostName} + {camera.name} / {camera.frigateHost?.name} diff --git a/src/shared/components/frigate/AutoUpdatingCameraImage.tsx b/src/shared/components/frigate/AutoUpdatingCameraImage.tsx index 3f9a724..94e1d09 100644 --- a/src/shared/components/frigate/AutoUpdatingCameraImage.tsx +++ b/src/shared/components/frigate/AutoUpdatingCameraImage.tsx @@ -5,7 +5,7 @@ import { useDocumentVisibility } from "@mantine/hooks"; import { AspectRatio, Flex } from "@mantine/core"; interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes { - cameraConfig: CameraConfig + cameraConfig?: CameraConfig searchParams?: {}; showFps?: boolean; className?: string; @@ -85,7 +85,7 @@ export default function AutoUpdatingCameraImage({ diff --git a/src/shared/components/frigate/CameraImage.tsx b/src/shared/components/frigate/CameraImage.tsx index e2f0126..77de137 100644 --- a/src/shared/components/frigate/CameraImage.tsx +++ b/src/shared/components/frigate/CameraImage.tsx @@ -4,10 +4,10 @@ import { AspectRatio, Flex, createStyles, Text } from "@mantine/core"; interface CameraImageProps extends React.ImgHTMLAttributes { className?: string; - cameraConfig: CameraConfig; + cameraConfig?: CameraConfig; onload?: () => void; - searchParams?: {}; - url: string + url: string, + enabled?: boolean }; const useStyles = createStyles((theme) => ({ @@ -19,20 +19,18 @@ export default function CameraImage({ className, cameraConfig, onload, - searchParams = "", + enabled, url, ...rest }: CameraImageProps) { const imgRef = useRef(null); const { classes } = useStyles(); - const name = cameraConfig.name - const enabled = cameraConfig.enabled useEffect(() => { if (!cameraConfig || !imgRef.current) { return; } imgRef.current.src = url - }, [name, imgRef, searchParams]); + }, [imgRef]); return ( diff --git a/src/shared/components/frigate/JSMpegPlayer.tsx b/src/shared/components/frigate/JSMpegPlayer.tsx index d9d8f4e..3bfc488 100644 --- a/src/shared/components/frigate/JSMpegPlayer.tsx +++ b/src/shared/components/frigate/JSMpegPlayer.tsx @@ -1,100 +1,53 @@ // @ts-ignore we know this doesn't have types import JSMpeg from "@cycjimmy/jsmpeg-player"; -import { useEffect, useMemo, useRef } from "react"; -import { useResizeObserver } from "../../utils/resize-observer"; +import { useEffect, useMemo, useRef, useState } from "react"; type JSMpegPlayerProps = { - className?: string; wsUrl: string; - camera: string; - width: number; - height: number; }; -export default function JSMpegPlayer({ - camera, - wsUrl, - width, - height, - className, -}: JSMpegPlayerProps) { - const playerRef = useRef(null); - const containerRef = useRef(null); - const [{ width: containerWidth, height: containerHeight }] = - useResizeObserver(containerRef); - // Add scrollbar width (when visible) to the available observer width to eliminate screen juddering. - // https://github.com/blakeblackshear/frigate/issues/1657 - let scrollBarWidth = 0; - if (window.innerWidth && document.body.offsetWidth) { - scrollBarWidth = window.innerWidth - document.body.offsetWidth; - } - const availableWidth = scrollBarWidth - ? containerWidth + scrollBarWidth - : containerWidth; - const aspectRatio = width / height; +const JSMpegPlayer = ( + { + wsUrl, - const scaledHeight = useMemo(() => { - const scaledHeight = Math.floor(availableWidth / aspectRatio); - const finalHeight = Math.min(scaledHeight, height); - - if (containerHeight < finalHeight) { - return containerHeight; - } - - if (finalHeight > 0) { - return finalHeight; - } - - return 100; - }, [availableWidth, aspectRatio, height]); - const scaledWidth = useMemo( - () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), - [scaledHeight, aspectRatio, scrollBarWidth] - ); + }: JSMpegPlayerProps +) => { + const videoRef = useRef(null); + const [playerInitialized, setPlayerInitialized] = useState(false) useEffect(() => { - if (!playerRef.current) { - return; + let player: any; + + if (player && playerInitialized) { + player.destroy() + console.log('JSMpegPlayer destroyed player') + } + if (!playerInitialized && videoRef.current) { + console.log('JSMpegPlayer creating player') + player = new JSMpeg.Player( + wsUrl, + { canvas: videoRef.current }, + {}, + { protocols: [], audio: false } + ); + setPlayerInitialized(true); } - const video = new JSMpeg.VideoElement( - playerRef.current, - wsUrl, - {}, - { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 } - ); - - const fullscreen = () => { - if (video.els.canvas.webkitRequestFullScreen) { - video.els.canvas.webkitRequestFullScreen(); - } else { - video.els.canvas.mozRequestFullScreen(); - } - }; - - video.els.canvas.addEventListener("click", fullscreen); - return () => { - if (playerRef.current) { - try { - video.destroy(); - } catch (e) {} - playerRef.current = null; + try { + console.log('JSMpegPlayer destroying player') + player.destroy() + console.log('JSMpegPlayer destroyed player') + setPlayerInitialized(false) + } catch (error) { + setPlayerInitialized(true) + console.error('JSMpegPlayer Error on unmount:', error); } }; }, [wsUrl]); - return ( -
-
-
- ); -} \ No newline at end of file + return ; +}; + +export default JSMpegPlayer \ No newline at end of file diff --git a/src/shared/components/frigate/Player.tsx b/src/shared/components/frigate/Player.tsx index 9cbb950..1d4fb0d 100644 --- a/src/shared/components/frigate/Player.tsx +++ b/src/shared/components/frigate/Player.tsx @@ -6,27 +6,27 @@ import { LivePlayerMode } from '../../../types/live'; import useCameraActivity from '../../../hooks/use-camera-activity'; import useCameraLiveMode from '../../../hooks/use-camera-live-mode'; import WebRtcPlayer from './WebRTCPlayer'; +import { Flex } from '@mantine/core'; +import { frigateApi } from '../../../services/frigate.proxy/frigate.api'; +import { GetCameraWHostWConfig } from '../../../services/frigate.proxy/frigate.schema'; type LivePlayerProps = { - className?: string; - cameraConfig: CameraConfig; + camera: GetCameraWHostWConfig; preferredLiveMode?: LivePlayerMode; showStillWithoutActivity?: boolean; windowVisible?: boolean; - host: string }; const Player = ({ - className, - cameraConfig, + camera, preferredLiveMode, - showStillWithoutActivity = true, windowVisible = true, - host }: LivePlayerProps) => { - const wsUrl = 'ws://localhost:4000/proxy-ws/ws?hostName=localhost:5000' - + const hostNameWPort = camera.frigateHost ? new URL(camera.frigateHost.host).host : '' + const wsUrl = frigateApi.cameraWsURL(hostNameWPort, camera.name) + const cameraConfig = camera.config! + const { activeMotion, activeAudio, activeTracking } = useCameraActivity(cameraConfig); @@ -53,6 +53,7 @@ const Player = ({ } }, [cameraActive, liveReady]); + console.log(`liveMode: `, liveMode) let player; if (liveMode == "webrtc") { player = ( @@ -86,20 +87,16 @@ const Player = ({ } else if (liveMode == "jsmpeg") { player = ( ); - - return ( -
- -
- ); } + + return ( + + {player} + + ); } export default Player; \ No newline at end of file diff --git a/src/shared/components/menu/HostSettingsMenu.tsx b/src/shared/components/menu/HostSettingsMenu.tsx index e43fc47..8a1feaa 100644 --- a/src/shared/components/menu/HostSettingsMenu.tsx +++ b/src/shared/components/menu/HostSettingsMenu.tsx @@ -25,6 +25,7 @@ const HostSettingsMenu = ({ id }: HostSettingsMenuProps) => { } const handleRestart = () => { + throw Error('Not yet implemented') } return ( diff --git a/src/types/frigateConfig.ts b/src/types/frigateConfig.ts index 77c51c8..57b97d6 100644 --- a/src/types/frigateConfig.ts +++ b/src/types/frigateConfig.ts @@ -22,7 +22,7 @@ export interface BirdseyeConfig { export interface CameraConfig { audio: { enabled: boolean; - enabled_in_config: boolean; + enabled_in_config?: boolean | null; filters: string[] | null; listen: string[]; max_not_heard: number;