Flutter高仿微信系列共59篇,从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。
效果图:
目前市场上第三方音频接口的价格高的吓人
语音通话价格:
5元/千分钟
这里的语音通话不接第三方sdk,自己实现的音视频服务器。
详情请参考 Flutter高仿微信-第29篇-单聊 , 这里只是提取音频通话的部分代码。
实现代码:
音频监听:
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/10/4 10:43 * Description : */ class VideoCallUtils { static final VideoCallUtils _instance = VideoCallUtils._internal(); static VideoCallUtils getInstance(){ return _instance; } VideoCallUtils._internal(){ } Signaling? _signaling; //String host = "demo.cloudwebrtc.com"; String host = CommonUtils.BASE_IP; Session? _session; var localStream; var remoteStream; void connect(BuildContext context) async { _signaling ??= Signaling(host, context)..connect(); _signaling?.onSignalingStateChange = (SignalingState state) { switch (state) { case SignalingState.ConnectionClosed: case SignalingState.ConnectionError: case SignalingState.ConnectionOpen: break; } }; _signaling?.onCallStateChange = (Session session, CallState state) async { LogUtils.d("video_call_utils 回调状态:${state}, ${session.sid} , ${session.pid}"); switch (state) { case CallState.CallStateNew: _session = session; break; case CallState.CallStateRinging: String sid = session.sid; String mediaFlag = ""; Map<String, dynamic>? configMap = session.pc?.getConfiguration; LogUtils.d("video_call_utils 是否map :${configMap}"); if(configMap != null){ configMap.forEach((key, value) { if(key == "mediaFlag"){ LogUtils.d("video_call_utils 是否3:${key} , ${value}"); mediaFlag = value; } }); } Navigator.push(context, MaterialPageRoute(builder: (context) => ShowVideoCall(host: host, signaling: _signaling, session: _session,key: keyShowVideoCall,mediaFlag: mediaFlag,))); break; case CallState.CallStateBye: LogUtils.d("video_call_utils 33退出:${keyShowVideoCall} , , ${keyShowVideoCall.currentState}"); keyShowVideoCall.currentState?.callStateBye(); break; case CallState.CallStateInvite: keyShowVideoCall.currentState?.callStateInvite(); break; case CallState.CallStateConnected: keyShowVideoCall.currentState?.callStateConnected(); break; case CallState.CallStateRinging: } }; _signaling?.onPeersUpdate = ((event) { }); _signaling?.onLocalStream = ((stream) { localStream = stream; keyShowVideoCall.currentState?.onLocalStream(stream); }); _signaling?.onAddRemoteStream = ((_, stream) { remoteStream = stream; keyShowVideoCall.currentState?.onAddRemoteStream(stream); }); _signaling?.onRemoveRemoteStream = ((_, stream) { keyShowVideoCall.currentState?.onRemoveRemoteStream(); }); } }
当接收到音频来电时弹出页面:
GlobalKey<ShowVideoCallState> keyShowVideoCall = GlobalKey(); /** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/10/4 10:43 * Description : 显示音视频通话 */ class ShowVideoCall extends StatefulWidget { static String tag = 'call_sample'; final String host; final Signaling? signaling; final Session? session; final String mediaFlag; ShowVideoCall({required Key key, required this.host, required this.signaling, required this.session, required this.mediaFlag}) :super(key:key); @override ShowVideoCallState createState() => ShowVideoCallState(); } class ShowVideoCallState extends State<ShowVideoCall> { Signaling? _signaling; String? _selfId; RTCVideoRenderer _localRenderer = RTCVideoRenderer(); RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); bool _inCalling = false; Session? _session; bool _waitAccept = false; bool _isExist = false; //好友id String otherUserId = ""; UserBean? userBean; //麦克风打开 bool isMic = true; //扬声器 bool isSpeaker = true; @override initState() { super.initState(); _session = widget.session; _signaling = widget.signaling; otherUserId = _session?.pid??""; initRenderers(); //_connect(context); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { LogUtils.d("显示视频加载完成。。。${_session} , ${_signaling}, ${_session?.sid} , ${_session?.pid}"); callStateRinging(); }); loadUser(); } void loadUser() async{ userBean = await UserRepository.getInstance().findUserByAccount(otherUserId); if(userBean != null){ setState(() { }); } } initRenderers() async { await _localRenderer.initialize(); await _remoteRenderer.initialize(); var localStream = VideoCallUtils.getInstance().localStream; var remoteStream = VideoCallUtils.getInstance().remoteStream; LogUtils.d("show_video_call initRenderers视频 :${localStream}, ${remoteStream}"); _localRenderer.srcObject = localStream; _remoteRenderer.srcObject = remoteStream; } Timer? _timer; //计时多少秒 int currentTimer = 0; //转换结果时间 String resultTimer = "00:00"; void _processTimer(){ if(_inCalling && widget.mediaFlag == CommonUtils.MEDIA_FLAG_VOICE){ _timer = Timer.periodic(Duration(seconds: 1), (timer) { currentTimer++; resultTimer = WnDateUtils.changeSecondToMMSS(currentTimer); setState(() { }); }); } } @override deactivate() { super.deactivate(); /*_signaling?.close(); _localRenderer.dispose(); _remoteRenderer.dispose();*/ _stopVoice(); } void callStateConnected(){ LogUtils.d("show_video_call callStateConnected :${_waitAccept}"); if (_waitAccept) { _waitAccept = false; Navigator.of(context).pop(false); } setState(() { _inCalling = true; _processTimer(); }); } void onRemoveRemoteStream(){ LogUtils.d("show_video_call onRemoveRemoteStream "); _remoteRenderer.srcObject = null; } void onAddRemoteStream(stream){ LogUtils.d("show_video_call onAddRemoteStream ${stream} "); _remoteRenderer.srcObject = stream; setState(() {}); } void onLocalStream(stream){ LogUtils.d("show_video_call onLocalStream ${stream} "); _localRenderer.srcObject = stream; setState(() {}); } void callStateInvite(){ LogUtils.d("show_video_call callStateInvite "); _waitAccept = true; _showInvateDialog(); } void callStateBye(){ LogUtils.d("show_video_call callStateBye ${_waitAccept}"); /*if (_waitAccept) { _waitAccept = false; Navigator.of(context).pop(false); }*/ if(!_isExist){ Navigator.pop(context); } setState(() { _localRenderer.srcObject = null; _remoteRenderer.srcObject = null; _inCalling = false; _session = null; }); } void callStateRinging() async{ _playVoice(); await _showAcceptWidget(); } void callStateRingingResult(bool? accept) async{ _stopVoice(); if (accept!) { _accept(); setState(() { _inCalling = true; _processTimer(); }); } else { _reject(); } } //开始播放视频声音 void _playVoice(){ final List<String> soundList = CommonUtils.getSoundList(); int selectedVideoCallId = SpUtils.getIntDefaultValue(CommonUtils.SETTING_VIDEO_CALL_ID, 2); bool videoCallSwitch = SpUtils.getBoolDefaultValue(CommonUtils.SETTING_VIDEO_CALL_SWITCH, true); //如果设置视频通话不响铃 if(!videoCallSwitch){ return; } //设置了视频通话响铃,但是选择无声音 if(videoCallSwitch && selectedVideoCallId == 0){ return; } String sound = "${soundList[selectedVideoCallId]}"; AudioPlayer.getInstance().playAsset("sounds/${sound}.mp3", isLoop:true, callback:(data){ LogUtils.d("播放视频声音:${data}"); }); } void _stopVoice(){ AudioPlayer.getInstance().stop(); } //显示邀请页面 Widget _showAcceptWidget(){ return Container( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ //SizedBox(height: 30,), Container( alignment: AlignmentDirectional.center, margin: EdgeInsets.only(top: 18), child: Column( children: [ //Image.asset(CommonUtils.getBaseIconUrlPng("wc_chat_speaker_open"), width: 28, height: 28,), Text("邀请你${widget.mediaFlag == CommonUtils.MEDIA_FLAG_VIDEO? '视频通话':'语音通话'}", style: TextStyle(fontSize: 18, color: Colors.black),), SizedBox(height: 30,), CommonAvatarView.showBaseImage(userBean?.avatar??"", 100, 100), SizedBox(height: 10,), Text("${userBean?.nickName}", style: TextStyle(fontSize: 26, color: Colors.black),), ], ), ), Container( margin: EdgeInsets.only(bottom: 40), alignment: AlignmentDirectional.center, child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 80, height: 80, child: FloatingActionButton( child: Icon(Icons.call_end, size: 38,), backgroundColor: Colors.pink, onPressed: (){ callStateRingingResult(false); }, ), ), SizedBox(width: 40,), Container( width: 80, height: 80, child: FloatingActionButton( child: Icon(Icons.call_end, size: 38,), backgroundColor: Colors.lightGreen, onPressed: (){ callStateRingingResult(true); }, ), ), ], ), ), ], ), ); } Future<bool?> _showAcceptDialog() { LogUtils.d("显示对话框。。${_inCalling}"); return showDialog<bool?>( context: context, builder: (context) { return AlertDialog( title: Text("视频通话"), content: Text("是否接受好友的视频请求?"), actions: <Widget>[ TextButton( child: Text("拒绝"), onPressed: () => Navigator.of(context).pop(false), ), TextButton( child: Text("接受"), onPressed: () { Navigator.of(context).pop(true); }, ), ], ); }, ); } Future<bool?> _showInvateDialog() { return showDialog<bool?>( context: context, builder: (context) { return AlertDialog( title: Text("视频通话"), content: Text("邀请好友视频通话,请等待对方接受。"), actions: <Widget>[ TextButton( child: Text("取消"), onPressed: () { Navigator.of(context).pop(false); _hangUp(); }, ), ], ); }, ); } _accept() { LogUtils.d("show_video_call 接受1:${_session}, ${_signaling}"); if (_session != null) { LogUtils.d("show_video_call 接受2:${_session}"); _signaling?.accept(_session!.sid); } } _reject() { LogUtils.d("show_video_call 拒绝:${_session}"); if (_session != null) { _signaling?.reject(_session!.sid); } } _hangUp() { LogUtils.d("show_video_call 挂起:${_session}, ${_session?.sid}"); if (_session != null) { _signaling?.bye(_session!.sid); } _isExist = true; Navigator.pop(context); } _switchCamera() { LogUtils.d("show_video_call 切换摄像头:${_session}"); _signaling?.switchCamera(); } _muteMic() { LogUtils.d("show_video_call 切换音频:_signaling = ${_signaling}"); _signaling?.muteMic(); } enableSpeakerphone() { LogUtils.d("show_video_call 外放:_signaling = ${_signaling}"); _signaling?.enableSpeakerphone(); } @override Widget build(BuildContext context) { return Scaffold( appBar: WnAppBar.getAppBar(context, Text(widget.mediaFlag == CommonUtils.MEDIA_FLAG_VIDEO? '视频通话':'语音通话')), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: _inCalling ? SizedBox( width: double.infinity, child: Row( children: <Widget>[ getSwitchCameraWidget(), getHangUpWidget(), getMicWidget(), getSpeakerWidget(), ])) : null, body: _inCalling? OrientationBuilder(builder: (context, orientation) { return Container( color: Colors.white, child: Stack(children: <Widget>[ Positioned( left: 0.0, right: 0.0, top: 0.0, bottom: 0.0, child: Offstage( offstage: widget.mediaFlag == CommonUtils.MEDIA_FLAG_VOICE, child: Container( margin: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0), width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: RTCVideoView(_remoteRenderer, filterQuality: FilterQuality.high,), decoration: BoxDecoration(color: Colors.black54), ), ), ), Positioned( left: 20.0, top: 20.0, child: Offstage( offstage: widget.mediaFlag == CommonUtils.MEDIA_FLAG_VOICE, child: Container( width: orientation == Orientation.portrait ? 90.0 : 120.0, height: orientation == Orientation.portrait ? 120.0 : 90.0, child: RTCVideoView(_localRenderer, mirror: true, filterQuality: FilterQuality.high,), decoration: BoxDecoration(color: Colors.black54), ), ), ), Positioned( left: 20.0, right: 20.0, top: 30.0, child: Offstage( offstage: widget.mediaFlag == CommonUtils.MEDIA_FLAG_VIDEO, child: Container( width: orientation == Orientation.portrait ? 190.0 : 220.0, height: orientation == Orientation.portrait ? 220.0 : 190.0, child: Column( children: [ Text("${resultTimer}", style: TextStyle(fontSize: 20, color: Colors.grey.shade500),), SizedBox(height: 40,), CommonAvatarView.showBaseImage(userBean?.avatar??"", 80, 80), SizedBox(height: 8,), Text("${userBean?.nickName}", style: TextStyle(fontSize: 18, color: Colors.black),), ], ), ), ), ) ]), ); }):_showAcceptWidget(), ); } //切换摄像头 Widget getSwitchCameraWidget(){ return Expanded( child: Container( width: 80, height: 100, child: Column( children: [ FloatingActionButton( child: const Icon(Icons.switch_camera), onPressed: _switchCamera, ), SizedBox(height: 10,), Text("切换摄像头", style: TextStyle(fontSize: 12, color: Colors.white),), ], ), ) ); } //挂断 Widget getHangUpWidget(){ return Expanded( child:Container( width: 80, height: 100, child: Column( children: [ FloatingActionButton( child: Icon(Icons.call_end), backgroundColor: Colors.pink, onPressed: _hangUp, ), SizedBox(height: 10,), Text("挂 断", style: TextStyle(fontSize: 12, color: Colors.white),), ], ), ) ); } //麦克风 Widget getMicWidget(){ return Expanded( child: Container( width: 80, height: 100, child: Column( children: [ FloatingActionButton( child: Icon(isMic?Icons.mic:Icons.mic_off), onPressed: _muteMic, ), SizedBox(height: 10,), Text(isMic?"麦克风已开":"麦克风已关", style: TextStyle(fontSize: 12, color: Colors.white),), ], ), ) ); } //扬声器 Widget getSpeakerWidget(){ return Expanded( child:Container( width: 80, height: 100, child: Column( children: [ FloatingActionButton( child: Image.asset(CommonUtils.getBaseIconUrlPng(isSpeaker?"wc_chat_speaker_open":"wc_chat_speaker_close"), width: 28, height: 28,), onPressed: enableSpeakerphone, ), SizedBox(height: 10,), Text(isSpeaker?"扬声器已开":"扬声器已关", style: TextStyle(fontSize: 12, color: Colors.white),), ], ), )); } }