You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudsolutions-atoms/lib/modules/cx_module/chat/calling/video_call_page.dart

338 lines
12 KiB
Dart

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<VideoCallPage> createState() => _VideoCallPageState();
}
class _VideoCallPageState extends State<VideoCallPage> {
late CallProvider callPro;
bool isDialogOpen = false;
bool isLocalVideoFullscreen = false;
@override
void initState() {
super.initState();
callPro = Provider.of<CallProvider>(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() {
callPro.endSession();
if (mounted) Navigator.pop(context);
}
void _showFailure(String message, {required String title}) {
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 (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<CallProvider, (bool, bool)>(
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<CallProvider, (bool, bool)>(
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<CallProvider, (bool, bool)>(
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<CallProvider, (bool, bool)>(
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<CallProvider, String>(
selector: (_, p) => p.currentStatus == "Connected" ? p.formattedCallDuration : p.currentStatus,
builder: (_, status, __) => InfoTextLabelWidget(label: status, textStyle: AppTextStyles.heading6, color: Colors.white),
),
4.height,
Selector<CallProvider, bool>(
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<CallProvider, bool>(
selector: (_, p) => p.isMuted,
builder: (_, isMuted, __) => callButton(isMuted ? "calling/mic_disable" : "calling/mic_enable", () => callPro.toggleMute()),
),
Selector<CallProvider, bool>(
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", () {
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);
}
}