当前位置:   article > 正文

Android平台实现无纸化同屏并推送RTMP或轻量级RTSP服务(毫秒级延迟)_安卓发送rtsp流

安卓发送rtsp流

技术背景

在写这篇文章之前,实际上几年之前,我们就有非常稳定的无纸化同屏的模块,本文借demo更新,算是做个新的总结,废话不多说,先看图,本文以Android平台屏幕实时采集推送,Windows播放为例,和大家做个技术分享。

技术考量指标

本文以大牛直播SDK前些年实现的Android同屏采集推送为例,大概介绍下一些技术考量指标。

1. 轻量级RTSP服务还是RTMP?

我们在做无纸化同屏的时候,问的最多的是,能不能不要自建服务,直接主讲人或教师端,直接启动轻量级RTSP服务,其他终端拉流,如果是小并发,比如5人内的小范围的同屏,Windows平台走轻量级RTSP无可厚非,如果是30-60甚至100人的会议室,建议走RTMP。

2. 推送分辨率和码率选择

我们接触到好多设备,性能一般,但是屏幕是高分屏,甚至可以采集到4K的,考虑到实时编码和并发环境下,AP的承载能力,一般建议选择适合自己的分辨率码率即可,不要只追求高分辨率高码率,导致组网困难,单个或双通道AP压力大,一般建议控制在1920*1080分辨率内,码率控制在1-5M。

3. 软编码还是硬编码

Windows平台,一般优先考虑软编,因为大多Windows性能瓶颈不太大,超过1080P可以考虑硬编,Android平台建议直接硬编码。

4. 高分屏采集编码效率低怎么办

高分屏,不管是Windows还是Android,采集后的数据,建议先压缩,再编码,Windows平台我们可以设置压缩比例(scale rate),Android平台亦可,比如采集原始屏幕,或者缩放后的屏幕,具体见下图:

  1. /* BackgroudService.java
  2. * Author: daniusdk.com
  3. */
  4. private void createScreenEnvironment() {
  5. sreenWindowWidth = mWindowManager.getDefaultDisplay().getWidth();
  6. screenWindowHeight = mWindowManager.getDefaultDisplay().getHeight();
  7. Log.i(TAG, "screenWindowWidth: " + sreenWindowWidth + ",screenWindowHeight: "
  8. + screenWindowHeight);
  9. if (sreenWindowWidth > 800)
  10. {
  11. if (screen_resolution_type_ == SCREEN_RESOLUTION_STANDARD)
  12. {
  13. scale_rate = SCALE_RATE_HALF;
  14. sreenWindowWidth = align(sreenWindowWidth / 2, 16);
  15. screenWindowHeight = align(screenWindowHeight / 2, 16);
  16. }
  17. else if(screen_resolution_type_ == SCREEN_RESOLUTION_LOW)
  18. {
  19. scale_rate = SCALE_RATE_TWO_FIFTHS;
  20. sreenWindowWidth = align(sreenWindowWidth * 2 / 5, 16);
  21. screenWindowHeight = align(screenWindowHeight * 2 / 5, 16);
  22. }
  23. }
  24. Log.i(TAG, "After adjust mWindowWidth: " + sreenWindowWidth + ", mWindowHeight: " + screenWindowHeight);
  25. int pf = mWindowManager.getDefaultDisplay().getPixelFormat();
  26. Log.i(TAG, "display format:" + pf);
  27. DisplayMetrics displayMetrics = new DisplayMetrics();
  28. mWindowManager.getDefaultDisplay().getMetrics(displayMetrics);
  29. mScreenDensity = displayMetrics.densityDpi;
  30. mImageReader = ImageReader.newInstance(sreenWindowWidth,
  31. screenWindowHeight, 0x1, 6);
  32. mMediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
  33. }

5. Android横竖屏自动适配

Android平台,如果是pad采集,基本就是横屏采集,如果手机端,需要确保横竖屏模式下都可以正常采集。

4. 为什么要考虑补帧

Android的时候,一定的采集模式下,屏幕如果没有变化,不会一直有实时屏幕数据回调下来,这时候,为了保持帧率或数据采集的完整性,建议补帧。

5. 异常网络处理、事件回调机制

网络状态,不管是推送端,还是播放端,都是需要有实时的状态回调,确保客户端可以实时感知网络状态。

  1. backgroudService.SetEventListener(new EventListener() {
  2. @Override
  3. public void onPublisherEventCallback(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {
  4. String publisher_event = "";
  5. switch (id) {
  6. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STARTED:
  7. publisher_event = "开始..";
  8. break;
  9. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING:
  10. publisher_event = "连接中..";
  11. break;
  12. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED:
  13. publisher_event = "连接失败..";
  14. break;
  15. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED:
  16. publisher_event = "连接成功..";
  17. break;
  18. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED:
  19. publisher_event = "连接断开..";
  20. break;
  21. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STOP:
  22. publisher_event = "关闭..";
  23. break;
  24. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE:
  25. publisher_event = "开始一个新的录像文件 : " + param3;
  26. break;
  27. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED:
  28. publisher_event = "已生成一个录像文件 : " + param3;
  29. break;
  30. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY:
  31. publisher_event = "发送时延: " + param1 + " 帧数:" + param2;
  32. break;
  33. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CAPTURE_IMAGE:
  34. publisher_event = "快照: " + param1 + " 路径:" + param3;
  35. if (param1 == 0) {
  36. publisher_event = publisher_event + "截取快照成功..";
  37. } else {
  38. publisher_event = publisher_event + "截取快照失败..";
  39. }
  40. break;
  41. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
  42. publisher_event = "RTSP服务URL: " + param3;
  43. break;
  44. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_RESPONSE_STATUS_CODE:
  45. publisher_event = "RTSP status code received, codeID: " + param1 + ", RTSP URL: " + param3;
  46. break;
  47. case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_NOT_SUPPORT:
  48. publisher_event = "服务器不支持RTSP推送, 推送的RTSP URL: " + param3;
  49. break;
  50. }
  51. String str = "当前状态:" + publisher_event;
  52. Log.i(TAG, str);
  53. if (handler_ != null) {
  54. Message message = new Message();
  55. message.what = PUBLISHER_EVENT_MSG;
  56. message.obj = publisher_event;
  57. handler_.sendMessage(message);
  58. }
  59. }
  60. });

6. 采集到的数据可以按需录像吗

可以,而且很有必要,同屏的时候,如果需要把开会或教授内容实时保存下来,可以随时启动录像。

  1. public boolean startRecorder()
  2. {
  3. Log.i(TAG, "onClick startRecorder..");
  4. if(!stream_publisher_.is_publishing())
  5. {
  6. startCaptureScreen();
  7. }
  8. if (layer_post_thread_ != null)
  9. layer_post_thread_.update_layers();
  10. if (stream_publisher_.is_recording()) {
  11. stopRecorder();
  12. return false;
  13. }
  14. InitAndSetConfig();
  15. ConfigRecorderParam();
  16. boolean start_ret = stream_publisher_.StartRecorder();
  17. if (!start_ret) {
  18. stream_publisher_.try_release();
  19. Log.e(TAG, "Failed to start recorder.");
  20. return false;
  21. }
  22. startAudioRecorder();
  23. startLayerPostThread();
  24. return true;
  25. }
  26. //停止录像
  27. public void stopRecorder() {
  28. stream_publisher_.StopRecorder();
  29. stream_publisher_.try_release();
  30. if (!stream_publisher_.is_publishing())
  31. stopAudioRecorder();
  32. }

7. 文字、图片水印

需要而且建议支持,比如实时时间、学校或公司logo等。

  1. //水印效果选择++++++++++
  2. watermarkSelctor = (Spinner) findViewById(R.id.watermarkSelctor);
  3. watermarkSelctor.setEnabled(false);
  4. final String[] watermarks = new String[]{"图片水印", "全部水印", "文字水印", "不加水印"};
  5. ArrayAdapter<String> adapterWatermark = new ArrayAdapter<String>(this,
  6. android.R.layout.simple_spinner_item, watermarks);
  7. adapterWatermark.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  8. watermarkSelctor.setAdapter(adapterWatermark);
  9. watermarkSelctor.setSelection(3,true);
  10. watemarkType = 3; //默认不加水印
  11. watermarkSelctor.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
  12. @Override
  13. public void onItemSelected(AdapterView<?> parent, View view,
  14. int position, long id) {
  15. watemarkType = position;
  16. Log.i(TAG, "[水印类型]Currently choosing: " + watermarks[position] + ", watemarkType: " + watemarkType);
  17. if(backgroudService !=null) {
  18. backgroudService.updateWatermarker(watemarkType);
  19. }
  20. }
  21. @Override
  22. public void onNothingSelected(AdapterView<?> parent) {
  23. }
  24. });

8. 可以同时启动轻量级RTSP服务吗

  1. public boolean startRtspService(int port)
  2. {
  3. Log.i(TAG, "startRtspService++");
  4. rtsp_handle_ = lib_publisher_.OpenRtspServer(0);
  5. if (rtsp_handle_ == 0) {
  6. Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性");
  7. } else {
  8. if (lib_publisher_.SetRtspServerPort(rtsp_handle_, port) != 0) {
  9. lib_publisher_.CloseRtspServer(rtsp_handle_);
  10. rtsp_handle_ = 0;
  11. Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!");
  12. }
  13. if (lib_publisher_.StartRtspServer(rtsp_handle_, 0) == 0) {
  14. Log.i(TAG, "启动rtsp server 成功!");
  15. } else {
  16. lib_publisher_.CloseRtspServer(rtsp_handle_);
  17. rtsp_handle_ = 0;
  18. Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
  19. }
  20. isRTSPServiceRunning = true;
  21. }
  22. return true;
  23. }
  24. //停止RTSP服务
  25. public void stopRtspService() {
  26. Log.i(TAG, "stopRtspService++");
  27. if(!isRTSPServiceRunning)
  28. {
  29. return;
  30. }
  31. if (lib_publisher_ != null && rtsp_handle_ != 0) {
  32. lib_publisher_.StopRtspServer(rtsp_handle_);
  33. lib_publisher_.CloseRtspServer(rtsp_handle_);
  34. rtsp_handle_ = 0;
  35. }
  36. isRTSPServiceRunning = false;
  37. }
  38. public boolean startRtspPublisher(){
  39. Log.i(TAG, "startRtspPublisher++");
  40. if(!stream_publisher_.is_publishing())
  41. {
  42. startCaptureScreen();
  43. }
  44. InitAndSetConfig();
  45. String rtsp_stream_name = "stream1";
  46. stream_publisher_.SetRtspStreamName(rtsp_stream_name);
  47. stream_publisher_.ClearRtspStreamServer();
  48. stream_publisher_.AddRtspStreamServer(rtsp_handle_);
  49. if (!stream_publisher_.StartRtspStream()) {
  50. stream_publisher_.try_release();
  51. Log.e(TAG, "调用发布rtsp流接口失败!");
  52. return false;
  53. }
  54. startAudioRecorder();
  55. startLayerPostThread();
  56. return true;
  57. }
  58. //停止发布RTSP流
  59. public void stopRtspPublisher() {
  60. Log.i(TAG, "stopRtspPublisher++");
  61. stream_publisher_.StopRtspStream();
  62. stream_publisher_.try_release();
  63. if (!stream_publisher_.is_publishing())
  64. stopAudioRecorder();
  65. }
  66. public int getRtspSessionNumbers(){
  67. int session_numbers = 0;
  68. if (lib_publisher_ != null && rtsp_handle_ != 0) {
  69. session_numbers = lib_publisher_.GetRtspServerClientSessionNumbers(rtsp_handle_);
  70. Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);
  71. }
  72. return session_numbers;
  73. }

9. 同屏延迟,能不能做到毫秒级

废话不多说,上视频,延迟毫秒级。

安卓采集屏幕至轻量级RTSP服务|推送RTMP整体毫秒级延迟

10. 能不能采集到扬声器的audio?

Windows不在话下,Android平台需要高版本支持,高版本是可以采集到扬声器数据的,我们也实现了相关的demo,可以同时采集麦克风和扬声器的audio,单独推送或者同时混音输出。

11. 同屏过程中,重点画面可以快照吗?

当然可以,我们同屏采集端,支持采集编码png或jpg格式输出。

总结

其实一个好的无纸化同屏系统,需要考虑的有整体组网、分辨率、码率、实时延迟、音视频同步和连续性等各个指标,做容易,做好难,上述抛砖引玉,未能面面俱到,感兴趣的开发者,可以跟我单独交流。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/852556
推荐阅读
相关标签
  

闽ICP备14008679号