import 'dart:developer'; import 'package:wanisclient/providers/utils_singleton.dart'; import 'package:flutter/material.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:get_it/get_it.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import '../services/agora_service.dart'; import '../services/auth_services.dart'; class VideoCallWidget extends StatefulWidget { final String channelName; final bool isDriver; final bool autoAnswer; final VoidCallback? onCallEnded; final Function(bool)? onCallStateChanged; const VideoCallWidget({ Key? key, required this.channelName, required this.isDriver, this.autoAnswer = false, this.onCallEnded, this.onCallStateChanged, }) : super(key: key); @override State createState() => _VideoCallWidgetState(); } class _VideoCallWidgetState extends State with WidgetsBindingObserver { final Set _remoteUsers = {}; bool _isCallActive = false; bool _isMuted = false; bool _isVideoEnabled = true; bool _isMinimized = true; int? _localUid; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); log("start !!!!!!!!!!!!!!!!!! _VideoCallWidgetState"); _initializeCall(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _leaveCall(); WakelockPlus.toggle(enable: false); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); // Handle app state changes for background/foreground switch (state) { case AppLifecycleState.paused: if (_isCallActive && !widget.isDriver) { // Keep call active in background for users _enableBackgroundMode(); } break; case AppLifecycleState.resumed: if (_isCallActive) { _disableBackgroundMode(); // Return to call if it was active setState(() { _isMinimized = false; }); } break; default: break; } } Future _initializeCall() async { await AgoraService.initialize(); await _setupEventHandlers(); log("_initializeCall ${widget.autoAnswer} && ${widget.isDriver}"); if (widget.autoAnswer || widget.isDriver) { await _joinCall(); } } Future _setupEventHandlers() async { AgoraService.engine?.registerEventHandler( RtcEngineEventHandler( onJoinChannelSuccess: (RtcConnection connection, int elapsed) { // --- ADD LOGGING --- print("✅ ✅ ✅ LOCAL USER JOINED SUCCESS: uid=${connection.localUid}"); setState(() { _localUid = connection.localUid; _isCallActive = true; }); widget.onCallStateChanged?.call(true); WakelockPlus.toggle(enable: true); }, onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) { // --- ADD LOGGING --- print("👤 ✅ REMOTE USER JOINED: uid=$remoteUid"); setState(() { _remoteUsers.add(remoteUid); }); }, onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) { // --- ADD LOGGING --- print("👤 ❌ REMOTE USER LEFT: uid=$remoteUid, reason=$reason"); setState(() { _remoteUsers.remove(remoteUid); }); }, onLeaveChannel: (RtcConnection connection, RtcStats stats) { // --- ADD LOGGING --- print("❌ ❌ ❌ LOCAL USER LEFT CHANNEL"); setState(() { _isCallActive = false; _remoteUsers.clear(); }); widget.onCallStateChanged?.call(false); widget.onCallEnded?.call(); }, onError: (ErrorCodeType err, String msg) { // --- ADD LOGGING --- print('⛔️ ⛔️ ⛔️ AGORA ERROR: $err - $msg'); }, ), ); } Future _joinCall() async { // --- 2. FETCH THE TOKEN FROM YOUR SERVER --- final response = await GetIt.I().fetchAgoraToken(widget.channelName, utils.user!.id!); log("token for agora : ${response.data}"); await AgoraService.engine?.joinChannel( token: response.data ??"null", // Use null for testing, implement token server for production channelId: widget.channelName, uid: utils.user!.id!, options: const ChannelMediaOptions( channelProfile: ChannelProfileType.channelProfileCommunication, clientRoleType: ClientRoleType.clientRoleBroadcaster, autoSubscribeAudio: true, autoSubscribeVideo: true, ), ); } Future _leaveCall() async { await AgoraService.engine?.leaveChannel(); } void _enableBackgroundMode() { // Minimize video to save battery but keep audio active AgoraService.engine?.muteLocalVideoStream(true); } void _disableBackgroundMode() { if (_isVideoEnabled) { AgoraService.engine?.muteLocalVideoStream(false); } } Widget _buildVideoView(int uid, {bool isLocal = false}) { if (isLocal) { return AgoraVideoView( controller: VideoViewController( rtcEngine: AgoraService.engine!, canvas: const VideoCanvas(uid: 0), ), ); } else { return AgoraVideoView( controller: VideoViewController.remote( rtcEngine: AgoraService.engine!, canvas: VideoCanvas(uid: uid), connection: RtcConnection(channelId: widget.channelName), ), ); } } Widget _buildMinimizedView() { return Positioned( top: 100, right: 16, child: GestureDetector( onTap: () => setState(() => _isMinimized = false), child: Container( width: 120, height: 160, decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: _remoteUsers.isNotEmpty ? _buildVideoView(_remoteUsers.first) : const Center( child: Icon(Icons.person, color: Colors.white, size: 40), ), ), ), ), ); } Widget _buildFullCallView() { return Container( color: Colors.black, child: Stack( children: [ // Remote video (main view) if (_remoteUsers.isNotEmpty) Positioned.fill( child: _buildVideoView(_remoteUsers.first), ) else const Positioned.fill( child: Center( child: Text( 'Waiting for connection...', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), // Local video (small overlay) Positioned( top: 50, right: 16, child: Container( width: 120, height: 160, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white, width: 2), ), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: _buildVideoView(0, isLocal: true), ), ), ), // Controls _buildControls(), ], ), ); } Widget _buildControls() { return Positioned( bottom: 50, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if(!widget.isDriver) // Mute button _buildControlButton( icon: _isMuted ? Icons.mic_off : Icons.mic, color: _isMuted ? Colors.red : Colors.white, backgroundColor: _isMuted ? Colors.white : Colors.black54, onTap: _toggleMute, ), if(!widget.isDriver) // Video toggle button _buildControlButton( icon: _isVideoEnabled ? Icons.videocam : Icons.videocam_off, color: _isVideoEnabled ? Colors.white : Colors.red, backgroundColor: _isVideoEnabled ? Colors.black54 : Colors.white, onTap: _toggleVideo, ), // Switch camera button (only for mobile) _buildControlButton( icon: Icons.cameraswitch, color: Colors.white, backgroundColor: Colors.black54, onTap: _switchCamera, ), // End call button (only for parent/user, not driver) if (!widget.isDriver) _buildControlButton( icon: Icons.call_end, color: Colors.white, backgroundColor: Colors.red, onTap: _endCall, ), // Minimize button _buildControlButton( icon: Icons.screenshot_monitor, color: Colors.white, backgroundColor: Colors.black54, onTap: () => setState(() => _isMinimized = true), ), ], ), ); } Widget _buildControlButton({ required IconData icon, required Color color, required Color backgroundColor, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( width: 50, height: 50, decoration: BoxDecoration( color: backgroundColor, shape: BoxShape.circle, ), child: Icon(icon, color: color, size: 24), ), ); } void _toggleMute() { setState(() => _isMuted = !_isMuted); AgoraService.engine?.muteLocalAudioStream(_isMuted); } void _toggleVideo() { setState(() => _isVideoEnabled = !_isVideoEnabled); AgoraService.engine?.muteLocalVideoStream(!_isVideoEnabled); } void _switchCamera() { AgoraService.engine?.switchCamera(); } void _endCall() { _leaveCall(); } @override Widget build(BuildContext context) { if (!_isCallActive) { return const SizedBox.shrink(); } if (_isMinimized) { return _buildMinimizedView(); } return _buildFullCallView(); } }