diff --git a/package.json b/package.json index 70c4708..dd10d30 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@testing-library/user-event": "^7.2.1", "boring-avatars": "^1.11.2", "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 7efe692..d863b1a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,7 @@ import EditExperimentProfile from "./EditExperimentProfile"; import EditConfig from "./EditConfig"; import Updates from "./Updates"; import Plugins from "./Plugins"; +import Webcam from "./Webcam"; import {Profiles} from "./Profiles"; import Inventory from "./Inventory"; import Leader from "./Leader"; @@ -121,6 +122,7 @@ function MainSite() { }/> }/> }/> + }/> }/> }/> }/> diff --git a/src/Webcam.jsx b/src/Webcam.jsx new file mode 100644 index 0000000..2055349 --- /dev/null +++ b/src/Webcam.jsx @@ -0,0 +1,98 @@ +import React, { useState } from "react"; +import Grid from '@mui/material/Grid'; +import Card from '@mui/material/Card'; +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 HLSVideoPlayer from "./components/HLSVideoPlayer"; // Import your HLSVideoPlayer component + +function WebcamContainer() { + // State to store stream URLs for two video players + const [streamUrl1, setStreamUrl1] = useState("data/webcam.m3u8"); + const [streamUrl2, setStreamUrl2] = useState("data/webcam.m3u8"); + + // Function to handle stream URL change for video player 1 + const handleUrlChange1 = (e) => { + setStreamUrl1(e.target.value); + }; + + // Function to handle stream URL change for video player 2 + const handleUrlChange2 = (e) => { + setStreamUrl2(e.target.value); + }; + + return ( + + + + + + Webcams + + + + + + + + + + + {/* 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 ( + + + + + + ); +} + +export default Webcam; diff --git a/src/components/HLSVideoPlayer.js b/src/components/HLSVideoPlayer.js new file mode 100644 index 0000000..741d7f3 --- /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 3e05c98..04218ca 100644 --- a/src/components/SideNavAndHeader.jsx +++ b/src/components/SideNavAndHeader.jsx @@ -23,6 +23,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"; @@ -310,6 +311,15 @@ export default function SideNavAndHeader() { Profiles + + } + component={} + active={isSelected("/webcam")} + > + Webcams +