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:
53
mobile/lib/platform/streaming_channel.dart
Normal file
53
mobile/lib/platform/streaming_channel.dart
Normal 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;
|
||||
}
|
||||
103
mobile/lib/platform/streaming_preview.dart
Normal file
103
mobile/lib/platform/streaming_preview.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user