Android Native 层实现跨进程 YUV 视频帧共享:基于抽象 Unix Socket 的高效通信方案。基于AOSP13源码或者lineage20 或相近版本。非hook 或者lsp 等插件方案。

 1.引言

在某些定制化 Android 应用场景中,我们可能需要动态替换系统相机的预览画面 —— 例如:虚拟摄像头、AR 贴图、录屏镜像、隐私保护等。这类需求的核心在于:如何从一个 APK 应用中生成 YUV 图像,并实时传递给另一个 Native 进程进行渲染替换

本文将介绍一种高效、稳定、无需 Binder 的跨进程通信方案:使用抽象 Unix Socket 在 Android Native 层传输 YUV 帧数据,并结合 libyuv 实现格式转换与缩放,最终实现预览缓冲区的无缝替换。

2.场景需求

我们有以下两个模块:

  1. APK 服务端:负责采集或生成 YUV 图像(如摄像头、视频解码、AI 生成等)。
  2. rom 模块:修改camra相关核心代码,实时获取外部 YUV 数据进行替换。

目标:APK →rom camera 进程,实时传输 YUV 帧(I420 格式),延迟低、稳定性高

3.APK服务端核心代码及思路

首先在apk中启动一个localsocket 接收 rom中camera 服务的请求

// v_cam_server.cpp#include "v_cam_server.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <thread>
#include <condition_variable>
#include <cerrno>
#include <android/log.h>#define LOG_TAG "VCamServer"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)VCamServer::VCamServer() {latest_frame_ = av_frame_alloc();
}VCamServer::~VCamServer() {stopServer();if (latest_frame_) av_frame_free(&latest_frame_);
}void VCamServer::setCurrentFrame(AVFrame* frame) {std::lock_guard<std::mutex> lock(frame_mutex_);av_frame_unref(latest_frame_);av_frame_move_ref(latest_frame_, frame);frame_ready_ = true;frame_cond_.notify_one();
}void VCamServer::startServer(int width, int height) {if (server_running_) return;width_ = width;height_ = height;server_running_ = true;server_thread_ = std::thread(&VCamServer::serverLoop, this);
}void VCamServer::stopServer() {server_running_ = false;if (server_thread_.joinable()) {server_thread_.join();}
}void VCamServer::serverLoop() {const char* SOCKET_NAME = "vcam_yuv_server"; // 抽象 socket 名:@vcam_yuv_serverint server_fd = socket(AF_UNIX, SOCK_STREAM, 0);if (server_fd < 0) {LOGE("创建 socket 失败");return;}struct sockaddr_un addr{};addr.sun_family = AF_UNIX;addr.sun_path[0] = '\0'; // 抽象命名空间strncpy(addr.sun_path + 1, SOCKET_NAME, sizeof(addr.sun_path) - 2);// ✅ 修复:抽象 socket 地址长度 = 1 + name lengthsocklen_t len = 1 + strlen(SOCKET_NAME);if (bind(server_fd, (struct sockaddr*)&addr, len) < 0) {LOGE("bind 失败: %s", strerror(errno));close(server_fd);return;}if (listen(server_fd, 1) < 0) {LOGE("listen 失败");close(server_fd);return;}LOGI("✅ 抽象 socket 服务启动: @%s", SOCKET_NAME);while (server_running_) {fd_set read_fds;FD_ZERO(&read_fds);FD_SET(server_fd, &read_fds);struct timeval tv{1, 0}; // 1秒超时int ret = select(server_fd + 1, &read_fds, nullptr, nullptr, &tv);if (ret > 0 && FD_ISSET(server_fd, &read_fds)) {int client_fd = accept(server_fd, nullptr, nullptr);if (client_fd >= 0) {LOGI("ROM 客户端连接,准备发送帧");// 等待最新帧AVFrame* frame = nullptr;{std::unique_lock<std::mutex> lock(frame_mutex_);frame_cond_.wait(lock, [this] { return frame_ready_; });if (latest_frame_->data[0]) {frame = av_frame_clone(latest_frame_);frame_ready_ = false;}}if (frame) {// 协议:header[width, height, magic]int header[3] = { width_, height_, 0x12345678 };if (send(client_fd, header, sizeof(header), 0) == sizeof(header)) {// 发 Yfor (int i = 0; i < height_; i++) {send(client_fd, frame->data[0] + i * frame->linesize[0], width_, 0);}// 发 Ufor (int i = 0; i < height_/2; i++) {send(client_fd, frame->data[1] + i * frame->linesize[1], width_/2, 0);}// 发 Vfor (int i = 0; i < height_/2; i++) {send(client_fd, frame->data[2] + i * frame->linesize[2], width_/2, 0);}}av_frame_free(&frame);}close(client_fd);}}}close(server_fd);LOGI("服务已停止");
}

其次apk启动线程获取rtmp 视频流画面并解码为yuv数据
 

#include "v_cam_decoder.h"
#include <fstream>
#include <unistd.h>VCamDecoder::VCamDecoder(): format_ctx_(nullptr), codec_ctx_(nullptr), frame_(nullptr),packet_(nullptr), sws_ctx_(nullptr), video_stream_index_(-1), running_(false) {avformat_network_init();
}VCamDecoder::~VCamDecoder() {stop();if (sws_ctx_) sws_freeContext(sws_ctx_);if (frame_) av_frame_free(&frame_);if (packet_) av_packet_free(&packet_);if (codec_ctx_) avcodec_free_context(&codec_ctx_);if (format_ctx_) avformat_close_input(&format_ctx_);avformat_network_deinit();
}bool VCamDecoder::init(const char* rtmp_url, int width, int height, int fps) {// 初始化成功后,启动 socket 服务server_.startServer(width, height); // 启动服务rtmp_url_ = rtmp_url;width_ = width;height_ = height;fps_ = fps;// 打开输入流if (avformat_open_input(&format_ctx_, rtmp_url_.c_str(), nullptr, nullptr) != 0) {LOGE("无法打开输入流: %s", rtmp_url_.c_str());return false;}if (avformat_find_stream_info(format_ctx_, nullptr) < 0) {LOGE("无法获取流信息");return false;}// 查找视频流for (int i = 0; i < format_ctx_->nb_streams; i++) {if (format_ctx_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {video_stream_index_ = i;break;}}if (video_stream_index_ == -1) {LOGE("未找到视频流");return false;}// 获取解码器AVCodecParameters* codec_par = format_ctx_->streams[video_stream_index_]->codecpar;const AVCodec* codec = avcodec_find_decoder(codec_par->codec_id);if (!codec) {LOGE("找不到解码器");return false;}codec_ctx_ = avcodec_alloc_context3(codec);if (avcodec_parameters_to_context(codec_ctx_, codec_par) < 0) {LOGE("无法复制编解码器参数");return false;}if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) {LOGE("无法打开解码器");return false;}// 分配帧和包frame_ = av_frame_alloc();packet_ = av_packet_alloc();if (!frame_ || !packet_) {LOGE("内存分配失败");return false;}LOGI("解码器初始化成功: %s", rtmp_url_.c_str());return true;
}bool VCamDecoder::writeYUVFrame(AVFrame* frame) {const char* tmp_path = "/data/local/tmp/0.yuv";const char* output_path = "/data/local/tmp/1.yuv";std::ofstream out_file(tmp_path, std::ios::binary);if (!out_file) {LOGE("无法打开临时文件");return false;}// 写入YUV数据 (YUV420P格式)for (int i = 0; i < height_; i++) {out_file.write(reinterpret_cast<const char*>(frame->data[0] + i * frame->linesize[0]), width_);}for (int i = 0; i < height_ / 2; i++) {out_file.write(reinterpret_cast<const char*>(frame->data[1] + i * frame->linesize[1]), width_ / 2);}for (int i = 0; i < height_ / 2; i++) {out_file.write(reinterpret_cast<const char*>(frame->data[2] + i * frame->linesize[2]), width_ / 2);}out_file.close();// 原子性重命名if (rename(tmp_path, output_path) != 0) {LOGE("重命名失败");return false;}return true;
}bool VCamDecoder::decodeFrame() {int ret = av_read_frame(format_ctx_, packet_);if (ret < 0) {if (ret == AVERROR_EOF) {LOGI("流结束");} else {LOGE("读取帧失败: %d", ret);}return false;}if (packet_->stream_index != video_stream_index_) {av_packet_unref(packet_);return true;}ret = avcodec_send_packet(codec_ctx_, packet_);if (ret < 0) {LOGE("发送包失败");av_packet_unref(packet_);return false;}while (ret >= 0) {ret = avcodec_receive_frame(codec_ctx_, frame_);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;} else if (ret < 0) {LOGE("解码错误");break;}// 成功解码一帧
//        if (!writeYUVFrame(frame_)) {
//            LOGE("写入YUV帧失败");
//        }// ✅ 替代 writeYUVFrame:推给 serverserver_.setCurrentFrame(frame_);av_frame_unref(frame_);}av_packet_unref(packet_);return true;
}void VCamDecoder::decodeLoop() {const int64_t frame_duration = 1000000 / fps_; // 微秒while (running_) {auto start_time = std::chrono::high_resolution_clock::now();if (!decodeFrame()) {// 解码失败,短暂等待后继续std::this_thread::sleep_for(std::chrono::milliseconds(100));continue;}auto end_time = std::chrono::high_resolution_clock::now();auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();LOGI("解码耗时: %ld μs", elapsed);if (elapsed < frame_duration) {int64_t remaining = frame_duration - elapsed;usleep(remaining);} else {LOGI("解码耗时超过帧间隔: %ld μs > %ld μs", elapsed, frame_duration);}}
}void VCamDecoder::start() {if (running_) return;running_ = true;decode_thread_ = std::thread(&VCamDecoder::decodeLoop, this);LOGI("开始解码");
}void VCamDecoder::stop() {running_ = false;if (decode_thread_.joinable()) {decode_thread_.join();}LOGI("停止解码");
}

4.rom端核心服务代码修改

rom的核心修改地方为:frameworks/av/services/camera/libcameraservice/device3/Camera3Stream.cpp

在status_t Camera3Stream::returnBuffer 方法中替换为我们解码的yuv数据。

status_t Camera3Stream::returnBuffer(const camera_stream_buffer &buffer,nsecs_t timestamp, nsecs_t readoutTimestamp, bool timestampIncreasing,const std::vector<size_t>& surface_ids, uint64_t frameNumber, int32_t transform) {ATRACE_HFR_CALL();Mutex::Autolock l(mLock);// 1. 检查缓冲区有效性(原有逻辑)if (!isOutstandingBuffer(buffer)) {ALOGE("%s: Stream %d: Returning an unknown buffer.", __FUNCTION__, mId);return BAD_VALUE;}// 2. 新增:仅在缓冲区状态正常时替换 YUV 数据if (buffer.status == CAMERA_BUFFER_STATUS_OK && buffer.buffer != nullptr) {GraphicBufferMapper &mapper = GraphicBufferMapper::get();android_ycbcr ycbcr = {};status_t lockStatus = mapper.lockYCbCr(*buffer.buffer,GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN,Rect(getWidth(), getHeight()),&ycbcr);if (lockStatus == OK) {// 替换 YUV 数据gImageReplacer.replaceYUVBuffer(ycbcr, getWidth(), getHeight());mapper.unlock(*buffer.buffer);} else {ALOGW("%s: Failed to lock buffer for YUV replacement: %s", __FUNCTION__, strerror(-lockStatus));}}// 3. 继续原有逻辑removeOutstandingBuffer(buffer);camera_stream_buffer b = buffer;if (timestampIncreasing && timestamp != 0 && timestamp <= mLastTimestamp) {ALOGE("%s: Stream %d: timestamp %" PRId64 " is not increasing. Prev timestamp %" PRId64,__FUNCTION__, mId, timestamp, mLastTimestamp);b.status = CAMERA_BUFFER_STATUS_ERROR;}mLastTimestamp = timestamp;status_t res = returnBufferLocked(b, timestamp, readoutTimestamp, transform, surface_ids);if (res == OK) {fireBufferListenersLocked(b, /*acquired*/false, /*output*/true, timestamp, frameNumber);}mOutputBufferReturnedSignal.signal();return res;
}
 int pullYUVFromAPK(std::vector<uint8_t>& y_data,std::vector<uint8_t>& u_data,std::vector<uint8_t>& v_data,uint32_t& out_width, uint32_t& out_height) {const char* SOCKET_NAME = "vcam_yuv_server";int sock = socket(AF_UNIX, SOCK_STREAM, 0);if (sock < 0) {ALOGE("pullYUVFromAPK ❌ 创建 socket 失败: %s", strerror(errno));return -1;}struct sockaddr_un addr{};addr.sun_family = AF_UNIX;addr.sun_path[0] = '\0'; // 抽象命名空间strncpy(addr.sun_path + 1, SOCKET_NAME, sizeof(addr.sun_path) - 2);// 正确计算抽象 socket 地址长度socklen_t len = 1 + strlen(SOCKET_NAME); // 不需要 offsetofif (connect(sock, (struct sockaddr*)&addr, len) < 0) {close(sock);ALOGE("pullYUVFromAPK ❌ connect 失败: %s", strerror(errno));return -1;}// 发送请求帧send(sock, "R", 1, 0);// 接收 header: width, height, magicint header[3];if (recv(sock, header, sizeof(header), 0) != sizeof(header)) {close(sock);ALOGE("pullYUVFromAPK ❌ 接收 header 失败");return -1;}int width = header[0], height = header[1];if (header[2] != 0x12345678 || width <= 0 || height <= 0) {close(sock);ALOGE("pullYUVFromAPK ❌ 无效 header: %dx%d, magic=0x%x", width, height, header[2]);return -1;}size_t y_size = width * height;size_t uv_size = (width / 2) * (height / 2);y_data.resize(y_size);u_data.resize(uv_size);v_data.resize(uv_size);auto recvAll = [&](void* buf, size_t len) -> bool {size_t total = 0;while (total < len) {ssize_t n = recv(sock, (char*)buf + total, len - total, 0);if (n <= 0) {ALOGE("pullYUVFromAPK ❌ recv 失败: n=%zd, errno=%s", n, strerror(errno));return false;}total += n;}return true;};bool success = recvAll(y_data.data(), y_size) &&recvAll(u_data.data(), uv_size) &&recvAll(v_data.data(), uv_size);close(sock);if (success) {out_width = width;out_height = height;return 0;} else {ALOGE("pullYUVFromAPK ❌ 接收 YUV 数据不完整");return -1;}}void replaceYUVBuffer(const android_ycbcr &ycbcr, uint32_t dstWidth, uint32_t dstHeight) {auto startTime = std::chrono::high_resolution_clock::now();ALOGD("【YUV】开始替换预览缓冲区 -> 目标分辨率: %ux%u", dstWidth, dstHeight);// === 替代文件读取:通过 socket 拉取 YUV 数据 ===std::vector<uint8_t> srcY, srcU, srcV;uint32_t srcWidth = 0, srcHeight = 0;if (pullYUVFromAPK(srcY, srcU, srcV, srcWidth, srcHeight) != 0) {ALOGE("【ERROR】从 APK 拉取 YUV 数据失败");return;}ALOGD("【YUV】成功拉取源帧: %ux%u", srcWidth, srcHeight);if (srcY.empty() || srcU.empty() || srcV.empty()) {ALOGE("【ERROR】YUV 数据为空");return;}const uint8_t* pSrcY = srcY.data();const uint8_t* pSrcU = srcU.data();const uint8_t* pSrcV = srcV.data();// === 以下逻辑完全保持不变 ===uint8_t* dstY = static_cast<uint8_t*>(ycbcr.y);uint8_t* dstUV = static_cast<uint8_t*>(ycbcr.cb);int libyuvResult = 0;if (srcWidth == dstWidth && srcHeight == dstHeight) {ALOGD("【YUV】直接格式转换: I420 -> NV12");libyuvResult = libyuv::I420ToNV12(pSrcY, srcWidth,pSrcU, srcWidth / 2,pSrcV, srcWidth / 2,dstY, ycbcr.ystride,dstUV, ycbcr.cstride,dstWidth, dstHeight);} else {ALOGD("【YUV】分步处理: 缩放 -> 格式转换");std::vector<uint8_t> tempY(dstWidth * dstHeight);std::vector<uint8_t> tempU((dstWidth / 2) * (dstHeight / 2));std::vector<uint8_t> tempV((dstWidth / 2) * (dstHeight / 2));libyuvResult = libyuv::I420Scale(pSrcY, srcWidth,pSrcU, srcWidth / 2,pSrcV, srcWidth / 2,srcWidth, srcHeight,tempY.data(), dstWidth,tempU.data(), dstWidth / 2,tempV.data(), dstWidth / 2,dstWidth, dstHeight,libyuv::kFilterBilinear);if (libyuvResult == 0) {libyuvResult = libyuv::I420ToNV12(tempY.data(), dstWidth,tempU.data(), dstWidth / 2,tempV.data(), dstWidth / 2,dstY, ycbcr.ystride,dstUV, ycbcr.cstride,dstWidth, dstHeight);}}if (libyuvResult != 0) {ALOGE("【ERROR】libyuv处理失败,错误码: %d", libyuvResult);return;}ALOGD("【YUV】缓冲区替换成功完成!");auto endTime = std::chrono::high_resolution_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);ALOGD("【YUV】处理耗时: %lld ms", duration.count());}

5.效果

使用测试视频 将桌面推送到rtmp服务器

命令如下

ffmpeg -f x11grab -framerate 30 -video_size 1920x1080 -i $DISPLAY -vf "scale=1600:1200" -c:v libx264 -preset ultrafast -tune zerolatency -pix_fmt yuv420p -b:v 2500k -maxrate 2500k -bufsize 5000k -f flv "rtmp://192.168.1.241:1935/live/desktop_stream"

QQ2025825-182222

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SSM从入门到实战:2.5 SQL映射文件与动态SQL

&#x1f44b; 大家好&#xff0c;我是 阿问学长&#xff01;专注于分享优质开源项目解析、毕业设计项目指导支持、幼小初高的教辅资料推荐等&#xff0c;欢迎关注交流&#xff01;&#x1f680; 12-SQL映射文件与动态SQL &#x1f4d6; 本文概述 本文是SSM框架系列MyBatis进…

vue+vite打包后的文件希望放在一个子目录下

比如我们常规操作是打包的项目文件直接放在域名下面。如果我们希望把项目放在子域名下面应该怎么处理呢&#xff1f;需要两个步骤vite.config.js里面指定base的路径假设我们希望放在子目录加做call那么我们可以这样base:/call/,注意不是build目录哈。return的最外层。如果本地和…

Java:Docx4j类库简介及使用

1.简介 Docx4j 是一个功能强大的 Java 类库&#xff0c;专门用于创建和操作 Microsoft Open XML 格式&#xff08;如 Word DOCX、PowerPoint PPTX 和 Excel XLSX&#xff09;的文件。它深受 Java 开发者喜爱&#xff0c;特别是在需要自动化处理 Office 文档的场景下。 下面是一…

【机械故障】旋转机械故障引起的振动信号调制效应概述

系列文章目录 提示&#xff1a;学习笔记 机械故障信号分析 共振峰 旋转机械故障引起的振动信号调制效应概述系列文章目录一、研究背景与意义二、故障引起的调制效应分类三、非平稳信号分析方法3.1 时频分析方法3.2 信号分解方法一、研究背景与意义 在工程实践中&#xff0c;可…

密码安全隐形基石:随机数、熵源与DRBG核心解析与技术关联

前言&#xff1a;密码安全的 “隐形基石” 在数字化浪潮席卷全球的今天&#xff0c;从金融交易的密钥生成到区块链的共识机制&#xff0c;从量子通信的加密协议到智能汽车的身份认证&#xff0c;随机数如同空气般渗透在信息系统的每一个安全节点。然而&#xff0c;看似简单的 …

Vue3 + Element Plus实现表格多行文本截断与智能Tooltip提示

在实际开发中&#xff0c;我们经常需要在表格中展示较长的文本内容&#xff0c;但又希望保持界面的整洁美观。本文将介绍如何在Vue3 和 Element Plus中实现表格多行文本截断&#xff0c;并智能控制Tooltip的显示——只有当文本被截断时才显示Tooltip&#xff0c;否则不显示。 需…

使用powerquery处理数据,取时间或者日期之前的

Table.AddColumn(#"已更改列类型 1", "自定义 (2)", each letcleanText Text.Replace([备注], "#(lf)", " "),hasTime Text.Contains(cleanText, "时间&#xff1a;"),hasDate Text.Contains(cleanText, "日期&…

Java面试全栈技术解析:从Spring Cloud到Kafka的实战演练

面试官&#xff1a;请简单介绍一下Spring Cloud的核心组件&#xff1f; 谢飞机&#xff1a;嗯...Spring Cloud主要是基于Spring Boot的&#xff0c;然后有Eureka做服务发现&#xff0c;Feign做声明式REST调用&#xff0c;还有Config做配置中心... 面试官&#xff1a;那在电商场…

极简 useState:手写 20 行,支持多次 setState 合并

不依赖 React&#xff0c;用 闭包 批处理队列 实现可合并更新的 useState。一、20 行完整代码 function createUseState(initialValue) {let state initialValue;let pending null; // 合并队列let listeners [];const flush () > {if (pending ! null) {…

LabVIEW Vision视觉引导撑簧圈智能插装

为解决人工插装连接器撑簧圈时劳动强度大、效率低、一致性差的问题&#xff0c;例以 LabVIEW为开发平台&#xff0c;结合 IMAQ Vision 机器视觉库&#xff0c;搭配精密硬件搭建智能插装系统。系统可适配 9 芯、13 芯、25 芯、66 芯、128 芯 5 种规格工件&#xff0c;经 100 只产…

【Lua】题目小练11

-- 题目1&#xff1a;-- 给定表 t {"apple", "banana", "apple", "orange", "banana", "apple"}-- 写一个函数 countFreq(tbl) 返回一个新表&#xff0c;统计每个元素出现次数-- 例如&#xff1a;返回 {apple3, …

ElementUI之菜单(Menu)使用

文章目录项目创建创建项目运行项目整理目录删除src/assets中的所有logo.png删除src/components中的所有文件修改src/route/index.js删除src/views中所有文件修改src/app.vue整理完目录如下引入ElementUI安装ElementUI引入ElementUI测试是否安装成功编写src/app.vue运行结果编写…

Python训练营打卡Day44-通道注意力(SE注意力)

知识点回顾&#xff1a; 不同CNN层的特征图&#xff1a;不同通道的特征图什么是注意力&#xff1a;注意力家族&#xff0c;类似于动物园&#xff0c;都是不同的模块&#xff0c;好不好试了才知道。通道注意力&#xff1a;模型的定义和插入的位置通道注意力后的特征图和热力图 内…

shiro进行解密

目录Shiro 解密的核心注意事项1. 密码处理&#xff1a;坚决避免 “可逆解密”2.例子【自己模拟数据库&#xff0c;未连数据库】:Shiro 解密的核心注意事项 1. 密码处理&#xff1a;坚决避免 “可逆解密” 禁用明文存储:永远不要将明文密码存入数据库&#xff0c;必须使用 Has…

更改 Microsoft Edge 浏览器的缓存与用户数据目录位置

Microsoft Edge浏览器默认会将缓存文件和用户数据存储在系统盘&#xff08;通常是C盘&#xff09;&#xff0c;随着使用时间的增长&#xff0c;这些文件可能会占用大量空间。本文将详细介绍多种更改Edge浏览器缓存位置和用户数据目录位置的方法&#xff0c;帮助您更好地管理磁盘…

【传奇开心果系列】Flet框架实现的图形化界面的PDF转word转换器办公小工具自定义模板

let框架实现的图形化界面的PDF转word转换器办公小工具自定义模板一、效果展示截图二、PDF转Word转换器概括介绍三、功能特性四、安装依赖五、运行程序六、使用说明七、注意事项八、技术栈九、系统要求十、源码下载地址 一、效果展示截图二、PDF转Word转换器概括介绍 一个基于Fl…

STM32 定时器(PWM输入捕获)

以下是基于STM32标准库&#xff08;以STM32F103为例&#xff09;实现PWM输入模式&#xff08;自动双沿捕获&#xff09;的完整代码&#xff0c;通过配置定时器的PWM输入模式&#xff0c;可自动捕获外部PWM信号的周期&#xff08;频率&#xff09;​和占空比&#xff0c;无需手动…

Web安全开发指导规范文档V1.0

一、背景 团队最近频繁遭受网络攻击,引起了部门技术负责人的重视,笔者在团队中相对来说更懂安全,因此花了点时间编辑了一份安全开发自检清单,觉得应该也有不少读者有需要,所以将其分享出来。 二、编码安全 2.1 输入验证 说明 检查项 概述 任何来自客户端的数据,如URL和…

在Godot中为您的游戏添加并控制游戏角色的完整技术指南

这是一个在Godot中为您的游戏添加并控制玩家角色的完整技术指南。这个过程分为三大步&#xff1a;​准备资源、构建场景、编写控制脚本。道可道&#xff0c;非常道&#xff0c;名可名&#xff0c;非常名&#xff01;第一步&#xff1a;准备资源&#xff08;建模与动画&#xff…

Flink 状态 RocksDBListState(写入时的Merge优化)

RocksDBListState<K, N, V> RocksDBListState 继承自 AbstractRocksDBState<K, N, List<V>>&#xff0c;并实现了 InternalListState<K, N, V> 接口。继承 AbstractRocksDBState: 这意味着它天然获得了与 RocksDB 交互的底层能力&#xff0c;包括&…