Linux平台Unity下RTMP|RTSP低延迟播放器技术实现

​技术背景

国产操作系统对于确保信息安全、促进技术创新、满足特定需求以及推动经济发展等方面都具有重要意义,多以Linux为基础二次开发。2014年4月8日起,美国微软公司停止了对Windows XP SP3操作系统提供支持,这引起了社会和广大用户的广泛关注和对信息安全的担忧。

国产操作系统具有诸多好处:

  1. 信息安全与可控性提升:国产操作系统在设计和开发过程中,可以更加注重国内的信息安全标准和需求,通过自主研发,能够避免对外部系统的过度依赖,降低数据泄露和信息安全风险。此外,由于源代码掌握在自己手中,国家可以更好地控制操作系统的安全漏洞和后门问题,从而提高整个信息系统的安全可控性。

  2. 促进技术创新与产业发展:发展国产操作系统有助于推动国内软件技术的自主创新,提升整个软件产业的核心竞争力。通过自主研发,可以打破国外操作系统的技术垄断,掌握核心技术,为国内软件产业的发展提供有力支撑。同时,这也将促进相关产业链的发展,包括硬件、中间件、应用软件等,形成良性的产业生态。

  3. 满足特定需求与提升用户体验:国产操作系统可以根据国内用户的特定需求进行定制开发,提供更加符合国内使用习惯的服务和功能。这不仅可以提高用户的满意度和忠诚度,还可以为政府、企业等用户提供更加安全、高效、便捷的信息化解决方案。

  4. 培育新的经济增长点:随着数字经济的快速发展,操作系统作为数字基础设施的重要组成部分,具有巨大的市场潜力和商业价值。发展国产操作系统有助于培育新的经济增长点,推动国内软件产业的快速发展,为国家经济发展注入新的动力。

  5. 增强国家信息安全战略地位:在信息化时代,信息安全已成为国家安全的重要组成部分。发展国产操作系统有助于提升国家信息安全战略地位,增强国家在网络空间的话语权和影响力。

在发布国产操作系统|Linux平台的RTMP|RTSP直播播放SDK之前,大牛直播SDK(官方)在Windows、Android、iOS平台已经有了非常成熟的技术积累,功能齐全、稳定性高、超低延迟、超低资源占用,推进到Linux平台是顺理成章的。国产操作系统和Linux上的RTMP|RTSP直播播放模块,视频绘制使用XLib相关库实现, 音频输出使用PulseAudio和Alsa Lib,除了常规功能如实时静音、快照、buffer time设定、网络自动重连等,RTMP支持扩展H265播放, RTSP也支持H265播放。

Linux原生的RTSP、RTMP播放模块这里我们不做赘述,本文主要讲的是如何在Linux平台构建Unity下的RTSP和RTMP低延迟直播播放。

技术实现

国产操作系统和Linux平台下,Unity环境的播放器,和Windows、Android、iOS平台基础流程并无大的差异,简单来说,通过调用原生的播放模块,回调解码后的YUV或RGB数据,投递到Unity侧,在Unity下完成绘制,这里就需要原生的RTMP、RTSP播放模块,拉流解码延迟非常低,数据投递效率非常高,无图无真相:

Linux平台,我们是回调的YUV的数据,也就是 NT_SP_E_VIDEO_FRAME_FROMAT_I420:

csharp 复制代码
        /*定义视频帧图像格式*/
        public enum NT_SP_E_VIDEO_FRAME_FORMAT : uint
        {
            NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 = 1, // 32位的rgb格式, r, g, b各占8, 另外一个字节保留, 内存字节格式为: bb gg rr xx, 主要是和windows位图匹配, 在小端模式下,按DWORD类型操作,最高位是xx, 依次是rr, gg, bb
            NT_SP_E_VIDEO_FRAME_FORMAT_ARGB = 2, // 32位的argb格式,内存字节格式是: bb gg rr aa 这种类型,和windows位图匹配
            NT_SP_E_VIDEO_FRAME_FROMAT_I420 = 3, // YUV420格式, 三个分量保存在三个面上
        }

开始播放之前,把回调设置下去:

ini 复制代码
//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_);

视频帧结构:

csharp 复制代码
    /*定义视频帧结构.*/
    [StructLayoutAttribute(LayoutKind.Sequential)]
    public struct NT_SP_VideoFrame
    {
        public Int32 format_;  // 图像格式, 请参考NT_SP_E_VIDEO_FRAME_FORMAT
	    public Int32 width_;   // 图像宽
	    public Int32 height_;  // 图像高

        public Int64 timestamp_; // 时间戳, 一般是0,不使用, 以ms为单位的

	    //具体的图像数据, argb和rgb32只用第一个, I420用前三个
	    public IntPtr plane0_;
	    public IntPtr plane1_;
	    public IntPtr plane2_;
	    public IntPtr plane3_;

	    // 每一个平面的每一行的字节数,对于argb和rgb32,为了保持和windows位图兼容,必须是width_*4
	    // 对于I420, stride0_ 是y的步长, stride1_ 是u的步长, stride2_ 是v的步长,
	    public Int32 stride0_;
	    public Int32 stride1_;
	    public Int32 stride2_;
	    public Int32 stride3_;
    }

开始播放:

ini 复制代码
/*
 * SmartPlayerLinuxMono.cs
 * Author: daniusdk.com
 */
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 (!OpenPlayerHandle(sel))
	{
		Debug.LogError("call OpenPlayerHandle failed, sel:" + sel);
		return;
	}

	//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
	{
		NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
		videoctrl[sel].player_handle_ = IntPtr.Zero;

		videoctrl[sel].is_need_get_frame_ = false;
		Debug.LogError("NT_SP_StartPlay failed, sel:" + sel);
	}

	videoctrl[sel].is_playing_ = true;
}

其中,调用的OpenPlayerHandle()实现如下:

ini 复制代码
/*
 * SmartPlayerLinuxMono.cs
 * Author: daniusdk.com
 */
private bool OpenPlayerHandle(int sel)
{
	if (videoctrl[sel].player_handle_ != IntPtr.Zero)
		return true;

	window_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)
	{
		Debug.LogError("call NT_SP_Open failed, sel: " + sel);
		return false;
	}

	if (IntPtr.Zero == videoctrl[sel].player_handle_)
		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);


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

	int play_buffer_time_ = 0;
	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;
}

停止播放:

ini 复制代码
/*
 * SmartPlayerLinuxMono.cs
 * Author: daniusdk.com
 */
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);
	}

	NTSmartPlayerSDK.NT_SP_Close(videoctrl[sel].player_handle_);
	videoctrl[sel].player_handle_ = IntPtr.Zero;

	videoctrl[sel].is_playing_ = false;
}

具体回调处理:

ini 复制代码
    private void SDKVideoFrameCallBack(UInt32 status, IntPtr frame, int sel)
    {
        //这里拿到回调frame,进行相关操作
        NT_SP_VideoFrame video_frame = (NT_SP_VideoFrame)Marshal.PtrToStructure(frame, typeof(NT_SP_VideoFrame));

        VideoFrame  u3d_frame = new VideoFrame();

        u3d_frame.width_  = video_frame.width_;
        u3d_frame.height_ = video_frame.height_;

        u3d_frame.timestamp_ = (UInt64)video_frame.timestamp_;

        int d_y_stride = video_frame.width_;
        int d_u_stride = (video_frame.width_ + 1) / 2;
        int d_v_stride = d_u_stride;

        int d_y_size = d_y_stride * video_frame.height_;
        int d_u_size = d_u_stride * ((video_frame.height_ + 1) / 2);
        int d_v_size = d_u_size;

        int u_v_height = ((u3d_frame.height_ + 1) / 2);

        u3d_frame.y_stride_ = d_y_stride;
        u3d_frame.u_stride_ = d_u_stride;
        u3d_frame.v_stride_ = d_v_stride;

        u3d_frame.y_data_ = new byte[d_y_size];
        u3d_frame.u_data_ = new byte[d_u_size];
        u3d_frame.v_data_ = new byte[d_v_size];


        CopyFramePlane(u3d_frame.y_data_, d_y_stride,
            video_frame.plane0_, video_frame.stride0_, u3d_frame.height_);

        CopyFramePlane(u3d_frame.u_data_, d_u_stride,
           video_frame.plane1_, video_frame.stride1_, u_v_height);

        CopyFramePlane(u3d_frame.v_data_, d_v_stride,
           video_frame.plane2_, video_frame.stride2_, u_v_height);

        lock (videoctrl[sel].frame_lock_ )
        {
            videoctrl[sel].cur_video_frame_ = u3d_frame;
            //Debug.LogError("sel: " + sel + " w:" + u3d_frame.width_ + "h:" + u3d_frame.height_);
        }
    }

Unity层拿到video frame后,刷新即可:

ini 复制代码
    private void UpdateProc(int sel)
    {
       VideoFrame video_frame = null;

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

            videoctrl[sel].cur_video_frame_ = null;
        }

        if ( video_frame == null )
            return;

        if (!videoctrl[sel].is_need_get_frame_)
            return;

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

        if ( !videoctrl[sel].is_need_init_texture_)
        {
            if (  video_frame.width_ != videoctrl[sel].video_width_
                || video_frame.height_ != videoctrl[sel].video_height_
                || video_frame.y_stride_ != videoctrl[sel].y_row_bytes_
                || video_frame.u_stride_ != videoctrl[sel].u_row_bytes_
                || video_frame.v_stride_ != videoctrl[sel].v_row_bytes_ )
            {
                videoctrl[sel].is_need_init_texture_ = true;
            }
        }

        if (videoctrl[sel].is_need_init_texture_)
        {
            if (InitYUVTexture(video_frame, sel))
            {
                videoctrl[sel].is_need_init_texture_ = false;
            }
        }

        UpdateYUVTexture(video_frame, sel);
    }

UpdateYUVTexture相关实现:

ini 复制代码
    private void UpdateYUVTexture(VideoFrame video_frame, int sel)
    {
        if (video_frame.y_data_ == null || video_frame.u_data_ == null || video_frame.v_data_ == null)
        {
            Debug.Log("video frame with null..");
            return;
        }

        if (videoctrl[sel].yTexture_ != null)
        {
            videoctrl[sel].yTexture_.LoadRawTextureData(video_frame.y_data_);
            videoctrl[sel].yTexture_.Apply();
        }

        if (videoctrl[sel].uTexture_ != null)
        {
            videoctrl[sel].uTexture_.LoadRawTextureData(video_frame.u_data_);
            videoctrl[sel].uTexture_.Apply();
        }

        if (videoctrl[sel].vTexture_ != null)
        {
            videoctrl[sel].vTexture_.LoadRawTextureData(video_frame.v_data_);
            videoctrl[sel].vTexture_.Apply();
        }
    }

技术总结

以上是Linux平台下Unity RTMP、RTSP直播播放器大概的实现参考,随着国产操作系统的推进,Linux下RTMP、RTSP高质量的播放器需求越来越大,Unity下,可以实现和Windows、Android等平台统一开发管理,非常方便。感兴趣的公司或开发者,可以单独跟我沟通探讨。

相关推荐
aqi0011 天前
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
android·ffmpeg·音视频·直播·流媒体
aqi0013 天前
FFmpeg开发笔记(五十一)适合学习研究的几个音视频开源框架
ffmpeg·音视频·直播·流媒体
aqi0019 天前
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生
ffmpeg·音视频·直播·流媒体
aqi0020 天前
FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
ffmpeg·音视频·直播·流媒体
aqi001 个月前
FFmpeg开发笔记(四十八)从0开始搭建直播系统的开源软件架构
ffmpeg·音视频·直播·流媒体
aqi001 个月前
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
ffmpeg·音视频·直播·流媒体
aqi001 个月前
FFmpeg开发笔记(四十六)利用SRT协议构建手机APP的直播Demo
ffmpeg·音视频·直播·流媒体
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
aqi001 个月前
FFmpeg开发笔记(四十五)使用SRT Streamer开启APP直播推流
ffmpeg·音视频·直播·流媒体
音视频牛哥1 个月前
Android摄像头采集选Camera1还是Camera2?
音视频开发·视频编码·直播