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>
190 lines
5.6 KiB
Dart
190 lines
5.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:logger/logger.dart';
|
|
import 'package:rxdart/rxdart.dart';
|
|
|
|
import '../../domain/entities/monitor_state.dart';
|
|
import '../../domain/entities/alarm_state.dart';
|
|
import '../models/bridge_event.dart';
|
|
import 'bridge_service.dart';
|
|
import 'alarm_service.dart';
|
|
|
|
/// Service for tracking overall system state (monitors + alarms)
|
|
class StateService {
|
|
final BridgeService _bridgeService;
|
|
final AlarmService _alarmService;
|
|
final Logger _logger = Logger();
|
|
|
|
StreamSubscription? _eventSubscription;
|
|
|
|
// Monitor state
|
|
final _monitorStatesController =
|
|
BehaviorSubject<Map<int, MonitorState>>.seeded({});
|
|
|
|
// Combined state stream (monitors with alarm flags)
|
|
final _combinedMonitorStatesController =
|
|
BehaviorSubject<Map<int, MonitorState>>.seeded({});
|
|
|
|
StateService({
|
|
required BridgeService bridgeService,
|
|
required AlarmService alarmService,
|
|
}) : _bridgeService = bridgeService,
|
|
_alarmService = alarmService;
|
|
|
|
/// Stream of monitor states (without alarm info)
|
|
Stream<Map<int, MonitorState>> get monitorStates =>
|
|
_monitorStatesController.stream;
|
|
|
|
/// Stream of monitor states with alarm flags
|
|
Stream<Map<int, MonitorState>> get combinedMonitorStates =>
|
|
_combinedMonitorStatesController.stream;
|
|
|
|
/// Stream of active alarms (delegated to AlarmService)
|
|
Stream<List<AlarmState>> get activeAlarms => _alarmService.alarms;
|
|
|
|
/// Current monitor states
|
|
Map<int, MonitorState> get currentMonitorStates =>
|
|
_monitorStatesController.value;
|
|
|
|
/// Get state for a specific monitor
|
|
MonitorState? getMonitorState(int viewerId) {
|
|
return _combinedMonitorStatesController.value[viewerId];
|
|
}
|
|
|
|
/// Initialize state tracking
|
|
Future<void> initialize() async {
|
|
// Subscribe to bridge events
|
|
_eventSubscription = _bridgeService.eventStream.listen(_handleBridgeEvent);
|
|
|
|
// Subscribe to alarm changes to update monitor flags
|
|
_alarmService.alarms.listen((_) => _updateCombinedStates());
|
|
|
|
_logger.i('StateService initialized');
|
|
}
|
|
|
|
/// Sync initial state from all bridges
|
|
Future<void> syncFromBridges() async {
|
|
_logger.i('Syncing state from bridges...');
|
|
|
|
final connectionStatus = _bridgeService.currentConnectionStatus;
|
|
final monitors = <int, MonitorState>{};
|
|
|
|
for (final entry in connectionStatus.entries) {
|
|
if (!entry.value) continue; // Skip disconnected servers
|
|
|
|
try {
|
|
final serverMonitors =
|
|
await _bridgeService.getMonitorStates(entry.key);
|
|
|
|
for (final monitorJson in serverMonitors) {
|
|
final state = MonitorState.fromJson(monitorJson);
|
|
monitors[state.viewerId] = state;
|
|
}
|
|
|
|
_logger.d(
|
|
'Synced ${serverMonitors.length} monitors from ${entry.key}');
|
|
} catch (e) {
|
|
_logger.e('Failed to sync monitors from ${entry.key}: $e');
|
|
}
|
|
}
|
|
|
|
_monitorStatesController.add(monitors);
|
|
_updateCombinedStates();
|
|
|
|
// Also sync alarms
|
|
await _alarmService.queryAllAlarms();
|
|
|
|
_logger.i('State sync complete: ${monitors.length} monitors');
|
|
}
|
|
|
|
/// Handle incoming bridge event
|
|
void _handleBridgeEvent(BridgeEvent event) {
|
|
if (event.isViewerConnected || event.isViewerSelectionChanged) {
|
|
_handleViewerConnected(event);
|
|
} else if (event.isViewerCleared) {
|
|
_handleViewerCleared(event);
|
|
} else if (event.isEventStarted ||
|
|
event.isEventStopped ||
|
|
event.isAlarmQueueNotification) {
|
|
// Delegate alarm events to AlarmService
|
|
_alarmService.handleAlarmEvent(event);
|
|
}
|
|
}
|
|
|
|
/// Handle viewer connected event
|
|
void _handleViewerConnected(BridgeEvent event) {
|
|
final viewer = event.viewer;
|
|
final channel = event.channel;
|
|
final playMode = event.playMode;
|
|
|
|
if (viewer == null) return;
|
|
|
|
final monitors = Map<int, MonitorState>.from(_monitorStatesController.value);
|
|
final existing = monitors[viewer];
|
|
|
|
monitors[viewer] = MonitorState(
|
|
viewerId: viewer,
|
|
currentChannel: channel ?? existing?.currentChannel ?? 0,
|
|
playMode: PlayMode.fromValue(playMode ?? existing?.playMode.value ?? 0),
|
|
serverId: event.serverId,
|
|
lastUpdated: event.timestamp,
|
|
);
|
|
|
|
_monitorStatesController.add(monitors);
|
|
_updateCombinedStates();
|
|
|
|
_logger.d('Monitor $viewer connected to channel $channel');
|
|
}
|
|
|
|
/// Handle viewer cleared event
|
|
void _handleViewerCleared(BridgeEvent event) {
|
|
final viewer = event.viewer;
|
|
if (viewer == null) return;
|
|
|
|
final monitors = Map<int, MonitorState>.from(_monitorStatesController.value);
|
|
final existing = monitors[viewer];
|
|
|
|
if (existing != null) {
|
|
monitors[viewer] = existing.cleared();
|
|
} else {
|
|
monitors[viewer] = MonitorState(
|
|
viewerId: viewer,
|
|
currentChannel: 0,
|
|
playMode: PlayMode.unknown,
|
|
serverId: event.serverId,
|
|
lastUpdated: event.timestamp,
|
|
);
|
|
}
|
|
|
|
_monitorStatesController.add(monitors);
|
|
_updateCombinedStates();
|
|
|
|
_logger.d('Monitor $viewer cleared');
|
|
}
|
|
|
|
/// Update combined states with alarm flags
|
|
void _updateCombinedStates() {
|
|
final monitors = _monitorStatesController.value;
|
|
final combined = <int, MonitorState>{};
|
|
|
|
for (final entry in monitors.entries) {
|
|
final hasAlarm = _alarmService.isMonitorBlocked(entry.key);
|
|
combined[entry.key] = entry.value.withAlarm(hasAlarm);
|
|
}
|
|
|
|
_combinedMonitorStatesController.add(combined);
|
|
}
|
|
|
|
/// Check if a monitor is blocked by an alarm
|
|
bool isMonitorBlocked(int viewerId) {
|
|
return _alarmService.isMonitorBlocked(viewerId);
|
|
}
|
|
|
|
/// Dispose resources
|
|
void dispose() {
|
|
_eventSubscription?.cancel();
|
|
_monitorStatesController.close();
|
|
_combinedMonitorStatesController.close();
|
|
}
|
|
}
|