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),),
],
),
));
}
}



















