RV的时间戳水印

RV的时间戳水印 Marc 2023-07-31 17:33:31 1513

最近有个需求, 要求在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的内存地址, 直接往内存里面填值就行.

明天我们再说怎么填入数据.

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
Marc
红包 3 6 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
Marc
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区