• Unity3D下如何实现跨平台低延迟的RTMP、RTSP播放


     技术背景

    好多开发者,希望我们能探讨下Unity平台RTMP或RTSP直播流数据播放和录制相关的模块,实际上,这块流程我们已经聊过多次,无非就是通过原生的RTMP或者RTSP模块,先从协议层拉取到数据,并解包解码,回调YUV或RGB数据,然后,在Unity创建响应的shader,获取图像数据填充纹理即可,说起来流程很简单,但是每个环节,如果做到极致体验,都非常难。简单来说,多一次拷贝,都会增大性能瓶颈或延迟。

    目前,Unity3D下,我们覆盖了以下常用的模块:

    • Windows平台RTMP直播推送模块(采集Unity窗体、摄像头或屏幕);
    • Windows平台轻量级RTSP服务模块(采集Unity窗体、摄像头或屏幕);
    • Windows平台RTMP|RTSP直播播放模块;
    • Linux平台RTMP直播推送模块(采集Unity窗体、Unity声音),也可扩展轻量级RTSP服务模块;
    • Linux平台RTMP|RTSP直播播放模块;
    • Android平台RTMP直播推送模块(采集Unity窗体、麦克风或Unity声音);
    • Android平台轻量级RTSP服务模块(采集Unity窗体、麦克风或Unity声音);
    • Android平台RTMP|RTSP直播播放模块;
    • iOS平台RTMP|RTSP直播播放模块。

    下图系Linux平台RTMP播放图,可以看到,延迟非常低。

    技术实现

    本文以Android平台RTMP、RTSP播放模块为例,介绍下Unity相关接口设置和逻辑处理:

    开始播放

    1. public void Play()
    2. {
    3. if (is_running)
    4. {
    5. Debug.Log("已经在播放。。");
    6. return;
    7. }
    8. //获取输入框的url
    9. string videoUrl = input_url_.text.Trim();
    10. OpenPlayer();
    11. if ( player_handle_ == 0 )
    12. return;
    13. NT_U3D_Set_Game_Object(player_handle_, game_object_);
    14. /* ++ 播放前参数配置可加在此处 ++ */
    15. int is_using_tcp = 0; //TCP/UDP模式设置
    16. NT_U3D_SetRTSPTcpMode(player_handle_, is_using_tcp);
    17. int is_report = 0;
    18. int report_interval = 1;
    19. NT_U3D_SetReportDownloadSpeed(player_handle_, is_report, report_interval); //下载速度回调
    20. NT_U3D_SetBuffer(player_handle_, play_buffer_time_); //设置buffer time
    21. NT_U3D_SetPlayerLowLatencyMode(player_handle_, is_low_latency_ ? 1 : 0); //设置是否启用低延迟模式
    22. NT_U3D_SetMute(player_handle_, is_mute_ ? 1 : 0); //是否启动播放的时候静音
    23. NT_U3D_SetAudioVolume(player_handle_, cur_audio_volume_); //设置播放音量
    24. NT_U3D_SetVideoDecoderMode(player_handle_, is_hw_decode_ ? 1 : 0); //设置H.264软硬解模式
    25. NT_U3D_SetVideoHevcDecoderMode(player_handle_, is_hw_decode_ ? 1 : 0); //设置H.265软硬解模式
    26. int is_fast_startup = 1;
    27. NT_U3D_SetFastStartup(player_handle_, is_fast_startup); //设置快速启动模式
    28. int rtsp_timeout = 10;
    29. NT_U3D_SetRTSPTimeout(player_handle_, rtsp_timeout); //设置RTSP超时时间
    30. int is_auto_switch_tcp_udp = 1;
    31. NT_U3D_SetRTSPAutoSwitchTcpUdp(player_handle_, is_auto_switch_tcp_udp); //设置TCP/UDP模式自动切换
    32. int is_audiotrack = 1;
    33. NT_U3D_SetAudioOutputType(player_handle_, is_audiotrack); //设置音频输出模式: if 0: 自动选择; if with 1: audiotrack模式
    34. NT_U3D_SetUrl(player_handle_, videoUrl);
    35. /* -- 播放前参数配置可加在此处 -- */
    36. int flag = NT_U3D_StartPlay(player_handle_);
    37. if (flag == DANIULIVE_RETURN_OK)
    38. {
    39. is_need_get_frame_ = true;
    40. Debug.Log("播放成功");
    41. }
    42. else
    43. {
    44. is_need_get_frame_ = false;
    45. Debug.LogError("播放失败");
    46. }
    47. is_running = true;
    48. }

    这里调用了OpenPlayer()设计如下:

    1. private void OpenPlayer()
    2. {
    3. if ( java_obj_cur_activity_ == null )
    4. {
    5. Debug.LogError("getApplicationContext is null");
    6. return;
    7. }
    8. player_handle_ = NT_U3D_Open();
    9. if (player_handle_ != 0)
    10. Debug.Log("open success");
    11. else
    12. Debug.LogError("open fail");
    13. }

    停止播放:

    1. private void ClosePlayer()
    2. {
    3. is_need_get_frame_ = false;
    4. is_need_init_texture_ = false;
    5. int flag = NT_U3D_StopPlay(player_handle_);
    6. if (flag == DANIULIVE_RETURN_OK)
    7. {
    8. Debug.Log("停止成功");
    9. }
    10. else
    11. {
    12. Debug.LogError("停止失败");
    13. }
    14. flag = NT_U3D_Close(player_handle_);
    15. if (flag == DANIULIVE_RETURN_OK)
    16. {
    17. Debug.Log("关闭成功");
    18. }
    19. else
    20. {
    21. Debug.LogError("关闭失败");
    22. }
    23. player_handle_ = 0;
    24. NT_U3D_UnInit();
    25. is_running = false;
    26. video_width_ = 0;
    27. video_height_ = 0;
    28. }

    Event状态回调处理:

    1. ///
    2. /// android 传递过来 code
    3. ///
    4. ///
    5. public void onNTSmartEvent(string event_message)
    6. {
    7. if (null == event_message || event_message.Length < 1)
    8. return;
    9. string[] strs = event_message.Split(',');
    10. if (null == strs || strs.Length < 6)
    11. return;
    12. string player_handle =strs[0];
    13. string code = strs[1];
    14. string param1 = strs[2];
    15. string param2 = strs[3];
    16. string param3 = strs[4];
    17. string param4 = strs[5];
    18. Debug.Log("[onNTSmartEvent] code: 0x" + Convert.ToString(Convert.ToInt32(code), 16));
    19. String player_event = "";
    20. switch (Convert.ToInt32(code))
    21. {
    22. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STARTED:
    23. player_event = "开始..";
    24. break;
    25. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING:
    26. player_event = "连接中..";
    27. break;
    28. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED:
    29. player_event = "连接失败..";
    30. break;
    31. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED:
    32. player_event = "连接成功..";
    33. break;
    34. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED:
    35. player_event = "连接断开..";
    36. break;
    37. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STOP:
    38. player_event = "停止播放..";
    39. break;
    40. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO:
    41. player_event = "分辨率信息: width: " + Convert.ToInt32(param1) + ", height: " + Convert.ToInt32(param2);
    42. break;
    43. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED:
    44. player_event = "收不到媒体数据,可能是url错误..";
    45. break;
    46. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL:
    47. player_event = "切换播放URL..";
    48. break;
    49. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE:
    50. player_event = "快照: " + param1 + " 路径:" + param3;
    51. if (Convert.ToInt32(param1) == 0)
    52. {
    53. player_event = "截取快照成功..";
    54. }
    55. else
    56. {
    57. player_event = "截取快照失败..";
    58. }
    59. break;
    60. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE:
    61. player_event = "[record]开始一个新的录像文件 : " + param3;
    62. break;
    63. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED:
    64. player_event = "[record]已生成一个录像文件 : " + param3;
    65. break;
    66. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING:
    67. player_event = "Start_Buffering..";
    68. break;
    69. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_BUFFERING:
    70. player_event = "Buffering: " + Convert.ToInt32(param1);
    71. break;
    72. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING:
    73. player_event = "Stop_Buffering..";
    74. break;
    75. case EVENTID.EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED:
    76. player_event = "download_speed:" + param1 + "Byte/s" + ", "
    77. + (Convert.ToInt32(param1) * 8 / 1000) + "kbps" + ", " + (Convert.ToInt32(param1) / 1024)
    78. + "KB/s";
    79. break;
    80. }
    81. Debug.Log(player_event);
    82. player_event = null;
    83. strs = null;
    84. }

    如果想扩展录像,实际上,我们也针对播放端录像做了接口的封装设计,整体接口设计如下:

    1. ///
    2. /// SmartPlayer Unity Interface
    3. /// Author: daniusdk.com
    4. ///
    5. ///
    6. /// Init
    7. ///
    8. public int NT_U3D_Init()
    9. {
    10. return player_obj_.Call<int>("Init", java_obj_cur_activity_);
    11. }
    12. ///
    13. /// 开始
    14. /// 返回播放句柄
    15. ///
    16. public long NT_U3D_Open()
    17. {
    18. return player_obj_.Call<long>("Open");
    19. }
    20. ///
    21. /// Register Game Object,用于消息传递
    22. ///
    23. public int NT_U3D_Set_Game_Object(long handle, string gameObjectName)
    24. {
    25. return player_obj_.Call<int>("SetGameObject", handle, gameObjectName);
    26. }
    27. ///
    28. /// 设置H.264解码方式 false 软件解码 true 硬件解码 默认为false
    29. ///
    30. ///
    31. public int NT_U3D_SetVideoDecoderMode(long handle, int isHwDecoder)
    32. {
    33. return player_obj_.Call<int>("SetPlayerVideoHWDecoder", handle, isHwDecoder);
    34. }
    35. ///
    36. /// 设置H.265 解码方式 false 软件解码 true 硬件解码 默认为false
    37. ///
    38. ///
    39. public int NT_U3D_SetVideoHevcDecoderMode(long handle, int isHevcHwDecoder)
    40. {
    41. return player_obj_.Call<int>("SetPlayerVideoHevcHWDecoder", handle, isHevcHwDecoder);
    42. }
    43. ///
    44. /// 设置音频输出模式: if 0: 自动选择; if with 1: audiotrack模式
    45. ///
    46. ///
    47. public int NT_U3D_SetAudioOutputType(long handle, int use_audiotrack)
    48. {
    49. return player_obj_.Call<int>("SetAudioOutputType", handle, use_audiotrack);
    50. }
    51. ///
    52. /// 设置播放端缓存大小, 默认200毫秒
    53. ///
    54. ///
    55. public int NT_U3D_SetBuffer(long handle, int buffer)
    56. {
    57. return player_obj_.Call<int>("SetBuffer", handle, buffer);
    58. }
    59. ///
    60. /// 接口可实时调用:设置是否实时静音,1:静音; 0: 取消静音
    61. ///
    62. ///
    63. public int NT_U3D_SetMute(long handle, int is_mute)
    64. {
    65. return player_obj_.Call<int>("SetMute", handle, is_mute);
    66. }
    67. ///
    68. /// 接口可实时调用:设置播放音量,范围是[0, 100], 0是静音,100是最大音量, 默认是100
    69. ///
    70. ///
    71. public int NT_U3D_SetAudioVolume(long handle, int audio_volume)
    72. {
    73. return player_obj_.Call<int>("SetAudioVolume", handle, audio_volume);
    74. }
    75. ///
    76. /// 设置RTSP TCP模式, 1: TCP; 0: UDP
    77. ///
    78. ///
    79. public int NT_U3D_SetRTSPTcpMode(long handle, int is_using_tcp)
    80. {
    81. return player_obj_.Call<int>("SetRTSPTcpMode", handle, is_using_tcp);
    82. }
    83. ///
    84. /// 设置RTSP超时时间, timeout单位为秒,必须大于0
    85. ///
    86. ///
    87. public int NT_U3D_SetRTSPTimeout(long handle, int timeout)
    88. {
    89. return player_obj_.Call<int>("SetRTSPTimeout", handle, timeout);
    90. }
    91. ///
    92. /// 设置RTSP TCP/UDP自动切换
    93. /// NOTE: 对于RTSP来说,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式.
    94. /// 为了方便使用,有些场景下可以开启自动尝试切换开关, 打开后如果udp无法播放,sdk会自动尝试tcp, 如果tcp方式播放不了,sdk会自动尝试udp.
    95. ///
    96. ///
    97. /// timeout:如果设置1的话, sdk将在tcp和udp之间尝试切换播放,如果设置为0,则不尝试切换.
    98. public int NT_U3D_SetRTSPAutoSwitchTcpUdp(long handle, int is_auto_switch_tcp_udp)
    99. {
    100. return player_obj_.Call<int>("SetRTSPAutoSwitchTcpUdp", handle, is_auto_switch_tcp_udp);
    101. }
    102. ///
    103. /// 设置快速启动该模式,
    104. ///
    105. ///
    106. public int NT_U3D_SetFastStartup(long handle, int is_fast_startup)
    107. {
    108. return player_obj_.Call<int>("SetFastStartup", handle, is_fast_startup);
    109. }
    110. ///
    111. /// 设置超低延迟模式 false不开启 true开启 默认false
    112. ///
    113. ///
    114. public int NT_U3D_SetPlayerLowLatencyMode(long handle, int mode)
    115. {
    116. return player_obj_.Call<int>("SetPlayerLowLatencyMode", handle, mode);
    117. }
    118. ///
    119. /// 设置视频垂直反转
    120. /// is_flip: 0: 不反转, 1: 反转
    121. ///
    122. ///
    123. public int NT_U3D_SetFlipVertical(long handle, int is_flip)
    124. {
    125. return player_obj_.Call<int>("SetFlipVertical", handle, is_flip);
    126. }
    127. ///
    128. /// 设置视频水平反转
    129. /// is_flip: 0: 不反转, 1: 反转
    130. ///
    131. ///
    132. public int NT_U3D_SetFlipHorizontal(long handle, int is_flip)
    133. {
    134. return player_obj_.Call<int>("SetFlipHorizontal", handle, is_flip);
    135. }
    136. ///
    137. /// 设置顺时针旋转, 注意除了0度之外, 其他角度都会额外消耗性能
    138. /// degress: 当前支持 0度,90度, 180度, 270度 旋转
    139. ///
    140. ///
    141. public int NT_U3D_SetRotation(long handle, int degress)
    142. {
    143. return player_obj_.Call<int>("SetRotation", handle, degress);
    144. }
    145. ///
    146. /// 设置是否回调下载速度
    147. /// is_report: if 1: 上报下载速度, 0: 不上报.
    148. /// report_interval: 上报间隔,以秒为单位,>0.
    149. ///
    150. ///
    151. ///
    152. public int NT_U3D_SetReportDownloadSpeed(long handle, int is_report, int report_interval)
    153. {
    154. return player_obj_.Call<int>("SetReportDownloadSpeed", handle, is_report, report_interval);
    155. }
    156. ///
    157. /// 设置是否需要在播放或录像过程中快照
    158. ///
    159. ///
    160. public int NT_U3D_SetSaveImageFlag(long handle, int is_save_image)
    161. {
    162. return player_obj_.Call<int>("SetSaveImageFlag", handle, is_save_image);
    163. }
    164. ///
    165. /// 播放或录像过程中快照
    166. ///
    167. ///
    168. public int NT_U3D_SaveCurImage(long handle, string imageName)
    169. {
    170. return player_obj_.Call<int>("SaveCurImage", handle, imageName);
    171. }
    172. ///
    173. /// 播放或录像过程中,快速切换url
    174. ///
    175. ///
    176. public int NT_U3D_SwitchPlaybackUrl(long handle, string uri)
    177. {
    178. return player_obj_.Call<int>("SwitchPlaybackUrl", handle, uri);
    179. }
    180. ///
    181. /// 创建录像存储路径
    182. ///
    183. ///
    184. public int NT_U3D_CreateFileDirectory(string path)
    185. {
    186. return player_obj_.Call<int>("CreateFileDirectory", path);
    187. }
    188. ///
    189. /// 设置录像存储路径
    190. ///
    191. ///
    192. public int NT_U3D_SetRecorderDirectory(long handle, string path)
    193. {
    194. return player_obj_.Call<int>("SetRecorderDirectory", handle, path);
    195. }
    196. ///
    197. /// 设置单个录像文件大小
    198. ///
    199. ///
    200. public int NT_U3D_SetRecorderFileMaxSize(long handle, int size)
    201. {
    202. return player_obj_.Call<int>("SetRecorderFileMaxSize", handle, size);
    203. }
    204. ///
    205. /// 设置录像时音频转AAC编码的开关
    206. /// aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.
    207. /// 注意: 转码会增加性能消耗
    208. ///
    209. ///
    210. /// is_transcode:设置为1的话,如果音频编码不是aac,则转成aac,如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.
    211. public int NT_U3D_SetRecorderAudioTranscodeAAC(long handle, int is_transcode)
    212. {
    213. return player_obj_.Call<int>("SetRecorderAudioTranscodeAAC", handle, is_transcode);
    214. }
    215. ///
    216. /// 设置播放路径
    217. ///
    218. public int NT_U3D_SetUrl(long handle, string url)
    219. {
    220. return player_obj_.Call<int>("SetUrl", handle, url);
    221. }
    222. ///
    223. /// 开始播放
    224. ///
    225. public int NT_U3D_StartPlay(long handle)
    226. {
    227. return player_obj_.Call<int>("StartPlay", handle);
    228. }
    229. ///
    230. /// 获取YUV数据
    231. ///
    232. public AndroidJavaObject NT_U3D_GetVideoFrame(long handle)
    233. {
    234. return player_obj_.Call("GetVideoFrame", handle);
    235. }
    236. ///
    237. /// 停止播放
    238. ///
    239. public int NT_U3D_StopPlay(long handle)
    240. {
    241. return player_obj_.Call<int>("StopPlay", handle);
    242. }
    243. ///
    244. /// 开始录像
    245. ///
    246. public int NT_U3D_StartRecorder(long handle)
    247. {
    248. return player_obj_.Call<int>("StartRecorder", handle);
    249. }
    250. ///
    251. /// 停止录像
    252. ///
    253. public int NT_U3D_StopRecorder(long handle)
    254. {
    255. return player_obj_.Call<int>("StopRecorder", handle);
    256. }
    257. ///
    258. /// 关闭播放
    259. ///
    260. public int NT_U3D_Close(long handle)
    261. {
    262. return player_obj_.Call<int>("Close", handle);
    263. }
    264. ///
    265. /// UnInit Player
    266. ///
    267. public int NT_U3D_UnInit()
    268. {
    269. return DANIULIVE_RETURN_OK;
    270. }

    总结

    Unity下实现RTMP或RTSP无论是播放还是录像,甚至快照,说难不难,但是做好真的比较难,特别是移动端,Unity和原生层交互的时候,数据交互效率相对较低,需要尽可能减少拷贝。录像的话,还需要考虑硬件性能瓶颈。此外,还需要逻辑分离,确保播放和录像相互不影响,以上是抛砖引玉,感兴趣的开发者,可以自行参考实现,如果需要单独和我交流的,可以相互交流。

  • 相关阅读:
    【李沐深度学习笔记】矩阵计算(4)
    Linux: Security: sudoers 语法错误
    【计算机网络】IP协议
    VU 非父子组件通信
    LTspice XVII > Transformer 变压器仿真
    linux驱动之调试技巧--- 应用程序远程gdb+vscode调试应用程序, 串口kgdboc调试.ko驱动程序
    Java面试题:如何在Java中进行代码优化以提高性能?
    JavaSE - 初识Java
    快速幂矩阵-python
    数据结构题型1--头插法建立单链表
  • 原文地址:https://blog.csdn.net/renhui1112/article/details/130841729