mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-12-19 19:06:16 +01:00
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:
parent
170899bd71
commit
e0b3b27b8a
@ -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
|
||||||
```
|
```
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user