Windows平台C#版RTSP转RTMP直播推送定制版

710 阅读5分钟

​技术背景

前几年我们发布了C++版的多路RTMP/RTSP转RTMP转发官方定制版。在秉承低延迟、灵活稳定、低资源占用的前提下,客户无需关注开发细节,只需图形化配置转发等各类参数,实现产品快速上线目的。

如监控类摄像机、NVR等,通过厂商说明或Onvif工具,获取拉流的RTSP地址,图形化配置,完成拉流转发等操作,轻松实现标准RTMP服务器对接。

视频转发支持H.264、H.265(需要RTMP服务器或CDN支持扩展H.265),音频支持配置PCMA/PCMU转AAC后转发,并支持只转发/录制视频或音频,RTSP拉流端支持鉴权和TCP/UDP模式设置和TCP/UDP模式自动切换,整个拉流、转发模块都有非常完善的自动重连机制。

运维方面,官方定制版转发系统支持7*24小时不间断运行,自带守护进程,转发程序被误关等各种操作后,会自动启动运行,此外,还支持开机自动启动转发或录像。

技术实现

随着开发者不同的技术诉求,好多公司都是基于我们C#的demo进一步开发,本次demo,我们在原有C#的转发程序的基础上,稍作调整,实现了开机自启动、推拉流xml配置、实时预览和自动转发操作:

​编辑

开机自启动

开机自启动,是好多开发者做rtsp转rtmp程序的时候,比较关注的功能。简单的实现如下:

private void SetAutoStart(bool is_auto_start)
{
	try
	{
		string exePath = Assembly.GetExecutingAssembly().Location;
		string name = Path.GetFileNameWithoutExtension(exePath);

		bool exist = false;

		using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true))
		{
			if (key != null)
			{
				string[] valueNames = key.GetValueNames();
				foreach (string valueName in valueNames)
				{
					string valueData = key.GetValue(valueName).ToString();
					if (valueData.Contains(exePath))
					{
						exist = true;
						break;
					}
				}

				if (exist)
				{

					if (!is_auto_start)
					{
						key.DeleteValue(name, false);
					}

					return;
				}

				if (is_auto_start)
				{
					key.SetValue(name, exePath);
				}
			}
		}
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
}

通过配置xml的形式,程序启动后,从configure.xml读取相关的参数,实现一键拉流转发。

常规的参数配置,比如推拉流的rtsp rtmp url,如果需要自采集audio,设置采集的audio类型,比如rtsp自带audio、麦克风、扬声器或麦克风扬声器混音。

<?xml version="1.0" encoding="utf-8" ?>
<StreamRelays>
  <Relay>
    <id>0</id>
	<AudioOption>4</AudioOption>
    <PullUrl>rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream00</PushUrl>
  </Relay>
  <Relay>
    <id>1</id>
	<AudioOption>1</AudioOption>
    <PullUrl>rtsp://admin:admin123456@192.168.0.121:554/cam/realmonitor?channel=1<![CDATA[&]]>subtype=0</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream01</PushUrl>
  </Relay>
  <Relay>
    <id>2</id>
	<AudioOption>3</AudioOption>
    <PullUrl>rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream02</PushUrl>
  </Relay>
  <Relay>
    <id>3</id>
	<AudioOption>3</AudioOption>
    <PullUrl>rtsp://admin:admin123456@192.168.0.121:554/cam/realmonitor?channel=1<![CDATA[&]]>subtype=0</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream03</PushUrl>
  </Relay>
    <Relay>
    <id>4</id>
	<AudioOption>4</AudioOption>
    <PullUrl>rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream04</PushUrl>
  </Relay>
  <Relay>
    <id>5</id>
	<AudioOption>1</AudioOption>
    <PullUrl>rtsp://admin:admin123456@192.168.0.121:554/cam/realmonitor?channel=1<![CDATA[&]]>subtype=0</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream05</PushUrl>
  </Relay>
  <Relay>
    <id>6</id>
	<AudioOption>4</AudioOption>
    <PullUrl>rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream06</PushUrl>
  </Relay>
  <Relay>
    <id>7</id>
	<AudioOption>2</AudioOption>
    <PullUrl>rtsp://admin:admin123456@192.168.0.121:554/cam/realmonitor?channel=1<![CDATA[&]]>subtype=0</PullUrl>
    <PushUrl>rtmp://192.168.0.103:1935/hls/stream07</PushUrl>
  </Relay>
</StreamRelays>

简单的读取代码如下:

private void GetXmlConfigure()
{
	List<StreamRelayConfig> streamRelayConfigList = new List<StreamRelayConfig>();
	
	try {
		XmlDocument xmlDoc = new XmlDocument();
		xmlDoc.Load(AppDomain.CurrentDomain.BaseDirectory + @"configure.xml");

		XmlNode rootNode = xmlDoc.SelectSingleNode("StreamRelays");

		XmlNodeList streamRelayNodeList = rootNode.ChildNodes;

		foreach (XmlNode skNode in streamRelayNodeList)
		{
			StreamRelayConfig streamRelayConfig = new StreamRelayConfig();
			XmlNodeList fileNodeList = skNode.ChildNodes;
			foreach (XmlNode fileNode in fileNodeList)
			{
				if (fileNode.Name == "id")
				{
					int id = Int32.Parse(fileNode.InnerText);
					streamRelayConfig.Id = id;
				}
				if (fileNode.Name == "AudioOption") 
				{
					int audio_option = Int32.Parse(fileNode.InnerText);
					streamRelayConfig.AudioOption = audio_option;
				}
				else if (fileNode.Name == "PullUrl")
				{
					streamRelayConfig.PullUrl = fileNode.InnerText; 
				}
				else if (fileNode.Name == "PushUrl")
				{
					streamRelayConfig.PushUrl = fileNode.InnerText;
				}
			}

			streamRelayConfigList.Add(streamRelayConfig);
		}

	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.ToString());
	}

	int i = 0;

	stream_relay_instance_count_ = streamRelayConfigList.Count();

	foreach (StreamRelayConfig steamrelay in streamRelayConfigList)
	{
		stream_relay_config_[i].AudioOption = steamrelay.AudioOption;
		stream_relay_config_[i].PullUrl = steamrelay.PullUrl;
		stream_relay_config_[i].PushUrl = steamrelay.PushUrl;
		lable_audio_option_[i].Text = ConvertAudioOption(steamrelay.AudioOption);
		Console.WriteLine(steamrelay);
		i++;
	}
}

如果需要预览,直接点预览按钮即可。

大概的封装实现如下:

/*
 * nt_relay_wrapper.cs.cs
 * nt_relay_wrapper.cs
 * 
 * WebSite: https://daniusdk.com
 * 
 * Created by DaniuLive on 2017/11/14.
 * Copyright © 2014~2024 DaniuLive. All rights reserved.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SmartRelayDemo
{
    class nt_relay_wrapper
    {
        int relay_index_;
        nt_player_wrapper player_wrapper_;
        nt_publisher_wrapper publisher_wrapper_;

        UInt32 video_option_ = (UInt32)NT.NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_ENCODED_DATA;
        UInt32 audio_option_ = (UInt32)NT.NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_ENCODED_DATA;

        public nt_player_wrapper GetPlayerWrapper() { return player_wrapper_; }
        public nt_publisher_wrapper GetPublisherWrapper() { return publisher_wrapper_; }

        public nt_relay_wrapper(int index, System.Windows.Forms.Control render_wnd, System.ComponentModel.ISynchronizeInvoke sync_invoke)
        {
            relay_index_ = index;
            player_wrapper_ = new nt_player_wrapper(index, render_wnd, sync_invoke);
            publisher_wrapper_ = new nt_publisher_wrapper(index, render_wnd, sync_invoke);
        }

        ~nt_relay_wrapper() { }

        private void OnVideoDataHandle(IntPtr handle, IntPtr user_data,
            UInt32 video_codec_id, IntPtr data, UInt32 size,
            IntPtr info, IntPtr reserve)
        {
            if (publisher_wrapper_.is_rtmp_publishing())
            {
                publisher_wrapper_.OnVideoDataHandle(handle, user_data, video_codec_id, data, size, info, reserve);
            }
        }

        private void OnAudioDataHandle(IntPtr handle, IntPtr user_data,
            UInt32 audio_codec_id, IntPtr data, UInt32 size,
            IntPtr info, IntPtr reserve)
        {
            if (publisher_wrapper_.is_rtmp_publishing())
            {
                publisher_wrapper_.OnAudioDataHandle(handle, user_data, audio_codec_id, data, size, info, reserve);
            }
        }

        public void StartPull(String url)
        {
            if (!player_wrapper_.is_pulling())
            {
                player_wrapper_.SetBuffer(0);

                if (!player_wrapper_.StartPull(url, false))
                    return;

                player_wrapper_.EventOnVideoDataHandle += new nt_player_wrapper.DelOnVideoDataHandle(OnVideoDataHandle);

                if (audio_option_ == (UInt32)NT.NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_ENCODED_DATA)
                {
                    player_wrapper_.EventOnAudioDataHandle += new nt_player_wrapper.DelOnAudioDataHandle(OnAudioDataHandle);
                }
            }
        }

        public void StopPull()
        {
            player_wrapper_.StopPull();
        }

        public void StartPlayer(String url, bool is_rtsp_tcp_mode, bool is_mute)
        {
            player_wrapper_.SetBuffer(0);

            if (!player_wrapper_.StartPlay(url, is_rtsp_tcp_mode, is_mute))
                return;
        }

        public void StopPlayer()
        {
            player_wrapper_.StopPlay();
        }

        public void PlayerDispose()
        {
            player_wrapper_.Dispose();
        }

        public void SetPusherOption(UInt32 video_option, UInt32 audio_option)
        {
            video_option_ = video_option;
            audio_option_ = audio_option;
        }

        public void StartPublisher(String url)
        {
            if (!publisher_wrapper_.OpenPublisherHandle(video_option_, audio_option_))
                return;

            if (url.Length < 8)
            {
                publisher_wrapper_.try_close_handle();
                return;
            }

            if (!publisher_wrapper_.StartPublisher(url))
            {
                return;
            }
        }

        public void StopPublisher()
        {
            publisher_wrapper_.StopPublisher();
        }

        public void PublisherDispose()
        {
            publisher_wrapper_.Dispose();
        }
    }
}

播放端封装关键代码如下:

/*
 * nt_player_wrapper.cs
 * nt_player_wrapper
 * 
 * Github: https://daniusdk.com
 * 
 * Created by DaniuLive on 2017/11/14.
 * Copyright © 2014~2024 DaniuLive. All rights reserved.
 */

public bool is_playing() {  return is_playing_; }

public bool is_pulling() {  return is_pulling_; }
	
public bool is_recording() { return is_recording_; }

public static bool is_zero_ptr(IntPtr ptr) { return IntPtr.Zero == ptr; }

public bool is_empty_handle() { return is_zero_ptr(player_handle_); }

private bool is_running()
{
	if (is_empty_handle())
		return false;

	return is_playing_ || is_recording_ || is_pulling_;
}

public bool OpenPullHandle(String url, bool is_rtsp_tcp_mode, bool is_mute)
{
	if ( player_handle_ != IntPtr.Zero )
		return true;

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

	IntPtr pull_handle = IntPtr.Zero;

	if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPlayerSDK.NT_SP_Open(out pull_handle, IntPtr.Zero, 0, IntPtr.Zero))
	{
		return false;
	}

	if (pull_handle == IntPtr.Zero)
	{
		return false;
	}

	pull_event_call_back_ = new SP_SDKEventCallBack(SDKPullEventCallBack);
	NTSmartPlayerSDK.NT_SP_SetEventCallBack(pull_handle, IntPtr.Zero, pull_event_call_back_);

	resolution_notify_callback_ = new ResolutionNotifyCallback(PlaybackWindowResized);

	set_video_frame_call_back_ = new VideoFrameCallBack(SDKVideoFrameCallBack);

	NTSmartPlayerSDK.NT_SP_SetBuffer(pull_handle, play_buffer_);
	NTSmartPlayerSDK.NT_SP_SetFastStartup(pull_handle, 1);
	NTSmartPlayerSDK.NT_SP_SetRtspAutoSwitchTcpUdp(pull_handle, 1);
	NTSmartPlayerSDK.NT_SP_SetRTSPTcpMode(pull_handle, is_rtsp_tcp_mode ? 1 : 0);

	NTSmartPlayerSDK.NT_SP_SetMute(pull_handle, is_mute_ ? 1 : 0);

	NTSmartPlayerSDK.NT_SP_SetAudioVolume(pull_handle, cur_audio_volume_);

	Int32 is_report = 1;
	Int32 report_interval = 3;
	NTSmartPlayerSDK.NT_SP_SetReportDownloadSpeed(pull_handle, is_report, report_interval);

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

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

	if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPlayerSDK.NT_SP_SetURL(pull_handle, url))
	{
		NTSmartPlayerSDK.NT_SP_Close(pull_handle);
		pull_handle = IntPtr.Zero;
		return false;
	}

	player_handle_ = pull_handle;

	return true;
}

private void PlaybackWindowResized(Int32 width, Int32 height)
{
	String resolution = width + "*" + height;
	EventGetVideoSize(player_index_, resolution);
}

public void SP_SDKVideoSizeHandle(IntPtr handle, IntPtr userData, Int32 width, Int32 height)
{
	if (null == sync_invoke_)
		return;

	System.ComponentModel.ISynchronizeInvoke sync_invoke_target = sync_invoke_.Target as System.ComponentModel.ISynchronizeInvoke;

	if (sync_invoke_target != null)
	{
		if (sync_invoke_target.InvokeRequired)
		{
			sync_invoke_target.BeginInvoke(resolution_notify_callback_, new object[] { width, height });
		}
		else
		{
			resolution_notify_callback_(width, height);
		}
	}
 
}

public bool StartPlay(String url, bool is_rtsp_tcp_mode, bool is_mute)
{
	if ( is_playing_ )
		return false;

	if (!is_pulling() && !is_recording())
	{
		if (!OpenPullHandle(url, is_rtsp_tcp_mode, is_mute))
			return false;
	}

	//video resolution callback
	video_size_call_back_ = new SP_SDKVideoSizeCallBack(SP_SDKVideoSizeHandle);
	NTSmartPlayerSDK.NT_SP_SetVideoSizeCallBack(player_handle_, IntPtr.Zero, video_size_call_back_);

	bool is_support_d3d_render = false;
	Int32 in_support_d3d_render = 0;

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

	// is_support_d3d_render = false;

	if (is_support_d3d_render)
	{
		// 支持d3d绘制的话,就用D3D绘制
		NTSmartPlayerSDK.NT_SP_SetRenderWindow(player_handle_, render_wnd_.Handle);
		NTSmartPlayerSDK.NT_SP_SetRenderScaleMode(player_handle_, 1);
	}
	else
	{
		// 不支持D3D就让播放器吐出数据来,用GDI绘制,本demo仅用来展示一对一互动使用,具体可参考播放端的demo

		//video frame callback (YUV/RGB)
		//format请参见 NT_SP_E_VIDEO_FRAME_FORMAT,如需回调YUV,请设置为 NT_SP_E_VIDEO_FRAME_FROMAT_I420
		video_frame_call_back_ = new SP_SDKVideoFrameCallBack(SetVideoFrameCallBack);
		NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FORMAT_RGB32, IntPtr.Zero, video_frame_call_back_);
	}

	uint ret = NTSmartPlayerSDK.NT_SP_StartPlay(player_handle_);
	if ( NTBaseCodeDefine.NT_ERC_OK != ret )
	{
		NTSmartPlayerSDK.NT_SP_Close(player_handle_);
		player_handle_ = IntPtr.Zero;

		return false;
	}

	is_playing_ = true;

	return true;
}

public void StopPlay(bool is_update_ui =true)
{
	if ( !is_playing_ )
		return;

	NTSmartPlayerSDK.NT_SP_StopPlay(player_handle_);

	if (!is_pulling() && !is_recording())
	{
		NTSmartPlayerSDK.NT_SP_Close(player_handle_);
		player_handle_ = IntPtr.Zero;
	}

	is_playing_ = false;

	if (is_update_ui && render_wnd_ != null)
	{
		render_wnd_.Invalidate();
	}
}

public bool StartPull(String url, bool is_rtsp_tcp_mode)
{
	if (is_pulling())
		return false;

	if (!is_playing() && !is_recording())
	{
		if (!OpenPullHandle(url, is_rtsp_tcp_mode, is_mute_))
			return false;
	}

	pull_stream_video_data_call_back_ = new SP_SDKPullStreamVideoDataCallBack(OnVideoDataHandle);
	pull_stream_audio_data_call_back_ = new SP_SDKPullStreamAudioDataCallBack(OnAudioDataHandle);

	NTSmartPlayerSDK.NT_SP_SetPullStreamVideoDataCallBack(player_handle_, IntPtr.Zero, pull_stream_video_data_call_back_);
	NTSmartPlayerSDK.NT_SP_SetPullStreamAudioDataCallBack(player_handle_, IntPtr.Zero, pull_stream_audio_data_call_back_);

	int is_transcode_aac = 1;   //PCMA/PCMU/Speex格式转AAC后 再转发
	NTSmartPlayerSDK.NT_SP_SetPullStreamAudioTranscodeAAC(player_handle_, is_transcode_aac);

	UInt32 ret = NTSmartPlayerSDK.NT_SP_StartPullStream(player_handle_);

	if (NTBaseCodeDefine.NT_ERC_OK != ret)
	{
		if (!is_playing_)
		{
			NTSmartPlayerSDK.NT_SP_Close(player_handle_);
			player_handle_ = IntPtr.Zero;
		}

		return false;
	}

	is_pulling_ = true;

	return true;
}

public void StopPull()
{
	if (!is_pulling_)
		return;

	NTSmartPlayerSDK.NT_SP_StopPullStream(player_handle_);

	if (!is_playing() && !is_recording())
	{
		NTSmartPlayerSDK.NT_SP_Close(player_handle_);
		player_handle_ = IntPtr.Zero;
	}

	is_pulling_ = false;
}

private void OnVideoDataHandle(IntPtr handle, IntPtr user_data,
	UInt32 video_codec_id, IntPtr data, UInt32 size,
	IntPtr info, IntPtr reserve)
{
	EventOnVideoDataHandle(handle, user_data, video_codec_id, data, size, info, reserve);
}

private void OnAudioDataHandle(IntPtr handle, IntPtr user_data,
					UInt32 audio_codec_id, IntPtr data, UInt32 size,
					IntPtr info, IntPtr reserve)
{
	EventOnAudioDataHandle(handle, user_data, audio_codec_id, data, size, info, reserve);
}

总结

Windows平台RTSP转RTMP推送定制版,目前发布的C#版本,只是做了基础的封装,方便开发者二次定制处理,如果有更复杂的界面和逻辑需求,基于此版本继续开发就好。