本文主要讲解及解决在开发中的实际问题
webRTC 的基本概念
webRTC 中 Peerconnection , SDP , ICE,NAT,TURN ,STUN 的概念
web 端基本实现与屏幕共享功能实现的方式
ios 端基本实现与屏幕共享功能的实现
本文重点讲述内容
webRTC 的基础重要的几个关键概念
web 端实现屏幕共享发送到远端
ios 端实现屏幕共享发送共享视频到远端的方法(这个会着重讲,因为实现真的很不容易)
引言
接触 webRTC 主要是公司基于现有 mcu 与网关需要实现一套全新的能连接多端,实现音视频通话与会议功能,实现 web 端 ,ios 端,安卓端,会议终端,实现点对点的通话,与组会,
前言
webRTC 是一个由 Google 发布的实时通讯解决方案,集成了视频的采集编码 传输,通信,解码,音视频的显示,通过 webRTC 我们可以很迅速的实现音视频通话功能,大大节约了成本,能迅速的构建起 web 与移动端的音视频通讯功能,项目开源,能实现全平台的互通
webRTC 的架构模型图如下
1.PeerConnection 的构建过程
ClientA 和 ClientB 均通过双向通信方式如 WebSocket 连接到 Signaling Server 上;
ClientA 在本地首先通 GetMedia 访问本地的 media 接口和数据,并创建 PeerConnection 对象,调用其 AddStream 方法把本地的 Media 添加到 PeerConnection 对象中。对于 ClientA 而言,既可以在与 Signaling Server 建立连接之前就创建并初始化 PeerConnection 如阶段 1,也可以在建立 Signaling Server 连接之后创建并初始化 PeerConnection 如阶段 2;ClientB 既可以在上图的 1 阶段也可以在 2 阶段做同样的事情,访问自己的本地接口并创建自己的 PeerConnection 对象
通信由 ClientA 发起,所以 ClientA 调用 PeerConnection 的 CreateOffer 接口创建自己的 SDP offer,然后把这个 SDP Offer 信息通过 Signaling Server 通道中转发给 ClientB;
ClientB 收到 Signaling Server 中转过来的 ClientA 的 SDP 信息也就是 offer 后,调用 CreateAnswer 创建自己的 SDP 信息也就是 answer,然后把这个 answer 同样通过 Signaling server 转发给 ClientA;
ClientA 收到转发的 answer 消息以后,两个 peers 就做好了建立连接并获取对方 media streaming 的准备;
ClientA 通过自己 PeerConnection 创建时传递的参数等待来自于 ICE server 的通信,获取自己的 candidate,当 candidate available 的时候会自动回掉 PeerConnection 的 OnIceCandidate;
ClientA 通过 Signling Server 发送自己的 Candidate 给 ClientB,ClientB 依据同样的逻辑把自己的 Candidate 通过 Signaling Server 中转发给 ClientA,至此 ClientA 和 ClientB 均已经接收到对方的 Candidate,通过 PeerConnection 建立连接。至此 P2P 通道建立
2.SDP 的基本概念及内容
SDP(Session Description Protocol)是一种通用的会话描述协议,主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。
WebRTC 主要在连接建立阶段用到 SDP,连接双方通过信令服务交换会话信息,包括音视频编解码器(codec)、主机候选地址、网络传输协议等
描述两个客户端各自的音视频能力集,通信信息,ice 信息,通过对 sdp 内容的修改(音视频编解码能力)可以指定双方采用的编码格式,默认双方协商会是 VP8 视频编解码格式,opus 或者 G722 的音频编解码格式,以信息在前为准(首先出现的编解码能力,首先协商),指定编解码格式只需对信息进行替换
=rtcp-muxa=rtpmap:111 opus/48000/2a=rtcp-fb:111 transport-cca=fmtp:111 minptime=10;useinbandfec=1a=rtpmap:103 ISAC/16000a=rtpmap:104 ISAC/32000 a=rtpmap:9 G722/8000 音频编码格式 G722a=rtpmap:0 PCMU/8000 音频编码格式G711Ua=rtpmap:8 PCMA/8000 音频编码格式G711Aa=rtpmap:106 CN/32000a=rtpmap:105 CN/16000a=rtpmap:13 CN/8000a=rtpmap:110 telephone-event/48000a=rtpmap:112 telephone-event/32000a=rtpmap:113 telephone-event/16000a=rtpmap:126 telephone-event/8000a=ssrc:360623571 cname:Hnq/JpW3Hk8HUdjpa=ssrc:360623571 msid:m8vgZoloLydQ91c3by9VTuO4yO74NBeUg5rW 5f044497-b759-422a-b2a6-473ecef0ef78a=ssrc:360623571 mslabel:m8vgZoloLydQ91c3by9VTuO4yO74NBeUg5rWa=ssrc:360623571 label:5f044497-b759-422a-b2a6-473ecef0ef78m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 102 121c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0//ice协商过程中的安全验证信息a=ice-ufrag:n8sqa=ice-pwd:9RapOjHE8lxxXVAygEjU2WITa=ice-options:trickle//dtls协商过程中需要的认证信息a=fingerprint:sha-256 05:13:93:45:52:6C:1D:AE:D6:2A:D8:ED:B8:D1:97:71:E2:6D:E3:C7:2A:06:C8:09:7B:C8:3E:98:21:44:98:ABa=setup:actpassa=mid:1a=extmap:14 urn:ietf:params:rtp-hdrext:toffseta=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delaya=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typea=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timinga=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacea=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mida=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-ida=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-ida=sendrecva=msid:m8vgZoloLydQ91c3by9VTuO4yO74NBeUg5rW 6cd136f0-e30c-45c8-9262-7696d760d224a=rtcp-muxa=rtcp-rsizea=rtpmap:96 VP8/90000 视频编码格式 a=rtcp-fb:96 goog-remba=rtcp-fb:96 transport-cca=rtcp-fb:96 ccm fira=rtcp-fb:96 nacka=rtcp-fb:96 nack plia=rtpmap:97 rtx/90000a=fmtp:97 apt=96a=rtpmap:98 VP9/90000 视频编码格式a=rtcp-fb:98 goog-remba=rtcp-fb:98 transport-cca=rtcp-fb:98 ccm fira=rtcp-fb:98 nacka=rtcp-fb:98 nack plia=fmtp:98 profile-id=0a=rtpmap:99 rtx/90000a=fmtp:99 apt=98a=rtpmap:102 H264/90000 视频编码格式 a=rtcp-fb:102 goog-remba=rtcp-fb:102 transport-cca=rtcp-fb:102 ccm fira=rtcp-fb:102 nacka=rtcp-fb:102 nack plia=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001fa=rtpmap:121 rtx/90000a=fmtp:121 apt=102
复制代码
3.ICE 是什么
互动式连接建立提供的是一种框架,使各种 NAT 穿透技术(STUN,TURN...)可以实现统一。该技术可以让客户端成功地穿透远程用户与网络之间可能存在的各类防火墙
4.NAT
网路地址转换可为你的装置提供公用 IP 地址。路由器具备公用 IP 地址,而连上路由器的所有装置则具备私有 IP 地址。接着针对请求,从装置的私有 IP 对应到路由器的公用 IP 与专属的通讯端口。如此一来,各个装置不需占用专属的公用 IP,亦可在网路上被清楚识别
5.STUN
NAT 的 UDP 简单穿越是一种网络协议,它允许位于 NAT(或多重 NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的 NAT 之后以及 NAT 为某一个本地端口所绑定的 Internet 端端口。这些信息被用来在两个同时处于 NAT 路由器之后的主机之间建立 UDP 通信
6.TURN
中继 NAT 实现的穿透(Traversal Using Relays around NAT)就是通过 TURN 服务器开启连线并转送所有数据,进而绕过 Symmetric NAT 的限制。你可通过 TURN 服务器建立连线,再告知所有端点传送封包至该服务器,最后让服务器转送封包给你。这个方法更耗时且更占频宽,因此在没有其他替代方案时才会使用这个方法
上面就是一些主要需要了解的内容,建立连接过程,与通信打洞的原理
介绍 web 端实现内容
var ice = {"iceServers": [ {"url": "stun:stun.l.google.com:19302"}, {"url": "turn:turnserver.com", "username": "user", "credential": "pass"} ]};
var signalingChannel = new SignalingChannel();var pc = new RTCPeerConnection(ice);navigator.mediaDevices.getUserMedia(gumConstraints) .then(function(stream) { var videotrack = stream.getVideoTracks()[0]; if(videotrack){ videotrack.applyConstraints({ frameRate: { max: globalVariable.video_call_property.video_framerate} }); } pluginHandle.consentDialog(false); streamsDone(handleId, jsep, media, callbacks, stream); }).catch(function(error) { pluginHandle.consentDialog(false); if (error.name == "OverconstrainedError") { //表示音视频的能力,可以实现本地是否发送音频或者视频 gumConstraints = { audio: (audioExist && !media.keepAudio) ? audioSupport : false, video: (videoExist && !media.keepVideo) ? videoSupport : false }; getMedia(gumConstraints, handleId, jsep, media, callbacks, stream, audioExist, videoExist, audioSupport, videoSupport, pluginHandle); } else { callbacks.error({code: error.code, name: error.name, message: error.message}); } }); // 添加视频,这里获取到远端的音视频流pc.onaddstream = function (remoteStream) { pluginHandle.onremotestream(remoteStream.stream);};
复制代码
实现屏幕共享功能的代码如下
//获取浏览器的共享画面var screenMedia = navigator.mediaDevices.getDisplayMedia({ video: {width:1280,height:720}, audio: true }) .then(function(stream) { stream.getTracks().forEach(track => { track.onended = function () { backState(false) } }) navigator.mediaDevices.getUserMedia({ audio: true, video: false }) .then(function (audioStream) { stream.addTrack(audioStream.getAudioTracks()[0]); //返回值标志,用于标志视频成功获取共享画面,此处用于页面交互的状态改变 backState(true) CloseLocalVideo() ScreenStream = stream //记录共享画面流信息 localConfig.myStream = stream videoCallpluginHandle.onlocalstream(stream) //改变本地显示的画面 var videoTransceiver = null; var transceivers = localConfig.pc.getTransceivers(); //找出当前的音频流信息 if(transceivers && transceivers.length > 0) { for(var i in transceivers) { var t = transceivers[i]; if((t.sender && t.sender.track && t.sender.track.kind === "audio") || (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { audioTransceiver = t; break; } } } //这段代码为屏幕共享的关键所在,替换当前会话句柄的音频源 if(audioTransceiver && audioTransceiver.sender) { audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]); } else { config.pc.addTrack(stream.getAudioTracks()[0], stream); } if(transceivers && transceivers.length > 0) { for(var i in transceivers) { var t = transceivers[i]; if((t.sender && t.sender.track && t.sender.track.kind === "video") || (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { videoTransceiver = t; break; } } } //替换会话句柄发送者的视频源 if(videoTransceiver && videoTransceiver.sender) { videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]); } else { localConfig.addTrack(stream.getVideoTracks()[0], stream); } })
// } }, function (error) { // pluginHandle.consentDialog(false); // VideoCallBack.error(error); console.log('==******==') backState(false) //此处标志调取屏幕共享失败或者用户未授权共享或者取消了屏幕共享 },function(canceled){ console.log('==stopped==') });
复制代码
web 屏幕共享在同一个文件中可以写方法实现调用屏幕共享,获取音视频信息,在本文件中即可将相应的流通过 RTC 的会话句柄发送者,发送到远端,和在本地显示,不需要对流进行任何的操作,主要代码也是固定调用,难点在于通过发送者发送带远端和修改本地显示视频,其中的难点代码如下,对 peerconnection 的会话句柄做全局变量处理 videoCallpluginHandle,localConfig.pc 为当前初始化的 peerconnection 对象,
videoCallpluginHandle.onlocalstream(stream) //改变本地显示的画面 var videoTransceiver = null; var transceivers = localConfig.pc.getTransceivers(); //找出当前的音频流信息 if(transceivers && transceivers.length > 0) { for(var i in transceivers) { var t = transceivers[i]; if((t.sender && t.sender.track && t.sender.track.kind === "audio") || (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { audioTransceiver = t; break; } } } //这段代码为屏幕共享的关键所在,替换当前会话句柄的音频源 if(audioTransceiver && audioTransceiver.sender) { audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]); } else { config.pc.addTrack(stream.getAudioTracks()[0], stream); }
if(transceivers && transceivers.length > 0) { for(var i in transceivers) { var t = transceivers[i]; if((t.sender && t.sender.track && t.sender.track.kind === "video") || (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { videoTransceiver = t; break; } } } //替换会话句柄发送者的视频源 if(videoTransceiver && videoTransceiver.sender) { videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]); } else { localConfig.addTrack(stream.getVideoTracks()[0], stream); }
复制代码
iOS 端主要实现
//生成解码的生成器 RTCDefaultVideoDecoderFactory *decoder = [[RTCDefaultVideoDecoderFactory alloc] init]; //生成编码的生成器 RTCDefaultVideoEncoderFactory *encoder = [[RTCDefaultVideoEncoderFactory alloc] init]; //储存编码 H264 VP8 VP9 ... NSArray *codes = [encoder supportedCodecs]; //取的编码是第二个 [encoder setPreferredCodec:codes[2]]; //把编码放进pc生成器中 _factory = [[RTCPeerConnectionFactory alloc] initWithEncoderFactory:encoder decoderFactory:decoder];
// 媒体约束 RTCMediaConstraints *constraints = [self defaultPeerConnectionConstraints]; // 创建配置 RTCConfiguration *config = [[RTCConfiguration alloc] init]; // ICE 中继服务器地址 NSArray *iceServers = @[[self defaultSTUNServer]]; config.iceServers = iceServers; // 创建一个RTCPeerConnection RTCPeerConnection *peerConnection = [_factory peerConnectionWithConfiguration:config constraints:constraints delegate:self]; // 添加视频轨 [peerConnection addStream:stream];
复制代码
初始化本地视频流信息
NSDictionary *mandatoryConstraints = @{};//媒体约束RTCMediaConstraints *constrains = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:nil];//通过生成器拿到音频资源idRTCAudioSource * audioSource = [_factory audioSourceWithConstraints:constrains];//获取媒体设备NSArray<AVCaptureDevice *> *captureDevices = [RTCCameraVideoCapturer captureDevices];//前置摄像头Front 后置backAVCaptureDevicePosition position = AVCaptureDevicePositionFront;_mediaStream = [_factory mediaStreamWithStreamId:KARDMediaStreamId];[_mediaStream addAudioTrack:_audioTrack];/*这里很重要 */localView.captureSession = _capture.captureSession;[_capture startCaptureWithDevice:device format:format fps:fps];
复制代码
初始化 PeerConnect,通过 webRTC 中的方法获取本地视流,自动会发送到远端,获取远端视频流信息,通过代理方法可以获取
- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didAddStream:(nonnull RTCMediaStream *)stream { NSLog(@"==009==="); if (self.onAddStream != NULL) { self.onAddStream(self, peerConnection, stream); }}
复制代码
在屏幕共享的过程中需要用到打开和关闭本地摄像头的功能,webRTC 中实现了摄像头的开关功能,这里处理了一个线程问题
- (void)startCapture{ if(_Running){ return; } _Running = true; AVCaptureDeviceFormat *format = [self selectFormatForDevice:self.LocalDevice withTargetWidth:640 withTargetHeight:480]; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [self.capture startCaptureWithDevice:_LocalDevice format:format fps:15 completionHandler:^(NSError * err) { dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);}
//关闭摄像头,在共享的时候需要关闭本地摄像头的采集,- (void)stopCapture{ if(!_Running){ return; } _Running = false; // Stopping the capture happens on another thread. Wait for it. dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [_capture stopCaptureWithCompletionHandler:^{ dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);}
//打开摄像头需要重新获取- (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device withTargetWidth:(int)targetWidth withTargetHeight:(int)targetHeight { NSArray<AVCaptureDeviceFormat *> *formats = [RTCCameraVideoCapturer supportedFormatsForDevice:device]; AVCaptureDeviceFormat *selectedFormat = nil; int currentDiff = INT_MAX;
for (AVCaptureDeviceFormat *format in formats) { CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription); FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription); int diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height); if (diff < currentDiff) { selectedFormat = format; currentDiff = diff; } else if (diff == currentDiff && pixelFormat == [self.capture preferredOutputPixelFormat]) { selectedFormat = format; } }
return selectedFormat;}
复制代码
上面是 iOS 端实现创建本地视频流发送到远端,与获取远端视频流的过程,下面开始说屏幕共享功能
iOS 端实现屏幕共享功能的难点问题
ios12 以后苹果推出了 replaykit2 可以实现桌面(系统内)的共享,但是需要在 extensionApp 中实现录制,extensionApp 寄生于主 App,两者在不同的进程中,需要使用进程间的通信才可以实现通信,信息交换传递
extensionApp 苹果只有 50M 的空间,超过就会被系统杀死录制的进程,所以我们在 extensionApp 中操作数据需要关注运行内存的优化处理
extensionApp 的录制数据如果不能再本进程中编码传到远端,或者本地需要显示,我们就需要将录制的视频传回到主 App 中,苹果没有给出直接能传 CMSampleBufferRef 数据的进程间通信,所以需要编码然后在解码传输
解决方案
通过 socket 与 CFNotificationCenterRef 实现进程间的通信,socket 实现传输音视频数据,CFNotificationCenterRef 实现状态信息的传递
通过对当先所占内存的统计检测,实现在内存大的时候即使释放录制的音视频数据,带数据处理完,内存下降时在对数据进行编码
使用 libyuv NTESI420Frame 对录制数据进行 yuv 数据转换 将视频信息转换为 NSData 数据通过 socket 进行回传到主 app,通过替换 webRTC 的视频源实现将共享画面发送到远端
//extensionAPP中发送代码
- (void)sendVideoBufferToHostApp:(CMSampleBufferRef)sampleBuffer { if (!self.socket) { return; } CFRetain(sampleBuffer); dispatch_async(self.videoQueue, ^{ // queue optimal @autoreleasepool { if (self.frameCount > 1000) { CFRelease(sampleBuffer); return; } self.frameCount ++ ; CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); //获取当前视频流信息的方向信息 CFStringRef RPVideoSampleOrientationKeyRef = (__bridge CFStringRef)RPVideoSampleOrientationKey; NSNumber *orientation = (NSNumber *)CMGetAttachment(sampleBuffer, RPVideoSampleOrientationKeyRef,NULL); switch ([orientation integerValue]) { case 1: self.orientation = NTESVideoPackOrientationPortrait; break; case 6: self.orientation = NTESVideoPackOrientationLandscapeRight; break; case 8: self.orientation = NTESVideoPackOrientationLandscapeLeft; break; default: break; } // To data NTESI420Frame *videoFrame = nil; videoFrame = [NTESYUVConverter pixelBufferToI420:pixelBuffer withCrop:self.cropRate targetSize:self.targetSize andOrientation:self.orientation]; CFRelease(sampleBuffer); // To Host App if (videoFrame){ NSData *raw = [videoFrame bytes]; //NSData *data = [NTESSocketPacket packetWithBuffer:raw]; NSData *headerData = [NTESSocketPacket packetWithBuffer:raw]; if (!_enterBack) { if (self.connected) { [self.socket writeData:headerData withTimeout:-1 tag:0]; [self.socket writeData:raw withTimeout:-1 tag:0]; } } } self.frameCount --; }; });}
复制代码
接收端,接收到视频处理方法
- (void)onRecvData:(NSData *)data{ dispatch_async(dispatch_get_main_queue(), ^{ NTESI420Frame *frame = [NTESI420Frame initWithData:data]; CMSampleBufferRef sampleBuffer = [frame convertToSampleBuffer]; if (sampleBuffer == NULL) { return; } if(!_isStart){ if (_startScreenBlock) { _startScreenBlock(true); }// if(!_StartScreen){//// }else{//// if (_startScreenBlock) {// _startScreenBlock(false);// }// } _isStart = true; _Running = false; [_capture stopCapture]; }else{ } if([UIScreen mainScreen].isCaptured){ if(_Running){ _isStart = true; _Running = false; [_capture stopCapture]; //暂停本地视频 if (_startScreenBlock) { _startScreenBlock(true);//状态回调 } } }
// if (self.StartScreen) { //NSEC_PER_SEC 此句颇为重要不能掉,不然不能暂停本地视频导致视频画面频繁的闪 int64_t timeStampNs = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * NSEC_PER_SEC; CVPixelBufferRef rtcPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); RTCCVPixelBuffer *cpvX = [[RTCCVPixelBuffer alloc]initWithPixelBuffer:rtcPixelBuffer]; RTCVideoFrame *aframe = [[RTCVideoFrame alloc]initWithBuffer:cpvX rotation:self.rotation timeStampNs:timeStampNs]; //使用代理方法实现替换视频源 [_videoSource capturer:_capture didCaptureVideoFrame:aframe];
// } CFRelease(sampleBuffer); });}
复制代码
解决 extension app 只有 50M 空间问题
主要问题在于 extension app 负责录制屏幕,运行空间只有 50M 超过立即会被系统杀死,录制结束,由于录制屏幕是根据屏幕内容变化才会启动录制流,当屏幕内容变化快的时候单位时间内的流帧数会变大,导致数据处理压力变大,在将流转换为 I420 yuv 数据的时候所需的缓冲去便会变大,导致内存不断增大,最终被系统杀死录制进程,解决方案就是监控当前运行所占内存,超过一定范围后就不在将录制数据编码,从而控制缓存区的内存增长,保持地缓存,具体代码如下
在 extension app 中写下如下代码
//获取当前运行内存空间- (double)getCurrentMemory{ task_basic_info_data_t taskInfo; mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT; kern_return_t kernReturn = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&taskInfo, &infoCount);
if (kernReturn != KERN_SUCCESS ) { return NSNotFound; } NSLog(@"%f",taskInfo.resident_size / 1024.0 / 1024.0); return taskInfo.resident_size / 1024.0 / 1024.0;}
复制代码
当前文件下调用
long curMem = [self getCurrentMemory]; if ((self.eventMemory > 0 && ((curMem - self.eventMemory) > 5)) || curMem > 40) { //当前内存暴增5M以上,或者总共超过40M,则不处理 CFRelease(sampleBuffer); return; };
复制代码
如上所示即可解决 50M 内存限制,使用过程工能保证录制功能不会意外退出,当然相应的在接收端或者本地预览端会有画面没有连续更新的问题,这也是鱼与熊掌不可兼得,
由于项目中代码写的不是很规范,demo 等整理出来在添加链接,供参考
评论