mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-05 18:55:23 +03:00
Add webUI
This commit is contained in:
parent
9c32404e91
commit
b1115fb2c9
@ -1505,7 +1505,7 @@ def vod_event(id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/export/<camera_name>/start/<start_time>/end/<end_time>")
|
@bp.route("/export/<camera_name>/start/<start_time>/end/<end_time>", methods=["POST"])
|
||||||
def export_recording(camera_name: str, start_time: int, end_time: int):
|
def export_recording(camera_name: str, start_time: int, end_time: int):
|
||||||
playback_factor = request.args.get("playback", type=str, default="realtime")
|
playback_factor = request.args.get("playback", type=str, default="realtime")
|
||||||
exporter = RecordingExporter(
|
exporter = RecordingExporter(
|
||||||
|
|||||||
@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class PlaybackFactorEnum(str, Enum):
|
class PlaybackFactorEnum(str, Enum):
|
||||||
real_time = "real_time"
|
realtime = "realtime"
|
||||||
timelapse_5x = "timelapse_5x"
|
timelapse_5x = "timelapse_5x"
|
||||||
|
|
||||||
|
|
||||||
@ -80,14 +80,14 @@ class RecordingExporter(threading.Thread):
|
|||||||
"/dev/stdin",
|
"/dev/stdin",
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.playback_factor == PlaybackFactorEnum.real_time:
|
if self.playback_factor == PlaybackFactorEnum.realtime:
|
||||||
ffmpeg_cmd.extend(["-c", "copy", file_name])
|
ffmpeg_cmd.extend(["-c", "copy", file_name])
|
||||||
elif self.playback_factor == PlaybackFactorEnum.timelapse_5x:
|
elif self.playback_factor == PlaybackFactorEnum.timelapse_5x:
|
||||||
ffmpeg_cmd.extend(["-vf", "setpts=0.25*PTS", "-r", "5", "-an", file_name])
|
ffmpeg_cmd.extend(["-vf", "setpts=0.25*PTS", "-r", "5", "-an", file_name])
|
||||||
|
|
||||||
p = sp.run(
|
p = sp.run(
|
||||||
ffmpeg_cmd,
|
ffmpeg_cmd,
|
||||||
input="\n".join(playlist_lines)
|
input="\n".join(playlist_lines),
|
||||||
encoding="ascii",
|
encoding="ascii",
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export default function Sidebar() {
|
|||||||
</Match>
|
</Match>
|
||||||
{birdseye?.enabled ? <Destination href="/birdseye" text="Birdseye" /> : null}
|
{birdseye?.enabled ? <Destination href="/birdseye" text="Birdseye" /> : null}
|
||||||
<Destination href="/events" text="Events" />
|
<Destination href="/events" text="Events" />
|
||||||
|
<Destination href="/exports" text="Exports" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<Destination href="/storage" text="Storage" />
|
<Destination href="/storage" text="Storage" />
|
||||||
<Destination href="/system" text="System" />
|
<Destination href="/system" text="System" />
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export default function App() {
|
|||||||
<AsyncRoute path="/cameras/:camera" getComponent={cameraComponent} />
|
<AsyncRoute path="/cameras/:camera" getComponent={cameraComponent} />
|
||||||
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
|
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
|
||||||
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
||||||
|
<AsyncRoute path="/exports" getComponent={Routes.getExports} />
|
||||||
<AsyncRoute
|
<AsyncRoute
|
||||||
path="/recording/:camera/:date?/:hour?/:minute?/:second?"
|
path="/recording/:camera/:date?/:hour?/:minute?/:second?"
|
||||||
getComponent={Routes.getRecording}
|
getComponent={Routes.getRecording}
|
||||||
|
|||||||
85
web/src/routes/Export.jsx
Normal file
85
web/src/routes/Export.jsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { h, Fragment } from 'preact';
|
||||||
|
import { useApiHost } from '../api';
|
||||||
|
import Heading from '../components/Heading';
|
||||||
|
import { useCallback, useState } from 'preact/hooks';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import Button from '../components/Button';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default function Export() {
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
const { data: config } = useSWR('config');
|
||||||
|
|
||||||
|
const [camera, setCamera] = useState('select');
|
||||||
|
const [playback, setPlayback] = useState('select');
|
||||||
|
const [message, setMessage] = useState({ text: '', error: false });
|
||||||
|
|
||||||
|
const onHandleExport = () => {
|
||||||
|
if (camera == 'select') {
|
||||||
|
setMessage({ text: 'A camera needs to be selected.', error: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playback == 'select') {
|
||||||
|
setMessage({ text: 'A playback factor needs to be selected.', error: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = new Date(document.getElementById('start').value).getTime() / 1000;
|
||||||
|
const end = new Date(document.getElementById('end').value).getTime() / 1000;
|
||||||
|
|
||||||
|
if (!start || !end) {
|
||||||
|
setMessage({ text: 'A start and end time needs to be selected', error: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage({ text: 'Successfully started export. View the file in the /exports folder.', error: false });
|
||||||
|
axios.post(`export/${camera}/start/${start}/end/${end}`, { playback });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 p-2 px-4 w-full">
|
||||||
|
<Heading>Export</Heading>
|
||||||
|
|
||||||
|
{message.text && (
|
||||||
|
<div className={`max-h-20 ${message.error ? 'text-red-500' : 'text-green-500'}`}>{message.text}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
className="me-2 cursor-pointer rounded dark:bg-slate-800"
|
||||||
|
value={camera}
|
||||||
|
onChange={(e) => setCamera(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="select">Select A Camera</option>
|
||||||
|
{Object.keys(config?.cameras || {}).map((item) => (
|
||||||
|
<option key={item} value={item}>
|
||||||
|
{item.replaceAll('_', ' ')}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="ms-2 cursor-pointer rounded dark:bg-slate-800"
|
||||||
|
value={playback}
|
||||||
|
onChange={(e) => setPlayback(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="select">Select A Playback Factor</option>
|
||||||
|
<option value="realtime">Realtime</option>
|
||||||
|
<option value="timelapse_5x">Timelapse</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Heading className="py-2" size="sm">
|
||||||
|
From:
|
||||||
|
</Heading>
|
||||||
|
<input className="dark:bg-slate-800" id="start" type="datetime-local" />
|
||||||
|
<Heading className="py-2" size="sm">
|
||||||
|
To:
|
||||||
|
</Heading>
|
||||||
|
<input className="dark:bg-slate-800" id="end" type="datetime-local" />
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => onHandleExport()}>Submit</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -23,6 +23,11 @@ export async function getEvents(_url, _cb, _props) {
|
|||||||
return module.default;
|
return module.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getExports(_url, _cb, _props) {
|
||||||
|
const module = await import('./Export.jsx');
|
||||||
|
return module.default;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRecording(_url, _cb, _props) {
|
export async function getRecording(_url, _cb, _props) {
|
||||||
const module = await import('./Recording.jsx');
|
const module = await import('./Recording.jsx');
|
||||||
return module.default;
|
return module.default;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user