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:
2026-05-26 17:45:37 +02:00
commit bba6df52c0
381 changed files with 20599 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import 'package:flutter/services.dart';
/// Bridge verso engine nativo Android (CameraX + RootEncoder).
class StreamingChannel {
StreamingChannel._();
static const MethodChannel _channel =
MethodChannel('com.matchlivetv.match_live_tv/streaming');
static const EventChannel _metricsChannel =
EventChannel('com.matchlivetv.match_live_tv/streaming_events');
static Future<void> startPreview() async {
await _channel.invokeMethod('startPreview');
}
static Future<bool> bindPreview() async {
final ok = await _channel.invokeMethod<bool>('bindPreview');
return ok ?? false;
}
static Future<void> stopPreview() async {
await _channel.invokeMethod('stopPreview');
}
static Future<void> startStream({
required String rtmpUrl,
int targetBitrate = 2500000,
int targetFps = 30,
}) async {
await _channel.invokeMethod('startStream', {
'rtmpUrl': rtmpUrl,
'videoBitrate': targetBitrate,
'fps': targetFps,
});
}
static Future<void> stopStream() async {
await _channel.invokeMethod('stopStream');
}
static Future<void> pauseStream() async {
await _channel.invokeMethod('pauseStream');
}
static Future<Map<String, dynamic>?> getMetrics() async {
final result =
await _channel.invokeMethod<Map<dynamic, dynamic>>('getMetrics');
return result?.map((key, value) => MapEntry(key.toString(), value));
}
static EventChannel get metricsStream => _metricsChannel;
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Preview camera nativa (OpenGL) o placeholder su altre piattaforme.
class NativeStreamingPreview extends StatelessWidget {
const NativeStreamingPreview({
super.key,
this.showGrid = true,
this.platformLabel = 'LIVE',
});
final bool showGrid;
final String platformLabel;
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return Stack(
fit: StackFit.expand,
children: [
AndroidView(
key: key,
viewType: 'match_live_tv/streaming_preview',
layoutDirection: TextDirection.ltr,
creationParamsCodec: StandardMessageCodec(),
),
if (showGrid) CameraPreviewOverlay(platformLabel: platformLabel),
],
);
}
return CameraPreviewPlaceholder(showGrid: showGrid);
}
}
class CameraPreviewPlaceholder extends StatelessWidget {
const CameraPreviewPlaceholder({super.key, this.showGrid = true});
final bool showGrid;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: const Center(
child: Icon(Icons.videocam, size: 64, color: Colors.white24),
),
);
}
}
/// Overlay REC / badge sopra la preview nativa.
class CameraPreviewOverlay extends StatelessWidget {
const CameraPreviewOverlay({super.key, this.platformLabel = 'LIVE'});
final String platformLabel;
@override
Widget build(BuildContext context) {
final inset = MediaQuery.viewPaddingOf(context);
const gap = 8.0;
return Stack(
fit: StackFit.expand,
children: [
Positioned(
top: inset.top + gap,
left: inset.left + gap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'REC',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
),
Positioned(
top: inset.top + gap,
right: inset.right + gap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
platformLabel,
style: const TextStyle(color: Colors.white, fontSize: 11),
),
),
),
],
);
}
}