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

news2025/8/12 12:39:00

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

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/33499.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营day55 | 392.判断子序列,115.不同的子序列

392.判断子序列 参考代码随想录算法训练营第五十五天 |392. 判断子序列、115. 不同的子序列 - 掘金 暴力解法&#xff1a; Time Complexity: O(M*N) Space Complexity: O(N) class Solution:def isSubsequence(self, s: str, t: str) -> bool:start 0 #used to make sur…

[附源码]java毕业设计医院网上预约系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Docker(9)DockerFile

文章目录什么是DockerFile构建过程执行DockerFile大致流程常用保留字DockerFile小案例创建dockerfile文件dockerfile内部脚本执行命令虚悬镜像什么是DockerFile DockerFile 使用来构建Docker镜像的文本文件&#xff0c;是由一条条构建镜像所需要的指令和参数组成脚本 构建过程…

CSDN Markdown 编辑器的目录与页内跳转功能

CSDN Markdown 编辑器的目录与页内跳转功能 文章目录CSDN Markdown 编辑器的目录与页内跳转功能 目录 页内跳转 正文内容跳转到标题正文内容跳转到正文内容结语目录 CSDN Markdown 编辑器的目录功能使用[TOC](你想要显示的目录总标题)格式&#xff0c;其中默认可省略括号及括…

2022-11-06 网工进阶(三十五)---PIM-SM工作原理(组播分发树的形成、ASM、SSM)

PIM-DM的局限性 中大型组播网络中由于网络较大&#xff0c;如果依然使用PIM-DM会遇到诸多问题&#xff1a; 1 使用“扩散-剪枝”方式需要全网扩散组播报文&#xff0c;对于网络有一定冲击。 2 所有组播路由器均需要维护组播路由表&#xff0c;即使该组播路由器无需转发组播数…

ARM-A架构入门基础(一)预备知识

14天学习训练营导师课程&#xff1a;周贺贺《ARMv8/ARMv9架构-快速入门》 1. 背景 ARM全称&#xff1a; Advanced RISC Machines。 ARM处理器&#xff1a; 基于ARM公司设计的架构而研发的处理器&#xff0c;包含arm core和外设。ARM公司本身不生产处理器&#xff0c;只出售技…

RNA-seq 详细教程:分析流程介绍(1)

学习目标 了解从 RNA 提取到获取基因表达矩阵&#xff0c; 既RNA-seq 分析的整个流程。 1. workflow 进行差异表达基因分析的前提是&#xff0c;获取代表基因表达水平的矩阵。因此在进行分析前&#xff0c;必须知道基因表达矩阵是如何产生的。 在本教程中&#xff0c;将会简要的…

基于JAVA的鲜花店商城平台【数据库设计、源码、开题报告】

数据库脚本下载地址&#xff1a; https://download.csdn.net/download/itrjxxs_com/86427660 摘要 在互联网不断发展的时代之下&#xff0c;鲜花软件可以为鲜花企业带来更多的发展机会&#xff0c;让企业可以挖掘到更多的潜在用户&#xff0c;同时结合企业的优势就能够为用户…

【单目标优化求解】粒子群混沌混合蝴蝶优化算法求解最优目标问题(HPSOBOA)【含Matlab源码 1538期】

⛄一、遗传算法简介 1 算法的种群初始化 设D维搜索空间中&#xff0c;随机生成初始解的表达式为&#xff1a; 式中&#xff0c;Xi表示蝴蝶群体中第i只蝴蝶(i 1, 2, 3, …, N)空间位置&#xff0c;N表示初始解的个数&#xff1b;Lb, Ub分别表示搜索空间的上界和下界&#xff…

Linux vmware 编译模块失败。 Vmware player Unable to install all modules.

Vmware player Unable to install all modules. See log for details ubuntu下的vmware play经常会让更新模块。一起点击确认就可以正常编译更新。 而且一定要编译后才能打开虚拟机系统 但是奇怪的是&#xff0c;最近一次点击 更新&#xff0c;报错&#xff1a; 显示的英文…

STM32F103的FSMC模块驱动LCD屏幕

以下内容为对正点原子的STM32F103STM32F103STM32F103精英板的FSMCFSMCFSMC模块驱动LCDLCDLCD屏幕例程的学习。做一个记录来加强对模块的认知。 FSMCFSMCFSMC的全称是FlexiblestaticmemorycontrollerFlexible\quad static\quad memory\quad controllerFlexiblestaticmemory…

【Python+Appium】开展自动化测试(四)使用weditor进行元素定位

目录 前言 一&#xff0c;weditor的安装与使用 二&#xff0c;遇到的问题 结语 前言 上一篇我们讲到了如何通过编写一个简单的Python脚本完成了登录app的操作&#xff0c;如何断言&#xff0c;如何编写一条完整的用例。那么今天继续我们的自动化测试第四天&#xff0c;使用…

【Java进阶篇】第五章 集合(下)--Map集合

文章目录1、概述2、Map接口中的常用方法3、Map集合的遍历4、哈希表的数据结构5、Map集合的存取6、hashCode(&#xff09;和equals()的重写7、一些小零散的东西8、HashMap和Hashtable的区别9、Properties类10、TreeMap11、自定义类实现Comparable接口12、二叉树13、Comparator接…

Caffeine《一》

《Caffeine&#xff08;Java顶级缓存组件&#xff09;一》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《Caffeine&#xff08;Java顶级缓存组件&#xff09;》《Caffeine&#xff08;Java顶级缓存组件&#xff09;一》1. Caffeine缓存概念1.1 缓存的分…

【MyBatis】一、概述

框架 框架就是对通用代码的封装&#xff0c;提前写好了一堆接口和类&#xff0c;我们可以在做项目的时候直接引入这些接口和类&#xff0c;基于这些现有的接口和类进行开发&#xff0c;提高开发效率。 框架一般以jar包形式存在 三层架构 表现层&#xff08;UI&#xff09;&am…

数字化助力生产管理:计件工资管理系统

目前因市场经济的发展&#xff0c;企业为求发展同时为防止消极怠工的情况&#xff0c;常把员工工资与产量结合起来。为了增加工人的劳动积极性&#xff0c;秉持多劳多得的科学管理理念&#xff0c;许多生产类型企业均实现了计件工资制。所谓的计件工资制是指按照生产的合格品的…

python之opencv人脸识别快速体验

目录 1、灰度转换 2、引用opencv官方提供的人脸训练模型 3、绘制方框&#xff08;用于框住人脸&#xff09; 4、效果及完整代码&#xff1a; 5、检测多张人脸&#xff1a; 学习链接&#xff1a; 1、灰度转换 # 灰度转换gary cv.cvtColor(img,cv.COLOR_BGR2GRAY) 2、引…

图解:Elasticsearch 8.X 如何求解环比上升比例?

1、企业级Elasticsearch 8.X 实战问题 问题描述&#xff1a;有个聚合的需求&#xff0c;问下大家&#xff0c;一个索引中有时间字段 要求 计算本月和上月相比的环比上升比例&#xff1f;——来自GPVIP群 2、问题释义 2.1 啥叫环比&#xff1f; 环比是统计学术语&#xff0c…

LabVIEW使用Deskto pExecution Trace工具包

LabVIEW使用Deskto pExecution Trace工具包 可以使用桌面执行跟踪工具包来调试和优化大型LabVIEW应用程序&#xff0c;包括具有多个循环的应用程序、客户端-服务器架构、动态加载VI等。该工具包从本地或远程计算机桌面上运行的应用程序捕获执行事件&#xff0c;并在表窗格中显…

PCB layout有DRC为什么还要用CAM和DFM检查?

随着电子产品的高速发展&#xff0c;PCB生产中大量使用BGA、QFP、PGA和CSP等高集成度封装器件&#xff0c;PCB的复杂程度也大大增加&#xff0c;这对于PCB设计也提出了更高的要求。所以在PCB设计阶段&#xff0c;除了基础的电气性能之外&#xff0c;还需要考虑可制造性&#xf…