RV的时间戳水印
最近有个需求, 要求在rtsp流上加时间戳水印, 跟大家看到的所有监控视频录像一样, 都有一个时间戳, 表明录像产生的时间, 之前在海思的平台实现过, 结果发现在RK的平台, 这套东西…完全是红果果的抄袭海思…
左边是RK的, 右边是海思的.
所以搞得我有点信心了啊.
首先这里有几个概念, RGN就是Region的缩写, reqion就是区域的意思, 通常就是加水印, 时间戳之类.
加水印的位置通常有两个一个是VI, 一个是VENC, 就是视频的输入跟编码环节.
下面以rv1106为例, 说下rgn的过程.
首先, 咱们参照sdk里面通常都有的, VI_VENC_RTSP的例程, 把这套推流的东西先跑通.
代码片段:
前面ISP初始化之类就不贴了.
int init_vi_chn1(SAMPLE_VI_CTX_S *viCtx)
{
int s32Ret = RK_SUCCESS;
viCtx[1].u32Width = RKNN_VI_WIDTH;
viCtx[1].u32Height = RKNN_VI_HEIGHT;
viCtx[1].s32DevId = 0;
viCtx[1].u32PipeId = viCtx[0].s32DevId;
viCtx[1].s32ChnId = VI_CHN_1; // 这里使用通道1
viCtx[1].stChnAttr.stIspOpt.u32BufCount = 2;
viCtx[1].stChnAttr.stIspOpt.enMemoryType = VI_V4L2_MEMORY_TYPE_DMABUF;
viCtx[1].stChnAttr.u32Depth = 0;
viCtx[1].stChnAttr.enPixelFormat = RK_FMT_YUV420SP;
viCtx[1].stChnAttr.stFrameRate.s32SrcFrameRate = -1;
viCtx[1].stChnAttr.stFrameRate.s32DstFrameRate = -1;
s32Ret = SAMPLE_COMM_VI_CreateChn(&viCtx[1]);
return s32Ret;
}
编码部分:
// 初始化编码器
int init_venc(SAMPLE_VENC_CTX_S *vencCtx)
{
int s32Ret = RK_SUCCESS;
// venc通道0
vencCtx->s32ChnId = VENC_CHN_0;
vencCtx->u32Width = RTSP_INPUT_VI_WIDTH;
vencCtx->u32Height = RTSP_INPUT_VI_HEIGHT;
vencCtx->u32Gop = 50;
vencCtx->u32BitRate = 4 * 1024;
// 使用h264编码
vencCtx->enCodecType = RK_CODEC_TYPE_H264;
vencCtx->enRcMode = VENC_RC_MODE_H264CBR;
vencCtx->enable_buf_share = 1;
vencCtx->getStreamCbFunc = venc_get_stream; // 注册venc回调. 编码完成之后就会调用这个函数
vencCtx->dstFilePath = "/data/";
/*
H264 66:Baseline 77:Main Profile 100:High Profile
H265 0:Main Profile 1:Main 10 Profile
MJPEG 0:Baseline
*/
vencCtx->stChnAttr.stVencAttr.u32Profile = 100; // 编码器的profile为高级profile
vencCtx->stChnAttr.stGopAttr.enGopMode = VENC_GOPMODE_NORMALP;
s32Ret = SAMPLE_COMM_VENC_CreateChn(vencCtx);
return s32Ret;
}
就只需要注意这个venc回调, 每次编码一帧之后, 就会产生这个回调.
接着就是绑定vi到venc, 我这里由于我的vi要拿去先做rknn的推理, 就没有直接绑定到venc, 而是使用一个把pipe启动之后, 用另外一个线程去直接RK_MPI_VI_GetChnFrame拿数据帧的方法, 如果不做rknn推理, 直接绑定vi到venc是没有任何问题的.
// 启动vi pip0
s32Ret = RK_MPI_VI_StartPipe(0);
if (s32Ret != RK_SUCCESS)
{
printf("RK_MPI_VI_StartPipe failure:$#X pipe:%d", s32Ret, 0);
goto __FINISHED;
}
// 从vi直接拿到数据帧, send去编码
static void *get_vi_stream(void *arg)
{
printf("#Start %s thread, arg:%p\n", __func__, arg);
int s32Ret;
int32_t loopCount = 0;
VIDEO_FRAME_INFO_S stViFrame;
RK_U8 rknnListIdx = 0;
RK_U8 rknnObjNumber = 0;
RK_U32 lastRknnDetectRefreshCounter = 0;
long time_before;
int ret = 0;
while (!quitApp)
{
// 从vi chn 0拿数据帧
s32Ret = RK_MPI_VI_GetChnFrame(0, VI_CHN_0, &stViFrame, GET_FRAME_TIMEOUT);
if (s32Ret == RK_SUCCESS)
{
void *data = RK_MPI_MB_Handle2VirAddr(stViFrame.stVFrame.pMbBlk);
// fd为dma buf的fd
int32_t fd = RK_MPI_MB_Handle2Fd(stViFrame.stVFrame.pMbBlk);
// 当有新的推理结果的时候
if (lastRknnDetectRefreshCounter != rknnDetectRefreshCounter)
{
// 清理列表
memset(&detectResultGroup, 0, sizeof(detectResultGroup));
// 复制列表
rknn_list_pop(rknn_list_, &time_before, &detectResultGroup);
lastRknnDetectRefreshCounter = rknnDetectRefreshCounter;
}
// 发送到编码器
s32Ret = RK_MPI_VENC_SendFrame(VENC_CHN_0, &stViFrame, SEND_FRAME_TIMEOUT);
if (s32Ret != RK_SUCCESS)
{
printf("RK_MPI_VENC_SendFrame timeout:%#X vi index:%d", s32Ret, VI_CHN_0);
}
// 释放数据帧
s32Ret = RK_MPI_VI_ReleaseChnFrame(0, VI_CHN_0, &stViFrame);
if (s32Ret != RK_SUCCESS)
{
printf("RK_MPI_VI_ReleaseChnFrame fail %x\n", s32Ret);
}
loopCount++;
}
}
return NULL;
}
多说一句, 当初的设计, 是从vi拿到数据之后要在这个线程里面画框, 所以中间把vi拿出来, 画框, 再手动送进venc, 其实如果用rgn来画框, 就不用这个多余的动作了.
关键是下面的把venc上粘上rgn的过程
首先venc的通道号是0
// 将rgn通道帮到venc上, 因为rgn只能绑定到venc或者vi
RK_S32 bind_rgn_to_venc(void)
{
RK_S32 s32Ret = RK_FAILURE;
RK_CHAR *pOutPath = NULL;
RK_CODEC_ID_E enCodecType = RK_VIDEO_ID_AVC;
RK_CHAR *pCodecName = "H264";
RK_S32 s32chnlId = 0;
RGN_HANDLE RgnHandle = VENC_RECORD_TIME_OSD_HANDLE;
BITMAP_S stBitmap;
RGN_ATTR_S stRgnAttr;
RGN_CHN_ATTR_S stRgnChnAttr;
// int u32Width = 128;
// int u32Height = 128;
int u32Width = 500; // 16 * 24; // 这里是rgn的区域宽高, 宽度不重要, 高度是32字节对齐的
int u32Height = 32;
int s32X = 100; // rgn的启示位置
int s32Y = 100;
MPP_CHN_S stMppChn;
stMppChn.enModId = RK_ID_VENC;
stMppChn.s32DevId = 0;
stMppChn.s32ChnId = VPSS_CHN_0; // 要绑定的venc的通道号
/****************************************
step 1: create overlay regions
****************************************/
memset(&stRgnAttr, 0, sizeof(stRgnAttr));
// stRgnAttr.unAttr.stOverlay.u32CanvasNum = 1;
// RGN的类型是OVERLAY方式
stRgnAttr.enType = OVERLAY_RGN;
// stRgnAttr.unAttr.stOverlay.enPixelFmt = (PIXEL_FORMAT_E)RK_FMT_ARGB8888;
// RGN格式为BGRA8888, 4字节.
stRgnAttr.unAttr.stOverlay.enPixelFmt = (PIXEL_FORMAT_E)RK_FMT_BGRA8888;
// RGN的宽高为364x32
stRgnAttr.unAttr.stOverlay.stSize.u32Width = u32Width;
stRgnAttr.unAttr.stOverlay.stSize.u32Height = u32Height;
stRgnAttr.unAttr.stOverlay.u32ClutNum = 0;
// 创建RGN区域
s32Ret = RK_MPI_RGN_Create(RgnHandle, &stRgnAttr);
if (RK_SUCCESS != s32Ret)
{
RK_LOGE("RK_MPI_RGN_Create (%d) failed with %#x!", RgnHandle, s32Ret);
RK_MPI_RGN_Destroy(RgnHandle);
return RK_FAILURE;
}
RK_LOGI("The handle: %d, create success!", RgnHandle);
/*********************************************
step 2: display overlay regions to groups
*********************************************/
memset(&stRgnChnAttr, 0, sizeof(stRgnChnAttr));
// RGN通道属性设置
// 可见
stRgnChnAttr.bShow = RK_TRUE;
// 通道类型为OVERLAY
stRgnChnAttr.enType = OVERLAY_RGN;
//
stRgnChnAttr.unChnAttr.stOverlayChn.stPoint.s32X = s32X;
stRgnChnAttr.unChnAttr.stOverlayChn.stPoint.s32Y = s32Y;
// 前景色透明度
stRgnChnAttr.unChnAttr.stOverlayChn.u32BgAlpha = 0;
// 背景色透明度
stRgnChnAttr.unChnAttr.stOverlayChn.u32FgAlpha = 0;
stRgnChnAttr.unChnAttr.stOverlayChn.u32Layer = 0;
// qpInfo设置
stRgnChnAttr.unChnAttr.stOverlayChn.stQpInfo.bEnable = RK_FALSE;
stRgnChnAttr.unChnAttr.stOverlayChn.stQpInfo.bForceIntra = RK_TRUE;
stRgnChnAttr.unChnAttr.stOverlayChn.stQpInfo.bAbsQp = RK_FALSE;
stRgnChnAttr.unChnAttr.stOverlayChn.stQpInfo.s32Qp = RK_FALSE;
stRgnChnAttr.unChnAttr.stOverlayChn.u32ColorLUT[0] = 0x00;
stRgnChnAttr.unChnAttr.stOverlayChn.u32ColorLUT[1] = 0xFFFFFF;
// 反色设置
stRgnChnAttr.unChnAttr.stOverlayChn.stInvertColor.bInvColEn = RK_FALSE;
stRgnChnAttr.unChnAttr.stOverlayChn.stInvertColor.stInvColArea.u32Width = 16;
stRgnChnAttr.unChnAttr.stOverlayChn.stInvertColor.stInvColArea.u32Height = 16;
stRgnChnAttr.unChnAttr.stOverlayChn.stInvertColor.enChgMod = LESSTHAN_LUM_THRESH;
stRgnChnAttr.unChnAttr.stOverlayChn.stInvertColor.u32LumThresh = 100;
s32Ret = RK_MPI_RGN_AttachToChn(RgnHandle, &stMppChn, &stRgnChnAttr);
if (RK_SUCCESS != s32Ret)
{
RK_LOGE("RK_MPI_RGN_AttachToChn (%d) failed with %#x!", RgnHandle, s32Ret);
return RK_FAILURE;
}
RK_LOGI("Display region to chn success!");
return RK_SUCCESS;
....
这里面涉及一个知识点就是rgn可以就理解成在venc/vi的屏幕上, 叠加了一个透明/不透明的画布, 类似以前2d时代的动画面的赛璐璐透明胶片, 把你要加入的内容, 放到这个透明胶片上, 显示一个logo或者时间戳, 或者动态的比如rknn的框子等等.
所以要设置rgn显示区域的大小,跟位置信息, 然后RK_MPI_RGN_Create, 即可, 这个RGN_HANDLE RgnHandle, 就是个单纯的数字, 方便你将来要动态改变rgn的内容的时候, 可以随时拿到这个rgn, 因为可能你的venc通道绑定的rgn不止一个, 显示logo一个, 显示时间戳一个, 画框也有一个, 所以把这些rgn区分开, 就给他分配一个const的数就行, 比如0 , 1, 2, 都行.
然后rgn的叠加方式有几种, 一种是OVERLAY, 一种是COVER, cover就是上面涂一个色块, 没啥意义, 当然还有一种是马赛克, 我也没想到怎么个用法.这里使用OVERLAY
另外, 你会发现有个稍微高级一点的监控, 它显示的时间戳数字, 会根据画面亮度变化而动态变化, 比如本来时间戳都是白色字, 但是某一时刻, 时间戳中的某个数字, 跟画面当中的命令部分重叠了, 这个数字就看不清了, 它就自动变成了黑色, 这个就是它的一个反色的功能, 这里不去仔细研究了, 大概知道就行.
因为这个rv1106的一个主流功能就是ipc, 所以这方面的设置还是挺丰富的.
另外, rgn的编码方式, 使用RK_FMT_BGRA8888, 这个跟他们的venc/vi的编码方式没啥关系, 只是用来你往rgn里面填充数据的时候, 让mpi知道你填入的数据格式是啥, 方便它将你的rgn与venc的输入进行融合.
把rgn创建并绑定好之后, 由于时间戳是每秒钟变化一次的, 所以我们起一个单独的线程, 来每秒钟改变时间戳rgn的显示内容, 就达到了动态时间戳的目的.
pthread_t rgn_ts_thread;
pthread_create(&rgn_ts_thread, NULL, add_ts_thread, NULL);
RK_VOID *add_ts_thread(RK_VOID *p)
{
RK_S32 s32Ret = RK_SUCCESS;
time_t timep;
struct tm *pLocalTime;
RK_U8 seconds = 80; // just for make difference for first load
// RGN_HANDLE Handle;
// Handle = VENC_RECORD_TIME_OSD_HANDLE;
// s32Ret = rgn_add(Handle, VENC_RECORD_TIME_OSD_HANDLE);
while (!quitApp)
{
time(&timep);
pLocalTime = localtime(&timep);
if (seconds == pLocalTime->tm_sec)
{
usleep(100 * 1000);
// usleep(150 * 1000);
continue;
}
else
{
seconds = pLocalTime->tm_sec;
}
// 每秒刷新
// printf(" >>>>>>>>>>>>>>>>>>>>>>>>> adding rgn\n");
// canvas_drawing();
s32Ret = rgn_add(VENC_RECORD_TIME_OSD_HANDLE);
if (RK_SUCCESS != s32Ret)
{
printf("RGN_Add line %d failed! s32Ret: 0x%x.\n", __LINE__, s32Ret);
break;
}
}
// pthread_detach(pthread_self());
return NULL;
}
这个rgn_add就是刷新之前新建的rgn的函数.
HI_S32 rgn_add(unsigned int Handle)
{
// printf("-------------------%s add rgn %d --------------------\n",__func__,Type);
HI_S32 s32Ret = HI_SUCCESS;
RGN_ATTR_S stRgnAttrSet;
RGN_CANVAS_INFO_S stCanvasInfo;
memset(&stRgnAttrSet, 0, sizeof(RGN_ATTR_S));
BITMAP_S stBitmap;
memset(&stBitmap, 0, sizeof(BITMAP_S));
SIZE_S stSize;
/* Photo logo */
s32Ret = RK_MPI_RGN_GetAttr(Handle /*VencOsdHandle*/, &stRgnAttrSet);
if (HI_SUCCESS != s32Ret)
{
printf("HI_MPI_RGN_GetAttr failed! s32Ret: 0x%x.\n", s32Ret);
return s32Ret;
}
s32Ret = RK_MPI_RGN_GetCanvasInfo(Handle /*VencOsdHandle*/, &stCanvasInfo);
if (HI_SUCCESS != s32Ret)
{
printf("HI_MPI_RGN_GetCanvasInfo failed! s32Ret: 0x%x.\n", s32Ret);
return s32Ret;
}
stBitmap.pData = reinterpret_cast<void *>(stCanvasInfo.u64VirAddr);
// stBitmap.pData = (void *)stCanvasInfo.u64VirAddr; // u64VirAddr
stSize.u32Width = stCanvasInfo.stSize.u32Width;
stSize.u32Height = stCanvasInfo.stSize.u32Height;
RK_U32 canvasHeight = stCanvasInfo.stSize.u32Height;
RK_U32 canvasWidth = stCanvasInfo.stSize.u32Width;
s32Ret = update_canvas(&stBitmap, 0x0000, canvasHeight, canvasWidth);
if (HI_SUCCESS != s32Ret)
{
printf("SAMPLE_RGN_UpdateCanvas failed! s32Ret: 0x%x.\n", s32Ret);
return s32Ret;
}
s32Ret = RK_MPI_RGN_UpdateCanvas(Handle /*VencOsdHandle*/);
if (HI_SUCCESS != s32Ret)
{
printf("RK_MPI_RGN_UpdateCanvas failed! s32Ret: 0x%x.\n", s32Ret);
return s32Ret;
}
return HI_SUCCESS;
}
刷新rgn有两种做法, 这里演示其中一种.
具体步骤mpi的文档写得还挺清楚的.
咱们这里的线程就是重复步骤3到5. 拿到画布信息, 修改画布内容, 更新画布.
其中需要咱么做的就是更新画布.
这里唯一一个知识点就是, 通过拿画布的信息:
stBitmap.pData = reinterpret_cast<void *>(stCanvasInfo.u64VirAddr);
其实拿的就是画布的rgn的内存地址, 直接往内存里面填值就行.
明天我们再说怎么填入数据.
- 分享
- 举报
-
浏览量:1967次2023-08-11 17:24:43
-
浏览量:4597次2019-09-01 11:15:39
-
浏览量:1275次2023-12-05 13:41:24
-
浏览量:645次2023-09-01 18:13:05
-
浏览量:1306次2023-01-31 10:06:39
-
浏览量:1405次2023-04-14 11:48:45
-
浏览量:718次2024-01-11 15:05:58
-
浏览量:1010次2024-01-19 17:52:40
-
浏览量:1082次2023-04-14 11:21:17
-
浏览量:1244次2024-01-19 17:57:59
-
浏览量:10439次2020-11-24 23:10:14
-
2023-04-07 15:12:43
-
浏览量:2535次2022-03-05 09:18:25
-
浏览量:1591次2023-12-29 17:51:55
-
浏览量:7625次2020-12-06 00:56:08
-
浏览量:1657次2020-02-28 09:44:10
-
浏览量:13645次2020-11-24 11:31:54
-
浏览量:3194次2020-08-17 19:55:50
-
浏览量:1949次2018-09-07 22:27:19
- rk3568 uart介绍及快速上手教程
- A251-RK3568瑞芯微系列开发板测评之上手体验
- 【瑞福团-内购电视盒】天敏D8+友情价!限购10天~
- 芯贵//不忘初心 方成正果-陈长海"我们如何优化android"
- 基于瑞芯微RV1109 Linux触摸屏GT911驱动调试心得(二)-设备树刷厂商给的触摸屏固件
- 【今天推荐的是!】RK3288芯 2048×1536视网膜屏 五元素ifive mini4
- 【干货评测】五款平板屏幕对比>>>TegraK1 MTK RK3288
- rv1126开发常用debug手段
- 瑞芯微推出全新RK3566教育平板方案,满足教育市场多元需求
- 6月 | 瑞芯微主题专栏征文活动,攻城狮快来攻占
-
广告/SPAM
-
恶意灌水
-
违规内容
-
文不对题
-
重复发帖
Marc
感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~
举报类型
- 内容涉黄/赌/毒
- 内容侵权/抄袭
- 政治相关
- 涉嫌广告
- 侮辱谩骂
- 其他
详细说明