欢迎光临
我们一直在努力

Hi3516CV100 RTSP 视频推流实操

目录

一、基础驱动的加载

二、运行 sample_rtsp.c

1、sample_venc_online_wrap_start_sys_vi_vpss

2、sample_venc_normal_start_encode

3、sample_easyvenc_comm_venc_start_get_stream

三、总结


一、基础驱动的加载

在 Hi3516CV610 SDK 中,smp 目录主要存放芯片厂商提供的 MPP、驱动、sample 示例程序等源码;open_source 目录主要存放第三方开源组件。因此,像 pin 复用、时钟配置、sensor 接口类型、VI/MIPI 接口模式 等底层硬件配置,一般不会放在 open_source 中处理,而是在 smp 目录下的 interdrv/sysconfig 模块中完成。

本工程中对应源码路径为:

smp/a7_linux/source/interdrv/sysconfig/

其中,sys_cfg.c、pin_mux.c、clk_cfg.c 等文件会一起编译生成内核模块:

sys_config.ko

加载基础驱动时,脚本:

smp/a7_linux/source/out/ko/load3516cv610/load3516cv610_20g_debug

会先设置 sensor、board、ir_auto 等参数,例如:

SNS_TYPE0=sc4336p
SNS_TYPE1=sc4336p
BOARD=dmeb_qfn
IR_AUTO=0

随后在 insert_ko() 中优先加载 sys_config.ko:

insmod sys_config.ko sensors=sns0=$SNS_TYPE0,sns1=$SNS_TYPE1 board=$BOARD ir_auto=$IR_AUTO

这里将 sensor 类型传递给 sys_config.ko,是为了让 sys_config.ko 根据 sensor 名称完成对应的底层硬件配置。例如,当 sensor 类型为 sc4336p 时,系统会识别它为 MIPI sensor,并配置对应的 sensor MCLK、VI/MIPI 接口模式以及相关 pinmux。sys_config.ko 完成基础硬件配置后,加载脚本会继续加载 MPP 相关内核模块,例如:

ot_osal.ko
ot_mmz.ko
ot_vb.ko
ot_sys.ko
ot_vi.ko
ot_isp.ko
ot_vpss.ko
ot_venc.ko
ot_mipi_rx.ko

需要注意的是,sys_config.ko 主要负责 sensor 相关的底层时钟、pinmux、复位、电源控制以及 VI/MIPI 接口类型配置。它并不负责真正初始化 sc4336p 的寄存器。

真正的 sensor 初始化会在后续运行 sample 程序时完成。用户态 sample 会根据 SENSOR0_TYPE 选择对应的 sensor 库,例如:

libsns_sc4336p

然后完成 sensor 注册、I2C 配置、寄存器初始化、AE/AWB 回调注册以及 ISP 绑定等工作。

二、运行 sample_rtsp.c

程序启动后,main() 里先调用:create_rtsp_demo(554)。实际内部会创建一个 RTSP server,在 554 端口上创建 TCP socket、bind、listen,用来等待客户端通过 RTSP 连接访问。

随后调用:create_rtsp_session(g_rtsplive, "/test.264"),创建一个 RTSP session。这个 session 对应一个访问路径,也就是客户端可以通过:rtsp://板端IP/test.264 来拉流。

create_rtsp_session() 内部会先创建 session,然后调用 rtsp_set_video() 设置该 session 的视频参数。当前代码默认设置为 H.264:

s->vcodec_id = codec_id;
用于记录当前 RTSP session 的视频编码格式,例如 H.264 或 H.265。

s->vrtpe.pt = VRTP_PT_ID;
设置 RTP 包头中的 Payload Type。这里 VRTP_PT_ID 为 96,属于动态负载类型,常用于 H.264/H.265 这类视频流。

s->vrtpe.seq = 0;
初始化 RTP 包序号,后续每封装一个 RTP 包都会递增。

s->vrtpe.ssrc = 0;
这里只是初始化 RTP encoder 里的 SSRC 字段。真正发送给每个客户端的 SSRC 会在客户端 SETUP 时生成,
并在发送 RTP 包前写入包头,因此不同客户端可以有自己的 RTP 连接状态。

s->vrtpe.sample_rate = 90000;
设置视频 RTP 时间戳频率。视频 RTP 通常使用 90000Hz,后续会根据 VENC 输出的 pts 换算 RTP timestamp。

memset(&s->vcodec_data, 0, sizeof(s->vcodec_data));
清空 session 中缓存的编码参数集信息,例如 H.264 的 SPS/PPS,或者 H.265 的 VPS/SPS/PPS。

如果用户传入了 codec_data,则会根据编码格式调用:
rtsp_codec_data_parse_from_user_h264() 解析 H.264 的 SPS/PPS;
rtsp_codec_data_parse_from_user_h265() 解析 H.265 的 VPS/SPS/PPS。
但当前 sample 传入的是 NULL,所以这里不会从用户 codec_data 中解析参数集。
后续 rtsp_sever_tx_video() 发送真实码流时,会再尝试从帧数据中解析 SPS/PPS。

s->vstreamq = streamq_alloc(RTP_MAX_PKTSIZ, VRTP_MAX_NBPKTS * 2 + 1);
创建视频 RTP 包缓存队列。VENC 输出的 H.264/H.265 NALU 会先被封装成 RTP 包,
然后暂存在这个队列中,再按照当前已经 PLAY 的客户端连接发送出去。

后面 VENC 获取到编码流后,sample_rtsp.c 中调用 rtsp_sever_tx_video(),
把每个 H.264 NALU 按 RTP 格式封装并发送给 RTSP 客户端。

1、sample_venc_online_wrap_start_sys_vi_vpss

sample_venc_online_wrap_start_sys_vi_vpss(sns_type, &vi_cfg, &vpss_cfg, &enc_param) 这个函数的主要作用是:根据 sns_type 获取并配置默认的 vi_cfg、vpss_cfg 和 enc_param,然后完成 SYS、VI、VPSS 的初始化和启动,最后把 VI 输出绑定到 VPSS 输入。

函数开始后,会先通过 sample_venc_online_wrap_get_default_sys_cfg() 设置系统工作模式。当前使用的是 OT_VI_ONLINE_VPSS_ONLINE 模式,表示 VI 和 VPSS 采用在线工作方式,同时默认打开 VPSS wrap。然后调用:

sample_comm_vi_get_default_vi_cfg(sns_type, vi_cfg)

根据 sensor 类型生成默认的 VI 配置。

这个 VI 配置主要包括 sensor 信息、MIPI 信息、VI dev 信息、dev 绑定的 pipe 信息、grp 信息,以及 pipe/channel 信息。后续 sample_venc_vi_init() 会继续调用 sample_comm_vi_start_vi(),完成 VI 的真正启动。

在 VI 启动过程中,会遍历创建 VI pipe。这里主要决定哪些 pipe 需要启动 ISP,以及 pipe 和 VI channel 之间的输出关系,例如把 pipe0 绑定到 VI channel0 输出。也就是说,VI 部分的核心作用是把 sensor 采集到的图像接入进来,并通过 pipe/channel 形成后续可以使用的视频数据流。

如果当前配置需要启动 VI wrap,则还会根据图像类型、图像分辨率等信息,计算一行图像数据所需要的内存大小,也就是 wrap 模式下需要使用的行缓存大小。之后,函数会通过:

sample_venc_vpss_get_wrap_cfg()

根据 sensor 类型判断是否支持 VPSS wrap。

如果当前 sensor 支持 VPSS wrap,就会根据 VPSS channel 属性、VI/VPSS 在线模式,以及 sensor 的 full_lines_std 等信息,计算 VPSS wrap 所需的 buf_line 和 buf_size。如果当前 sensor 类型不在支持列表中,则会关闭 VPSS wrap。

vpss_cfg 主要用于配置 VPSS 的处理组,例如默认使用 VPSS grp0。同时还会配置 VPSS channel 的数量和输出方式,例如有多少个 VPSS channel 用于裁剪、缩放或者输出不同分辨率的视频流。

如果 VPSS 也需要启动 wrap,同样会根据图像类型、图像分辨率等信息计算 wrap 缓冲区大小,用于 VPSS 在线处理时的行缓存管理。接着调用:

sample_venc_online_wrap_sys_init()

初始化系统 VB 池。VB 池大小会根据 enc_param 中的小码流输出尺寸,以及 VPSS wrap 计算出的 buf_size 来分配。同时,该函数还会设置 VI-VPSS 的工作模式为:

OT_VI_ONLINE_VPSS_ONLINE

SYS 初始化完成后,函数会依次启动 VI 和 VPSS。最后调用:

sample_comm_vi_bind_vpss(vi_pipe, 0, vpss_grp, 0)

把 VI pipe0 的 VI channel0 绑定到 VPSS grp0 的 VPSS channel0。

这样,sensor 采集到的图像会先进入 VI,再由 VI 输出到 VPSS,最后由 VPSS 进行缩放、裁剪、格式处理等操作,为后续编码做准备。

需要注意的是,sample_venc_online_wrap_start_sys_vi_vpss() 这个函数只负责启动 SYS、VI、VPSS,并完成 VI 到 VPSS 的绑定,并不会创建 VENC 编码通道。

真正的编码通道创建、编码格式设置、帧率设置、GOP 设置、码率控制参数设置,以及 VPSS channel 绑定到 VENC channel,是后面的:

sample_venc_normal_start_encode()

完成的。

2、sample_venc_normal_start_encode

sample_venc_normal_start_encode() 的主要作用是配置并启动 VENC 编码通道,然后把 VPSS channel 绑定到对应的 VENC channel,最后启动取流线程。需要注意的是,这个函数不会根据摄像头类型生成 enc_param。在调用它之前,enc_param.enc_size[] 已经准备好了。该函数只是根据传入的 enc_size、vpss_grp 和 venc_vpss_chn 来配置编码通道。

函数开始后,先设置 GOP 模式为 OT_VENC_GOP_MODE_NORMAL_P,并通过 sample_comm_venc_get_gop_attr() 获取 GOP 属性。然后调用 sample_venc_set_video_param() 配置两个 VENC 通道:

VENC chn0:H.265 编码,用于大码流
VENC chn1:H.264 编码,用于小码流

每个编码通道都会配置帧率、GOP、码率控制模式、编码分辨率、profile 等参数。例如当前代码中帧率是 30fps,GOP 是 60,码率控制模式是 CBR。

参数配置完成后,函数调用 sample_comm_venc_mini_buf_en() 开启 VENC mini buffer 相关配置。

接着启动两个编码通道,并完成绑定关系:

VPSS grp0 chn0 -> VENC chn0
VPSS grp0 chn1 -> VENC chn1

这样,VPSS 输出的视频帧就会进入对应的 VENC 通道进行编码。最后调用 sample_easyvenc_comm_venc_start_get_stream() 启动取流线程,从 VENC 通道获取编码后的码流。在当前 sample_rtsp.c 中,取流流程里会把 VENC chn1 得到的 H.264 小码流通过 RTSP 发送出去。

3、sample_easyvenc_comm_venc_start_get_stream

sample_easyvenc_comm_venc_start_get_stream() 的主要作用是启动 VENC 取流线程。前面已经通过 sample_comm_venc_start() 创建并启动了 VENC 编码通道,所以这里需要再创建一个线程,专门从 VENC 通道中取出编码后的码流。

该函数会先把需要取流的 VENC 通道号保存到全局参数 g_param 中,然后通过 pthread_create() 创建取流线程:sample_easyvenc_comm_venc_get_venc_stream_proc。这个线程才是真正负责取流的地方。

线程启动后,会先调用 sample_easyvenc_comm_set_name_save_stream(),获取每个 VENC 通道的信息,例如编码类型、文件后缀、VENC fd、stream buffer 信息等,并保存到 stream_proc_info 中。其中:

payload_type:记录当前通道是 H.264、H.265 还是 JPEG
venc_fd:用于配合 select() 判断该通道是否有码流可读

准备完成后,线程进入 while 循环。每次循环都会把各个 VENC 通道的 fd 加入 fd_set,然后调用 select() 等待 VENC 通道产生数据。

如果 select() 检测到某个通道有数据,就调用 sample_easyvenc_comm_fd_isset() 判断具体是哪一个通道可读,然后再调用 sample_easyvenc_comm_get_stream_from_one_channl() 从该通道取出码流。

sample_easyvenc_comm_get_stream_from_one_channl() 内部会先调用 ss_mpi_venc_query_status() 查询当前帧有多少个 pack,然后申请 stream.pack 内存,接着调用 ss_mpi_venc_get_stream() 获取编码后的码流。

取到码流后,会调用 sample_easyvenc_comm_save_frame_to_file() 处理这一帧数据。

在原始 sample 中,这个函数通常是把码流保存到本地文件;如果是 JPEG,就会生成 JPEG 文件名并保存图片。

但在当前 sample_rtsp.c 中,普通文件保存逻辑被注释掉了,改成了 RTSP 发送逻辑。当前代码只处理 index == 1 的通道,也就是 VENC chn1。这个通道前面被配置成 H.264 小码流。函数会遍历 stream->pack[],跳过 H.264 的 SEI 数据,然后取出每个 pack 的有效码流地址、长度和 PTS

pStremData = stream->pack[j].addr + stream->pack[j].offset;
nSize = stream->pack[j].len – stream->pack[j].offset;
pts = stream->pack[j].pts;

然后调用:

rtsp_sever_tx_video(g_rtsplive, session, pStremData, nSize, stream->pack[j].pts);

把 H.264 小码流送给 RTSP 发送函数。

rtsp_sever_tx_video() 后续会把 H.264 码流解析成 NALU,再封装成 RTP 包,并发送给所有处于 PLAYING 状态的 RTSP 客户端。整体流程可以简化理解为:

启动 VENC 编码通道

启动取流线程

select() 监听 VENC fd

发现 VENC 通道有码流

ss_mpi_venc_get_stream() 取出编码流

当前 sample_rtsp.c 中把 VENC chn1 的 H.264 小码流通过 RTSP 发出去

所以,sample_easyvenc_comm_venc_start_get_stream() 的核心作用就是:

启动一个取流线程,
不断从 VENC 通道取出编码后的码流,
然后根据代码逻辑保存到文件或通过 RTSP 发送出去。

三、总结

RTSP 出流的主要流程如下:

第一步,先加载 MPP 相关基础驱动:./load3516cv610_20g_debug -i 。该脚本会加载运行 MPP sample 所需的内核模块,例如 sys_config.ko、ot_sys.ko、sensor I2C 驱动等。驱动加载完成后,用户态 sample 才能调用 VI、VPSS、VENC 等 MPI 接口。

第二步,运行 ./sample_rtsp

sample_rtsp 启动后,会先创建 RTSP server 和 RTSP session。RTSP server 监听 554 端口,session 对应访问路径 /test.264,客户端可以通过下面的地址拉流:rtsp://板端IP/test.264

然后调用 sample_venc_online_wrap_start_sys_vi_vpss(),完成 SYS、VI、VPSS 的配置和启动。该函数会根据 sensor 类型生成默认 VI 配置,启动 MIPI、VI dev、VI pipe、ISP 等模块,同时配置 VPSS grp0 和对应的 VPSS channel,并根据 sensor 类型判断是否开启 VPSS wrap。最后把 VI pipe0 的 VI channel0 绑定到 VPSS grp0 的 VPSS channel0。

接着调用 sample_venc_normal_start_encode(),配置并启动 VENC 编码通道。当前代码中会启动两路编码:

VPSS chn0 -> VENC chn0 -> H.265 大码流
VPSS chn1 -> VENC chn1 -> H.264 小码流

该函数会配置编码类型、分辨率、帧率、GOP、码率控制模式等参数,然后启动 VENC channel,并完成 VPSS channel 到 VENC channel 的绑定。

然后调用 sample_easyvenc_comm_venc_start_get_stream() 启动 VENC 取流线程。取流线程会获取各个 VENC 通道的属性、编码类型、VENC fd 和 stream buffer 信息,然后通过 select() 监听各个 VENC fd。当某个 VENC 通道有编码数据时,就调用 ss_mpi_venc_get_stream() 从 VENC 通道取出编码后的码流。

取到码流后,会进入 sample_easyvenc_comm_save_frame_to_file()。在原始 sample 中,这个函数通常用于保存码流文件;但在当前 sample_rtsp.c 中,文件保存逻辑被改成了 RTSP 发送逻辑。

当前代码只处理 index == 1 的通道,也就是 VENC chn1 的 H.264 小码流。它会遍历 stream->pack[],跳过 SEI 数据,然后取出有效码流地址、长度和 PTS

有效地址 = pack.addr + pack.offset
有效长度 = pack.len – pack.offset
时间戳 = pack.pts

最后调用:rtsp_sever_tx_video(g_rtsplive, session, data, size, pts)。rtsp_sever_tx_video() 会把 H.264 码流解析成 NALU,封装成 RTP 包,并发送给所有处于 PLAYING 状态的 RTSP 客户端。

四、补充

本博客在 RTSP 示例中增加了时间 OSD 功能。

OSD 简单来说,就是在 VENC 编码通道上叠加一块可动态更新的图像区域,用于显示时间、水印等信息。实现时,首先配置 rgn_attr,用于描述 OSD 区域本身的属性,包括区域类型 OT_RGN_OVERLAY、像素格式、区域宽高、背景色以及调色板颜色等。配置完成后,调用:

ss_mpi_rgn_create(TIME_OSD_HANDLE, &rgn_attr)

创建一个 RGN 区域。

随后配置 g_time_osd_chn 和 chn_attr。其中,g_time_osd_chn 用来指定 OSD 要绑定到哪个模块和通道。本示例中,OSD 被绑定到 VENC 编码通道上。chn_attr 用来配置 OSD 在编码画面中的显示属性,例如是否显示、叠加位置、前景透明度、背景透明度以及图层等。配置完成后,调用:

ss_mpi_rgn_attach_to_chn(TIME_OSD_HANDLE, &g_time_osd_chn, &chn_attr)

将创建好的 OSD 区域挂载到 VENC 编码通道上。

OSD 内容的更新由 sample_rtsp_time_osd_update() 完成。该函数会先获取当前系统时间,然后将时间字符串绘制到 g_time_osd_bitmap 缓冲区中,最后调用:

ss_mpi_rgn_set_bmp(TIME_OSD_HANDLE, &bmp)

把新的 bitmap 内容刷新到 OSD 区域。

为了让时间持续更新,程序还创建了 sample_rtsp_time_osd_proc 线程。该线程每隔 1 秒调用一次 sample_rtsp_time_osd_update(),从而实现编码画面中时间 OSD 的动态刷新。

main()
├─ sample_sys_signal()
├─ create_rtsp_demo(554)
├─ create_rtsp_session("/test.264")
└─ sample_venc_normal()
├─ sample_venc_get_enc_size()
├─ sample_set_venc_vpss_chn()
│ ├─ vpss_chn[0] -> venc_chn[0]
│ └─ vpss_chn[1] -> venc_chn[1] <– RTSP 实际发送这一路
├─ sample_venc_online_wrap_start_sys_vi_vpss()
│ ├─ 初始化 VB/SYS
│ ├─ 启动 VI
│ ├─ 启动 VPSS
│ └─ VI bind VPSS
└─ sample_venc_normal_start_encode()
├─ 启动 VENC chn0
├─ VPSS chn0 bind VENC chn0
├─ 启动 VENC chn1
├─ VPSS chn1 bind VENC chn1
├─ sample_rtsp_start_time_osd(venc_chn[1], enc_size[1])
│ ├─ 配置 RGN 属性
│ │ ├─ type = OT_RGN_OVERLAY
│ │ ├─ pixel_format = ARGB_CLUT4
│ │ ├─ size = 384 x 32
│ │ ├─ clut[0] = 透明
│ │ └─ clut[1] = 白色
│ ├─ ss_mpi_rgn_create(TIME_OSD_HANDLE)
│ ├─ 计算 OSD 居中坐标
│ │ ├─ x = (图像宽 – OSD宽) / 2
│ │ └─ y = (图像高 – OSD高) / 2
│ ├─ 配置通道显示属性
│ │ ├─ is_show = TD_TRUE
│ │ ├─ attach target = OT_ID_VENC / chn1
│ │ ├─ fg_alpha = 255
│ │ └─ bg_alpha = 0
│ ├─ ss_mpi_rgn_attach_to_chn()
│ ├─ sample_rtsp_time_osd_update()
│ │ ├─ 获取当前系统时间
│ │ ├─ 格式化为 YYYY-MM-DD HH:MM:SS
│ │ ├─ 清空 g_time_osd_bitmap
│ │ ├─ 按点阵字体画入 bitmap
│ │ └─ ss_mpi_rgn_set_bmp()
│ └─ 创建 time_osd 线程
│ └─ 每 1 秒重复 sample_rtsp_time_osd_update()
└─ sample_easyvenc_comm_venc_start_get_stream()
└─ 获取 VENC chn1 码流并通过 RTSP 发送

效果展示:

赞(0)
未经允许不得转载:171主机测评 » Hi3516CV100 RTSP 视频推流实操
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址