Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -121,6 +122,7 @@ function MainSite() {
<Route path="/experiment-profiles/:profileFilename/" element={<Profiles title="Pioreactor ~ Experiment profiles"/>}/>
<Route path="/experiment-profiles/new" element={<CreateExperimentProfile title="Pioreactor ~ Create experiment profile"/>}/>
<Route path="/experiment-profiles/:profileFilename/edit" element={<EditExperimentProfile title="Pioreactor ~ Edit experiment profile"/>}/>
<Route path="/webcam" element={<Webcam title="Pioreactor ~ Webcam"/>}/>
<Route path="/config" element={<EditConfig title="Pioreactor ~ Configuration"/>}/>
<Route path="/leader" element={<Leader title="Pioreactor ~ Leader"/>}/>
<Route path="/calibrations" element={<Calibrations title="Pioreactor ~ Calibrations"/>}/>
Expand Down
98 changes: 98 additions & 0 deletions src/Webcam.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<React.Fragment>
<Box>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}>
<Typography variant="h5" component="h2">
<Box fontWeight="fontWeightBold">
Webcams
</Box>
</Typography>
</Box>





</Box>

{/* Grid layout to display two video players side by side */}
<Grid container spacing={2}>
<Grid item md={6} xs={12}>
<Card>
<CardContent sx={{ p: 1 }}>
{/* Input for stream URL 1 */}
<TextField
label="Enter Stream URL 1"
variant="outlined"
fullWidth
value={streamUrl1}
onChange={handleUrlChange1}
sx={{ marginBottom: 2 }}
/>
{/* First Video Player */}
<HLSVideoPlayer streamUrl={streamUrl1} />
</CardContent>
</Card>
</Grid>

<Grid item md={6} xs={12}>
<Card>
<CardContent sx={{ p: 1 }}>
{/* Input for stream URL 2 */}
<TextField
label="Enter Stream URL 2"
variant="outlined"
fullWidth
value={streamUrl2}
onChange={handleUrlChange2}
sx={{ marginBottom: 2 }}
/>
{/* Second Video Player */}
<HLSVideoPlayer streamUrl={streamUrl2} />
</CardContent>
</Card>
</Grid>
</Grid>
</React.Fragment>
);
}

function Webcam(props) {
React.useEffect(() => {
document.title = props.title;
}, [props.title]);

return (
<Grid container spacing={2}>
<Grid item md={12} xs={12}>
<WebcamContainer />
</Grid>
</Grid>
);
}

export default Webcam;
56 changes: 56 additions & 0 deletions src/components/HLSVideoPlayer.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{streamUrl ? (
<video ref={videoRef} controls style={{ width: '100%', height: 'auto' }} />
) : (
<p>Error: Stream URL is missing.</p>
)}
</div>
);
};

export default HLSVideoPlayer;
10 changes: 10 additions & 0 deletions src/components/SideNavAndHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -310,6 +311,15 @@ export default function SideNavAndHeader() {
Profiles
</MenuItem>

<MenuItem
icon={
<VideocamOutlinedIcon/>
}
component={<Link to="/webcam" className="link" />}
active={isSelected("/webcam")}
>
Webcams
</MenuItem>

<Divider sx={{marginTop: "15px", marginBottom: "15px"}} />
</Menu>
Expand Down