aves_mio100/lib/services/cast/cast_server.dart
2026-07-06 14:52:45 +02:00

291 lines
9.2 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:mime/mime.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
class CastServer {
HttpServer? _server;
String? _ip;
bool get isRunning => _server != null;
// ------------------------------------------------------------
// TIMING DEBUG
// ------------------------------------------------------------
static final Stopwatch _timingSw = Stopwatch()..start();
static int _ms() => _timingSw.elapsedMilliseconds;
static String _isoNow() => DateTime.now().toIso8601String();
static void _tlog(String message) {
print('CASTT [${_ms()} ms][${_isoNow()}] $message');
}
// ============================================================
// START SERVER — Versione identica al vecchio AVES + timing logs
// ============================================================
Future<void> start() async {
if (_server != null) {
_tlog('castServer.start skip already-running ip=$_ip port=${_server?.port}');
return;
}
final sw = Stopwatch()..start();
_tlog('castServer.start begin');
// 1) Ottieni IP Wi-Fi reale
final interfaces = await NetworkInterface.list(
includeLoopback: false,
type: InternetAddressType.IPv4,
);
_tlog('castServer.start interfaces found count=${interfaces.length} ms=${sw.elapsedMilliseconds}');
final wifi = interfaces.firstWhere(
(i) => i.name == 'wlan0' || i.name.contains('wifi'),
orElse: () => interfaces.first,
);
_ip = wifi.addresses.first.address;
_tlog('castServer.start selected interface=${wifi.name} ip=$_ip ms=${sw.elapsedMilliseconds}');
// 2) Bind su 0.0.0.0 e porta random (0)
_server = await HttpServer.bind(
InternetAddress.anyIPv4,
0,
shared: true,
);
final port = _server!.port;
_tlog('castServer.start bound ip=$_ip port=$port ms=${sw.elapsedMilliseconds}');
// 3) Avvia Shelf sopra HttpServer esistente
final handler = const Pipeline().addHandler(_handleRequest);
shelf_io.serveRequests(_server!, handler);
_tlog('castServer.start serveRequests active url=http://$_ip:$port ms=${sw.elapsedMilliseconds}');
}
// ============================================================
// URL BUILDER — usa ID reale del MediaStore
// ============================================================
String buildUrlForEntry(dynamic entry) {
final uri = entry.uri;
final mediaId = uri.split('/').last;
final port = _server?.port ?? 0;
final url = 'http://$_ip:$port/media/$mediaId';
_tlog('castServer.buildUrlForEntry mediaId=$mediaId url=$url');
return url;
}
// ============================================================
// HANDLER HTTP
// ============================================================
static Future<Response> _handleRequest(Request request) async {
final sw = Stopwatch()..start();
final path = request.requestedUri.path;
final range = request.headers['range'];
_tlog('CAST SERVER request start method=${request.method} path=$path range=$range');
// URL tipo: /media/1000113738
if (request.url.pathSegments.isEmpty ||
request.url.pathSegments.first != 'media') {
_tlog('CAST SERVER invalid path path=$path ms=${sw.elapsedMilliseconds}');
return Response.notFound('Invalid path');
}
final idStr = request.url.pathSegments.length > 1
? request.url.pathSegments[1]
: null;
final id = int.tryParse(idStr ?? '');
if (id == null) {
_tlog('CAST SERVER invalid id raw=$idStr ms=${sw.elapsedMilliseconds}');
return Response.notFound('Invalid ID');
}
final lookupSw = Stopwatch()..start();
final filePath = await _getFilePath(id);
_tlog('CAST SERVER getFilePath done id=$id ms=${lookupSw.elapsedMilliseconds} path=$filePath');
if (filePath == null) {
_tlog('CAST SERVER No file for ID=$id totalMs=${sw.elapsedMilliseconds}');
return Response.notFound('No file for ID=$id');
}
final file = File(filePath);
final exists = await file.exists();
if (!exists) {
_tlog('CAST SERVER file missing id=$id path=$filePath totalMs=${sw.elapsedMilliseconds}');
return Response.notFound('File not found');
}
final fileLength = await file.length();
final mime = lookupMimeType(file.path) ?? 'application/octet-stream';
_tlog('CAST SERVER file ready id=$id size=$fileLength mime=$mime totalMs=${sw.elapsedMilliseconds}');
return _serveFileWithRange(
file,
request,
id: id,
requestStopwatch: sw,
fileLength: fileLength,
mime: mime,
);
}
// ============================================================
// RANGE / 206 (necessario per Chromecast)
// ============================================================
static Future<Response> _serveFileWithRange(
File file,
Request request, {
required int id,
required Stopwatch requestStopwatch,
required int fileLength,
required String mime,
}) async {
final range = request.headers['range'];
_tlog('CAST SERVER response preparing id=$id method=${request.method} range=$range totalMs=${requestStopwatch.elapsedMilliseconds}');
// HEAD
if (request.method == 'HEAD') {
_tlog('CAST SERVER HEAD response id=$id size=$fileLength totalMs=${requestStopwatch.elapsedMilliseconds}');
return Response.ok(
null,
headers: {
'Content-Type': mime,
'Content-Length': '$fileLength',
'Accept-Ranges': 'bytes',
},
);
}
// Nessun range → 200
if (range == null || !range.startsWith('bytes=')) {
final stream = _logStreamCompletion(
file.openRead(),
label: 'CAST SERVER 200 stream',
id: id,
startOffset: 0,
endOffset: fileLength - 1,
declaredLength: fileLength,
requestStopwatch: requestStopwatch,
);
_tlog('CAST SERVER 200 response start id=$id bytes=0-${fileLength - 1}/$fileLength totalMs=${requestStopwatch.elapsedMilliseconds}');
return Response.ok(
stream,
headers: {
'Content-Type': mime,
'Content-Length': '$fileLength',
'Accept-Ranges': 'bytes',
},
);
}
// Range parsing
final spec = range.substring('bytes='.length).trim();
final parts = spec.split('-');
int? start = int.tryParse(parts.first);
int? end = parts.length > 1 ? int.tryParse(parts[1]) : null;
if (start == null && end != null) {
final suffix = end;
start = (fileLength - suffix).clamp(0, fileLength);
end = fileLength - 1;
}
start ??= 0;
end ??= fileLength - 1;
if (start < 0) start = 0;
if (end >= fileLength) end = fileLength - 1;
final contentLength = end - start + 1;
final rawStream = file.openRead(start, end + 1);
final stream = _logStreamCompletion(
rawStream,
label: 'CAST SERVER 206 stream',
id: id,
startOffset: start,
endOffset: end,
declaredLength: contentLength,
requestStopwatch: requestStopwatch,
);
_tlog('CAST SERVER 206 response start id=$id range=$start-$end/$fileLength contentLength=$contentLength totalMs=${requestStopwatch.elapsedMilliseconds}');
return Response(
206,
body: stream,
headers: {
'Content-Type': mime,
'Content-Length': '$contentLength',
'Content-Range': 'bytes $start-$end/$fileLength',
'Accept-Ranges': 'bytes',
},
);
}
static Stream<List<int>> _logStreamCompletion(
Stream<List<int>> source, {
required String label,
required int id,
required int startOffset,
required int endOffset,
required int declaredLength,
required Stopwatch requestStopwatch,
}) async* {
int sentBytes = 0;
final streamSw = Stopwatch()..start();
_tlog('$label begin id=$id from=$startOffset to=$endOffset declaredLength=$declaredLength totalMs=${requestStopwatch.elapsedMilliseconds}');
try {
await for (final chunk in source) {
sentBytes += chunk.length;
yield chunk;
}
_tlog('$label end id=$id sentBytes=$sentBytes streamMs=${streamSw.elapsedMilliseconds} totalMs=${requestStopwatch.elapsedMilliseconds}');
} catch (e) {
_tlog('$label error id=$id sentBytes=$sentBytes streamMs=${streamSw.elapsedMilliseconds} totalMs=${requestStopwatch.elapsedMilliseconds} error=$e');
rethrow;
}
}
// ============================================================
// Recupero path dal DB AVES
// ============================================================
static Future<String?> _getFilePath(int id) async {
const channel = MethodChannel('aves/entry_path');
final sw = Stopwatch()..start();
_tlog('CAST SERVER getFilePath start id=$id');
try {
final path = await channel.invokeMethod<String>(
'getEntryPath',
{'id': id},
);
_tlog('CAST SERVER getFilePath end id=$id ms=${sw.elapsedMilliseconds} path=$path');
return path;
} catch (e) {
_tlog('CAST SERVER getFilePath error id=$id ms=${sw.elapsedMilliseconds} error=$e');
return null;
}
}
Future<void> ensureStarted() async {
if (!isRunning) {
await start();
}
}
}
final castServer = CastServer();