• 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服务。

    先说支持的音视频类型:

    1. public void SelVideoPushType(int type)
    2. {
    3. switch (type)
    4. {
    5. case 0:
    6. video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_LAYER; //采集Unity窗体
    7. break;
    8. case 1:
    9. video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_CAMERA; //采集摄像头
    10. break;
    11. case 2:
    12. video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_SCREEN; //采集屏幕
    13. break;
    14. case 3:
    15. video_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_NO_VIDEO; //不采集视频
    16. break;
    17. }
    18. Debug.Log("SelVideoPushType type: " + type + " video_push_type: " + video_push_type_);
    19. }
    20. public void SelAudioPushType(int type)
    21. {
    22. switch (type)
    23. {
    24. case 0:
    25. audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_EXTERNAL_PCM_DATA; //采集Unity声音
    26. break;
    27. case 1:
    28. audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_CAPTURE_MIC; //采集麦克风
    29. break;
    30. case 2:
    31. audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_CAPTURE_SPEAKER; //采集扬声器
    32. break;
    33. case 3:
    34. audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_TWO_EXTERNAL_PCM_MIXER; //两路Unity AudioClip混音测试
    35. break;
    36. case 4:
    37. audio_push_type_ = (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_NO_AUDIO; //不采集音频
    38. break;
    39. }
    40. Debug.Log("SelAudioPushType type: " + type + " audio_push_type: " + audio_push_type_);
    41. }

    采集音视频数据:

    1. private void StartCaptureAvData()
    2. {
    3. if (audio_push_type_ == (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_EXTERNAL_PCM_DATA
    4. || audio_push_type_ == (uint)NTSmartPublisherDefine.NT_PB_E_AUDIO_OPTION.NT_PB_E_AUDIO_OPTION_TWO_EXTERNAL_PCM_MIXER)
    5. {
    6. PostUnityAudioClipData();
    7. }
    8. textures_poll_ = new TexturesPool();
    9. post_image_worker_ = new PostImageWorker(textures_poll_, publisher_wrapper_);
    10. post_worker_thread_ = new Thread(post_image_worker_.run);
    11. post_worker_thread_.Start();
    12. }
    13. private void StopCaptureAvData()
    14. {
    15. if (post_image_worker_ != null)
    16. {
    17. post_image_worker_.requestStop();
    18. post_image_worker_ = null;
    19. }
    20. if (post_worker_thread_ != null)
    21. {
    22. post_worker_thread_.Join();
    23. post_worker_thread_ = null;
    24. }
    25. if (textures_poll_ != null)
    26. {
    27. textures_poll_.clear();
    28. textures_poll_ = null;
    29. }
    30. StopAudioSource();
    31. }

    RTMP推送:

    1. public void btn_start_rtmp_pusher_Click()
    2. {
    3. if (publisher_wrapper_.IsPushingRtmp())
    4. {
    5. StopPushRTMP();
    6. btn_rtmp_pusher_.GetComponentInChildren().text = "推送RTMP";
    7. return;
    8. }
    9. String url = rtmp_pusher_url_.text;
    10. if (url.Length < 8)
    11. {
    12. publisher_wrapper_.Close();
    13. Debug.LogError("请输入RTMP推送地址");
    14. return;
    15. }
    16. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
    17. {
    18. publisher_wrapper_.SetVideoPushType(video_push_type_);
    19. publisher_wrapper_.SetAudioPushType(audio_push_type_);
    20. }
    21. if (!publisher_wrapper_.StartRtmpPusher(url))
    22. {
    23. Debug.LogError("调用StartPublisher失败..");
    24. return;
    25. }
    26. btn_rtmp_pusher_.GetComponentInChildren().text = "停止推送";
    27. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
    28. {
    29. StartCaptureAvData();
    30. coroutine_ = StartCoroutine(OnPostVideo());
    31. }
    32. }

    轻量级RTSP服务相关调用:

    1. public void btn_rtsp_service_Click()
    2. {
    3. if (publisher_wrapper_.IsRtspServiceRunning())
    4. {
    5. publisher_wrapper_.StopRtspService();
    6. btn_rtsp_service_.GetComponentInChildren().text = "启动RTSP服务";
    7. btn_rtsp_publisher_.interactable = false;
    8. return;
    9. }
    10. if (!publisher_wrapper_.StartRtspService())
    11. {
    12. Debug.LogError("调用StartRtspService失败..");
    13. return;
    14. }
    15. btn_rtsp_publisher_.interactable = true;
    16. btn_rtsp_service_.GetComponentInChildren().text = "停止RTSP服务";
    17. }
    18. public void btn_rtsp_publisher_Click()
    19. {
    20. if (publisher_wrapper_.IsRtspPublisherRunning())
    21. {
    22. publisher_wrapper_.StopRtspStream();
    23. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRecording())
    24. {
    25. StopCaptureAvData();
    26. if (coroutine_ != null)
    27. {
    28. StopCoroutine(coroutine_);
    29. coroutine_ = null;
    30. }
    31. }
    32. btn_rtsp_service_.interactable = true;
    33. btn_rtsp_publisher_.GetComponentInChildren().text = "发布RTSP";
    34. }
    35. else
    36. {
    37. if (!publisher_wrapper_.IsRtspServiceRunning())
    38. {
    39. Debug.LogError("RTSP service is not running..");
    40. return;
    41. }
    42. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRecording())
    43. {
    44. publisher_wrapper_.SetVideoPushType(video_push_type_);
    45. publisher_wrapper_.SetAudioPushType(audio_push_type_);
    46. }
    47. publisher_wrapper_.StartRtspStream();
    48. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRecording())
    49. {
    50. StartCaptureAvData();
    51. coroutine_ = StartCoroutine(OnPostVideo());
    52. }
    53. btn_rtsp_publisher_.GetComponentInChildren().text = "停止RTSP";
    54. btn_rtsp_service_.interactable = false;
    55. }
    56. }
    57. public void btn_get_rtsp_session_numbers_Click()
    58. {
    59. if (publisher_wrapper_.IsRtspServiceRunning())
    60. {
    61. btn_get_rtsp_session_numbers_.GetComponentInChildren().text = "RTSP会话数:" + publisher_wrapper_.GetRtspSessionNumbers();
    62. }
    63. }

    实时录像调用:

    1. public void btn_record_Click()
    2. {
    3. if (publisher_wrapper_.IsRecording())
    4. {
    5. StopRecord();
    6. btn_record_.GetComponentInChildren().text = "开始录像";
    7. return;
    8. }
    9. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsPushingRtmp())
    10. {
    11. publisher_wrapper_.SetVideoPushType(video_push_type_);
    12. publisher_wrapper_.SetAudioPushType(audio_push_type_);
    13. }
    14. if (!publisher_wrapper_.StartRecorder())
    15. {
    16. Debug.LogError("调用StartRecorder失败..");
    17. return;
    18. }
    19. btn_record_.GetComponentInChildren().text = "停止录像";
    20. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsPushingRtmp())
    21. {
    22. StartCaptureAvData();
    23. coroutine_ = StartCoroutine(OnPostVideo());
    24. }
    25. }
    26. public void StopRecord()
    27. {
    28. if (!publisher_wrapper_.IsPreviewing() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsPushingRtmp())
    29. {
    30. StopCaptureAvData();
    31. if (coroutine_ != null)
    32. {
    33. StopCoroutine(coroutine_);
    34. coroutine_ = null;
    35. }
    36. }
    37. publisher_wrapper_.StopRecorder();
    38. }
    39. private void btn_pause_record_Click()
    40. {
    41. if (!publisher_wrapper_.IsPublisherHandleAvailable())
    42. {
    43. return;
    44. }
    45. String btn_pause_rec_text = btn_pause_record_.GetComponentInChildren().text;
    46. if ("暂停录像" == btn_pause_rec_text)
    47. {
    48. UInt32 ret = publisher_wrapper_.PauseRecorder(true);
    49. if ((UInt32)NT.NTSmartPublisherDefine.NT_PB_E_ERROR_CODE.NT_ERC_PB_NEED_RETRY == ret)
    50. {
    51. Debug.LogError("暂停录像失败, 请重新尝试!");
    52. return;
    53. }
    54. else if (NTBaseCodeDefine.NT_ERC_OK == ret)
    55. {
    56. btn_pause_record_.GetComponentInChildren().text = "恢复录像";
    57. }
    58. }
    59. else
    60. {
    61. UInt32 ret = publisher_wrapper_.PauseRecorder(false);
    62. if ((UInt32)NT.NTSmartPublisherDefine.NT_PB_E_ERROR_CODE.NT_ERC_PB_NEED_RETRY == ret)
    63. {
    64. Debug.LogError("恢复录像失败, 请重新尝试!");
    65. return;
    66. }
    67. else if (NTBaseCodeDefine.NT_ERC_OK == ret)
    68. {
    69. btn_pause_record_.GetComponentInChildren().text = "暂停录像";
    70. }
    71. }
    72. }

    实时预览相关:

    1. public void btn_preview_Click()
    2. {
    3. if (btn_preview_.GetComponentInChildren().text.Equals("本地预览"))
    4. {
    5. if (!publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
    6. {
    7. publisher_wrapper_.SetVideoPushType(video_push_type_);
    8. }
    9. if (publisher_wrapper_.StartPreview())
    10. {
    11. btn_preview_.GetComponentInChildren().text = "停止预览";
    12. }
    13. if (!publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
    14. {
    15. StartCaptureAvData();
    16. coroutine_ = StartCoroutine(OnPostVideo());
    17. }
    18. }
    19. else
    20. {
    21. if (!publisher_wrapper_.IsPushingRtmp() && !publisher_wrapper_.IsRtspPublisherRunning() && !publisher_wrapper_.IsRecording())
    22. {
    23. StopCaptureAvData();
    24. if (coroutine_ != null)
    25. {
    26. StopCoroutine(coroutine_);
    27. coroutine_ = null;
    28. }
    29. }
    30. publisher_wrapper_.StopPreview();
    31. btn_preview_.GetComponentInChildren().text = "本地预览";
    32. }
    33. }

    总结

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

  • 相关阅读:
    字符串压缩(一)之ZSTD
    代码随想录第45天|70. 爬楼梯,322. 零钱兑换,279.完全平方数
    1029 旧键盘
    【特征选取】计算数据点曲率
    反压缩 js ,我的万花筒写轮眼开了,CV 能力大幅提升
    深入理解React中的useState:函数组件状态管理的利器
    ImageNet数据集用法
    <C++>深度学习多态
    TASK05|GITMODEL数据可视化plotnine
    JS-运算符
  • 原文地址:https://blog.csdn.net/renhui1112/article/details/134554456