一、屏幕录制权限
应用首次创建屏幕截图或者窗口截图时(以下任一接口都可以),就会显示系统的权限申请弹窗,通过选中Screen Recording
里面的相关应用,重启应用后即可开启权限。
CGDisplayCreateImage()
CGDisplayCreateImageForRect()
CGWindowListCreateImage()
CGWindowListCreateImageFromArray()
二、获取screenId
通过AppKit的NSScreen.screens
或者通过CoreGraphics的CGGetActiveDisplayList()
都可以获取当前的所有显示器信息,下面用NSScreen.screens
举例子:
[NSScreen.screens enumerateObjectsUsingBlock:^(NSScreen * _Nonnull screen, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber *screenNumber = screen.deviceDescription[@"NSScreenNumber"];
if (screenNumber) {
CGDirectDisplayID displayId = screenNumber.unsignedIntValue;
NSLog(@"displayId: %d", displayId);
}
}];
注意:如果是镜像显示,则只会打印一个显示器信息。
三、获取windowId
1、获取windowId列表
通过接口CGWindowListCopyWindowInfo()
可以获取windowId列表。
其中kCGWindowListExcludeDesktopElements
参数表示从列表中排除所有属于桌面元素的窗口。
CGWindowListCopyWindowInfo()
返回的是一个CFArrayRef
,使用完后需要主动release。 这里有一个技巧,利用CFBridgingRelease()
函数,可以把CFArrayRef
桥接到NSArray
,并且会把内存管理转移到ARC,就是说我们不需要再去主动release了。
NSArray *windowDictArr = CFBridgingRelease(CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, 0));
2、过滤无用windowId
通过以上接口获取到的windowId列表可能会包含很多我们不关心的windowId:
1. 没显示出来的window
通过kCGWindowLayer
跟kCGWindowAlpha
两个key,可以获取到当前window是否有layer以及是否透明,我们需要过滤掉不可见的window。
2. 过滤信息不全的window
kCGWindowNumber
key可以从字典里获取到windowId,需要过滤没有windowId的字典信息。
3. 过滤无法获取截图的window
通过CGWindowListCreateImage()
接口,传入对应windowId,可获取到window的截图,如果无法获取到截图,可能没有录屏权限、不是window主体、window不可见。
3、获取window frame
通过CGRectMakeWithDictionaryRepresentation()
可获取窗口的frame,某些业务可能需要用到frame。
四、屏幕共享
屏幕共享的视频输入总体来说跟摄像头的视频输入相当类似,可以理解成视频流从摄像头改成了屏幕输入(AVCaptureDeviceInput
->AVCaptureScreenInput
),视频输出的处理则跟摄像头的输出处理完全一样。
1、关键代码:
- (void)startCapture {
self.captureSession = [[AVCaptureSession alloc] init];
self.captureSession.sessionPreset = AVCaptureSessionPresetMedium;
// input
AVCaptureScreenInput *input = [[AVCaptureScreenInput alloc] initWithDisplayID:kCGDirectMainDisplay];
[self.captureSession addInput:input];
// output
[self.captureSession beginConfiguration];
dispatch_queue_t queue = dispatch_queue_create("smaple_buffer", DISPATCH_QUEUE_SERIAL);
AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
videoDataOutput.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)};
[videoDataOutput setSampleBufferDelegate:self queue:queue];
AVCaptureConnection *videoConnection = [videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
[self.captureSession addOutput:videoDataOutput];
[self.captureSession commitConfiguration];
[self.captureSession startRunning];
}
2、视频输出分辨率设置
我们的编码分辨率最大为720p,一个是避免捕获到的图片过大造成性能浪费,一个是避免难以适配太多的屏幕分辨率,我们需要指定输出的分辨率。
captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
// 如果下面代码不设置不会生效
AVCaptureVideoDataOutput *output = captureSession.outputs[0];
[output setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
(id)kCVPixelBufferWidthKey:@1280,
(id)kCVPixelBufferHeightKey:@720}];
五、窗口共享
窗口共享没有现有的系统接口,跟屏幕共享差异较大,总体来说,有以下几步
- 启动定时器;
- 定时获取窗口的截图;
- 对截图进行处理(添加光标、裁剪);
- 把
CGImageRef
转换为yuv数据; - 调整分辨率。
1、启动定时器
定时器的间隔与设置的帧率相关,dispatch_source_set_timer()
要注意的是interval
参数的单位是纳秒。
2、获取窗口截图
通过CGWindowListCreateImage()
可以获取指定windowId的窗口截图,我们需要原始分辨率,而且截图中不需要包含窗口的装饰(如阴影),所以其中imageOption
需要指定(kCGWindowImageNominalResolution | kCGWindowImageBoundsIgnoreFraming)
。
CGImageRef windowImage = CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowId, (kCGWindowImageNominalResolution | kCGWindowImageBoundsIgnoreFraming));
3、追加光标
通过CGWindowListCreateImage()
获取的窗口截图不会包含光标,所以需要额外追加光标。
- 获取光标的坐标,
[NSEvent mouseLocation]
- 获取光标的图片,
[[NSCursor currentSystemCursor] image]
- 创建上下文,
CGBitmapContextCreate()
- 画背景、画光标,
CGContextDrawImage()
4、裁剪
这里主要是为了需要指定区域捕获视频,接口CGImageCreateWithImageInRect()
5、转换为yuv数据
- 把
CGImageRef
转换为rgbaBuffer,CVPixelBufferCreateWithBytes()
- 创建yuvBuffer的内存空间,
CVPixelBufferCreate()
- 利用libyuv库的
ARGBToI420()
函数把rgbaBuffer的信息写到yuvBuffer里
6、分辨率处理
窗口的分辨率相对不可控,可能会被用户拉伸成一个特别大的窗口,我们在初始化渲染画布时就不好确定长宽。
- 设置最大分辨率为1280*720,把窗口截图的长宽等比缩放为小于最大分辨率的长宽,关键代码:
- (CGSize)resize:(CGSize)size maxSize:(CGSize)maxSize {
CGSize outSize = size;
if (size.width > maxSize.width || size.height > maxSize.height) {
if ((size.width / size.height) > (maxSize.width / maxSize.height)) {
outSize.width = maxSize.width;
outSize.height = ceil(size.height * maxSize.width / size.width);
} else {
outSize.height = maxSize.height;
outSize.width = ceil(size.width * maxSize.height / size.height);
}
}
return outSize;
}
- 通过libyuv库的
I420Scale()
把上面的yuvBuffer缩放为scaledYuvBuffer
六、捕获指定区域视频
1、计算Rect
- 判断是否需要裁剪,若传入的是
CGRectNull
或CGRectZero
,则不需要裁剪; - 取交集,可通过
CGRectIntersection()
比较方便获取两个Rect的交集,如果无交集则认为传入的Rect无效,不做裁剪;另外,如果计算出来的Rect的长宽小于8,编码时可能崩溃,这里加上了长宽大于等于10的判断; - 转换坐标系,参数传入时的坐标系是左上角为原点,屏幕共享时设置捕获区域的接口的坐标系是左下角为原点,
targetRect.origin.y = CGRectGetMaxY(screenBound) - CGRectGetMaxY(intersection);
2、屏幕共享
直接设置AVCaptureScreenInput
的cropRect
属性即可,self.screenInput.cropRect = targetRect;
3、窗口共享
获取到窗口截图后做裁剪即可,CGImageRef croppedImage = CGImageCreateWithImageInRect(showImage, cropRect);
七、设置捕获帧率
1、屏幕共享
设置AVCaptureScreenInput
的minFrameDuration
属性即可。
AVCaptureScreenInput *screenInput = (AVCaptureScreenInput *)input;
screenInput.minFrameDuration = CMTimeMake(1, videoFrameRate);
2、窗口共享
设置窗口截图定时器的时间间隔即可。
八、其它
1、窗口最小化
窗口最小化后,下面接口都返回了空,注意要做防崩溃处理
CGWindowListCopyWindowInfo()
CGWindowListCreateImage()
2、屏幕的不同显示模式
- Mirror Displays,如果选中了镜像模式,则
NSScreen.screens
只返回一个显示器的信息; - Scaled,如果选中了缩放,则输出的分辨率会变化,注意要做处理;
- Rotation,同上;
- 不同分辨率的显示器,同上。