ReplayKit2 屏幕錄制

ReplayKit2 屏幕錄制

如果你需要錄制蘋果手機(jī)屏幕,ReplayKit肯定需要了解阔馋。本文主要介紹Replaykit2 在iOS12后的一些技巧及使用方法。為啥不介紹iOS12前的錄制呢排嫌,因為操作起來太麻煩了芯勘,麻煩到開發(fā)的我們使用起來都很不順手,別說用戶使用装蓬,而且現(xiàn)在蘋果都iOS14了...

ReplayKit2 使用的技巧

  1. 由于系統(tǒng)提供的是個RPSystemBroadcastPickerView類型的View著拭,需要用戶點擊這個View才能彈出錄制界面。那如何才能優(yōu)雅的把它給隱藏呢牍帚?答案是我們在它上面覆蓋一層View儡遮,然后把點擊事件傳遞給它達(dá)到點擊效果。事件類型根據(jù)不同系統(tǒng)版本會稍有不同,直接貼代碼:

    @property (nonatomic, strong) RPSystemBroadcastPickerView *sysTemBroadCastPickerView; //錄制view
    @property (nonatomic, strong) UIButton *startPushStreamBtn;                           //開始錄制按鈕
    
    
    - (void)showReplayKitView
    {
       if (@available(iOS 12.0, *)) {
           for (UIView *view in _sysTemBroadCastPickerView.subviews) {
               if ([view isKindOfClass:[UIButton class]]) {
                   float iOSVersion = [[UIDevice currentDevice].systemVersion floatValue];
                   UIButton *button = (UIButton *)view;
                   if (button != self.startPushStreamBtn) {
                       if (iOSVersion >= 13) {
                           [(UIButton *)view sendActionsForControlEvents:UIControlEventTouchDown];
                           [(UIButton *)view sendActionsForControlEvents:UIControlEventTouchUpInside];
                       } else {
                           [(UIButton *)view sendActionsForControlEvents:UIControlEventTouchDown];
                       }
                   }
               }
           }
       }
    }
    
  1. 如何指定錄制應(yīng)用暗赶,是否使用麥克風(fēng)鄙币。注意BundleID為錄制TargetBundleID,和主工程的BundleID區(qū)分下蹂随。如果不填寫十嘿,彈窗的錄制界面將會帶上手機(jī)上所有支持錄制的應(yīng)用,是不是很不友好..

    - (RPSystemBroadcastPickerView *)sysTemBroadCastPickerView
        API_AVAILABLE(ios(12.0))
    {
        if (!_sysTemBroadCastPickerView) {
            _sysTemBroadCastPickerView = [[RPSystemBroadcastPickerView alloc] init];
            _sysTemBroadCastPickerView.showsMicrophoneButton = NO;//是否顯示麥克風(fēng)
            _sysTemBroadCastPickerView.preferredExtension = [MnaConfig replayKitBundleID];//指定錄制應(yīng)用BundleID
        }
        return _sysTemBroadCastPickerView;
    }
    
    
  1. 點擊開始直播后有個倒計時岳锁,倒計時結(jié)束后如何優(yōu)雅的退出錄制界面绩衷?首先我們需要捕獲系統(tǒng)錄制彈窗,看彈出效果應(yīng)該是 presentViewController 然后采用Method Swizzling嘗試下,發(fā)現(xiàn)可以拿到唇聘。然后我們可以在錄制進(jìn)程啟動后發(fā)送進(jìn)程通知版姑,主進(jìn)程收到后進(jìn)行dismiss:代碼如下:

    @implementation UIViewController (MnaPresentSwizzleAdd)
    
    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self swizzleSelector:@selector(presentViewController:animated:completion:) withAnotherSelector:@selector(mna_presentViewController:animated:completion:)];
        });
    }
    
    + (void)swizzleSelector:(SEL)originalSelector withAnotherSelector:(SEL)swizzledSelector
    {
        Class aClass = [self class];
    
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
    
        BOOL didAddMethod =
            class_addMethod(aClass,
                            originalSelector,
                            method_getImplementation(swizzledMethod),
                            method_getTypeEncoding(swizzledMethod));
    
        if (didAddMethod) {
            class_replaceMethod(aClass,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    
    #pragma mark - Method Swizzling
    
    - (void)mna_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
    {
        if ([NSStringFromClass(viewControllerToPresent.class) isEqualToString:@"RPBroadcastPickerStandaloneViewController"]) {
            MnaReplayKitHiddenManager.sharedInstance.replayKitBraodViewControler = viewControllerToPresent; //該管理類監(jiān)聽錄制進(jìn)程啟動完成的通知然后進(jìn)行Dismiss
            [self mna_presentViewController:viewControllerToPresent animated:flag completion:completion];
        } else {
            [self mna_presentViewController:viewControllerToPresent animated:flag completion:completion];
        }
        
    }
    
    
    @end
    
    

    更新:iOS14后彈出方法更改柱搜,暫時還不知道如何顯示迟郎,該方法只適用iOS14以下

  2. 進(jìn)程通知,進(jìn)程間通訊非常麻煩聪蘸∠苄ぃ可以采用CFNotificationCenterPostNotification進(jìn)行消息傳遞,不過有個問題是不能傳遞數(shù)據(jù)健爬。不過我們可以使用App Groups可以數(shù)據(jù)共享的方式來進(jìn)行傳遞控乾。推薦一個已經(jīng)封裝好的開源庫 MMWormhole

踩過的坑

  1. Extension進(jìn)程最麻煩的就是調(diào)試。如果要查看Log娜遵,錄制調(diào)試可以選擇 錄制進(jìn)程->運行->選擇主進(jìn)程蜕衡。這樣啟動后可以在終端看到Log,不過以前同事遇到過某些Xcode版本不能運行设拟,并看不了Log慨仿。測試可行的Xcode版本:Version 12.1 (12A7403)

  2. 如果你需要打印一些日志保存,進(jìn)行問題定位,需要保存到Group共享區(qū)纳胧。獲取方式:

    [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:goupName];
    
  3. 內(nèi)存限制50M這個非常重要镰吆,因為如果超過這個大小直接被系統(tǒng)殺掉。如果你做推流:必須控制緩沖隊列大小,做好內(nèi)存管理跑慕。

  1. 音頻輸出為大端万皿,如果需要,可以轉(zhuǎn)換下大小端核行。轉(zhuǎn)換為小端代碼如下:

    - (NSData *)convertAudioSamepleBufferToPcmData:(CMSampleBufferRef)sampleBuffer
    {
        CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
        if (blockBuffer == nil) {
            return nil;
        }
    
        AudioBufferList bufferList;
        CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
                                                                NULL,
                                                                &bufferList,
                                                                sizeof(bufferList),
                                                                NULL,
                                                                NULL,
                                                                kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
                                                                &blockBuffer);
    
        int8_t *audioBuffer = (int8_t *)bufferList.mBuffers[0].mData;
        UInt32 audioBufferSizeInBytes = bufferList.mBuffers[0].mDataByteSize;
    
        CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
        const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription);
    
        // Perform an endianess conversion, if needed. A TVIAudioDevice should deliver little endian samples.
        if (asbd->mFormatFlags & kAudioFormatFlagIsBigEndian) { //大端
            for (int i = 0; i < (audioBufferSizeInBytes - 1); i += 2) {
                int8_t temp = audioBuffer[i];
                audioBuffer[i] = audioBuffer[i + 1];
                audioBuffer[i + 1] = temp;
            }
        } else { //小端
        }
        NSData *data = [NSData dataWithBytes:audioBuffer length:audioBufferSizeInBytes];
        CFRelease(blockBuffer);
    
        return data;
    }
    
  1. 不管你手機(jī)豎屏還是橫屏牢硅,非常無語的是視頻輸出,都是豎屏芝雪。還好iOS11后有個方法可以判定當(dāng)前輸出的視頻幀方向:

    CGImagePropertyOrientation oritation = ((__bridge NSNumber *)CMGetAttachment(buffer, (__bridge CFStringRef)RPVideoSampleOrientationKey, NULL)).unsignedIntValue;
    typedef CF_CLOSED_ENUM(uint32_t, CGImagePropertyOrientation) {
        kCGImagePropertyOrientationUp = 1,        // 0th row at top,    0th column on left   - default orientation
        kCGImagePropertyOrientationUpMirrored,    // 0th row at top,    0th column on right  - horizontal flip
        kCGImagePropertyOrientationDown,          // 0th row at bottom, 0th column on right  - 180 deg rotation
        kCGImagePropertyOrientationDownMirrored,  // 0th row at bottom, 0th column on left   - vertical flip
        kCGImagePropertyOrientationLeftMirrored,  // 0th row on left,   0th column at top
        kCGImagePropertyOrientationRight,         // 0th row on right,  0th column at top    - 90 deg CW
        kCGImagePropertyOrientationRightMirrored, // 0th row on right,  0th column on bottom
        kCGImagePropertyOrientationLeft           // 0th row on left,   0th column at bottom - 90 deg CCW
    };
    
  1. 由于視頻都是豎屏輸出唤衫,所以需要旋轉(zhuǎn)方向然后再轉(zhuǎn)換為CVPixelBufferRef進(jìn)行硬編碼。找過很多資料绵脯,這塊介紹很少佳励。有介紹使用開源庫libyuv 不過是轉(zhuǎn)換為i420后調(diào)用的騰訊云接口無相關(guān)涉及。現(xiàn)在使用的是方法:

    #pragma mark - Rotation default stream
    
    - (void)dealWithSampleBuffer:(CMSampleBufferRef)buffer timeStamp:(uint64_t)timeStamp
    {
        if (@available(iOS 11.0, *)) {
            CGImagePropertyOrientation oritation = ((__bridge NSNumber *)CMGetAttachment(buffer, (__bridge CFStringRef)RPVideoSampleOrientationKey, NULL)).unsignedIntValue;
            CIImage *outputImage = nil;
            CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(buffer);
            CGFloat outputWidth = self.session.videoConfiguration.videoSize.width;
            CGFloat outputHeight = self.session.videoConfiguration.videoSize.height;
            BOOL isLandScape = self.session.videoConfiguration.landscape;
            size_t inputWidth = CVPixelBufferGetWidth(pixelBuffer);
            size_t inputHeight = CVPixelBufferGetHeight(pixelBuffer);
            CGAffineTransform lastRotateTransform = CGAffineTransformMakeScale(0.5, 0.5);
            CIImage *sourceImage = nil;
            
            CGImagePropertyOrientation lastRotateOritation = oritation;
            sourceImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
            // 如果是橫屏且輸入源為橫屏(iPad Pro)或者 豎屏且輸入源為豎屏
            if ((inputWidth > inputHeight && isLandScape) || (inputWidth <= inputHeight && !isLandScape)) {
                if (oritation == kCGImagePropertyOrientationUp) {
                    lastRotateOritation = kCGImagePropertyOrientationUp;
                } else if (oritation == kCGImagePropertyOrientationDown) {
                    lastRotateOritation = kCGImagePropertyOrientationDown;
                }
                lastRotateTransform = CGAffineTransformMakeScale(outputWidth / inputWidth, outputHeight / inputHeight);
            } else {
                if (oritation == kCGImagePropertyOrientationLeft) {
                    lastRotateOritation = kCGImagePropertyOrientationRight;
                } else if (oritation == kCGImagePropertyOrientationRight) {
                    lastRotateOritation = kCGImagePropertyOrientationLeft;
                } else {
                    lastRotateOritation = kCGImagePropertyOrientationLeft;
                }
                lastRotateTransform = CGAffineTransformMakeScale(outputWidth / inputHeight, outputHeight / inputWidth);
            }
            sourceImage = [sourceImage imageByApplyingCGOrientation:lastRotateOritation];
            outputImage = [sourceImage imageByApplyingTransform:lastRotateTransform];
            
            
            if (outputImage) {
                NSDictionary *pixelBufferOptions = @{(NSString *)kCVPixelBufferWidthKey : @(outputWidth),
                                                     (NSString *)kCVPixelBufferHeightKey : @(outputHeight),
                                                     (NSString *)kCVPixelBufferOpenGLESCompatibilityKey : @YES,
                                                     (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{} };
                
                
                CVPixelBufferLockBaseAddress(pixelBuffer, 0);
                CVPixelBufferRef newPixcelBuffer = nil;
                CVPixelBufferCreate(kCFAllocatorDefault, outputWidth, outputHeight, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, (__bridge CFDictionaryRef)pixelBufferOptions, &newPixcelBuffer);
                [_ciContext render:outputImage toCVPixelBuffer:newPixcelBuffer];
                CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
                CMVideoFormatDescriptionRef videoInfo = nil;
                CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, newPixcelBuffer, &videoInfo);
                CMTime duration = CMSampleBufferGetDuration(buffer);
                CMTime presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(buffer);
                CMTime decodeTimeStamp = CMSampleBufferGetDecodeTimeStamp(buffer);
                CMSampleTimingInfo sampleTimingInfo;
                sampleTimingInfo.duration = duration;
                sampleTimingInfo.presentationTimeStamp = presentationTimeStamp;
                sampleTimingInfo.decodeTimeStamp = decodeTimeStamp;
                //
                CMSampleBufferRef newSampleBuffer = nil;
                CMSampleBufferCreateForImageBuffer(kCFAllocatorMalloc, newPixcelBuffer, true, nil, nil, videoInfo, &sampleTimingInfo, &newSampleBuffer);
                // 對新buffer做處理
                [self.session pushVideoBuffer:newSampleBuffer timeStamp:timeStamp];
                // release
                if (newPixcelBuffer) {
                    CVPixelBufferRelease(newPixcelBuffer);
                }
                if (newSampleBuffer) {
                    CFRelease(newSampleBuffer);
                }
            }
        } else {
            // Fallback on earlier versions
            [self.session pushVideoBuffer:buffer timeStamp:timeStamp];
        }
    }
    
  1. 推流蛆挫≡叱校可以參考 LFLiveKit ,需要注意的是前面說的緩沖區(qū)大小設(shè)置悴侵,還有就是里面LibRTMP調(diào)整輸出塊大小瞧剖,減少CPU消耗。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抓于,隨后出現(xiàn)的幾起案子做粤,更是在濱河造成了極大的恐慌,老刑警劉巖捉撮,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怕品,死亡現(xiàn)場離奇詭異,居然都是意外死亡巾遭,警方通過查閱死者的電腦和手機(jī)肉康,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灼舍,“玉大人吼和,你說我怎么就攤上這事∑锼兀” “怎么了炫乓?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長献丑。 經(jīng)常有香客問我末捣,道長,這世上最難降的妖魔是什么阳距? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任塔粒,我火速辦了婚禮,結(jié)果婚禮上筐摘,老公的妹妹穿的比我還像新娘卒茬。我一直安慰自己,他們只是感情好咖熟,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布圃酵。 她就那樣靜靜地躺著,像睡著了一般馍管。 火紅的嫁衣襯著肌膚如雪郭赐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天确沸,我揣著相機(jī)與錄音捌锭,去河邊找鬼。 笑死罗捎,一個胖子當(dāng)著我的面吹牛观谦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播桨菜,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼豁状,長吁一口氣:“原來是場噩夢啊……” “哼捉偏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起泻红,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤夭禽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谊路,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讹躯,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年凶异,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜀撑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挤巡。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡剩彬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出矿卑,到底是詐尸還是另有隱情喉恋,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布母廷,位于F島的核電站轻黑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏琴昆。R本人自食惡果不足惜氓鄙,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望业舍。 院中可真熱鬧抖拦,春花似錦、人聲如沸舷暮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽下面。三九已至复颈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沥割,已是汗流浹背耗啦。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留机杜,地道東北人帜讲。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像叉庐,于是被迫代替她去往敵國和親舒帮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容