• Flutter高仿微信-第35篇-单聊-视频通话


    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 {
      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 _showAcceptDialog() {
        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);
                  },
                ),
              ],
            );
          },
        );
      }
    
      //开始播放视频声音
      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 _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: [
                        //扬声器图标:  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: [
    
                      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),),
            ],
          ),
        ));
      }
    
    }

  • 相关阅读:
    SpringBoot入门教程:数据库恢复(mysqldump和mysqlbinlog)
    通过containerd部署k8s集群环境及初始化时部分报错解决
    IP路由的原理
    手写RPC框架--4.服务注册
    在其他进程中访问NLOG创建的日志
    linux下的文本处理工具awk学习
    海外接单被没收百万收入并处以罚款,承德的这位程序员到底做了什么?
    Windows cmd/powershell 管道过滤命令: find
    【大数据采集工具-gobblin】
    【推荐系统中的Hash 1】Hash Trick:原始数据—>特征,尽可能避免冲突
  • 原文地址:https://blog.csdn.net/maoning20080808/article/details/128014604