技术背景

我们在做Unity平台RTSP、RTMP播放器的时候,有公司提出来这样的技术需求,希望在头显播放全景的8K RTSP|RTMP直播流,8K的数据,对头显和播放器,都提出了新的要求,我们从几个方面,探讨下VR头显设备如何播放8K的RTSP|RTMP流数据:

一、播放器支持

  1. 兼容性:首先,RTSP|RTMP播放器需要支持8K分辨率的视频流。这意味着播放器必须能够解码8K视频,并在支持8K分辨率的显示设备上播放,这个不必多说,我们已经支持。
  2. 解码能力:播放器需要具备强大的解码能力,以处理8K视频流中的大量数据。这通常要求播放器使用高效的解码算法,并充分利用硬件加速功能(如GPU加速),这就需要头显支持8K的硬解码。

二、网络要求

  1. 带宽:8K视频流需要极高的网络带宽来支持实时传输。确保网络带宽足够大,以避免播放过程中出现卡顿、延迟或缓冲等问题,如果是内网环境下,基本不要纠结带宽问题。
  2. 稳定性:网络连接的稳定性也非常重要。不稳定的网络连接可能导致视频流中断或质量下降。

三、硬件要求

  1. 处理器与内存:VR头显播放8K的视频流,对VR头显的性能,提了很高的要求,比如说quest3,就是不错的选择。

四、播放步骤

  1. 选择RTSP播放器:我们的做法,是用大牛直播SDK的原生的RTSP|RTMP播放器,硬解码模式,回调解码后的YUV或RGB数据到unity,需要注意的是,由于8K的RTSP|RTMP流,数据量非常大,特别是解码后的数据,条件允许的情况下,需要尽可能少的减少拷贝。

技术实现

本文以大牛直播SDK的Android平台Unity3D RTSP|RTMP播放模块为例:

VR头显如何低延迟播放8K的RTSP|RTMP流_Unity3D RTSP

开始播放:

/*
 * SmartPlayerAndroidMono.cs
 * Author: daniusdk.com
 * QQ:89030985
 */
public void Play()
{
	if (is_running)
	{
		Debug.Log("已经在播放。。");   
		return;
	}

	//获取输入框的url
	string url = input_url_.text.Trim();

	if (!url.StartsWith("rtmp://") && !url.StartsWith("rtsp://"))
	{
		videoUrl = "rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream";
	}
	else
	{
		videoUrl = url;
	}

	OpenPlayer();

	if ( player_handle_ == 0 )
		return;

	NT_U3D_Set_Game_Object(player_handle_, game_object_);

	/* ++ 播放前参数配置可加在此处 ++ */
	int is_using_tcp = 0;        //TCP/UDP模式设置
	NT_U3D_SetRTSPTcpMode(player_handle_, is_using_tcp);

	int is_report = 0;
	int report_interval = 1;
	NT_U3D_SetReportDownloadSpeed(player_handle_, is_report, report_interval);  //下载速度回调

	NT_U3D_SetBuffer(player_handle_, play_buffer_time_);                        //设置buffer time

	NT_U3D_SetPlayerLowLatencyMode(player_handle_, is_low_latency_ ? 1 : 0);    //设置是否启用低延迟模式

	NT_U3D_SetMute(player_handle_, is_mute_ ? 1 : 0);                           //是否启动播放的时候静音

	NT_U3D_SetAudioVolume(player_handle_, cur_audio_volume_);                   //设置播放音量

	NT_U3D_SetVideoDecoderMode(player_handle_, is_hw_decode_ ? 1 : 0);          //设置H.264软硬解模式

	NT_U3D_SetVideoHevcDecoderMode(player_handle_, is_hw_decode_ ? 1 : 0);          //设置H.265软硬解模式

	int is_output = 1;
	int disable_use_image_planes = 0;
	bool is_supports_texture_format = SystemInfo.SupportsTextureFormat(TextureFormat.RG16);
	Debug.Log("is_supports_texture_format: " + is_supports_texture_format);
	int is_supported_multiple_format = is_supports_texture_format? 1:0;
	int max_images = 3;
	int buffer_pool_max_size = 0;
	NT_U3D_SetImageReaderOutput(player_handle_, is_output, disable_use_image_planes, is_supported_multiple_format, max_images, buffer_pool_max_size);  //硬解码image reader

	int is_fast_startup = 1;
	NT_U3D_SetFastStartup(player_handle_, is_fast_startup);                     //设置快速启动模式

	int rtsp_timeout = 10;
	NT_U3D_SetRTSPTimeout(player_handle_, rtsp_timeout);                        //设置RTSP超时时间

	int is_auto_switch_tcp_udp = 1;
	NT_U3D_SetRTSPAutoSwitchTcpUdp(player_handle_, is_auto_switch_tcp_udp);    //设置TCP/UDP模式自动切换

	int is_audiotrack = 1;
	NT_U3D_SetAudioOutputType(player_handle_, is_audiotrack);                   //设置音频输出模式: if 0: 自动选择; if with 1: audiotrack模式

	NT_U3D_SetUrl(player_handle_, videoUrl);
	/* -- 播放前参数配置可加在此处 -- */

	int flag = NT_U3D_StartPlay(player_handle_);

	if (flag  == DANIULIVE_RETURN_OK)
	{
		is_need_get_frame_ = true;
		Debug.Log("播放成功");
	}
	else
	{
		is_need_get_frame_ = false;
		Debug.LogError("播放失败");
	}

	is_running = true;  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.

对应的OpenPlayer()实现如下:

private void OpenPlayer()
{
	if ( java_obj_cur_activity_ == null )
	{
		Debug.LogError("getApplicationContext is null");
		return;
	}

	player_handle_ = NT_U3D_Open();

	if (player_handle_ != 0)
		Debug.Log("open success");
	else
		Debug.LogError("open fail");
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

关闭Player:

private void ClosePlayer()
{
	is_need_get_frame_ = false;
	is_need_init_texture_ = false;

	int flag = NT_U3D_StopPlay(player_handle_);
	if (flag == DANIULIVE_RETURN_OK)
	{
		Debug.Log("停止成功");
	}
	else
	{
		Debug.LogError("停止失败");
	}

	flag = NT_U3D_Close(player_handle_);
	if (flag == DANIULIVE_RETURN_OK)
	{
		Debug.Log("关闭成功");
	}
	else
	{
		Debug.LogError("关闭失败");
	}

	player_handle_ = 0;

	NT_U3D_UnInit();

	is_running = false;
	video_format_ = VideoFrame.FORMAT_UNKNOWN;
	video_width_ = 0;
	video_height_ = 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

Update刷新数据:

private void Update()
{
	if (!is_need_get_frame_)
		return;

	if (player_handle_ == 0)
		return;

	AndroidJavaObject u3d_video_frame_obj = NT_U3D_GetVideoFrame(player_handle_);

	if (u3d_video_frame_obj == null)
	{
		return;
	}

	VideoFrame converted_video_frame = ConvertToVideoFrame(u3d_video_frame_obj);

	if (converted_video_frame == null)
	{
		u3d_video_frame_obj.Call("release");

	   u3d_video_frame_obj = null;
	   return;
	}

	if (!is_need_init_texture_)
	{
		if (converted_video_frame.format_ != video_format_)
		{
			is_need_init_texture_ = true;

		}
		else if (converted_video_frame.width_ != video_width_
			|| converted_video_frame.height_ != video_height_
			|| converted_video_frame.stride0_ != y_row_bytes_
			|| converted_video_frame.stride1_ != u_row_bytes_
			|| converted_video_frame.stride2_ != v_row_bytes_)
		{
			is_need_init_texture_ = true;
		}
	}

	if (is_need_init_texture_)
	{
		if (InitYUVTexture(converted_video_frame))
		{
			is_need_init_texture_ = false;
		}
	}

	UpdateYUVTexture(converted_video_frame);

	converted_video_frame.java_frame_obj_ = null;
	converted_video_frame = null;
	u3d_video_frame_obj.Call("release");
	u3d_video_frame_obj = null;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.

总结

VR头显如果需要播放8K的RTSP或RTSP流,对硬件和网络的要求非常高,因此在实际应用中可能会遇到一些挑战。通过实际测试,在quest3头显,配合我们的RTSP|RTMP播放器,在unity下,可以实现毫秒级延迟的8K视频数据播放,以满足平衡操控等对实时性要求非常高的使用场景,感兴趣的开发者,可以单独跟我探讨。