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.
338 lines
12 KiB
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);
|
|
}
|
|
}
|