一、屏幕录制权限

e33c5801ba456666fadd2aa2602bd730

应用首次创建屏幕截图或者窗口截图时(以下任一接口都可以),就会显示系统的权限申请弹窗,通过选中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

通过kCGWindowLayerkCGWindowAlpha两个key,可以获取到当前window是否有layer以及是否透明,我们需要过滤掉不可见的window。

2. 过滤信息不全的window

kCGWindowNumberkey可以从字典里获取到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}];

五、窗口共享

窗口共享没有现有的系统接口,跟屏幕共享差异较大,总体来说,有以下几步

  1. 启动定时器;
  2. 定时获取窗口的截图;
  3. 对截图进行处理(添加光标、裁剪);
  4. CGImageRef转换为yuv数据;
  5. 调整分辨率。

1、启动定时器

定时器的间隔与设置的帧率相关,dispatch_source_set_timer()要注意的是interval参数的单位是纳秒。

2、获取窗口截图

通过CGWindowListCreateImage()可以获取指定windowId的窗口截图,我们需要原始分辨率,而且截图中不需要包含窗口的装饰(如阴影),所以其中imageOption需要指定(kCGWindowImageNominalResolution | kCGWindowImageBoundsIgnoreFraming)

CGImageRef windowImage = CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowId, (kCGWindowImageNominalResolution | kCGWindowImageBoundsIgnoreFraming));

3、追加光标

通过CGWindowListCreateImage()获取的窗口截图不会包含光标,所以需要额外追加光标。

  1. 获取光标的坐标,[NSEvent mouseLocation]
  2. 获取光标的图片,[[NSCursor currentSystemCursor] image]
  3. 创建上下文,CGBitmapContextCreate()
  4. 画背景、画光标,CGContextDrawImage()

4、裁剪

这里主要是为了需要指定区域捕获视频,接口CGImageCreateWithImageInRect()

5、转换为yuv数据

  1. CGImageRef转换为rgbaBuffer,CVPixelBufferCreateWithBytes()
  2. 创建yuvBuffer的内存空间,CVPixelBufferCreate()
  3. 利用libyuv库的ARGBToI420()函数把rgbaBuffer的信息写到yuvBuffer里

6、分辨率处理

窗口的分辨率相对不可控,可能会被用户拉伸成一个特别大的窗口,我们在初始化渲染画布时就不好确定长宽。

  1. 设置最大分辨率为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;
}
  1. 通过libyuv库的I420Scale()把上面的yuvBuffer缩放为scaledYuvBuffer

六、捕获指定区域视频

1、计算Rect

  1. 判断是否需要裁剪,若传入的是CGRectNullCGRectZero,则不需要裁剪;
  2. 取交集,可通过CGRectIntersection()比较方便获取两个Rect的交集,如果无交集则认为传入的Rect无效,不做裁剪;另外,如果计算出来的Rect的长宽小于8,编码时可能崩溃,这里加上了长宽大于等于10的判断;
  3. 转换坐标系,参数传入时的坐标系是左上角为原点,屏幕共享时设置捕获区域的接口的坐标系是左下角为原点,targetRect.origin.y = CGRectGetMaxY(screenBound) - CGRectGetMaxY(intersection);

2、屏幕共享

直接设置AVCaptureScreenInputcropRect属性即可,self.screenInput.cropRect = targetRect;

3、窗口共享

获取到窗口截图后做裁剪即可,CGImageRef croppedImage = CGImageCreateWithImageInRect(showImage, cropRect);

七、设置捕获帧率

1、屏幕共享

设置AVCaptureScreenInputminFrameDuration属性即可。

AVCaptureScreenInput *screenInput = (AVCaptureScreenInput *)input;
screenInput.minFrameDuration = CMTimeMake(1, videoFrameRate);

2、窗口共享

设置窗口截图定时器的时间间隔即可。

八、其它

1、窗口最小化

窗口最小化后,下面接口都返回了空,注意要做防崩溃处理

CGWindowListCopyWindowInfo()
CGWindowListCreateImage()

2、屏幕的不同显示模式

  1. Mirror Displays,如果选中了镜像模式,则NSScreen.screens只返回一个显示器的信息;
  2. Scaled,如果选中了缩放,则输出的分辨率会变化,注意要做处理;
  3. Rotation,同上;
  4. 不同分辨率的显示器,同上。