基于智慧教室|无纸化会议的新选择:RTMP解决方案
基于智慧教室或是会议的技术方案,一般主要是涉及到屏幕采集和推送,整体技术方案这块,一般建议走RTMP,说到这里,好人开发者提到,市面上也有RTSP的技术方案,甚至RTSP组播方案,这块,大牛直播SDK Github 也做过相关对比,总的来说60人智慧教室或类似同屏场景下,最可靠的还是RTMP的解决方案(不赘述,具体可自行测试对比)。
有人说,RTMP延迟大,这种说法,相对片面,好多是由于推拉流模块本身问题导致(如果服务器系NIGNX或SRS,基本可排除服务器转发导致的大时延,不要再赖服务器了),从我们官方和实际场景来看,RTMP整体技术方案,延迟可做到1秒内,毫秒级。
整体设计方案如下
注意事项
1. 组网:无线组网,需要好的AP模块才能撑得住大的并发流量,推送端到AP,最好是有线网链接;
2. 服务器部署:如果Windows平台,可以考虑NGINX,如果是Linux,可以考虑SRS或NGINX,服务器可以和Windows平台的教师机部署在一台机器;
3. 教师端:如教师有移动的PAD,可以直接推到RTMP服务器,然后共享出去;
4. 学生端:直接拉取RTMP流播放即可;
5. 教师和学生互动:学生端如需作为示范案例,屏幕数据共享给其他同学,只需请求同屏,数据反推到RTMP服务器,其他学生查看即可。
6. 扩展监控:如果需要更进一步的技术方案,如教师端想监控学生端的屏幕情况,可以有两种方案,如学生端直接推RTMP过来,或者,学生端启动内置RTSP服务,教师端想看的时候,随时看即可(亦可轮询播放)。
以下分平台介绍相关配置选项
Windows平台RTMP推送端
对应DEMO:SmartPublisherDemo.exe
1. 如果采集屏幕,只要采集部分区域的话,可以点击“选取屏幕区域”按钮,选择需要采集的区域,采集推送过程中,可以移动采集区域;
2. 如果是高分屏(如有些采集设备,是4K屏,原始分辨率过高),用户又不想推这么高的分辨率的话,可以选中“缩放屏幕大小”,并指定缩放比例,可以先缩放,后编码推送数据;
3. 设置采集帧率:如果是PPT/Word文档类,一般8-12帧足矣,如果是电影之类,可以设置到20-30帧不等,关键帧间隔一般设置到帧率的2-4倍,屏幕推送的话,建议平均码率模式;
4. 如果需要采集电脑端输出的声音,可以选中“采集扬声器”,如果需要采集外部麦克风的音频,选择“采集麦克风”即可,并选择对应的采集设备;
5. 设置下推送的RTMP URL,然后,点击“推送”,就可以了;
6. 如果想预览推送出去的数据,点击“预览”即可,想停止预览的话,点击“停止预览”即可。
Android平台RTMP屏幕推送端
需要注意的事项:
1. Android 8.0及以上版本设备,需要加入省电优化白名单,6.0以上版本,需要动态获取audio权限,具体代码如下:
//加入省电优化白名单,以免8.0及以上版本设备后台运行超过一分钟被自动停掉 //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) if (Build.VERSION.SDK_INT >=26) { if(!isIgnoringBatteryOptimizations()) { gotoSettingIgnoringBatteryOptimizations(); } } //6.0及以上版本,动态获取Audio权限 if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { RequestAudioPermission(); } //拉起请求加入省电白名单弹窗 private void gotoSettingIgnoringBatteryOptimizations() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { Intent intent = new Intent(); String packageName = getPackageName(); intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:" + packageName)); startActivityForResult(intent, REQUEST_IGNORE_BATTERY_CODE); } catch (Exception e) { e.printStackTrace(); } } } //动态获取Audio权限 private void RequestAudioPermission() { if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(this.getApplicationContext(), android.Manifest.permission.RECORD_AUDIO)) { } else { //提示用户开户权限音频 String[] perms = {"android.permission.RECORD_AUDIO"}; ActivityCompat.requestPermissions(this, perms, RESULT_CODE_STARTAUDIO); } }
2. 持续的补帧策略,防止屏幕不动,没数据下去;
3. 如果需要传部分区域下去,可以用 SmartPublisherOnCaptureVideoClipedRGBAData() 接口;
4. 横竖屏切换,上层无需过问,底层会自动切。
iOS平台RTMP屏幕推送端
对应工程: SmartServiceCameraPublisherV2
注意事项:ReplayKit2 的直播扩展目前是有50M的内存使用限制,超过此限制系统会直接杀死扩展进程,因此 ReplayKit2 上建议推流分辨率和帧率、码率不要太高。
以下是核心processSampleBuffer() 处理,iOS 11.0以上 加入了横竖屏自动切换适配:
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType { CGFloat cur_memory = [self GetCurUsedMemoryInMB]; if( cur_memory > 20.0f) { //NSLog(@"processSampleBuffer cur: %.2fM", cur_memory); return; } switch (sampleBufferType) { case RPSampleBufferTypeVideo: { if (!CMSampleBufferIsValid(sampleBuffer)) return; NSInteger rotation_degress = 0; //11.1以上支持自动旋转 #ifdef __IPHONE_11_1 if (UIDevice.currentDevice.systemVersion.floatValue > 11.1) { CGImagePropertyOrientation orientation = ((__bridge NSNumber*)CMGetAttachment(sampleBuffer, (__bridge CFStringRef)RPVideoSampleOrientationKey , NULL)).unsignedIntValue; //NSLog(@"cur org: %d", orientation); switch (orientation) { //竖屏 case kCGImagePropertyOrientationUp:{ rotation_degress = 0; } break; case kCGImagePropertyOrientationDown:{ rotation_degress = 180; break; } case kCGImagePropertyOrientationLeft: { //静音键那边向上 所需转90度 rotation_degress = 90; } break; case kCGImagePropertyOrientationRight:{ //关机键那边向上 所需转270 rotation_degress = 270; } break; default: break; } } #endif //NSLog(@"RPSampleBufferTypeVideo"); if(_smart_publisher_sdk) { //[_smart_publisher_sdk SmartPublisherPostVideoSampleBuffer:sampleBuffer]; [_smart_publisher_sdk SmartPublisherPostVideoSampleBufferV2:sampleBuffer rotateDegress:rotation_degress]; } //NSLog(@"video ts:%.2f", CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer))); } break; case RPSampleBufferTypeAudioApp: //NSLog(@"RPSampleBufferTypeAudioApp"); if (CMSampleBufferDataIsReady(sampleBuffer) != NO) { if(_smart_publisher_sdk) { NSInteger type = 2; [_smart_publisher_sdk SmartPublisherPostAudioSampleBuffer:sampleBuffer inputType:type]; } } //NSLog(@"App ts:%.2f", CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer))); break; case RPSampleBufferTypeAudioMic: //NSLog(@"RPSampleBufferTypeAudioMic"); if(_smart_publisher_sdk) { NSInteger type = 1; [_smart_publisher_sdk SmartPublisherPostAudioSampleBuffer:sampleBuffer inputType:type]; } //NSLog(@"Mic ts:%.2f", CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer))); break; default: break; } }