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;
}
}
}
複製代碼
系統回調回來的視頻幀都是豎屏的全尺寸圖像,咱們須要對其進行處理session
/**
* 縮放旋轉
*/
- (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,或者輸入密碼等操做,不方便給觀衆看到,就須要用到隱私模式,用一張或多張圖片來代替屏幕截屏。app
# UIImage 圖片轉成 CIImage 而後能夠調整大小和方向,直接經過 CIContext 渲染到 CVPixelBufferRef 中
UIImage *privacyImage = [UIImage imageNamed:privacyImageName];
CIImage *sourceImage = [[CIImage alloc] initWithImage:privacyImage];
複製代碼
緩存上一個視頻幀,根據推流的fps適當的補幀框架
彈幕和禮物信息的顯示有兩種方案:ide
一、是在主 App 中,創建 Socket 鏈接,收到消息後,建立本地通知,顯示禮物和彈幕,大部分直播應用採用這種方式比較多,對原來彈幕系統的改造比較小。ui
二、相似企鵝電競的作法,經過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的聲音被中止。
解決的方案只能是告知用戶,先打開直播,再進入遊戲,這樣後播放的聲音纔不會有問題。
這個問題沒有很好的解決辦法,只能在斷開之後建立通知告知用戶。