import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:external_cam/frame_server.dart'; import 'package:external_cam/jpeg_encoder.dart'; class CameraScreen extends StatefulWidget { const CameraScreen({super.key}); @override State createState() => _CameraScreenState(); } class _CameraScreenState extends State { CameraController? _controller; final _server = FrameServer(); final _beacon = DiscoveryBeacon(); bool _isStreaming = false; bool _isProcessing = false; bool _flashOn = false; String _error = ''; List _addresses = []; List _cameras = []; int _cameraIndex = 0; int _fps = 0; int _frameCount = 0; double _zoom = 1.0; double _minZoom = 1.0; double _maxZoom = 1.0; int _quality = 50; Timer? _fpsTimer; StreamSubscription? _commandSub; StreamSubscription? _connectSub; @override void initState() { super.initState(); _init(); } Future _init() async { try { if (!await Permission.camera.request().isGranted) { setState(() => _error = 'Camera permission required'); return; } _cameras = await availableCameras(); if (_cameras.isEmpty) { setState(() => _error = 'No cameras found'); return; } await _initCamera(_cameras[_cameraIndex]); await _startServer(); } catch (error) { setState(() => _error = error.toString()); } } Future _initCamera(CameraDescription camera) async { await _controller?.dispose(); _flashOn = false; _zoom = 1.0; final controller = CameraController( camera, ResolutionPreset.high, enableAudio: false, imageFormatGroup: ImageFormatGroup.yuv420, ); await controller.initialize(); if (!mounted) return; _controller = controller; _minZoom = await controller.getMinZoomLevel(); _maxZoom = await controller.getMaxZoomLevel(); setState(() {}); } Future _startServer() async { await _server.start(); await _beacon.start(); _addresses = await _networkAddresses(); _commandSub = _server.commands.listen(_handleCommand); _connectSub = _server.clientConnected.listen((_) => _broadcastState()); _fpsTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (!mounted) return; setState(() { _fps = _frameCount; _frameCount = 0; }); }); setState(() {}); _startStreaming(); } void _handleCommand(String command) { if (command.startsWith('zoom:')) { final level = double.tryParse(command.substring(5)); if (level != null) _setZoom(level); return; } if (command.startsWith('quality:')) { final q = int.tryParse(command.substring(8)); if (q != null) _setQuality(q); return; } switch (command) { case 'switch_camera': _switchCamera(); case 'toggle_flash': _toggleFlash(); } } void _broadcastState() { if (_cameras.isEmpty) return; final camera = _cameras[_cameraIndex]; _server.sendText( jsonEncode({ 'cameras': _cameras.length, 'current': _cameraIndex, 'flash': _flashOn, 'lens': camera.lensDirection.name, 'name': camera.name, 'zoom': _zoom, 'minZoom': _minZoom, 'maxZoom': _maxZoom, 'quality': _quality, }), ); } void _startStreaming() { if (_isStreaming || _controller == null) return; _controller!.startImageStream(_onFrame); _isStreaming = true; WakelockPlus.enable(); } void _stopStreaming() { if (!_isStreaming || _controller == null) return; _controller!.stopImageStream(); _isStreaming = false; } void _onFrame(CameraImage image) { if (_isProcessing || _server.clientCount == 0) return; _isProcessing = true; encodeJpeg(image, quality: _quality).then((jpeg) { if (jpeg != null) { _server.sendFrame(jpeg); _frameCount++; } }).whenComplete(() { _isProcessing = false; }); } Future> _networkAddresses() async { final interfaces = await NetworkInterface.list(); final results = []; for (final iface in interfaces) { for (final addr in iface.addresses) { if (addr.type != InternetAddressType.IPv4 || addr.isLoopback) continue; results.add('${addr.address} (${iface.name})'); } } return results; } Future _switchCamera() async { if (_cameras.length < 2) return; _stopStreaming(); _cameraIndex = (_cameraIndex + 1) % _cameras.length; await _initCamera(_cameras[_cameraIndex]); _startStreaming(); _broadcastState(); } Future _setZoom(double level) async { final controller = _controller; if (controller == null) return; _zoom = level.clamp(_minZoom, _maxZoom); await controller.setZoomLevel(_zoom); setState(() {}); _broadcastState(); } void _setQuality(int value) { _quality = value.clamp(10, 100); setState(() {}); _broadcastState(); } Future _toggleFlash() async { final controller = _controller; if (controller == null) return; try { _stopStreaming(); _flashOn = !_flashOn; await controller.setFlashMode( _flashOn ? FlashMode.torch : FlashMode.off, ); } catch (_) { _flashOn = false; } finally { _startStreaming(); setState(() {}); _broadcastState(); } } @override void dispose() { _fpsTimer?.cancel(); _commandSub?.cancel(); _connectSub?.cancel(); _stopStreaming(); _controller?.dispose(); _beacon.stop(); _server.stop(); WakelockPlus.disable(); super.dispose(); } @override Widget build(BuildContext context) { if (_error.isNotEmpty) { return Scaffold( appBar: AppBar(title: const Text('Camera')), body: Center( child: Padding( padding: const EdgeInsets.all(32), child: Text(_error, textAlign: TextAlign.center), ), ), ); } return Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ if (_controller?.value.isInitialized ?? false) Center(child: CameraPreview(_controller!)), SafeArea( child: Column( children: [_buildInfoBar(), const Spacer(), _buildControls()], ), ), ], ), ); } Widget _buildInfoBar() { final addressText = _addresses.isNotEmpty ? _addresses.join('\n') : 'No network'; return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( Icons.circle, color: _server.clientCount > 0 ? Colors.green : Colors.red, size: 12, ), const SizedBox(width: 10), Expanded( child: Text(addressText, style: const TextStyle(fontSize: 13)), ), Text('${_server.clientCount} viewers $_fps fps'), ], ), ); } Widget _buildControls() { return Padding( padding: const EdgeInsets.all(24), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton.filled( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back), iconSize: 28, ), if (_cameras.length > 1) IconButton.filled( onPressed: _switchCamera, icon: const Icon(Icons.flip_camera_android), iconSize: 28, ), IconButton.filled( onPressed: _toggleFlash, icon: Icon(_flashOn ? Icons.flash_on : Icons.flash_off), iconSize: 28, ), ], ), ); } }