// lib/widgets/cast/backends/dlna_cast_backend.dart // // Backend DLNA estratto dal vecchio CastController. // Gestisce: // - selezione renderer // - avvio media server locale // - cast di una entry // - slideshow DLNA // - next/prev manuali // - heartbeat // - stop DLNA import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/cast/cast_server.dart'; import 'package:aves/widgets/cast/dlna.dart'; import '../cast_controller.dart'; class DlnaCastBackend extends CastBackendBase { DlnaCastBackend(super.controller); DLNADevice? _renderer; HttpServer? _mediaServer; Timer? _slideshowTimer; Timer? _heartbeatTimer; int _currentIndex = 0; List? _entries; // ------------------------------------------------------------ // API: set renderer dall’esterno (CastIconButton) // ------------------------------------------------------------ void setRenderer(DLNADevice device) { _renderer = device; } // ------------------------------------------------------------ // FOTO/VIDEO → delay slideshow // ------------------------------------------------------------ @override Duration? getNextDelay(AvesEntry entry) { final isVideo = entry.mimeType?.startsWith('video') ?? false; if (isVideo) { // 🎬 VIDEO → nessun timer → aspettiamo STOPPED return null; } // 📸 FOTO → timer 5 secondi return const Duration(seconds: 5); } // ------------------------------------------------------------ // Prewarm media server // ------------------------------------------------------------ Future ensureMediaServerPrewarm() async { await _ensureMediaServer(); } // ------------------------------------------------------------ // START CAST (equivale a fullscreen DLNA) // ------------------------------------------------------------ @override Future startCast(List entries, int index) => startFullscreen(entries, index); // ------------------------------------------------------------ // FULLSCREEN DLNA // ------------------------------------------------------------ @override Future startFullscreen(List entries, int index) async { _entries = entries; _currentIndex = index; controller.castIconState.value = CastIconState.connecting; controller.isCastingNotifier.value = true; await _ensureRenderer(); await _waitForDlnaReady(); await _ensureMediaServer(); await _castEntry(entries[index]); controller.castIconState.value = CastIconState.connected; } // ------------------------------------------------------------ // SLIDESHOW DLNA (manuale) // ------------------------------------------------------------ @override Future startSlideshow(List entries, int index) async { await startFullscreen(entries, index); _slideshowTimer?.cancel(); _slideshowTimer = Timer.periodic( const Duration(seconds: 5), (_) => nextManual(), ); } // ------------------------------------------------------------ // QUEUE MANUALE DLNA // ------------------------------------------------------------ @override Future startManualQueue(List entries, int index) async { _entries = entries; _currentIndex = index; controller.castIconState.value = CastIconState.connecting; controller.isCastingNotifier.value = true; await _ensureRenderer(); await _waitForDlnaReady(); await _ensureMediaServer(); await _castEntry(entries[index]); controller.castIconState.value = CastIconState.connected; } // ------------------------------------------------------------ // NEXT / PREV // ------------------------------------------------------------ @override Future nextManual() async { if (_entries == null) return; _currentIndex++; if (_currentIndex >= _entries!.length) _currentIndex = 0; await _castEntry(_entries![_currentIndex]); } @override Future prevManual() async { if (_entries == null) return; _currentIndex--; if (_currentIndex < 0) _currentIndex = _entries!.length - 1; await _castEntry(_entries![_currentIndex]); } // ------------------------------------------------------------ // STOP DLNA // ------------------------------------------------------------ @override Future stopCast() async { _slideshowTimer?.cancel(); _slideshowTimer = null; _heartbeatTimer?.cancel(); _heartbeatTimer = null; try { await _renderer?.stop(); } catch (_) {} controller.castIconState.value = CastIconState.idle; controller.isCastingNotifier.value = false; } // ------------------------------------------------------------ // CAST ENTRY DLNA // ------------------------------------------------------------ Future _castEntry(AvesEntry entry) async { debugPrint('DLNA: castEntry() renderer=$_renderer entry=${entry.id}'); if (_renderer == null) { debugPrint('DLNA: _castEntry chiamato senza renderer'); return; } await castServer.ensureStarted(); // URL corretto per DLNA: usa il media server DLNA locale final url = 'http://${_mediaServer!.address.host}:${_mediaServer!.port}/${entry.id}'; debugPrint('DLNA: URL generato = $url'); debugPrint('DLNA: serving entry ${entry.id} at $url'); final bool isVideo = (entry.durationMillis ?? 0) > 0; try { await _renderer!.setUrl( url, title: entry.bestTitle ?? '', type: isVideo ? PlayType.Video : PlayType.Image, ); } catch (e) { // Fallback Sony Bravia if (_isSonyBravia) { debugPrint('DLNA: fallback Sony Bravia, ritento senza metadata'); await _renderer!.setUrl( url, title: '', type: PlayType.Image, ); } else { debugPrint('DLNA: errore setUrl → $e'); return; } } try { await _renderer!.play(); } catch (e) { debugPrint('DLNA: play non supportato → $e'); } _startHeartbeat(); } // ------------------------------------------------------------ // HEARTBEAT DLNA // ------------------------------------------------------------ void _startHeartbeat() { _heartbeatTimer?.cancel(); _heartbeatTimer = Timer.periodic( const Duration(seconds: 5), (_) async { try { await DLNAHttp.postSoap( url: _renderer!.avTransportUrl, serviceType: 'urn:schemas-upnp-org:service:AVTransport:1', action: 'GetTransportInfo', body: ''' 0 ''', ); } catch (_) {} }, ); } // ------------------------------------------------------------ // MEDIA SERVER LOCALE // ------------------------------------------------------------ Future _ensureMediaServer() async { if (_mediaServer != null) return; final interfaces = await NetworkInterface.list( includeLoopback: false, type: InternetAddressType.IPv4, ); String? ip; for (final iface in interfaces) { for (final addr in iface.addresses) { if (!addr.isLoopback && addr.address.startsWith('192.168.')) { ip = addr.address; break; } } if (ip != null) break; } if (ip == null) return; _mediaServer = await HttpServer.bind(ip, 0, shared: true); _mediaServer!.listen((req) async { final id = int.tryParse(req.uri.path.substring(1)); if (id == null) { req.response.statusCode = 404; return req.response.close(); } final entry = _entries?.firstWhere((e) => e.id == id); if (entry == null) { req.response.statusCode = 404; return req.response.close(); } final file = File(entry.path!); if (!await file.exists()) { req.response.statusCode = 404; return req.response.close(); } final length = await file.length(); final range = req.headers.value('range'); if (range == null) { req.response.headers.set('Content-Type', entry.mimeType); req.response.headers.set('Content-Length', '$length'); req.response.headers.set('Accept-Ranges', 'bytes'); await req.response.addStream(file.openRead()); return req.response.close(); } final spec = range.substring('bytes='.length); final parts = spec.split('-'); int start = int.tryParse(parts[0]) ?? 0; int end = parts.length > 1 ? int.tryParse(parts[1]) ?? length - 1 : length - 1; start = start.clamp(0, length - 1); end = end.clamp(start, length - 1); req.response.statusCode = 206; req.response.headers.set('Content-Type', entry.mimeType); req.response.headers.set('Content-Length', '${end - start + 1}'); req.response.headers .set('Content-Range', 'bytes $start-$end/$length'); req.response.headers.set('Accept-Ranges', 'bytes'); await req.response.addStream(file.openRead(start, end + 1)); return req.response.close(); }); } // ------------------------------------------------------------ // RENDERER READY // ------------------------------------------------------------ Future _waitForDlnaReady() async { if (_renderer == null) return; for (int i = 0; i < 20; i++) { try { final resp = await DLNAHttp.postSoap( url: _renderer!.avTransportUrl, serviceType: 'urn:schemas-upnp-org:service:AVTransport:1', action: 'GetTransportInfo', body: ''' 0 ''', ); final body = resp.body; if (body.contains('STOPPED') || body.contains('NO_MEDIA_PRESENT')) { return; } } catch (_) {} await Future.delayed(const Duration(milliseconds: 300)); } } // ------------------------------------------------------------ // HELPERS // ------------------------------------------------------------ bool get _isSonyBravia { final name = _renderer?.info.friendlyName.toLowerCase() ?? ''; return name.contains('bravia') || name.contains('sony'); } // ------------------------------------------------------------ // SELEZIONE RENDERER DLNA // ------------------------------------------------------------ Future _ensureRenderer() async { if (_renderer != null) return; debugPrint('DLNA: _ensureRenderer chiamato senza renderer (setRenderer non ancora usato)'); } }