RK3588从零到一实现摄像头视频推流实现(二)

该文章已生成可运行项目,

目录

一、前言

二、H265编码格式

三、MPP组件使用

三、zlmediakit组件应用


一、前言

上一章节,利用V4L2框架完成了对摄像头数据的采集,然后通过opencv进行视频显示。这一章节我们完成使用采集到的数据,通过MPP组件进行H265编码,然后进行推流。

二、H265编码格式

在我zlmediakit组件详解(四)里讲解了H264的文件结构,以及在推流分包的时候需要注意哪些要点。所以这里我说明一下H265和H264文件之间的结构差异。

2.1 起始码:一致

  • H.264:00 00 0100 00 00 01

  • H.265:完全一样,也是使用 Annex-B 格式起始码

2.2 NALU 头格式:不同

 H.264 NALU Header(1字节):

|F|NRI| Type | --> 共8位

  • F: forbidden_zero_bit (1)

  • NRI: nal_ref_idc (2)

  • Type: nal_unit_type (5)

H.265 NALU Header(2字节):

|F| Type(6bit) | LayerId(6bit) | TID(3bit) |

也就是说,H.265 的 NALU 头是 2 字节,并且类型字段的位置发生了变化!

2.3 RSTP协议中的sdp

m=video 0 RTP/AVP 96
a=rtpmap:96 H265/90000
a=control:track0

三、MPP组件使用

对于MPP组件的使用,rk官方平台有对应相关的介绍,官方github地址。我这里就讲解在应用过程中需要注意的点。
3.1下面是编码器的初始化

主要写入的是原始摄像头的宽高和填充的宽高、图像数据的格式、还有帧率和码率。

    MppEncoderParams mpp_encoder_params;
    memset(&mpp_encoder_params, 0, sizeof(MppEncoderParams));
    mpp_encoder_params.width = WIDTH;
    mpp_encoder_params.height = HEIGHT;
    mpp_encoder_params.hor_stride = 832;//MPP_ALIGN(WIDTH, 16);
    mpp_encoder_params.ver_stride = 512;//MPP_ALIGN(HEIGHT, 16);
    mpp_encoder_params.fmt = MPP_FMT_YUV420SP;

    mpp_encoder_params.rc_mode=MPP_ENC_RC_MODE_CBR;
    mpp_encoder_params.fps_in_flex=0;
    mpp_encoder_params.fps_in_num=30;
    mpp_encoder_params.fps_in_den=1;
    mpp_encoder_params.fps_out_num=30;
    mpp_encoder_params.fps_out_den=1;
    mpp_encoder_params.bps=2000000;
    mpp_encoder_params.bps_max=4000000;

    mpp_encoder_params.type=MPP_VIDEO_CodingHEVC;



    if(mpp_encoder.Init(mpp_encoder_params,NULL))
    {
        fprintf(stderr, "mpp_encoder init failed\n");
    }

上面需要额外注意的点就是水平和垂直的对齐长度,需要是32的整数倍,还要看原始图像的排列方式。我的视频流宽高是800X500。

编码器的输入图像宽高配置需要与图像数据在内存中的排布一致。以1920x1080大小的YUV420图像编码为例,假设有两种情况如下:

上图图情况:亮度分量的宽度为1920,高度为1080,亮度数据与色度数据不直接相接,中间有8行的空行。

这种情况下,水平stride为1920,垂直stride为1088,应用需要以1920*1088*3/2的大小分配空间并写入数据,使用宽1920,高1080,水平stride 1920,垂直stride 1088的配置即可以正常进行编码。

下图情况:亮度分量的宽度为1920,高度为1080,亮度数据与色度数据直接相接,中间没有空行。

这种情况下,水平stride为1920,垂直stride为1080,但由于编码器对数据的访问是16对齐的,在读取亮度下边缘数据时会读取到色度部分,读取色度下边缘数据时会读取到色度数据之外的部分,需要用户开出额外的空间,这里的空间为1920*1080*3/2+1920*4的填充,才能保证编码器不出现访问未分配空间的情况。

如果没有正确对齐的话,视频就会出现花屏的现象。

3.2下面代码初始化编码器头信息,然后是申请数据缓冲区

 //初始化编码器信息头
    char header_buf[1024];
    int header_size = mpp_encoder.GetHeader(header_buf, 1024);
    if(header_size>0)
    {
        queue_buf_deal(consumer_queue,header_buf,header_size);
        printf("header wirte suc %d\n",header_size);
    }
    else 
    {
        fprintf(stderr, "get header failed\n");
        return -1;
    }

    MppBuffer frm_buf=(MppBuffer)mpp_encoder.GetInputFrameBuffer();//申请buf缓冲区

    void * frm_ptr=mpp_encoder.GetInputFrameBufferAddr(frm_buf);
    if(frm_ptr)
    {  
        int frm_size=mpp_encoder.GetFrameSize();
        printf("frame size %d\n",frm_size);
    }
    else
    {
        fprintf(stderr, "get input frame buffer failed\n");
        return -1;
    }

3.3 接下来就是encoder视频数据了,大致流程我是从队列中获取摄像头数据然后进行编码,然后将编码后的数据让放入另一个队列供后续的RTSP推流使用。

这里需要的核心点是对原始NV12摄像头数据的处理,因为原始数据宽高是800X500,但是经过填充后是832X512,所以要按照上面提到的地址对齐。

 Video_frame_context frame_context;
    size_t frame_count = 0;
    const int hor_stride = 832;
    const int ver_stride = 512;
    while(1)
    {
        if(consumer_queue.pop_cam_data(frame_context))//摄像头数据队列
        {
            //printf("copy frame data to input frame buffer index %d\n",i);

            
            uint8_t* dst_y = (uint8_t*)frm_ptr;
            uint8_t* dst_uv = dst_y + hor_stride * ver_stride;
    

            uint8_t* src_y = frame_context.frame_data;
            uint8_t* src_uv = frame_context.frame_data + 800 * 500;

            // Y
            for (int i = 0; i < 500; i++)
                memcpy(dst_y + i * hor_stride, src_y + i * 800, 800);

            // UV
            for (int i = 0; i < 250; i++)
                memcpy(dst_uv + i * hor_stride, src_uv + i * 800, 800);
       
            char enc_buf[2 * 1024 * 1024]; // 每帧最大2MB
            int enc_size = mpp_encoder.Encode(frm_buf, enc_buf, sizeof(enc_buf));
            if (enc_size > 0)
            {
                //编码器数据队列
                queue_buf_deal(consumer_queue,enc_buf,enc_size);
           
                //printf("Encoded frame %d  size: %d\n", frame_count, enc_size);
            }
        }
        else
        {
            perror("pop_cam_data failed\n");
            break;
        }
            

        
    }

三、zlmediakit组件应用

关于zlmediakit的编译和应用,在我其他文章有介绍。zlmediakit组件详解。上面我们已经将摄像头数据进行H265的编码,并且将编码的数据放入了队列当中,接下来我们只需要从队列当中取数据然后进行推流即可。

对于zlmediakit的使用,我们就可以参照demo的方式,只不过将读取文件的流程,改成从队列中读取即可。直接贴上代码,这里需要注意的就是没帧发送的时间,还有就是目前存在两个队列,需要注意两个队列的缓存大小,要做内存防溢出处理。

static int exit_flag = 0;
static void s_on_exit(int sig) {
    exit_flag = 1;
}

static void on_h264_frame(void *user_data, mk_h264_splitter splitter, const char *data, int size) {
#ifdef _WIN32
    Sleep(40);
#else
   // usleep(10 * 1000);
#endif
    uint64_t dts = mk_util_get_current_millisecond();
    mk_frame frame = mk_frame_create(MKCodecH265, dts, dts, data, size, NULL, NULL);
    mk_media_input_frame((mk_media) user_data, frame);
    mk_frame_unref(frame);
}

int rtsp_server(ProducerConsumerQueue &consumerQueue)
{
    /* code */

    char *ini_path = mk_util_get_exe_dir("config.ini");
    mk_config config = {
        .thread_num = 0,
        .log_level = 0,
        .log_mask = LOG_CONSOLE,
        .log_file_path = NULL,
        .log_file_days = 0,
        .ini_is_path = 1,
        .ini = ini_path,
        .ssl_is_path = 1,       
        .ssl = NULL,
        .ssl_pwd = NULL,
            
    };

    mk_env_init(&config);
    mk_free(ini_path);

    mk_rtsp_server_start(554, 0);

    printf("rtsp server start\n");
    mk_events events = {
        .on_mk_media_changed = NULL,
        .on_mk_media_publish = NULL,
        .on_mk_media_play = NULL,
        .on_mk_media_not_found = NULL,
        .on_mk_media_no_reader = NULL,
        .on_mk_http_request = NULL,
        .on_mk_http_access = NULL,
        .on_mk_http_before_access = NULL,
        .on_mk_rtsp_get_realm = NULL,
        .on_mk_rtsp_auth = NULL,
        .on_mk_record_mp4 = NULL,
        .on_mk_shell_login = NULL,
        .on_mk_flow_report = NULL
    };
    mk_events_listen(&events);


    mk_media media = mk_media_create("__defaultVhost__", "live", "test", 0, 0, 0);
    // h264的codec  [AUTO-TRANSLATED:940c6a32]
    // h264 codec
    //mk_media_init_video(media, 0, 0, 0, 0, 2 * 104 * 1024);
    codec_args v_args = {0};
    mk_track v_track = mk_track_create(MKCodecH265, &v_args);
    mk_media_init_track(media, v_track);
    mk_media_init_complete(media);
    mk_track_unref(v_track);

    // 创建h264分帧器  [AUTO-TRANSLATED:5775837d]
    // Create h264 frame splitter
    mk_h264_splitter splitter = mk_h264_splitter_create(on_h264_frame, media,1);
    signal(SIGINT, s_on_exit);// 设置退出信号

    char buf[1024];
    while (!exit_flag) {

        mpp_encoder_context item={0};
        consumerQueue.pop_encoder_data(item) ;
        printf(" pop Encoded frame size: %d\n", item.data_size);
        if (item.data_size > 0) {
            mk_h264_splitter_input_data(splitter, (char *)item.encoder_data, item.data_size );
         }

    }


    mk_h264_splitter_release(splitter);
  
    mk_stop_all_server();

    return 0;

至此我们就完成过了利用V4L2框架读取摄像头数据,使用opencv在本地屏幕显示,使用mpp组件进行H265编码,使用zlmediakit推流,其中涉及的技术框架还是挺多的,需要细细的去熟悉理解。整个工程文件代码已上传。

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值