forked from Github/frigate
Add go2rtc and add restream role / live source (#4082)
* Pull go2rtc dependency * Add go2rtc to local services and add to s6 * Add relay controller for go2rtc * Add restream role * Add restream role * Add restream to nginx * Add camera live source config * Disable RTMP by default and use restream * Use go2rtc for camera config * Fix go2rtc move * Start restream on frigate start * Send restream to camera level * Fix restream * Make sure jsmpeg works as expected * Make view rspect live size config * Tweak player options to fit live view * Adjust VideoPlayer to accept live option which disables irrelevant controls * Add multiple options from restream live view * Add base for webrtc option * Setup specific restream modules * Make mp4 the default streaming for now * Expose 8554 for rtsp relay from go2rtc * Formatting * Update docs to suggest new restream method. * Update docs to reflect restream role * Update docs to reflect restream role * Add webrtc player * Improvements to webRTC * Support webrtc * Cleanup * Adjust rtmp test and add restream test * Fix tests * Add restream tests * Add live view docs and show different options * Small docs tweak * Support all stream types * Update to beta 9 of go2rtc * Formatting * Make jsmpeg the default * Support wss if made from https * Support wss if made from https * Use onEffect * Set url outside onEffect * Fix passed deps * Update docs about required host mode * Try memo instead * Close websocket on changing camera * Formatting * Close pc connection * Set video source to null on cleanup * Use full path since go2rtc can't see PATH var * Adjust audio codec to enable browser audio by default * Cleanup stream creation * Add restream tests * Format tests * Mock requests * Adjust paths * Move stream configs to restream * Remove live source * Remove live config * Use live persistence for which view to use on each camera * Fix live sizes * Only use jsmpeg sizes for jsmpeg live * Set max live size * Remove access of live config * Add selector for live view source in web view * Remove RTMP from default list of roles * Update docs * Fix tests * Fix docs for live view modes * make default undefined to avoid race condition * Wait until camera source is loaded to avoid race condition * Fix tests * Add config to go2rtc * Work with config * Set full path for config * Set to use stun * Check for mounted file * Look for frigate-go2rtc * Update docs to reflect webRTC configuration. * Add link to go2rtc config * Update docs to be more clear * Update docs to be more clear * Update format Co-authored-by: Felipe Santos <felipecassiors@gmail.com> * Update live docs * Improve bash startup script * Add option to force audio compatibility * Formatting * Fix mapping * Fix broken link * Update go2rtc version * Get go2rtc webui working * Add support for mse * Remove mp4 option * Undo changes to video player * Update docs for new live view options * Make separate path for mse * Remove unused * Remove mp4 path * Try to get go2rtc proxy working * Try to get go2rtc proxy working * Remove unused callback * Allow websocket on restrea dashboard * Make mse default stream option * Fix mse sizing * don't assume roles is defined * Remove nginx mapping to go2rtc ui Co-authored-by: Felipe Santos <felipecassiors@gmail.com> Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import { usePersistence } from '../context';
|
||||
import { useCallback, useMemo, useState } from 'preact/hooks';
|
||||
import { useApiHost } from '../api';
|
||||
import useSWR from 'swr';
|
||||
import WebRtcPlayer from '../components/WebRtcPlayer';
|
||||
import MsePlayer from '../components/MsePlayer';
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
|
||||
@@ -23,9 +25,11 @@ export default function Camera({ camera }) {
|
||||
const [viewMode, setViewMode] = useState('live');
|
||||
|
||||
const cameraConfig = config?.cameras[camera];
|
||||
const liveWidth = cameraConfig
|
||||
? Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height))
|
||||
const jsmpegWidth = cameraConfig
|
||||
? Math.round(cameraConfig.restream.jsmpeg.height * (cameraConfig.detect.width / cameraConfig.detect.height))
|
||||
: 0;
|
||||
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(`${camera}-source`, 'mse');
|
||||
const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['mse', 'webrtc', 'jsmpeg'] : ['mse'];
|
||||
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
||||
|
||||
const handleSetOption = useCallback(
|
||||
@@ -51,7 +55,7 @@ export default function Camera({ camera }) {
|
||||
setShowSettings(!showSettings);
|
||||
}, [showSettings, setShowSettings]);
|
||||
|
||||
if (!cameraConfig) {
|
||||
if (!cameraConfig || !sourceIsLoaded) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
@@ -93,13 +97,31 @@ export default function Camera({ camera }) {
|
||||
|
||||
let player;
|
||||
if (viewMode === 'live') {
|
||||
player = (
|
||||
<Fragment>
|
||||
<div>
|
||||
<JSMpegPlayer camera={camera} width={liveWidth} height={cameraConfig.live.height} />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
if (viewSource == 'mse') {
|
||||
player = (
|
||||
<Fragment>
|
||||
<div className="max-w-5xl">
|
||||
<MsePlayer camera={camera} />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (viewSource == 'webrtc') {
|
||||
player = (
|
||||
<Fragment>
|
||||
<div className="max-w-5xl">
|
||||
<WebRtcPlayer camera={camera} />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
player = (
|
||||
<Fragment>
|
||||
<div>
|
||||
<JSMpegPlayer camera={camera} width={jsmpegWidth} height={cameraConfig.restream.jsmpeg.height} />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
} else if (viewMode === 'debug') {
|
||||
player = (
|
||||
<Fragment>
|
||||
@@ -120,7 +142,23 @@ export default function Camera({ camera }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-2 px-4">
|
||||
<Heading size="2xl">{camera.replaceAll('_', ' ')}</Heading>
|
||||
<div className="flex justify-between">
|
||||
<Heading className="p-2" size="2xl">
|
||||
{camera.replaceAll('_', ' ')}
|
||||
</Heading>
|
||||
<select
|
||||
className="basis-1/8 cursor-pointer rounded dark:bg-slate-800"
|
||||
value={viewSource}
|
||||
onChange={(e) => setViewSource(e.target.value)}
|
||||
>
|
||||
{sourceValues.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ButtonsTabbed viewModes={['live', 'debug']} setViewMode={setViewMode} />
|
||||
|
||||
{player}
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('Camera Route', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetOptions = jest.fn();
|
||||
mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions]);
|
||||
mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions, true]);
|
||||
jest.spyOn(AutoUpdatingCameraImage, 'default').mockImplementation(({ searchParams }) => {
|
||||
return <div data-testid="mock-image">{searchParams.toString()}</div>;
|
||||
});
|
||||
@@ -32,11 +32,12 @@ describe('Camera Route', () => {
|
||||
regions: false,
|
||||
},
|
||||
mockSetOptions,
|
||||
true,
|
||||
]);
|
||||
|
||||
render(<Camera camera="front" />);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'), { timeout: 10 });
|
||||
|
||||
fireEvent.click(screen.queryByText('Debug'));
|
||||
fireEvent.click(screen.queryByText('Show Options'));
|
||||
@@ -47,15 +48,20 @@ describe('Camera Route', () => {
|
||||
|
||||
test('updates camera feed options to persistence', async () => {
|
||||
mockUsePersistence
|
||||
.mockReturnValueOnce([{}, mockSetOptions])
|
||||
.mockReturnValueOnce([{}, mockSetOptions])
|
||||
.mockReturnValueOnce([{}, mockSetOptions])
|
||||
.mockReturnValueOnce([{ bbox: true }, mockSetOptions])
|
||||
.mockReturnValueOnce([{ bbox: true, timestamp: true }, mockSetOptions]);
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{ bbox: true }, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{ bbox: true, timestamp: true }, mockSetOptions, true]);
|
||||
|
||||
render(<Camera camera="front" />);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'), { timeout: 10 });
|
||||
|
||||
fireEvent.click(screen.queryByText('Debug'));
|
||||
fireEvent.click(screen.queryByText('Show Options'));
|
||||
@@ -63,9 +69,8 @@ describe('Camera Route', () => {
|
||||
fireEvent.change(screen.queryByTestId('timestamp-input'), { target: { checked: true } });
|
||||
fireEvent.click(screen.queryByText('Hide Options'));
|
||||
|
||||
expect(mockUsePersistence).toHaveBeenCalledTimes(5);
|
||||
expect(mockUsePersistence).toHaveBeenCalledTimes(10);
|
||||
expect(mockSetOptions).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetOptions).toHaveBeenCalledWith({ bbox: true, timestamp: true });
|
||||
expect(screen.queryByTestId('mock-image')).toHaveTextContent('bbox=1×tamp=1');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user