Add option for live mode & timezone config, fix MSE check for iPad (#5079)

* Add config fields

* Clean up camera default values

* Set recordings timezone with config if available

* Adjust for timezone config

* Cleanup setting of the timezone

* Don't fail on MSE check iPad

* Fix MSE check for birdseye

* Add docs

* Fix test
This commit is contained in:
Nicolas Mowen 2023-01-13 16:27:16 -07:00 committed by GitHub
parent 170899bd71
commit e0b3b27b8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 19 deletions

View File

@ -486,4 +486,13 @@ cameras:
order: 0 order: 0
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below) # Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
dashboard: True dashboard: True
# Optional
ui:
# Optional: Set the default live mode for cameras in the UI (default: shown below)
live_mode: mse
# Optional: Set a timezone to use in the UI (default: use browser local time)
timezone: None
# Optional: Use an experimental recordings / camera view UI (default: shown below)
experimental_ui: False
``` ```

View File

@ -60,7 +60,17 @@ class FrigateBaseModel(BaseModel):
extra = Extra.forbid extra = Extra.forbid
class LiveModeEnum(str, Enum):
jsmpeg = "jsmpeg"
mse = "mse"
webrtc = "webrtc"
class UIConfig(FrigateBaseModel): class UIConfig(FrigateBaseModel):
live_mode: LiveModeEnum = Field(
default=LiveModeEnum.mse, title="Default Live Mode."
)
timezone: Optional[str] = Field(title="Override UI timezone.")
use_experimental: bool = Field(default=False, title="Experimental UI") use_experimental: bool = Field(default=False, title="Experimental UI")

View File

@ -6,7 +6,6 @@ import Heading from '../components/Heading';
import WebRtcPlayer from '../components/WebRtcPlayer'; import WebRtcPlayer from '../components/WebRtcPlayer';
import MsePlayer from '../components/MsePlayer'; import MsePlayer from '../components/MsePlayer';
import useSWR from 'swr'; import useSWR from 'swr';
import videojs from 'video.js';
export default function Birdseye() { export default function Birdseye() {
const { data: config } = useSWR('config'); const { data: config } = useSWR('config');
@ -20,19 +19,19 @@ export default function Birdseye() {
let player; let player;
if (viewSource == 'mse' && config.restream.birdseye) { if (viewSource == 'mse' && config.restream.birdseye) {
if (videojs.browser.IS_IOS) { if ('MediaSource' in window) {
player = ( player = (
<Fragment> <Fragment>
<div className="w-5xl text-center text-sm"> <div className="max-w-5xl">
MSE is not supported on iOS devices. You'll need to use jsmpeg or webRTC. See the docs for more info. <MsePlayer camera="birdseye" />
</div> </div>
</Fragment> </Fragment>
); );
} else { } else {
player = ( player = (
<Fragment> <Fragment>
<div className="max-w-5xl"> <div className="w-5xl text-center text-sm">
<MsePlayer camera="birdseye" /> MSE is not supported on iOS devices. You'll need to use jsmpeg or webRTC. See the docs for more info.
</div> </div>
</Fragment> </Fragment>
); );

View File

@ -15,7 +15,6 @@ import { useApiHost } from '../api';
import useSWR from 'swr'; import useSWR from 'swr';
import WebRtcPlayer from '../components/WebRtcPlayer'; import WebRtcPlayer from '../components/WebRtcPlayer';
import MsePlayer from '../components/MsePlayer'; import MsePlayer from '../components/MsePlayer';
import videojs from 'video.js';
const emptyObject = Object.freeze({}); const emptyObject = Object.freeze({});
@ -29,7 +28,10 @@ export default function Camera({ camera }) {
const jsmpegWidth = cameraConfig const jsmpegWidth = cameraConfig
? Math.round(cameraConfig.restream.jsmpeg.height * (cameraConfig.detect.width / cameraConfig.detect.height)) ? Math.round(cameraConfig.restream.jsmpeg.height * (cameraConfig.detect.width / cameraConfig.detect.height))
: 0; : 0;
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(`${camera}-source`, 'mse'); const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(
`${camera}-source`,
getDefaultLiveMode(config, cameraConfig)
);
const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['mse', 'webrtc', 'jsmpeg'] : ['jsmpeg']; const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['mse', 'webrtc', 'jsmpeg'] : ['jsmpeg'];
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject); const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
@ -77,7 +79,13 @@ export default function Camera({ camera }) {
labelPosition="after" labelPosition="after"
/> />
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" /> <Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" />
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} label="Motion Masks" labelPosition="after" /> <Switch
checked={options['mask']}
id="mask"
onChange={handleSetOption}
label="Motion Masks"
labelPosition="after"
/>
<Switch <Switch
checked={options['motion']} checked={options['motion']}
id="motion" id="motion"
@ -99,19 +107,19 @@ export default function Camera({ camera }) {
let player; let player;
if (viewMode === 'live') { if (viewMode === 'live') {
if (viewSource == 'mse' && cameraConfig.restream.enabled) { if (viewSource == 'mse' && cameraConfig.restream.enabled) {
if (videojs.browser.IS_IOS) { if ('MediaSource' in window) {
player = ( player = (
<Fragment> <Fragment>
<div className="w-5xl text-center text-sm"> <div className="max-w-5xl">
MSE is not supported on iOS devices. You'll need to use jsmpeg or webRTC. See the docs for more info. <MsePlayer camera={camera} />
</div> </div>
</Fragment> </Fragment>
); );
} else { } else {
player = ( player = (
<Fragment> <Fragment>
<div className="max-w-5xl"> <div className="w-5xl text-center text-sm">
<MsePlayer camera={camera} /> MSE is not supported on iOS devices. You'll need to use jsmpeg or webRTC. See the docs for more info.
</div> </div>
</Fragment> </Fragment>
); );
@ -191,3 +199,15 @@ export default function Camera({ camera }) {
</div> </div>
); );
} }
function getDefaultLiveMode(config, cameraConfig) {
if (cameraConfig) {
if (cameraConfig.restream.enabled) {
return config.ui.live_mode;
}
return 'jsmpeg';
}
return undefined;
}

View File

@ -122,7 +122,7 @@ export default function Events({ path, ...props }) {
return memo; return memo;
}, config?.objects?.track || []) }, config?.objects?.track || [])
.filter((value, i, self) => self.indexOf(value) === i), .filter((value, i, self) => self.indexOf(value) === i),
sub_labels: (allSubLabels || []).length > 0 ? [...Object.values(allSubLabels), "None"] : [], sub_labels: (allSubLabels || []).length > 0 ? [...Object.values(allSubLabels), 'None'] : [],
}), }),
[config, allSubLabels] [config, allSubLabels]
); );
@ -295,6 +295,8 @@ export default function Events({ path, ...props }) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
const timezone = config.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
return ( return (
<div className="space-y-4 p-2 px-4 w-full"> <div className="space-y-4 p-2 px-4 w-full">
<Heading>Events</Heading> <Heading>Events</Heading>
@ -512,8 +514,8 @@ export default function Events({ path, ...props }) {
({(event.top_score * 100).toFixed(0)}%) ({(event.top_score * 100).toFixed(0)}%)
</div> </div>
<div className="text-sm"> <div className="text-sm">
{new Date(event.start_time * 1000).toLocaleDateString()}{' '} {new Date(event.start_time * 1000).toLocaleDateString({ timeZone: timezone })}{' '}
{new Date(event.start_time * 1000).toLocaleTimeString()} ( {new Date(event.start_time * 1000).toLocaleTimeString({ timeZone: timezone })} (
{clipDuration(event.start_time, event.end_time)}) {clipDuration(event.start_time, event.end_time)})
</div> </div>
<div className="capitalize text-sm flex align-center mt-1"> <div className="capitalize text-sm flex align-center mt-1">

View File

@ -9,7 +9,8 @@ import { useApiHost } from '../api';
import useSWR from 'swr'; import useSWR from 'swr';
export default function Recording({ camera, date, hour = '00', minute = '00', second = '00' }) { export default function Recording({ camera, date, hour = '00', minute = '00', second = '00' }) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const { data: config } = useSWR('config');
let timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const currentDate = useMemo( const currentDate = useMemo(
() => (date ? parseISO(`${date}T${hour || '00'}:${minute || '00'}:${second || '00'}`) : new Date()), () => (date ? parseISO(`${date}T${hour || '00'}:${minute || '00'}:${second || '00'}`) : new Date()),
[date, hour, minute, second] [date, hour, minute, second]
@ -113,10 +114,14 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
} }
}, [seekSeconds, playlistIndex]); }, [seekSeconds, playlistIndex]);
if (!recordingsSummary || !recordings) { if (!recordingsSummary || !recordings || !config) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
if (config.ui.timezone) {
timezone = config.ui.timezone;
}
if (recordingsSummary.length === 0) { if (recordingsSummary.length === 0) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">