技术背景
在写这篇文章之前,实际上几年之前,我们就有非常稳定的无纸化同屏的模块,本文借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平台亦可,比如采集原始屏幕,或者缩放后的屏幕,具体见下图:
/* BackgroudService.java * Author: daniusdk.com */ private void createScreenEnvironment() { sreenWindowWidth = mWindowManager.getDefaultDisplay().getWidth(); screenWindowHeight = mWindowManager.getDefaultDisplay().getHeight(); Log.i(TAG, "screenWindowWidth: " + sreenWindowWidth + ",screenWindowHeight: " + screenWindowHeight); if (sreenWindowWidth > 800) { if (screen_resolution_type_ == SCREEN_RESOLUTION_STANDARD) { scale_rate = SCALE_RATE_HALF; sreenWindowWidth = align(sreenWindowWidth / 2, 16); screenWindowHeight = align(screenWindowHeight / 2, 16); } else if(screen_resolution_type_ == SCREEN_RESOLUTION_LOW) { scale_rate = SCALE_RATE_TWO_FIFTHS; sreenWindowWidth = align(sreenWindowWidth * 2 / 5, 16); screenWindowHeight = align(screenWindowHeight * 2 / 5, 16); } } Log.i(TAG, "After adjust mWindowWidth: " + sreenWindowWidth + ", mWindowHeight: " + screenWindowHeight); int pf = mWindowManager.getDefaultDisplay().getPixelFormat(); Log.i(TAG, "display format:" + pf); DisplayMetrics displayMetrics = new DisplayMetrics(); mWindowManager.getDefaultDisplay().getMetrics(displayMetrics); mScreenDensity = displayMetrics.densityDpi; mImageReader = ImageReader.newInstance(sreenWindowWidth, screenWindowHeight, 0x1, 6); mMediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); }
5. Android横竖屏自动适配
Android平台,如果是pad采集,基本就是横屏采集,如果手机端,需要确保横竖屏模式下都可以正常采集。
4. 为什么要考虑补帧
Android的时候,一定的采集模式下,屏幕如果没有变化,不会一直有实时屏幕数据回调下来,这时候,为了保持帧率或数据采集的完整性,建议补帧。
5. 异常网络处理、事件回调机制
网络状态,不管是推送端,还是播放端,都是需要有实时的状态回调,确保客户端可以实时感知网络状态。
backgroudService.SetEventListener(new EventListener() { public void onPublisherEventCallback(long handle, int id, long param1, long param2, String param3, String param4, Object param5) { String publisher_event = ""; switch (id) { case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STARTED: publisher_event = "开始.."; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING: publisher_event = "连接中.."; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED: publisher_event = "连接失败.."; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED: publisher_event = "连接成功.."; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED: publisher_event = "连接断开.."; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STOP: publisher_event = "关闭.."; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE: publisher_event = "开始一个新的录像文件 : " + param3; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED: publisher_event = "已生成一个录像文件 : " + param3; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY: publisher_event = "发送时延: " + param1 + " 帧数:" + param2; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CAPTURE_IMAGE: publisher_event = "快照: " + param1 + " 路径:" + param3; if (param1 == 0) { publisher_event = publisher_event + "截取快照成功.."; } else { publisher_event = publisher_event + "截取快照失败.."; } break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL: publisher_event = "RTSP服务URL: " + param3; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_RESPONSE_STATUS_CODE: publisher_event = "RTSP status code received, codeID: " + param1 + ", RTSP URL: " + param3; break; case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_NOT_SUPPORT: publisher_event = "服务器不支持RTSP推送, 推送的RTSP URL: " + param3; break; } String str = "当前状态:" + publisher_event; Log.i(TAG, str); if (handler_ != null) { Message message = new Message(); message.what = PUBLISHER_EVENT_MSG; message.obj = publisher_event; handler_.sendMessage(message); } } });
6. 采集到的数据可以按需录像吗
可以,而且很有必要,同屏的时候,如果需要把开会或教授内容实时保存下来,可以随时启动录像。
public boolean startRecorder() { Log.i(TAG, "onClick startRecorder.."); if(!stream_publisher_.is_publishing()) { startCaptureScreen(); } if (layer_post_thread_ != null) layer_post_thread_.update_layers(); if (stream_publisher_.is_recording()) { stopRecorder(); return false; } InitAndSetConfig(); ConfigRecorderParam(); boolean start_ret = stream_publisher_.StartRecorder(); if (!start_ret) { stream_publisher_.try_release(); Log.e(TAG, "Failed to start recorder."); return false; } startAudioRecorder(); startLayerPostThread(); return true; } //停止录像 public void stopRecorder() { stream_publisher_.StopRecorder(); stream_publisher_.try_release(); if (!stream_publisher_.is_publishing()) stopAudioRecorder(); }
7. 文字、图片水印
需要而且建议支持,比如实时时间、学校或公司logo等。
//水印效果选择++++++++++ watermarkSelctor = (Spinner) findViewById(R.id.watermarkSelctor); watermarkSelctor.setEnabled(false); final String[] watermarks = new String[]{"图片水印", "全部水印", "文字水印", "不加水印"}; ArrayAdapter<String> adapterWatermark = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, watermarks); adapterWatermark.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); watermarkSelctor.setAdapter(adapterWatermark); watermarkSelctor.setSelection(3,true); watemarkType = 3; //默认不加水印 watermarkSelctor.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { watemarkType = position; Log.i(TAG, "[水印类型]Currently choosing: " + watermarks[position] + ", watemarkType: " + watemarkType); if(backgroudService !=null) { backgroudService.updateWatermarker(watemarkType); } } public void onNothingSelected(AdapterView<?> parent) { } });
8. 可以同时启动轻量级RTSP服务吗
public boolean startRtspService(int port) { Log.i(TAG, "startRtspService++"); rtsp_handle_ = lib_publisher_.OpenRtspServer(0); if (rtsp_handle_ == 0) { Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性"); } else { if (lib_publisher_.SetRtspServerPort(rtsp_handle_, port) != 0) { lib_publisher_.CloseRtspServer(rtsp_handle_); rtsp_handle_ = 0; Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!"); } if (lib_publisher_.StartRtspServer(rtsp_handle_, 0) == 0) { Log.i(TAG, "启动rtsp server 成功!"); } else { lib_publisher_.CloseRtspServer(rtsp_handle_); rtsp_handle_ = 0; Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!"); } isRTSPServiceRunning = true; } return true; } //停止RTSP服务 public void stopRtspService() { Log.i(TAG, "stopRtspService++"); if(!isRTSPServiceRunning) { return; } if (lib_publisher_ != null && rtsp_handle_ != 0) { lib_publisher_.StopRtspServer(rtsp_handle_); lib_publisher_.CloseRtspServer(rtsp_handle_); rtsp_handle_ = 0; } isRTSPServiceRunning = false; } public boolean startRtspPublisher(){ Log.i(TAG, "startRtspPublisher++"); if(!stream_publisher_.is_publishing()) { startCaptureScreen(); } InitAndSetConfig(); String rtsp_stream_name = "stream1"; stream_publisher_.SetRtspStreamName(rtsp_stream_name); stream_publisher_.ClearRtspStreamServer(); stream_publisher_.AddRtspStreamServer(rtsp_handle_); if (!stream_publisher_.StartRtspStream()) { stream_publisher_.try_release(); Log.e(TAG, "调用发布rtsp流接口失败!"); return false; } startAudioRecorder(); startLayerPostThread(); return true; } //停止发布RTSP流 public void stopRtspPublisher() { Log.i(TAG, "stopRtspPublisher++"); stream_publisher_.StopRtspStream(); stream_publisher_.try_release(); if (!stream_publisher_.is_publishing()) stopAudioRecorder(); } public int getRtspSessionNumbers(){ int session_numbers = 0; if (lib_publisher_ != null && rtsp_handle_ != 0) { session_numbers = lib_publisher_.GetRtspServerClientSessionNumbers(rtsp_handle_); Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers); } return session_numbers; }
9. 同屏延迟,能不能做到毫秒级
10. 能不能采集到扬声器的audio?
Windows不在话下,Android平台需要高版本支持,高版本是可以采集到扬声器数据的,我们也实现了相关的demo,可以同时采集麦克风和扬声器的audio,单独推送或者同时混音输出。
11. 同屏过程中,重点画面可以快照吗?
当然可以,我们同屏采集端,支持采集编码png或jpg格式输出。
总结
其实一个好的无纸化同屏系统,需要考虑的有整体组网、分辨率、码率、实时延迟、音视频同步和连续性等各个指标,做容易,做好难,上述抛砖引玉,未能面面俱到,感兴趣的开发者,可以跟我单独交流。