Flutter高仿微信系列共59篇,从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。
详情请查看
效果图:


目前市场上第三方视频接口的价格高的吓人
视频通话价格:
标清(SD) 14元/千分钟
高清(HD) 28元/千分钟
超高清(Full HD)63元/千分钟
2K 112元/千分钟
4K 252元/千分钟
这里的视频通话不接第三方sdk,自己实现的视频服务器。
详情请参考Flutter高仿微信-第29篇-单聊 , 这里只是提取视频通话的部分代码。
实现代码:
/**
* Author : wangning
* Email : maoning20080809@163.com
* Date : 2022/9/25 14:46
* Description : 发起视频请求页面
*/
class VideoCallWidget extends StatefulWidget {
static String tag = 'video_call_widget';
//视频账号
final String videoPeerId;
final String mediaFlag;
String host = CommonUtils.BASE_IP;
VideoCallWidget({required this.videoPeerId, required this.mediaFlag});
@override
_VideoCallState createState() => _VideoCallState();
}
class _VideoCallState extends State<VideoCallWidget> {
Signaling? _signaling;
String? _selfId;
final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
bool _inCalling = false;
Session? _session;
DesktopCapturerSource? selected_source_;
bool _waitAccept = false;
bool _isExist = false;
UserBean? userBean;
//麦克风打开
bool isMic = true;
//扬声器
bool isSpeaker = true;
@override
initState() {
super.initState();
initRenderers();
_connect(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Timer(Duration(seconds: 1),(){
_invitePeer(context, widget.videoPeerId, false);
_inCalling = false;
setState(() {
});
});
});
loadUser();
_playVoice();
}
void loadUser () async{
userBean = await UserRepository.getInstance().findUserByAccount(widget.videoPeerId);
if(userBean != null){
setState(() {
});
}
}
initRenderers() async {
await _localRenderer.initialize();
await _remoteRenderer.initialize();
}
@override
deactivate() {
super.deactivate();
_signaling?.close();
_localRenderer.dispose();
_remoteRenderer.dispose();
_timer?.cancel();
_stopVoice();
}
void _connect(BuildContext context) async {
LogUtils.d("connect开始 ${widget.mediaFlag}");
_signaling ??= Signaling(widget.host, context)..connect();
LogUtils.d("connect结束");
_signaling?.onSignalingStateChange = (SignalingState state) {
LogUtils.d("video_call_sample onSignalingStateChange1: ${state}");
switch (state) {
case SignalingState.ConnectionClosed:
case SignalingState.ConnectionError:
case SignalingState.ConnectionOpen:
break;
}
};
_signaling?.onCallStateChange = (Session session, CallState state) async {
LogUtils.d("video_call_sample onCallStateChange2:${state} , _waitAccept = ${_waitAccept}");
switch (state) {
case CallState.CallStateNew:
setState(() {
_session = session;
});
break;
case CallState.CallStateRinging:
bool? accept = await _showAcceptDialog();
if (accept!) {
_accept();
setState(() {
_inCalling = true;
_processTimer();
});
} else {
_reject();
}
break;
case CallState.CallStateBye:
LogUtils.d("video_call_sample 挂断::${_waitAccept}, ${mounted}");
if(!_isExist){
Navigator.pop(context);
}
setState(() {
_localRenderer.srcObject = null;
_remoteRenderer.srcObject = null;
_inCalling = false;
_session = null;
});
break;
case CallState.CallStateInvite:
_waitAccept = true;
LogUtils.d("video_call_sample 邀请开始::${_waitAccept}");
break;
case CallState.CallStateConnected:
_stopVoice();
setState(() {
_inCalling = true;
_processTimer();
});
break;
case CallState.CallStateRinging:
}
};
_signaling?.onPeersUpdate = ((event) {
setState(() {
_selfId = event['self'];
LogUtils.d("video_call_sample 我的账号:${_selfId}");
});
});
_signaling?.onLocalStream = ((stream) {
LogUtils.d("video_call_sample onLocalStream 3:");
_localRenderer.srcObject = stream;
setState(() {});
});
_signaling?.onAddRemoteStream = ((_, stream) {
LogUtils.d("video_call_sample onAddRemoteStream 4:");
_remoteRenderer.srcObject = stream;
setState(() {});
});
_signaling?.onRemoveRemoteStream = ((_, stream) {
LogUtils.d("video_call_sample onRemoveRemoteStream 5 :");
_remoteRenderer.srcObject = null;
});
}
Future<bool?> _showAcceptDialog() {
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);
},
),
],
);
},
);
}
//开始播放视频声音
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 _showInvateWidget(){
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("等待对方接受邀请.", 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: FloatingActionButton(
child: Icon(Icons.call_end),
backgroundColor: Colors.pink,
onPressed: _hangUp,
),
),
],
),
);
}
_invitePeer(BuildContext context, String peerId, bool useScreen) async {
if (_signaling != null && peerId != _selfId) {
LogUtils.d("video_call_sample 邀请:${peerId} - ${widget.mediaFlag}");
_signaling?.invite(peerId, 'video', widget.mediaFlag, useScreen);
}
}
_accept() {
LogUtils.d("video_call_sample 接受:${_session}");
if (_session != null) {
_signaling?.accept(_session!.sid);
}
}
_reject() {
LogUtils.d("video_call_sample 拒绝:${_session}");
if (_session != null) {
_signaling?.reject(_session!.sid);
}
}
_hangUp() {
LogUtils.d("video_call_sample 挂起:${_session} , ${_session?.sid}");
if (_session != null) {
_signaling?.bye(_session!.sid);
}
_isExist = true;
Navigator.pop(context);
}
_switchCamera() {
LogUtils.d("video_call_sample 切换摄像头:${_session}");
_signaling?.switchCamera();
}
_muteMic() {
LogUtils.d("video_call_sample 音频:${_session}");
_signaling?.muteMic();
isMic = !isMic;
setState(() {
});
}
enableSpeakerphone() {
LogUtils.d("show_video_call 外放:_signaling = ${_signaling}");
_signaling?.enableSpeakerphone();
isSpeaker =!isSpeaker;
setState(() {
});
}
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
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>[
//扬声器图标: https://www.iconfont.cn/search/index?searchType=icon&q=扬声器
getSwitchCameraWidget(),
getHangUpWidget(),
getMicWidget(),
getSpeakerWidget(),
]))
: null,
body: _inCalling
? OrientationBuilder(builder: (context, orientation) {
return Container(
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),
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),
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),),
],
),
),
),
)
]),
);
})
: _showInvateWidget(),
);
}
//切换摄像头
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),),
],
),
));
}
}

![[附源码]java毕业设计医院网上预约系统](https://img-blog.csdnimg.cn/4d32debe961d47929466abe08bceb862.png)

















