Non propone più il link MLTV per sessioni YouTube; la pagina /live reindirizza a YouTube; il relay verso YouTube si riavvia al connect. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
4.9 KiB
Dart
178 lines
4.9 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/score_sync_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':
|
|
final remote = ScoreState.fromJson(data);
|
|
if (_ref.read(scoreSyncProvider).acceptCableScore(remote)) {
|
|
_ref.read(scoreProvider.notifier).applyRemote(remote);
|
|
}
|
|
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;
|
|
case 'stream_event':
|
|
final event = data['event'] as String?;
|
|
if (event == 'paused') {
|
|
_ref.read(sessionProvider.notifier).applyCommand('pause_stream');
|
|
} else if (event == 'resumed') {
|
|
_ref.read(sessionProvider.notifier).applyCommand('resume_stream');
|
|
}
|
|
break;
|
|
case 'youtube_ready':
|
|
_ref.read(sessionProvider.notifier).updateYoutubeLinks(
|
|
youtubeWatchUrl: data['youtube_watch_url'] as String?,
|
|
shareUrl: data['share_url'] as String?,
|
|
youtubeBroadcastId: data['youtube_broadcast_id'] as String?,
|
|
);
|
|
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);
|
|
}
|