Windows平台Unity下实现camera场景推送RTMP|轻量级RTSP服务|实时录像

技术背景

我们在对接Unity平台camera场景采集的时候,除了常规的RTMP推送、录像外,还有一些开发者,需要能实现轻量级RTSP服务,对外提供个拉流的RTSP URL。

目前我们在Windows平台Unity下数据源可采集到以下部分:

  • 采集Unity camera场景;
  • 采集摄像头;
  • 采集屏幕;
  • 采集Unity声音;
  • 采集麦克风;
  • 采集扬声器;
  • Unity PCM混音;

对外提供的技术能力有:

  • RTMP直播推送;
  • 轻量级RTSP服务;
  • 实时录像、暂停|恢复录像;
  • 实时预览。

以下录制下来的MP4文件是采集Unity camera场景,音频是unity声音。

Unity平台实现camera场景实时录像

技术实现

实际上,在实现Unity平台音视频能力之前,我们原生模块已经有非常成熟的技术积累,Unity下还是调用的原生的推送模块,不同的是,数据源需要采集Unity的audio、video,然后高效的投递到底层模块,底层模块负责编码打包,并投递到RTMP或RTSP服务。

先说支持的音视频类型:

    public void SelVideoPushType(int type)
    {
        switch (type)
        {
            case 0:
                video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_LAYER;    //采集Unity窗体
                break;
            case 1:
                video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_CAMERA;   //采集摄像头
                break;
            case 2:
                video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_SCREEN;   //采集屏幕
                break;
            case 3:
                video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_NO_VIDEO; //不采集视频
                break;
        }

        Debug.Log("SelVideoPushType type: " + type + " video_push_type: " + video_push_type_);
    }

    public void SelAudioPushType(int type)
    {
        switch (type)
        {
            case 0:
                audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_EXTERNAL_PCM_DATA;    //采集Unity声音
                break;
            case 1:
                audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_CAPTURE_MIC;  //采集麦克风
                break;
            case 2:
                audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_CAPTURE_SPEAKER;  //采集扬声器
                break;
            case 3:
                audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_TWO_EXTERNAL_PCM_MIXER;  //两路Unity AudioClip混音测试
                break;
            case 4:
                audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_NO_AUDIO;   //不采集音频
                break;
        }

        Debug.Log("SelAudioPushType type: " + type + " audio_push_type: " + audio_push_type_);
    }

采集音视频数据:

    private void StartCaptureAvData()
    {
        if (audio_push_type_ == (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_EXTERNAL_PCM_DATA
            || audio_push_type_ == (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_TWO_EXTERNAL_PCM_MIXER)
        {
            PostUnityAudioClipData();
        }

        textures_poll_ = new TexturesPool();
        post_image_worker_ = new PostImageWorker(textures_poll_, publisher_wrapper_);
        post_worker_thread_ = new Thread(post_image_worker_.run);
        post_worker_thread_.Start();
    }

    private void StopCaptureAvData()
    {
        if (post_image_worker_ != null)
        {
            post_image_worker_.requestStop();
            post_image_worker_ = null;
        }

        if (post_worker_thread_ != null)
        {
            post_worker_thread_.Join();
            post_worker_thread_ = null;
        }

        if (textures_poll_ != null)
        {
            textures_poll_.clear();
            textures_poll_ = null;
        }

        StopAudioSource();
    }

RTMP推送:

    public void btn_start_rtmp_pusher_Click()
    {
        if (publisher_wrapper_.IsPushingRtmp())
        {
            StopPushRTMP();
            btn_rtmp_pusher_.GetComponentInChildren<Text>().text = "推送RTMP";

            return;
        }

        String url = rtmp_pusher_url_.text;

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

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

        if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
        {
            publisher_wrapper_.SetVideoPushType(video_push_type_);
            publisher_wrapper_.SetAudioPushType(audio_push_type_);
        }

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

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

        if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
        {
            StartCaptureAvData();
            coroutine_ = StartCoroutine(OnPostVideo());
        }
    }

轻量级RTSP服务相关调用:

    public void btn_rtsp_service_Click()
    {
        if (publisher_wrapper_.IsRtspServiceRunning())
        {
            publisher_wrapper_.StopRtspService();
            btn_rtsp_service_.GetComponentInChildren<Text>().text = "启动RTSP服务";

            btn_rtsp_publisher_.interactable = false;
            return;
        }

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

        btn_rtsp_publisher_.interactable = true;

        btn_rtsp_service_.GetComponentInChildren<Text>().text = "停止RTSP服务";
    }

    public void btn_rtsp_publisher_Click()
    {
        if (publisher_wrapper_.IsRtspPublisherRunning())
        {
            publisher_wrapper_.StopRtspStream();

            if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRecording())
            {
                StopCaptureAvData();

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

            btn_rtsp_service_.interactable = true;

            btn_rtsp_publisher_.GetComponentInChildren<Text>().text = "发布RTSP";
        }
        else
        {
            if (!publisher_wrapper_.IsRtspServiceRunning())
            {
                Debug.LogError("RTSP service is not running..");
                return;
            }


            if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRecording())
            {
                publisher_wrapper_.SetVideoPushType(video_push_type_);
                publisher_wrapper_.SetAudioPushType(audio_push_type_);
            }

            publisher_wrapper_.StartRtspStream();

            if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRecording())
            {
                StartCaptureAvData();
                coroutine_ = StartCoroutine(OnPostVideo());
            }

            btn_rtsp_publisher_.GetComponentInChildren<Text>().text = "停止RTSP";

            btn_rtsp_service_.interactable = false;
        }
    }

    public void btn_get_rtsp_session_numbers_Click()
    {
        if (publisher_wrapper_.IsRtspServiceRunning())
        {
            btn_get_rtsp_session_numbers_.GetComponentInChildren<Text>().text = "RTSP会话数:" + publisher_wrapper_.GetRtspSessionNumbers();
        }
    }

实时录像调用:

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

            return;
        }

        if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsPushingRtmp())
        {
            publisher_wrapper_.SetVideoPushType(video_push_type_);
            publisher_wrapper_.SetAudioPushType(audio_push_type_);
        }

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

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

        if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsPushingRtmp())
        {
            StartCaptureAvData();
            coroutine_ = StartCoroutine(OnPostVideo());
        }
    }

    public void StopRecord()
    {
        if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsPushingRtmp())
        {
            StopCaptureAvData();

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

        publisher_wrapper_.StopRecorder();
    }

    private void btn_pause_record_Click()
    {
        if (!publisher_wrapper_.IsPublisherHandleAvailable())
        {
            return;
        }

        String btn_pause_rec_text = btn_pause_record_.GetComponentInChildren<Text>().text;

        if ("暂停录像" == btn_pause_rec_text)
        {
            UInt32 ret = publisher_wrapper_.PauseRecorder(true);

            if ((UInt32)NT.NTSmartPublisherDefine.NT_PB_E_ERROR_CODE.NT_ERC_PB_NEED_RETRY == ret)
            {
                Debug.LogError("暂停录像失败, 请重新尝试!");
                return;
            }
            else if (NTBaseCodeDefine.NT_ERC_OK == ret)
            {
                btn_pause_record_.GetComponentInChildren<Text>().text = "恢复录像";
            }
        }
        else
        {
            UInt32 ret = publisher_wrapper_.PauseRecorder(false);
            if ((UInt32)NT.NTSmartPublisherDefine.NT_PB_E_ERROR_CODE.NT_ERC_PB_NEED_RETRY == ret)
            {
                Debug.LogError("恢复录像失败, 请重新尝试!");
                return;
            }
            else if (NTBaseCodeDefine.NT_ERC_OK == ret)
            {
                btn_pause_record_.GetComponentInChildren<Text>().text = "暂停录像";
            }
        }
    }

实时预览相关:

    public void btn_preview_Click()
    {
        if (btn_preview_.GetComponentInChildren<Text>().text.Equals("本地预览"))
        {
            if (!publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
            {
                publisher_wrapper_.SetVideoPushType(video_push_type_);
            }

            if (publisher_wrapper_.StartPreview())
            {
                btn_preview_.GetComponentInChildren<Text>().text = "停止预览";
            }

            if (!publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
            {
                StartCaptureAvData();
                coroutine_ = StartCoroutine(OnPostVideo());
            }
        }
        else
        {
            if (!publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
            {
                StopCaptureAvData();

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

            publisher_wrapper_.StopPreview();
            btn_preview_.GetComponentInChildren<Text>().text = "本地预览";
        }
    }

总结

Unity平台下RTMP推送、录像、轻量级RTSP服务,在虚拟仿真、医疗、教育等场景下,应用非常广泛。要实现低延迟,除了需要高效率的音视频数据采集,编码和数据投递外,还需要好的直播播放器支持。配合我们的SmartPlayer,可轻松实现毫秒级体验,满足绝大多数应用场景技术诉求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/182144.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

反转链表系列问题

反转链表系列问题 作者&#xff1a;Grey 原文地址&#xff1a; 博客园&#xff1a;反转链表系列问题 CSDN&#xff1a;反转链表系列问题 反转单链表 题目描述见&#xff1a;LeetCode 206. Reverse Linked List 思路如下 对于任何一个节点 cur 来说&#xff0c;记录一个…

01-详细介绍函数式接口和Lambda表达式语法

函数式接口介绍 如果在一个接口中只声明了一个抽象方法,则此接口就被称为函数式接口(该接口可以包含其他非抽象方法) 接口上使用FunctionalInterface注解可以验证该接口是否为函数式接口,javadoc生成的文档时也会保留该注解, 若接口中有多个抽象方法编译器会报错 随着Python…

经典的回溯算法题leetcode组合问题整理及思路代码详解

目录 组合问题 leetcode77题.组合 leetcode216题.组合总和III leetcode40题.组合总和II leetcode39题.组合总和 倘若各位不太清楚回溯算法可以去看我上一篇文章。 回溯算法详解-CSDN博客 组合问题 一般组合和排列类的问题我们都会转化成一个树形问题&#xff0c;更便于…

(二)C语言之变量与算数运算表达式概述

C语言之变量与算数运算表达式概述 一、华氏温度与摄氏温度对照二、代码概述三、练习 一、华氏温度与摄氏温度对照 #include <stdio.h>/*当华氏温度为 0,20,40,...300时&#xff0c;打印出华氏温度与摄氏温度对照表华氏温度与摄氏温度 C(5/9)(̧F-32) 其中C表示摄氏温度&…

CANdelaStudio 使用教程 1

文章目录 CANdelaStudio 软件下载CANdelaStudio 软件的权限View Edition 和 Admin Edition 区别&#xff1a;打开文件 CDD / CDDT 文件新建 CDD 文件新建 CDDT 文件CDD 和 CDDT 文件的区别 CANdelaStudio 软件下载 1、 来到 Vector 官网下载中心 https://www.vector.com/cn/zh…

机器学习【01】相关环境的安装

学习实例 参考资料&#xff1a;联邦学习实战{杨强}https://book.douban.com/subject/35436587/ 项目地址&#xff1a;https://github.com/FederatedAI/Practicing-Federated-Learning/tree/main/chapter03_Python_image_classification 一、环境准备 GPU安装CUDA、cuDNN pytho…

【Python】批量将PDG合成PDF,以及根据SS号重命名秒传的文件

目录 说明批量zip2pdf批量zip2pdf下载SS号重命名源代码SS号重命名源代码下载附录&#xff0c;水文年鉴 说明 1、zip2pdf是一个开源软件&#xff0c;支持自动化解压压缩包成PDG&#xff0c;PDG合成PDF&#xff0c;笔者在其基础上做了部分修改&#xff0c;支持批量转换。 2、秒…

23年下半年软考成绩查询时间是什么时候?

一、成绩查询时间 2023年下半年软考成绩查询时间预计2023年12月份公布&#xff0c;成绩查询入口为计算机技术职业资格网&#xff08;全国统一成绩查询时间&#xff0c;统一查询入口&#xff09;。 二、成绩查询方法 登陆中国计算机技术职业资格网&#xff0c;点击“成绩查询”…

【Python】Fastapi swagger-ui.css 、swagger-ui-bundle.js 无法加载,docs无法加载,redocs无法使用

使用fastapi的时候&#xff0c;swagger-ui.css 、swagger-ui-bundle.js、redoc.standalone.js 有时候无法加载&#xff08;国内环境原因或者是局域网屏蔽&#xff09;&#xff0c;此时就需要自己用魔法下载好对应文件&#xff0c;然后替换到fastapi里面去。 fastapi里面依靠这…

通过ros系统中websocket中发送sensor_msgs::Image数据给web端显示

通过ros系统中websocket中发送sensor_msgs::Image数据给web端显示 #include <ros/ros.h> #include <signal.h> #include <sensor_msgs/Image.h> #include <message_filters/subscriber.h> #include <message_filters/synchronizer.h> #include &…

人工智能今天能为你做什么?生成式人工智能如何改变技术文档领域

▲ 搜索“大龙谈智能内容”关注GongZongHao▲ 作者 | Fabrice Lacroix 大型语言模型&#xff08;LLM&#xff09;和生成式人工智能&#xff08;GenAI&#xff09;&#xff0c;尤其是ChatGPT&#xff0c;这些是引领科技革新的新兴技术。它们不仅在科技界引起了轩然大波&#x…

【C++】拷贝构造函数,析构函数详解!

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

【MATLAB源码-第87期】基于matlab的Q-learning算法栅格地图路径规划,自主选择起始点和障碍物。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 Q-learning是一种无模型的强化学习算法&#xff0c;适用于有限的马尔可夫决策过程&#xff08;MDP&#xff09;。它的核心是学习一个动作价值函数&#xff08;action-value function&#xff09;&#xff0c;即Q函数&#xf…

脉冲幅度调制信号的功率谱计算

本篇文章是博主在通信等领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对人工智能等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅解。文章分类在通信领域笔记&#xf…

12.docker的网络-host模式

1.docker的host网络模式简介 host模式下&#xff0c;容器将不会虚拟出自己的网卡、配置IP等&#xff0c;而是使用宿主机的IP和端口&#xff1b;也就说&#xff0c;宿主机的就是我的。 2. 以host网络模式创建容器 2.1 创建容器 我们仍然以tomcat这个镜像来说明一下。我们以h…

Python入门教程 | Python3 字典(dict)

Python3 字典 字典是另一种可变容器模型&#xff0c;且可存储任意类型对象。 Python3中的字典是一种无序、可变、可迭代的数据结构&#xff0c;它由键&#xff08;key&#xff09;和对应的值&#xff08;value&#xff09;组成。字典在Python中被视为可变对象&#xff0c;这意…

离散下学期提纲

概览 Realtions(关系)Semigroups and Groups(群和半群)Groups and coding(群和码) Advanced Counting Techniques(高级计数技巧) Groups(图)Trees(树) 关系 Relations and their properties(关系及关系性质)n-ary relations and their applicaitons(n元关系及应用)Represent…

SQL LIKE 运算符:用法、示例和通配符解释

SQL中的LIKE运算符用于在WHERE子句中搜索列中的指定模式。通常与LIKE运算符一起使用的有两个通配符&#xff1a; 百分号 % 代表零个、一个或多个字符。下划线 _ 代表一个单个字符。 以下是LIKE运算符的用法和示例&#xff1a; 示例 选择所有以字母 “a” 开头的客户&#x…

飞翔的小鸟游戏

一.建一个bird的类&#xff0c;放入素材 二.代码 1.Bird类 package bird;import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException;/** 小鸟类* */ public class Bird {int x;// 坐标int y;int width; // 宽高int height;BufferedIm…

机器学习---最大似然估计和贝叶斯参数估计

1. 估计 贝叶斯框架下的数据收集&#xff0c;在以下条件下我们可以设计一个可选择的分类器 : P(wi) (先验)&#xff1b;P(x | wi) (类条件密度) 但是。我们很少能够完整的得到这些信息! 从一个传统的样本中设计一个分类器&#xff1a; ①先验估计不成问题 ②对类条件密度…