YYEVA(YY Effect Video Animate)是一个开源的支持可插入动态元素的MP4动效播放器解决方案,包含设计资源输出的AE扩展,客户端渲染引擎,在线预览工具。
对比传统的序列帧的动画播放方式,具有更高的压缩率,硬解码效率更高的优点,同时支持插入动态的业务元素;对比SVGA、Lottie等播放器,支持更多的特效支持,如复杂3D效果、描边、粒子效果等,达到所见即所得的效果。
我们发布项目已经有数月,也有很多朋友加入了我们的社群提出一些优化方案,我们陆续在支持。也欢迎大家到 github给我们点个⭐⭐⭐,您的Star是对我们最大的支持。
从这篇开始,我们陆续会发布三篇文章,分别给大家介绍下,YYEVA-SDK是如何解析YYEVA资源,并结合动态元素渲染到屏幕上的。现在,就让我们开始YYEVA渲染之旅吧。
YYEVA-iOS 接入
使用Cocoapods
安装依赖
- 添加依赖 'YYEVA' 到 Podfile 文件中:
target 'MyApp' do
pod 'YYEVA'
end
- pod upte
创建一个YYEVAPlayer
实例
- 先将YYEVA插件导出的资源YYEVA-MP4,放置在资源bundle中
- 使用下列代码使用YYEVPlayer
YYEVAPlayer *player = [[YYEVAPlayer alloc] init];
[self.view addSubview:player];
//config dynmaic elements
[player setImageUrl:localPath forKey:@"image_key1"];
[player setImageUrl:localPath forKey:@"image_key2"];
[player setImageUrl:localPath forKey:@"image_key3"];
[player setText:str.text forKey:@"text_key1"];
[player play:file];
YYEVA-iOS 实现之旅
RGB+Alpha混合原理
先来回顾下之前YY实现的一套普通透明MP4的方案。
大家熟知的MP4视频,以h264方式编码视频,MPEG-4的颜色采样标准是YUV,YUV是亮度和色度的分量叠加,不支持alpha通道,因此,如何让MP4视频支持透明度,业界常用的方式是使用两个通道分别进行 存储视频的RGB数据和Alpha数据。
如上图所示,内部是扩充了源素材的分辨率的一半,用新扩展的像素来实现使用YUV分量的来存储alpha的数值
在视频上屏的时候,根据每一帧的左右像素值,其中
- 左侧像素值作为最终像素值的RGB值
- 右侧像素值作为最终像素值的Alpha值
vec3 rgb = texture2D(texture,vec2(vUx.x/2,vUv.y))).rgb
vec alpha = texture2D(texture,vec2(0.5 + vUx.x/2,vUv.y))).r
gl_FragColor=vec4(rgb,alpha)
YYEVA客户端渲染
YYEVA实现动态元素的方案,是在透明MP4的基础上,结合Alpha区域的混合,将需要插入的动态元素信息,提前解析并保存在一个Json数据中,同时经过编码、压缩处理,写入MP4的Metadata段。客户端在解析的时候,分别提取音视频轨信息和描述信息。完成渲染工作
YYEVA渲染框架主要分为:播放器类、 文件解析类、动态元素处理类、视频解码类、渲染组件类 这三个部分组成。
-
播放器类:负责暴露给外部使用的播放器接口及接受外界设置相关动态元素
-
文件解析类: 负责解协议YYEVA的MP4资源文件,将视频轨交给视频解码器处理,将
Metadata
数据交给动态元素解析器处理。YYEVADemuxMedia
,YYEVAEffectInfo
-
动态元素处理类:负责将动态元素数据模态化为内部的数据结构,同时针对文字、图片类的动态提前生成需要渲染的纹理数据
-
视频解码类:将视频轨数据通过硬解码器,解码后缓存到解码缓冲队列中
-
渲染组件类:读取解码缓冲队列的缓存帧,使用自定义的Shader,完成YYEVA帧渲染
通过上面几个组件类,每一个YYEVA资源都会最终分离成2个数据
-
视频帧缓冲队列
-
Json描述信息
描述信息
通过上面几个图,我们发现,有一个比较关键的信息,就是Json描述信息,这个描述信息我们在使用插件解析设计师图层的时候,已经通过编码压缩,写入到了Metadata类。客户端拿到这个Json类,可以知道每一个动态元素,每一帧的渲染大小、位置以及形状。
{
"descript": { //视频的描述信息
"width": 1808, //输出视频的宽
"height": 1008, //输出视频的高
"isEffect": 1, //是否为动态元素视频
"version": 1, //插件的版本号
"rgbFrame": [0, 0, 900, 1000], //rgb位置信息
"alphaFrame": [900, 0, 450, 500] //alpha位置信息
},
"effect": [
{ //动态元素的遮罩描述信息 : 文字类型
"effectWidth": 700, //动态元素宽
"effectHeight": 1049, //动态元素高
"effectId": 1, //动态元素索引id
"effectTag": "nickName", //动态元素的tag,业务使用的时候,表示的key
"effectType": "txt", //动态元素类型 有 txt和img 2种
"fontColor":"#ffffff", //当为txt类型的时候才存在 如果设计侧未指定,由渲染端自行指定默认值
"fontSize":13, //当为txt类型的时候才存在 如果设计侧未指定,由渲染端自行指定默认值
},{ //动态元素的遮罩描述信息 : 图片类型
"effectWidth": 300, //同上
"effectHeight": 400,//同上
"effectId": 2, //同上
"effectTag": "user_avatar", //同上
"effectType": "img", //同上
"scaleMode":"aspectFill", //当为img类型的时候才存在 如果设计侧未指定,由渲染端自行指定默认值
}...],
"datas": [{ //每一帧的动态元素位置信息
"frameIndex": 0, //帧索引
"data": [{
"renderFrame": [x1, y1, w1, h1], //在画布上的位置
"effectId": 1, //标志是哪个动态元素
"outputFrame": [x1`, y1`, w1`, h1`] //在视频区域的位置
},{
"renderFrame": [x2, y2, w2, h2], //在画布上的位置
"effectId": 2, //标志是哪个动态元素
"outputFrame": [x2`, y2`, w2`, h2`] //在视频区域的位置
} ... ]
}
Json数据 包含三层:descript/effect/datas
- descript: 描述该资源的整体信息
- effect : 描述 该资源下的所有遮罩相关信息
- datas : 描述 每一帧遮罩的位置信息
通过这个描述信息,在渲染每一个动态元素的时候,可以知道该动态元素在每一frameIndex
的遮罩位置outputFrame
,以及在画布上的位置renderFrame
该Json信息,在客户端渲染SDK中,会模态化成一个本地对象 YYEVAEffectInfo
YYEVA渲染流程
YYEVA的整体渲染流程,可以通过下图来表示
我们可以看到,在业务上传入的每一个动态元素信息,在初始化的时候,都会缓存一张纹理对象在内存里。
即在创建YYEVAEffectSource的时候,就会将业务传入的数据生成图片,缓存,因为这样生成一次,之后在渲染的时候就可以直接使用,防止渲染过程中,重复生成。
针对
-
图片类型:直接是使用UIImage组件加载到内存中即可
-
文字类 :把文字使用UIGraphics转换成图片然后保存在内存中
渲染组件
每一个渲染周期,都会先渲染我们的视频背景层,再渲染我们的动态元素层
视频背景层的渲染
- 背景层渲染
//背景层的顶点着色器
vertex VertexMaskSharderOutput maskVertexShader(uint vertexID [[ vertex_id ]],
constant YSVideoMetalMaskVertex *vertexArray [[ buffer(YSVideoMetalVertexInputIndexVertices) ]])
{
VertexMaskSharderOutput out;
out.postion = float4(vertexArray[vertexID].positon);
out.rgbTextureCoordinate = vertexArray[vertexID].rgbTexturCoordinate;
out.alphaTextureCoordinate = vertexArray[vertexID].alphaTexturCoordinate;
return out;
}
//背景层的片元着色器
fragment float4 maskFragmentSharder(VertexMaskSharderOutput input [[stage_in]],
texture2d<**float**> textureY [[ texture(YSVideoMetalFragmentTextureIndexTextureY) ]],
texture2d<**float**> textureUV [[ texture(YSVideoMetalFragmentTextureIndexTextureUV) ]],
constant YSVideoMetalConvertMatrix *convertMatrix [[ buffer(YSVideoMetalFragmentBufferIndexMatrix) ]])
{
constexpr sampler textureSampler (mag_filter::linear,
min_filter::linear);
float3 sourceYUV = float3(textureY.sample(textureSampler, input.rgbTextureCoordinate).r,
textureUV.sample(textureSampler, input.rgbTextureCoordinate).rg);
float3 alphaYUV = float3(textureY.sample(textureSampler, input.alphaTextureCoordinate).r,
textureUV.sample(textureSampler, input.alphaTextureCoordinate).rg);
float3 sourceRGB = convertMatrix->matrix * (sourceYUV + convertMatrix->offset);
float3 alphaRGB = convertMatrix->matrix * (alphaYUV + convertMatrix->offset);
return float4(sourceRGB, alphaRGB.r);
}
- 动态元素层的Shader
//动态元素层的顶点着色器
vertex VertexElementSharderOutput elementVertexShader(uint vertexID [[ vertex_id ]],
**constant** YSVideoMetalElementVertex *vertexArray [[ buffer(YSVideoMetalVertexInputIndexVertices) ]])
{
VertexElementSharderOutput out;
out.postion = vertexArray[vertexID].positon;
out.sourceTextureCoordinate = vertexArray[vertexID].sourceTextureCoordinate;
out.maskTextureCoordinate = vertexArray[vertexID].maskTextureCoordinate;
return out;
}
//动态元素层的片元着色器
fragment float4 elementFragmentSharder(VertexElementSharderOutput input [[ stage_in ]],
texture2d<**float**> lumaTexture [[ texture(0) ]],
texture2d<**float**> chromaTexture [[ texture(1) ]],
texture2d<**float**> sourceTexture [[ texture(2) ]],
constant YSVideoMetalConvertMatrix *convertMatrix [[ buffer(0) ]],
constant YSVideoElementFragmentParameter *fillParams [[ buffer(1) ]]) {
constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);
matrix_float3x3 rotationMatrix = convertMatrix->matrix;
float3 offset = convertMatrix->offset;
float3 mask = RGBColorFromYuvTextures(textureSampler, input.maskTextureCoordinate, lumaTexture, chromaTexture, rotationMatrix, offset);
float4 source = sourceTexture.sample(textureSampler, input.sourceTextureCoordinate);
float alpha = source.a * mask.r;
return float4(source.rgb, alpha);
}
性能分析
结语
本篇文章主要介绍了移动端iOS是如何解析YYEVA资源,并将YYEVA资源分离成Json描述信息和视频轨道,最后结合业务动态元素,完成上屏工作。我们会在之后的文章,出一些落地case,给大家呈现一些好玩的应用。大家敬请期待