技术背景

好多开发者,希望我们能够分享下如何实现Unity下的camera场景采集并推送rtmp服务,然后低延迟播放出来。简单来说,在Unity 中实现采集 Camera 场景并推送RTMP的话,先是获取 Camera 场景数据,通过创建 RenderTexture 和读取图像数据到 Texture2D。选择选择合适的RTMP推送库或SDK,并设置推流地址和初始化推流。然后说明了推送数据时需要将图像数据转换为合适格式并推送到服务器,如果需要推送音频,还可以采集unity场景的audio或麦克风、扬声器audio数据,在结束推流时要停止推流并释放资源。

技术实现

本文以大牛直播SDK的Windows平台Unity下camera场景采集,并推送RTMP服务为例,先说

创建 RenderTexture:在 Unity 中,使用 RenderTexture 类来创建一个用于捕获 Camera 图像的纹理。

Texture2D image_texture = textures_poll_.get(video_width_, video_height_);
if (null == image_texture)
   return;

RenderTexture old_camera_rt = camera_.targetTexture;
camera_.targetTexture = render_texture_;
camera_.Render();

RenderTexture old_rt = RenderTexture.active;

RenderTexture.active = render_texture_;

image_texture.ReadPixels(new Rect(0, 0, video_width_, video_height_), 0, 0, false);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

构建FrameTexture:

/* SmartPublisherWinMono.cs
 * Created by daniusdk.com on 2018/05/10.
 */
public class FrameTexture
{
	public FrameTexture(Texture2D texture, IntPtr video_buffer, int video_buffer_size,
		int video_width, int video_height, int is_vertical_flip, int is_horizontal_flip, int scale_width, int scale_height, bool is_alpha)
	{
		texture_ = texture;
		video_buffer_ = video_buffer;
		video_buffer_size_ = video_buffer_size;
		video_width_ = video_width;
		video_height_ = video_height;
		is_vertical_flip_ = is_vertical_flip;
		is_horizontal_flip_ = is_horizontal_flip;
		scale_width_ = scale_width;
		scale_height_ = scale_height;
		is_alpha_ = is_alpha;
	}

	public Texture2D texture_;
	public IntPtr video_buffer_;
	public int video_buffer_size_;
	public int video_width_;
	public int video_height_;
	public int is_vertical_flip_;
	public int is_horizontal_flip_;
	public int scale_width_;
	public int scale_height_;
	public bool is_alpha_;
}
  • 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.

拿到数据后,通过SendImage()发送数据的大牛直播SDK封装后的RTMP推送模块。

private bool sendImage()
{
	FrameTexture frame;
	if (frames_.TryDequeue(out frame))
	{
		if (frame != null && frame.texture_ != null)
		{
			if (frame.video_buffer_ != IntPtr.Zero)
			{
				handle_.OnPostRGBXData(0, frame.video_buffer_, video_buffer_size_, frame.video_width_ * 4, frame.video_width_, -frame.video_height_, frame.is_alpha_);
			}

			pool_.add(frame.texture_);
			frame.texture_ = null;
		}

		frame = null;
		return true;
	}

	frame = null;
	return false;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

如果需要同时推送多个camera场景,只要启动多个推送实例即可,需要注意的是,SmartPublisher模块,不管多少实例,仅需要调用一次Init()和UnInit()接口。

/* SmartPublisherWinMono.cs
 * Created by daniusdk.com on 2018/05/10.
 */
private bool InitSDK()
{
	// 设置日志路径(请确保目录存在)
	//String log_path = "D:\\pulisherlog";
	//NTSmartLog.NT_SL_SetPath(log_path);

	UInt32 pub_init_ret = NTSmartPublisherSDK.NT_PB_Init(0, IntPtr.Zero);

	if (NTBaseCodeDefine.NT_ERC_OK == pub_init_ret)
	{
		return true;
	}
	else
	{
		if (NTBaseCodeDefine.NT_ERC_OK == pub_init_ret)
		{
			NTSmartPublisherSDK.NT_PB_UnInit();
		}

		is_sdk_has_inited_ = false;

		return false;
	}
}

private bool UnInitSDK()
{
	if (is_sdk_has_inited_)
	{
		NTSmartPublisherSDK.NT_PB_UnInit();
		is_sdk_has_inited_ = false;
	}

	return 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.

以初始化InitSDK()为例,Awake()的时候,调一次即可:

public void Awake()
{
	//rtmp_pusher_url.text = "rtmp://192.168.0.211:1935/hls/stream" + (System.Environment.TickCount % 100000).ToString();
	rtmp_pusher_url_.text = "rtmp://192.168.0.108:1935/hls/stream1";

	video_width_ = camera_.pixelWidth;
	video_height_ = camera_.pixelHeight;

	btn_rtmp_pusher_.onClick.AddListener(btn_start_rtmp_pusher_Click);
	btn_preview_.onClick.AddListener(btn_preview_Click);
	video_option_sel_.onValueChanged.AddListener(SelVideoPushType);
	audio_option_sel_.onValueChanged.AddListener(SelAudioPushType);

	btn_rtsp_service_.onClick.AddListener(btn_rtsp_service_Click);
	btn_rtsp_publisher_.onClick.AddListener(btn_rtsp_publisher_Click);
	btn_get_rtsp_session_numbers_.onClick.AddListener(btn_get_rtsp_session_numbers_Click);

	btn_rtsp_publisher_.interactable = false;

	btn_record_.onClick.AddListener(btn_record_Click);
	btn_pause_record_.onClick.AddListener(btn_pause_record_Click);

	InitSDK();

	publisher_wrapper_ = new nt_publisher_wrapper();

	int w = video_width_;
	if ((w&1) == 1)
		w--;

	int h = video_height_;
	if ((h&1) == 1)
		h--;

	publisher_wrapper_.SetCaptureVesolution(w, h);
	publisher_wrapper_.OnLogEventMsg += OnLogHandle;

	Loom.Initialize();
}
  • 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.

推送RTMP实现如下:

/* SmartPublisherWinMono.cs
 * Created by daniusdk.com on 2018/05/10.
 */
public void btn_start_rtmp_pusher_Click()
{
	if (publisher_wrapper_.is_rtmp_publishing())
	{
		StopPushRTMP();
		btn_rtmp_pusher_.GetComponentInChildren<Text>().text = "推送RTMP";

		return;
	}

	String url = rtmp_pusher_url_.text;

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

		Debug.LogError("请输入RTMP推送地址");
		return;
	}

	if (!OpenPublisherHandle())
		return;

	SetCommonOptionToPublisherSDK();

	if (!publisher_wrapper_.StartPublisher(url))
	{
		Debug.LogError("调用StartPublisher失败..");
		return;
	}

	btn_rtmp_pusher_.GetComponentInChildren<Text>().text = "停止推送";

	if (!publisher_wrapper_.is_previewing() && !publisher_wrapper_.is_rtsp_publishing() && !publisher_wrapper_.is_recording())
	{
		StartCaptureAvData();
		coroutine_ = StartCoroutine(OnPostVideo());
	}
}
  • 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.

其中,调到的SetCommonOptionToPublisherSDK()实现如下:

private void SetCommonOptionToPublisherSDK()
{
	if (publisher_wrapper_.is_empty_handle())
		return;

	if (publisher_wrapper_.handle_reference_count() > 0)
		return;

	publisher_wrapper_.config_layers(false);

	publisher_wrapper_.SetFrameRate((uint)video_fps_);

	bool is_hw_encoder = false; //默认软编码
	bool is_h264_encoder = true;
	Int32 type = is_hw_encoder ? 1 : 0;
	Int32 encoder_id = is_hw_encoder ? 128 : 0;
	UInt32 codec_id = is_h264_encoder?(UInt32)NTCommonMediaDefine.NT_MEDIA_CODEC_ID.NT_MEDIA_CODEC_ID_H264: 
		(UInt32)NTCommonMediaDefine.NT_MEDIA_CODEC_ID.NT_MEDIA_CODEC_ID_H265;
	Int32 param1 = is_hw_encoder ? -1 : 0;

	publisher_wrapper_.SetVideoEncoder(type, encoder_id, codec_id, param1);

	publisher_wrapper_.SetVideoQualityV2(publisher_wrapper_.CalVideoQuality(video_width_, video_height_, is_h264_encoder));

	publisher_wrapper_.SetVideoBitRate(publisher_wrapper_.CalBitRate(video_fps_, video_width_, video_height_));

	publisher_wrapper_.SetVideoMaxBitRate(publisher_wrapper_.CalMaxKBitRate(video_fps_, video_width_, video_height_, false));

	publisher_wrapper_.SetVideoKeyFrameInterval(key_frame_interval_);

	if (is_h264_encoder)
	{
		publisher_wrapper_.SetVideoEncoderProfile(3);
	}

	publisher_wrapper_.SetVideoEncoderSpeed(publisher_wrapper_.CalVideoEncoderSpeed(video_width_, video_height_, is_h264_encoder));

	publisher_wrapper_.SetPublisherAudioCodecType(1);   //1: AAC 2: Speex
}
  • 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.

获取到的数据,除了推送RTMP外,如果需要录像,录像设计实现如下:

public void btn_record_Click()
{
	if (publisher_wrapper_.is_recording())
	{
		StopRecord();
		btn_record_.GetComponentInChildren<Text>().text = "开始录像";

		return;
	}

	if (!OpenPublisherHandle())
		return;

	SetCommonOptionToPublisherSDK();

	if (!publisher_wrapper_.StartRecorder())
	{
		Debug.LogError("调用StartRecorder失败..");
		return;
	}

	btn_record_.GetComponentInChildren<Text>().text = "停止录像";

	if (!publisher_wrapper_.is_previewing() && !publisher_wrapper_.is_rtsp_publishing() && !publisher_wrapper_.is_rtmp_publishing())
	{
		StartCaptureAvData();
		coroutine_ = StartCoroutine(OnPostVideo());
	}
}

public void StopRecord()
{
	if (!publisher_wrapper_.is_previewing() && !publisher_wrapper_.is_rtsp_publishing() && !publisher_wrapper_.is_rtmp_publishing())
	{
		StopCaptureAvData();

		if (coroutine_ != null)
		{
			StopCoroutine(coroutine_);
			coroutine_ = null;
		}
	}

	publisher_wrapper_.StopRecorder();
}
  • 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.

Unity下采集camera场景,然后本地预览(见推送端界面左上角回显部分),并采集camera场景的audio数据,推送到RTMP服务,并通过大牛直播SDK的SmartPlayer播放器播放,整体延迟效果如下:

Unity3D下采集camera场景并推送RTMP服务实现毫秒级延迟直播_unity rtmp推流

总结

Unity 下采集 camera 场景并推送 RTMP 服务的使用场景有很多,对延迟、高性能、低功耗和稳定性,有很高的要求,数据源这块,除了采集camera场景外,还可以采集摄像头数据、屏幕数据,或设备自带的USB摄像头等,常用的使用场景比如:

虚拟现实和增强现实应用

  • VR 体验分享:在虚拟现实场景中,用户可以将自己的 VR 体验过程实时分享给其他人。例如,用户佩戴 VR 头显在一个用 Unity 开发的虚拟博物馆中参观,通过采集 camera 场景并推送 RTMP 流,其他人可以在电脑、手机或其他设备上同步观看用户的参观过程,仿佛身临其境。
  • AR 导航直播:在一些特殊的应用场景中,如室内导航、户外探险等,使用 AR 技术结合 Unity 开发的应用可以将用户的视角和导航信息通过 RTMP 推流分享给其他人。比如一个探险队在野外使用 AR 导航应用进行探险,团队成员可以将自己的视角实时推送给后方的指挥中心,以便指挥中心随时了解探险队的位置和情况。

教育与培训领域

  • 虚拟实验教学:学校或教育机构可以使用 Unity 开发虚拟实验场景,如物理实验、化学实验、生物实验等。教师在进行实验教学时,通过采集虚拟实验场景中的 camera 场景并推送到 RTMP 服务器,学生可以在自己的设备上实时观看实验过程,加深对实验原理和操作方法的理解。
  • 远程培训与演示:企业或培训机构在进行远程培训时,可以使用 Unity 开发培训课程的虚拟场景,如机械操作培训、软件使用培训等。培训师在虚拟场景中进行操作演示,通过 RTMP 推流让远程的学员实时观看,学员还可以通过互动功能提问和交流,提高培训效果。

建筑与设计领域

  • 建筑设计展示:建筑师和设计师可以使用 Unity 构建建筑模型和室内设计场景,通过采集 camera 场景并推送 RTMP 流,客户可以远程实时查看设计效果,提出修改意见。例如,一个建筑设计公司为客户设计了一个大型商业建筑,设计师可以将建筑的外观、内部空间布局等通过 RTMP 直播展示给客户,客户可以在不同的设备上查看,如同亲自在建筑中参观一样。
  • 城市规划演示:城市规划部门可以使用 Unity 开发城市规划的虚拟模型,将城市的未来发展规划以直观的方式展示出来。通过 RTMP 推流,政府部门、专家和公众可以远程实时观看城市规划的演示,提出建议和意见,促进城市规划的科学决策。

监控与安防领域

  • 智能监控系统:在一些特殊的监控场景中,如工厂车间、仓库、建筑工地等,使用 Unity 结合监控摄像头可以实现智能监控系统。监控摄像头采集到的画面可以通过 Unity 进行处理和分析,如识别异常行为、检测物体移动等,然后将处理后的画面通过 RTMP 推流到监控中心,安保人员可以实时监控现场情况,及时发现和处理安全隐患。
  • 应急指挥与救援:在应急指挥和救援场景中,现场的救援人员可以使用配备 Unity 应用的移动设备采集现场的 camera 场景,并推送到 RTMP 服务器,指挥中心可以实时了解现场的情况,制定科学的救援方案。例如,在地震、火灾等灾害现场,救援人员可以通过这种方式将现场的情况实时传输给指挥中心,以便指挥中心协调各方面的救援力量。

Unity3D下对camera场景的采集并推送RTMP,特别是高帧率的情况下,对数据采集、编码等,都有非常高的技术要求,以上是大概流程,感兴趣的开发者,可以单独跟我探讨。