• Flutter高仿微信-第36篇-单聊-语音通话


    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? 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 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 {
      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 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 _showAcceptDialog() {
        LogUtils.d("显示对话框。。${_inCalling}");
        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);
                  },
                ),
              ],
            );
          },
        );
      }
    
      Future _showInvateDialog() {
        return showDialog(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: Text("视频通话"),
              content: Text("邀请好友视频通话,请等待对方接受。"),
              actions: [
                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: [
                    getSwitchCameraWidget(),
                    getHangUpWidget(),
                    getMicWidget(),
                    getSpeakerWidget(),
                  ]))
              : null,
          body: _inCalling?
          OrientationBuilder(builder: (context, orientation) {
            return Container(
              color: Colors.white,
              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, 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),),
                  ],
                ),
              ));
      }
    
    }

  • 相关阅读:
    【FreeRTOS】基于STM32F407的Freertos实时操作系统移植
    完整boot引导代码详解(完整无注释代码boot.asm+简单loader.asm)
    生成二维坐标数组numpy.meshgrid()
    【C++上层应用】3. 动态内存
    程序员都看不懂的代码
    jmeter固定定时器,生效是在请求发送前还是发送后
    Linux V4L2编程和驱动底层分析
    Java池化技术
    快速了解 Kubernetes 的架构及特性
    音视频流媒体开发难以学习?今天教你如何“丝滑”入门
  • 原文地址:https://blog.csdn.net/maoning20080808/article/details/128016201