出发
为什么是NV16
我选择了节点rkispp_scale0,这个节点支持1到8倍的缩放,并且支持最大分辨率,也就是4K
但是出于某种原因,2k及以上需要使用到NV16,具体可以看上一篇文章,官方文档有提及
具体的实现代码也不复杂
VI_CHN_ATTR_S vi_chn_attr;
memset(&vi_chn_attr, 0, sizeof(vi_chn_attr));
vi_chn_attr.pcVideoNode = pDeviceName;
vi_chn_attr.u32BufCnt = 4;
vi_chn_attr.u32Width = width;
vi_chn_attr.u32Height = height;
vi_chn_attr.enPixFmt = NVmod;
vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL;
ret = RK_MPI_VI_SetChnAttr(s32CamId, 0, &vi_chn_attr);
ret |= RK_MPI_VI_EnableChn(s32CamId, 0);
ret |= RK_MPI_VI_StartStream(s32CamId, 0);
if (ret) {
printf("#####首次启动视频流失败! ret=%d\n", ret);
return -1;
}
但是实际效果是,通过AMCAP打开UVC摄像头查看2k图像,出现了颜色偏移问题
通过一番讨论和测试,发现,其实并不是图像本身存在问题,图像就是NV16的正常格式,是因为在UVC处理里,没有针对NV16或者说YUV422SP的处理
为什么是NV12
在看UVC源码之前,我曾多次测试了播放器和图像查看器
通过v4l2分别抓取NV12和NV16的图像,并且通过ffmpeg按照对应格式转换为png,其实都是正常的图片,并没有颜色偏移的问题
得出一个结论,或许大部分(或者说我用到的几个)播放器都是不兼容NV16的,多数都是YUV422的形式
这个是VLC播放器的信息
最后抉择
现在搞清楚了为什么不支持,会出现颜色偏移的问题,我有了这么一个思路:
在NV16(2k及以上时)模式下,针对数据buff进行修改,将NV16转换为NV12再传递给UVC的接口,让它将NV12转换为YUYV好适配绝大多数的播放软件
过程
数据结构
基于这个出发点,我稍微分析了一下mb,也就是rk的buff
void *RK_MPI_MB_GetPtr(MEDIA_BUFFER mb) {
if (!mb)
return NULL;
MEDIA_BUFFER_IMPLE *mb_impl = (MEDIA_BUFFER_IMPLE *)mb;
return mb_impl->ptr;
}
顺藤摸瓜,找到强转mb后的结构体
/* SDK/external/rkmedia/src/c_api/rkmedia_buffer_impl.h */
typedef struct _rkMEDIA_BUFFER_S {
MB_TYPE_E type;
void *ptr; // Virtual address of buffer
int fd; // dma buffer fd
int handle; // dma buffer handle
int dev_fd; // dma device fd
size_t size; // The size of the buffer
MOD_ID_E mode_id; // The module to which the buffer belongs
RK_U16 chn_id; // The channel to which the buffer belongs
RK_U64 timestamp; // buffer timesatmp
RK_U32 flag; // buffer flag
RK_U32 tsvc_level; // buffer level
std::shared_ptr<easymedia::MediaBuffer> rkmedia_mb;
union {
MB_IMAGE_INFO_S stImageInfo;
MB_AUDIO_INFO_S stAudioInfo;
};
} MEDIA_BUFFER_IMPLE;
根据注释,可以了解到指针ptr是指向数据本身的
那么我就可以仿照uvc_encode.cpp的处理来实现NV16到NV12
修改
根据现有代码,对主程序的函数进行了修改
// 获取媒体缓冲区的线程函数
static void *GetMediaBuffer(void *arg) {
MEDIA_BUFFER mb = NULL;
(void)arg;
size_t nv12_size = Wwidth * Hheight * 3 / 2; // 提前计算 NV12 数据大小
uint8_t *nv12_data = NULL;
while (!bufq) {
pthread_mutex_lock(&camera_mutex);
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VI, 0, -1); // 获取缓冲区
pthread_mutex_unlock(&camera_mutex);
if (!mb) {
printf("RK_MPI_SYS_GetMediaBuffer get null buffer!\n");
break;
}
void *data = RK_MPI_MB_GetPtr(mb); // buffer 的数据指针
size_t size = RK_MPI_MB_GetSize(mb);
if (!data || size == 0) {
printf("获取到无效缓冲区,跳过\n");
RK_MPI_MB_ReleaseBuffer(mb);
continue;
}
if (NVmod == IMAGE_TYPE_NV16) {
if (!nv12_data) {
// 分配 NV12 缓冲区
nv12_data = (uint8_t *)malloc(nv12_size);
if (!nv12_data) {
printf("分配 NV12 缓冲区失败\n");
RK_MPI_MB_ReleaseBuffer(mb);
break;
}
}
// NV16 转 NV12
NV16_to_NV12(Wwidth, Hheight, data, nv12_data);
// 构建并传递 NV12 数据给 UVC
MPP_ENC_INFO_DEF info = {0};
info.fd = RK_MPI_MB_GetFD(mb);
info.size = nv12_size;
uvc_read_camera_buffer((void *)nv12_data, &info, NULL, 0);
} else {
// 构建并传递原始数据给 UVC
MPP_ENC_INFO_DEF info = {0};
info.fd = RK_MPI_MB_GetFD(mb);
info.size = size;
uvc_read_camera_buffer(data, &info, NULL, 0);
}
RK_MPI_MB_ReleaseBuffer(mb);
}
// 释放 NV12 缓冲区
if (nv12_data) {
free(nv12_data);
nv12_data = NULL;
}
return NULL;
}
这里只是逻辑上是合理的
- 判断是否为NV16
- 是:将NV16处理为NV12,再传递给UVC
- 否:直接传递给UVC
可惜逻辑上的自洽并不能实现目标
- 要怎么修改buff数据?
这里只是勉强实现了NV16到NV12的函数
void NV16_to_NV12(int width, int height, void* src, void* dst) {
uint8_t* src_y = (uint8_t*)src; // NV16 的 Y 数据起始位置
uint8_t* src_uv = src_y + width * height; // NV16 的 UV 数据起始位置
uint8_t* dst_y = (uint8_t*)dst; // NV12 的 Y 数据目标地址
uint8_t* dst_uv = dst_y + width * height; // NV12 的 UV 数据目标地址
// 拷贝 Y 平面数据
memcpy(dst_y, src_y, width * height);
// 转换 UV 平面数据
for (int j = 0; j < height; j += 2) { // 每两行处理一次
uint8_t* src_uv_line = src_uv + j * width; // NV16 当前行的 UV 数据
uint8_t* dst_uv_line = dst_uv + (j / 2) * width;
for (int i = 0; i < width; i += 2) { // 每两个像素处理一次
dst_uv_line[i] = src_uv_line[i]; // 拷贝 U 分量
dst_uv_line[i + 1] = src_uv_line[i + 1]; // 拷贝 V 分量
}
}
}
抵达
还没搞完,别急
ps.如果有大佬遇到过或者处理过类似的问题,还请务必带带菜鸟