Flutter web app replacing legacy WPF CCTV surveillance keyboard controller. Includes wall overview, section view with monitor grid, camera input, PTZ control, alarm/lock/sequence BLoCs, and legacy-matching UI styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.1 KiB
title, description
| title | description |
|---|---|
| Data Flows | CrossSwitch, Alarms, Sequences, Playback, and Camera Lock workflows |
Data Flows
1. CrossSwitch (Switch Camera to Monitor)
The primary operation — routing a camera feed to a physical monitor output.
sequenceDiagram
participant Op as Operator
participant VM as SegmentViewModel
participant CenD as CentralServerDriver
participant Srv as Camera Server
Op->>VM: Enter camera number (digits + Enter)
VM->>VM: Validate camera exists via MediaChannelService
alt Camera exists
alt Playback active on this viewer
VM->>VM: playbackStateService.SetPlaybackState(false)
end
alt Sequence active on this viewer
VM->>VM: hub.Stop(viewerId)
end
VM->>CenD: CrossSwitch(viewerId, cameraNumber)
CenD->>Srv: SDK CrossSwitch command
Note over Srv: Server switches video output<br/>and fires ViewerConnectionEvent
else Camera not found
VM->>VM: ShowError("MediaChannel does not exist", 2s)
end
CrossSwitch Triggers
- Numeric entry — Type camera number + Enter
- Plus/Minus keys — Cycle to next/previous camera in sorted order
- Function buttons — F1-F7 mapped to preconfigured CrossSwitch actions
- Home key — When not in PTZ mode or playback, executes function button "Home"
Camera Number Editing
State: Not Editing
│
├─ Press digit → BeginEdit(), append digit, start cancel timer
│
▼
State: Editing (showing entered digits)
│
├─ Press digit → append digit, restart cancel timer
├─ Press Backspace → remove last digit, restart cancel timer
├─ Press Enter → validate, CrossSwitch if valid, EndEdit()
└─ Cancel timer fires → CancelEdit(), revert to original camera
Camera number can be up to 6 digits. Configurable prefixes (e.g., "500", "501", "502") are prepended automatically based on the selected prefix.
2. Camera Lock Workflow
Camera locks coordinate PTZ access across multiple keyboards via the AppServer SignalR hub.
sequenceDiagram
participant K1 as Keyboard 1 (Low priority)
participant Hub as AppServer Hub
participant DB as SQLite DB
participant K2 as Keyboard 2 (High priority)
K1->>Hub: TryLockCamera(camId, "K1", Low)
Hub->>DB: Check existing lock
DB-->>Hub: No lock exists
Hub->>DB: Create lock(camId, "K1", Low, expires=5min)
Hub-->>K1: LockResult(Acquired=true)
K1->>K1: IsCameraTelemetryActive = true
Note over K1: Operator controls PTZ...<br/>Each action calls ResetCameraLockExpiration()
K2->>Hub: TryLockCamera(camId, "K2", High)
Hub->>DB: Check existing lock → held by K1
Hub-->>K2: LockResult(Acquired=false, Owner="K1")
K2->>Hub: RequestCameraLock(camId, "K2", High)
Hub->>K1: CameraLockNotify(ConfirmTakeOver)
K1->>K1: Show dialog "K2 requests lock"
alt K1 confirms takeover
K1->>Hub: CameraLockConfirmTakeOver(confirm=true)
Hub->>K1: CameraLockNotify(Unlocked)
Hub->>K2: CameraLockNotify(Confirmed)
K2->>Hub: TryLockCamera(camId, "K2", High)
Hub-->>K2: LockResult(Acquired=true)
else K1 denies
K1->>Hub: CameraLockConfirmTakeOver(confirm=false)
Hub->>K2: CameraLockNotify(Rejected)
end
Lock Priority Levels
Configured per keyboard in appsettings-copilot.json:
Low— Standard operatorHigh— Supervisor (can request takeover)
Lock Expiration
- Timeout: 5 minutes (
LockExpirationConfig.LockExpirationTimeout) - Warning: 1 minute before expiration (
NotificationBeforeExpiration) - Reset: Every PTZ action or explicit reset call extends the timer
3. Alarm System
sequenceDiagram
participant Worker as CameraAlarmsUpdateWorker
participant CAS as CameraAlarmService
participant Hub as CopilotHub
participant AS as AppServer
participant Camea as Camea API
Note over Worker: Background worker (periodic)
Worker->>CAS: UpdateCameraToAlarmsMapping()
CAS->>Hub: GetCamerasWithAlarms()
Hub->>AS: Query
AS->>Camea: GET /api/alarms
Camea-->>AS: List of cameras with active alarms
AS-->>Hub: HashSet<int> cameraIds
Hub-->>CAS: Store in camerasWithAlarms
Note over CAS: UI can now check HasCameraAlarms(cameraId)
Note over Worker: Also on SegmentViewModel...
participant VM as SegmentViewModel
VM->>CAS: GetAlarmForCamera(camId, from, to)
CAS->>Hub: GetAlarmsForCamera(camId, from, to)
Hub->>AS: Query alarm history
AS-->>Hub: List<CameraAlarm>
Hub-->>CAS: Alarms (UTC → LocalTime)
CAS-->>VM: Sorted by StartTime desc
Alarm-Related Behavior
- Screens with active alarms block camera number editing (CrossSwitch disabled)
- Alarm history page uses configurable regex to extract camera ID from alarm names:
^\D*(?<CameraId>\d{6})\D*$ - Default search interval: 30 days
4. Sequence Management
Sequences automatically cycle cameras on a monitor at configurable intervals.
sequenceDiagram
participant Op as Operator
participant VM as SegmentViewModel
participant SeqS as SequenceService
participant Hub as SignalR Hub
participant AS as AppServer
Op->>VM: Press Sequence key
VM->>VM: Navigate to SequenceCategoriesPage
Op->>VM: Select category
VM->>SeqS: GetSequences(categoryId)
SeqS->>Hub: GetSequences(categoryId)
Hub-->>SeqS: List<SequenceMessage>
Op->>VM: Select sequence
VM->>SeqS: Start(viewerId, sequenceId)
SeqS->>Hub: Start(viewerId, sequenceId)
Note over AS: AppServer runs sequence timer
AS->>Hub: ViewerSequenceStateChanged
Hub->>SeqS: OnChanged(viewerSequenceState)
SeqS->>VM: Update UI (sequence indicator)
Note over AS: Timer fires every N seconds
AS->>AS: CrossSwitch next camera in list
Sequence Configuration
{
"Id": 1,
"CategoryId": 1,
"Name": "Test-A",
"Interval": 1, // seconds between switches
"MediaChannels": [500100, 500101, 500102, 500103]
}
Sequences are run on the AppServer, not on the keyboard. The keyboard only starts/stops them.
5. Playback Control
sequenceDiagram
participant Op as Operator
participant VM as SegmentViewModel
participant VC as IViewerController
participant Srv as Camera Server
Note over Op: Playback enters via Jog/Shuttle<br/>or Playback button
Op->>VM: Jog right (arrow key)
VM->>VM: IsPlaybackActive = true
VM->>VC: StepForwardAndStop()
VC->>Srv: Step forward one frame
Op->>VM: Shuttle right position 3
VM->>VM: TryGetShuttleSpeed(speeds, out speed, out forward)
Note over VM: position 3 → speeds[2] = 5
VM->>VC: FastForward(5)
VC->>Srv: Play at 5x speed
Op->>VM: Shuttle center
VM->>VC: Stop()
Op->>VM: Press Home key
VM->>VC: PlayLive()
VM->>VM: IsPlaybackActive = false
Playback Controls
| Input | Action | SDK Method |
|---|---|---|
| Jog Left | Step back 1 frame | StepBackwardAndStop() |
| Jog Right | Step forward 1 frame | StepForwardAndStop() |
| Shuttle Left 1-7 | Rewind at speed | FastBackward(speed) |
| Shuttle Right 1-7 | Fast forward at speed | FastForward(speed) |
| Shuttle Center | Pause | Stop() |
| Home | Return to live | PlayLive() |
6. Function Buttons
F1-F7 and Home keys execute preconfigured action lists per wall:
graph TD
Key["F1-F7 / Home Key Press"] --> FBS["FunctionButtonsService"]
FBS --> Config["Load from appsettings-function-buttons.json"]
Config --> Actions["Get actions for wallId + buttonKey"]
Actions --> ForEach["For each action:"]
ForEach -->|CrossSwitch| CS["CentralServerDriver.CrossSwitch(viewerId, sourceId)<br/>+ Minimize viewer"]
ForEach -->|SequenceStart| SS["SequenceService.Start(viewerId, sourceId)"]
Example: Pressing F1 on Wall 2 executes:
{ "ViewerId": 12322, "ActionType": "CrossSwitch", "SourceId": 500123 }
This switches camera 500123 to viewer 12322.
7. Configuration Sync
Configuration files are managed centrally on the AppServer and synced to keyboards:
sequenceDiagram
participant AS as AppServer
participant Hub as SignalR Hub
participant CfgS as ConfigurationService
participant Disk as Local JSON Files
Note over CfgS: On startup
CfgS->>Hub: GetConfigurationFile(filename) for each manager
Hub-->>CfgS: ConfigurationFile (content + hash)
CfgS->>Disk: Compare hash, write if changed
Note over AS: Admin changes config via Blazor UI
AS->>Hub: ConfigurationFileChanged event
Hub->>CfgS: OnConfigurationFileChanged(file)
CfgS->>Disk: Write updated content
Note over CfgS: Also triggered when AppServer<br/>becomes available after disconnect
Configuration files synced:
appsettings-copilot.json— Keyboard-specific settingsappsettings-camera-servers.json— Camera server connectionsappsettings-monitor-wall.json— Monitor wall topologyappsettings-function-buttons.json— Function button actionsappsettings-prepositions.json— Camera prepositionsappsettings-sequences.json— Sequence definitionsappsettings-sequence-categories.json— Sequence categories