From dc353d77eef84f638072950a3a244f290b6f6a51 Mon Sep 17 00:00:00 2001 From: Noah Sprent Date: Wed, 11 Dec 2024 11:37:21 +0000 Subject: [PATCH 1/5] Webcam changes --- package.json | 2 + src/App.jsx | 2 + src/Webcam.jsx | 238 ++++++++++++++++++++++++++++ src/components/HLSVideoPlayer.js | 56 +++++++ src/components/SideNavAndHeader.jsx | 15 +- 5 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 src/Webcam.jsx create mode 100644 src/components/HLSVideoPlayer.js diff --git a/package.json b/package.json index c540445..6446c27 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "boring-avatars": "^1.11.2", "codeflask": "^1.4.1", "dayjs": "^1.11.13", + "hls": "^0.0.1", + "hls.js": "^1.5.17", "js-yaml": "^4.1.0", "material-ui-confirm": "^3.0.5", "mqtt": "^5.7.3", diff --git a/src/App.jsx b/src/App.jsx index 4b171c0..8f18087 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,7 @@ import EditConfig from "./EditConfig"; import Updates from "./Updates"; import Plugins from "./Plugins"; import Profiles from "./Profiles"; +import Webcam from "./Webcam"; import Inventory from "./Inventory"; //import Analysis from "./Analysis"; import Experiments from "./Experiments"; @@ -115,6 +116,7 @@ function MainSite() { }/> }/> }/> + }/> }/> }/> }/> diff --git a/src/Webcam.jsx b/src/Webcam.jsx new file mode 100644 index 0000000..a1c2ea8 --- /dev/null +++ b/src/Webcam.jsx @@ -0,0 +1,238 @@ +import React, {useEffect, useState} from "react"; + +import Grid from '@mui/material/Grid'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/Card'; +import FormControl from '@mui/material/FormControl'; +import LoadingButton from '@mui/lab/LoadingButton'; +import FormLabel from '@mui/material/FormLabel'; +import Box from '@mui/material/Box'; +import MenuItem from '@mui/material/MenuItem'; +import {Typography} from '@mui/material'; +import Snackbar from '@mui/material/Snackbar'; +import Select from '@mui/material/Select'; +import SaveIcon from '@mui/icons-material/Save'; +import Editor from 'react-simple-code-editor'; +import { highlight, languages } from 'prismjs'; +import 'prismjs/components/prism-ini'; +import HLSVideoPlayer from "./components/HLSVideoPlayer"; + +import dayjs from "dayjs"; + + +function EditableCodeDiv(props) { + const [state, setState] = useState({ + code: null, + openSnackbar: false, + filename: "config.ini", + snackbarMsg: "", + saving: false, + historicalConfigs: [{ filename: "config.ini", data: "", timestamp: "2000-01-01" }], + timestamp_ix: 0, + errorMsg: "", + isError: false, + hasChangedSinceSave: true, + availableConfigs: [{ name: "shared config.ini", filename: "config.ini" }] + }); + + const getConfig = (filename) => { + fetch(`/api/configs/${filename}`) + .then(response => response.text()) + .then(text => setState(prev => ({ ...prev, code: text }))); + }; + + + const getHistoricalConfigFiles = (filename) => { + fetch(`/api/configs/${filename}/history`) + .then(response => response.json()) + .then(listOfHistoricalConfigs => setState(prev => ({ + ...prev, + historicalConfigs: listOfHistoricalConfigs, + timestamp_ix: 0 + }))); + }; + + const saveCurrentCode = () => { + setState(prev => ({ ...prev, saving: true, isError: false })); + fetch(`/api/configs/${state.filename}`, { + method: "PATCH", + body: JSON.stringify({ code: state.code, filename: state.filename }), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }) + .then(res => { + if (res.ok) { + setState(prev => ({ ...prev, snackbarMsg: `Success: ${state.filename} saved and synced.`, hasChangedSinceSave: false, saving: false, openSnackbar: true })); + } else { + res.json().then(parsedJson => + setState(prev => ({ ...prev, errorMsg: parsedJson['msg'], isError: true, hasChangedSinceSave: true, saving: false })) + ) + } + }); + }; + + useEffect(() => { + getConfig(state.filename); + getHistoricalConfigFiles(state.filename); + }, []); + + useEffect(() => { + let ignore = false; + + async function getConfigs() { + fetch("/api/configs") + .then(response => response.json()) + .then(json => { + if (ignore){ + return + } + setState(prev => ({ + ...prev, + availableConfigs: [...prev.availableConfigs, ...json.filter(e => e !== 'config.ini').map(e => ({ name: e, filename: e }))] + })) + }); + } + + getConfigs() + + return () => { + ignore = true; + }; + }, []); + + const onSelectionChange = (e) => { + const filename = e.target.value; + setState(prev => ({ ...prev, filename: filename, code: "Loading..." })); + getConfig(filename); + getHistoricalConfigFiles(filename); + }; + + const onSelectionHistoricalChange = (e) => { + const timestamp = e.target.value; + const ix = state.historicalConfigs.findIndex((c) => c.timestamp === timestamp); + const configBlob = state.historicalConfigs[ix]; + setState(prev => ({ ...prev, code: configBlob.data, timestamp_ix: ix })); + }; + + const onTextChange = (code) => { + setState(prev => ({ ...prev, code: code, hasChangedSinceSave: true })); + }; + + const handleSnackbarClose = () => { + setState(prev => ({ ...prev, openSnackbar: false })); + }; + + return ( + +
+ +
+ Config file + +
+
+ {state.historicalConfigs.length > 0 ? ( + +
+ Versions + +
+
+ ) :
} + +
+ +
+ {(state.code !== null) && + highlight(code, languages.ini)} + padding={10} + style={{ + fontSize: "14px", + fontFamily: 'monospace', + backgroundColor: "hsla(0, 0%, 100%, .5)", + borderRadius: "3px", + minHeight: "100%" + }} + /> + } +
+
+
+ } + > + {state.timestamp_ix === 0 ? "Save" : "Revert"} + +

{state.isError ? {state.errorMsg} : ""}

+
+
+ +
+ ); +} + + +function Webcam(props) { + React.useEffect(() => { + document.title = props.title; + }, [props.title]) + return ( + + + + + + ) +} + +export default Webcam; + diff --git a/src/components/HLSVideoPlayer.js b/src/components/HLSVideoPlayer.js new file mode 100644 index 0000000..884a18b --- /dev/null +++ b/src/components/HLSVideoPlayer.js @@ -0,0 +1,56 @@ +import React, { useEffect, useRef } from 'react'; +import Hls from 'hls.js'; + +const HLSVideoPlayer = ({ streamUrl }) => { + const videoRef = useRef(null); + + useEffect(() => { + if (!streamUrl) { + console.error('Stream URL is not provided'); + return; + } + + const video = videoRef.current; + let hls; + + if (Hls.isSupported()) { + hls = new Hls({ + startLevel: -1, + maxBufferLength: 10, // Lower max buffer length + maxBufferSize: 60 * 1000 * 1000, // 60 MB + maxBufferHole: 0.2, // Allow smaller buffer holes + fragLoadingTimeOut: 5000, // Timeout for fragment loading + liveSyncDurationCount: 2, // Reduce live sync duration to lower latency + }); + hls.loadSource(streamUrl); + hls.attachMedia(video); + hls.on(Hls.Events.MANIFEST_PARSED, () => { + video.play(); + }); + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + // For Safari + video.src = streamUrl; + video.addEventListener('loadedmetadata', () => { + video.play(); + }); + } + + return () => { + if (hls) { + hls.destroy(); + } + }; + }, [streamUrl]); + + return ( +
+ {streamUrl ? ( +
+ ); +}; + +export default HLSVideoPlayer; diff --git a/src/components/SideNavAndHeader.jsx b/src/components/SideNavAndHeader.jsx index 5c1130b..62b1d77 100644 --- a/src/components/SideNavAndHeader.jsx +++ b/src/components/SideNavAndHeader.jsx @@ -22,6 +22,7 @@ import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; import InsertChartOutlinedIcon from '@mui/icons-material/InsertChartOutlined'; import ViewTimelineOutlinedIcon from '@mui/icons-material/ViewTimelineOutlined'; +import VideocamOutlinedIcon from '@mui/icons-material/VideocamOutlined'; import ChatOutlinedIcon from '@mui/icons-material/ChatOutlined'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Sidebar, Menu, MenuItem, SubMenu} from "react-pro-sidebar"; @@ -194,7 +195,7 @@ export default function SideNavAndHeader() { return response.json(); }) .then((data) => { - setLatestVersion(data['tag_name']) + setLatestVersion(data['tag_naViewTimelineOutlinedIconme']) }).catch(e => { // no internet? }); @@ -224,7 +225,7 @@ export default function SideNavAndHeader() { renderExpandIcon={({level, active, disabled}) => null } menuItemStyles={{ label: {whiteSpace: "pre-wrap"}, - button: ({ level, active, disabled }) => { + button: ({ level, activeViewTimelineOutlinedIcon, disabled }) => { // only apply styles on first level elements of the tree if (level === 0) return { @@ -272,6 +273,16 @@ export default function SideNavAndHeader() { Profiles + + } + component={} + active={isSelected("/webcam")} + > + Webcam + + From 0b5355748efc41f0e1dad3799ec0cc614293a6d7 Mon Sep 17 00:00:00 2001 From: Noah Sprent Date: Wed, 11 Dec 2024 11:48:27 +0000 Subject: [PATCH 2/5] Some more webcam changes --- src/Webcam.jsx | 24 +++++++++++++++++++++++- src/components/SideNavAndHeader.jsx | 6 +++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Webcam.jsx b/src/Webcam.jsx index a1c2ea8..f95ddb6 100644 --- a/src/Webcam.jsx +++ b/src/Webcam.jsx @@ -220,6 +220,28 @@ function EditableCodeDiv(props) { ); } +function WebcamContainer(){ + return( + + + + + + + Webcams + + + + + + + + + + + +)} + function Webcam(props) { React.useEffect(() => { @@ -228,7 +250,7 @@ function Webcam(props) { return ( - + ) diff --git a/src/components/SideNavAndHeader.jsx b/src/components/SideNavAndHeader.jsx index 62b1d77..f60ed35 100644 --- a/src/components/SideNavAndHeader.jsx +++ b/src/components/SideNavAndHeader.jsx @@ -195,7 +195,7 @@ export default function SideNavAndHeader() { return response.json(); }) .then((data) => { - setLatestVersion(data['tag_naViewTimelineOutlinedIconme']) + setLatestVersion(data['tag_name']) }).catch(e => { // no internet? }); @@ -225,7 +225,7 @@ export default function SideNavAndHeader() { renderExpandIcon={({level, active, disabled}) => null } menuItemStyles={{ label: {whiteSpace: "pre-wrap"}, - button: ({ level, activeViewTimelineOutlinedIcon, disabled }) => { + button: ({ level, active, disabled }) => { // only apply styles on first level elements of the tree if (level === 0) return { @@ -280,7 +280,7 @@ export default function SideNavAndHeader() { component={} active={isSelected("/webcam")} > - Webcam + Webcams From 0aaea05a022eaffceb9a3fe7daa383df71851862 Mon Sep 17 00:00:00 2001 From: Noah Sprent Date: Wed, 11 Dec 2024 11:54:12 +0000 Subject: [PATCH 3/5] Allow two webcams and changing of the stream URL --- src/Webcam.jsx | 304 ++++++++----------------------- src/components/HLSVideoPlayer.js | 2 +- 2 files changed, 72 insertions(+), 234 deletions(-) diff --git a/src/Webcam.jsx b/src/Webcam.jsx index f95ddb6..e39d8d9 100644 --- a/src/Webcam.jsx +++ b/src/Webcam.jsx @@ -1,229 +1,29 @@ -import React, {useEffect, useState} from "react"; - +import React, { useState } from "react"; import Grid from '@mui/material/Grid'; import Card from '@mui/material/Card'; -import CardContent from '@mui/material/Card'; -import FormControl from '@mui/material/FormControl'; -import LoadingButton from '@mui/lab/LoadingButton'; -import FormLabel from '@mui/material/FormLabel'; +import CardContent from '@mui/material/CardContent'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; -import MenuItem from '@mui/material/MenuItem'; -import {Typography} from '@mui/material'; -import Snackbar from '@mui/material/Snackbar'; -import Select from '@mui/material/Select'; -import SaveIcon from '@mui/icons-material/Save'; -import Editor from 'react-simple-code-editor'; -import { highlight, languages } from 'prismjs'; -import 'prismjs/components/prism-ini'; -import HLSVideoPlayer from "./components/HLSVideoPlayer"; - -import dayjs from "dayjs"; - - -function EditableCodeDiv(props) { - const [state, setState] = useState({ - code: null, - openSnackbar: false, - filename: "config.ini", - snackbarMsg: "", - saving: false, - historicalConfigs: [{ filename: "config.ini", data: "", timestamp: "2000-01-01" }], - timestamp_ix: 0, - errorMsg: "", - isError: false, - hasChangedSinceSave: true, - availableConfigs: [{ name: "shared config.ini", filename: "config.ini" }] - }); - - const getConfig = (filename) => { - fetch(`/api/configs/${filename}`) - .then(response => response.text()) - .then(text => setState(prev => ({ ...prev, code: text }))); - }; - - - const getHistoricalConfigFiles = (filename) => { - fetch(`/api/configs/${filename}/history`) - .then(response => response.json()) - .then(listOfHistoricalConfigs => setState(prev => ({ - ...prev, - historicalConfigs: listOfHistoricalConfigs, - timestamp_ix: 0 - }))); - }; - - const saveCurrentCode = () => { - setState(prev => ({ ...prev, saving: true, isError: false })); - fetch(`/api/configs/${state.filename}`, { - method: "PATCH", - body: JSON.stringify({ code: state.code, filename: state.filename }), - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }) - .then(res => { - if (res.ok) { - setState(prev => ({ ...prev, snackbarMsg: `Success: ${state.filename} saved and synced.`, hasChangedSinceSave: false, saving: false, openSnackbar: true })); - } else { - res.json().then(parsedJson => - setState(prev => ({ ...prev, errorMsg: parsedJson['msg'], isError: true, hasChangedSinceSave: true, saving: false })) - ) - } - }); - }; +import HLSVideoPlayer from "./components/HLSVideoPlayer"; // Import your HLSVideoPlayer component - useEffect(() => { - getConfig(state.filename); - getHistoricalConfigFiles(state.filename); - }, []); +function WebcamContainer() { + // State to store stream URLs for two video players + const [streamUrl1, setStreamUrl1] = useState("http://localhost:8000/stream.m3u8"); + const [streamUrl2, setStreamUrl2] = useState("http://localhost:8000/stream.m3u8"); - useEffect(() => { - let ignore = false; - - async function getConfigs() { - fetch("/api/configs") - .then(response => response.json()) - .then(json => { - if (ignore){ - return - } - setState(prev => ({ - ...prev, - availableConfigs: [...prev.availableConfigs, ...json.filter(e => e !== 'config.ini').map(e => ({ name: e, filename: e }))] - })) - }); - } - - getConfigs() - - return () => { - ignore = true; - }; - }, []); - - const onSelectionChange = (e) => { - const filename = e.target.value; - setState(prev => ({ ...prev, filename: filename, code: "Loading..." })); - getConfig(filename); - getHistoricalConfigFiles(filename); - }; - - const onSelectionHistoricalChange = (e) => { - const timestamp = e.target.value; - const ix = state.historicalConfigs.findIndex((c) => c.timestamp === timestamp); - const configBlob = state.historicalConfigs[ix]; - setState(prev => ({ ...prev, code: configBlob.data, timestamp_ix: ix })); + // Function to handle stream URL change for video player 1 + const handleUrlChange1 = (e) => { + setStreamUrl1(e.target.value); }; - const onTextChange = (code) => { - setState(prev => ({ ...prev, code: code, hasChangedSinceSave: true })); - }; - - const handleSnackbarClose = () => { - setState(prev => ({ ...prev, openSnackbar: false })); + // Function to handle stream URL change for video player 2 + const handleUrlChange2 = (e) => { + setStreamUrl2(e.target.value); }; return ( -
- -
- Config file - -
-
- {state.historicalConfigs.length > 0 ? ( - -
- Versions - -
-
- ) :
} - -
- -
- {(state.code !== null) && - highlight(code, languages.ini)} - padding={10} - style={{ - fontSize: "14px", - fontFamily: 'monospace', - backgroundColor: "hsla(0, 0%, 100%, .5)", - borderRadius: "3px", - minHeight: "100%" - }} - /> - } -
-
-
- } - > - {state.timestamp_ix === 0 ? "Save" : "Revert"} - -

{state.isError ? {state.errorMsg} : ""}

-
-
- -
- ); -} - -function WebcamContainer(){ - return( - - @@ -232,29 +32,67 @@ function WebcamContainer(){ + + + + + - - - - - - -)} + {/* Grid layout to display two video players side by side */} + + + + + {/* Input for stream URL 1 */} + + {/* First Video Player */} + + + + + + + + {/* Input for stream URL 2 */} + + {/* Second Video Player */} + + + + + + + ); +} function Webcam(props) { - React.useEffect(() => { - document.title = props.title; - }, [props.title]) - return ( - - - - - - ) + React.useEffect(() => { + document.title = props.title; + }, [props.title]); + + return ( + + + + + + ); } export default Webcam; - diff --git a/src/components/HLSVideoPlayer.js b/src/components/HLSVideoPlayer.js index 884a18b..741d7f3 100644 --- a/src/components/HLSVideoPlayer.js +++ b/src/components/HLSVideoPlayer.js @@ -45,7 +45,7 @@ const HLSVideoPlayer = ({ streamUrl }) => { return (
{streamUrl ? ( -