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>
This commit is contained in:
158
mobile/lib/shared/websocket_service.dart
Normal file
158
mobile/lib/shared/websocket_service.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user