Windows平台如何实现多路RTSP|RTMP流合成后录像或转发RTMP服务

​技术背景

我们在对接Windows平台RTSP|RTMP直播播放模块的时候,有开发者提出来这样的技术需求,他们做驾考、全景摄像头、多路会议录制等场景的时候,希望把多路视频流数据,合并到一路保存或者对外推送到RTMP服务。

技术实现

多路RTSP|RTMP流合流,实际上我们2016年就有这块demo,当时合流的数据是本地采集的摄像头或屏幕数据,和外部RTSP、RTMP流,合成后输出(类似于传统意义的连麦操作)。这里大概说下思路,外部的RTSP|RTMP流数据,解码后,把YUV或RGB数据回调上来,然后,按照图层的形式,分别贴摄像头、屏幕数据或解码后的流数据。

本次以四路RTSP摄像头数据合流为例:

开始播放:

ini 复制代码
/*
 * SmartPlayerDemo.cs
 * Author: daniusdk.com
 * QQ: 89030985
 */
private void btn_play1_Click(object sender, EventArgs e)
{
	if (btn_play1.Text == "播放")
	{
		String url = textBox_url1.Text;
		if (!InitCommonSDKParam(player1_handle_, url))
		{
			MessageBox.Show("设置参数错误!");
			return;
		}

		bool is_support_d3d_render = false;
		Int32 in_support_d3d_render = 0;

		if (NT.NTBaseCodeDefine.NT_ERC_OK == NTSmartPlayerSDK.NT_SP_IsSupportD3DRender(player1_handle_, playWnd1.Handle, ref in_support_d3d_render))
		{
			if (1 == in_support_d3d_render)
			{
				is_support_d3d_render = true;
			}
		}

		if (is_support_d3d_render)
		{
			// 支持d3d绘制的话,就用D3D绘制
			NTSmartPlayerSDK.NT_SP_SetRenderWindow(player1_handle_, playWnd1.Handle);

			if (btn_check_render_scale_mode.Checked)
				NTSmartPlayerSDK.NT_SP_SetRenderScaleMode(player1_handle_, 1);
			else
				NTSmartPlayerSDK.NT_SP_SetRenderScaleMode(player1_handle_, 0);

		}

		video_frame_call_back1_ = new SP_SDKVideoFrameCallBack(SetVideoFrameCallBack);
		NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(player1_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, IntPtr.Zero, video_frame_call_back1_);

		UInt32 ret_start = NTSmartPlayerSDK.NT_SP_StartPlay(player1_handle_);

		if (ret_start != 0)
		{
			MessageBox.Show("播放失败..");
			return;
		}

		btn_play1.Text = "停止";
	}
	else
	{
		StopPlayback1();
	}
}

其中InitCommonSDKParam()主要完成一些初始化参数设置:

scss 复制代码
private bool InitCommonSDKParam(IntPtr handle, String url) {
	if (IntPtr.Zero == handle)
		return false;

	if (String.IsNullOrEmpty(url))
		return false;

	Int32 buffer_time = int.Parse(textBox_buffer_time.Text);

	NTSmartPlayerSDK.NT_SP_SetBuffer(handle, buffer_time);

	// 设置rtsp tcp模式,rtmp不使用, 可以不设置
	if (checkBox_rtsp_tcp.Checked)
		NTSmartPlayerSDK.NT_SP_SetRTSPTcpMode(handle, 1);
	else
		NTSmartPlayerSDK.NT_SP_SetRTSPTcpMode(handle, 0);

	//RTSP timeout设置
	Int32 rtsp_timeout = 10;
	NTSmartPlayerSDK.NT_SP_SetRtspTimeout(handle, rtsp_timeout);

	//RTSP TCP/UDP自动切换设置
	Int32 is_auto_switch_tcp_udp = 1;
	NTSmartPlayerSDK.NT_SP_SetRtspAutoSwitchTcpUdp(handle, is_auto_switch_tcp_udp);

	if (checkBox_mute.Checked)
		NTSmartPlayerSDK.NT_SP_SetMute(handle, 1);
	else
		NTSmartPlayerSDK.NT_SP_SetMute(handle, 0);

	if (checkBox_fast_startup.Checked)
		NTSmartPlayerSDK.NT_SP_SetFastStartup(handle, 1);
	else
		NTSmartPlayerSDK.NT_SP_SetFastStartup(handle, 0);

	if (checkBox_hardware_decoder.Checked)
	{
		NTSmartPlayerSDK.NT_SP_SetH264HardwareDecoder(handle, is_support_h264_hardware_decoder_ ? 1 : 0, 0);
		NTSmartPlayerSDK.NT_SP_SetH265HardwareDecoder(handle, is_support_h265_hardware_decoder_ ? 1 : 0, 0);
	}
	else
	{
		NTSmartPlayerSDK.NT_SP_SetH264HardwareDecoder(handle, 0, 0);
		NTSmartPlayerSDK.NT_SP_SetH265HardwareDecoder(handle, 0, 0);
	}

	// 设置是否只解码关键帧
	if (btn_check_only_decode_video_key_frame.Checked)
		NTSmartPlayerSDK.NT_SP_SetOnlyDecodeVideoKeyFrame(handle, 1);
	else
		NTSmartPlayerSDK.NT_SP_SetOnlyDecodeVideoKeyFrame(handle, 0);

	// 设置低延迟模式
	if (checkBox_low_latency.Checked)
		NTSmartPlayerSDK.NT_SP_SetLowLatencyMode(handle, 1);
	else
		NTSmartPlayerSDK.NT_SP_SetLowLatencyMode(handle, 0);

	NTSmartPlayerSDK.NT_SP_SetRotation(handle, rotate_degrees_);

	NTSmartPlayerSDK.NT_SP_SetAudioVolume(handle, slider_audio_volume.Value);

	NTSmartPlayerSDK.NT_SP_SetReportDownloadSpeed(handle, 1, 1);

	NTSmartPlayerSDK.NT_SP_SetURL(handle, url);

	return true;
}

开始播放之前,我们设置YUV数据回调:

ini 复制代码
video_frame_call_back1_ = new SP_SDKVideoFrameCallBack(SetVideoFrameCallBack);
NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(player1_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, IntPtr.Zero, video_frame_call_back1_);

回调处理如下,如果是多个图层,通过推送端,把yuv或rgb数据,投递给推送端,video frame数据回调,可以根据handle区分不同的图层或实例:

ini 复制代码
public void SetVideoFrameCallBack(IntPtr handle, IntPtr userData, UInt32 status, IntPtr frame)
{
	if (frame == IntPtr.Zero)
		return;

	NT_SP_VideoFrame video_frame = (NT_SP_VideoFrame)Marshal.PtrToStructure(frame, typeof(NT_SP_VideoFrame));

	if (publisher_wrapper_ != null) {

		int video_layer_index;
		if (handle == player_handle_)
			video_layer_index = publisher_wrapper_.get_external_video_layer0_index();
		else if (handle == player1_handle_)
			video_layer_index = publisher_wrapper_.get_external_video_layer1_index();
		else if (handle == player2_handle_)
			video_layer_index = publisher_wrapper_.get_external_video_layer2_index();
		else if (handle == player3_handle_)
			video_layer_index = publisher_wrapper_.get_external_video_layer3_index();
		else
			video_layer_index = -1;

		if (video_layer_index > -1)
		{
			publisher_wrapper_.post_i420_layer_image(video_layer_index, video_frame.plane0_, video_frame.stride0_, video_frame.plane1_, video_frame.stride1_,
				  video_frame.plane2_, video_frame.stride2_,
				  video_frame.width_, video_frame.height_);
		}
	}

}

推送端,目前以四路合成为例,另外加个实时文字水印,图层设计如下:

ini 复制代码
public bool config_layers(bool is_add_rgbx_zero_layer)
{
	if (video_option_ != (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_LAYER)
		return false;

	if (is_empty_handle())
		return false;

	int w = video_width_;
	int h = video_height_;

	if ((w & 0x1) != 0)
		--w;

	if ((h & 0x1) != 0)
		--h;

	if (w < 2 || h < 2)
		return false;

	NTSmartPublisherSDK.NT_PB_ClearLayersConfig(handle_, 0, 0, IntPtr.Zero);

	int type, index = 0;
	if (is_add_rgbx_zero_layer)
	{
		NT_PB_RGBARectangleLayerConfig rgba_layer = new NT_PB_RGBARectangleLayerConfig();
		type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_RGBA_RECTANGLE;
		fill_layer_base(rgba_layer, out rgba_layer.base_, type, index, true, 0, 0, w, h);
		rgba_layer.red_ = 0;
		rgba_layer.green_ = 0;
		rgba_layer.blue_ = 0;
		rgba_layer.alpha_ = 255;
		if (add_layer_config(rgba_layer, type))
			index++;
	}

	NT_PB_ExternalVideoFrameLayerConfig external_video_layer0 = new NT_PB_ExternalVideoFrameLayerConfig();
	type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
	fill_layer_base(external_video_layer0, out external_video_layer0.base_, type, index, true, 0, 0, w/2, h/2);
	if (add_layer_config(external_video_layer0, type))
		external_video_layer0_index_ = index++;

	NT_PB_ExternalVideoFrameLayerConfig external_video_layer1 = new NT_PB_ExternalVideoFrameLayerConfig();
	type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
	fill_layer_base(external_video_layer1, out external_video_layer1.base_, type, index, true, w / 2, 0, w / 2, h / 2);
	if (add_layer_config(external_video_layer1, type))
		external_video_layer1_index_ = index++;

	NT_PB_ExternalVideoFrameLayerConfig external_video_layer2 = new NT_PB_ExternalVideoFrameLayerConfig();
	type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
	fill_layer_base(external_video_layer2, out external_video_layer2.base_, type, index, true, 0, h / 2, w / 2, h / 2);
	if (add_layer_config(external_video_layer2, type))
		external_video_layer2_index_ = index++;

	NT_PB_ExternalVideoFrameLayerConfig external_video_layer3 = new NT_PB_ExternalVideoFrameLayerConfig();
	type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
	fill_layer_base(external_video_layer3, out external_video_layer3.base_, type, index, true, w / 2, h / 2, w / 2, h / 2);
	if (add_layer_config(external_video_layer3, type))
		external_video_layer3_index_ = index++;


	//叠加的文本层
	NT_PB_ExternalVideoFrameLayerConfig text_layer = new NT_PB_ExternalVideoFrameLayerConfig();
	type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
	fill_layer_base(text_layer, out text_layer.base_, type, index, false, w / 2, h / 2, 64, 64);
	if (add_layer_config(text_layer, type))
		text_layer_index_ = index++;

	return index > 0;
}

合成后数据,可以对外推送到RTMP服务,也可以注入到本地RTSP服务,或者本地直接录制MP4文件,录制出来四宫格效果如下:

总结

多路RTSP|RTMP数据合流,在多媒体处理、实时监控、驾考、教育等各个行业,应用非常广泛,除了视频外,音频如果需要合成,可以以采集系统扬声器的形式合流出来。多路合流,可以事先做好排版编辑,如果期间不希望显示某一路数据,可以点隐藏图层,实时对图层进行隐藏。感兴趣的开发者,可以单独跟我沟通交流。

相关推荐
音视频牛哥3 小时前
从「行走」到「思考」:机器人进化之路与感知—决策链路的工程化实践
机器学习·机器人·音视频开发
chenchao_shenzhen3 天前
RK3568嵌入式音视频硬件编解码4K 60帧 rkmpp FFmpeg7.1 音视频开发
ffmpeg·音视频·rk3588·音视频开发·嵌入式开发·瑞芯微rk3568·硬件编解码
码流怪侠5 天前
Google SoundStream音频编解码器技术解析
深度学习·音视频开发
字节跳动视频云技术团队6 天前
基于 DiT 大模型与字体级分割的视频字幕无痕擦除方案,助力短剧出海
aigc·音视频开发·视频编码
音视频牛哥7 天前
跨平台轻量级RTSP服务模块技术详解与内网低延迟直播实践
音视频开发·视频编码·直播
aqi008 天前
FFmpeg开发笔记(八十)使用百变魔音AiSound实现变声特效
android·ffmpeg·音视频·直播·流媒体
aqi009 天前
FFmpeg开发笔记(七十九)专注于视频弹幕功能的国产弹弹播放器
android·ffmpeg·音视频·直播·流媒体
音视频牛哥12 天前
SmartMediaKit 模块化音视频框架实战指南:场景链路 + 能力矩阵全解析
音视频开发·视频编码·直播
子龙_12 天前
JS解析wav音频数据并使用wasm加速
前端·javascript·音视频开发