291 lines
9.2 KiB
Dart
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();
|