2017-01-13 16:41:57 wf990051004 阅读数 9381

最近在pad上写了个录屏的项目,下面总结一下最近的收获:

查看了下git上的不少的录屏的demo,好多没有实现暂停和继续的功能,当然我这个写的也有不少瑕疵,希望能提出来共同进步!

一、需求:

1、要求录屏的时候有暂停和继续功能

2、对录制完的视频有做处理(获取第一帧图片,获取视频大小等等)


二、实现:

①、demo中的Lib文件夹的Recoder就是录制视频的相关文件


②、BlazeiceAudioRecordAndTransCoding.h 是音频录制的类; 

WFCapture.h 是视频录制的类;

WFCaptureUtilities.h是录制完毕之后的视频和音频的融合的类

三、使用: 在ViewConreoller中使用,具体代码写的很清楚了,有不懂的可以留言


我的demo地址为:

           Demo


有喜欢的可以在git上面给个star,git地址就是Demo地址,谢了!!!





添加个借鉴的demo,https://github.com/Blazeice/ScreenAndAudioRecordDemoScreenAndAudioRecordDemo(在此基础上添加了暂停恢复功能)










2018-01-11 14:01:14 Mob_com 阅读数 1059

众所周知,由于iOS系统的封闭性,也出于保护用户隐私的角度,苹果并没有公开的API供开发者调用,来录制屏幕内容。导致许多游戏或者应用没有办法直接通过调用系统API的方式提供录制功能,用户也无法将自己一些玩游戏的过程录制下来分享到其他玩家。基于此,ShareREC应运而生。下面我们从说一下ShareREC的录屏的实现原理。

ShareREC for iOS录屏原理解析

由于苹果UI是基于不同的引擎渲染,所以目前针对不同的引擎,主要是采用以下几种不同的方式实现:

(1) 原生UI。主要是指UIKit框架下面的UI,即苹果原生UI。其实现方式主要是通过获取当前显示的layer,然后通过Core Graphics将这个layer绘制成UIImage,然后将UIImage拼接成视频。这种做法有个问题,就是每一帧都需要使用Core Graphics来重绘,会造成CPU占用率暴涨,效率非常低。

(2) OpenGL 。由于 Unity 3D 或 Cocos2d两种引擎,在iOS设备上都是采用OpenGL ES这个底层库实现渲染,所以后面会将两者放在OpenGL中一起讨论。

(3) Metal。Metal是苹果推出的专门针对iPhone和iPad中GPU编程高度优化的框架。目前Unity 5已经支持64位iOS Metal技术,导出Xcode项目时,可以进行选择。Metal这个名称的来源是想说明这个图形框架的的确确是非常底层的- -底层到已经非常接近金属板了(metal)。

PS:

(1)如果是基于越狱系统,开发者还可以通过调用系统的私有API方式,其中比较重要一个方法是UIGetScreenImage来实现录制功能,这种方式的优点是录制效率高且是无损画质,但同时也有一个致命的弱点,就是应用没办法上架。

(2)ReplayKit。ReplayKit是苹果在iOS9上苹果公开的一个API,通过这个API,可以录制除AVPlayer播放视频以外的应用界面。但是由于对于系统版本要求比较高,同时由于没办法获取到录制的视频的路径,所以可定制化比较低。但iOS11的ReplayKit,已经可以拿到每一帧的回调(这个没有做详细验证,只是看到新的方法里面已经含有samplebuffer的回调,有兴趣的同学可以试验一下),这样就可以实现更高的定制化功能。所以不考虑低版本兼容性的话,也可以通过调用这个系统库实现。

目前ShareREC支持OpenGL和Metal两种渲染引擎的录制,上面提到过Unity3d与Cocos2d底层其实也是通过OpenGL来渲染的,所以在其上面开发的游戏,ShareREC均是完美支持的。ShareREC是通过HOOK(钩子)的方式,捕捉屏幕画面,进行录制的;其中心原理是首先捕获到当前绘制的内容,此时拿到绘制的纹理后,可以自行进行处理;然后重新将内容绘制到屏幕上【这一步很重要,否则由于已经渲染的内容被钩取,屏幕上将不会显示任何内容】。下面我们将分别介绍ShareREC捕获两种引擎OpenGL和Metal的实现原理。

OpenGL:

首先iOS系统默认支持OpenGL ES 1.0、ES2.0以及ES3.0 (OpenGL ES是OpenGL在移动端的简化版本)三个版本,三者之间并不是简单的版本升级,设计理念甚至完全不同。所以我们后续的一些操作还会对于版本的不同分别做处理。废话不多说,首先我们是要先通过钩子,获取到当前绘制的上下文对象Context(Context是一个非常抽象的概念,我们姑且把它理解成一个包含了所有OpenGL状态的对象,如果我们把一个Context销毁了,那么OpenGL也不复存在)。画了一个图,仅供理解。

ShareREC for iOS录屏原理解析

然后根据当前的context,创建捕获屏幕纹理CVOOpenGLESTextureRef,随后创建中间渲染纹理;最后绑定纹理到FBO上面,此时,原本绘制到屏幕上的内容,将转为绘制到我们创建的中间渲染纹理上面。此时,当OpenGL再次渲染屏幕内容时,将会首先被我们创建的屏幕纹理捕获,从而拿到渲染内容;最后再重新将渲染画面输出到屏幕。其实现流程如图所示:

ShareREC for iOS录屏原理解析

其中绑定纹理到FBO的代码如下:

ShareREC for iOS录屏原理解析

上面主要阐述创建自己的renderTexture后,然后通过绑定纹理到FBO上面,执行这样的操作以后,原本输出到屏幕上的内容,将转为绘制到renderTexture中,然后再创建输出屏幕FBO,以及截图的FBO;最后再通过_captureFbo画入捕捉纹理,通过_outFbo输出到屏幕。

Metal:

iOS8.0起,Apple为了更充分地发挥GPU的潜力,引入了Metal框架。Metal和OpenGL ES是并列的,他们都是应用对GPU访问的底层接口。而Metal则提供了更底层,更面向硬件的接口,这也是为何Apple给这个框架起名为“Metal”的原因。OpenGL ES3.1之前,GPU只能做图形渲染流水线,而不能直接做通用计算流水线。现在iOS的Metal把这道门打开了。通过Metal,我们可以直接使用通用计算流水线,也就是GPU的Compute Shader。因此,在目前的Metal框架中可以使用三种着色器——Vertex Shader、Fragment Shader以及Compute Shader。当然,正因为Metal要求GPU得具有通用计算能力,因此一些老旧的GPU就不能支持了。目前支持Metal的GPU必须是Apple A7开始的,也就是至少为Power VR 6系列。首先我们先了解下Metal引擎的渲染流程,它的渲染流水线如下图所示:

ShareREC for iOS录屏原理解析

目前很多API都通过具体的“类”来实现平台支持,不过Metal使用的方法是基于“协议”的。因为Metal中具体的类型是由运行的设备所决定的。这很好的鼓励了程序员选择面向接口编程而非面向实现,以降低程序的耦合。当然也意味着需要冒着风险大量的在Objective-C 运行时来对Metal的类型添加继承和扩展类型。

其整个流程如下图所示。

ShareREC for iOS录屏原理解析

但协议的这种方式,又无形中增加了我们钩子的复杂程度。因为我们没有办法直接拿到相关的类;同时又考虑到兼容低版本Xcode环境的问题,我们也无法直接导入Metal框架,只能通过动态加载Metal.framework的方式。只能通过动态(NSClassFromString和NSSelectorFromString)获取相关类和方法的方式来钩取。同时基于“协议”的类,就只能通过dlsym/dlopen【最近苹果对热更新的审核比较严格,这种动态方法尽量还是少用】的方式获取。再来说一下具体实现。其根本也是通过钩子进行的。其中一个最重要的一个钩子是presentDrawable:,这个主要是用于展示最终的渲染内容到屏幕上面的函数,其中有一个最重要的参数MTLDrawableRef,这个参数就是一个可绘制对象,也包含了最终要展示到屏幕的纹理。当我们取得最后展示到屏幕的drawable后,最后调用copyTextureAction方法将勾取到的内容画回屏幕:

ShareREC for iOS录屏原理解析

至此,整个Metal的录制过程就结束了。最后,将获取到的CVPixelBufferRef按照指定格式写入文件。

最后,关于音频与视频多线程同步的问题,是使用两个信号量dispatch_semaphore_t分别进行控制,以防引起线程崩溃。

上面就是ShareREC iOS分别对于OpenGL ES和Metal两种引擎的渲染的录制过程。其核心的方式就是通过HOOK的方式钩取最后要渲染的内容,然后再将原来的内容重新渲染到屏幕上。

文/Mob开发者平台 iOS开发专家 李永超

2019-01-14 10:27:46 qq_29858855 阅读数 55

iOS-Swift使用ReplayKit实现录屏功能

前段时间有碰到Android用户使用系统录屏发来的一些bug重现录像,iOS端不会用系统录屏的用户都是拍视频联系产品然后反馈给我们开发,这中间有的时候挺耗费时间和精力的,空下来就琢磨了下iOS的录屏使用。录屏用得比较多的是苹果自带的ReplayKit框架,不需要导包,使用比较便捷,而且耗费内存较小,基本忽略不计,这也是我选择ReplayKit的原因,如对功能研究较感兴趣的同学可以忽略本篇。
我使用的手机是锤子M1的是比较老的版本,我对照着锤子做了一个简单的demo,效果图如下:

效果图

系统版本9.0以下ReplayKit是不可使用的,所以在使用前一定要判断系统版本是否符合要求,在Targets->Build Settings->iOS Deployment Target中设置的版本号高于9.0也可使用,也可加if #avaliable(iOS 9.0,*)判断减少此类风险,这边是个大坑,我写的demo中Timer等需要10.0以上版本,所以请勿抬杠,个人因情况而定。
Targets->Build Settings->iOS Deployment Target
工具条对应的文件
“展示录屏控件”触发方法,在给ReplayToolsView中的关闭按钮和录屏开始按钮加事件的时候我是使用的闭包,这样可以控制效果图中红色按钮的状态;对红色按钮的状态判断是为了避免多次点击按钮后ReplayToolsView的叠加;replayVideoView.addGestureRecognizer给replayVideoView添加移动手势,王者荣耀中在录屏时按钮是固定的,以下demo中的录屏可以在整个app中使用,包括记录页面跳转过程等。

@IBAction func showActionView(_ sender: UIButton) {
        if !self.showViewBtn.isSelected {
            let replayVideoView = ReplayToolsView(frame: CGRect(x: 20, y: self.view.bounds.height-100, width: self.view.bounds.width-40, height: 50))
            //使用block代替在view中编写关闭方法,添加对“app录屏”按钮的选中状态设置
            replayVideoView.closeBlock = { () in
                //此处设置结束录制后将replayVideoView移除,也可以隐藏isHidden,在此处和startRecordingScreen方法中将view改成隐藏,replayVideoView需要改为全局变量使用
                replayVideoView.removeFromSuperview()
                self.showViewBtn.isSelected = !self.showViewBtn.isSelected
            }
            replayVideoView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(dragFunction)))
            replayVideoView.actionBlock = {
                self.startRecordingScreen(replayVideoView)
                replayVideoView.closeButton.isHidden = replayVideoView.actionButton.isSelected
            }
            let window = UIApplication.shared.keyWindow
            window!.addSubview(replayVideoView)
            self.showViewBtn.isSelected = !self.showViewBtn.isSelected
        }
    }
    // Janise: 设置拖动手势(可用于拖动控件后设置控件依赖位置,此处只设置可在y轴方向上移动,如需要在x方向上移动,可对x位置修改)
    @objc func dragFunction(_ pan: UIPanGestureRecognizer) {
        let point = pan.translation(in: view)
        if let v = pan.view {
            //设置移至顶部和底部时不可移动,64为个人主观设置值,可根据个人需要更改
            if (v.frame.origin.y > 64 && point.y < 0)
                || (v.frame.origin.y + v.frame.height < (UIApplication.shared.keyWindow?.frame.height)! && point.y > 0) {
                v.center.y = v.center.y + point.y
            }
            pan.setTranslation(.zero, in: view)
        }
    }
    // Janise:  开始录制屏幕(考虑问题,可否像android那样在录屏时不将ReplayVideoToolView视图作为屏幕的一部分录制,我暂时还没找到方法)
    
    /// 开始录屏(遗留问题:初次录屏时会询问用户是否同意使用录屏功能,但是时间在用户点击录屏按钮时已开始计时,与用户同意使用录屏功能时间点有时间差,这部分需要解决)
    ///
    /// - Parameter toolView: 自制黑色半透明录屏视图(用于图标的展示修改和时间的变化)
    @objc func startRecordingScreen(_ toolView: ReplayToolsView) {
        // 检测设备是否支持录屏功能
        if RPScreenRecorder.shared().isAvailable && systemVersionAvaliable()  {
            //录屏按钮是否选中
            if toolView.actionButton.isSelected {
                //停止计时
                self.time?.invalidate()
                self.time = nil
                toolView.seconds = 0
                //选中状态下停止录制
                RPScreenRecorder.shared().stopRecording { (previewCon, error) in
                    if let errors = error {
                        print(errors)
                    }
                    if let controller = previewCon {
                        controller.previewControllerDelegate = self
                        UIApplication.shared.keyWindow?.rootViewController?.present(controller, animated: true, completion: nil)
                    }
                }
                
                toolView.actionButton.isSelected = false
                toolView.removeFromSuperview()
                self.showViewBtn.isSelected = !self.showViewBtn.isSelected
                return
            }else {
                //开始计时
                self.time = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (timer) in
                    toolView.seconds += 1
                })
                //未选中状态下开始录屏
                RPScreenRecorder.shared().startRecording { (err) in
                    if let error = err {
                        print(error)
                    }
                }
                toolView.actionButton.isSelected = true
            }
        }else {
            //            //正式编写时需将两种提示分开展示,权限与版本两个不可合二为一
            //            let alert = UIAlertController(title: "提示", message: "请先授予app录屏权限,系统版本低于9.0不支持录屏功能,请升级版本后使用该功能", preferredStyle: .alert)
            //            self.present(alert, animated: true, completion: nil)
        }
    }
    /// 视频播放
    ///
    /// - Parameter previewController: 视频预览播放器
    func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
        //关闭视频预览
        previewController.dismiss(animated: true, completion: nil)
    }
    /// 判断版本是否在9.0版本及以上版本
    ///
    /// - Returns: 布尔值
    func systemVersionAvaliable() -> Bool {
        if #available(iOS 9.0, *) {
            return true
        }
        return false
    }
replayVideoView的编写较为简单,此处不做展示。

我没有添加麦克风录音功能,startRecording(withMicrophoneEnabled:true…在10.0版本已被弃用,我在找其他的方法来替代。

RPScreenRecorder.shared().startRecording(withMicrophoneEnabled:true) { (<#Error?#>)in
                    <#code#>
}

ReplayKit不可以与AVPlayer同用,会出现兼容问题,录屏功能只能在真机上使用,虚拟机不支持,我是用的Swift编写,网上较多的使用OC,IOS 一个很好的录制屏幕实现IOS功能 - 飞翔的熊blabla - CSDN博客csdn上这篇写得比较好。

以上代码我传到了GitHub上,地址是demo

2019-11-05 19:43:31 WangErice 阅读数 29

 

iOS在录屏功能是走过了长长的一段路,在其他平台尤其是直播平台日益火爆的推动下,iOS12.0终于开放在应用中唤起系统录屏权限给开发者.以下内容以iOS 12.0+为基础,不再讨论之前的版本.

理论准备

  • 了解ReplayKit框架以及了解一下iOS9.x,iOS10.x,iOS11.x以及iOS12.x上录屏权限以及api的不同;
  • 了解App Group内存共享实现;
  • 了解CFNotificationCenterGetDarwinNotifyCenter进行进程通信实现;

功能拆分

  • App Groups数据共享

由于沙盒机制,在不同的应用进程之间想要共享数据,使用原来的本地持久化数据方法就不行了,这时候需要使用到App Group,该应用组允许你在不同的应用进程之间共享数据空间,通过存取操作,从而实现数据的通信.使用起来也很简单,

在Xcode-->Target-->Capability中添加App Groups功能,添加需要的应用组名(一般习惯上使用group.+bundleid)作为应用组的标识.在统一个开发账号下的应用开启了具有相同id的App Groups时,应用之间可以通过这个空间共享数据(所以不仅仅局限于同一个应用的不同target,统一账号下的多个应用也可以共享这个数据,比如同一家公司的多个应用利用这个技术就可以实现只要登陆了其中一个另外一个就可以自动登录的功能).

通过App Groups共享数据一般有两种方式:

  • 使用NSUserDefaults
//初始化一个供App Groups使用的NSUserDefaults对象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.Ericydong.TempDemo"];
//写入数据
[userDefaults setValue:@"value" forKey:@"key"];
//读取数据
NSLog(@"%@", [userDefaults valueForKey:@"key"]);
  • 使用NSFileManager
//获取到对应的空间路径就可以在该路径下读写文件
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.Ericydong.TempDemo"];

在这里,使用NSFileManager来创建一个单独的文件夹来存放录制的视频:

@interface NSDate (Timestamp)
+ (NSString *)timestamp;
@end

@implementation NSDate (Timestamp)
+ (NSString *)timestamp {
    long long timeinterval = (long long)([NSDate timeIntervalSinceReferenceDate] * 1000);
    return [NSString stringWithFormat:@"%lld", timeinterval];
}
@end
@implementation ShareDataManager
/*

    获取文件存储的主路径
*/
+ (NSString *)documentPath {
    static NSString *replaysPath;
    if (!replaysPath) {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSURL *documentRootPath = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.Ericydong.TempDemo"];
        replaysPath = [documentRootPath.path stringByAppendingPathComponent:@"Replays"];
        if (![fileManager fileExistsAtPath:replaysPath]) {
            NSError *error_createPath = nil;
            BOOL success_createPath = [fileManager createDirectoryAtPath:replaysPath withIntermediateDirectories:true attributes:@{} error:&error_createPath];
            if (success_createPath && !error_createPath) {
                NSLog(@"%@路径创建成功!", replaysPath);
            } else {
                NSLog(@"%@路径创建失败:%@", replaysPath, error_createPath);
            }
        }
    }
    return replaysPath;
}

/*
    获取当前将要录制视频文件的保存路径
*/
+ (NSURL *)filePathURL {
    NSString *timestamp = [NSDate timestamp];
    NSString *fileName = [timestamp stringByAppendingPathExtension:@"mp4"];
    NSString *fullPath = [[self documentPath] stringByAppendingPathComponent:fileName];
    return [NSURL fileURLWithPath:fullPath];
}

/*
    用于获取自定义路径下的所有文件
 */
+ (NSArray <NSURL *> *)fetechAllResource {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    NSString *documentPath = [self documentPath];
    NSURL *documentURL = [NSURL fileURLWithPath:documentPath];
    NSError *error = nil;
    NSArray<NSURL *> *allResource  =  [fileManager contentsOfDirectoryAtURL:documentURL includingPropertiesForKeys:@[] options:(NSDirectoryEnumerationSkipsSubdirectoryDescendants) error:&error];
    return allResource;
    
}

@end

 

  • RPSystemBroadcastPickerView

在iOS 12.0+上出现了一个新的UI控件RPSystemBroadcastPickerView,用于展示用户启动系统录屏的指定视图.该控件使用起来很简单,文档使用示例是这么说的:

class ViewController: UIViewController {
    @IBOutlet var containerView: UIView!
    override func viewDidLoad() {
        super.viewDidLoad()
        let broadcastPicker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
        broadcastPicker.preferredExtension = "com.your-app.broadcast.extension"
        containerView.addSubview(broadcastPicker)
    }
    
}

作为一个听话的孩子乖乖按照文档说明进行集成,创建项目,在界面上放置一个开始录屏的按钮,并创建对应的Broadcast Upload Extension(在Xcode-->File-->New-->Target中):

- (IBAction)startRecorScreen:(id)sender {
    if (@available(iOS 12.0, *)) {
        RPSystemBroadcastPickerView *pickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:(CGRect){0, 0, 70, 70}];
        //如果不设置改参数,则会展示所有具有Broadcast Upload Extension的应用列表
        pickerView.preferredExtension = @"Ericydong.TempDemo.upload";
        pickerView.showsMicrophoneButton = true;//是否显示录制按钮
        [self.containerView addSubview:pickerView];
    }
}

点击"开始录屏"按钮你就会发现视图上有一个黑点,怎么样,是不是很漂亮?-----?

然后你点击一下又会弹出来一个用户确定开始的界面:

然后再选择 "开始直播",才能开始录屏,是不是有一万句mmp飘过,那前边的小黑钮是干嘛的?到底有完没完!!!!所以,我们这么处理:

  • 找到RPSystemBroadcastPickerView中的按钮对象;
@interface RPSystemBroadcastPickerView (FindButton)
- (UIButton *)findButton;
@end
@implementation RPSystemBroadcastPickerView (FindButton)
- (UIButton *)findButton {
    return [self findButton:self];
}
- (UIButton *)findButton:(UIView *)view {
    if (!view.subviews.count) {
        return nil;
    }
    if ([view isKindOfClass:[UIButton class]]) {
        return (UIButton *)view;
    }
    UIButton *btn;
    for (UIView *subView in view.subviews) {
        UIView *destinationView = [self findButton:subView];
        if(destinationView) {
            btn = (UIButton *)destinationView;
            break;
        }
    }
    return btn;
}
@end



  • 直接触发按钮点击事件:

只时候你再点击"开始录屏"就可以直接弹出需要用户确定直播录屏界面了,开不开森^_^^_^

Attention:在测试过程中,发现在iOS12.2之前的一些设备上上如果指定了RPSystemPickerView的preferredExtension参数有时候会出现不显示对应extension的情况:

所以安全期间,可以对这些系统做隔离:

- (IBAction)startRecorScreen:(id)sender {
    if (@available(iOS 12.0, *)) {
        RPSystemBroadcastPickerView *pickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:(CGRect){0, 0, 70, 70}];

        //如果不设置改参数,则会展示所有具有Broadcast Upload Extension的应用列表
        if(@available(iOS 12.2, *)) {
            pickerView.preferredExtension = @"Ericydong.TempDemo.upload";
        }
        pickerView.showsMicrophoneButton = true;//是否显示录制按钮
        [self.containerView addSubview:pickerView];
    }
}

 

  • 开始录屏

当用户确定开始录屏之后,录屏过程会开启另一个应用进程,这时候就来到了Broadcast Upload Extension这个Target中.

  • 在启动方法中进行初始化操作
@interface SampleHandler ()
@property (strong, nonatomic) AVAssetWriter *assetWriter;
@property (strong, nonatomic) AVAssetWriterInput *videoInput;
@property (strong, nonatomic) AVAssetWriterInput *audioInput;

@end
@implementation SampleHandler

#pragma 生命周期方法

/*
    用户点击"开始直播"之后就会进入到这个方法[SampleHandler生命周期方法]
*/
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    //开始录制时初始化相关对象
    [self _initlization];
}
    




#pragma 自定义方法

- (void)_initlization {
    if ([self.assetWriter canAddInput:self.videoInput]) {
        [self.assetWriter addInput:self.videoInput];
    } else {
        NSAssert(false, @"添加失败");
    }
}
- (AVAssetWriter *)assetWriter {
    if (!_assetWriter) {
        NSError *error = nil;
        _assetWriter = [[AVAssetWriter alloc] initWithURL:[ShareDataManager filePathURL] fileType:(AVFileTypeMPEG4) error:&error];
        NSAssert(!error, @"_assetWriter初始化失败");
    }
    return _assetWriter;
}

- (AVAssetWriterInput *)audioInput {
    if (!_audioInput) {
        NSDictionary *audioCompressionSettings = @{ AVEncoderBitRatePerChannelKey : @(28000),
                                                    AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                                                    AVNumberOfChannelsKey : @(1),
                                                    AVSampleRateKey : @(22050) };
        
        _audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings];
        
    }
    return _audioInput;
}

- (AVAssetWriterInput *)videoInput {
    if (!_videoInput) {
        CGSize size = [UIScreen mainScreen].bounds.size;
        //写入视频大小
        NSInteger numPixels = size.width  * size.height /* [UIScreen mainScreen].scale * size.height * [UIScreen mainScreen].scale*/;
        //每像素比特
        CGFloat bitsPerPixel = 7.5;
        NSInteger bitsPerSecond = numPixels * bitsPerPixel;
        // 码率和帧率设置
        NSDictionary *compressionProperties = @{
            AVVideoAverageBitRateKey : @(bitsPerSecond),//码率(平均每秒的比特率)
            AVVideoExpectedSourceFrameRateKey : @(25),//帧率(如果使用了AVVideoProfileLevelKey则该值应该被设置,否则可能会丢弃帧以满足比特流的要求)
            AVVideoMaxKeyFrameIntervalKey : @(15),//关键帧最大间隔
            AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel,
            AVVideoPixelAspectRatioKey: @{
                    AVVideoPixelAspectRatioHorizontalSpacingKey: @(1),
                    AVVideoPixelAspectRatioVerticalSpacingKey: @(1)
            },
        };
        CGFloat scale = [UIScreen mainScreen].scale;
        
        NSDictionary *videoOutputSettings = @{
            AVVideoCodecKey : AVVideoCodecTypeH264,
            AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill,
            AVVideoWidthKey : @(size.width * scale),
            AVVideoHeightKey : @(size.height * scale),
            AVVideoCompressionPropertiesKey : compressionProperties
        };
        _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoOutputSettings];
        //        self.videoInput.transform = CGAffineTransformMakeRotation(M_PI / 2.0);
        _videoInput.expectsMediaDataInRealTime = true;//实时录制
        
    }
    return _videoInput;
}
@end
  • 拼接视频流


#pragma 生命周期方法
/*
    在录制的过程中会不断回调输出视频流
*/
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            @autoreleasepool {
                AVAssetWriterStatus status = self.assetWriter.status;
                if ( status == AVAssetWriterStatusFailed || status == AVAssetWriterStatusCompleted || status == AVAssetWriterStatusCancelled) {
                    return;
                }
                if (status == AVAssetWriterStatusUnknown) {
                    [self.assetWriter startWriting];
                    CMTime time = CMSampleBufferGetDuration(sampleBuffer);
                    [self.assetWriter startSessionAtSourceTime:time];
                }
                
                
                if (status == AVAssetWriterStatusWriting) {
                    if (self.videoInput.isReadyForMoreMediaData) {
                       BOOL success = [self.videoInput appendSampleBuffer:sampleBuffer];
                        if (!success) {
                            [self stopRecording];
                        }
                    }
                }
            }
            // Handle video sample buffer
            break;
        case RPSampleBufferTypeAudioApp:
            // Handle audio sample buffer for app audio
            break;
        case RPSampleBufferTypeAudioMic:
            if (self.audioInput.isReadyForMoreMediaData) {
                BOOL success = [self.audioInput appendSampleBuffer:sampleBuffer];
                if (!success) {
                    [self stopRecording];
                }
            }
            // Handle audio sample buffer for mic audio
            break;
            
        default:
            break;
    }
}


  • 录制完成或者中断
#pragma 生命周期方法
/*
    录制过程暂停(暂时不知道何时触发)
*/
- (void)broadcastPaused {
    [self stopRecording];
    // User has requested to pause the broadcast. Samples will stop being delivered.
}
/*
    进程被挂起(例如突然来电话等)
*/
- (void)broadcastResumed {
        [self stopRecording];
    // User has requested to resume the broadcast. Samples delivery will resume.
}

/*
    当用户点击停止直播时触发

*/
- (void)broadcastFinished {
    [self stopRecording];
    // User has requested to finish the broadcast.
}

#pragma 自定义方法

- (void)stopRecording {
    if (self.assetWriter.status == AVAssetWriterStatusWriting) {
        [self.audioInput markAsFinished];
        //这里有个问题就是iOS新的api不会触发回调,视频录制有时是不能播放的,未找到具体原因(多半是系统api的问题)
        [self.assetWriter finishWriting];
    }
}
  • 读取到录制的视频

可以在最初的控制器中再添加一个浏览视频的按钮,用来查看录制的视频

- (IBAction)scan:(id)sender {
    
    NSArray<NSURL *> *allResource = [[ShareDataManager fetechAllResource] sortedArrayUsingComparator:^NSComparisonResult(NSURL *  _Nonnull obj1, NSURL * _Nonnull obj2) {
        //排序,每次都查看最新录制的视频
        return [obj1.path compare:obj2.path options:(NSCaseInsensitiveSearch)];
    }];
    AVPlayerViewController *playerViewController;
    playerViewController = [[AVPlayerViewController alloc] init];

    playerViewController.player = [AVPlayer playerWithURL:url];
//    playerViewController.delegate = self;
    [self presentViewController:playerViewController animated:YES completion:^{
        [playerViewController.player play];
        NSLog(@"error == %@", playerViewController.player.error);
    }];
    
    
}

 

同时把ShareDataManager这个类的代码对两个项目进行共享

 

就可以在控制器中查看刚才录制的视频了.

  • CFNotificationCenterGetDarwinNotifyCenter的使用

这是一个进程级的通知监听方法,可以实现在不同进程之间的消息传递.例如在系统录屏结束之后,想要发送一条通知来方便用户回到原始应用中进行相关的操作,就可以通过CFNotificationCenterGetDarwinNotifyCenter发送一个通知,然后在接收到通知之后注册一个即时生效的本地推送通知,这样只要录制过程一结束,就会收到一条本地的推送,用户发现有推送消息就会回到应用中做进一步的处理.

//定义推送通知的回调方法
//该方法中的第四个参数object在设计时为预留参数并没有实际的作用,也就是说无论你怎么传递在通知中都收不到这个参数(其实也可以理解毕竟broadcast upload extension与宿主应用并不在一个应用中,不同进程之间的对象也就不能共存)

void MyHoleNotificationCallback(CFNotificationCenterRef center,
                                   void * observer,
                                   CFStringRef name,
                                   void const * object,
                                   CFDictionaryRef userInfo) {
    NSString *identifier = (__bridge NSString *)name;
    NSObject *sender = (__bridge NSObject *)observer;
    NSDictionary *info = (__bridge NSDictionary *)userInfo;
    if ([identifier isEqualToString:broadcastFinishedNotification]) {
        if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
            ViewController *vc = (ViewController *)sender;
            [vc addLocalNotice:identifier];
        }
    } 
    
}



//添加通知
    CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
    CFStringRef str = (__bridge CFStringRef)identifier;
    CFNotificationCenterAddObserver(center,
                                    (__bridge const void *)(observer),
                                    notificationCallback,
                                    str,
                                    NULL,               CFNotificationSuspensionBehaviorDeliverImmediately);




//移除通知
            CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
            CFStringRef str = (__bridge CFStringRef)identifier;
            CFNotificationCenterRemoveObserver(center,
                                               (__bridge const void *)(observer),
                                               str,
                                               NULL);

整个过程大概就是这个样子,欢迎留言.

2017-06-16 16:02:41 NB_Token 阅读数 3078

1.首先导入系统库 

#import <ReplayKit/ReplayKit.h>


2.进行运行环境判断 ,不支持模拟器

#if TARGET_IPHONE_SIMULATOR
#define SIMULATOR 1
#elif TARGET_OS_IPHONE
#define SIMULATOR 0
#endif


3.系统版本不低于iOS 9.0

[[UIDevice currentDevice].systemVersion floatValue] < 9.0


4.开始录制的方法

//麦克风是否开启
[[RPScreenRecorder sharedRecorder] setMicrophoneEnabled:YES];
[[RPScreenRecorder sharedRecorder] startRecordingWithHandler:^(NSError *error){}


5.结束录制的方法及回调

//previewViewController:视频预览vc,系统自带,不能直接进行修改
[[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController *previewViewController, NSError *  error){}


6.为视频预览控制器实现代理方法

//遵守此协议
@interface ViewController () <RPPreviewViewControllerDelegate>

//添加代理
previewViewController.previewControllerDelegate = self;

 

7.可选的代理方法

//关闭后的回调
- (void)previewControllerDidFinish:(RPPreviewViewController *)previewController;
//进行某些操作的回调  分享或者保存到相册
- (void)previewController:(RPPreviewViewController *)previewController didFinishWithActivityTypes:(NSSet <NSString *> *)activityTypes __TVOS_PROHIBITED;


iOS11可以直接在控制中心中加入“屏幕录制(Screen Recording)”按钮,实现手机屏幕录制功能(不限于app应用内),

貌似这个没啥卵用了。

 

 

没有更多推荐了,返回首页