技术背景
前几年我们发布了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#版本,只是做了基础的封装,方便开发者二次定制处理,如果有更复杂的界面和逻辑需求,基于此版本继续开发就好。