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 { final BridgeService _bridgeService; final CoordinationService _coordinationService; Timer? _lockResetTimer; PtzBloc({ required BridgeService bridgeService, required CoordinationService coordinationService, }) : _bridgeService = bridgeService, _coordinationService = coordinationService, super(const PtzState()) { on(_onPanStart); on(_onTiltStart); on(_onZoomStart); on(_onStop); on(_onGoToPreset); on(_onSetCamera); } /// Ensure we have a lock on the camera before PTZ movement. /// Returns true if lock was acquired or already held. Future _ensureLock(int cameraId, Emitter 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 _onPanStart( PtzPanStart event, Emitter 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 _onTiltStart( PtzTiltStart event, Emitter 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 _onZoomStart( PtzZoomStart event, Emitter 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 _onStop( PtzStop event, Emitter 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 _onGoToPreset( PtzGoToPreset event, Emitter 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 emit, ) { if (event.cameraId == null) { _lockResetTimer?.cancel(); emit(state.copyWith( clearCamera: true, lockStatus: PtzLockStatus.none)); } else { emit(state.copyWith(activeCameraId: event.cameraId)); } } @override Future close() { _lockResetTimer?.cancel(); return super.close(); } }