import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; const serverPort = 8080; const discoveryPort = 8081; const _beacon = 'EXTCAM'; class FrameServer { HttpServer? _httpServer; final List _clients = []; final _commands = StreamController.broadcast(); final _connections = StreamController.broadcast(); int get clientCount => _clients.length; Stream get commands => _commands.stream; Stream get clientConnected => _connections.stream; Future start() async { _httpServer = await HttpServer.bind(InternetAddress.anyIPv4, serverPort); _httpServer!.listen(_handleRequest); } void _handleRequest(HttpRequest request) { if (request.uri.path == '/stream') { _upgradeToWebSocket(request); } else { request.response ..statusCode = HttpStatus.ok ..headers.contentType = ContentType.json ..write('{"clients":${_clients.length}}') ..close(); } } Future _upgradeToWebSocket(HttpRequest request) async { final socket = await WebSocketTransformer.upgrade(request); _clients.add(socket); _connections.add(null); socket.listen((data) { if (data is String) _commands.add(data); }, onDone: () => _clients.remove(socket)); } void sendFrame(Uint8List jpeg) { for (final client in _clients.toList()) { try { client.add(jpeg); } catch (_) { _clients.remove(client); } } } void sendText(String message) { for (final client in _clients.toList()) { try { client.add(message); } catch (_) { _clients.remove(client); } } } Future stop() async { await _commands.close(); await _connections.close(); for (final client in _clients.toList()) { await client.close(); } _clients.clear(); await _httpServer?.close(); _httpServer = null; } } class DiscoveryBeacon { RawDatagramSocket? _socket; Timer? _timer; Future start() async { _socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); _socket!.broadcastEnabled = true; _sendBroadcast(); _timer = Timer.periodic( const Duration(seconds: 1), (_) => _sendBroadcast(), ); } Future _sendBroadcast() async { final data = utf8.encode(_beacon); final socket = _socket; if (socket == null) return; // global broadcast socket.send(data, InternetAddress('255.255.255.255'), discoveryPort); // subnet-specific broadcasts (needed on hotspot/tethering) final interfaces = await NetworkInterface.list(); for (final iface in interfaces) { for (final addr in iface.addresses) { if (addr.type != InternetAddressType.IPv4 || addr.isLoopback) continue; final parts = addr.address.split('.'); final subnetBroadcast = '${parts[0]}.${parts[1]}.${parts[2]}.255'; socket.send(data, InternetAddress(subnetBroadcast), discoveryPort); } } } void stop() { _timer?.cancel(); _socket?.close(); } } Future discoverServer({ Duration timeout = const Duration(seconds: 5), }) async { final results = await Future.wait([ _discoverViaBroadcast(timeout), _discoverViaProbe(), ]); return results.firstWhere((r) => r != null, orElse: () => null); } Future _discoverViaBroadcast(Duration timeout) async { final socket = await RawDatagramSocket.bind( InternetAddress.anyIPv4, discoveryPort, ); try { await for (final event in socket.timeout(timeout)) { if (event == RawSocketEvent.read) { final datagram = socket.receive(); if (datagram == null) continue; final message = utf8.decode(datagram.data); if (message == _beacon) return datagram.address.address; } } } on TimeoutException { // no server found } finally { socket.close(); } return null; } Future _discoverViaProbe() async { final interfaces = await NetworkInterface.list(); final candidates = {}; for (final iface in interfaces) { for (final addr in iface.addresses) { if (addr.type != InternetAddressType.IPv4 || addr.isLoopback) continue; final parts = addr.address.split('.'); final subnet = '${parts[0]}.${parts[1]}.${parts[2]}'; for (var suffix = 1; suffix <= 254; suffix++) { final candidate = '$subnet.$suffix'; if (candidate != addr.address) candidates.add(candidate); } } } final futures = candidates.map((ip) async { try { final socket = await Socket.connect( ip, serverPort, timeout: const Duration(milliseconds: 800), ); socket.destroy(); return ip; } catch (_) { return null; } }); final results = await Future.wait(futures); return results.whereType().firstOrNull; }