import 'dart:async'; import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; import 'package:test_sa/modules/cx_module/chat/model/call_session.dart'; import 'package:test_sa/modules/cx_module/chat/call/utils/permission_helper.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; class VideoCallPage extends StatefulWidget { const VideoCallPage({Key? key}) : super(key: key); @override State createState() => _VideoCallPageState(); } class _VideoCallPageState extends State { late CallManager _callManager; Timer? _streamCheckTimer; bool _isLocalVideoFullscreen = false; @override void initState() { super.initState(); // HYBRID: Get CallManager from Provider (UI layer) _callManager = Provider.of(context, listen: false); // Listen for call end _callManager.addListener(_checkCallStatus); // Start periodic check to ensure remote stream gets assigned _startStreamCheckTimer(); // CRITICAL: Check permissions after first frame // Permissions must be requested in UI layer, not service layer WidgetsBinding.instance.addPostFrameCallback((_) { _checkPermissions(); }); } /// Request video call permissions using production-ready helper Future _checkPermissions() async { if (!mounted) return; // Request all video call permissions (microphone + camera) final granted = await CallPermissionHelper.requestVideoCallPermissions(context); if (!granted && mounted) { // Permission denied - end the call gracefully await CallPermissionHelper.handlePermissionDeniedDuringCall( context, 'Camera or Microphone', onEndCall: () => _callManager.declineCall('permission_denied'), delaySeconds: 2, ); } } void _startStreamCheckTimer() { _streamCheckTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { final webrtc = _callManager.webrtcService; if (webrtc == null || !mounted) { timer.cancel(); return; } // If we have remote stream but renderer doesn't have it, assign it if (webrtc.remoteStream != null && webrtc.remoteRenderer != null && webrtc.remoteRenderer!.srcObject == null) { try { webrtc.remoteRenderer!.srcObject = webrtc.remoteStream; if (mounted) setState(() {}); } catch (e) { if (kDebugMode) { log('⚠️ Stream assignment failed: $e', name: 'VideoCallPage'); } } } // If call is connected and remote stream is assigned, slow down checks if (_callManager.callStatus == CallStatus.connected && webrtc.remoteRenderer?.srcObject != null) { timer.cancel(); } }); } void _checkCallStatus() { if (_callManager.callStatus == CallStatus.idle && mounted) { Navigator.of(context).pop(); } } @override void dispose() { _streamCheckTimer?.cancel(); _callManager.removeListener(_checkCallStatus); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.backgroundTabBarDark, body: SafeArea( top: false, child: ListenableBuilder( listenable: _callManager, builder: (context, _) { final session = _callManager.currentCall; if (session == null) { return const Center( child: Text('No active call', style: TextStyle(color: Colors.white)), ); } return Stack( children: [ // Remote video (fullscreen by default) if (!_isLocalVideoFullscreen) _buildRemoteVideo(context), // Local video (fullscreen when tapped) if (_isLocalVideoFullscreen) _buildFullscreenLocalVideo(context), // Small preview (top-right corner) if (_isLocalVideoFullscreen) _buildSmallRemoteVideo(context) else _buildLocalVideoPreview(context), _buildTopBar(context, session), _buildBottomControls(context, session), ], ); }, ), ), ); } Widget _buildRemoteVideo(BuildContext context) { final webrtc = _callManager.webrtcService; final isPeerCameraOn = _callManager.isPeerCameraOn; return Positioned.fill( child: GestureDetector( onTap: () => setState(() => _isLocalVideoFullscreen = true), child: Container( color: Colors.black, child: !isPeerCameraOn || webrtc?.remoteRenderer == null ? _buildAvatarPlaceholder(_callManager.currentCall?.peerName ?? 'User', large: true) : RTCVideoView( webrtc!.remoteRenderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, ), ), ), ); } Widget _buildFullscreenLocalVideo(BuildContext context) { final webrtc = _callManager.webrtcService; final isCameraOn = _callManager.isCameraOn; return Positioned.fill( child: GestureDetector( onTap: () => setState(() => _isLocalVideoFullscreen = false), child: Container( color: Colors.grey[800], child: !isCameraOn || webrtc?.localRenderer == null ? const Center(child: Icon(Icons.videocam_off, color: Colors.white, size: 64)) : RTCVideoView( webrtc!.localRenderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: true, ), ), ), ); } Widget _buildLocalVideoPreview(BuildContext context) { final webrtc = _callManager.webrtcService; final isCameraOn = _callManager.isCameraOn; return Positioned( top: 100, right: 16, child: GestureDetector( onTap: () => setState(() => _isLocalVideoFullscreen = true), child: Container( width: 120, height: 160, decoration: BoxDecoration( color: Colors.grey[800], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white24, width: 2), ), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: !isCameraOn || webrtc?.localRenderer == null ? const Center(child: Icon(Icons.videocam_off, color: Colors.white, size: 32)) : RTCVideoView( webrtc!.localRenderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: true, ), ), ), ), ); } Widget _buildSmallRemoteVideo(BuildContext context) { final webrtc = _callManager.webrtcService; final isPeerCameraOn = _callManager.isPeerCameraOn; return Positioned( top: 100, right: 16, child: GestureDetector( onTap: () => setState(() => _isLocalVideoFullscreen = false), child: Container( width: 120, height: 160, decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white24, width: 2), ), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: !isPeerCameraOn || webrtc?.remoteRenderer == null ? _buildAvatarPlaceholder(_callManager.currentCall?.peerName ?? 'User', large: false) : RTCVideoView( webrtc!.remoteRenderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, ), ), ), ), ); } Widget _buildAvatarPlaceholder(String name, {required bool large}) { return Container( color: AppColor.backgroundTabBarDark, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.person, size: large ? 80 : 40, color: Colors.white54, ), if (large) ...[ 12.height, Text( name, style: AppTextStyles.heading5.copyWith(color: Colors.white), ), 4.height, Text( 'Camera is off', style: AppTextStyles.bodyText2.copyWith(color: Colors.white54), ), ], ], ), ), ); } Widget _buildTopBar(BuildContext context, CallSession session) { final status = _callManager.callStatus; final duration = _callManager.callDuration; String statusText; if (status == CallStatus.connected) { final minutes = duration.inMinutes; final seconds = duration.inSeconds % 60; statusText = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } else { statusText = _getStatusText(status); } return Positioned( top: 0, left: 0, right: 0, child: Container( padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top + 16, bottom: 16, left: 16, right: 16, ), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.6), Colors.transparent, ], ), ), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), Expanded( child: Column( children: [ Text( session.peerName, style: AppTextStyles.heading6.copyWith(color: Colors.white), ), 4.height, Text( statusText, style: AppTextStyles.bodyText2.copyWith(color: Colors.white70), ), ], ), ), const SizedBox(width: 48), ], ), ), ); } String _getStatusText(CallStatus status) { switch (status) { case CallStatus.connecting: return 'Connecting...'; case CallStatus.incomingRinging: return 'Incoming call...'; case CallStatus.outgoingRinging: return 'Ringing...'; case CallStatus.connected: return 'Connected'; default: return ''; } } Widget _buildBottomControls(BuildContext context, CallSession session) { final isMuted = _callManager.isMuted; final isCameraOn = _callManager.isCameraOn; final isSpeakerOn = _callManager.isSpeakerOn; return Positioned( bottom: 40, left: 0, right: 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildControlButton( icon: isMuted ? Icons.mic_off : Icons.mic, label: isMuted ? 'Unmute' : 'Mute', onPressed: () => _callManager.toggleMute(), backgroundColor: isMuted ? Colors.red : Colors.white24, ), _buildControlButton( icon: isCameraOn ? Icons.videocam : Icons.videocam_off, label: isCameraOn ? 'Camera' : 'Camera Off', onPressed: () => _callManager.toggleCamera(), backgroundColor: isCameraOn ? Colors.white24 : Colors.red, ), _buildControlButton( icon: Icons.cameraswitch, label: 'Switch', onPressed: () => _callManager.switchCamera(), backgroundColor: Colors.white24, ), _buildControlButton( icon: Icons.call_end, label: 'End', onPressed: () => _callManager.hangUp(), backgroundColor: Colors.red, iconColor: Colors.white, ), ], ), ), ); } Widget _buildControlButton({ required IconData icon, required String label, required VoidCallback onPressed, Color? backgroundColor, Color? iconColor, }) { return Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: backgroundColor ?? Colors.white24, shape: BoxShape.circle, border: Border.all(color: Colors.white24), ), child: Icon(icon, size: 24, color: iconColor ?? Colors.white), ).onPress(onPressed), 8.height, Text( label, style: AppTextStyles.bodyText2.copyWith(color: Colors.white), ), ], ); } }