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 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 _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 _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> _logStreamCompletion( Stream> 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 _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( '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 ensureStarted() async { if (!isRunning) { await start(); } } } final castServer = CastServer();