Windows远程桌面实现之十 - 移植xdisp_virt之iOS平台屏幕截取,声音采集,摄像头采集(二)

10 篇文章 4 订阅
1 篇文章 0 订阅

                                             by fanxiushu 2019-12-13 转载或引用请注明原始作者。

接上文,
虽然这篇文章阐述的还是以xdisp_virt项目的移植为基础,但是这里主要描述的是iOS平台下的
屏幕图像数据截取,摄像头图像采集,app内部声音和麦克风声音采集,基本上是通用的。
所以如果对xdisp_virt没兴趣,可以无需关注xdisp_virt部分,只专心与iOS相关的采集函数部分。

描述的内容看似比较多,其实与windows平台中这些内容采集的复杂度比较起来,基本就属于太轻松了。
本文基本以 iOS 13.3,iPhone11Pro设备,也就是写这篇文章的时候最新的iOS系统为准,
语言以Obj-C为主, Xcode版本为 11 。

首先来看看摄像头图像数据采集和麦克风音频采集。
这里统一使用 AVCaptureSession框架,其实在iOS系统中也有好些其他框架来实现音频和摄像头采集,比如AvAudioRecorder,AVAudioQueue等,这里采用AVCaptureSession,
因为看中了它的统一,既能录制摄像头,也能录制麦克风。录制两种数据,只需改变少量参数就可以了。
正如上文所述,xdisp_virt提供的是的是每一类采集对象的接口C函数,为了符合这种模式,
给每一类创建 一个类对象,比如摄像头可以做如下声明:

@interface camVideoCapture: NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
{
@public DP_FRAME  callback;  // 回调函数,xdisp_virt提供的回调函数,当捕获到数据时候,调用此回调函数。此概念就跟
                                                      AVCaptureVideoDataOutputSampleBufferDelegate 提供的代理回调函数一个意思。因此我们直接在
                                                      AVCaptureVideoDataOutputSampleBufferDelegate代理回调函数中,做些格式转换处理,
                                                       然后调用 callback 就算是给xdisp_virt提供了摄像头数据源了。
@public void* param;                 // callback对应的扩展参数,
@private int width,height;           // 摄像头实际宽和高。
}
@property(strong, nonatomic) AVCaptureSession* session; /// 捕获Session

-(int)Create:(NSInteger)index // 创建摄像头 index是序号,比如 0 是后置,1是前置摄像头,
@end;

AVCaptureVideoDataOutputSampleBufferDelegate 就是代理类, 当摄像头捕获到图像数据时候,
代理类中的对应的回调函数会被调用。

实现大致如下:
@implementation camVideoCapture

-(int)Create:(NSInteger)index
{
    if(index <0 || index > 1)return -1;
    AVCaptureDevicePosition pos;
    if(index ==0 )pos =AVCaptureDevicePositionBack;// 后置摄像头
    else pos = AVCaptureDevicePositionFront;  //前置摄像头
   
    AVCaptureDevice *avCaptureDevice=nil;
    NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
               //这里查找视频,如果是查找音频,使用 AVMediaTypeAudio
    for (AVCaptureDevice *device in cameras) {
        if (device.position == pos) {
            avCaptureDevice = device;
        }
    }
    ///创建 AVCaptureSesion,并且设置摄像头宽和高,这里假设为1920X1080
     self.session = [[AVCaptureSession alloc] init];
     self.session.sessionPreset = AVCaptureSessionPreset1920x1080;
   ///根据上面查找到的摄像头设备, 创建摄像头输入设备,并且把这个输入设备添加到 AVCaptureSession中
    NSError *error = nil;
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:avCaptureDevice error:&error];
    [self.session addInput:videoInput];

    接下来创建输出设备,并且把这个输出设备添加到 Session中, 并且设置输出设备的代理类为我们的类,
    这样下面的  captureOutput 回调函数会被调用,
    AVCaptureVideoDataOutput *avCaptureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    NSDictionary*settings =
            @{(__bridge id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)}; // NV12
    avCaptureVideoDataOutput.videoSettings = settings; //设置 摄像头输出图像格式为NV12,
     dispatch_queue_t queue = dispatch_queue_create("xdisp_virt_camera_io", NULL);
    [avCaptureVideoDataOutput setSampleBufferDelegate:self queue:queue]; ///  设置代理类为我们的类
    [self.session addOutput:avCaptureVideoDataOutput];

    return 0;
}

/// 代理回调函数,当采集到摄像头图像数据之后,这个函数被调用
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
       ///这里我们需要获取到具体的图像数据,因为先前设置的是NV12格式的图像数据,
       /// CVPixelBufferRef 这里存储的就是具体的图像数据,我们再进一步调用对应函数,获取NV12每个平面的数据
       CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
     
        int status = CVPixelBufferLockBaseAddress(pixelBuffer,0);  /// 锁定
        if(status !=0) return;

        char data[4]; int stride[4];
        for(int i=0;i<2;++i){
             data[i] = (char*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i);
             stride[i] = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer,i);
        }
        ----- data存储的就是每个平面的图像数据, stride是对应的步距。
       ------ 然后就是做些转换,转换到xdisp_virt对应的数据格式上
      dp_frame_t frame; ///xdisp_virt对应的数据结构。
       ........
     
      self->callback(&frame);//调用xdisp_virt对应回调函数,
     
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); ///解锁
}

@end

整个摄像头的采集就这么搞定了,比起windows平台使用DirectShow实现摄像头采集的复杂度,简直就不在一个层面。
 麦克风的采集,基本跟上面的代码差不多,只把 AVMediaTypeVideo改成 Audio,也就是Video改成Audio就差不多了,
不过 iOS中麦克风设置不支持,也就是 audioSeting属性不支持,好像iOS中麦克风采集的格式永远是  1个声道,44100,16位的PCM。
另外上面的回调函数中,不再是 CMSampleBufferGetImageBuffer 获取 CVPixelBufferRef,
而是
  CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
  size_t pcmLength = 0;
  char *pcmData = NULL;
  //获取blockbuffer中的pcm数据的指针和长度
  OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &pcmLength, &pcmData);

这样来获取PCM数据和PCM数据长度。获得这些数据之后,我们再提供给 xdisp_virt就可以了。

上文说过,xdisp_virt提供的全是C函数接口,这里需要把obj-c转成 C标准函数,其实也不难,类似如下:
extern "C" void* cameraVideoCreate(struct cm_create_t* ct)
{
    camVideoCapture* cm = [[camVideoCapture alloc]init];
    cm->callback = ct->frame;
    cm->param=ct->param;
   
    if([cm Create:ct->idevice] <0){
        return nil;
    }
   
    if(ct->is_run){//  如果是要求立即运行,则启动
        [cm.session startRunning];
    }
    printf("ptr=%p\n", cm);
   
    ///增加引用,防止函数退出后对象被自动回收,在 cameraVideoDestroy接口函数中调用 CFRelease来解除引用。
    CFRetain((__bridge void*)cm); ///
   
    return (__bridge void*)cm; ///转成 C函数的  void* 指针。
}
以上全部代码都写到 后缀是 mm的文件中,这样保证 C,C++, OBJ-C都能编译。

下面再来看看如何采集 iOS系统的屏幕数据和iOS系统APP程序的声音。
其实这个更简单,因为全部工作其实是iOS系统帮我们做好了。
使用 ReplayKit 框架。
说起这个框架,在iOS经历了很大的变化时期,iOS9之前是没有ReplayKit框架的,
为了要在iOS9之前的系统采集屏幕数据,基本是想出各类奇招,比如破解AirPlay的通讯协议,iOS系统越狱等。
反正就是非常麻烦。
在iOS9虽然提供了ReplayKit,但只支持APP内录屏,不能在整个系统范围内录屏,直到iOS11,才支持系统范围内录屏。
但是iOS11虽然支持,但做起来稍微麻烦,直到到了iOS12,iOS13,终于可以以最简单的方式来录屏了。
简单到何种程度呢?
在我们的Xcode工程中,File ->New -> Target...
弹出的对话框中,选择 BroadCast Uplaod Extension 就能在我们的工程中添加一个扩展程序,
然后自动生成两个文件 SampleHandler.h 和 SampleHandler.m
然后安装到手机之后,自动会在手机的屏幕录制里边出现我们刚添加的BroadCast Uplaod Extension名字。
SampleHandler.m出现如下四个回调函数,一看就明白:

- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
    ///
}
- (void)broadcastPaused {
    // User has requested to pause the broadcast. Samples will stop being delivered.
}
- (void)broadcastResumed {
    // User has requested to resume the broadcast. Samples delivery will resume.
}
- (void)broadcastFinished {
    // User has requested to finish the broadcast.
}
-(void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
   
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            // Handle video sample buffer
            break;
        case RPSampleBufferTypeAudioApp:
            // Handle audio sample buffer for app audio
            break;
        case RPSampleBufferTypeAudioMic:
            // Handle audio sample buffer for mic audio
            break;      
        default:
            break;
    }
}

processSampleBuffer 回调函数就是对应采集到的数据的回调函数,而且一分就是三类,
一类 RPSampleBufferTypeVideo 是系统屏幕图像数据,
一类 RPSampleBufferTypeAudioApp 是系统内的App发出的声音数据
一类   RPSampleBufferTypeAudioMic   是麦克风采集的数声音数据。

主要工作就是在 processSampleBuffer 回调函数中处理这些数据,
比如直接在这里把录屏数据编码,然后做直播推流,或者传递到服务端等。

不过 BroadCast Uplaod Extension 是一个完全独立的程序,与我们的App程序就是两个程序,而且运行的权限不同,
我们的App是宿主程序,运行在沙盒中。
BroadCast Uplaod Extension程序采集的是系统范围内的屏幕和声音,需要更高的权限,
同时iOS对这个程序的内存占用限制也很大,一般不超过50M,否则就会被kill掉。

在我们的xdisp_virt项目中,所有数据都是在xdisp_virt中进行处理的,
所以这里必须把 BroadCast Uplaod Extension采集的数据传输到我们的宿主xdisp_virt中,这里牵涉到的就是两个进程通讯的问题。
iOS提供了一个叫 App Group来通讯,但是这里不采用这种办法,而是采用万能的socket来通讯。
简单的说,在BroadCast Uplaod Extension 创建一个侦听到127.0.0.1的某个端口比如13579端口的服务端socket,
接收来自xdisp_virt的链接,xdisp_virt创建客户端socket,并且链接到 127.0.0.1:13579, 这样,BroadCast Uplaod Extension 在
procesSampleBuffer回调函数中,把采集到的数据通过socket发送到xdisp_virt宿主程序上。

至此,我们采集到了我们需要的屏幕数据,系统内部声音,麦克风声音,摄像头数据。
可惜iOS不支持鼠标键盘模拟,否则就可以远程控制手机了。

但是还有一些必须做的额外工作要做。
在摄像头采集中,我们的App必须处于前台,一旦切换到后台之后或者屏幕锁定之后,摄像头采集就自动停止,
也就是在采集摄像头数据的时候,必须让我们的App处于前台,不让系统自动锁屏,
要解决这个问题,直接调用
[UIApplication sharedApplication].idleTimerDisabled=YES; //

还有一个问题,就是屏幕采集的时候,是传递到我们的宿主程序再来处理的。
而默认情况下,App切换到后台之后,会自动休眠,iOS系统还会根据情况杀掉某些休眠的App。
为了录屏的时候,让我们的App切换到后台也保持存活状态,就必须采取某些措施。
一个比较常用的办法就是让App在后台播放一个无声的wav声音,因为iOS认为播放声音的App即使切换到后台也应该保持存活的。
在工程的info.list中添加 Required background modes ,然后在工程设置的Signing& Capabilities添加Background Mode 为
Audio,Airplay, and Picture In Picture,
做好这样的配置之后,就是在我们的代码中,循环播放一个声音文件。大致代码如下
-(void) playBackSound
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ///
        AVAudioSession* session = [AVAudioSession sharedInstance];
        NSError* err=nil;     
        [session setCategory:AVAudioSessionCategoryPlayback
          withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDuckOthers   
          error:&err];    

        NSString* path = [[NSBundle mainBundle] pathForResource:@"wusheng" ofType:@"wav"];
        NSData * data = [NSData dataWithContentsOfFile:path]; 
        self.audioPlayer =[[ AVAudioPlayer alloc]initWithData:data error:nil];
        if(self.audioPlayer){
            ///
            self.audioPlayer.delegate = self;        
            self.audioPlayer.numberOfLoops = -1; // loop for ever          
           [self.audioPlayer prepareToPlay];
           [self.audioPlayer play];
        //
    });
}
同时还注意出现 播放声音的时候,Interrupt的情况,比如打电话会中断声音的播放,这个时候在中断结束的时候,需要重新再播放。

因为iOS发布还得买账号,还得去AppStore过审,甚是麻烦,因此目前暂时不打算发布iOS版本的xdisp_virt程序了。
不过以后的macOS和CentOS版本的xdisp_virt程序会发布到 GITHUB上,有兴趣可关注。

下图是手机屏幕采集的xdisp_virt,


来个更热闹的。
这个图是在本地电脑上浏览器上显示iOS屏幕内容,然后在远端电脑的chrome浏览器中显示iOS,同时使用VLC显示RTMP推流的屏幕。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值