美文网首页Flutter工作生活Flutter学习
flutter学习笔记:利用声网的flutter插件实现视频通话

flutter学习笔记:利用声网的flutter插件实现视频通话

作者: 鹿乐吾 | 来源:发表于2019-07-05 12:02 被阅读327次

    效果展示

    实现细节包括:视频的呼叫和接听,来电时的响铃,画面切换等。先看效果图,相关视频界面截图如下:

    有人来电时的界面 呼叫其他人时的界面 视频接通时的界面 点击小窗口切换画面时的界面

    准备工作

    ● 创建声网账户,并获取App ID,☞官网

    ● 用到的声网Flutter插件有两个:
    1> agora_rtc_engine(https://github.com/AgoraIO/Flutter-RTM)
    主要实现视频通话部分的插件,点击官方demo查看代码示例。

    2>agora_rtm(https://github.com/AgoraIO/Flutter-RTM)
    主要实现呼叫的信令系统插件,点击官方demo可查看代码示例。
    功能细节:登录、收发消息

    代码部分

    配置

    • 在项目根目录下的 pubspec.yaml 文件中添加插件:
    dependencies:
      flutter:
        sdk: flutter
    
      # The following adds the Cupertino Icons font to your application.
      # Use with the CupertinoIcons class for iOS style icons.
      cupertino_icons: ^0.1.2
      agora_rtc_engine: 0.9.4
      agora_rtm: 0.9.3
    
    • android-app-build.gradle 中添加
    android {
        ..
        defaultConfig {
            ..
             ndk {
                 abiFilters 'armeabi-v7a'
            }
            ..
        }
        ..
    }
    
    • androidAndroidManifest.xml 文件中添加权限:
    ..
    <uses-permission android:name="android.permission.READ_PHONE_STATE” />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- The Agora SDK requires Bluetooth permissions in case users are using Bluetooth devices.-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    ..
    
    • iosinfo.plist 文件中添加
    
        Privacy - Microphone Usage Description, and add a note in the Value column.
        Privacy - Camera Usage Description, and add a note in the Value column.
    
    

    视频相关

    1. 初始化
    AgoraRtmClient _client;
      /// 收到通话请求时的响铃页面
      VideoAnswerPage answer;
      /// 声网RTM初始化、注册接收
      Future initAgoraRtm() async {
        // 初始化
        _client =await AgoraUtils.getAgoraRtmClient();
        // 设置消息接收器
        _client.onMessageReceived = (AgoraRtmMessage message, String peerId) {
          if(!EmptyUtil.textIsEmpty(message.text)){
            // 收到视频请求,消息内容定义为: “CALLVIDEO,视频通道id”(请求者id接收者id)
            if(message.text.contains(AgoraUtils.getAgoraMsgType(1)) && message.text.contains(",")){
              try{
                String _channelName =message.text.split(",")[1];
                // 收到通话请求时的响铃页面
                answer = new VideoAnswerPage(_channelName,peerId,_client);
                // 跳到响铃页面
                Navigator.push(c, new MaterialPageRoute(
                    builder: (BuildContext context) {
                      return answer;
                    }));
              }catch(e){
                print(e.toString());
              }
              // 收到消息:视频请求者取消了通话
            }else if(message.text.contains(AgoraUtils.getAgoraMsgType(2)) ){
              if(answer != null){
                answer.videoAnswerState.isClosedByOne = true;
                answer.videoAnswerState.onCallEnd(c);
              }
              // 对方拒绝了通话请求
            }else if(message.text.contains(AgoraUtils.getAgoraMsgType(3)) ){
              if(AgoraUtils.videoCallState != null){
                AgoraUtils.videoCallState.isClosedByOne = true;
                AgoraUtils.videoCallState.onCallEnd(c);
              }
            }
          }
        };
        _client.onConnectionStateChanged = (int state, int reason) {
          // _log('Connection state changed: ' +  state.toString() +  ', reason: ' + reason.toString());
          if (state == 5) {
            _client.logout();
          }
        };
      }
    
    1. 登录
      自定义user id 提交登录
    /// 声网登录
      void _toggleLogin() async {
        if (!_isLogin) {
          // 获取输入框的user id(英文 || 数字)
          String userId = _userNameController.text;
          if (userId.isEmpty) {
            Fluttertoast.showToast(msg: "Please input your user id to login");
            return;
          }
          if(_client == null){
            return;
          }
          try {
            await _client.login(null, userId);
            setState(() {
              _isLogin = true;
            });
            //
            User user = new User();
            user.agoraId = userId;
            // 保存用户信息
            ConstantObject.mUser = user;
          } catch (errorCode) {
            print(errorCode);
          }
        }
      }
    
    1. 请求视频通话
    class AgoraCustomPage extends StatefulWidget {
      @override
      createState() => new AgoraCustomState();
    }
    
    class AgoraCustomState extends State<AgoraCustomPage> {
      TextEditingController _friendController = new TextEditingController();
      TextEditingController _groupController = new TextEditingController();
      String _channelName = "zhijie";
      AgoraRtmClient _client;
    
      @override
      void initState() {
        super.initState();
        initAgoraRtm();
      }
    
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
          appBar: new AppBar(
            title: new Text("声网"),
          ),
          body: buildStartPage(),
        );
      }
      Widget buildStartPage(){
        return SingleChildScrollView(
          child: ConstrainedBox(// 添加额外为限制条件到child,如最小/大宽度、高度。。。
            constraints: BoxConstraints(
              minHeight: 120.0,
            ),
            child: Column(children: <Widget>[
              Container(
                  width: double.infinity,
                  margin:  const EdgeInsets.fromLTRB(20,30,20,0),
                  child: TextField(
                    controller: _friendController,
                    autofocus: false,
                    decoration: InputDecoration(
                      icon: Icon(Icons.person),
                      labelText: '请输入好友id',
                      helperText: '请正确输入好友的id',
                    ),
                  )
              ),
              Container(
                  width: double.infinity,
                  height: 50,
                  margin:  const EdgeInsets.fromLTRB(20,30,20,0),
                  child: RaisedButton(
                    onPressed: (){
                      clickFriendVideo();
                    },
                    // 文本内容
                    child: Text("和好友视频通话"),
                    // 按钮颜色
                    color: ThemeColors.colorTheme,
                  ))
            ]),),
        );
      }
      void clickFriendVideo(){
        if(EmptyUtil.textIsEmpty(_friendController.text)){
          Fluttertoast.showToast(msg: "Friend id cannot be empty");
        }else{
          _callVideo(_friendController.text);
        }
      }
      // 发送视频通话请求
      Future _callVideo(String peerId) async {
        if(_client != null){
          // 查看对方是否在线
          bool online =await AgoraUtils.queryPeerOnlineStatus(_client, peerId);
          if(online){
            try{
              // 自定义视频通道(这里采取 自己的agoraid+对方的id),
              _channelName = ConstantObject.getUser().agoraId+peerId;
              // 发送消息 : “CALLVIDEO,_channelName”
              String msg = AgoraUtils.getAgoraMsgType(1)+","+_channelName;
              await _client.sendMessageToPeer(peerId, AgoraRtmMessage(msg));
              // 跳到拨打视频的等待接听页面
              Navigator.push(context, new MaterialPageRoute(
                  builder: (BuildContext context) {
                    return new VideoCallPage(_channelName,peerId);
                  }));
            }catch(e){
              print(e.toString());
            }
          }else{
            Fluttertoast.showToast(msg: "The friend is offline");
          }
        }
      }
      /// 获取AgoraRtmClient
      Future initAgoraRtm() async {
        _client =await AgoraUtils.getAgoraRtmClient();
      }
    }
    
    1. 拨打视频的等待接听页面(及通话页面)
    class VideoCallPage extends StatefulWidget {
      /// 视频通道
      final String channelName;
      /// 好友的 agora Id
      final String firendName;
    
      VideoCallPage(this.channelName, this.firendName);
    
      /*/// Creates a call page with given channel name.
      const VideoCallPage({Key key, this.channelName, this.firendName}) : super(key: key);*/
    
      @override
      createState() => new VideoCallState();
    }
    
    class VideoCallState extends State<VideoCallPage> {
      /// 和android本地交互的通道
      static const _methodChannel1 = const MethodChannel(MethodChannelUtils.channelMedia);
      static final _sessions = List<VideoSession>();
      final _infoStrings = <String>[];
      BuildContext mcontext;
      AgoraRtmClient _client;
      /// 声网上获取的App ID
      var APP_ID = APPApiKey.Agora_app_id;
      bool muted = false;
      /// 视频是否成功接通
      bool videoSuccess = false;
      /// 发出视频请求但未接通时,自己取消通话
      bool isClosedByOne = false;
      /// 主窗口展示自己?
      /// true 展示自己   false 展示好友
      bool mainWindowShowOneself = true;
      /// 计时的数值
      int _count = 0;
      Timer _timer;
    
      @override
      void dispose() {
        // clean up native views & destroy sdk
        _sessions.forEach((session) {
          AgoraRtcEngine.removeNativeView(session.viewId);
        });
        _sessions.clear();
        AgoraRtcEngine.leaveChannel();
    
        // 停止播放响铃
        stopPlay();
        if(!isClosedByOne && !videoSuccess){
          /// 请求视频对方还未接听时,自己先取消,则需要通知对方,我已取消
          _initSendMessage();
        }
        stopTimer();
        AgoraUtils.clearVideoCallState();
        super.dispose();
      }
    
      @override
      void initState() {
        super.initState();
        // initialize third.agora sdk
        initialize();// 初始化视频SDK
        startPlay();// 开始播放响铃
      }
    
      void initialize() {
        if (APP_ID.isEmpty) {
          setState(() {
            _infoStrings
                .add("APP_ID missing, please provide your APP_ID in settings.dart");
            _infoStrings.add("Agora Engine is not starting");
          });
          return;
        }
    
        _initAgoraRTM();// 信令系统
        _initAgoraRtcEngine();// 视频通话
        _addAgoraEventHandlers();
        // use _addRenderView everytime a native video view is needed
        _addRenderView(0, (viewId) {
          AgoraRtcEngine.setupLocalVideo(viewId, VideoRenderMode.Hidden);
          AgoraRtcEngine.startPreview();
          // state can access widget directly
         // 加入视频通话(或者可以考虑在对方接听后发个消息通知请求方,请求方再加入视频,可以节省点视频分钟数)
          AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);
        });
      }
      /// 获取 AgoraRtmClient
      Future<void> _initAgoraRTM() async{
        _client =await AgoraUtils.getAgoraRtmClient();
        AgoraUtils.videoCallState = this;
      }
      /// 发送消息通知对方取消通话
      Future<void> _initSendMessage() async{
        String msg = AgoraUtils.getAgoraMsgType(2);
        await _client.sendMessageToPeer(widget.firendName, AgoraRtmMessage(msg));
      }
      /// Create third.agora sdk instance and initialze
      Future<void> _initAgoraRtcEngine() async {
        AgoraRtcEngine.create(APP_ID);
        AgoraRtcEngine.enableVideo();
      }
    
      /// Add third.agora event handlers
      void _addAgoraEventHandlers() {
        AgoraRtcEngine.onError = (int code) {
          setState(() {
            String info = 'onError: ' + code.toString();
            _infoStrings.add(info);
          });
        };
        /// 成功加入某次视频的回调
        AgoraRtcEngine.onJoinChannelSuccess =
            (String channel, int uid, int elapsed) {
          setState(() {
            String info = 'onJoinChannel: ' + channel + ', uid: ' + uid.toString();
            _infoStrings.add(info);
          });
        };
    
        AgoraRtcEngine.onLeaveChannel = () {
          setState(() {
            _infoStrings.add('onLeaveChannel');
          });
        };
        /// 有其他用户(好友)成功加入到视频中的回调
        AgoraRtcEngine.onUserJoined = (int uid, int elapsed) {
          setState(() {
            String info = 'userJoined: ' + uid.toString();
            //setState(() { videoSuccess = true; });
            _infoStrings.add(info);
            videoSuccess = true;// 成功开始视频通话
            stopPlay(); // 停止播放响铃
            startTimer(); // 开始通话计时
            _addRenderView(uid, (viewId) {
              AgoraRtcEngine.setupRemoteVideo(viewId, VideoRenderMode.Hidden, uid);
            });
          });
        };
        /// 好友退出通话
        AgoraRtcEngine.onUserOffline = (int uid, int reason) {
          setState(() {
            String info = 'userOffline: ' + uid.toString();
            _infoStrings.add(info);
            onCallEnd(mcontext);// 自己也退出
            _removeRenderView(uid);
          });
        };
    
        AgoraRtcEngine.onFirstRemoteVideoFrame =
            (int uid, int width, int height, int elapsed) {
          setState(() {
            String info = 'firstRemoteVideo: ' +
                uid.toString() +
                ' ' +
                width.toString() +
                'x' +
                height.toString();
            _infoStrings.add(info);
          });
        };
      }
    
      /// Create a native view and add a new video session object
      /// The native viewId can be used to set up local/remote view
      void _addRenderView(int uid, Function(int viewId) finished) {
        Widget view = AgoraRtcEngine.createNativeView(uid, (viewId) {
          setState(() {
            _getVideoSession(uid).viewId = viewId;
            if (finished != null) {
              finished(viewId);
            }
          });
        });
        VideoSession session = VideoSession(uid, view);
        _sessions.add(session);
      }
    
      /// Remove a native view and remove an existing video session object
      void _removeRenderView(int uid) {
        VideoSession session = _getVideoSession(uid);
        if (session != null) {
          _sessions.remove(session);
        }
        AgoraRtcEngine.removeNativeView(session.viewId);
      }
    
      /// Helper function to filter video session with uid
      VideoSession _getVideoSession(int uid) {
        return _sessions.firstWhere((session) {
          return session.uid == uid;
        });
      }
    
      /// Helper function to get list of native views
      List<Widget> _getRenderViews() {
        return _sessions.map((session) => session.view).toList();
      }
    
      /// Video view wrapper
      /// Expanded组件必须用在Row、Column、Flex内,并且从Expanded到封装它的Row、Column、Flex的路径必须只包括StatelessWidgets或StatefulWidgets组件(不能是其他类型的组件,像RenderObjectWidget,它是渲染对象,不再改变尺寸了,因此Expanded不能放进RenderObjectWidget)。
      Widget _videoView(view) {
        return Expanded(child: Container(child: view));
      }
    
      /// Video layout wrapper
      Widget _viewRows() {
        //List<Widget> views = _getRenderViews();
        List<Widget> views = new List();
        views.addAll(_getRenderViews());
        return _mainWindow(views);
      }
    
      /// 主窗口视图
      Widget _mainWindow(List<Widget> views){
        return GestureDetector(
          child: Container(
              child: Column(
                children: <Widget>[
                  mainWindowShowOneself ?
                  _videoView(views[0]) : _videoView(views[1])
                ],
              )),
        );
      }
      /// 右上角小窗口视图
      Widget _smallWindow() {
        //List<Widget> views = _getRenderViews();
        List<Widget> views = new List();
        if(!videoSuccess ){
          return _emptyView();
        }else {
          views.addAll(_getRenderViews());
          if( mainWindowShowOneself ){
            if(!EmptyUtil.listIsEmpty(views) && views.length > 1){
              return  _smallVideoView(views[1]);
            }else{
              return _emptyView();
            }
          }else {
            if(!EmptyUtil.listIsEmpty(views)){
              return _smallVideoView(views[0]);
            }else{
              return _emptyView();
            }
          }
        }
      }
      /// 右上角小窗口视图
      Widget _smallVideoView(Widget view){
        return GestureDetector(
          onTap: updateDoubleWindow,
          onDoubleTap: updateDoubleWindow,
          child: Align(
            alignment: Alignment.topRight,
            child: Container(
                width: 80.0,
                height: 130.0,
                margin: EdgeInsets.all(20),
                color: ThemeColors.colorWhite,
                child:   Stack(children: <Widget>[
                  Column(
                    children: <Widget>[
                      _videoView(view)
                    ],
                  ),
                  Container(
                    width: double.infinity,
                    height: double.infinity,
                    color: ThemeColors.transparent,
                    child: Text("  "),
                  )
                ],)
            ),
          ),
        );
      }
      /// 未接通视频前的一层透明遮罩
      Widget _mask(){
        return Container(
          child:Offstage(
            offstage: videoSuccess,
            child: Container(
              width: double.infinity,
              height: double.infinity,
              color: ThemeColors.transparent1,
            ),
          ) ,
        );
      }
      /// 响铃时的dialog
      Widget _ProgressDialog() {
        return Offstage(
          offstage: videoSuccess,
          child:Container(
            height: 25.0,
            color: ThemeColors.transparent,
            margin: EdgeInsets.only(top:110),
            alignment: Alignment.topCenter,
            child: SpinKitWave(color: ThemeColors.colorTheme),
          ) ,
        );
      }
      /// 视频界面底部的工具栏(静音、挂断、摄像头切换)
      Widget _toolbar() {
        return Container(
          alignment: Alignment.bottomCenter,
          padding: EdgeInsets.symmetric(vertical: 48),
          child:Container(
            height: 100.0,
            child: Column(children: <Widget>[
              Offstage(
                offstage: !videoSuccess,//true -显示
                child: Container(
                  height: 20.0,
                  margin: EdgeInsets.only(bottom:10.0),
                  child: Text(DateTimeUtil.getHMmmss_Seconds(_count),
                      style: TextStyle(
                        color: ThemeColors.colorWhite,
                        fontSize: 16,
                      )),
                ),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  RawMaterialButton(
                    onPressed: () => _onToggleMute(),
                    child: new Icon(
                      muted ? Icons.mic : Icons.mic_off,
                      color: muted ? Colors.white : ThemeColors.colorTheme,
                      size: 20.0,
                    ),
                    shape: new CircleBorder(),
                    elevation: 2.0,
                    fillColor: muted ? ThemeColors.colorTheme : Colors.white,
                    padding: const EdgeInsets.all(12.0),
                  ),
                  RawMaterialButton(
                    onPressed: () => onCallEnd(context),
                    child: new Icon(
                      Icons.call_end,
                      color: Colors.white,
                      size: 35.0,
                    ),
                    shape: new CircleBorder(),
                    elevation: 2.0,
                    fillColor: Colors.redAccent,
                    padding: const EdgeInsets.all(15.0),
                  ),
                  RawMaterialButton(
                    onPressed: () => _onSwitchCamera(),
                    child: new Icon(
                      Icons.switch_camera,
                      color: ThemeColors.colorTheme,
                      size: 20.0,
                    ),
                    shape: new CircleBorder(),
                    elevation: 2.0,
                    fillColor: Colors.white,
                    padding: const EdgeInsets.all(12.0),
                  )
                ],
              ),
            ],),
          ) ,
        );
      }
      
      /// 好友的信息视图(名称)
      Widget _friendInfo() {
        return Container(
            height: 50.0,
            alignment: Alignment.topCenter,
            margin: EdgeInsets.only(top:60),
            child: Offstage(
              offstage: videoSuccess,
              child:   Text(
                widget.firendName,
                style: TextStyle(
                  color: ThemeColors.colorWhite,
                  fontSize: 26,
                ),
              ),
            )
        );
      }
      /// 退出通话
      void onCallEnd(BuildContext context) {
        Navigator.pop(context);
      }
    
      void _onToggleMute() {
        setState(() {
          muted = !muted;
        });
        AgoraRtcEngine.muteLocalAudioStream(muted);
      }
      /// 切换摄像头
      void _onSwitchCamera() {
        AgoraRtcEngine.switchCamera();
      }
      /// 开始播放自定义的响铃文件
      void startPlay(){
        _methodChannel1.invokeListMethod(MethodChannelUtils.methodStartMedia);
      }
      /// 停止播放响铃
      void stopPlay(){
        _methodChannel1.invokeListMethod(MethodChannelUtils.methodStopMedia);
      }
      /// 更换主窗口和小窗口的画面
      void updateDoubleWindow(){
        setState(() {
          mainWindowShowOneself = !mainWindowShowOneself;
        });
      }
      /// 开始计时
      void startTimer() {
        const oneSec = const Duration(seconds: 1);
        var callback = (timer) => {
        setState(() {
          _count++;// 秒数+1
        })
      };
        _timer = Timer.periodic(oneSec, callback);
      }
      /// 停止计时
      void stopTimer(){
        if(_timer != null){
          _timer.cancel();
        }
      }
      @override
      Widget build(BuildContext context) {
        mcontext = context;
        return Scaffold(
            appBar: AppBar(
              title: Text('Agora Flutter'),
            ),
            backgroundColor: Colors.black,
            body: Center(
                child: Stack(
                  children: <Widget>[_viewRows(),_smallWindow(),_mask(),_ProgressDialog(), _toolbar(),_friendInfo()],//_panel(),
                )));
      }
      Widget _emptyView(){
        return Container(
          width: 1.0,
          height: 1.0,
        );
      }
    }
    
    1. 等待接听视频的页面(及通话页面)
    class VideoAnswerPage extends StatefulWidget {
      /// non-modifiable channel name of the page
      final String channelName;
      final String firendName;
      VideoAnswerState videoAnswerState;
      AgoraRtmClient _client;
      /// 接听视频邀请
      /// 参数(视频通道id,好友名称)
      VideoAnswerPage(this.channelName, this.firendName,this._client);
    
      @override
      VideoAnswerState createState() {
        videoAnswerState = new VideoAnswerState();
        return videoAnswerState;
      }
    }
    
    class VideoAnswerState extends State<VideoAnswerPage> {
      static const _methodChannel1 = const MethodChannel(MethodChannelUtils.channelMedia);
      static final _sessions = List<VideoSession>();
      final _infoStrings = <String>[];
      BuildContext mcontext;
      var APP_ID = APPApiKey.Agora_app_id;
      bool muted = false;
      /// 视频是否成功接通
      bool videoSuccess = false;
      /// 拒绝通话
      bool isClosedByOne = false;
      /// 主窗口展示自己?
      /// true 自己   false 好友
      bool mainWindowShowOneself = true;
      int _count = 0;
      Timer _timer;
    
    
      @override
      void dispose() {
        // clean up native views & destroy sdk
        _sessions.forEach((session) {
          AgoraRtcEngine.removeNativeView(session.viewId);
        });
        _sessions.clear();
        AgoraRtcEngine.leaveChannel();
        stopPlay();
        if(!isClosedByOne && !videoSuccess){
          /// 视频没有接通前自己挂断,则需要通知对方,我已拒绝
          _initSendMessage();
        }
        stopTimer();
        super.dispose();
      }
    
      @override
      void initState() {
        super.initState();
        // initialize third.agora sdk
        initialize();
        startPlay();
      }
      void startPlay(){
        _methodChannel1.invokeListMethod(MethodChannelUtils.methodStartMedia);
      }
      void stopPlay(){
        _methodChannel1.invokeListMethod(MethodChannelUtils.methodStopMedia);
      }
      void initialize() {
        if (APP_ID.isEmpty) {
          setState(() {
            _infoStrings
                .add("APP_ID missing, please provide your APP_ID in settings.dart");
            _infoStrings.add("Agora Engine is not starting");
          });
          return;
        }
    
        _initAgoraRtcEngine();
        _addAgoraEventHandlers();
        // use _addRenderView everytime a native video view is needed
        _addRenderView(0, (viewId) {
          AgoraRtcEngine.setupLocalVideo(viewId, VideoRenderMode.Hidden);
          AgoraRtcEngine.startPreview();
          // state can access widget directly
          // AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);// 修改为点击接听按钮后再接通
        });
      }
      Future<void> _initSendMessage() async{
        /*-------收到消息--------*/
        try {
          String msg = AgoraUtils.getAgoraMsgType(3);
          await widget._client.sendMessageToPeer(widget.firendName, AgoraRtmMessage(msg));
        } catch (e) {
          print(e);
        }
      }
      /// Create third.agora sdk instance and initialze
      Future<void> _initAgoraRtcEngine() async {
        AgoraRtcEngine.create(APP_ID);
        AgoraRtcEngine.enableVideo();
      }
    
      /// Add third.agora event handlers
      void _addAgoraEventHandlers() {
        AgoraRtcEngine.onError = (int code) {
          setState(() {
            String info = 'onError: ' + code.toString();
            _infoStrings.add(info);
          });
        };
        /// 成功加入某次视频的回调
        AgoraRtcEngine.onJoinChannelSuccess =
            (String channel, int uid, int elapsed) {
          setState(() {
            String info = 'onJoinChannel: ' + channel + ', uid: ' + uid.toString();
            _infoStrings.add(info);
    
          });
        };
    
        AgoraRtcEngine.onLeaveChannel = () {
          setState(() {
            _infoStrings.add('onLeaveChannel');
          });
        };
        /// 有其他用户加入到视频中的回调
        AgoraRtcEngine.onUserJoined = (int uid, int elapsed) {
          setState(() {
            String info = 'userJoined: ' + uid.toString();
            //setState(() { videoSuccess = true; });
            _infoStrings.add(info);
            videoSuccess = true;
            startTimer();
            _addRenderView(uid, (viewId) {
              AgoraRtcEngine.setupRemoteVideo(viewId, VideoRenderMode.Hidden, uid);
            });
          });
        };
    
        AgoraRtcEngine.onUserOffline = (int uid, int reason) {
          setState(() {
            String info = 'userOffline: ' + uid.toString();
            _infoStrings.add(info);
            onCallEnd(mcontext);
            _removeRenderView(uid);
          });
        };
    
        AgoraRtcEngine.onFirstRemoteVideoFrame =
            (int uid, int width, int height, int elapsed) {
          setState(() {
            String info = 'firstRemoteVideo: ' +
                uid.toString() +
                ' ' +
                width.toString() +
                'x' +
                height.toString();
            _infoStrings.add(info);
          });
        };
      }
    
      /// Create a native view and add a new video session object
      /// The native viewId can be used to set up local/remote view
      void _addRenderView(int uid, Function(int viewId) finished) {
        Widget view = AgoraRtcEngine.createNativeView(uid, (viewId) {
          setState(() {
            _getVideoSession(uid).viewId = viewId;
            if (finished != null) {
              finished(viewId);
            }
          });
        });
        VideoSession session = VideoSession(uid, view);
        _sessions.add(session);
      }
    
      /// Remove a native view and remove an existing video session object
      void _removeRenderView(int uid) {
        VideoSession session = _getVideoSession(uid);
        if (session != null) {
          _sessions.remove(session);
        }
        AgoraRtcEngine.removeNativeView(session.viewId);
      }
    
      /// Helper function to filter video session with uid
      VideoSession _getVideoSession(int uid) {
        return _sessions.firstWhere((session) {
          return session.uid == uid;
        });
      }
    
      /// Helper function to get list of native views
      List<Widget> _getRenderViews() {
        return _sessions.map((session) => session.view).toList();
      }
    
      /// Video view wrapper
      /// Expanded组件必须用在Row、Column、Flex内,并且从Expanded到封装它的Row、Column、Flex的路径必须只包括StatelessWidgets或StatefulWidgets组件(不能是其他类型的组件,像RenderObjectWidget,它是渲染对象,不再改变尺寸了,因此Expanded不能放进RenderObjectWidget)。
      Widget _videoView(view) {
        return Expanded(child: Container(child: view));
      }
    
      /// Video layout wrapper
      Widget _viewRows() {
        //List<Widget> views = _getRenderViews();
        List<Widget> views = new List();
        views.addAll(_getRenderViews());
        return _mainWindow(views);
      }
      /// 主窗口视图
      Widget _mainWindow(List<Widget> views){
        return GestureDetector(
          child: Container(
              child: Column(
                children: <Widget>[
                  mainWindowShowOneself ?
                  _videoView(views[0]) : _videoView(views[1])
                ],
              )),
        );
      }
      /// 右上角小窗口视图
      Widget _smallWindow() {
        //List<Widget> views = _getRenderViews();
        List<Widget> views = new List();
        if(!videoSuccess ){
          return _emptyView();
        }else {
          views.addAll(_getRenderViews());
          if( mainWindowShowOneself ){
            if(!EmptyUtil.listIsEmpty(views) && views.length > 1){
              return  _smallVideoView(views[1]);
            }else{
              return _emptyView();
            }
          }else {
            if(!EmptyUtil.listIsEmpty(views)){
              return _smallVideoView(views[0]);
            }else{
              return _emptyView();
            }
          }
        }
      }
      Widget _smallVideoView(Widget view){
        return GestureDetector(
          onTap: updateDoubleWindow,
          onDoubleTap: updateDoubleWindow,
          child: Align(
            alignment: Alignment.topRight,
            child: Container(
                width: 80.0,
                height: 130.0,
                margin: EdgeInsets.all(20),
                color: ThemeColors.colorWhite,
                child:   Stack(children: <Widget>[
                  Column(
                    children: <Widget>[
                      _videoView(view)
                    ],
                  ),
                  Container(
                    width: double.infinity,
                    height: double.infinity,
                    color: ThemeColors.transparent,
                    child: Text(" 视图 ",
                      style: TextStyle(
                          color: ThemeColors.transparent,
                          fontSize: 20.0,
                      ),
                    ),
                  )
                ],)
            ),
          ),
        );
      }
      /// 未接通视频前的一层遮罩
      Widget _mask(){
        return Container(
          child:Offstage(
            offstage: videoSuccess,
            child: Container(
              width: double.infinity,
              height: double.infinity,
              color: ThemeColors.transparent1,
            ),
          ) ,
        );
      }
    
      /// Toolbar layout
      Widget _toolbar() {
        if(videoSuccess){
          return _answerSuccessToolbar();
        }else{
          return _waitAnswerToolbar();
        }
      }
      /// 通话时的工具栏
      Widget _answerSuccessToolbar(){
        return Container(
          alignment: Alignment.bottomCenter,
          padding: EdgeInsets.symmetric(vertical: 48),
    
          child:Container(
            height: 100.0,
            child: Column(children: <Widget>[
              Container(
                height: 20.0,
                margin: EdgeInsets.only(bottom:10.0),
                child: Text(DateTimeUtil.getHMmmss_Seconds(_count),
                    style: TextStyle(
                      color: ThemeColors.colorWhite,
                      fontSize: 16,
                    )),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  RawMaterialButton(
                    onPressed: () => _onToggleMute(),
                    child: new Icon(
                      muted ? Icons.mic : Icons.mic_off,
                      color: muted ? Colors.white : ThemeColors.colorTheme,
                      size: 20.0,
                    ),
                    shape: new CircleBorder(),
                    elevation: 2.0,
                    fillColor: muted ? ThemeColors.colorTheme : Colors.white,
                    padding: const EdgeInsets.all(12.0),
                  ),
                  RawMaterialButton(
                    onPressed: () => onCallEnd(context),
                    child: new Icon(
                      Icons.call_end,
                      color: Colors.white,
                      size: 35.0,
                    ),
                    shape: new CircleBorder(),
                    elevation: 2.0,
                    fillColor: Colors.redAccent,
                    padding: const EdgeInsets.all(15.0),
                  ),
                  RawMaterialButton(
                    onPressed: () => _onSwitchCamera(),
                    child: new Icon(
                      Icons.switch_camera,
                      color: ThemeColors.colorTheme,
                      size: 20.0,
                    ),
                    shape: new CircleBorder(),
                    elevation: 2.0,
                    fillColor: Colors.white,
                    padding: const EdgeInsets.all(12.0),
                  )
                ],
              ),
            ],),
          ) ,
        );
      }
      /// 响铃时的工具栏
      Widget _waitAnswerToolbar(){
        return  Container(
          alignment: Alignment.bottomCenter,
          padding: EdgeInsets.symmetric(vertical: 48),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: <Widget>[
              RawMaterialButton(
                onPressed: () => _onCancelAnswer(context),
                child: new Icon(
                  Icons.call_end,
                  color: Colors.white,
                  size: 35.0,
                ),
                shape: new CircleBorder(),
                elevation: 2.0,
                fillColor: Colors.redAccent,
                padding: const EdgeInsets.all(15.0),
              ),
              RawMaterialButton(
                onPressed: () => _onAnswerVideo(),
                child: new Icon(
                  Icons.call_received,
                  color: Colors.white,
                  size: 35.0,
                ),
                shape: new CircleBorder(),
                elevation: 2.0,
                fillColor: Colors.green,
                padding: const EdgeInsets.all(12.0),
              )
            ],
          ),
        );
      }
      /// 好友的信息视图
      Widget _friendInfo() {
        return Container(
            alignment: Alignment.topLeft,
            margin: EdgeInsets.all(15),
            child: Offstage(
                offstage: videoSuccess,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      widget.firendName,
                      textAlign: TextAlign.left,
                      style: TextStyle(
                        color: ThemeColors.colorWhite,
                        fontSize: 25,
                      ),
                    ),
                    Text(
                      "邀请你进行视频通话",
                      textAlign: TextAlign.start,
                      style: TextStyle(
                        color: ThemeColors.colorWhite,
                        fontSize: 12,
                        height: 1.5,
                      ),
                    ),
                  ],)
            )
        );
      }
     
      void _onToggleMute() {
        setState(() {
          muted = !muted;
        });
        AgoraRtcEngine.muteLocalAudioStream(muted);
      }
      /// 切换摄像头
      void _onSwitchCamera() {
        AgoraRtcEngine.switchCamera();
      }
      /// 当点击接受应答
      void _onAnswerVideo() {
        try {
          stopPlay();
          AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);
        } catch (e) {
          print(e);
        }
      }
      /// 当点击取消应答
      void _onCancelAnswer(BuildContext context) {
        Navigator.pop(context);
      }
      /// 退出视频页面,停止视频
      void onCallEnd(BuildContext context) {
        Navigator.pop(context);
      }
      void updateDoubleWindow(){
        setState(() {
          mainWindowShowOneself = !mainWindowShowOneself;
        });
      }
      /// 开始计时
      void startTimer() {
        const oneSec = const Duration(seconds: 1);
        var callback = (timer) => {
        setState(() {
          _count++;// 秒数+1
        })
      };
        _timer = Timer.periodic(oneSec, callback);
      }
      /// 停止计时
      void stopTimer(){
        if(_timer != null){
          _timer.cancel();
        }
      }
      @override
      Widget build(BuildContext context) {
        mcontext = context;
        return Scaffold(
            appBar: AppBar(
              title: Text('Agora Flutter QuickStart'),
            ),
            backgroundColor: Colors.black,
            body: Center(
                child: Stack(
                  children: <Widget>[_viewRows(),_smallWindow(),_mask(), _toolbar(),_friendInfo()],//_panel(),
                )));
      }
      Widget _emptyView(){
        return Container(
          width: 1.0,
          height: 1.0,
        );
      }
    }
    
    1. Agora的工具类
    class AgoraUtils{
    
      static AgoraRtmClient _client;
      static VideoCallState _videoCallState;
    
      /// Agora 初始化
      static Future<AgoraRtmClient> getAgoraRtmClient() async {
        if(_client == null){
          _client =
          await AgoraRtmClient.createInstance(APPApiKey.Agora_app_id);
        }
        return _client;
      }
    
      /// 查询用户是否在线
      ///  true-在线  , false-离线
      static Future<bool> queryPeerOnlineStatus(AgoraRtmClient _client, String peerUid) async {
        if(EmptyUtil.textIsEmpty(peerUid)){
          return false;
        }else{
          try {
            Map<String, bool> result =
            await _client.queryPeersOnlineStatus([peerUid]);
            return result[peerUid];
          } catch (errorCode) {
            return false;
          }
        }
      }
      /// 获取声网的消息类型
      /// 1-请求视频通话
      /// 2-取消请求通话
      /// 3-拒绝通话请求
      static String getAgoraMsgType(int type){
        switch(type){
          case 1:
            return "CALLVIDEO";
          case 2:
            return "CANCEL_VIDEO";
          case 3:
            return "REFUSE_VIDEO";
          default:
            return "";
        }
    
      }
      /// 视频请求
      static set videoCallState(VideoCallState value) {
        _videoCallState = value;
      }
      /// 视频请求
      static VideoCallState get videoCallState => _videoCallState;
    
      static clearVideoCallState(){
        _videoCallState= null;
      }
    }
    
    1. Android 本地播放响铃的相关代码
    MediaPlayer mediaPlayer;
        private  float BEEP_VOLUME = 9.10f;
        MediaPlayer.OnCompletionListener beepListener;
       
        private void startPlayBell(){
            if(beepListener == null){
                beepListener = new MediaPlayer.OnCompletionListener() {
                    // 声音
                    public void onCompletion(MediaPlayer mediaPlayer) {
                        mediaPlayer.seekTo(0);
                    }
                };
            }
                mediaPlayer = new MediaPlayer();
                mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
                mediaPlayer.setOnCompletionListener(beepListener);
                mediaPlayer.setLooping(true);
    
                AssetFileDescriptor file = getResources().openRawResourceFd(R.raw.wechat_video);
                try {
                    mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength());
                    file.close();
                    mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
                    mediaPlayer.prepare();
                } catch (IOException e) {
                    mediaPlayer = null;
                }
           // }
            if(mediaPlayer != null){
                mediaPlayer.start();
            }
        }
        private void stopPlayBell(){
            if(mediaPlayer != null){
                mediaPlayer.stop();
                mediaPlayer.release();
            }
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            if(mediaPlayer != null){
                mediaPlayer.release();
            }
        }
    

    其中 R.raw.wechat_video 是我找的音频文件,类似微信视频来电时的响铃

    图片.png

    最后

    GitHub地址(https://github.com/Lightforest/FlutterVideo)
    ---end---

    相关文章

      网友评论

        本文标题:flutter学习笔记:利用声网的flutter插件实现视频通话

        本文链接:https://www.haomeiwen.com/subject/otgchctx.html