Files
COPILOT/copilot_keyboard/lib/presentation/blocs/ptz/ptz_bloc.dart
klas 40143734fc Initial commit: COPILOT D6 Flutter keyboard controller
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>
2026-02-12 14:57:38 +01:00

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();
}
}