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

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

六毛六66

你的鼓励是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值