import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:chepuhagram/domain/services/webrtc_service.dart'; import 'package:chepuhagram/data/datasources/ws_client.dart'; import 'package:provider/provider.dart'; import 'package:chepuhagram/logic/auth_provider.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:chepuhagram/domain/services/api_service.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class CallScreen extends StatefulWidget { final String callId; final bool isIncoming; final String callerName; final int targetUserId; final bool startAccepted; static String? currentActiveCallId; const CallScreen({ super.key, required this.callId, required this.isIncoming, required this.callerName, required this.targetUserId, this.startAccepted = false, }); @override State createState() => _CallScreenState(); } class _CallScreenState extends State with TickerProviderStateMixin { final RTCVideoRenderer _localRenderer = RTCVideoRenderer(); final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); bool _isAccepted = false; bool _isMuted = false; bool _isVideoOff = true; bool _isSpeakerOn = false; bool _isRemoteVideoOn = false; bool _renderersInitialized = false; bool _isPopped = false; Timer? _durationTimer; int _durationSeconds = 0; // Анимация пульсации для экрана вызова late AnimationController _pulseController; late Animation _pulseAnimation; @override void initState() { super.initState(); CallScreen.currentActiveCallId = widget.callId; _isAccepted = widget.startAccepted; _initRenderers(); _setupPulseAnimation(); _setupWebRTC(); } Future _initRenderers() async { await _localRenderer.initialize(); await _remoteRenderer.initialize(); if (mounted) { setState(() { _renderersInitialized = true; }); } } void _setupPulseAnimation() { _pulseController = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); _pulseAnimation = Tween(begin: 1.0, end: 1.6).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeOut), ); } void _setupWebRTC() async { // 1. Устанавливаем обработчик завершения звонка WebRTCService().onCallEnded = () { if (mounted && !_isPopped) { _isPopped = true; if (CallScreen.currentActiveCallId == widget.callId) { CallScreen.currentActiveCallId = null; } _stopTimer(); FlutterCallkitIncoming.endAllCalls(); Navigator.of(context).pop(); } }; // Слушаем изменение состояния камеры собеседника WebRTCService().onRemoteVideoToggled = (enabled) { if (mounted) { setState(() { _isRemoteVideoOn = enabled; }); } }; // 2. Если звонок исходящий или входящий и уже принят через CallKit, // сразу же создаем/инициализируем PeerConnection, чтобы быть готовыми принимать входящие сигналы (offer и т.д.) if (!widget.isIncoming || widget.startAccepted) { if (widget.startAccepted || !widget.isIncoming) { WebRTCService().isCallAcceptedLocally = true; } await _initConnection(); } // 3. Запускаем локальный стрим (камера открывается параллельно/следом) await WebRTCService().startLocalStream((localStream) { if (mounted) { setState(() { _localRenderer.srcObject = localStream; }); } }); // 4. Помечаем WebRTCService как готовый для обмена сигналами WebRTCService().setReadyForSignaling(); } Future _initConnection() async { var myId = context.read().currentUserId ?? 0; if (myId == 0) { final storage = FlutterSecureStorage(); final userIdStr = await storage.read(key: 'user_id'); myId = int.tryParse(userIdStr ?? '') ?? 0; } final socket = SocketService(); if (!socket.isConnected()) { print("CallScreen: Socket not connected during init, connecting..."); try { await socket.connect(ApiService()); } catch (e) { print("CallScreen socket connection error: $e"); } } await WebRTCService().initPeerConnection( widget.callId, widget.targetUserId, (remoteStream) { if (mounted) { setState(() { _remoteRenderer.srcObject = remoteStream; _isAccepted = true; }); _startTimer(); } }, myId, !widget.isIncoming, ); } void _startTimer() { _durationTimer?.cancel(); _durationSeconds = 0; _durationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (mounted) { setState(() { _durationSeconds++; }); } }); } void _stopTimer() { _durationTimer?.cancel(); } String _formatDuration(int seconds) { final int minutes = seconds ~/ 60; final int remainingSeconds = seconds % 60; return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; } void _handleAccept() async { setState(() { _isAccepted = true; }); WebRTCService().isCallAcceptedLocally = true; final myId = context.read().currentUserId ?? 0; // Сообщаем вызывающему, что вызов принят SocketService().sendMessage({ "type": "call_accepted", "call_id": widget.callId, "receiver_id": widget.targetUserId, "sender_id": myId, }); // Инициализируем WebRTC-соединение await _initConnection(); _startTimer(); } void _handleDecline() { SocketService().sendMessage({ "type": "decline", "call_id": widget.callId, "receiver_id": widget.targetUserId, }); _cleanupAndPop(); } void _handleHangup() { SocketService().sendMessage({ "type": "hangup", "call_id": widget.callId, "receiver_id": widget.targetUserId, }); _cleanupAndPop(); } void _cleanupAndPop() { if (_isPopped) return; _isPopped = true; if (CallScreen.currentActiveCallId == widget.callId) { CallScreen.currentActiveCallId = null; } _stopTimer(); WebRTCService().dispose(); FlutterCallkitIncoming.endAllCalls(); if (mounted) { Navigator.of(context).pop(); } } void _toggleMute() { setState(() { _isMuted = !_isMuted; }); WebRTCService().toggleMute(_isMuted); } void _toggleVideo() { setState(() { _isVideoOff = !_isVideoOff; }); WebRTCService().toggleVideo(_isVideoOff); // Уведомляем собеседника о переключении нашей камеры final myId = context.read().currentUserId ?? 0; SocketService().sendMessage({ "type": "video_toggle", "call_id": widget.callId, "receiver_id": widget.targetUserId, "sender_id": myId, "video_enabled": !_isVideoOff, }); } void _toggleSpeaker() { setState(() { _isSpeakerOn = !_isSpeakerOn; }); WebRTCService().toggleSpeakerphone(_isSpeakerOn); } void _switchCamera() async { await WebRTCService().switchCamera(); } @override void dispose() { _stopTimer(); _pulseController.dispose(); _localRenderer.srcObject = null; _remoteRenderer.srcObject = null; _localRenderer.dispose(); _remoteRenderer.dispose(); WebRTCService().onCallEnded = null; WebRTCService().onRemoteVideoToggled = null; WebRTCService().dispose(); if (CallScreen.currentActiveCallId == widget.callId) { CallScreen.currentActiveCallId = null; } super.dispose(); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final initials = widget.callerName.isNotEmpty ? widget.callerName.split(RegExp(r'\s+')).take(2).map((p) => p[0].toUpperCase()).join() : '?'; return Scaffold( backgroundColor: const Color(0xFF0F0F12), body: Stack( children: [ // 1. Задний фон / Удаленное видео Positioned.fill( child: _renderersInitialized && _isAccepted && _isRemoteVideoOn && _remoteRenderer.srcObject != null ? RTCVideoView( _remoteRenderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, ) : _buildWaitingScreen(initials), ), // 2. Плавающее локальное видео (картинка в картинке) после ответа if (_renderersInitialized && _isAccepted && !_isVideoOff && _localRenderer.srcObject != null) Positioned( top: MediaQuery.of(context).padding.top + 16, right: 16, child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Container( width: 110, height: 160, color: Colors.black26, child: RTCVideoView( _localRenderer, mirror: true, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, ), ), ), ), // 3. Статус звонка и таймер Positioned( top: MediaQuery.of(context).padding.top + 80, left: 0, right: 0, child: Column( children: [ Text( widget.callerName, style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 0.5, ), ), const SizedBox(height: 8), Text( _isAccepted ? _formatDuration(_durationSeconds) : (widget.isIncoming ? 'Входящий звонок...' : 'Вызов...'), style: TextStyle( color: Colors.white.withOpacity(0.7), fontSize: 16, fontWeight: FontWeight.w500, ), ), ], ), ), // 4. Панель управления звонком Positioned( bottom: 40, left: 20, right: 20, child: _buildControlsBar(colorScheme), ), ], ), ); } Widget _buildWaitingScreen(String initials) { return Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF1E1E24), Color(0xFF0F0F12)], ), ), child: Center( child: Stack( alignment: Alignment.center, children: [ if (!_isAccepted) AnimatedBuilder( animation: _pulseAnimation, builder: (context, child) { return Container( width: 140 * _pulseAnimation.value, height: 140 * _pulseAnimation.value, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.blueAccent.withOpacity(1.0 - (_pulseAnimation.value - 1.0) / 0.6), ), ); }, ), Container( width: 130, height: 130, decoration: const BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [Colors.blueAccent, Colors.indigoAccent], ), ), child: Center( child: Text( initials, style: const TextStyle( fontSize: 48, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ), ); } Widget _buildControlsBar(ColorScheme colorScheme) { return Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), decoration: BoxDecoration( color: Colors.black.withOpacity(0.4), borderRadius: BorderRadius.circular(30), border: Border.all(color: Colors.white.withOpacity(0.08)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // Кнопка звука _buildCircleButton( icon: _isMuted ? Icons.mic_off : Icons.mic, isActive: !_isMuted, onPressed: _toggleMute, ), // Кнопка динамика _buildCircleButton( icon: _isSpeakerOn ? Icons.volume_up : Icons.volume_down, isActive: _isSpeakerOn, onPressed: _toggleSpeaker, ), // Кнопка камеры _buildCircleButton( icon: _isVideoOff ? Icons.videocam_off : Icons.videocam, isActive: !_isVideoOff, onPressed: _toggleVideo, ), // Поворот камеры _buildCircleButton( icon: Icons.flip_camera_ios, isActive: true, onPressed: _switchCamera, ), const SizedBox(width: 8), // Кнопка Отклонить/Принять или Завершить if (widget.isIncoming && !_isAccepted) ...[ FloatingActionButton( heroTag: 'decline_btn', backgroundColor: Colors.redAccent, onPressed: _handleDecline, child: const Icon(Icons.call_end, color: Colors.white, size: 28), ), FloatingActionButton( heroTag: 'accept_btn', backgroundColor: Colors.greenAccent, onPressed: _handleAccept, child: const Icon(Icons.call, color: Colors.white, size: 28), ), ] else ...[ FloatingActionButton( heroTag: 'hangup_btn', backgroundColor: Colors.redAccent, onPressed: _handleHangup, child: const Icon(Icons.call_end, color: Colors.white, size: 28), ), ], ], ), ); } Widget _buildCircleButton({ required IconData icon, required bool isActive, required VoidCallback onPressed, }) { return Container( width: 50, height: 50, decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? Colors.white.withOpacity(0.12) : Colors.red.withOpacity(0.2), ), child: IconButton( icon: Icon(icon, color: isActive ? Colors.white : Colors.redAccent, size: 24), onPressed: onPressed, ), ); } }