本文系微信公众号和知乎专栏《MediaStack》原创文章,欢迎大家关注,随时进行交流。

    之前几篇文章介绍了Android平台解析HEIF图像格式的流程
然后小伙伴不淡定了:只是解析难度不大吧,也把生成HEIF文件的流程走一下呀! 这个还真有,今天我们看看具体实现,完成这个小伙伴这个很好的建议。
    Java层可以通过HeifWriter进行HEIF文件写文件的操作调用,具体用法参考如下链接:
https://developer.android.com/jetpack/androidx/releases/heifwriter
https://developer.android.com/reference/androidx/heifwriter/HeifWriter
如果要调用相关内容,需要导入如下库:
dependencies {implementation"androidx.heifwriter:heifwriter:1.1.0-alpha01"}
HeifWriter介绍
    此类将一个或多个静止图像(相同尺寸)写入 heif 文件。它目前支持三种输入模式:INPUT_MODE_BUFFER、INPUT_MODE_SURFACE 或 INPUT_MODE_BITMAP。使用该类编写 heif 文件的一般顺序(伪代码)如下: 
1)构造 writer:HeifWriter heifwriter = new HeifWriter(...); 
2)如果使用surface输入方式,获取输入surface:Surface surface = heifwriter.getInputSurface(); 
3)调用start:heifwriter.start();
4)根据选择的输入模式,使用以下方法之一添加一个或多个图像:heifwriter.addYuvBuffer(...); 或 heifwriter.addBitmap(...); 或者渲染到之前得到的surface
5)调用stop: heifwriter.stop(...); 
6)关闭writer:heifwriter.close(); 具体用法请参考各个方法的文档。

我们依次看一下相关流程:
(1)构造Writer
    HeifEncoder使用 HEVC 编码器将图像编码为与 HEIF 兼容的样本。它目前支持三种输入模式:INPUT_MODE_BUFFER、INPUT_MODE_SURFACE 或 INPUT_MODE_BITMAP。输出格式和样本在 HeifEncoder.Callback.onOutputFormatChanged(HeifEncoder, MediaFormat)
和 HeifEncoder.Callback.onDrainOutputBuffer(HeifEncoder, ByteBuffer) 中发回。如果客户端请求使用网格,每个图块将单独发回。HeifEncoder 与 HeifWriter 分开,因为一些更高级的用例可能希望直接在 HeifEncoder 之上构建解决方案。(例如,将静止图像和视频轨道混合到一个容器中)。
配置 heif 编码器
    相关参数:width - 图像的宽度;height - 图像的高度;useGrid - 是否将图像编码为图块。如果启用,将自动选择平铺大小;quality – 一个介于 0 和 100(含)之间的数字,100 表示此实现支持的最佳质量(通常会导致文件更大);inputMode – 此编码会话的输入类型;handler - 如果不为 null,客户端将接收处理程序循环器上的所有回调。否则,客户端将在我们创建的 Looper 上收到回调;cb – 从 heif 编码器接收各种消息的回调。
(2)输入模式,
    如果选择surface模式,需要进行Surface的获取,主要的输入模式有三种方式,每一种模式对应不同的选项:
(3)Start启动
    HeifWriter启动,在该函数中主要是设置标志位,同时调用HeifEncoder的启动函数。
    当然和其他的编解码器一样的限制:只能被调用一次,否则出现异常。
(4)添加图片
    三种模式对应的调用接口如下:
    addYuvBuffer
    int: 中定义的YUV格式ImageFormat,目前只支持YUV_420_888。
    byte: 包含 YUV 数据的字节数组。如果格式有多个平面,则必须将它们连接起来。
    通过acquireEmptyBuffer接口获取一个空输入缓冲区并将数据复制到其中。在发送输入 EOS 之前,这将阻塞直到数据被复制。输入 EOS 发送后,立即返回。
    如果有可用的输入和编解码器缓冲区,则例程复制一个图块。必须在处理 MediaCodec 回调的处理程序循环器上调用。
    熟悉Media Codec框架的童鞋看到这里应该非常熟悉了。就是通过mediaCodec框架完成原始数据输入,编码后再通知app相关数据完成,主动或被动获取。
getInputSurface
    检索输入Surface以便进行编码,前提是如果配置了surface。
addBitmap
    Bitmap:要添加到文件中的位图。
    调用HeifEncoder addBitmap接口完成输入bitmap和encoderEGLSurface绑定

(5)stop停止
    同步停止 heif Writer。如果Writer没有成功完成写入,则抛出异常。成功返回后: 
- 对于buffer和bitmap输入模式,停止前将写入所有发送的图像。
- 对于Surface输入模式,需要分情况讨论:setInputEndOfStreamTimestamp被调用,则写入时间戳等于或早于 这个指定的时间戳的图像会写入,否则如果
setInputEndOfStreamTimestamp(long)
从未调用过,stop 将阻塞直到接收到足够数量的图像。
    timeoutMs long:等待编写器完成的最长时间(以微秒为单位),零表示无限期等待。
(6)close关闭
    停止和释放 writer 的例程必须在接收 heif 编码器回调的同一个 looper 上调用。
(7)其他内容
    还有其他一些callback函数进行妥善处理,不然解码后outputbuffer的处理

MPEG4Write
    通过MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC 格式在framework中走的流程是MPEG4Write相关流程,该部分很多大佬在讲解extractor时候有介绍,我们这边简单说明一下:
    首先我们介绍一下Android平台输出所支持类型:主要包括MPEG4, WEBM, 3gpp,HEIF,OGG等。说实话Android对于这个类型支持力度还是略显不足,需要各个厂商或者app开发者进行各种形式兼容和优化。
    MP4文件生成时会判断一下输出格式是否是MPEG4,3GPP,或者HEIF文件格式,否则不做处理。
    可以看到在writeFtypBox时候完成heic文件格式写入:

    之后依次调用writeFilelevelMetaBox和writeMoovBox等接口完成相关box的写入操作,而各类box的写入完全按照格式标准进行,基本是读取时候的反操作:
我们看主要接口:writeMoovBox的调用过程,如下
writeFilelevelMetaBox接口主要完成各个meta信息汇总写入操作:
writeMoovBox主要完成moov西悉尼吸入,以及各个track的信息写入:
writeTrackHeader一次完成各个box的数据写入:

实例代码log
    正好手头上有一台平板,用的是rk的平台,所以就跑了一个demo相关流程的log部分截图如下,Android12的代码会走codec2的接口。



我是一枚爱跑步的程序猿,很多内容属于自学内容,文中难免会出现一些错误或者不准确的地方,恳请大家批评指正。
继续阅读
阅读原文