当前位置: 首页 > news >正文

MediaCodec 解码后数据对齐导致的绿边问题

前言

Android 使用 MediaCodec 解码 h264 数据后会有个数据对齐的问题。

简单说就是 MediaCodec 使用 GPU 进行解码,而解码后的输出数据是有一个对齐规则的,不同设备表现不一,如宽高都是 16 位对齐,或 32 位、64 位、128 位,当然也可能出现类似宽度以 128 位对齐而高度是 32 位对齐的情况。

例子

简单起见先画个 16 位对齐的:

假设需要解码的图像宽高为 1515,在使用 16 位对齐的设备进行硬解码后,输出的 YUV 数据将会是 1616 的,而多出来的宽高将自动填充。

这时候如果按照 1515 的大小取出 YUV 数据进行渲染,表现为花屏,而按照 1616 的方式渲染,则出现绿边(如上图)。

怎么去除绿边呢?很简单,把原始图像抠出来就行了(废话)。

以上面为例子,分别取出 YUV 数据的话,可以这么做:

int width = 15, height = 15;
int alignWidth = 16, alignHeight = 16;

//假设 outData 是解码后对齐数据
byte[] outData = new byte[alignWidth * alignHeight * 3 / 2];

byte[] yData = new byte[width * height];
byte[] uData = new byte[width * height / 4];
byte[] vData = new byte[width * height / 4];

yuvCopy(outData, 0, alignWidth, alignHeight, yData, width, height);
yuvCopy(outData, alignWidth * alignHeight, alignWidth / 2, alignHeight / 2, uData, width / 2, height / 2);
yuvCopy(outData, alignWidth * alignHeight * 5 / 4, alignWidth / 2, alignHeight / 2, vData, width / 2, height / 2);

...

private static void yuvCopy(byte[] src, int offset, int inWidth, int inHeight, byte[] dest, int outWidth, int outHeight) {
    for (int h = 0; h < inHeight; h++) {
        if (h < outHeight) {
            System.arraycopy(src, offset + h * inWidth, dest, h * outWidth, outWidth);
        }
    }
}

其实就是逐行抠出有效数据啦~

问题

那现在的问题就剩怎么知道解码后输出数据的宽高了。

起初我用华为荣耀note8做测试机,解码 1520x1520 后直接按照 1520x1520 的方式渲染是没问题的,包括解码后给的 buffer 大小也是 3465600(也就是 152015203/2)。

而当我使用OPPO R11,解码后的 buffer 大小则为 3538944(153615363/2),这时候再按照 1520x1 520 的方式渲染的话,图像是这样的:

使用 yuvplayer 查看数据最终确定 1536x1536 方式渲染是没问题的,那么 1536 这个值在代码中怎么得到的呢?

我们可以拿到解码后的 buffer 大小,同时也知道宽高的对齐无非就是 16、32、64、128 这几个值,那很简单了,根据原来的宽高做对齐一个个找,如下(不着急,后面还有坑,这里先给出第一版解决方案):

align:
for (int w = 16; w <= 128; w = w << 1) {
    for (int h = 16; h <= w; h = h << 1) {
        alignWidth = ((width - 1) / w + 1) * w;
        alignHeight = ((height - 1) / h + 1) * h;
        int size = alignWidth * alignHeight * 3 / 2;
        if (size == bufferSize) {
            break align;
        }
    }
}

代码比较简单,大概就是从 16 位对齐开始一个个尝试,最终得到跟 bufferSize 相匹配的宽高。

当我屁颠屁颠的把 apk 发给老大之后,现实又无情地甩了我一巴掌,还好我在自己新买的手机上面调试了一下啊哈哈哈哈哈~

你以为华为的机子表现都是一样的吗?错了,我的华为mate9就不是酱紫的,它解出来的 buffer 大小是 3538944(153615363/2),而当我按照上面的方法得到 1536 这个值之后,渲染出来的图像跟上面的花屏差不多,谁能想到他按照 1520x1520 的方式渲染才是正常的。

这里得到结论:通过解码后 buffer 的 size 来确定对齐宽高的方法是不可靠的。

解决方案

就在我快绝望的时候,我在官方文档上发现这个(网上资料太少了,事实证明官方文档的资料才最可靠):

Accessing Raw Video ByteBuffers on Older Devices

Prior to LOLLIPOP and Image support, you need to use the KEY_STRIDE and KEY_SLICE_HEIGHT output format values to understand the layout of the raw output buffers.

Note that on some devices the slice-height is advertised as 0. This could mean either that the slice-height is the same as the frame height, or that the slice-height is the frame height aligned to some value (usually a power of 2). Unfortunately, there is no standard and simple way to tell the actual slice height in this case. Furthermore, the vertical stride of the U plane in planar formats is also not specified or defined, though usually it is half of the slice height.

大致就是使用 KEY_STRIDE 和 KEY_SLICE_HEIGHT 可以得到原始输出 buffer 的对齐后的宽高,但在某些设备上可能会获得 0,这种情况下要么它跟图像的值相等,要么就是对齐后的某值。

OK,那么当 KEY_STRIDE 和 KEY_SLICE_HEIGHT 能拿到数据的时候我们使用他们,拿不到的时候再用第一个解决方案:

//视频宽高,如果存在裁剪范围的话,宽等于右边减左边坐标,高等于底部减顶部
width = format.getInteger(MediaFormat.KEY_WIDTH);
if (format.containsKey("crop-left") && format.containsKey("crop-right")) {
    width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left");
}
height = format.getInteger(MediaFormat.KEY_HEIGHT);
if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {
    height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
}

//解码后数据对齐的宽高,在有些设备上会返回0
int keyStride = format.getInteger(MediaFormat.KEY_STRIDE);
int keyStrideHeight = format.getInteger(MediaFormat.KEY_SLICE_HEIGHT);
// 当对齐后高度返回0的时候,分两种情况,如果对齐后宽度有给值,
// 则只需要计算高度从16字节对齐到128字节对齐这几种情况下哪个值跟对齐后宽度相乘再乘3/2等于对齐后大小,
// 如果计算不出则默认等于视频宽高。
// 当对齐后宽度也返回0,这时候也要对宽度做对齐处理,原理同上
alignWidth = keyStride;
alignHeight = keyStrideHeight;
if (alignHeight == 0) {
    if (alignWidth == 0) {
        align:
        for (int w = 16; w <= 128; w = w << 1) {
            for (int h = 16; h <= w; h = h << 1) {
                alignWidth = ((videoWidth - 1) / w + 1) * w;
                alignHeight = ((videoHeight - 1) / h + 1) * h;
                int size = alignWidth * alignHeight * 3 / 2;
                if (size == bufferSize) {
                    break align;
                }
            }
        }
    } else {
        for (int h = 16; h <= 128; h = h << 1) {
            alignHeight = ((videoHeight - 1) / h + 1) * h;
            int size = alignWidth * alignHeight * 3 / 2;
            if (size == bufferSize) {
                break;
            }
        }
    }
    int size = alignWidth * alignHeight * 3 / 2;
    if (size != bufferSize) {
        alignWidth = videoWidth;
        alignHeight = videoHeight;
    }
}

int size = videoWidth * videoHeight * 3 / 2;
if (size == bufferSize) {
    alignWidth = videoWidth;
    alignHeight = videoHeight;
} 

最后

文中只提供了个人处理的思路,实际使用的时候,还要考虑颜色格式以及效率的问题,个人不建议在java代码层面做这类转换。

作者:超兽

原文:https://www.jianshu.com/p/ac53e9595940


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

移动端技术交流喊你入群啦~~~

推荐几个堪称教科书级别的 Android 音视频入门项目

OpenGL ES 学习资源分享

OpenGL 实现视频编辑中的转场效果

喜欢就点个「在看」吧 ▽

相关文章:

  • JNI编程如何巧妙获取JNIEnv
  • 最新 Android 面试点梳理,我收藏了你呢?
  • 三年Android开发,跳槽腾讯音乐,历经三面终获Offer,定级T2-1(超全面试题+学习经验总结)...
  • 「Android音视频编码那点破事」序章
  • Android短文:理解插值器和估值器
  • 用户调研:音视频方面的书籍,哪些内容才是你需要的?
  • 职场PUA到底有多可怕?
  • Fragment 的过去、现在和将来
  • 数字图像处理领域中常见的几种色彩模式
  • Android VSYNC (Choreographer)与UI刷新原理分析
  • Android 实现 图片 转 字符画 效果
  • Android 实现 视频 转 字符画效果
  • 在 iOS 上用 Shader 实现 图片 转 字符画 效果~~
  • 【每周一记-003】~~~
  • Android Canvas 绘制小黄人
  • 【编码】-360实习笔试编程题(二)-2016.03.29
  • ECMAScript6(0):ES6简明参考手册
  • GDB 调试 Mysql 实战(三)优先队列排序算法中的行记录长度统计是怎么来的(上)...
  • github从入门到放弃(1)
  • JAVA多线程机制解析-volatilesynchronized
  • LeetCode刷题——29. Divide Two Integers(Part 1靠自己)
  • Mysql优化
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • node 版本过低
  • SpiderData 2019年2月23日 DApp数据排行榜
  • vue2.0项目引入element-ui
  • vue总结
  • 大数据与云计算学习:数据分析(二)
  • 关于使用markdown的方法(引自CSDN教程)
  • 记录一下第一次使用npm
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 如何使用 OAuth 2.0 将 LinkedIn 集成入 iOS 应用
  • 微服务核心架构梳理
  • 我是如何设计 Upload 上传组件的
  • 由插件封装引出的一丢丢思考
  • 走向全栈之MongoDB的使用
  • NLPIR智能语义技术让大数据挖掘更简单
  • Python 之网络式编程
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • ###C语言程序设计-----C语言学习(6)#
  • #pragma once与条件编译
  • #QT(TCP网络编程-服务端)
  • (42)STM32——LCD显示屏实验笔记
  • (70min)字节暑假实习二面(已挂)
  • (C语言)球球大作战
  • (delphi11最新学习资料) Object Pascal 学习笔记---第5章第5节(delphi中的指针)
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (zz)子曾经曰过:先有司,赦小过,举贤才
  • (二开)Flink 修改源码拓展 SQL 语法
  • (附源码)spring boot儿童教育管理系统 毕业设计 281442
  • (三)elasticsearch 源码之启动流程分析
  • (算法)前K大的和
  • (五)网络优化与超参数选择--九五小庞
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • *1 计算机基础和操作系统基础及几大协议