农村小学校园网站建设方案,文字转码unicode,留言板 wordpress,idc机房托管费用前言
本文是由声网社区的开发者“小猿”撰写的Flutter基础教程系列中的第一篇。本文除了讲述实现多人视频通话的过程#xff0c;还有一些 Flutter 开发方面的知识点。该系列将基于声网 Fluttter SDK 实现视频通话、互动直播#xff0c;并尝试虚拟背景等更多功能的实现。 如果…前言
本文是由声网社区的开发者“小猿”撰写的Flutter基础教程系列中的第一篇。本文除了讲述实现多人视频通话的过程还有一些 Flutter 开发方面的知识点。该系列将基于声网 Fluttter SDK 实现视频通话、互动直播并尝试虚拟背景等更多功能的实现。 如果你有一个实现 “多人视频通话” 的场景需求你会选择从零实现还是接第三方 SDK如果在这个场景上你还需要支持跨平台你会选择怎么样的技术路线
我的答案是Flutter 声网 SDK这个组合可以完美解决跨平台和多人视频通话的所有痛点因为 Flutter 天然支持手机端和 PC 端的跨平台能力并拥有不错的性能表现 声网的 Flutter RTC SDK 同样支持 Android、iOS、MacOS 和 Windows 等平台同时也是难得针对 Flutter 进行了全平台支持和优化的音视频 SDK
在开始之前有必要提前简单介绍一下声网的 RTC SDK 相关实现这也是我选择声网的原因。
声网属于是国内最早一批做 Flutter SDK 全平台支持的厂家声网的 Flutter SDK 之所以能在 Flutter 上最早保持多平台的支持原因在于声网并不是使用常规的 Flutter Channel 去实现平台音视频能力 声网的 RTC SDK 的逻辑实现都来自于封装好的 C/C 等 native 代码而这些代码会被打包为对应平台的动态链接库例如.dll、.so 、.dylib 最后通过 Dart 的 FFI(ffigen) 进行封装调用。 这样做的好处在于
Dart 可以和 native SDK 直接通信减少了 Flutter 和原生平台交互时在 Channel 上的性能开销C/C 相关实现在获得更好性能支持的同时也不需要过度依赖原生平台的 API 可以得到更灵活和安全的 API 支持。
如果说这样做有什么坏处那大概就是 SDK 的底层开发和维护成本会剧增不过从用户角度来看这无异是一个绝佳的选择。
开发之前
接下来让我们进入正题既然选择了 Flutter 声网的实现路线那么在开始之前肯定有一些需要准备的前置条件首先是为了满足声网 RTC SDK 的使用条件必须是
Flutter 2.0 或更高版本Dart 2.14.0 或更高版本
从目前 Flutter 和 Dart 版本来看上面这个要求并不算高然后就是你需要注册一个声网开发者账号从而获取后续配置所需的 App ID 和 Token 等配置参数。 如果对后续配置“门清”可以忽略跳过。 创建项目
首先可以在声网控制台的项目管理页面上点击「创建项目」然后在弹出框选输入项目名称之后选择「视频通话」场景和「安全模式(APP ID Token)」 即可完成项目创建。 根据法规创建项目需要实名认证这个必不可少另外使用场景不必太过纠结项目创建之后也是可以根据需要自己修改。 获取 App ID
成功创建项目之后在项目列表点击项目「配置」进入项目详情页面之后会看到基本信息栏目有个 App ID 的字段点击如下图所示图标即可获取项目的 App ID。 App ID 也算是敏感信息之一所以尽量妥善保存避免泄密。 获取 Token
为提高项目的安全性声网推荐了使用Token对加入频道的用户进行鉴权在生产环境中一般为保障安全是需要用户通过自己的服务器去签发 Token而如果是测试需要可以在项目详情页面的“临时 token 生成器”获取临时 Token 在频道名输出一个临时频道比如 Test2 然后点击生成临时 token 按键即可获取一个临时 Token有效期为 24 小时。 这里得到的 Token 和频道名就可以直接用于后续的测试如果是用在生产环境上建议还是在服务端签发 Token 签发 Token 除了 App ID 还会用到 App 证书获取 App 证书同样可以在项目详情的应用配置上获取。 更多服务端签发 Token 可见 token server 文档 开始开发
通过前面的配置我们现在拥有了 App ID、 频道名和一个有效的临时 Token 接下里就是在 Flutter 项目里引入声网的 RTC SDK agora_rtc_engine。
项目配置
首先在Flutter项目的pubspec.yaml文件中添加以下依赖其中 agora_rtc_engine 这里引入的是 6.1.0 版本。 其实 permission_handler 并不是必须的只是因为「视频通话」项目必不可少需要申请到「麦克风」和「相机」权限所以这里推荐使用 permission_handler 来完成权限的动态申请。 dependencies:flutter:sdk: flutteragora_rtc_engine: ^6.1.0permission_handler: ^10.2.0这里需要注意的是Android 平台不需要特意在主工程的 AndroidManifest.xml文件上添加 uses-permission因为 SDK 的 AndroidManifest.xml 已经添加过所需的权限。
iOS 和 macOS 可以直接在 Info.plist 文件添加加 NSCameraUsageDescription 和 NSCameraUsageDescription 的权限声明或者在 Xcode 的 Info 栏目添加Privacy - Microphone Usage Description和Privacy - Camera Usage Description。 keyNSCameraUsageDescription/keystring*****/stringkeyNSMicrophoneUsageDescription/keystring*****/string使用声网 SDK
获取权限
在正式调用声网 SDK 的 API 之前首先我们需要申请权限如下代码所示可以使用 permission_handler 的 request 提前获取所需的麦克风和摄像头权限。
override
void initState() {super.initState();_requestPermissionIfNeed();
}Futurevoid _requestPermissionIfNeed() async {await [Permission.microphone, Permission.camera].request();
}初始化引擎
接下来开始配置 RTC 引擎如下代码所示通过 import 对应的 dart 文件之后就可以通过 SDK 自带的 createAgoraRtcEngine 方法快速创建引擎然后通过 initialize 方法就可以初始化 RTC 引擎了可以看到这里会用到前面创建项目时得到的 App ID 进行初始化。 注意这里需要在请求完权限之后再初始化引擎并更新初始化成功状态 initStatus因为没成功初始化之前不能使用 RtcEngine。 import package:agora_rtc_engine/agora_rtc_engine.dart;late final RtcEngine _engine;///初始化状态
late final Futurebool? initStatus;override
void initState() {super.initState();///请求完成权限后初始化引擎更新初始化成功状态initStatus _requestPermissionIfNeed().then((value) async {await _initEngine();return true;}).whenComplete(() setState(() {}));
}Futurevoid _initEngine() async {//创建 RtcEngine_engine createAgoraRtcEngine();// 初始化 RtcEngineawait _engine.initialize(RtcEngineContext(appId: appId,));···
}
接着我们需要通过registerEventHandler注册一系列回调方法在 RtcEngineEventHandler 里有很多回调通知而一般情况下我们比如常用到的会是下面这 5 个
onError 判断错误类型和错误信息onJoinChannelSuccess加入频道成功onUserJoined有用户加入了频道onUserOffline有用户离开了频道onLeaveChannel离开频道 ///是否加入聊天
bool isJoined false;
/// 记录加入的用户id
Setint remoteUid {};Futurevoid _initEngine() async {···_engine.registerEventHandler(RtcEngineEventHandler(// 遇到错误onError: (ErrorCodeType err, String msg) {print([onError] err: $err, msg: $msg);},onJoinChannelSuccess: (RtcConnection connection, int elapsed) {// 加入频道成功setState(() {isJoined true;});},onUserJoined: (RtcConnection connection, int rUid, int elapsed) {// 有用户加入setState(() {remoteUid.add(rUid);});},onUserOffline:(RtcConnection connection, int rUid, UserOfflineReasonType reason) {// 有用户离线setState(() {remoteUid.removeWhere((element) element rUid);});},onLeaveChannel: (RtcConnection connection, RtcStats stats) {// 离开频道setState(() {isJoined false;remoteUid.clear();});},));
} 用户可以根据上面的回调来判断 UI 状态比如当前用户处于频道内时显示对方的头像和数据其他用户加入和离开频道时更新当前 UI 等。 接下来因为我们的需求是「多人视频通话」所以还需要调用 enableVideo 打开视频模块支持同时我们还可以对视频编码进行一些简单配置比如通过 VideoEncoderConfiguration 配置
dimensions配置视频的分辨率尺寸默认是 640x360frameRate配置视频的帧率默认是 15 fps Future _initEngine() async { Futurevoid _initEngine() async {···// 打开视频模块支持await _engine.enableVideo();// 配置视频编码器编码视频的尺寸像素帧率await _engine.setVideoEncoderConfiguration(const VideoEncoderConfiguration(dimensions: VideoDimensions(width: 640, height: 360),frameRate: 15,),);await _engine.startPreview();}更多参数配置支持如下所示 最后调用 startPreview 开启画面预览功能接下来只需要把初始化好的 Engine 配置到 AgoraVideoView 控件就可以完成渲染。
渲染画面
接下来就是渲染画面如下代码所示在 UI 上加入 AgoraVideoView 控件并把上面初始化成功_engine通过VideoViewController配置到 AgoraVideoView 就可以完成本地视图的预览。 根据前面的initStatus状态在_engine初始化成功后才加载 AgoraVideoView。 Scaffold(appBar: AppBar(),body: FutureBuilderbool?(future: initStatus,builder: (context, snap) {if (snap.data ! true) {return Center(child: new Text(初始化ing,style: TextStyle(fontSize: 30),),);}return AgoraVideoView(controller: VideoViewController(rtcEngine: _engine,canvas: const VideoCanvas(uid: 0),),);}),
);这里还有另外一个参数 VideoCanvas 其中的 uid 是用来标志用户id的这里因为是本地用户这里暂时用 0 表示 。 如果需要加入频道可以调用 joinChannel 方法加入对应频道以下的参数都是必须的其中
token 就是前面临时生成的 TokenchannelId 就是前面的渠道名uid 和上面一样逻辑channelProfile 选择 channelProfileLiveBroadcasting 因为我们需要的是多人通话。clientRoleType 选择 clientRoleBroadcaster因为我们需要多人通话所以我们需要进来的用户可以交流发送内容。
Scaffold(appBar: AppBar(),body: FutureBuilderbool?(future: initStatus,builder: (context, snap) {if (snap.data ! true) {return Center(child: new Text(初始化ing,style: TextStyle(fontSize: 30),),);}return AgoraVideoView(controller: VideoViewController(rtcEngine: _engine,canvas: const VideoCanvas(uid: 0),),);}),
);同样的道理通过前面的 RtcEngineEventHandler 我们可以获取到加入频道用户的 uid(rUid) 所以还是AgoraVideoView但是我们使用 VideoViewController.remote根据 uid 和频道id去创建 controller 配合 SingleChildScrollView 在顶部显示一排可以左右滑动的用户小窗效果。 用 Stack 嵌套层级。 Scaffold(appBar: AppBar(),body: Stack(children: [AgoraVideoView(·····),Align(alignment: Alignment.topLeft,child: SingleChildScrollView(scrollDirection: Axis.horizontal,child: Row(children: List.of(remoteUid.map((e) SizedBox(width: 120,height: 120,child: AgoraVideoView(controller: VideoViewController.remote(rtcEngine: _engine,canvas: VideoCanvas(uid: e),connection: RtcConnection(channelId: channel),),),),)),),),)],),
);这里的 remoteUid 就是一个保存加入到 channel 的 uid 的 Set 对象。 最终运行效果如下图所示引擎加载成功之后点击 FloatingActionButton 加入可以看到移动端和PC端都可以正常通信交互并且不管是通话质量还是画面流畅度都相当优秀可以感受到声网 SDK 的完成度还是相当之高的。 红色是我自己加上的打码。 在使用该例子测试了 12 人同时在线通话效果基本和微信视频会议没有差别以下是完整代码 class VideoChatPage extends StatefulWidget {const VideoChatPage({Key? key}) : super(key: key);overrideStateVideoChatPage createState() _VideoChatPageState();
}class _VideoChatPageState extends StateVideoChatPage {late final RtcEngine _engine;///初始化状态late final Futurebool? initStatus;///是否加入聊天bool isJoined false;/// 记录加入的用户idSetint remoteUid {};overridevoid initState() {super.initState();initStatus _requestPermissionIfNeed().then((value) async {await _initEngine();return true;}).whenComplete(() setState(() {}));}Futurevoid _requestPermissionIfNeed() async {await [Permission.microphone, Permission.camera].request();}Futurevoid _initEngine() async {//创建 RtcEngine_engine createAgoraRtcEngine();// 初始化 RtcEngineawait _engine.initialize(RtcEngineContext(appId: appId,));_engine.registerEventHandler(RtcEngineEventHandler(// 遇到错误onError: (ErrorCodeType err, String msg) {print([onError] err: $err, msg: $msg);},onJoinChannelSuccess: (RtcConnection connection, int elapsed) {// 加入频道成功setState(() {isJoined true;});},onUserJoined: (RtcConnection connection, int rUid, int elapsed) {// 有用户加入setState(() {remoteUid.add(rUid);});},onUserOffline:(RtcConnection connection, int rUid, UserOfflineReasonType reason) {// 有用户离线setState(() {remoteUid.removeWhere((element) element rUid);});},onLeaveChannel: (RtcConnection connection, RtcStats stats) {// 离开频道setState(() {isJoined false;remoteUid.clear();});},));// 打开视频模块支持await _engine.enableVideo();// 配置视频编码器编码视频的尺寸像素帧率await _engine.setVideoEncoderConfiguration(const VideoEncoderConfiguration(dimensions: VideoDimensions(width: 640, height: 360),frameRate: 15,),);await _engine.startPreview();}overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(),body: Stack(children: [FutureBuilderbool?(future: initStatus,builder: (context, snap) {if (snap.data ! true) {return Center(child: new Text(初始化ing,style: TextStyle(fontSize: 30),),);}return AgoraVideoView(controller: VideoViewController(rtcEngine: _engine,canvas: const VideoCanvas(uid: 0),),);}),Align(alignment: Alignment.topLeft,child: SingleChildScrollView(scrollDirection: Axis.horizontal,child: Row(children: List.of(remoteUid.map((e) SizedBox(width: 120,height: 120,child: AgoraVideoView(controller: VideoViewController.remote(rtcEngine: _engine,canvas: VideoCanvas(uid: e),connection: RtcConnection(channelId: channel),),),),)),),),)],),floatingActionButton: FloatingActionButton(onPressed: () async {// 加入频道_engine.joinChannel(token: token,channelId: channel,uid: 0,options: ChannelMediaOptions(channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,clientRoleType: ClientRoleType.clientRoleBroadcaster,),);},),);}
进阶调整
最后我们再来个进阶调整前面 remoteUid 保存的只是远程用户 id 如果我们将 remoteUid 修改为 remoteControllers 用于保存 VideoViewController 那么就可以简单实现画面切换比如「点击用户画面实现大小切换」这样的需求。
如下代码所示简单调整后逻辑为
remoteUid 从保存远程用户 id 变成了 remoteControllers 的 Mapint,VideoViewController新增了currentController用于保存当前大画面下的 VideoViewController 默认是用户自己registerEventHandler 里将 uid 保存更改为 VideoViewController 的创建和保存在小窗处增加 InkWell 点击在单击之后切换 VideoViewController 实现画面切换
class VideoChatPage extends StatefulWidget {const VideoChatPage({Key? key}) : super(key: key);overrideStateVideoChatPage createState() _VideoChatPageState();
}class _VideoChatPageState extends StateVideoChatPage {late final RtcEngine _engine;///初始化状态late final Futurebool? initStatus;///当前 controllerlate VideoViewController currentController;///是否加入聊天bool isJoined false;/// 记录加入的用户idMapint, VideoViewController remoteControllers {};overridevoid initState() {super.initState();initStatus _requestPermissionIfNeed().then((value) async {await _initEngine();///构建当前用户 currentControllercurrentController VideoViewController(rtcEngine: _engine,canvas: const VideoCanvas(uid: 0),);return true;}).whenComplete(() setState(() {}));}Futurevoid _requestPermissionIfNeed() async {await [Permission.microphone, Permission.camera].request();}Futurevoid _initEngine() async {//创建 RtcEngine_engine createAgoraRtcEngine();// 初始化 RtcEngineawait _engine.initialize(RtcEngineContext(appId: appId,));_engine.registerEventHandler(RtcEngineEventHandler(// 遇到错误onError: (ErrorCodeType err, String msg) {print([onError] err: $err, msg: $msg);},onJoinChannelSuccess: (RtcConnection connection, int elapsed) {// 加入频道成功setState(() {isJoined true;});},onUserJoined: (RtcConnection connection, int rUid, int elapsed) {// 有用户加入setState(() {remoteControllers[rUid] VideoViewController.remote(rtcEngine: _engine,canvas: VideoCanvas(uid: rUid),connection: RtcConnection(channelId: channel),);});},onUserOffline:(RtcConnection connection, int rUid, UserOfflineReasonType reason) {// 有用户离线setState(() {remoteControllers.remove(rUid);});},onLeaveChannel: (RtcConnection connection, RtcStats stats) {// 离开频道setState(() {isJoined false;remoteControllers.clear();});},));// 打开视频模块支持await _engine.enableVideo();// 配置视频编码器编码视频的尺寸像素帧率await _engine.setVideoEncoderConfiguration(const VideoEncoderConfiguration(dimensions: VideoDimensions(width: 640, height: 360),frameRate: 15,),);await _engine.startPreview();}overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(),body: Stack(children: [FutureBuilderbool?(future: initStatus,builder: (context, snap) {if (snap.data ! true) {return Center(child: new Text(初始化ing,style: TextStyle(fontSize: 30),),);}return AgoraVideoView(controller: currentController,);}),Align(alignment: Alignment.topLeft,child: SingleChildScrollView(scrollDirection: Axis.horizontal,child: Row(///增加点击切换children: List.of(remoteControllers.entries.map((e) InkWell(onTap: () {setState(() {remoteControllers[e.key] currentController;currentController e.value;});},child: SizedBox(width: 120,height: 120,child: AgoraVideoView(controller: e.value,),),),)),),),)],),floatingActionButton: FloatingActionButton(onPressed: () async {// 加入频道_engine.joinChannel(token: token,channelId: channel,uid: 0,options: ChannelMediaOptions(channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,clientRoleType: ClientRoleType.clientRoleBroadcaster,),);},),);}
}完整代码如上图所示运行后效果如下图所示可以看到画面在点击之后可以完美切换这里主要提供一个大体思路如果有兴趣的可以自己优化并添加切换动画效果。 另外如果你想切换前后摄像头可以通过 _engine.switchCamera(); 等 API 简单实现。 总结
从上面可以看到其实跑完基础流程很简单回顾一下前面的内容总结下来就是
申请麦克风和摄像头权限创建和通过 App ID 初始化引擎注册 RtcEngineEventHandler 回调用于判断状态打开和配置视频编码支持并且启动预览 startPreview调用 joinChannel 加入对应频道通过 AgoraVideoView 和 VideoViewController 配置显示本地和远程用户画面
当然声网 SDK 在多人视频通话领域还拥有各类丰富的底层接口例如虚拟背景、美颜、空间音效、音频混合等等这些我们后面在进阶内容里讲到更多 API 效果可以查阅 Flutter RTC API 获取。
额外拓展
最后做个内容拓展这部分和实际开发可能没有太大关系纯粹是一些技术补充。
如果使用过 Flutter 开发过视频类相关项目的应该知道Flutter 里可以使用外界纹理和PlatfromView两种方式实现画面接入而由此对应的是 AgoraVideoView 在使用 VideoViewController 时是有 useFlutterTexture 和 useAndroidSurfaceView 两个可选参数。 这里我们不讨论它们之间的优劣和差异只是让大家可以更直观理解声网 SDK 在不同平台渲染时的差异作为拓展知识点补充。 首先我们看 useFlutterTexture从源码中我们可以看到
在 macOS 和 windows 版本中声网 SDK 默认只支持 Texture 这种外界纹理的实现这主要是因为 PC 端的一些 API 限制导致。Android 上并不支持配置为 Texture 只支持 PlatfromView 模式这里应该是基于性能考虑。只有 iOS 支持 Texture 模式或者 PlatfromView 的渲染模式可选择所以 useFlutterTexture 更多是针对 iOS 生效。 而针对 useAndroidSurfaceView 参数从源码中可以看到它目前只对 android 平台生效但是如果你去看原生平台的 java 源码实现可以看到其实不管是 AgoraTextureView 配置还是 AgoraSurfaceView 配置最终 Android 平台上还是使用 TextureView 渲染所以这个参数目前来看不会有实际的作用。 最后就像前面说的 声网 SDK 是通过 Dart FFI 调用底层动态库进行支持而这些调用目前看是通过AgoraRtcWrapper进行比如通过 libAgoraRtcWrapper.so 再去调用 lib-rtc-sdk.so 如果对于这一块感兴趣的可以继续深入探索一下。