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<String, dynamic>? 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<ShowVideoCallState> 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<ShowVideoCall> {
  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<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 _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<bool?> _showAcceptDialog() {
    LogUtils.d("显示对话框。。${_inCalling}");
    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);
              },
            ),
          ],
        );
      },
    );
  }

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

}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

六毛六66

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

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

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

打赏作者

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

抵扣说明:

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

余额充值