ShareREC for iOS录屏原理解析

Stella981
• 阅读 710

文 / 游族网络Mob云平台iOS开发专家 李永超

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

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

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

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

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

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

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的代码如下:

//绑定纹理到FBO上

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _renderTexture, 0);

GLint rbo;

glGetIntegerv(GL_RENDERBUFFER_BINDING, &rbo);

GLint fbo;

glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);

//创建输出屏幕FBO

glGenFramebuffers(1, &_outputFbo);

glBindFramebuffer(GL_FRAMEBUFFER, _outputFbo);

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);

//创建截图FBO

glGenFramebuffers(1, &_captureFbo);

glBindFramebuffer(GL_FRAMEBUFFER, _captureFbo);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(_captureTexture), 0);

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

上面主要阐述创建自己的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方法将勾取到的内容画回屏幕:

id  blitCommandEncoder = blitCommandEncoderAction(commandBuffer, _blitCommandEncoderSEL);

id tmptexture = textFromDrawableAction(drawable, _textureFromDrawableSEL);

copyTextureAction(blitCommandEncoder,

_copyTextureSEL,

tmptexture,

0,

0,

OriginMake_SRE(0, 0, 0),

SizeMake_SRE(textureSizeAction(captureTexture, _widthFromTextureSEL), textureSizeAction(captureTexture, _heightFromTextureSEL), textureSizeAction(captureTexture, _depthFromTextureSEL)),

captureTexture,

0,

0,

OriginMake_SRE(0, 0, 0));

endEncodingAction(blitCommandEncoder, _endEncodingSEL);

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

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

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

本文分享自微信公众号 - LiveVideoStack(livevideostack)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
JS 苹果手机日期显示NaN问题
问题描述newDate("2019122910:30:00")在IOS下显示为NaN原因分析带的日期IOS下存在兼容问题解决方法字符串替换letdateStr"2019122910:30:00";datedateStr.repl
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这