《网络摄像机实战①:Flutter 集成声网 Agora 打造实时音视频对讲》

72 阅读5分钟

声网RTC介绍

声网实时互动(Real-Time Communication,RTC)是提供音视频实时互动服务的产品,可帮助你将实时音视频功能集成到
App 中,实现互动直播、纯语音通话、高清视频通话等功能。在大规模的实时互动场景下,你可以用它实现更好的实时互动效果。 优势介绍
沉浸式音频体验 3A 处理:配备回声消除 (AEC)、降噪 (ANS) 和增益控制 (AGC)
技术,可强力消除百种突发噪声,实现复杂场景非线性回声抑制和近端人声保真,带来纯净沟通体验。
虚拟声卡:声网自研虚拟声卡算法,通过专业算法对用户声音进行优化,让用户的声音美妙动听、更有魅力。
空间音效:用户可以通过音频实时感知到其他用户距离、方位、朝向的变化,增强声音传播的空间感,打造沉浸式音频体验。 至臻画质体验
全平台极致高清:移动端支持 1080P 60 fps,PC端支持 4K 60 fps,配合声网自研 AI 画质算法,实现画质跨级提升。 高效视频编解码:SDK 自适应贴合场景的编解码方式,提供优秀的画质体验和更低的带宽消耗。 自适应高清策略:声网推出自适应高清视频策略,根据设备性能和网络质量自动调整视频属性,实现高清且流畅的视频效果。

声网 RTC 使用流程

在这里插入图片描述

控制台:声网提供给开发者管理声网各项服务的工具。控制台提供直观的界面,方便开发者在使用声网各项服务时进行充值、查询、管理等操作。

App ID:声网为开发项目生成的字符串,是项目的唯一标识。

App 证书:又名 App Certificate,它是声网控制台为开发项目生成的随机字符串,用于开启 Token 鉴权,并作为生成 Token 的参数之一。

Token:也称为动态密钥,是在加入频道时用于校验用户权限的一组字符串。

在控制台获取 App ID、App 证书和 Token 后,就可以开始尝试实现音视频通信的基本流程了。


《网络摄像机实战》系列文章索引

《网络摄像机实战①:Flutter 集成声网 Agora 打造实时音视频对讲》

《网络摄像机实战②:Flutter集成声网Agora图片抓拍与录像功能》

《网络摄像机实战③:设备端集成声网Agora打造实时音视频对讲功能》

《网络摄像机实战④:设备端集成声网Agora 打造远程控制设备或者异常报警推送功能》

《网络摄像机实战⑤:手机端 flutter集成声网Agora RTM打造远程控制设备功能》

《网络摄像机实战⑥:手机端 flutter集成亚马逊 AWS Kinesis Video Streams 音视频界面和控制》

《网络摄像机实战⑦:嵌入式端AWS KVS IoT SDK 库编译与测试全流程-君正/瑞芯微/全志(超详细指南)》

《网络摄像机实战⑧:嵌入式端接入 AWS KVS Producer 的设计要点与注意事项》


声网rtc 的flutter库安装

flutter pub add agora_rtc_engine

声网rtc核心业务集成

  1. 定义变量appId userId token channelName

注意: 必须要的变量是 appid 和channelName,其他的参数可以不用定义,使用空,默认值

  static const String appId = "xxx-xxx-xxx"; // 替换 声网控制台获取的 appid
  static const int userId = 0; // 替换成用户的 id
  static const String token = '';   // 替换成控制台获取的 token,或者为空
  var  channelName = "xxx-xxx-xxx".obs; // 替换设备的 uuid
   var localUid = RxnInt(0); // 远端用户ID
  var remoteUid = RxnInt(0); // 远端用户ID
  RxList<int> remoteUidList = <int>[].obs;//远程用户 ID 列表
 
  1. 定义声网rtcEngine
 var rtcEngine = Rxn<RtcEngine>();//rtc 引擎-核心
  1. 初始化声网引擎rtcEngine
rtcEngine.value = createAgoraRtcEngine();
await rtcEngine.value?.initialize(const RtcEngineContext(appId: appId));

  1. 注册事件处理回调函数registerEventHandler

注意: onSnapshotTaken是抓拍的回调函数 onJoinChannelSuccess加入 rtc 通道成功的回调函数 onUserJoined有其他的用户或者设备进入通道的回调函数 onUserOffline是其他的用户退出通道的回调函数 onRemoteVideoStateChanged是远程视频状态改变的回调函数

// 监听事件
   rtcEngine.value?.registerEventHandler(
     RtcEngineEventHandler(
         onSnapshotTaken: (RtcConnection connection, int uid, String filePath,
             int width, int height, int errCode) async {
           Get.log('[onSnapshotTaken] connection: ${connection.toJson()}, uid: $uid, filePath: $filePath, width: $width, height: $height, errCode: $errCode');

           // if (_snapshotPath.isNotEmpty) {
           //   final preSnapshotFile = File(_snapshotPath);
           //   if (await preSnapshotFile.exists()) {
           //     await preSnapshotFile.delete();
           //   }
           // }
           thumbImageData.value = filePath;
           Get.log("thumbImageData.value: ${thumbImageData.value}");

         },
       onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
         isJoined.value = true;
         localUid.value = connection.localUid;
         Get.log("local user join success: ${connection.localUid}");
       },
       onUserJoined: (RtcConnection connection, int uid, int elapsed) {
         if (!remoteUidList.contains(uid)) {
           remoteUidList.add(uid);
           remoteUid.value = remoteUidList.first;
         }
         // 如果当前没有选中远端用户,则默认选第一个加入的
         if (remoteUid.value == 0) {
           remoteUid.value = remoteUidList.first;
         }
         Get.log("remote user joined: $uid  remoteUid.value: ${remoteUid.value}" );
       },
       onUserOffline: (RtcConnection connection, int uid, UserOfflineReasonType reason) {
         remoteUidList.remove(uid);
         // 只有列表还有用户才更新 remoteUid
         if (remoteUidList.isNotEmpty) {
           remoteUid.value = remoteUidList.first;
         } else {
           remoteUid.value = 0; // 可选,也可以保持原来的值
         }
         Get.log("remote uid leave: $uid remoteUid.value:${remoteUid.value}");
       },
       onRemoteVideoStateChanged: (connection, uid, state, reason, elapsed) {
         // 判断当前远程用户是否可用
         final available = state == RemoteVideoState.remoteVideoStateDecoding;
         if (!available) return; // 只在解码成功时处理
       

         // final available = state == RemoteVideoState.remoteVideoStateStopped;
         Get.log("remote video state changed: $uid, state=$state available= $available");
         // 在 remoteVideoStreamList 中查找是否已经有该 uid
         final index = remoteVideoStreamList.indexWhere((e) => e.uid == uid);
         if (index != -1) {
           // 已经存在,更新 remoteVideoAvailable
           remoteVideoStreamList[index] = remoteVideoStreamList[index].copyWith(remoteVideoAvailable: available);
         } else {
           // 不存在,新增
           remoteVideoStreamList.add(RemoteVideoStream(uid: uid, remoteVideoAvailable: available));
         }
       }
     ),
   );
  1. UI 界面处理显示远程的音视频
controller.isRemoteVideoAvailable(controller.remoteUid.value!)?
Stack(
  children: [
    AgoraVideoView(
      controller: VideoViewController.remote(
        rtcEngine: controller.rtcEngine.value!,
        canvas: VideoCanvas(uid: controller.remoteUid.value),
        // canvas: VideoCanvas(uid: controller.selectedRemoteUid.value),
        // canvas: VideoCanvas(uid: null),
        connection:  RtcConnection(channelId: controller.channelName.value),
      ),
    ),
    Positioned(
        top:0 ,
        right:10,
        child: Center(
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
            decoration: BoxDecoration(
              color: Colors.black54,
              borderRadius: BorderRadius.circular(6),
            ),
            child: Text(
              "UID: ${controller.remoteUid.value}",
              style: const TextStyle(
                color: Colors.white,
                fontSize: 12,
              ),
            ),
          ),
        )
    ),
  ],
) :Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      // CircularProgressIndicator(),
      SpinKitThreeInOut(
        color: Get.theme.primaryColor,
        size: 30.0,
      ),
      SizedBox(height: 20),
      Text('Loading'.tr,style: TextStyle(color: Get.theme.primaryColor)),
    ],
  ),
)
  1. UI 界面显示本地的音视频
Stack(
   children: [
      ClipRRect(
        borderRadius: BorderRadius.circular(12),
        child:  controller.muteLocalCameraFlag.value?AgoraVideoView(
          // child:  controller.rtcEngine.value != null?AgoraVideoView(
          controller: VideoViewController(
            rtcEngine: controller.rtcEngine.value!,
            canvas: const VideoCanvas(uid: 0,renderMode: RenderModeType.renderModeHidden),
          ),
        ):Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // CircularProgressIndicator(),
            SpinKitThreeInOut(
              color: Get.theme.primaryColor,
              size: 30.0,
            ),
            SizedBox(height: 20),
            Text('Loading'.tr,style: TextStyle(color: Get.theme.primaryColor)),
          ],
        ),
      ),
      /// 底部中间的控制栏
      Positioned.fill(
        child: Align(
          alignment: Alignment.bottomCenter, // 底部居中
          child: Row(
            mainAxisSize: MainAxisSize.min, // Row 宽度自适应内容
            children: [
              IconButton(
                onPressed: () => controller.switchPublishMicrophoneTrack(),
                icon: controller.isPublishMicrophoneTrack.value
                    ? Icon(Icons.mic,
                    size: 26,
                    color: Get.theme.primaryColor.withOpacity(1))
                    : Icon(Icons.mic_off, size: 26, color: Colors.red),
              ),
              IconButton(
                onPressed: () => controller.switchPublishCameraTrack(),
                icon: controller.isPublishCameraTrack.value
                    ? Icon(Icons.videocam_outlined,
                    size: 26,
                    color: Get.theme.primaryColor.withOpacity(1))
                    : Icon(Icons.videocam_off_outlined,
                    size: 26,
                    color: Colors.red),
              ),
            ],
          ),
        ),
      )
    ],
  )

标题APP 截图显示

在这里插入图片描述

友情提示🔔

🙏 感谢你的阅读! 如果这篇文章对你有所启发,欢迎关注我 ⭐,欢迎点击 “打赏支持作者” 支持一下我,你的支持是我持续创作的最大动力! 我会持续分享更多关于 智能摄像头 📷、机器人项目、 🤖音视频 RTC 🎧、App 开发 📱、嵌入式开发 🔧 等方向的实战经验,让你更快落地、更少踩坑。 欢迎浏览我其他文章 📚,或许能解决你当前的难题。 如果你正好在做相关项目产品,也欢迎随时私信我,一起技术交流、一起搞事情! 🤝💬📞 联系微信/电话:13826173658