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

真丶深紅騎士發表於2019-05-27

一、前言

今天用聲網提供的Flutter外掛聲網Agore來簡單實現體驗音視訊功能。首先前往聲網官網看看大致介紹:

聲網介紹
可以看到聲網sdk支援語音通話,視訊通話和互動直播,接著點選立即體驗註冊賬號和建立專案,目的是獲取App ID,最後在專案詳情能看到專案名字,App ID,專案狀態,建立時間,應用證照,信令令牌除錯開關等:

聲網專案資訊
目前對我最有用的是App ID,其他可以先忽略。

二、依賴外掛

因為我是用Flutter來實現,因此聲網外掛應該在pub.dev/packages/上,搜尋Agore,可以看到:

Flutter聲網外掛
從上面資訊可以知道聲網的外掛叫agora_rtc_engine,版本是0.9.5,Agore.io提供構建模組,通過SDK新增實時語音和視訊通訊。另外簡單說了用法,一些所必要許可權和注意事項,下面直接依賴此外掛進行開發,首先在pubspec.yaml檔案下新增依賴:

依賴聲網外掛
可以看到我還依賴了許可權庫和吐司庫,目的是為了動態申請許可權和彈出提示。

三、專案結構

專案結構
整個demo例子結構很簡單,主要是四個Dart檔案:分別是視訊語音物件,首頁,語音頁,視訊頁。

1.首頁

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

首頁草圖
根佈局是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),
              ),
            ),
          ],
        ),
      ),
    );
  }
複製程式碼

效果如下:

首頁佈局
下面實現點選事件,邏輯很簡單,首先是要授予許可權(許可權用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)

這裡我只做了一對一語音通話的介面效果,也可以實現多人通話,只是把介面樣式改成自己喜歡的樣式即可。

2.1.樣式

一對一通話的介面類似微信語音通話介面一樣,螢幕中間是對方頭像(這裡我只顯示對方使用者ID),底部是選單欄:是否靜音,結束通話,是否外放,草圖如下:

一對一通話樣式草圖
主要用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.邏輯

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

  • 初始化引擎
  • 啟用音訊模組
  • 建立房間
  • 設定事件監聽(成功加入房間,是否有使用者加入,使用者是否離開,使用者是否掉線)
  • 佈局實現
  • 退出語音(根據需要銷燬引擎,釋放資源)
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.最終效果

視訊效果
最終效果如上圖,前後攝像頭切換,結束通話和靜音的功能效果沒錄進去,程式碼寫的有點亂,為了體驗,沒有封裝,後面有機會就再具體完善了。

四、總結

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

五、參考資料

相關文章