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? 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();});}}
当接收到音频来电时弹出页面:
GlobalKeykeyShowVideoCall = 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);@overrideShowVideoCallState createState() => ShowVideoCallState(); }class ShowVideoCallState extends State {Signaling? _signaling;String? _selfId;RTCVideoRenderer _localRenderer = RTCVideoRenderer();RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();bool _inCalling = false;Session? _session;bool _waitAccept = false;bool _isExist = false;//好友idString otherUserId = "";UserBean? userBean;//麦克风打开bool isMic = true;//扬声器bool isSpeaker = true;@overrideinitState() {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(() {});});}}@overridedeactivate() {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 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 _showAcceptDialog() {LogUtils.d("显示对话框。。${_inCalling}");return showDialog (context: context,builder: (context) {return AlertDialog(title: Text("视频通话"),content: Text("是否接受好友的视频请求?"),actions: [TextButton(child: Text("拒绝"),onPressed: () => Navigator.of(context).pop(false),),TextButton(child: Text("接受"),onPressed: () {Navigator.of(context).pop(true);},),],);},);}Future _showInvateDialog() {return showDialog (context: context,builder: (context) {return AlertDialog(title: Text("视频通话"),content: Text("邀请好友视频通话,请等待对方接受。"),actions: [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();}@overrideWidget 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: [getSwitchCameraWidget(),getHangUpWidget(),getMicWidget(),getSpeakerWidget(),])): null,body: _inCalling?OrientationBuilder(builder: (context, orientation) {return Container(color: Colors.white,child: Stack(children: [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),),],),));}}