ReplayKit2 直播

1. iOS 遊戲直播方案簡介

  • 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 提供了更通用的桌面級錄屏方案,本文接下來會着重介紹這種方案。緩存

2. ReplayKit2 概述

錄屏功能是 iOS 10 新推出的特性,蘋果在 iOS 9 的 ReplayKit 保存錄屏視頻的基礎上,增長了視頻流實時直播功能,官方介紹見 Go Live with ReplayKit。iOS 11 加強爲 ReplayKit2,進一步提高了 Replaykit 的易用性和通用性,而且能夠對整個手機實現屏幕錄製,而非某些作了支持ReplayKit功能的App,所以錄屏推流建議直接使用iOS11的ReplayKit2屏幕錄製方式。系統錄屏採用的是擴展方式,擴展程序有單獨的進程,iOS 系統爲了保證系統流暢,給擴展程序的資源相對較少,擴展程序內存佔用過大也會被 Kill 掉。bash

3. 部分關鍵功能實現

# 系統回調處理
- (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;
        }
    }
}
複製代碼

4. 部分已知問題及解決方案

4.1 屏幕幀方向

系統回調回來的視頻幀都是豎屏的全尺寸圖像,咱們須要對其進行處理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;
    }
}

複製代碼

4.2 隱私模式的實現

在直播過程當中好比要切換到QQ,或者輸入密碼等操做,不方便給觀衆看到,就須要用到隱私模式,用一張或多張圖片來代替屏幕截屏。app

# UIImage 圖片轉成 CIImage 而後能夠調整大小和方向,直接經過 CIContext 渲染到 CVPixelBufferRef 中
UIImage *privacyImage = [UIImage imageNamed:privacyImageName];
CIImage *sourceImage = [[CIImage alloc] initWithImage:privacyImage];
複製代碼

4.3 某些狀況下視頻幀不回調

緩存上一個視頻幀,根據推流的fps適當的補幀框架

4.4 彈幕和禮物信息的顯示

彈幕和禮物信息的顯示有兩種方案:ide

一、是在主 App 中,創建 Socket 鏈接,收到消息後,建立本地通知,顯示禮物和彈幕,大部分直播應用採用這種方式比較多,對原來彈幕系統的改造比較小。ui

二、相似企鵝電競的作法,經過Apns 遠程推送通知的方式,實現彈幕禮物通知。

4.5 後臺保活

採用第一種彈幕就涉及到主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];
}
}];
複製代碼

4.6 部分遊戲無聲音

騰訊系的遊戲,例如王者榮耀,刺激戰場,在 先開遊戲,再開直播的狀況下,會出現主播沒法聽到遊戲聲音的問題。

緣由是咱們利用 AVAudioPlayer 後臺保活,咱們設置了 AVAudioSession 的 Option 爲 AVAudioSessionCategoryOptionMixWithOthers 保證能和其餘應用共用揚聲器,可是這屬性會致使 已經在播放的非Mix的聲音被中止。

解決的方案只能是告知用戶,先打開直播,再進入遊戲,這樣後播放的聲音纔不會有問題。

4.7 鎖屏後錄屏斷開

這個問題沒有很好的解決辦法,只能在斷開之後建立通知告知用戶。

5. 參考

  1. 騰訊雲文檔-遊戲錄屏(ReplayKit)
  2. replaykit2 直播踩坑總結
  3. iOS11 ReplayKit2 問題總結
相關文章
相關標籤/搜索