Add on-screen clock overlay (outlined text, configurable corner/timezone)
A small always-on-top mpv window renders the local time in a screen corner. Implementation notes: - Transparent lavfi canvas (color=...@0.0, --alpha=yes) so the camera video shows through; no new dependencies. If the compositor can't do alpha it degrades to a dark backing, still legible. - Time drawn as an ASS osd-overlay pushed over mpv IPC once a second: white fill + black outline (\bord), so it reads on both bright (day) and dark (night) scenes without sampling the picture. Formatting the text in Go avoids any filtergraph escaping. - Time computed with time.LoadLocation against a configured IANA zone (default "Local"), so it's correct regardless of the host clock's zone and handles DST. A bad zone name fails at startup. - Managed on the daemon's own context (survives layout switches); the daemon keeps it positioned and raised above camera tiles. ensureClock is a no-op when the clock config + resolution are unchanged, so a reload never disturbs it. Config: new `clock` section (enabled, timezone, format, corner, font_size, width, height, margin) with defaults and corner validation. Documented in README and config.example (shipped enabled, America/Denver, 24-hour w/ seconds). Tests cover corner geometry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,33 @@ type Config struct {
|
||||
// active layout from its linked UniFi Protect live view (ProtectView).
|
||||
// Requires controller credentials available to the daemon. 0 = off.
|
||||
ViewRefreshSeconds int `yaml:"view_refresh_seconds,omitempty"`
|
||||
|
||||
// Clock overlays a live clock in a screen corner.
|
||||
Clock Clock `yaml:"clock,omitempty"`
|
||||
}
|
||||
|
||||
// Clock configures the on-screen clock overlay: a small always-on-top window
|
||||
// showing the current local time, drawn as outlined white text over the video
|
||||
// so it stays legible on both bright (day) and dark (night) scenes.
|
||||
type Clock struct {
|
||||
// Enabled turns the overlay on.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
// Timezone is an IANA name (e.g. "America/Denver"); "Local" or empty uses
|
||||
// the system timezone. DST is handled automatically.
|
||||
Timezone string `yaml:"timezone,omitempty"`
|
||||
// Format is a Go time layout. Default "15:04:05" (24-hour with seconds).
|
||||
// Examples: "3:04:05 PM", "Mon Jan 2 15:04".
|
||||
Format string `yaml:"format,omitempty"`
|
||||
// Corner places the overlay: bottom-right (default), bottom-left,
|
||||
// top-right, top-left.
|
||||
Corner string `yaml:"corner,omitempty"`
|
||||
// FontSize is the glyph height in pixels (default 44).
|
||||
FontSize int `yaml:"font_size,omitempty"`
|
||||
// Width/Height are the overlay window size in pixels (defaults 300x72).
|
||||
Width int `yaml:"width,omitempty"`
|
||||
Height int `yaml:"height,omitempty"`
|
||||
// Margin is the gap from the screen edges in pixels (default 24).
|
||||
Margin int `yaml:"margin,omitempty"`
|
||||
}
|
||||
|
||||
// Controller holds UniFi Protect connection details.
|
||||
@@ -306,6 +333,29 @@ func (c *Config) Defaults() {
|
||||
if c.Player.RestartBackoffSeconds == 0 {
|
||||
c.Player.RestartBackoffSeconds = 3
|
||||
}
|
||||
if c.Clock.Enabled {
|
||||
if c.Clock.Timezone == "" {
|
||||
c.Clock.Timezone = "Local"
|
||||
}
|
||||
if c.Clock.Format == "" {
|
||||
c.Clock.Format = "15:04:05"
|
||||
}
|
||||
if c.Clock.Corner == "" {
|
||||
c.Clock.Corner = "bottom-right"
|
||||
}
|
||||
if c.Clock.FontSize == 0 {
|
||||
c.Clock.FontSize = 44
|
||||
}
|
||||
if c.Clock.Width == 0 {
|
||||
c.Clock.Width = 300
|
||||
}
|
||||
if c.Clock.Height == 0 {
|
||||
c.Clock.Height = 72
|
||||
}
|
||||
if c.Clock.Margin == 0 {
|
||||
c.Clock.Margin = 24
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks referential integrity and returns the first problem found.
|
||||
@@ -354,6 +404,14 @@ func (c *Config) Validate() error {
|
||||
if c.ActiveLayout != "" && !layoutNames[c.ActiveLayout] {
|
||||
return fmt.Errorf("active_layout %q is not a defined layout", c.ActiveLayout)
|
||||
}
|
||||
|
||||
if c.Clock.Enabled && c.Clock.Corner != "" {
|
||||
switch c.Clock.Corner {
|
||||
case "bottom-right", "bottom-left", "top-right", "top-left":
|
||||
default:
|
||||
return fmt.Errorf("clock.corner %q must be one of bottom-right, bottom-left, top-right, top-left", c.Clock.Corner)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user