Pipeline YouTube con relay, overlay e publisher sync lato backend; fix GL pauseGlDuringRotation su Android per evitare crash in rotazione durante lo streaming Flutter. Co-authored-by: Cursor <cursoragent@cursor.com>
319 lines
9.7 KiB
Dart
319 lines
9.7 KiB
Dart
import 'package:dio/dio.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../core/config.dart';
|
||
import '../providers/auth_provider.dart';
|
||
import 'models/invite_preview.dart';
|
||
import 'models/match.dart';
|
||
import 'models/recording_item.dart';
|
||
import 'models/score_state.dart';
|
||
import 'models/stream_session.dart';
|
||
import 'models/team.dart';
|
||
import 'models/user.dart';
|
||
|
||
final apiClientProvider = Provider<ApiClient>((ref) {
|
||
final auth = ref.watch(authProvider);
|
||
return ApiClient(accessToken: auth.accessToken);
|
||
});
|
||
|
||
class ApiClient {
|
||
ApiClient({this.accessToken}) {
|
||
_dio = Dio(
|
||
BaseOptions(
|
||
baseUrl: AppConfig.apiV1,
|
||
connectTimeout: const Duration(seconds: 30),
|
||
receiveTimeout: const Duration(seconds: 30),
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
),
|
||
);
|
||
|
||
_dio.interceptors.add(
|
||
InterceptorsWrapper(
|
||
onRequest: (options, handler) {
|
||
if (accessToken != null && accessToken!.isNotEmpty) {
|
||
options.headers['Authorization'] = 'Bearer $accessToken';
|
||
}
|
||
handler.next(options);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
late final Dio _dio;
|
||
final String? accessToken;
|
||
|
||
// --- Auth ---
|
||
|
||
Future<({User user, AuthTokens tokens})> login({
|
||
required String email,
|
||
required String password,
|
||
}) async {
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/auth/login',
|
||
data: {'email': email, 'password': password},
|
||
);
|
||
final data = response.data!;
|
||
return (
|
||
user: User.fromJson(data['user'] as Map<String, dynamic>),
|
||
tokens: AuthTokens.fromJson(data),
|
||
);
|
||
}
|
||
|
||
Future<({User user, AuthTokens tokens})> refreshSession(
|
||
String refreshToken,
|
||
) async {
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/auth/refresh',
|
||
data: {'refresh_token': refreshToken},
|
||
);
|
||
final data = response.data!;
|
||
return (
|
||
user: User.fromJson(data['user'] as Map<String, dynamic>),
|
||
tokens: AuthTokens.fromJson(data),
|
||
);
|
||
}
|
||
|
||
Future<User> me() async {
|
||
final response = await _dio.get<Map<String, dynamic>>('/auth/me');
|
||
return User.fromJson(response.data!);
|
||
}
|
||
|
||
Future<void> logout() async {
|
||
await _dio.post('/auth/logout');
|
||
}
|
||
|
||
// --- Teams ---
|
||
|
||
Future<List<Team>> fetchTeams() async {
|
||
final response = await _dio.get<List<dynamic>>('/teams');
|
||
return response.data!
|
||
.map((e) => Team.fromJson(e as Map<String, dynamic>))
|
||
.toList();
|
||
}
|
||
|
||
Future<InvitePreview> fetchInvitePreview(String token) async {
|
||
final response = await _dio.get<Map<String, dynamic>>('/invitations/$token');
|
||
return InvitePreview.fromJson(response.data!);
|
||
}
|
||
|
||
Future<String> acceptInvitation(String token) async {
|
||
final response = await _dio.post<Map<String, dynamic>>('/invitations/$token/accept');
|
||
return response.data!['message'] as String? ?? 'Invito accettato';
|
||
}
|
||
|
||
Future<String> youtubeAuthorizeUrl(String teamId, {bool returnApp = true}) async {
|
||
final response = await _dio.get<Map<String, dynamic>>(
|
||
'/teams/$teamId/youtube/authorize',
|
||
queryParameters: returnApp ? {'return_app': '1'} : null,
|
||
);
|
||
return response.data!['authorization_url'] as String;
|
||
}
|
||
|
||
Future<List<RecordingItem>> fetchRecordings(String teamId) async {
|
||
final response = await _dio.get<List<dynamic>>('/teams/$teamId/recordings');
|
||
return response.data!
|
||
.map((e) => RecordingItem.fromJson(e as Map<String, dynamic>))
|
||
.toList();
|
||
}
|
||
|
||
Future<String> fetchRecordingDownloadUrl(String recordingId) async {
|
||
final response = await _dio.get<Map<String, dynamic>>(
|
||
'/recordings/$recordingId/download',
|
||
);
|
||
return response.data!['download_url'] as String;
|
||
}
|
||
|
||
// --- Matches ---
|
||
|
||
Future<List<MatchModel>> fetchMatches(String teamId) async {
|
||
final response =
|
||
await _dio.get<List<dynamic>>('/teams/$teamId/matches');
|
||
return response.data!
|
||
.map((e) => MatchModel.fromJson(e as Map<String, dynamic>))
|
||
.toList();
|
||
}
|
||
|
||
Future<MatchModel> fetchMatch(String matchId) async {
|
||
final response =
|
||
await _dio.get<Map<String, dynamic>>('/matches/$matchId');
|
||
return MatchModel.fromJson(response.data!);
|
||
}
|
||
|
||
Future<MatchModel> updateMatch(String matchId, Map<String, dynamic> body) async {
|
||
final response = await _dio.patch<Map<String, dynamic>>(
|
||
'/matches/$matchId',
|
||
data: {'match': body},
|
||
);
|
||
return MatchModel.fromJson(response.data!);
|
||
}
|
||
|
||
Future<void> deleteMatch(String matchId) async {
|
||
await _dio.delete<void>('/matches/$matchId');
|
||
}
|
||
|
||
Future<MatchModel> createMatch(String teamId, Map<String, dynamic> body) async {
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/teams/$teamId/matches',
|
||
data: {'match': body},
|
||
);
|
||
return MatchModel.fromJson(response.data!);
|
||
}
|
||
|
||
// --- Sessions ---
|
||
|
||
Future<StreamSession> createSession(
|
||
String matchId, {
|
||
String platform = 'matchlivetv',
|
||
String? youtubeChannel,
|
||
String privacyStatus = 'public',
|
||
String qualityPreset = '720p_30_2.5mbps',
|
||
int? targetBitrate,
|
||
int? targetFps,
|
||
}) async {
|
||
// YouTube: creazione broadcast + path MediaMTX può richiedere 20–30s lato server.
|
||
final sessionTimeout = platform == 'youtube'
|
||
? const Duration(seconds: 60)
|
||
: const Duration(seconds: 30);
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/matches/$matchId/sessions',
|
||
data: {
|
||
'platform': platform,
|
||
'privacy_status': privacyStatus,
|
||
'quality_preset': qualityPreset,
|
||
if (youtubeChannel != null) 'youtube_channel': youtubeChannel,
|
||
if (targetBitrate != null) 'target_bitrate': targetBitrate,
|
||
if (targetFps != null) 'target_fps': targetFps,
|
||
},
|
||
options: Options(
|
||
sendTimeout: sessionTimeout,
|
||
receiveTimeout: sessionTimeout,
|
||
),
|
||
);
|
||
return StreamSession.fromJson(response.data!);
|
||
}
|
||
|
||
Future<StreamSession> fetchSession(String sessionId) async {
|
||
final response =
|
||
await _dio.get<Map<String, dynamic>>('/sessions/$sessionId');
|
||
return StreamSession.fromJson(response.data!);
|
||
}
|
||
|
||
Future<StreamSession> startSession(String sessionId) async {
|
||
final response =
|
||
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/start');
|
||
return StreamSession.fromJson(response.data!);
|
||
}
|
||
|
||
Future<StreamSession> stopSession(String sessionId) async {
|
||
final response =
|
||
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/stop');
|
||
return StreamSession.fromJson(response.data!);
|
||
}
|
||
|
||
Future<StreamSession> pauseSession(String sessionId) async {
|
||
final response =
|
||
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/pause');
|
||
return StreamSession.fromJson(response.data!);
|
||
}
|
||
|
||
Future<StreamSession> resumeSession(String sessionId) async {
|
||
final response =
|
||
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/resume');
|
||
return StreamSession.fromJson(response.data!);
|
||
}
|
||
|
||
/// Persiste il tabellone e restituisce lo stato salvato sul server.
|
||
Future<ScoreState?> syncScore(String sessionId, Map<String, dynamic> payload) async {
|
||
final response = await _dio.patch<Map<String, dynamic>>(
|
||
'/sessions/$sessionId/score',
|
||
data: payload,
|
||
);
|
||
final score = response.data?['score'];
|
||
if (score is Map<String, dynamic>) {
|
||
return ScoreState.fromJson(score);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
Future<RegiaLinkResponse> createRegiaLink(String sessionId) async {
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/sessions/$sessionId/regia_link',
|
||
);
|
||
return RegiaLinkResponse.fromJson(response.data!);
|
||
}
|
||
|
||
Future<PairingTokenResponse> createPairingToken(String sessionId) async {
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/sessions/$sessionId/pairing_token',
|
||
);
|
||
return PairingTokenResponse.fromJson(response.data!);
|
||
}
|
||
|
||
Future<StreamSession> claimPairing({
|
||
required String sessionId,
|
||
required String pairingToken,
|
||
required String deviceRole,
|
||
}) async {
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/sessions/$sessionId/claim_pairing',
|
||
data: {
|
||
'pairing_token': pairingToken,
|
||
'device_role': deviceRole,
|
||
},
|
||
);
|
||
return StreamSession.fromJson(response.data!);
|
||
}
|
||
|
||
Future<NetworkTestResult> networkTest({
|
||
required String sessionId,
|
||
required double downloadMbps,
|
||
required double uploadMbps,
|
||
required int latencyMs,
|
||
required String networkType,
|
||
}) async {
|
||
final response = await _dio.post<Map<String, dynamic>>(
|
||
'/sessions/$sessionId/network_test',
|
||
data: {
|
||
'download_mbps': downloadMbps,
|
||
'upload_mbps': uploadMbps,
|
||
'latency_ms': latencyMs,
|
||
'network_type': networkType,
|
||
},
|
||
);
|
||
return NetworkTestResult.fromJson(response.data!);
|
||
}
|
||
|
||
Future<YoutubeStats> youtubeStats(String sessionId) async {
|
||
final response =
|
||
await _dio.get<Map<String, dynamic>>('/sessions/$sessionId/youtube_stats');
|
||
return YoutubeStats.fromJson(response.data!);
|
||
}
|
||
|
||
Future<void> postTelemetry({
|
||
required String sessionId,
|
||
required String deviceRole,
|
||
int? batteryLevel,
|
||
String? networkType,
|
||
int? signalStrength,
|
||
int? currentBitrate,
|
||
int? targetBitrate,
|
||
int? fps,
|
||
}) async {
|
||
await _dio.post(
|
||
'/sessions/$sessionId/telemetry',
|
||
data: {
|
||
'device_role': deviceRole,
|
||
if (batteryLevel != null) 'battery_level': batteryLevel,
|
||
if (networkType != null) 'network_type': networkType,
|
||
if (signalStrength != null) 'signal_strength': signalStrength,
|
||
if (currentBitrate != null) 'current_bitrate': currentBitrate,
|
||
if (targetBitrate != null) 'target_bitrate': targetBitrate,
|
||
if (fps != null) 'fps': fps,
|
||
},
|
||
);
|
||
}
|
||
}
|