Flutter 之聲網 Agora 實現音頻體驗記錄 | 掘金技術徵文

1、前言

今天用聲網提供的Flutter插件聲網Agora來簡單實現體驗音視頻功能。首先前往聲網官網看看大體介紹:java

聲網介紹
能夠看到 聲網sdk支持語音通話,視頻通話和互動直播,接着點擊 當即體驗註冊帳號和建立項目,目的是獲取 App ID,最後在項目詳情能看到項目名字,App ID,項目狀態,建立時間,應用證書,信令令牌調試開關等:

聲網項目信息
目前對我最有用的是 App ID,其餘能夠先忽略。

2、依賴插件

由於我是用Flutter來實現,所以聲網插件應該在pub.dev/packages/上,搜索Agore,能夠看到:ios

Flutter聲網插件
從上面信息能夠知道聲網的插件叫 agora_rtc_engine,版本是0.9.5, Agore.io提供構建模塊,經過SDK添加實時語音和視頻通訊。另外簡單說了用法,一些所必要權限和注意事項,下面直接依賴此插件進行開發,首先在 pubspec.yaml文件下添加依賴:

依賴聲網插件
能夠看到我還依賴了權限庫和吐司庫,目的是爲了動態申請權限和彈出提示。

3、項目結構

項目結構
整個demo例子結構很簡單,主要是四個Dart文件:分別是視頻語音對象,首頁,語音頁,視頻頁。

1.首頁

首頁佈局很簡單,就兩個按鈕,分別是語音通話和視頻通話,先上草圖:git

首頁草圖
根佈局是 Center,孩子是 RowRow裏分別是左右排列的 RaisedButton按鈕,代碼具體以下:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,//主軸空白區域均分
          children: <Widget>[
            //左邊的按鈕
            RaisedButton(
              padding: EdgeInsets.all(0),
              //點擊事件
              onPressed: () {
                //去往語音頁面
                onAudio();
              },
              child: Container(
                height: 120,
                width: 120,
                //裝飾
                decoration: BoxDecoration(

                    //漸變色
                    gradient: const LinearGradient( colors: [Colors.blueAccent, Colors.lightBlueAccent], ), //圓角12度 borderRadius: BorderRadius.circular(12.0)), child: Text( "語音通話", style: TextStyle(color: Colors.white, fontSize: 18.0), ), //文字居中 alignment: Alignment.center, ), shape: new RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), ), //右邊的按鈕 RaisedButton( padding: EdgeInsets.all(0), onPressed: () {
                //去往視頻頁面
                onVideo();
              },
              child: Container(
                height: 120,
                width: 120,
                //裝飾--->漸變
                decoration: BoxDecoration(
                    gradient: const LinearGradient( colors: [Colors.blueAccent, Colors.lightBlueAccent], ), //圓角12度 borderRadius: BorderRadius.circular(12.0)), child: Text( "視頻通話", style: TextStyle(color: Colors.white, fontSize: 18.0), ), //文字居中 alignment: Alignment.center, ), shape: new RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), ), ], ), ), );
  }
複製代碼

效果以下: github

首頁佈局
下面實現點擊事件,邏輯很簡單,首先是要授予權限(權限用simple_permissions這個庫),權限授予以後再進入相應的頁面:

  • 語音點擊事件onAudio()
onAudio() async {
    SimplePermissions.requestPermission(Permission.RecordAudio)
        .then((status_first) {
      if (status_first == PermissionStatus.denied) {
        //若是拒絕
        Toast.show("此功能須要授予錄音權限", context,
            duration: Toast.LENGTH_SHORT, gravity: Toast.CENTER);
      } else if (status_first == PermissionStatus.authorized) {
        //若是受權贊成 跳轉到語音頁面
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => new AudioCallPage(
                  //頻道寫死,爲了方便體驗
                  channelName: "122343",
                ),
          ),
        );
      }
    });
  }
複製代碼

語音只授予錄音權限便可。服務器

  • 視頻通點擊事件onVideo() 視頻須要授予的權限多了相機權限而兒:
onVideo() async {
    SimplePermissions.requestPermission(Permission.Camera).then((status_first) {
      if (status_first == PermissionStatus.denied) {
        //若是拒絕
        Toast.show("此功能須要授予相機權限", context,
            duration: Toast.LENGTH_SHORT, gravity: Toast.CENTER);
      } else if (status_first == PermissionStatus.authorized) {
        //若是贊成
        SimplePermissions.requestPermission(Permission.RecordAudio)
            .then((status_second) {
          if (status_second == PermissionStatus.denied) {
            //若是拒絕
            Toast.show("此功能須要授予錄音權限", context,
                duration: Toast.LENGTH_SHORT, gravity: Toast.CENTER);
          } else if (status_second == PermissionStatus.authorized) {
            //若是受權贊成
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => new VideoCallPage(
                      //視頻房間頻道號寫死,爲了方便體驗
                      channelName: "122343",
                    ),
              ),
            );
          }
        });
      }
    });
  }
複製代碼

這樣首頁算完成了。微信

2.語音頁面(AudioCallPage)

這裏我只作了一對一語音通話的界面效果,也能夠實現多人通話,只是把界面樣式改爲本身喜歡的樣式便可。app

2.1.樣式

一對一通話的界面相似微信語音通話界面同樣,屏幕中間是對方頭像(這裏我只顯示對方用戶ID),底部是菜單欄:是否靜音,掛斷,是否外放,草圖以下:async

一對一通話樣式草圖
主要用 Stack層疊控件+ Positioned來定位:

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: Text(widget.channelName),
      ),
      //背景黑色
      backgroundColor: Colors.black,
      body: new Center(
        child: Stack(
          children: <Widget>[_viewAudio(), _bottomToolBar()],
        ),
      ),
    );
  }
複製代碼

2.2.邏輯

實現語音主要五個步驟,分別是:ide

  • 初始化引擎
  • 啓用音頻模塊
  • 建立房間
  • 設置事件監聽(成功加入房間,是否有用戶加入,用戶是否離開,用戶是否掉線)
  • 佈局實現
  • 退出語音(根據須要銷燬引擎,釋放資源)
2.2.1.初始化引擎

初始化引擎只有一句代碼:工具

//初始化引擎
    AgoraRtcEngine.create(agore_appId);
複製代碼

進去源碼發現:

/// Creates an RtcEngine instance.
  ///
  /// The Agora SDK only supports one RtcEngine instance at a time, therefore the app should create one RtcEngine object only.
  /// Only users with the same App ID can join the same channel and call each other.
  //在RtcEngine SDK的應用程序應該只建立一個RtcEngine實例
  static Future<void> create(String appid) async {
    _addMethodCallHandler();
    return await _channel.invokeMethod('create', {'appId': appid});
  }

複製代碼

發現裏面還調用例_addMethodCallHandler方法,忘看看裏面:

// CallHandler
  static void _addMethodCallHandler() {
    _channel.setMethodCallHandler((MethodCall call) {
      Map values = call.arguments;

      switch (call.method) {
        // Core Events
        case 'onWarning':
          if (onWarning != null) {
            onWarning(values['warn']);
          }
          break;
        case 'onError':
          if (onError != null) {
            onError(values['err']);
          }
          break;
        case 'onJoinChannelSuccess':
          if (onJoinChannelSuccess != null) {
            onJoinChannelSuccess(
                values['channel'], values['uid'], values['elapsed']);
          }
          break;
        case 'onRejoinChannelSuccess':
          if (onRejoinChannelSuccess != null) {
            onRejoinChannelSuccess(
                values['channel'], values['uid'], values['elapsed']);
          }
          break;
          ......
          }
     }
     
  }
複製代碼

能夠看到主要是特定觸發條件的回調,如:SDK錯誤,是否成功建立頻道,是否離開頻道等,那麼如今能夠知道AgoraRtcEngine.create(agore_appId)這行代碼是初始化引擎和實現某些狀態下的監聽回調。

2.2.2.啓用音頻模塊

啓用音頻模塊:

//設置視頻爲可用 啓用音頻模塊
    AgoraRtcEngine.enableAudio();
複製代碼

看官方文檔介紹:

啓用音頻模塊

2.2.3.加入房間

當初始化完引擎和啓用音頻模塊後,下面進行建立房間:

//建立渲染視圖
  void _createRendererView(int uid) {
    //增長音頻會話對象 爲了音頻佈局須要(經過uid和容器信息)
    //加入頻道 第一個參數是 token 第二個是頻道id 第三個參數 頻道信息 通常爲空 第四個 用戶id
    setState(() {
      AgoraRtcEngine.joinChannel(null, widget.channelName, null, uid);
    });

    VideoUserSession videoUserSession = VideoUserSession(uid);
    _userSessions.add(videoUserSession);
    print("集合大小"+_userSessions.length.toString());
  }
複製代碼

主要看AgoraRtcEngine.joinChannel(null, widget.channelName, null, uid);這個方法:

加入聲音頻道
第一個參數是服務器生成的token,第二個參數是聲音的頻道號,第三個參數是頻道的信息,第四個參數是用戶的uid,我這邊傳0,sdk會自動分配。另外注意我這邊用 VideoUserSession類來管理用戶信息,經過集合 List<VideoUserSession>來存放當前在房間的人數,目的就是爲了佈局方便。

2.2.4.設置事件的監聽

當若是有用戶新加入進來,或者用戶離開又或者是掉線,咱們能不能知道呢?答案是確定的:

//設置事件監聽
  void setAgoreEventListener() {
    //成功加入房間
    AgoraRtcEngine.onJoinChannelSuccess =
        (String channel, int uid, int elapsed) {
      print("成功加入房間,頻道號:${channel}+uid+${uid}");
    };

    //監聽是否有新用戶加入
    AgoraRtcEngine.onUserJoined = (int uid, int elapsed) {
      print("新用戶所加入的id爲:$uid");

      setState(() {
        //更新UI佈局
        _createRendererView(uid);
        self_uid = uid;
      });
    };

    //監聽用戶是否離開這個房間
    AgoraRtcEngine.onUserOffline = (int uid, int reason) {
      print("用戶離開的id爲:$uid");
      setState(() {
        //移除用戶 更新UI佈局
        _removeRenderView(uid);
      });
    };

    //監聽用戶是否離開這個頻道
    AgoraRtcEngine.onLeaveChannel = () {
      print("用戶離開");
    };
  }
複製代碼
2.2.5.佈局實現

下面簡單實現屏幕中間的UI實現,我這邊只作了一對一通話,也就是中間只顯示對方的用戶id,若是多人通話,也能夠根據List<VideoUserSession>的數量依次顯示。

//音頻佈局視圖佈局
  Widget _viewAudio() {
    //先獲取音頻人數
    List<int> views = _getRenderViews();
    switch (views.length) {
      //只有一個用戶(即本身)
      case 1:
        return Center(
          child: Container(
            child: Text("用戶1"),
          ),
        );
      //兩個用戶
      case 2:
        return Positioned(//在中間顯示對方id
          top: 180,
          left: 30,
          right: 30,
          child: Container(
            height: 260,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Container(
                    alignment: Alignment.center,
                    width: 140,
                    height: 140,
                    color: Colors.red,
                    child: Text("對方用戶uid:\n${self_uid}",
                      textAlign: TextAlign.center,

                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ],
            ),
          ),
        );

      default:
    }
    return new Container();
  }
複製代碼

上面主要是根據List<VideoUserSession>集合本身控制語音經過頁面。

2.2.6.退出語音

若是用戶退出本界面或者掛斷,必須調用AgoraRtcEngine.leaveChannel();

//本頁面即將銷燬
  @override
    void dispose() {
    //把集合清掉
    _userSessions.clear();
    AgoraRtcEngine.leaveChannel();
    //sdk資源釋放
    AgoraRtcEngine.destroy();
    super.dispose();
  }
複製代碼

當有用戶離開了這個房間後,會回調AgoraRtcEngine.onUserOffline這個方法,文檔也有說明:

用戶掉線
文檔清晰說明當用戶主動離開或者掉線都會回調這個方法,我經過這個方法來實現當用戶退出房間後(移除用戶會話對象)UI更新效果:

//移除對應的用戶界面 而且移除用戶會話對象
  void _removeRenderView(int uid) {
    //先從會話對象根據uid來清除
    VideoUserSession videoUserSession = _getVideoUidSession(uid);

    if (videoUserSession != null) {
      _userSessions.remove(videoUserSession);
    }
  }
複製代碼
2.2.7.是否靜音

是否靜音是經過AgoraRtcEngine.muteLocalAudioStream(muted);方法來實現:

//開關本地音頻發送
  void _isMute() {
    setState(() {
      muted = !muted;
    });
    // true:麥克風靜音 false:取消靜音(默認)
    AgoraRtcEngine.muteLocalAudioStream(muted);
  }
複製代碼
2.2.8.是否開揚聲器
//是否開啓揚聲器
  void _isSpeakPhone() {
    setState(() {
      speakPhone = !speakPhone;
    });
    AgoraRtcEngine.setEnableSpeakerphone(speakPhone);
  }
複製代碼

2.3.最終效果

壓縮後的聲音效果
由於是gif,因此聽不見聲音,上面還有兩個小問題要完善的:

  • 一對一通話應該是雙方鏈接才能進入通話界面
  • 當一方退出後,另外一方也應該退出

3.視頻頁面(VideoCallPage)

這裏視頻支持多人視頻,工具欄也和語音同樣,也是在底部,當和一對一對方視頻通話時,屏幕分爲兩部分,上面是本身,下面是對方的視頻,其餘邏輯和語音基本一致,實現視頻主要有四個步驟:

  • 初始化引擎
  • 啓用視頻模塊
  • 建立視頻渲染視圖
  • 設置本地視圖
  • 開啓視頻預覽
  • 加入頻道
  • 設置事件監聽

3.1.啓用視頻

啓用視頻模塊主要也是一句代碼AgoraRtcEngine.enableVideo();,看文檔說明:

啓用視頻模塊
主要意思是能夠在加入頻道以前或通話期間調用此方法。

3.2.建立視頻渲染視圖

建立視頻播放插件:

//建立渲染視圖
  void _createDrawView(int uid,Function(int viewId) successCreate){
    //該方法建立視頻渲染視圖 而且添加新的視頻會話對象,這個渲染視圖能用在本地/遠端流 這裏須要更新
    //Agora SDK 在 App 提供的 View 上進行渲染。
    Widget view = AgoraRtcEngine.createNativeView(uid, (viewId){
        setState(() {
           _getVideoUidSession(uid).viewId = viewId;
           if(successCreate != null){
             successCreate(viewId);
           }
        });
    });


    //增長視頻會話對象 爲了視頻須要(經過uid和容器信息)
    VideoUserSession videoUserSession = VideoUserSession(uid, view: view);
    _userSessions.add(videoUserSession);


  }
複製代碼

也是經過集合來存放管理會話對象信息,就是爲了方便視頻佈局。

3.3.設置本地視圖

設置本地視圖
官方文檔的意思是設置本地視頻視圖並配置本地設備上的視頻顯示設置:

//設置本地視圖。 該方法設置本地視圖。App 經過調用此接口綁定本地視頻流的顯示視圖 (View),並設置視頻顯示模式。
    // 在 App 開發中,一般在初始化後調用該方法進行本地視頻設置,而後再加入頻道。退出頻道後,綁定仍然有效,若是須要解除綁定,能夠指定空 (null) View 調用
    //該方法設置本地視頻顯示模式。App 能夠屢次調用此方法更改顯示模式。
    //RENDER_MODE_HIDDEN(1):優先保證視窗被填滿。視頻尺寸等比縮放,直至整個視窗被視頻填滿。若是視頻長寬與顯示窗口不一樣,多出的視頻將被截掉
    AgoraRtcEngine.setupLocalVideo(viewId, VideoRenderMode.Hidden);
複製代碼

而且制定視頻渲染模式。

3.4.開啓視頻預覽

開啓視頻預覽
加入頻道以前啓動本地視頻預覽,固然調用此方法以前,必須調用 setupLocalVideoenableVideo

3.5.加入頻道

當一切準備就緒後就要加入視頻房間,加入視頻房間和加入語音房間是同樣的:

//加入頻道 第一個參數是 token 第二個是頻道id 第三個參數 頻道信息 通常爲空 第四個 用戶id
    AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);
複製代碼

3.6.設置事件監聽

設置事件監聽視頻和語音最大一點不同就是,多了設置遠程用戶的視頻視圖,這個方法主要是此方法將遠程用戶綁定到視頻顯示窗口(爲指定的遠程用戶設置視圖uid)。

遠程用戶的視頻視圖
這個方法要在用戶加入的回調方法中調用:

//設置事件監聽
  void setAgoreEventListener(){
    //成功加入房間
    AgoraRtcEngine.onJoinChannelSuccess = (String channel,int uid,int elapsed){
      print("成功加入房間,頻道號:$channel");
    };

    //監聽是否有新用戶加入
    AgoraRtcEngine.onUserJoined = (int uid,int elapsed){
      print("新用戶所加入的id爲:$uid");
      setState(() {
        _createDrawView(uid, (viewId){
          //設置遠程用戶的視頻視圖

          AgoraRtcEngine.setupRemoteVideo(viewId, VideoRenderMode.Hidden, uid);
        });
      });

    };

    //監聽用戶是否離開這個房間
    AgoraRtcEngine.onUserOffline = (int uid,int reason){
      print("用戶離開的id爲:$uid");
      setState(() {
        _removeRenderView(uid);
      });

    };

    //監聽用戶是否離開這個頻道
    AgoraRtcEngine.onLeaveChannel  =  (){
      print("用戶離開");
    };

  }
複製代碼

3.7.佈局實現

這裏要分狀況,1-5各用戶的狀況:

//視頻視圖佈局
  Widget _videoLayout(){
    //先獲取視頻試圖個數
    List<Widget> views = _getRenderViews();

    switch(views.length){
      //只有一個用戶的時候 整個屏幕
      case 1:
        return new Container(
          child: new Column(
            children: <Widget>[
              _videoView(views[0])
            ],
          ),
        );

      //兩個用戶的時候 上下佈局 本身在上面 對方在下面
      case 2:
        return new Container(
          child: new Column(
            children: <Widget>[
              _createVideoRow([views[0]]),
              _createVideoRow([views[1]]),
            ],
          ),
        );

      //三個用戶
      case 3:
        return new Container(
          child: new Column(
            children: <Widget>[
              //截取0-2 不包括2 上面一列兩個 下面一個
              _createVideoRow(views.sublist(0, 2)),

              //截取2 -3 不包括3
              _createVideoRow(views.sublist(2, 3))
            ],
          ),
        );

      //四個用戶
      case 4:
         return new Container(
           child: new Column(
             children: <Widget>[
               //截取0-2 不包括2 也就是0,1 上面 下面各兩個用戶
               _createVideoRow(views.sublist(0, 2)),

               //截取2-4 不包括4 也就是 3,4
               _createVideoRow(views.sublist(2, 4))
             ],
           ),
         );
      default:
    }
    return new Container();
  }
複製代碼

最核心的就是,有用戶退出和加入就要更新UI視圖。

3.8.最終效果

視頻效果
最終效果如上圖,先後攝像頭切換,掛斷和靜音的功能效果沒錄進去,代碼寫的有點亂,爲了體驗,沒有封裝,後面有機會就再具體完善了。

4、總結

  • 總體開發來看並非很難,按照具體的文檔來作,普通的一些功能是能實現的,固然若是後面作一些比較高級的功能就要花多一點心思去研究。
  • 語音,視頻效果仍是不錯的。
  • 有具體的詳細開發,有文檔開發者社區,便於開發者交流,反饋使用過程當中的問題,這一點是很是nice的。
  • 另外,在ios模擬器是運行不了的,報的錯誤是:pod not install,找了不少資料沒解決。。。

5、參考資料

相關文章
相關標籤/搜索