Linux Media 子系统 V4l2
一 创建 V4l2 的 entity
在Linux内核的Media Controller框架中,V4L2设备作为实体(entity)的注册过程涉及以下步骤:
1. 初始化Media Controller结构
- 驱动首先创建一个
media_device实例,并与V4L2设备(如v4l2_device)关联。例如:struct media_device *mdev = devm_kzalloc(dev, sizeof(*mdev), GFP_KERNEL); media_device_init(mdev); mdev->dev = dev; // 关联到父设备 v4l2_dev->mdev = mdev; // 将media_device绑定到V4L2设备
2. 创建V4L2子设备(Subdev)并注册为Entity
- 每个硬件组件(如传感器、ISP)通过
v4l2_subdev表示,并初始化其media_entity:struct v4l2_subdev *sd = &sensor->sd; v4l2_subdev_init(sd, &sensor_ops); // 初始化子设备 sd->entity.function = MEDIA_ENT_F_CAM_SENSOR; // 设置实体类型 media_entity_pads_init(&sd->entity, num_pads, pads); // 初始化pads
3. 注册Entities到Media Controller
- 将子设备的实体添加到
media_device中:int ret = media_device_register_entity(mdev, &sd->entity);
4. 建立实体间的连接(Links)
- 使用
media_create_pad_link()在源和目标的pad之间创建链接:media_create_pad_link(&sensor_sd->entity, SENSOR_PAD_SRC,&isp_sd->entity, ISP_PAD_SINK, 0);
5. 注册Media Controller到内核
- 最后,注册整个
media_device,用户空间可通过/dev/mediaX访问:int ret = media_device_register(mdev);
关键点
- Entity类型:V4L2子设备(如传感器、ISP)通过
media_entity表示,其类型由function字段标识(如MEDIA_ENT_F_CAM_SENSOR)。 - 自动关联:当V4L2子设备注册时,其
media_entity会自动加入media_device,无需手动添加。 - 用户空间交互:用户可通过
media-ctl工具查看拓扑结构,例如:media-ctl -p -d /dev/media0
示例驱动代码片段
// 初始化media_device
mdev = devm_kzalloc(dev, sizeof(*mdev), GFP_KERNEL);
media_device_init(mdev);
mdev->dev = dev;
strscpy(mdev->model, "My Camera", sizeof(mdev->model));// 初始化传感器子设备
v4l2_subdev_init(&sensor_sd, &sensor_ops);
sensor_sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
sensor_pads[0].flags = MEDIA_PAD_FL_SOURCE;
media_entity_pads_init(&sensor_sd.entity, 1, sensor_pads);
media_device_register_entity(mdev, &sensor_sd.entity);// 注册media_device
media_device_register(mdev);
通过上述步骤,V4L2子设备被注册为Media Controller框架中的实体,用户空间可管理复杂的数据流拓扑。
二 构建 pipeline
在 Linux 的 Media Controller 框架中,构建一个 **Pipeline(数据流管道)**需要明确设备中各个硬件组件(Entity)的拓扑关系,并通过 Pads 和 Links 将它们连接起来。以下是完整的构建流程和关键步骤:
1. 理解基本概念
在构建 Pipeline 前,需明确以下核心概念:
- Entity(实体):表示硬件组件(如摄像头传感器、ISP、DMA引擎等)。
- Pad(端点):Entity 的输入/输出端点,分为 Source Pad(源) 和 Sink Pad(汇)。
- Link(链路):连接两个 Entity 的 Pad,定义数据流方向(如传感器 → ISP → DMA)。
- Format(格式):数据格式(如分辨率、像素格式)需要在连接的 Pad 之间协商。
2. 构建 Pipeline 的步骤
(1) 识别设备中的 Entities
每个硬件组件在驱动中注册为一个 media_entity,例如:
- 传感器:
MEDIA_ENT_F_CAM_SENSOR - ISP(图像处理器):
MEDIA_ENT_F_PROC_VIDEO_ISP - DMA 引擎:
MEDIA_ENT_F_IO_V4L
通过 media-ctl 工具查看设备拓扑:
media-ctl -p -d /dev/media0
输出示例:
Entity 1: Camera Sensor (type 0x0000, function CAM_SENSOR)Pad 0: Source [fmt:SRGGB10/1920x1080]
Entity 2: ISP (type 0x0000, function PROC_VIDEO_ISP)Pad 0: Sink [fmt:SRGGB10/1920x1080]Pad 1: Source [fmt:YUYV/1920x1080]
Entity 3: DMA Engine (type 0x0000, function IO_V4L)Pad 0: Sink [fmt:YUYV/1920x1080]
(2) 建立 Links(连接 Pads)
通过 media-ctl 或驱动代码创建连接,确保数据流方向正确:
# 连接传感器(Entity 1)的 Source Pad 0 → ISP(Entity 2)的 Sink Pad 0
media-ctl -d /dev/media0 -l "'Camera Sensor':0 -> 'ISP':0 [1]"# 连接 ISP(Entity 2)的 Source Pad 1 → DMA Engine(Entity 3)的 Sink Pad 0
media-ctl -d /dev/media0 -l "'ISP':1 -> 'DMA Engine':0 [1]"
[1]表示启用 Link,[0]表示禁用。
(3) 协商格式(Format Negotiation)
每个 Pad 需要设置一致的数据格式(如分辨率、像素格式)。例如,在用户空间通过 media-ctl 设置:
# 设置传感器(Entity 1)的 Source Pad 0 格式为 SRGGB10_1920x1080
media-ctl -d /dev/media0 -V "'Camera Sensor':0 [fmt:SRGGB10/1920x1080]"# 设置 ISP(Entity 2)的 Sink Pad 0 格式为 SRGGB10_1920x1080(与传感器匹配)
media-ctl -d /dev/media0 -V "'ISP':0 [fmt:SRGGB10/1920x1080]"# 设置 ISP(Entity 2)的 Source Pad 1 格式为 YUYV_1920x1080
media-ctl -d /dev/media0 -V "'ISP':1 [fmt:YUYV/1920x1080]"# 设置 DMA Engine(Entity 3)的 Sink Pad 0 格式为 YUYV_1920x1080(与 ISP 匹配)
media-ctl -d /dev/media0 -V "'DMA Engine':0 [fmt:YUYV/1920x1080]"
(4) 激活 Pipeline
通过 V4L2 API 启动数据流:
# 使用 v4l2-ctl 捕获数据(假设 DMA Engine 对应 /dev/video0)
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=YUYV
v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=10 --stream-to=output.raw
3. 内核驱动中的关键操作
驱动需要实现以下功能以支持 Pipeline 构建:
(1) 注册 Entities 和 Pads
在驱动中初始化 media_entity 和 media_pad:
// 示例:摄像头传感器驱动
struct v4l2_subdev *sensor_sd = &sensor->sd;// 初始化子设备
v4l2_subdev_init(sensor_sd, &sensor_ops);// 设置 Entity 类型
sensor_sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;// 初始化 Pads(假设传感器只有一个 Source Pad)
struct media_pad *sensor_pads = &sensor->pads[0];
sensor_pads[0].flags = MEDIA_PAD_FL_SOURCE; // Source Pad
media_entity_pads_init(&sensor_sd->entity, 1, sensor_pads);// 注册到 Media Controller
media_device_register_entity(mdev, &sensor_sd->entity);
(2) 创建 Links
在驱动中动态创建 Links:
// 连接传感器(Source Pad 0)→ ISP(Sink Pad 0)
media_create_pad_link(&sensor_sd->entity, 0, // Source Pad 0&isp_sd->entity, 0, // Sink Pad 0MEDIA_LNK_FL_ENABLED // 启用 Link
);
4. 验证 Pipeline
通过以下方法验证 Pipeline 是否正常工作:
- 查看拓扑:
media-ctl -p -d /dev/media0 - 检查格式协商:
media-ctl -V -d /dev/media0 - 捕获数据:
v4l2-ctl --stream-mmap --stream-to=frame.raw ```## 标题
5. 常见问题
- Link 未启用:检查
media-ctl命令中的[1]标志。 - 格式不匹配:确保相邻 Pads 的格式(如像素格式、分辨率)一致。
- 权限问题:确保用户有权限访问
/dev/mediaX和/dev/videoX。
总结
构建 Media Controller Pipeline 的核心步骤为:
- 识别 Entities 和 Pads(通过驱动或
media-ctl)。 - 建立 Links 定义数据流方向。
- 协商 Formats 确保相邻 Pads 格式一致。
- 激活数据流(通过 V4L2 API)。
通过这一流程,复杂的硬件拓扑(如摄像头 → ISP → 编码器 → 显示)可以在用户空间和内核中高效管理。
三 pipeline 传输数据
在 Linux 的 Media Controller 框架中,启动一个 Pipeline 的数据传输需要结合用户空间的 V4L2 API 和内核驱动的协作。以下是完整的流程和步骤:
1. Pipeline 数据传输的启动条件
在开始传输前,需确保以下条件已满足:
-
Pipeline 已正确构建:
- Entities(实体,如传感器、ISP、DMA)已注册到 Media Controller。
- Pads(端点)之间的 Links(链路)已建立并启用。
- 所有相邻 Pads 的格式(分辨率、像素格式)已协商一致。
-
用户空间与内核的通信准备:
- V4L2 设备节点(如
/dev/video0)已打开。 - 内存缓冲区(DMA Buffer)已分配并映射到用户空间。
- V4L2 设备节点(如
2. 用户空间启动 Pipeline 的步骤
通过 V4L2 API 启动数据传输的核心步骤如下:
(1) 打开 V4L2 设备节点
int fd = open("/dev/video0", O_RDWR);
if (fd < 0) {perror("Failed to open device");exit(EXIT_FAILURE);
}
(2) 设置数据格式
设置视频流的格式(需与 Pipeline 中 DMA Sink Pad 的格式一致):
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // 与 DMA Sink Pad 的格式一致
fmt.fmt.pix.field = V4L2_FIELD_NONE;if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {perror("Failed to set format");exit(EXIT_FAILURE);
}
(3) 申请缓冲区(Request Buffers)
请求内核分配一定数量的 DMA 缓冲区:
struct v4l2_requestbuffers req = {0};
req.count = 4; // 缓冲区数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP; // 使用内存映射模式if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {perror("Failed to request buffers");exit(EXIT_FAILURE);
}
(4) 内存映射(MMAP)缓冲区
将内核分配的缓冲区映射到用户空间:
struct buffer *buffers = calloc(req.count, sizeof(*buffers));
for (int i = 0; i < req.count; i++) {struct v4l2_buffer buf = {0};buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = i;if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {perror("Failed to query buffer");exit(EXIT_FAILURE);}// 映射到用户空间buffers[i].length = buf.length;buffers[i].start = mmap(NULL, buf.length,PROT_READ | PROT_WRITE,MAP_SHARED, fd, buf.m.offset);
}
(5) 将缓冲区入队(Queue Buffers)
将缓冲区放入内核的输入队列,等待填充数据:
for (int i = 0; i < req.count; i++) {struct v4l2_buffer buf = {0};buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = i;if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {perror("Failed to queue buffer");exit(EXIT_FAILURE);}
}
(6) 启动流传输(Start Streaming)
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {perror("Failed to start streaming");exit(EXIT_FAILURE);
}
(7) 捕获数据(Dequeue Buffers)
循环从内核队列中取出已填充数据的缓冲区:
while (1) {struct v4l2_buffer buf = {0};buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {perror("Failed to dequeue buffer");break;}// 处理数据(例如保存到文件)process_image(buffers[buf.index].start, buf.bytesused);// 重新将缓冲区入队if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {perror("Failed to re-queue buffer");break;}
}
3. 内核驱动的职责
内核驱动需要实现以下关键回调函数以支持数据传输:
(1) 启动流(Start Streaming)
当用户空间调用 VIDIOC_STREAMON 时,驱动需启动硬件的数据流:
static int my_driver_start_streaming(struct vb2_queue *vq, unsigned int count) {struct my_device *dev = vb2_get_drv_priv(vq);// 1. 配置硬件寄存器,启动传感器、ISP、DMA 等start_hardware(dev);// 2. 通知下游 Entities(如 ISP)开始工作v4l2_subdev_call(dev->isp_sd, video, s_stream, 1);return 0;
}
(2) 停止流(Stop Streaming)
当用户空间调用 VIDIOC_STREAMOFF 时,驱动需停止硬件:
static void my_driver_stop_streaming(struct vb2_queue *vq) {struct my_device *dev = vb2_get_drv_priv(vq);// 1. 停止传感器、ISP、DMA 等stop_hardware(dev);// 2. 通知下游 Entities 停止工作v4l2_subdev_call(dev->isp_sd, video, s_stream, 0);
}
(3) 缓冲区处理
驱动需要将硬件填充的 DMA 缓冲区返回给用户空间:
static void my_driver_dma_callback(struct my_device *dev) {struct vb2_buffer *vb = dev->current_vb;// 标记缓冲区已填充数据vb2_buffer_done(vb, VB2_BUF_STATE_DONE);dev->current_vb = NULL;
}
4. 调试与验证
(1) 检查 Pipeline 状态
# 查看所有 Entities 和 Links
media-ctl -p -d /dev/media0# 查看格式协商结果
media-ctl -V -d /dev/media0
(2) 验证数据传输
使用 v4l2-ctl 捕获数据:
v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=10 --stream-to=output.raw
(3) 常见问题
- 无数据流:
- 检查 Links 是否启用(
media-ctl -l)。 - 确保所有 Pads 的格式一致。
- 检查 Links 是否启用(
- 权限问题:
sudo chmod 666 /dev/media0 /dev/video0 - 驱动未实现回调:
- 确认
vb2_ops中的start_streaming和stop_streaming已注册。
- 确认
总结
启动 Pipeline 数据传输的完整流程:
- 用户空间:
- 打开设备 → 设置格式 → 申请缓冲区 → 启动流 → 循环捕获数据。
- 内核驱动:
- 实现
start_streaming和stop_streaming回调,协调硬件和下游 Entities。
- 实现
- 调试工具:
- 使用
media-ctl和v4l2-ctl验证 Pipeline 状态和数据流。
- 使用
通过这一流程,复杂硬件(如摄像头 → ISP → 编码器)的实时数据流可以在 Linux 系统中高效运行。
相关文章:
Linux Media 子系统 V4l2
一 创建 V4l2 的 entity 在Linux内核的Media Controller框架中,V4L2设备作为实体(entity)的注册过程涉及以下步骤: 1. 初始化Media Controller结构 驱动首先创建一个media_device实例,并与V4L2设备(如v4…...
14,.左下角的值,路径和,由序列确定树
找树左下角的值 迭代法 层序遍历 class Solution { public:int findBottomLeftValue(TreeNode* root) {queue<TreeNode*> qu;qu.push(root);TreeNode* leftqu.front();while(!qu.empty()){int szqu.size();leftqu.front();for(int i0;i<sz;i){TreeNode* curqu.fron…...
RabbitMQ 如何设置限流?
RabbitMQ 的限流(流量控制)主要依赖于 QoS(Quality of Service) 机制,即 prefetch count 参数。这个参数控制每个消费者一次最多能获取多少条未确认的消息,从而避免某个消费者被大量消息压垮。 1. RabbitMQ…...
Python常见面试题的详解3
1. 类和对象的区别、对象访问类的方法、创建对象时的操作 类和对象的区别:类是一种抽象的概念,它定义了一组具有相同属性和方法的对象的蓝图或模板。而对象是类的具体实例,是根据类创建出来的实体,每个对象都有自己独立的状态&am…...
【推理llm论文精读】DeepSeek V3技术论文_精工见效果
先附上原始论文和效果对比https://arxiv.org/pdf/2412.19437 摘要 (Abstract) DeepSeek-V3是DeepSeek-AI团队推出的最新力作,一个强大的混合专家(Mixture-of-Experts,MoE)语言模型。它拥有671B的总参数量,但每个tok…...
python自动化测试之Pytest框架之YAML详解以及Parametrize数据驱动!
一、YAML详解 YAML是一种数据类型,它能够和JSON数据相互转化,它本身也是有很多数据类型可以满足我们接口 的参数类型,扩展名可以是.yml或.yaml 作用: 1.全局配置文件 基础路径,数据库信息,账号信息&…...
DeepSeek 本地部署指南
在人工智能飞速发展的今天,大语言模型的应用越来越广泛。DeepSeek 作为一款强大的大语言模型,具备出色的语言理解和生成能力。然而,许多用户希望能够在本地部署 DeepSeek,以实现更高的隐私性、更低的延迟和更好的定制化。本文将为…...
[LeetCode]day21 15.三数之和
题目链接 题目描述 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k ,同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 注意:答案中不可以包含重复…...
Unity学习part1
课程为b站【Unity教程】零基础带你从小白到超神 1、脚本执行顺序 unity的脚本执行顺序不像blender的修改器那样按顺序执行,而是系统默认给配置一个值,值越小,执行顺序越靠前(注意,这个顺序是全局生效的) …...
【AI知识点】Adversarial Validation(对抗验证)
【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】【读书与思考】【AI应用】 Adversarial Validation(对抗验证) 是一种用于检查 训练集(Train Set)和测试集(Test Set)是否同分布 的方法。它…...
力扣 15.三数之和
题目: 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k,同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 注意:答案中不可以包含重复的…...
Spring boot中实现字典管理
数据库脚本 CREATE TABLE data_dict (id bigint NOT NULL COMMENT 主键,dict_code varchar(32) DEFAULT NULL COMMENT 字典编码,dict_name varchar(64) DEFAULT NULL COMMENT 字典名称,dict_description varchar(255) DEFAULT NULL COMMENT 字典描述,dict_status tinyint DEFA…...
唯一值校验的实现思路(续)
本文接着上一篇文章《唯一值校验的实现思路》,在后端实现唯一值校验。用代码实现。 /*** checkUniqueException[唯一值校验]** param entity 新增或编辑的学生实体* param insert 是否新增,如果是传入true;反之传入false* return void* date…...
【AI论文】10亿参数大语言模型能超越405亿参数大语言模型吗?重新思考测试时计算最优缩放
摘要:测试时缩放(Test-Time Scaling,TTS)是一种通过在推理阶段使用额外计算来提高大语言模型(LLMs)性能的重要方法。然而,目前的研究并未系统地分析策略模型、过程奖励模型(Process …...
Ubuntu20.04上搭建nginx正向代理提供上网服务
背景:公司很多电脑因软件管控问题不得不禁止设备上网,现需搭建上网代理服务器提供给这些用户使用。 操作系统:ubuntu20.04 工具:nginx-1.25.4 1、下载nginx安装包及依赖 由于nginx默认只持支持转发http协议,所以如…...
web前端布局--使用element中的Container布局容器
前端页面,跟Qt中一样,都是有布局设置的。 先布局,然后再在各布局中添加显示的内容。 Element网站布局容器:https://element.eleme.cn/#/zh-CN/componet/container 1.将element相应的布局容器代码layout,粘贴到vue项…...
使用 PDF SDK 通过页面分割和数据提取对建筑图纸进行分类
一家专门从事设计和建设的建筑公司对大量多页建筑 PDF 图纸进行分类,从而提高协作和运营效率。 这类公司通常承担多个建筑设计项目,每个项目包含多个设计图纸,如详细的结构计划、电气与水管计划、机械计划等。如果项目图纸可以在上传后自动分…...
Linux命名管道与共享内存
命名管道与共享内存 命名管道介绍和基本使用 理解了匿名管道后,命名管道的理解就会变得容易。在前面使用匿名管道时可以发现,之所以可以匿名是因为由父进程创建,子进程拷贝所以子进程和父进程都可以看到这个管道。但是如果对于任意两个进程…...
maven web项目如何定义filter
在 Maven Web 项目中定义一个 Servlet 过滤器(Filter),需要遵循 Java Servlet 规范,并利用 Maven 来管理项目结构和依赖。下面是如何在 Maven Web 项目中定义和配置一个过滤器的基本步骤: 1. 创建过滤器类 首先&…...
使用 Notepad++ 编辑显示 MarkDown
Notepad 是一款免费的开源文本编辑器,专为 Windows 用户设计。它是替代记事本(Notepad)的最佳选择之一,因为它功能强大且轻量级。Notepad 支持多种编程语言和文件格式,并可以通过插件扩展其功能。 Notepad 是一款功能…...
@synchronized的使用
synchronized 介绍 synchronized 是 Objective-C 提供的一种 互斥锁(Mutex),它用于确保一段代码在同一时间只有一个线程能执行,避免多线程访问共享资源时出现数据竞争。 基本语法 synchronized (lockObject) {// 需要加锁的代码…...
解锁Rust:融合多语言特性的编程利器
如果你曾为理解Rust的特性或它们之间的协同工作原理而苦恼,那么这篇文章正是为你准备的。 Rust拥有许多令人惊叹的特性,但这些特性并非Rust所独有。实际上,Rust巧妙地借鉴了众多其他语言的优秀特性,并将它们融合成了一个完美的整体。深入了解Rust这些重要特性的来源以及它是…...
zyNo.23
SQL注入漏洞 1.SQL语句基础知识 一个数据库由多个表空间组成,sql注入关系到关系型数据库,常见的关系型数据库有MySQL,Postgres,SQLServer,Oracle等 以Mysql为例,输入 mysql-u用户名-p密码 即可登录到MySQL交互式命令行界面。 既然是…...
visual studio 在kylin v10上跨平台编译时c++标准库提示缺少无法打开的问题解决
情况1:提示无法打开 源文件 "string"之类导致无法编译 情况2:能编译,但无法打开这些库文件或标准库使用提示下划红色问题 解决方案: 一、通过工具->选项->跨平台里,在“远程标头IntelliSense管理器”更新下载一下…...
黑马Mistral Le chat逆转deepseek
法国人工智能聊天机器人出来了。 Mistral AI比deepseek 性能快很多,准确率更高,非常好用。 全新的发现! 站在老美已经出来的方法&理论上,感觉有0.2亿美金和有gpu算力,感觉搞一个超越国内deepseek难道其实…...
Spring Cloud — 深入了解Eureka、Ribbon及Feign
Eureka 负责服务注册与发现;Ribbon负责负载均衡;Feign简化了Web服务客户端调用方式。这三个组件可以协同工作,共同构建稳定、高效的微服务架构。 1 Eureka 分布式系统的CAP定理: 一致性(Consistency)&am…...
Web项目测试专题(六)压力测试
概述: 压力测试检验Web应用在高并发、高负载情况下的表现,帮助预估系统承载能力和发现瓶颈 步骤: 并发用户测试:增加虚拟用户数测试系统在多人同时使用时的表现 负载测试:模拟高负载情况测试系统的稳定性和响应时间…...
2.5 使用注解进行单元测试详解
Mockito 使用注解进行单元测试详解 Mockito 提供了一系列注解来简化测试代码的编写,减少手动创建和管理 Mock 对象的样板代码。结合 JUnit 5,可以更高效地构建清晰、易维护的单元测试。 1. 核心注解概览 注解作用Mock创建并注入一个 Mock 对象…...
2025年SEO工具有哪些?老品牌SEO工具有哪些
随着2025年互联网的发展和企业线上营销的日益重要,SEO(搜索引擎优化)逐渐成为了提高网站曝光率和流量的重要手段。SEO的工作不仅仅是简单地通过关键词优化和内容发布就能够实现的,它需要依赖一系列专业的SEO工具来帮助分析、监测和…...
使用 React 16+Webpack 和 pdfjs-dist 或 react-pdf 实现 PDF 文件显示、定位和高亮
写在前面 在本文中,我们将探讨如何使用 React 16Webpack 和 pdfjs-dist 或 react-pdf 库来实现 PDF 文件的显示、定位和高亮功能。这些库提供了强大的工具和 API,使得在 Web 应用中处理 PDF 文件变得更加容易。 项目设置 首先,我们需要创建…...
