基于声网 Flutter SDK 实现多人视频通话

news2025/7/21 23:05:40

前言

本文是由声网社区的开发者“小猿”撰写的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: flutter

  agora_rtc_engine: ^6.1.0
  permission_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。

 <key>NSCameraUsageDescription</key>
 <string>*****</string>
 <key>NSMicrophoneUsageDescription</key>
 <string>*****</string>

图片

使用声网 SDK

获取权限

在正式调用声网 SDK 的 API 之前,首先我们需要申请权限,如下代码所示,可以使用 permission_handler 的 request 提前获取所需的麦克风和摄像头权限。

@override
void initState() {
  super.initState();

  _requestPermissionIfNeed();
}

Future<void> _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 Future<bool?> initStatus;

@override
void initState() {
  super.initState();
  ///请求完成权限后,初始化引擎,更新初始化成功状态
  initStatus = _requestPermissionIfNeed().then((value) async {
    await _initEngine();
    return true;
  }).whenComplete(() => setState(() {}));
}


Future<void> _initEngine() async {
  //创建 RtcEngine
  _engine = createAgoraRtcEngine();
  // 初始化 RtcEngine
  await _engine.initialize(RtcEngineContext(
    appId: appId,
  ));
  ···
}

接着我们需要通过registerEventHandler注册一系列回调方法,在 RtcEngineEventHandler 里有很多回调通知,而一般情况下我们比如常用到的会是下面这 5 个:

  • onError :判断错误类型和错误信息
  • onJoinChannelSuccess:加入频道成功
  • onUserJoined:有用户加入了频道
  • onUserOffline:有用户离开了频道
  • onLeaveChannel:离开频道

///是否加入聊天
bool isJoined = false;
/// 记录加入的用户id
Set<int> remoteUid = {};

Future<void> _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:配置视频的分辨率尺寸,默认是 640x360
  • frameRate:配置视频的帧率,默认是 15 fps Future _initEngine() async {
  Future<void> _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: FutureBuilder<bool?>(
      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 就是前面临时生成的 Token
  • channelId 就是前面的渠道名
  • uid 和上面一样逻辑
  • channelProfile 选择 channelProfileLiveBroadcasting ,因为我们需要的是多人通话。
  • clientRoleType 选择 clientRoleBroadcaster,因为我们需要多人通话,所以我们需要进来的用户可以交流发送内容。
Scaffold(
  appBar: AppBar(),
  body: FutureBuilder<bool?>(
      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);

  @override
  State<VideoChatPage> createState() => _VideoChatPageState();
}

class _VideoChatPageState extends State<VideoChatPage> {
  late final RtcEngine _engine;

  ///初始化状态
  late final Future<bool?> initStatus;

  ///是否加入聊天
  bool isJoined = false;

  /// 记录加入的用户id
  Set<int> remoteUid = {};

  @override
  void initState() {
    super.initState();
    initStatus = _requestPermissionIfNeed().then((value) async {
      await _initEngine();
      return true;
    }).whenComplete(() => setState(() {}));
  }

  Future<void> _requestPermissionIfNeed() async {
    await [Permission.microphone, Permission.camera].request();
  }

  Future<void> _initEngine() async {
    //创建 RtcEngine
    _engine = createAgoraRtcEngine();
    // 初始化 RtcEngine
    await _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();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Stack(
        children: [
          FutureBuilder<bool?>(
              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 的 Map<int,VideoViewController>
  • 新增了currentController用于保存当前大画面下的 VideoViewController ,默认是用户自己
  • registerEventHandler 里将 uid 保存更改为 VideoViewController 的创建和保存
  • 在小窗处增加 InkWell 点击,在单击之后切换 VideoViewController 实现画面切换
class VideoChatPage extends StatefulWidget {
  const VideoChatPage({Key? key}) : super(key: key);

  @override
  State<VideoChatPage> createState() => _VideoChatPageState();
}

class _VideoChatPageState extends State<VideoChatPage> {
  late final RtcEngine _engine;

  ///初始化状态
  late final Future<bool?> initStatus;

  ///当前 controller
  late VideoViewController currentController;

  ///是否加入聊天
  bool isJoined = false;

  /// 记录加入的用户id
  Map<int, VideoViewController> remoteControllers = {};

  @override
  void initState() {
    super.initState();
    initStatus = _requestPermissionIfNeed().then((value) async {
      await _initEngine();
      ///构建当前用户 currentController
      currentController = VideoViewController(
        rtcEngine: _engine,
        canvas: const VideoCanvas(uid: 0),
      );
      return true;
    }).whenComplete(() => setState(() {}));
  }

  Future<void> _requestPermissionIfNeed() async {
    await [Permission.microphone, Permission.camera].request();
  }

  Future<void> _initEngine() async {
    //创建 RtcEngine
    _engine = createAgoraRtcEngine();
    // 初始化 RtcEngine
    await _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();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Stack(
        children: [
          FutureBuilder<bool?>(
              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 ,如果对于这一块感兴趣的,可以继续深入探索一下。

图片

图片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/395114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

lvgl 笔记 基础对象与组件

一、基础对象 1.1基础对象简介 lvgl 库是纯 c 语言编写的&#xff0c;使用结构体实现类的操作。其所有组件都继承字lv_obj_t&#xff0c;可以使用他的属性和方法。 创建一个基本对象非常简单&#xff1a; lv_obj_t* obj1 lv_obj_create(lv_scr_act()); 其中填入的 lv_scr_a…

KUKA U盘备份系统

KUKA U盘备份系统 原创 March Cai Cass 机器人 2022-03-10 12:39 首先想做系统备份&#xff0c;需要KUKA 官方的U盘&#xff0c;需要设置如下 双击KUKA U盘里的Recovery.exe 打开如右上图&#xff0c;点开专家设置 设置好需要的功能 返回主菜单组选择 配置界面&#xff0c;…

20230306 LS-DYNA悬臂梁案例学习笔记

一、创建材料 在左侧linear elastic,将这个拉至新建的mat材料上

【立体匹配论文阅读】Pyramid Stereo Matching Network

Authors: Jia-Ren Chang, Yong-Sheng Chen Link: https://arxiv.org/abs/1803.08669 Years: 2018 Credit Novelty and Question set up 本文提出之时&#xff0c;基于深度学习的立体匹配主流方式仍是patch-based孪生结构&#xff0c;这种方式缺乏上下文信息的获取能力&#xf…

buuctf-web-[SUCTF 2018]MultiSQL1

打开界面&#xff0c;全部点击一遍&#xff0c;只有注册和登录功能可以使用注册一个账号&#xff0c;注册admin提示用户存在&#xff0c;可能有二次注入&#xff0c;注册admin自动加了一个字符&#xff0c;无法二次注入&#xff0c;点击其他功能点换浏览器重新登录后&#xff0…

java多线程(二一)并发协作生产者消费者设计模式

1.两个线程一个生产者一个消费者 需求情景 两个线程&#xff0c;一个负责生产&#xff0c;一个负责消费&#xff0c;生产者生产一个&#xff0c;消费者消费一个。 涉及问题 同步问题&#xff1a;如何保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用标记或加…

如何查看并读懂昇腾平台的应用日志

当您完成训练/推理工程开发后&#xff0c;将工程放到昇腾平台运行&#xff0c;以调试工程是否正常&#xff0c;此时&#xff0c;可能会出现各种各样、五花八门的异常状况。当问题发生时&#xff0c;我们的第一反应是不是查看日志&#xff0c;看看哪里报错了。昇腾平台有哪些日志…

面向对象设计模式:结构型模式之装饰器模式

一、引入 咖啡馆订购系统 Initial 初始 4 种咖啡 House blend (混合咖啡)Dark Roast (深度烘培)Decaf (低咖啡因咖啡)Espresso (意式浓缩咖啡) 需求变更&#xff1a;客户可以加料&#xff08;咖啡、牛奶、糖等&#xff09; steamed milksoymochaWhip 使用继承&#xff1…

Vue2的基本内容(二)

目录 一、props配置 二、全局事件总线 1.作用 2.原理 3.使用 三、插槽 1.默认插槽 2.具名插槽 3.作用域插槽 四、Vuex 1.vuex是什么 2.vuex工作流程 3.vuex的API &#xff08;1&#xff09;state &#xff08;2&#xff09;state辅助函数mapState &#xff08;…

Linux 练习六 (IPC 管道)

文章目录1 标准管道流2 无名管道&#xff08;PIPE&#xff09;3 命名管道&#xff08;FIFO&#xff09;3.1 创建删除管道文件3.2 打开和关闭FIFO文件3.3 管道案例&#xff1a;基于管道的客服端服务器程序使用环境&#xff1a;Ubuntu18.04 使用工具&#xff1a;VMWare workstati…

【C++】STL——string类的模拟实现

文章目录&#x1f449;string类&#x1f448;&#x1f4d5; 概念&#x1f4d5; 成员变量&#x1f4d5; 构造函数、析构函数&#x1f4d5; size() 、getstr() 函数&#x1f4d5; 拷贝构造&#x1f4d5; 赋值重载&#x1f4d5; 迭代器&#x1f4d5; 运算符重载&#x1f4d5; 尾插…

如何管控第三方软件,保护企业数据安全?

日前&#xff0c;密码管理供应商LastPass公布了关于其数据泄露事件的调查新进展。据其透露&#xff0c;这是一起“二次协同攻击”事件。LastPass在2022年8月、12月先后披露的两起违规事件&#xff0c;这两起事件的攻击链有关联。在此次攻击事件中&#xff0c;LastPass发现恶意黑…

王道计算机组成原理课代表 - 考研计算机 第五章 中央处理器 究极精华总结笔记

本篇博客是考研期间学习王道课程 传送门 的笔记&#xff0c;以及一整年里对 计算机组成 知识点的理解的总结。希望对新一届的计算机考研人提供帮助&#xff01;&#xff01;&#xff01; 关于对 “中央处理器” 章节知识点总结的十分全面&#xff0c;涵括了《计算机组成原理》课…

C++基础——C++相比C语言的新特性梳理总结(C++新特性、输入输出方式、命名空间namespace)

【系列专栏】&#xff1a;博主结合工作实践输出的&#xff0c;解决实际问题的专栏&#xff0c;朋友们看过来&#xff01; 《QT开发实战》 《嵌入式通用开发实战》 《从0到1学习嵌入式Linux开发》 《Android开发实战》 《实用硬件方案设计》 长期持续带来更多案例与技术文章分享…

论文翻译 | Momentum Contrast for Unsupervised Visual Representation Learning(前三章)

前言&#xff1a; 上一次读恺明大神的文章还是两年前&#xff0c;被ResNet的设计折服得不行&#xff0c;两年过去了&#xff0c;我已经被卷死在沙滩上 Momentum Contrast for Unsupervised Visual Representation Learning 摘要 我们提出了针对无监督表征学习的方法MOCO,利用…

上门按摩预约APP源码-东郊到家源码(开发,PHP,平台搭建)

一、什么是上门按摩预约APP源码&#xff1f; 上门按摩预约APP源码是一款家政服务类型的APP&#xff0c;可以帮忙用户在家就能享受按摩的服务。APP源码分两端&#xff0c;一端是用户端&#xff0c;另外一端是技师端。采用的技术&#xff0c;前端是安卓IOS&#xff0c;后端是PHP&…

java_Day004

1.二维数组 二维数组的创建与初始化&#xff08;java是支持规则数组和不规则数组的&#xff09; 例&#xff1a;int[][] array {{1,2},{2,3}{3,4,5}}; 结构如下&#xff1a; 二维数组的遍历&#xff1a; 例子&#xff1a; Testpublic void test21(){int[][] array new int[…

C++学习记录——십이 vector

文章目录1、vector介绍和使用2、vector模拟实现insert和erase和迭代器失效补齐其他函数深浅拷贝难点思考1、vector介绍和使用 vector可以管理任意类型的数组&#xff0c;是一个表示可变大小数组的序列容器。 通过vector文档来看它的使用。 #include <iostream> #inclu…

集群、分布式的理解

一、单机模式小型系统相对简单&#xff0c;所有的业务全部写在一个项目中&#xff0c;部署服务到一台服务器上&#xff0c;所有的请求业务都由这台服务器处理&#xff0c;这就是单机模式。显然&#xff0c;当业务增长到一定程度的时候&#xff0c;服务器的硬件会无法满足业务需…

强化学习 | 课堂笔记 | 第三课 MP的便利性,随机逼近方法

一、回顾 一、值函数、贝尔曼方程、贝尔曼最优方程 二、最优值函数 三、ADP 3.1 VI 3.2 PI 四、ADP可以使用的条件 五、Q函数 六、解决问题的方案 &#xff08;指的是解决“四 ADP可以使用的条件”中的三个问题&#xff09; 二、期望的计算 一、Markov过程的便利性 1…