Bootstrap

Windows平台Unity3D下如何低延迟低资源占用播放RTMP或RTSP流?

技术探讨

自2017年我们发布跨平台的低延迟Unity下的RTSP|RTMP直播播放器后,Unity下的直播体验有了质的提升,特别是RTMP,从大家认知里面的几秒钟,直接缩减到100-300ms,满足了绝大多数场景下低延迟的技术诉求。今天就Unity下的RTSP|RTMP的低延迟播放,从以下几个维度,抛砖引玉,做个探讨:

 选择合适的播放插件

 Unity下的RTSP|RTMP低延迟播放,业内想到最多的是大牛直播SDK的SmartPlayer,支持 RTMP 和 RTSP 直播流播放,在资源占用、延迟等方面表现较好。以下是原生模块的功能支持,如不单独说明,系Windows、Linux(含x86_64|aarch64)、Android、iOS全平台支持。以下图为例,播放海康2560*1440 8M码率的数据,左侧是VLC,右侧是SmartPlayer,延迟可见一斑。

  •  [支持播放协议]RTSP|RTMP;
  •  [多实例播放]支持多实例播放;
  •  [事件回调]支持网络状态、buffer状态等回调;
  •  [视频格式]支持H.265、H.264,此外,还支持RTSP MJPEG播放;
  •  [音频格式]支持AAC/PCMA/PCMU;
  •  [H.264/H.265软解码]支持H.264/H.265软解;
  •  [H.264硬解码]Windows/Android/iOS支持特定机型H.264硬解;
  •  [H.265硬解]Windows/Android/iOS支持特定机型H.265硬解;
  •  [H.264/H.265硬解码]Android支持设置Surface模式硬解和普通模式硬解码;
  •  [RTSP模式设置]支持RTSP TCP/UDP模式设置;
  •  [RTSP TCP/UDP自动切换]支持RTSP TCP、UDP模式自动切换;
  •  [RTSP超时设置]支持RTSP超时时间设置,单位:秒;
  •  [RTSP 401认证处理]支持上报RTSP 401事件,如URL携带鉴权信息,会自动处理;
  •  [缓冲时间设置]支持buffer time设置;
  •  [首屏秒开]支持首屏秒开模式;
  •  [复杂网络处理]支持断网重连等各种网络环境自动适配;
  •  [快速切换URL]支持播放过程中,快速切换其他URL,内容切换更快;
  •  [音视频多种render机制]Android平台,视频:surfaceview/OpenGL ES,音频:AudioTrack/OpenSL ES;
  •  [实时静音]支持播放过程中,实时静音/取消静音;
  •  [实时音量调节]支持播放过程中实时调节音量;
  •  [实时快照]支持播放过程中截取当前播放画面;
  •  [只播关键帧]Windows平台支持实时设置是否只播放关键帧;
  •  [渲染角度]支持0°,90°,180°和270°四个视频画面渲染角度设置;
  •  [渲染镜像]支持水平反转、垂直反转模式设置;
  •  [等比例缩放]支持图像等比例缩放绘制(Android设置surface模式硬解模式不支持);
  •  [实时下载速度更新]支持当前下载速度实时回调(支持设置回调时间间隔);
  •  [解码前视频数据回调]支持H.264/H.265数据回调;
  •  [解码后视频数据回调]支持解码后YUV/RGB数据回调;
  •  [解码前音频数据回调]支持AAC/PCMA/PCMU数据回调;
  •  [音视频自适应]支持播放过程中,音视频信息改变后自适应;
  •  [扩展录像功能]完美支持和录像SDK组合使用。

优化播放参数设置

  • 缓冲设置:将缓冲时间设置得尽可能短,以减少数据缓冲带来的延迟。但缓冲时间过短可能会导致播放不稳定,需要根据实际网络情况进行调整。一般来说,将缓冲时间设置在几十毫秒到几百毫秒之间。
  • 解码模式:启用硬件解码(如果插件支持)可以大大提高解码速度,减少延迟。同时,根据视频的编码格式(如 H.264、H.265)选择合适的硬解码模式。
  • 低延迟模式:如果插件或 SDK 提供了低延迟模式的选项,一定要开启该模式。不过,有些情况下开启低延迟模式可能会牺牲一定的视频质量或稳定性,需要进行权衡。
  • 网络协议设置:对于 RTSP 流,设置合适的 TCP/UDP 模式。在网络状况较好的情况下,可以优先使用 UDP 模式,因为它的传输效率更高,但可能会有一定的丢包风险;在网络状况不稳定的情况下,使用 TCP 模式可以保证数据的可靠性,但延迟可能会稍微增加。另外,设置好 TCP/UDP 模式的自动切换功能,以便在网络状况变化时自动选择合适的传输模式。

 优化网络环境

  • 网络带宽:确保网络带宽足够大,以支持 RTMP 或 RTSP 流的实时传输。如果是在本地网络环境下,可以使用有线网络连接,避免使用无线网络,以减少网络波动和延迟。
  • 网络稳定性:尽量减少网络中的干扰因素,如关闭其他占用网络带宽的应用程序、避免在网络高峰期进行播放等。如果可能,可以使用专业的网络设备和工具来优化网络性能。

 优化代码和渲染流程

  • 数据处理优化:在接收到流媒体数据后,尽量减少数据的拷贝和处理操作,避免不必要的性能开销。可以使用高效的数据结构和算法来处理视频数据,提高数据处理的速度。
  • 渲染优化:在 Unity 中,合理设置渲染参数,如减少不必要的渲染特效、优化材质和纹理等,以提高渲染效率,减少延迟。同时,根据视频的分辨率和帧率,选择合适的渲染方式和渲染线程数量。

 实时监控和调试

  • 性能监控:使用 Unity 的性能监控工具,实时监测播放过程中的性能参数,如 CPU 使用率、内存占用、帧率等。根据监测结果,及时发现性能瓶颈和问题,并进行优化。
  • 日志记录:记录播放过程中的日志信息,包括网络连接状态、数据传输情况、错误信息等。在出现问题时,可以通过查看日志信息来快速定位和解决问题。 

技术实现

以大牛直播SDK的Windows平台Unity下的RTSP|RTMP直播播放模块为例,开始播放实现如下,开始播放之前,可以选择用软解码还是硬解码:

/* SmartPlayerWinMono.cs
 * Created by daniusdk.com on 2018/05/10.
 * WeChat: xinsheng120
 */
public void StartPlayer(int sel)
{
	Debug.Log("StartPlayer++, sel: " + sel);

	if (videoctrl[sel].is_playing_)
	{
		Debug.Log("StartPlayer, already started.. sel: " + sel);
		return;
	}

	lock (videoctrl[sel].frame_lock_)
	{
		videoctrl[sel].cur_video_frame_ = null;
	}

	if (!videoctrl[sel].is_recording_)
	{
		if (!OpenPlayerHandle(sel))
		{
			Debug.LogError("call OpenPlayerHandle failed, sel:" + sel);
			return;
		}
	}

	if (is_enable_hardware_decoder_)
	{
		NTSmartPlayerSDK.NT_SP_SetH264HardwareDecoder(videoctrl[sel].player_handle_, is_support_h264_hardware_decoder_ ? 1 : 0, 0);
		NTSmartPlayerSDK.NT_SP_SetH265HardwareDecoder(videoctrl[sel].player_handle_, is_support_h265_hardware_decoder_ ? 1 : 0, 0);
	}
	else
	{
		NTSmartPlayerSDK.NT_SP_SetH264HardwareDecoder(videoctrl[sel].player_handle_, 0, 0);
		NTSmartPlayerSDK.NT_SP_SetH265HardwareDecoder(videoctrl[sel].player_handle_, 0, 0);
	}

	//video frame callback (YUV/RGB)
	videoctrl[sel].sdk_video_frame_call_back_ = new VideoControl.SetVideoFrameCallBack(SDKVideoFrameCallBack);
	videoctrl[sel].video_frame_call_back_ = new SP_SDKVideoFrameCallBack(NT_SP_SetVideoFrameCallBack);
	NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(videoctrl[sel].player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, window_handle_, videoctrl[sel].video_frame_call_back_);

	UInt32 flag = NTSmartPlayerSDK.NT_SP_StartPlay(videoctrl[sel].player_handle_);

	if (flag == DANIULIVE_RETURN_OK)
	{
		videoctrl[sel].is_need_get_frame_ = true;
		Debug.Log("NT_SP_StartPlay succeed, sel:" + sel);
	}
	else
	{
		videoctrl[sel].is_need_get_frame_ = false;
		Debug.LogError("NT_SP_StartPlay failed, sel:" + sel);
	}

	videoctrl[sel].is_playing_ = true;
}

对应的OpenPlayerHandle()实现如下,OpenPlayerHandle()主要完成播放实例的创建、事件回调设置、基础参数的设置(比如buffer time、RTSP tcp-udp模式设置、rtsp超时时间设置、低延迟模式设置等)。

private bool OpenPlayerHandle(int sel)
{
	if (videoctrl[sel].player_handle_ != IntPtr.Zero)
		return true;

	window_handle_ = IntPtr.Zero;

	if (videoctrl[sel].player_handle_ == IntPtr.Zero)
	{
		videoctrl[sel].player_handle_ = new IntPtr();
		UInt32 ret_open = NTSmartPlayerSDK.NT_SP_Open(out videoctrl[sel].player_handle_, window_handle_, 0, IntPtr.Zero);
		if (ret_open != 0)
		{
			videoctrl[sel].player_handle_ = IntPtr.Zero;
			Debug.LogError("call NT_SP_Open failed, sel: " + sel);
			return false;
		}
	}

	videoctrl[sel].event_call_back_ = new SP_SDKEventCallBack(NT_SP_SDKEventCallBack);
	NTSmartPlayerSDK.NT_SP_SetEventCallBack(videoctrl[sel].player_handle_, window_handle_, videoctrl[sel].event_call_back_);
	videoctrl[sel].sdk_event_call_back_ = new VideoControl.SetEventCallBack(SDKEventCallBack);

	if (IntPtr.Zero == videoctrl[sel].player_handle_)
		return false;

	/* ++ 播放前参数配置可加在此处 ++ */

	int play_buffer_time_ = 100;
	NTSmartPlayerSDK.NT_SP_SetBuffer(videoctrl[sel].player_handle_, play_buffer_time_);                 //设置buffer time

	//int is_using_tcp = 1;        //TCP模式
	//NTSmartPlayerSDK.NT_SP_SetRTSPTcpMode(videoctrl[sel].player_handle_, is_using_tcp);

	int timeout = 10;
	NTSmartPlayerSDK.NT_SP_SetRtspTimeout(videoctrl[sel].player_handle_, timeout);

	int is_auto_switch_tcp_udp = 1;
	NTSmartPlayerSDK.NT_SP_SetRtspAutoSwitchTcpUdp(videoctrl[sel].player_handle_, is_auto_switch_tcp_udp);

	Boolean is_mute_ = false;
	NTSmartPlayerSDK.NT_SP_SetMute(videoctrl[sel].player_handle_, is_mute_ ? 1 : 0);                    //是否启动播放的时候静音

	int is_fast_startup = 1;
	NTSmartPlayerSDK.NT_SP_SetFastStartup(videoctrl[sel].player_handle_, is_fast_startup);              //设置快速启动模式

	Boolean is_low_latency_ = false;
	NTSmartPlayerSDK.NT_SP_SetLowLatencyMode(videoctrl[sel].player_handle_, is_low_latency_ ? 1 : 0);    //设置是否启用低延迟模式

	//设置旋转角度(设置0, 90, 180, 270度有效,其他值无效)
	int rotate_degrees = 0;
	NTSmartPlayerSDK.NT_SP_SetRotation(videoctrl[sel].player_handle_, rotate_degrees);

	int volume = 100;
	NTSmartPlayerSDK.NT_SP_SetAudioVolume(videoctrl[sel].player_handle_, volume);	//设置播放音量, 范围是[0, 100], 0是静音,100是最大音量, 默认是100

	// 设置上传下载报速度
	int is_report = 0;
	int report_interval = 2;
	NTSmartPlayerSDK.NT_SP_SetReportDownloadSpeed(videoctrl[sel].player_handle_, is_report, report_interval);

	//设置播放URL
	NTSmartPlayerSDK.NT_SP_SetURL(videoctrl[sel].player_handle_, videoctrl[sel].playback_url_);
	/* -- 播放前参数配置可加在此处 -- */

	return true;
}

停止播放

private void StopPlayer(int sel)
{
	Debug.Log("StopPlayer++, sel: " + sel);

	videoctrl[sel].is_need_get_frame_ = false;
	videoctrl[sel].is_need_init_texture_ = false;

	if (videoctrl[sel].player_handle_ == IntPtr.Zero)
	{
		return;
	}

	UInt32 flag = NTSmartPlayerSDK.NT_SP_StopPlay(videoctrl[sel].player_handle_);
	if (flag == DANIULIVE_RETURN_OK)
	{
		Debug.Log("call NT_SP_StopPlay succeed, sel: " + sel);
	}
	else
	{
		Debug.LogError("call NT_SP_StopPlay failed, sel: " + sel);
	}

	if (!videoctrl[sel].is_recording_)
	{
		NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
		videoctrl[sel].player_handle_ = IntPtr.Zero;
	}

	videoctrl[sel].is_playing_ = false;
}

如果需要录像,实现如下:

private void StartRecorder(int sel)
{
	Debug.Log("StartRecorder++, sel: " + sel);

	if (videoctrl[sel].is_recording_)
	{
		Debug.Log("StartRecorder, already started.. sel: " + sel);
		return;
	}

	if (!videoctrl[sel].is_playing_)
	{
		if (!OpenPlayerHandle(sel))
		{
			Debug.LogError("call OpenPlayerHandle failed..");
			return;
		}
	}

	bool is_rec_video = true;
	bool is_rec_audio = true;
	NTSmartPlayerSDK.NT_SP_SetRecorderVideo(videoctrl[sel].player_handle_, is_rec_video ? 1 : 0);
	NTSmartPlayerSDK.NT_SP_SetRecorderAudio(videoctrl[sel].player_handle_, is_rec_audio ? 1 : 0);

	String rec_dir = "D:\\Rec";     //录像目录可自行指定
	String rec_name_file_prefix_= "daniu" + sel.ToString();
	UInt32 max_file_size = 200 * 1024; // 单位是KByte, 默认200MB
	bool is_append_date = true;
	bool is_append_time = true;
	bool is_audio_transcode_aac = true;

	UInt32 ret = NTSmartPlayerSDK.NT_SP_SetRecorderDirectory(videoctrl[sel].player_handle_, rec_dir);
	if (NT.NTBaseCodeDefine.NT_ERC_OK != ret)
	{
		Debug.LogError("设置录像目录失败,请确保目录存在且是英文目录");
		return;
	}

	NTSmartPlayerSDK.NT_SP_SetRecorderFileMaxSize(videoctrl[sel].player_handle_, max_file_size);

	NT_SP_RecorderFileNameRuler rec_name_ruler = new NT_SP_RecorderFileNameRuler();

	rec_name_ruler.type_ = 0;
	rec_name_ruler.file_name_prefix_ = rec_name_file_prefix_;
	rec_name_ruler.append_date_ = is_append_date ? 1 : 0;
	rec_name_ruler.append_time_ = is_append_time ? 1 : 0;

	NTSmartPlayerSDK.NT_SP_SetRecorderFileNameRuler(videoctrl[sel].player_handle_, ref rec_name_ruler);

	NTSmartPlayerSDK.NT_SP_SetRecorderAudioTranscodeAAC(videoctrl[sel].player_handle_, is_audio_transcode_aac ? 1 : 0);

	videoctrl[sel].record_call_back_ = new SP_SDKRecorderCallBack(NT_SP_SDKRecorderCallBack);
	NTSmartPlayerSDK.NT_SP_SetRecorderCallBack(videoctrl[sel].player_handle_, IntPtr.Zero, videoctrl[sel].record_call_back_);
	videoctrl[sel].set_record_call_back_ = new VideoControl.SetRecordCallBack(RecordCallBack);

	if (NT.NTBaseCodeDefine.NT_ERC_OK != NTSmartPlayerSDK.NT_SP_StartRecorder(videoctrl[sel].player_handle_))
	{
		Debug.LogError("call NT_SP_StartRecorder failed..");
		return;
	}

	videoctrl[sel].is_recording_ = true;
}

private void StopRecorder(int sel)
{
	Debug.Log("StopRecorder++, sel: " + sel);

	if (videoctrl[sel].player_handle_ == IntPtr.Zero)
	{
		return;
	}

	NTSmartPlayerSDK.NT_SP_StopRecorder(videoctrl[sel].player_handle_);

	videoctrl[sel].is_recording_ = false;

	if (!videoctrl[sel].is_playing_)
	{
		NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
		videoctrl[sel].player_handle_ = IntPtr.Zero;
	}
}

SmartPlayer同时播放4路海康2560*1440分辨率的RTSP流,资源占用如下:

总结

Windows平台如果对延迟和资源占有等,要求非常高,可以选择合适的低延迟RTSP或RTMP播放插件、优化播放参数设置、优化网络环境、优化代码和渲染流程。流播放之前,做好参数配置,如缓冲设置、解码模式、低延迟模式、网络协议设置等播放参数。感兴趣的开发者,可以单独跟我沟通探讨。

;