iOS 9 以前能够经过私有框架 CoreSurface.framework 来实现录制屏幕。由于用了私有框架,只能经过企业包的形式安装到用户设备中,这种方法的优势是效率很高,可是没法获取游戏声音,只能经过麦克风录制外放的声音。html
iOS 9 之后,苹果去掉了 CoreSurface, 所以,上面的方法完全失效。iOS 9 发布之后的很长一段时间,都没有办法录屏直播。 后来你们另辟蹊径,经过破解 AirPlay 投屏协议的方式实现,在手机上虚拟一个 AirPlay Server 来接受屏幕镜像, 而后解码后再直播。目前 大部分直播平台都是直接接入第三方 SDK,如乐播, xindawn。 这种方案的缺点是 每次 iOS 系统升级,对应的 Airplay Mirroring协议会更新,破解成本高,技术门槛比较高。git
iOS 10 中苹果提供 ReplayKit 了,能够在游戏中实现录屏直播,可是须要游戏厂商支持通用性很低。因此基本上各大厂商基本上仍是采用 Airplay的模式。github
iOS 11 苹果加强为ReplayKit2 提供了更通用的桌面级录屏方案,本文接下来会着重介绍这种方案。缓存
录屏功能是 iOS 10 新推出的特性,苹果在 iOS 9 的 ReplayKit 保存录屏视频的基础上,增长了视频流实时直播功能,官方介绍见 Go Live with ReplayKit。iOS 11 加强为 ReplayKit2,进一步提高了 Replaykit 的易用性和通用性,而且能够对整个手机实现屏幕录制,而非某些作了支持ReplayKit功能的App,所以录屏推流建议直接使用iOS11的ReplayKit2屏幕录制方式。系统录屏采用的是扩展方式,扩展程序有单独的进程,iOS 系统为了保证系统流畅,给扩展程序的资源相对较少,扩展程序内存占用过大也会被 Kill 掉。bash
# 系统回调处理 - (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType { @synchronized(self) { KSYRKStreamerKit* kit = [KSYRKStreamerKit sharedInstance]; switch (sampleBufferType) { case RPSampleBufferTypeVideo: { if (!CMSampleBufferIsValid(sampleBuffer) || !sampleBuffer) return; if (tempVideoTimeStamp && (CFAbsoluteTimeGetCurrent() - tempVideoTimeStamp < 0.025)) { #ifdef DEBUG NSLog(@"帧数传入过快,丢帧处理"); #endif return; } if (tempVideoPixelBuffer) { CFRelease(tempVideoPixelBuffer); tempVideoPixelBuffer = NULL; } CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); //11.1以上支持自动旋转 if (UIDevice.currentDevice.systemVersion.floatValue > 11.1) { CGImagePropertyOrientation oritation = ((__bridge NSNumber*)CMGetAttachment(sampleBuffer, (__bridge CFStringRef)RPVideoSampleOrientationKey , NULL)).unsignedIntValue; pixelBuffer = [kit resizeAndRotatePixelBuffer:pixelBuffer withOrientation:oritation]; } else { // 0为unknown,走默认横屏纵屏处理 pixelBuffer = [kit resizeAndRotatePixelBuffer:pixelBuffer withOrientation:0]; } CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); [kit.streamerBase processVideoPixelBuffer:pixelBuffer timeInfo:pts]; tempVideoTimeStamp = CFAbsoluteTimeGetCurrent(); tempVideoPts = pts; tempVideoPixelBuffer = pixelBuffer; CFRetain(tempVideoPixelBuffer); } break; case RPSampleBufferTypeAudioApp: { [kit mixAudio:sampleBuffer to:kit.appTrack]; } break; case RPSampleBufferTypeAudioMic: [kit mixAudio:sampleBuffer to:kit.micTrack]; break; default: break; } } } 复制代码
系统回调回来的视频帧都是竖屏的全尺寸图像,咱们须要对其进行处理markdown
/** * 缩放旋转 */ - (CVPixelBufferRef)resizeAndRotatePixelBuffer:(CVPixelBufferRef)sourcePixelBuffer withOrientation:(CGImagePropertyOrientation)orientation { @synchronized(self) { CIImage *outputImage; if (self.privacyMode) { if (_privacyImage && outputPixelBuffer) { return outputPixelBuffer; } outputImage = self.privacyImage; } else { if (_privacyImage) { _privacyImage = nil; } CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:sourcePixelBuffer]; if (lastSourceOritation != orientation) { CGFloat outputWidth = self.videoSize.width; CGFloat outputHeight = self.videoSize.height; CGFloat inputWidth = sourceImage.extent.size.width; CGFloat inputHeight = sourceImage.extent.size.height; // 若是是横屏且输入源为横屏(iPad Pro)或者 竖屏且输入源为竖屏 if ((inputWidth > inputHeight && self.isLandscape) || (inputWidth <= inputHeight && !self.isLandscape)) { if (orientation == kCGImagePropertyOrientationUp) { lastRotateOritation = kCGImagePropertyOrientationUp; } else if (orientation == kCGImagePropertyOrientationDown) { lastRotateOritation = kCGImagePropertyOrientationDown; } lastRotateTransform = CGAffineTransformMakeScale(outputWidth/inputWidth, outputHeight/inputHeight); } else { // 须要进行旋转 if (orientation == kCGImagePropertyOrientationLeft) { lastRotateOritation = kCGImagePropertyOrientationRight; } else if (orientation == kCGImagePropertyOrientationRight) { lastRotateOritation = kCGImagePropertyOrientationLeft; } else { lastRotateOritation = kCGImagePropertyOrientationLeft; } lastRotateTransform = CGAffineTransformMakeScale(outputWidth/inputHeight, outputHeight/inputWidth); } } sourceImage = [sourceImage imageByApplyingOrientation:lastRotateOritation]; outputImage = [sourceImage imageByApplyingTransform:lastRotateTransform]; lastSourceOritation = orientation; } if (!outputPixelBuffer) { //推流 NSDictionary* pixelBufferOptions = @{ (NSString*) kCVPixelBufferWidthKey : @(self.videoSize.width), (NSString*) kCVPixelBufferHeightKey : @(self.videoSize.height), (NSString*) kCVPixelBufferOpenGLESCompatibilityKey : @YES, (NSString*) kCVPixelBufferIOSurfacePropertiesKey : @{}}; CVReturn ret = CVPixelBufferCreate(kCFAllocatorDefault, self.videoSize.width, self.videoSize.height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)pixelBufferOptions, &outputPixelBuffer); if (ret!= noErr) { NSLog(@"建立streamer buffer失败"); outputPixelBuffer = nil; return outputPixelBuffer; } } MTIImage *mtiImage = [[MTIImage alloc] initWithCIImage:outputImage]; if (cicontext) { NSError *error; [cicontext renderImage:mtiImage toCVPixelBuffer:outputPixelBuffer error:&error]; NSAssert(error == nil, @"渲染失败"); } return outputPixelBuffer; } } 复制代码
在直播过程当中好比要切换到QQ,或者输入密码等操做,不方便给观众看到,就须要用到隐私模式,用一张或多张图片来代替屏幕截屏。session
# UIImage 图片转成 CIImage 而后能够调整大小和方向,直接经过 CIContext 渲染到 CVPixelBufferRef 中 UIImage *privacyImage = [UIImage imageNamed:privacyImageName]; CIImage *sourceImage = [[CIImage alloc] initWithImage:privacyImage]; 复制代码
缓存上一个视频帧,根据推流的fps适当的补帧app
弹幕和礼物信息的显示有两种方案:框架
一、是在主 App 中,创建 Socket 链接,收到消息后,建立本地通知,显示礼物和弹幕,大部分直播应用采用这种方式比较多,对原来弹幕系统的改造比较小。ide
二、相似企鹅电竞的作法,经过Apns 远程推送通知的方式,实现弹幕礼物通知。
采用第一种弹幕就涉及到主App的后台保活问题:
经常使用的几种后台保活方式:VOIP,后台定位,播放空白声音。
考虑到耗电和上线审核的问题,咱们目前采用的是利用background task 播放空白声音。
# 建立 background task self.taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [[UIApplication sharedApplication] endBackgroundTask:weakSelf.taskIdentifier]; weakSelf.taskIdentifier = UIBackgroundTaskInvalid; }]; # 播放背景音乐 self.taskTimer = [NSTimer scheduledTimerWithTimeInterval:20.0f repeats:YES block:^(NSTimer * _Nonnull timer) { if ([[UIApplication sharedApplication] backgroundTimeRemaining] < 61.f) { //建立播放器 AVAudioSession *session = [AVAudioSession sharedInstance]; [session setActive:YES error:nil]; [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:weakSelf.musicUrl error:nil]; [audioPlayer prepareToPlay]; [audioPlayer play]; [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil]; } }]; 复制代码
腾讯系的游戏,例如王者荣耀,刺激战场,在 先开游戏,再开直播的状况下,会出现主播没法听到游戏声音的问题。
缘由是咱们利用 AVAudioPlayer 后台保活,咱们设置了 AVAudioSession 的 Option 为 AVAudioSessionCategoryOptionMixWithOthers 保证能和其余应用共用扬声器,可是这属性会致使 已经在播放的非Mix的声音被中止。
解决的方案只能是告知用户,先打开直播,再进入游戏,这样后播放的声音才不会有问题。
这个问题没有很好的解决办法,只能在断开之后建立通知告知用户。