Files
MatchLiveTv/mobile/lib/shared/websocket_service.dart
Emiliano Frascaro bba6df52c0 Initial commit: monorepo Match Live TV.
Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 17:45:37 +02:00

159 lines
4.1 KiB
Dart

import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:x_action_cable_v2/x_action_cable.dart';
import '../core/config.dart';
import '../providers/auth_provider.dart';
import '../providers/score_provider.dart';
import '../providers/session_provider.dart';
import 'models/device_state.dart';
import 'models/score_state.dart';
final websocketServiceProvider = Provider<WebsocketService>((ref) {
final service = WebsocketService(ref);
ref.onDispose(service.dispose);
return service;
});
typedef CableMessageHandler = void Function(Map<String, dynamic> data);
class WebsocketService {
WebsocketService(this._ref);
final Ref _ref;
ActionCable? _cable;
ActionChannel? _channel;
String? _sessionId;
bool get isConnected => _cable != null && _channel != null;
Future<void> connect({
required String sessionId,
required String deviceRole,
}) async {
if (_sessionId == sessionId && isConnected) return;
await disconnect();
final token = _ref.read(authProvider).accessToken;
if (token == null || token.isEmpty) {
throw StateError('Token di accesso mancante');
}
_sessionId = sessionId;
_cable = ActionCable.connect(
AppConfig.cableUrl,
headers: {'Authorization': 'Bearer $token'},
onConnected: () {
_ref.read(sessionProvider.notifier).setConnected(true);
},
onConnectionLost: () {
_ref.read(sessionProvider.notifier).setConnected(false);
},
onCannotConnect: (_) {
_ref.read(sessionProvider.notifier).setConnected(false);
},
);
_channel = _cable!.subscribe(
'SessionChannel',
channelParams: {
'session_id': sessionId,
'device_role': deviceRole,
},
onSubscribed: () {
_ref.read(sessionProvider.notifier).setConnected(true);
},
onDisconnected: () {
_ref.read(sessionProvider.notifier).setConnected(false);
},
callbacks: [
ActionCallback(
name: 'receive_message',
callback: _handleMessage,
),
],
);
}
void _handleMessage(ActionResponse response) {
if (response.hasError || response.data == null) return;
_dispatch(Map<String, dynamic>.from(response.data!));
}
void _dispatch(Map<String, dynamic> data) {
final type = data['type'] as String?;
switch (type) {
case 'score_update':
_ref.read(scoreProvider.notifier).applyRemote(ScoreState.fromJson(data));
break;
case 'device_state':
_ref
.read(sessionProvider.notifier)
.updateDeviceState(DeviceStateModel.fromJson(data));
break;
case 'timeout':
final team = data['team'] as String?;
if (team == 'home') {
_ref.read(scoreProvider.notifier).setTimeoutHome(true);
} else if (team == 'away') {
_ref.read(scoreProvider.notifier).setTimeoutAway(true);
}
break;
case 'command':
final action = data['action'] as String?;
if (action != null) {
_ref.read(sessionProvider.notifier).applyCommand(action);
}
break;
default:
break;
}
}
void sendScoreUpdate(ScoreState score) {
_perform({'type': 'score_update', ...score.toCablePayload()});
}
void sendTimeout(String team) {
_perform({'type': 'timeout', 'team': team});
}
void sendCommand(String action) {
_perform({'type': 'command', 'action': action});
}
void sendDeviceState(DeviceStateModel state) {
_perform({
'type': 'device_state',
'device_role': state.deviceRole,
'battery': state.batteryLevel,
'network': state.networkType,
'bitrate': state.currentBitrate,
'fps': state.fps,
});
}
void _perform(Map<String, dynamic> payload) {
_channel?.performAction('receive', params: payload);
}
Future<void> disconnect() async {
_channel?.unsubscribe();
_channel = null;
_cable?.disconnect();
_cable = null;
_sessionId = null;
_ref.read(sessionProvider.notifier).setConnected(false);
}
void dispose() {
disconnect();
}
String encodeQrPayload(Map<String, dynamic> payload) => jsonEncode(payload);
}