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
4.9 KiB
Dart
190 lines
4.9 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../../../data/services/bridge_service.dart';
|
|
import '../../../data/services/coordination_service.dart';
|
|
import 'ptz_event.dart';
|
|
import 'ptz_state.dart';
|
|
|
|
class PtzBloc extends Bloc<PtzEvent, PtzState> {
|
|
final BridgeService _bridgeService;
|
|
final CoordinationService _coordinationService;
|
|
|
|
Timer? _lockResetTimer;
|
|
|
|
PtzBloc({
|
|
required BridgeService bridgeService,
|
|
required CoordinationService coordinationService,
|
|
}) : _bridgeService = bridgeService,
|
|
_coordinationService = coordinationService,
|
|
super(const PtzState()) {
|
|
on<PtzPanStart>(_onPanStart);
|
|
on<PtzTiltStart>(_onTiltStart);
|
|
on<PtzZoomStart>(_onZoomStart);
|
|
on<PtzStop>(_onStop);
|
|
on<PtzGoToPreset>(_onGoToPreset);
|
|
on<PtzSetCamera>(_onSetCamera);
|
|
}
|
|
|
|
/// Ensure we have a lock on the camera before PTZ movement.
|
|
/// Returns true if lock was acquired or already held.
|
|
Future<bool> _ensureLock(int cameraId, Emitter<PtzState> emit) async {
|
|
// Already locked by us
|
|
if (_coordinationService.isCameraLockedByMe(cameraId)) {
|
|
emit(state.copyWith(lockStatus: PtzLockStatus.locked));
|
|
return true;
|
|
}
|
|
|
|
emit(state.copyWith(lockStatus: PtzLockStatus.acquiring));
|
|
|
|
final result = await _coordinationService.tryLock(cameraId);
|
|
if (result.acquired) {
|
|
emit(state.copyWith(lockStatus: PtzLockStatus.locked));
|
|
_startLockResetTimer(cameraId);
|
|
return true;
|
|
}
|
|
|
|
// Lock denied — someone else has it
|
|
emit(state.copyWith(
|
|
lockStatus: PtzLockStatus.denied,
|
|
lockedBy: result.lock?.ownerName,
|
|
error: 'Camera locked by ${result.lock?.ownerName ?? "another keyboard"}',
|
|
));
|
|
return false;
|
|
}
|
|
|
|
void _startLockResetTimer(int cameraId) {
|
|
_lockResetTimer?.cancel();
|
|
_lockResetTimer = Timer.periodic(const Duration(minutes: 2), (_) {
|
|
if (_coordinationService.isCameraLockedByMe(cameraId)) {
|
|
_coordinationService.resetExpiration(cameraId);
|
|
} else {
|
|
_lockResetTimer?.cancel();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _onPanStart(
|
|
PtzPanStart event,
|
|
Emitter<PtzState> emit,
|
|
) async {
|
|
emit(state.copyWith(activeCameraId: event.cameraId));
|
|
|
|
if (!await _ensureLock(event.cameraId, emit)) return;
|
|
|
|
final direction =
|
|
event.direction == 'left' ? PtzDirection.left : PtzDirection.right;
|
|
emit(state.copyWith(
|
|
currentDirection: direction,
|
|
isMoving: true,
|
|
error: null,
|
|
));
|
|
|
|
try {
|
|
await _bridgeService.ptzPan(event.cameraId, event.direction, event.speed);
|
|
} catch (e) {
|
|
emit(state.copyWith(error: e.toString(), isMoving: false));
|
|
}
|
|
}
|
|
|
|
Future<void> _onTiltStart(
|
|
PtzTiltStart event,
|
|
Emitter<PtzState> emit,
|
|
) async {
|
|
emit(state.copyWith(activeCameraId: event.cameraId));
|
|
|
|
if (!await _ensureLock(event.cameraId, emit)) return;
|
|
|
|
final direction =
|
|
event.direction == 'up' ? PtzDirection.up : PtzDirection.down;
|
|
emit(state.copyWith(
|
|
currentDirection: direction,
|
|
isMoving: true,
|
|
error: null,
|
|
));
|
|
|
|
try {
|
|
await _bridgeService.ptzTilt(
|
|
event.cameraId, event.direction, event.speed);
|
|
} catch (e) {
|
|
emit(state.copyWith(error: e.toString(), isMoving: false));
|
|
}
|
|
}
|
|
|
|
Future<void> _onZoomStart(
|
|
PtzZoomStart event,
|
|
Emitter<PtzState> emit,
|
|
) async {
|
|
emit(state.copyWith(activeCameraId: event.cameraId));
|
|
|
|
if (!await _ensureLock(event.cameraId, emit)) return;
|
|
|
|
final direction =
|
|
event.direction == 'in' ? PtzDirection.zoomIn : PtzDirection.zoomOut;
|
|
emit(state.copyWith(
|
|
currentDirection: direction,
|
|
isMoving: true,
|
|
error: null,
|
|
));
|
|
|
|
try {
|
|
await _bridgeService.ptzZoom(
|
|
event.cameraId, event.direction, event.speed);
|
|
} catch (e) {
|
|
emit(state.copyWith(error: e.toString(), isMoving: false));
|
|
}
|
|
}
|
|
|
|
Future<void> _onStop(
|
|
PtzStop event,
|
|
Emitter<PtzState> emit,
|
|
) async {
|
|
emit(state.copyWith(
|
|
currentDirection: PtzDirection.none,
|
|
isMoving: false,
|
|
error: null,
|
|
));
|
|
|
|
try {
|
|
await _bridgeService.ptzStop(event.cameraId);
|
|
} catch (e) {
|
|
emit(state.copyWith(error: e.toString()));
|
|
}
|
|
}
|
|
|
|
Future<void> _onGoToPreset(
|
|
PtzGoToPreset event,
|
|
Emitter<PtzState> emit,
|
|
) async {
|
|
if (!await _ensureLock(event.cameraId, emit)) return;
|
|
|
|
emit(state.copyWith(error: null));
|
|
|
|
try {
|
|
await _bridgeService.ptzPreset(event.cameraId, event.preset);
|
|
} catch (e) {
|
|
emit(state.copyWith(error: e.toString()));
|
|
}
|
|
}
|
|
|
|
void _onSetCamera(
|
|
PtzSetCamera event,
|
|
Emitter<PtzState> emit,
|
|
) {
|
|
if (event.cameraId == null) {
|
|
_lockResetTimer?.cancel();
|
|
emit(state.copyWith(
|
|
clearCamera: true, lockStatus: PtzLockStatus.none));
|
|
} else {
|
|
emit(state.copyWith(activeCameraId: event.cameraId));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> close() {
|
|
_lockResetTimer?.cancel();
|
|
return super.close();
|
|
}
|
|
}
|