文章是以實(shí)現(xiàn)iOS12+的錄屏功能(RPSystemBroadcastPickerView)來(lái)進(jìn)行闡述遇到的問(wèn)題,后續(xù)也會(huì)繼續(xù)補(bǔ)充相關(guān)技術(shù)點(diǎn)。方式是利用添加擴(kuò)展(RPBroadcastSampleHandler)的方式來(lái)實(shí)現(xiàn)錄屏功能,進(jìn)而闡述如何使用ffmpeg進(jìn)行視頻格式的轉(zhuǎn)碼。本文難點(diǎn)有兩點(diǎn):1,擴(kuò)展中獲取到的原始數(shù)據(jù)CMSampleBufferRef如何傳遞到宿主App;2,宿主App接收到擴(kuò)展(SampleHandler)傳遞的視頻流如何對(duì)其進(jìn)行硬編碼,最后實(shí)現(xiàn)格式轉(zhuǎn)碼。
添加擴(kuò)展,調(diào)取系統(tǒng)錄屏的方法
- 錄屏的方法
// 開(kāi)始錄屏
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// // 監(jiān)聽(tīng) 綁定socket
[[HYLocalServerBufferSocketManager defaultManager] setupSocket];
// 開(kāi)始錄屏?xí)r發(fā)出通知
[self sendNotificationForMessageWithIdentifier:@"broadcastStartedWithSetupInfo" userInfo:nil];
}
// 暫停錄屏
- (void)broadcastPaused {
[[HYLocalServerBufferSocketManager defaultManager] disConnectSocket];
[self sendNotificationForMessageWithIdentifier:@"broadcastPaused" userInfo:nil];
}
// 恢復(fù)錄屏
- (void)broadcastResumed {
[[HYLocalServerBufferSocketManager defaultManager] disConnectSocket];
[self sendNotificationForMessageWithIdentifier:@"broadcastResumed" userInfo:nil];
}
// 結(jié)束錄屏
- (void)broadcastFinished {
[[HYLocalServerBufferSocketManager defaultManager] disConnectSocket];
[self sendNotificationForMessageWithIdentifier:@"broadcastFinished" userInfo:nil];
}
// 這個(gè)方法是實(shí)時(shí)獲取錄屏直播,所以不要在這個(gè)方法中寫(xiě)發(fā)通知(同步),會(huì)造成線(xiàn)程阻塞的問(wèn)題
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVideo:
@autoreleasepool {
[[HYLocalServerBufferSocketManager defaultManager] sendVideoBufferToHostApp:sampleBuffer];
}
break;
case RPSampleBufferTypeAudioApp:
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
break;
default:
break;
}
}
- 錄屏的喚起方法
if (@available(iOS 12.0, *)) {
RPSystemBroadcastPickerView *broadcastPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame: [UIScreen mainScreen].bounds];
broadcastPickerView.showsMicrophoneButton = YES;
//你的app對(duì)用upload extension的 bundle id, 必須要填寫(xiě)對(duì)
if (@available(iOS 12.0, *)) {
broadcastPickerView.preferredExtension = @"com.hyTechnology.ffmpegDemo.XIBOBroadcastUploadExtension";
}
// 間接移除系統(tǒng)錄屏自帶的錄屏按鈕
for (UIView *view in broadcastPickerView.subviews) {
if ([view isKindOfClass:[UIButton class]]) {
[(UIButton*)view sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
}
上面就正式完成了iOS12+喚起系統(tǒng)自帶錄屏功能的擴(kuò)展及調(diào)用方式
數(shù)據(jù)傳遞方式
- local socket
- 通知(CFNotificationCenterRef)// 進(jìn)程間的通知方式,由于擴(kuò)展和宿主App分別是兩個(gè)獨(dú)立的運(yùn)行模塊,所以是兩個(gè)進(jìn)程。
- App Group
注意點(diǎn):利用CFNotificationCenterRef的方式傳遞數(shù)據(jù)時(shí),無(wú)法傳遞實(shí)時(shí)采集的原始數(shù)據(jù), 所以我這里利用通知來(lái)傳遞錄屏的狀態(tài), 有可能是自己方式不對(duì),后續(xù)繼續(xù)深入去了解... 。
方式一:通知方式傳遞
void NotificationCallback(CFNotificationCenterRef center,
void * observer,
CFStringRef name,
void const * object,
CFDictionaryRef userInfo) {
NSString *identifier = (__bridge NSString *)name;
NSObject *sender = (__bridge NSObject *)observer;
// 使用通知打印的userInfo為空, 這里我傳的是采集的原始幀數(shù)據(jù)
// NSDictionary *info = (__bridge NSDictionary *)userInfo;
// NSDictionary *infoss= CFBridgingRelease(userInfo);
NSDictionary *notiUserInfo = @{@"identifier":identifier};
[[NSNotificationCenter defaultCenter] postNotificationName:ScreenHoleNotificationName
object:sender
userInfo:notiUserInfo];
}
方式二:local socket
這種方式是參照阿里云的智能雙錄質(zhì)檢的幫助中心 iOS屏幕共享使用說(shuō)明的文檔。socket是一對(duì)套接字,服務(wù)端的套接字運(yùn)行在擴(kuò)展中,客戶(hù)端的套接字運(yùn)行在宿主App中。服務(wù)端的socket會(huì)先將CMSampleBufferRef
轉(zhuǎn)為 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
最后利用NTESI420Frame
將CVPixelBufferRef
轉(zhuǎn)為NSData
發(fā)給對(duì)應(yīng)得Host App的socket。
- 1,創(chuàng)建服務(wù)端和客戶(hù)端的socket。
- 2,服務(wù)端的socket將采集到的
CMSampleBufferRef
原始幀視頻轉(zhuǎn)為NSData格式發(fā)給客戶(hù)端的socket。 - 3,客戶(hù)端的socket接收到數(shù)據(jù)后再對(duì)數(shù)據(jù)進(jìn)行解碼得到原始幀數(shù)據(jù)
CMSampleBufferRef
。
利用ffmpeg進(jìn)行裸幀數(shù)據(jù)的格式編碼
- 安裝ffmpeg
1, 安裝過(guò)程Homebrew
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
2, 安裝 gas-preprocessor
sudo git clone https://github.com/bigsen/gas-preprocessor.git /usr/local/bin/gas
sudo cp /usr/local/bin/gas/gas-preprocessor.pl /usr/local/bin/gas-preprocessor.pl
sudo chmod 777 /usr/local/bin/gas-preprocessor.pl
sudo rm -rf /usr/local/bin/gas/
3, 安裝 yams
brew info yasm
4, 下載ffmpeg
brew install ffmpeg
5,進(jìn)入文件執(zhí)行下列命令
./build-ffmpeg.sh
注意: 再install ffmpeg的時(shí)候可能會(huì)失敗(eg:下圖1),按照提示install 即可,成功之后重新install ffmpeg
iOS集成ffmpeg
當(dāng)我們下載ffmpeg腳本后進(jìn)入文件夾使用./build-ffmpeg.sh
就會(huì)自動(dòng)生成FFmpeg-iOS等文件(如下圖)
。
- 第一步:將
FFmpeg-iOS
文件夾拖進(jìn)工程 - 第二步:配置頭文件的搜索路徑
在工程文件->Bulid Setting->Search Paths->Header Search Paths添加"$(SRCROOT)/ffmpegDemo/FFmpeg-iOS/include"
-
第三部:配置靜態(tài)庫(kù)等文件
配置靜態(tài)庫(kù)等文件 -
第四部: 添加fftools文件夾,tools文件中不是所有的都需要,按自己需求添加
fftools - 第五步:
#import "ffmpeg.h"
并在工程中寫(xiě)入av_register_all();
后?commond+B
編譯一下,這里會(huì)某些文件找不到的報(bào)錯(cuò),可直接到編譯后的腳本文件夾中
例如:config.h文件找不到,可以到下圖的文件夾中添加即可,其他同理
#include "libavutil/thread.h"
#include "libavutil/reverse.h"
#include "libavutil/libm.h"
#include "libpostproc/postprocess.h"
#include "libpostproc/version.h"
#include "libavformat/os_support.h"
#include "libavcodec/mathops.h"
#include "libavresample/avresample.h"
#include "compat/va_copy.h"
#include "libavutil/internal.h"
如果項(xiàng)目不要用到相關(guān)的文件,可以直接注釋錯(cuò)誤
eg: // #include "libavutil/internal.h"
ffmpeg的格式轉(zhuǎn)換
NSString *fromFile = [[NSBundle mainBundle]pathForResource:@"videp.mp4" ofType:nil];
NSString *toFile = @"/Users/Alex/output/source/video.gif";
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:toFile]) {
[fileManager removeItemAtPath:toFile error:nil];
}
char *a[] = {
"ffmpeg", "-i", (char *)[fromFile UTF8String], (char *)[toFile UTF8String]
};
int result = ffmpeg_main(sizeof(a)/sizeof(*a), a);
NSLog(@"這是結(jié)果 %d",result);
或
ffmpeg –i test.mp4 –vcodec h264 –s 352*278 –an –f m4v video.264 //轉(zhuǎn)碼為碼流原始文件
ffmpeg –i test.mp4 –vcodec h264 –bf 0 –g 25 –s 352*278 –an –f m4v video.264 //轉(zhuǎn)碼為碼流原始文件
ffmpeg –i test.avi -vcodec mpeg4 –vtag xvid –qsame test_xvid.avi //轉(zhuǎn)碼為封裝文件
//-bf B幀數(shù)目控制,-g 關(guān)鍵幀間隔控制,-s 分辨率控制
思路解析
我們?cè)谔砑訑U(kuò)展后拿到的一般是CMSampleBufferRef格式的原始數(shù)據(jù),而這種格式的數(shù)據(jù)不能進(jìn)行進(jìn)程間的直接傳遞,以及播放。所以需要進(jìn)行處理和轉(zhuǎn)碼。
1,將CMSampleBufferRef格式轉(zhuǎn)為.264(NSData)的格式
2,可以使用ffmpeg的轉(zhuǎn)碼將.264(NSData)轉(zhuǎn)為MP4,需要注意的是設(shè)置的size,不對(duì)的話(huà),可能會(huì)造成視頻的拉伸或擠壓。
將CMSampleBufferRef格式的幀數(shù)據(jù)轉(zhuǎn)成mp4,并保存到沙盒中
思路:
-
1,利用socket、通知、AppGroup的方式將擴(kuò)展中中實(shí)時(shí)采集的幀數(shù)據(jù)CMSampleBufferRef傳遞到宿主App中
Snip20210912_9.png 2,利用AVCaptureSession、AVAssetWriter、AVAssetWriterInput將CMSampleBufferRef轉(zhuǎn)為mp4, 并[圖片上傳中...(Snip20210912_7.png-aa8236-1631443458420-0)]
利用NSFileManger保存到指定沙盒
- (void)startScreenRecording {
self.captureSession = [[AVCaptureSession alloc]init];
self.screenRecorder = [RPScreenRecorder sharedRecorder];
if (self.screenRecorder.isRecording) {
return;
}
NSError *error = nil;
NSArray *pathDocuments = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *outputURL = pathDocuments[0];
self.videoOutPath = [[outputURL stringByAppendingPathComponent:@"demo是嗯嗯嗯是是"] stringByAppendingPathExtension:@"mp4"];
NSLog(@"self.videoOutPath=%@",self.videoOutPath);
self.assetWriter = [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:self.videoOutPath] fileType:AVFileTypeMPEG4 error:&error];
NSDictionary *compressionProperties =
@{AVVideoProfileLevelKey : AVVideoProfileLevelH264HighAutoLevel,
AVVideoH264EntropyModeKey : AVVideoH264EntropyModeCABAC,
AVVideoAverageBitRateKey : @(1920 * 1080 * 11.4),
AVVideoMaxKeyFrameIntervalKey : @60,
AVVideoAllowFrameReorderingKey : @NO};
NSNumber* width= [NSNumber numberWithFloat:[[UIScreen mainScreen] bounds].size.width];
NSNumber* height = [NSNumber numberWithFloat:[[UIScreen mainScreen] bounds].size.height];
NSDictionary *videoSettings =
@{
AVVideoCompressionPropertiesKey : compressionProperties,
AVVideoCodecKey : AVVideoCodecTypeH264,
AVVideoWidthKey : width,
AVVideoHeightKey : height
};
self.assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
self.pixelBufferAdaptor =
[[AVAssetWriterInputPixelBufferAdaptor alloc]initWithAssetWriterInput:self.assetWriterInput
sourcePixelBufferAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA],kCVPixelBufferPixelFormatTypeKey,nil]];
[self.assetWriter addInput:self.assetWriterInput];
[self.assetWriterInput setMediaTimeScale:60];
[self.assetWriter setMovieTimeScale:60];
[self.assetWriterInput setExpectsMediaDataInRealTime:YES];
//寫(xiě)入視頻
[self.assetWriter startWriting];
[self.assetWriter startSessionAtSourceTime:kCMTimeZero];
[self.captureSession startRunning];
}
-
3,開(kāi)始錄屏?xí)r初始化assetWriter,結(jié)束錄屏?xí)r進(jìn)行寫(xiě)入完成操作
初始化assetWriter和完成
總結(jié)
整個(gè)過(guò)程中最簡(jiǎn)單的方式還是使用AppGroup
的方式進(jìn)行進(jìn)程間的傳值操作,通知方式CFNotificationCenterRef
不能傳遞錄屏的數(shù)據(jù),所以在項(xiàng)目中使用通知是監(jiān)聽(tīng)錄屏的狀態(tài)status
。socket
的方式需要用到GCDAsyncSocket
,在套接字之間傳的是NTESI420Frame
轉(zhuǎn)化的NSData
,最后再轉(zhuǎn)回為CMSampleBufferRef
。類(lèi)似剪映等產(chǎn)品的錄屏應(yīng)該都是使用AVAssetWriter、AVAssetWriterInput、AVAssetWriterInputPixelBufferAdaptor、AVCaptureSession
來(lái)處理原始的幀數(shù)據(jù)后保存到沙盒中。