import 'package:flutter/material.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/model/chat_participant_model.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/swipe_module/dialoge/single_btn_dialog.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import '../chat_provider.dart'; import 'call_provider.dart'; class VideoCallPage extends StatefulWidget { final Participants sender; final Participants recipient; final int moduleId; final int? referenceID; final bool isAccept; const VideoCallPage({Key? key, required this.sender, required this.recipient, this.isAccept = false, required this.moduleId, this.referenceID}) : super(key: key); @override State createState() => _VideoCallPageState(); } class _VideoCallPageState extends State { late CallProvider callPro; bool isDialogOpen = false; bool isLocalVideoFullscreen = false; bool _isClosing = false; @override void initState() { super.initState(); callPro = Provider.of(context, listen: false); makeCall(); } void makeCall() async { bool isConnected = false; try { isConnected = await callPro.buildHubConnection(widget.sender.employeeNumber ?? "", hubC: chatHubConnection); } catch (e) { isConnected = false; } if (!mounted) return; if (!isConnected) { _showFailure("Error while connecting to call service. Please try again later.", title: "Connection Error"); return; } callPro.initCallListeners(); if (widget.isAccept) { callPro.acceptCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", true, moduleId: widget.moduleId, onSuccess: (String msg) { _closeCall(); }, onFailed: (String msg) { _showFailure(msg, title: "Failed"); }); } else { callPro.startCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", true, widget.moduleId, widget.referenceID, onSuccess: (String msg) { _closeCall(); }, onFailed: (String msg) { _showFailure(msg, title: "Failed"); }); } } void _closeCall() { if (_isClosing) return; _isClosing = true; callPro.endSession(); if (mounted) Navigator.pop(context); } void _showFailure(String message, {required String title}) { if (_isClosing) return; callPro.endSession(); if (!mounted || isDialogOpen) return; isDialogOpen = true; showDialog( context: context, barrierDismissible: false, useRootNavigator: false, builder: (BuildContext cxt) => PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) return; }, child: SingleBtnDialog( title: title, message: message, okTitle: "Go Back", onTap: () { Navigator.pop(cxt); if (_isClosing) return; _isClosing = true; if (mounted) Navigator.pop(context); }), ), ).then((_) { isDialogOpen = false; }); } @override void dispose() { callPro.endSession(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ _background(), // NOTE: the selector must include renderer/srcObject readiness, not // just isPeerCameraOn. isPeerCameraOn rarely changes during a call, // so a Selector keyed on it alone never rebuilds once the remote // video stream attaches later (after WebRTC negotiation finishes) - // which made the remote feed look permanently "off". Selector( selector: (_, p) => (p.isPeerCameraOn, p.webRTCService.remoteRenderer?.srcObject != null), builder: (_, state, __) => isLocalVideoFullscreen ? _fullscreenLocalVideo() : _remoteVideo(state.$1), ), isLocalVideoFullscreen ? _smallRemoteVideo() : _localVideoPreview(), _topBar(), _bottomControls(), ], ), ); } Widget _background() { return Container( width: double.infinity, height: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0xFF202641), Color(0xFF283859), Color(0xFF335073), Color(0xFF425E7A), Color(0xFF4A637D), Color(0xFF39495F), Color(0xFF1A2032), Color(0xFF121426), Color(0xFF121426), Color(0xFF25283A), Color(0xFF383D4D), ], stops: [0.0, 0.08, 0.15, 0.23, 0.27, 0.34, 0.42, 0.51, 0.70, 0.89, 0.97], ), ), ); } Widget _remoteVideo(bool isPeerCameraOn) { final renderer = callPro.webRTCService.remoteRenderer; final hasStream = renderer?.srcObject != null; return Positioned.fill( child: GestureDetector( onTap: () => setState(() => isLocalVideoFullscreen = true), child: !isPeerCameraOn || !hasStream ? _avatarPlaceholder(widget.recipient.userName ?? '', large: true) : RTCVideoView(renderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover), ), ); } Widget _fullscreenLocalVideo() { return Selector( selector: (_, p) => (p.isCameraOn, p.webRTCService.localRenderer?.srcObject != null), builder: (_, state, __) { final isCameraOn = state.$1; final renderer = callPro.webRTCService.localRenderer; final hasStream = renderer?.srcObject != null; return Positioned.fill( child: GestureDetector( onTap: () => setState(() => isLocalVideoFullscreen = false), child: !isCameraOn || !hasStream ? const Center(child: Icon(Icons.videocam_off, color: Colors.white54, size: 64)) : RTCVideoView(renderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: true), ), ); }, ); } Widget _localVideoPreview() { return Positioned( top: 90, right: 16, child: GestureDetector( onTap: () => setState(() => isLocalVideoFullscreen = true), child: Selector( selector: (_, p) => (p.isCameraOn, p.webRTCService.localRenderer?.srcObject != null), builder: (_, state, __) { final isCameraOn = state.$1; final renderer = callPro.webRTCService.localRenderer; final hasStream = renderer?.srcObject != null; return _previewBox( child: !isCameraOn || !hasStream ? const Center(child: Icon(Icons.videocam_off, color: Colors.white, size: 28)) : RTCVideoView(renderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: true), ); }, ), ), ); } Widget _smallRemoteVideo() { return Positioned( top: 90, right: 16, child: GestureDetector( onTap: () => setState(() => isLocalVideoFullscreen = false), child: Selector( selector: (_, p) => (p.isPeerCameraOn, p.webRTCService.remoteRenderer?.srcObject != null), builder: (_, state, __) { final isPeerCameraOn = state.$1; final renderer = callPro.webRTCService.remoteRenderer; final hasStream = renderer?.srcObject != null; return _previewBox( child: !isPeerCameraOn || !hasStream ? _avatarPlaceholder(widget.recipient.userName ?? '', large: false) : RTCVideoView(renderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover), ); }, ), ), ); } Widget _previewBox({required Widget child}) { return Container( width: 110, height: 150, decoration: BoxDecoration( color: const Color(0xff414352), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withValues(alpha: .2), width: 1), ), child: ClipRRect(borderRadius: BorderRadius.circular(11), child: child), ); } Widget _avatarPlaceholder(String name, {required bool large}) { return Container( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person, size: large ? 80 : 36, color: Colors.white54), if (large && name.isNotEmpty) ...[ 12.height, InfoTextLabelWidget(label: name, textStyle: AppTextStyles.heading5, color: Colors.white), ], ], ), ); } Widget _topBar() { return Positioned( top: 0, left: 0, right: 0, child: SafeArea( child: Column( children: [ 16.height, InfoTextLabelWidget(label: widget.recipient.userName ?? '', textStyle: AppTextStyles.heading4.copyWith(fontWeight: FontWeight.w600), color: Colors.white), 4.height, Selector( selector: (_, p) => p.currentStatus == "Connected" ? p.formattedCallDuration : p.currentStatus, builder: (_, status, __) => InfoTextLabelWidget(label: status, textStyle: AppTextStyles.heading6, color: Colors.white), ), 4.height, Selector( selector: (_, p) => p.isPeerMuted, builder: (_, isPeerMuted, __) => isPeerMuted ? InfoTextLabelWidget(label: "Mic Mute", textStyle: AppTextStyles.tinyFont, color: AppColor.red30) : const SizedBox(), ), ], ), ), ); } Widget _bottomControls() { return Positioned( left: 24, right: 24, bottom: 50, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Selector( selector: (_, p) => p.isMuted, builder: (_, isMuted, __) => callButton(isMuted ? "calling/mic_disable" : "calling/mic_enable", () => callPro.toggleMute()), ), Selector( selector: (_, p) => p.isCameraOn, builder: (_, isCameraOn, __) => callButton(isCameraOn ? "calling/video_enable" : "calling/video_disable", () => callPro.toggleCamera()), ), callButton("calling/toggle_camera", () => callPro.switchCamera()), callButton("calling/end_call", () { if (_isClosing) return; _isClosing = true; callPro.endCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", moduleId: widget.moduleId); Navigator.pop(context); }, isEndCall: true, size: 68), ], ), ); } Widget callButton(String icon, VoidCallback onTap, {bool isEndCall = false, double size = 52}) { return (isEndCall ? Container( height: size, width: size, padding: const EdgeInsets.all(18), decoration: const BoxDecoration(color: AppColor.red30, shape: BoxShape.circle), child: icon.toSvgAsset(height: 24, width: 24, color: Colors.white), ) : Container( height: size, width: size, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: const Color(0xff414352), shape: BoxShape.circle, border: Border.all(color: Colors.white.withValues(alpha: .2), width: 1)), child: icon.toSvgAsset(height: 24, width: 24, color: Colors.white), )) .onPress(onTap); } }