YYEVA支持动态元素的透明MP4动效播放器 - 客户端渲染篇之iOS (六)

501 阅读7分钟

YYEVA(YY Effect Video Animate)是一个开源的支持可插入动态元素的MP4动效播放器解决方案,包含设计资源输出的AE扩展,客户端渲染引擎,在线预览工具。

对比传统的序列帧的动画播放方式,具有更高的压缩率,硬解码效率更高的优点,同时支持插入动态的业务元素;对比SVGA、Lottie等播放器,支持更多的特效支持,如复杂3D效果、描边、粒子效果等,达到所见即所得的效果。

我们发布项目已经有数月,也有很多朋友加入了我们的社群提出一些优化方案,我们陆续在支持。也欢迎大家到 github给我们点个⭐⭐⭐,您的Star是对我们最大的支持。

从这篇开始,我们陆续会发布三篇文章,分别给大家介绍下,YYEVA-SDK是如何解析YYEVA资源,并结合动态元素渲染到屏幕上的。现在,就让我们开始YYEVA渲染之旅吧。

out.gif

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数据。

image.png

如上图所示,内部是扩充了源素材的分辨率的一半,用新扩展的像素来实现使用YUV分量的来存储alpha的数值

image.png

在视频上屏的时候,根据每一帧的左右像素值,其中

  • 左侧像素值作为最终像素值的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段。客户端在解析的时候,分别提取音视频轨信息和描述信息。完成渲染工作

image.png

YYEVA渲染框架主要分为:播放器类、 文件解析类、动态元素处理类、视频解码类、渲染组件类 这三个部分组成。

  • 播放器类:负责暴露给外部使用的播放器接口及接受外界设置相关动态元素

  • 文件解析类: 负责解协议YYEVA的MP4资源文件,将视频轨交给视频解码器处理,将Metadata数据交给动态元素解析器处理。YYEVADemuxMedia , YYEVAEffectInfo

  • 动态元素处理类:负责将动态元素数据模态化为内部的数据结构,同时针对文字、图片类的动态提前生成需要渲染的纹理数据

  • 视频解码类:将视频轨数据通过硬解码器,解码后缓存到解码缓冲队列中

  • 渲染组件类:读取解码缓冲队列的缓存帧,使用自定义的Shader,完成YYEVA帧渲染

通过上面几个组件类,每一个YYEVA资源都会最终分离成2个数据

image.png

  • 视频帧缓冲队列

  • 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

  1. descript: 描述该资源的整体信息
  2. effect : 描述 该资源下的所有遮罩相关信息
  3. datas : 描述 每一帧遮罩的位置信息

通过这个描述信息,在渲染每一个动态元素的时候,可以知道该动态元素在每一frameIndex的遮罩位置outputFrame,以及在画布上的位置renderFrame

该Json信息,在客户端渲染SDK中,会模态化成一个本地对象 YYEVAEffectInfo

image.png

YYEVA渲染流程

YYEVA的整体渲染流程,可以通过下图来表示

image.png

我们可以看到,在业务上传入的每一个动态元素信息,在初始化的时候,都会缓存一张纹理对象在内存里。

即在创建YYEVAEffectSource的时候,就会将业务传入的数据生成图片,缓存,因为这样生成一次,之后在渲染的时候就可以直接使用,防止渲染过程中,重复生成。

针对

  • 图片类型:直接是使用UIImage组件加载到内存中即可

  • 文字类 :把文字使用UIGraphics转换成图片然后保存在内存中

image.png

渲染组件

image.png

每一个渲染周期,都会先渲染我们的视频背景层,再渲染我们的动态元素层

视频背景层的渲染

image.png

  • 背景层渲染
//背景层的顶点着色器
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

image.png

//动态元素层的顶点着色器
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);

}

性能分析

image.png

结语

本篇文章主要介绍了移动端iOS是如何解析YYEVA资源,并将YYEVA资源分离成Json描述信息和视频轨道,最后结合业务动态元素,完成上屏工作。我们会在之后的文章,出一些落地case,给大家呈现一些好玩的应用。大家敬请期待

希望可以为我们的项目 YYEVA 点上一个⭐⭐⭐,您的支持就是我们一直更新文章下去的动力哈。希望大家多多鼓励