Android: MediaCodec视频文件硬件解码,高效率得到YUV格式帧,快速保存JPEG图片(不使用OpenGL)(附Demo)
Android: hardware decode video file through MediaCodec, get YUV format video frames directly (without OpenGL), efficiently save frames as YUV/JEPG format to file.
特点
以H.264编码分辨率1920x1080视频文件为例
- 需要Android API 21
- 直接将视频解码为YUV格式帧,不经过OpenGL,不转换为RGB
- 对绝大多数设备和绝大多数视频编码格式,都可以解码得到NV21或I420格式帧数据
- 30ms内获得NV21或I420格式帧数据
- 10ms内将NV21或I420格式帧数据写入到文件
- 对得到的NV21格式帧数据,在110ms内完成JPEG格式的转换和写入到文件
背景
因为实验需要在Android上高效率解码视频文件,并获得YUV格式帧数据,遂搜索寻找解决方法。最初找到bigflake的Android MediaCodec stuff,硬件解码视频不可多得的示例代码,其中提供了结合MediaCodec和OpenGL硬件解码视频并得到RGB格式帧数据,以及写入bitmap图片到文件的方法,测试发现效果不错,但我想要的是得到YUV格式的帧数据;在继续寻找RGB转YUV的方法时,苦于没有找到高效实现这个转换的方法,遂作罢。
后来发现MediaCodec解码得到的原始帧数据应当就是YUV格式,然后看到stackoverflow上的讨论Why doesn't the decoder of MediaCodec output a unified YUV format(like YUV420P)?,发现有人和我有一样的需要,但他已经发现了不同设备MediaCodec解码得到的YUV格式不相同这个问题,且由于各种格式繁杂,很难写出高效的格式转换方法。然后又发现了来自加州理工学院的一篇文章Android MediaCodec Formats,别人统计了市面上Android设备MediaCodec解码得到的不同YUV格式所占的比例,表格中显示出格式之繁多,且以COLOR_QCOM_FormatYUV420SemiPlanar32m,OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar64x32Tile2m8ka和COLOR_FormatYUV420SemiPlanar占据绝大多数。考虑放弃MediaCodec直接得到统一格式的YUV格式帧数据。
再后来不死心继续找,偶然找到了一份Android CTS测试Image
和ImageReader
类的代码,发现了由MediaCodec解码直接得到指定YUV格式(如NV21,I420)视频帧的方法,遂有了此文。
概述
简单来说,整个过程是,MediaCodec将编码后的视频文件解码得到YUV420类的视频帧,然后将视频帧格式转换为NV21或I420格式,由用户进行后续处理;若需要写入.yuv文件,直接将转换后的数据写入即可。若需要保存为JPEG格式图片,将NV21格式帧数据转换为JPEG格式并写入。
详细来说,CTS测试中透露出可以指定硬件解码得到帧编码格式,虽然不同设备支持的编码格式都不尽相同,但得益于API 21加入的COLOR_FormatYUV420Flexible格式,MediaCodec的所有硬件解码都支持这种格式。但这样解码后得到的YUV420的具体格式又会因设备而异,如YUV420Planar,YUV420SemiPlanar,YUV420PackedSemiPlanar等。然而又得益于API 21对MediaCodec加入的Image
类的支持,可以实现简单且高效的任意YUV420格式向如NV21,I420等格式的转换,这样就得到了一个统一的、可以预先指定的YUV格式视频帧。再进一步,YuvImage
类提供了一种高效的NV21格式转换为JPEG格式并写入文件的方法,可以实现将解码得到的视频帧保存为JPEG格式图片的功能,且整个过程相比bigflake中提供的YUV经OpenGL转换为RGB格式,然后通过Bitmap
类保存为图片,效率高很多。
MediaCodec指定帧格式
实际上,MediaCodec不仅在编码,而且在解码是也能够指定帧格式。能够指定的原因是,解码得到的帧的格式,并不是由如H.264编码的视频文件提前确定的,而是由解码器确定的,解码器支持哪些帧格式,就可以解码出哪些格式的帧。
获取支持的格式
MediaCodec虽然可以指定帧格式,但也不是能指定为任意格式,是需要硬件支持的。首先看看对于特定视频编码格式的MediaCodec解码器,支持哪些帧格式。
private static int selectTrack(MediaExtractor extractor) {
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
if (VERBOSE) {
Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format);
}
return i;
}
}
return -1;
}
private void showSupportedColorFormat(MediaCodecInfo.CodecCapabilities caps) {
System.out.print("supported color format: ");
for (int c : caps.colorFormats) {
System.out.print(c + "\t");
}
System.out.println();
}
MediaExtractor extractor = null;
MediaCodec decoder = null;
File videoFile = new File(videoFilePath);
extractor = new MediaExtractor();
extractor.setDataSource(videoFile.toString());
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + videoFilePath);
}
extractor.selectTrack(trackIndex);
MediaFormat mediaFormat = extractor.getTrackFormat(trackIndex);
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
showSupportedColorFormat(decoder.getCodecInfo().getCapabilitiesForType(mime));
MediaExtractor
负责读取视频文件,获得视频文件信息,以及提供 视频编码后的帧数据(如H.264)。selectTrack()
获取视频所在的轨道号,getTrackFormat()
获得视频的编码信息。再以此编码信息通过createDecoderByType()
获得一个解码器,然后通过showSupportedColorFormat()
就可以得到这个解码器支持的帧格式了。
比如对于我的设备,对于支持video/avc
的解码器,支持的帧格式是
supported color format: 2135033992 21 47 25 27 35 40 52 2130706433 2130706434 20
这里的数字对应MediaCodecInfo.CodecCapabilities
定义的帧格式,如2135033992对应COLOR_FormatYUV420Flexible,21对应COLOR_FormatYUV420SemiPlanar,25对应COLOR_FormatYCbYCr,27对应COLOR_FormatCbYCrY,35对应COLOR_FormatL8,40对应COLOR_FormatYUV422PackedSemiPlanar,20对应COLOR_FormatYUV420PackedPlanar。
COLOR_FormatYUV420Flexible
这里简单谈谈COLOR_FormatYUV420Flexible,YUV420Flexible并不是一种确定的YUV420格式,而是包含COLOR_FormatYUV411Planar, COLOR_FormatYUV411PackedPlanar, COLOR_FormatYUV420Planar, COLOR_FormatYUV420PackedPlanar, COLOR_FormatYUV420SemiPlanar和COLOR_FormatYUV420PackedSemiPlanar。在API 21引入YUV420Flexible的同时,它所包含的这些格式都deprecated掉了。
那么为什么所有的解码器都支持YUV420Flexible呢?官方没有说明这点,但我猜测,只要解码器支持YUV420Flexible中的任意一种格式,就会被认为支持YUV420Flexible格式。也就是说,几乎所有的解码器都支持YUV420Flexible代表的格式中的一种或几种。
指定帧格式
平常初始化MediaCodec并启动解码器是用如下代码
decoder.configure(mediaFormat, null, null, 0);
decoder.start();
其中mediaFormat
是之前得到的视频编码信息,这样向解码器确定了各种参数后,就能正常解码了。
而指定帧格式是在上述代码前增加
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
仅此一行,用来指定解码后的帧格式,换句话说,解码器将编码的帧解码为这种指定的格式。前面说到YUV420Flexible是几乎所有解码器都支持的,所以可以直接写死。
这个指定方法就是我在CTS中发现的,因为官方文档对KEY_COLOR_FORMAT
的描述是set by the user for encoders, readable in the output format of decoders,也就是说只用在编码器中,而不是我们现在用的解码器中!
转换格式和写入文件
主体框架
先贴主体部分的代码
final int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
final int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
int outputFrameCount = 0;
while (!sawOutputEOS) {
if (!sawInputEOS) {
int inputBufferId = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufferId);
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
decoder.queueInputBuffer(inputBufferId, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
sawInputEOS = true;
} else {
long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(inputBufferId, 0, sampleSize, presentationTimeUs, 0);
extractor.advance();
}
}
}
int outputBufferId = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US);
if (outputBufferId >= 0) {
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
boolean doRender = (info.size != 0);
if (doRender) {
outputFrameCount++;
Image image = decoder.getOutputImage(outputBufferId);
if (outputImageFileType != -1) {
String fileName;
switch (outputImageFileType) {
case FILE_TypeI420:
fileName = OUTPUT_DIR + String.format("frame_%05d_I420_%dx%d.yuv", outputFrameCount, width, height);
dumpFile(fileName, getDataFromImage(image, COLOR_FormatI420));
break;
case FILE_TypeNV21:
fileName = OUTPUT_DIR + String.format("frame_%05d_NV21_%dx%d.yuv", outputFrameCount, width, height);
dumpFile(fileName, getDataFromImage(image, COLOR_FormatNV21));
break;
case FILE_TypeJPEG:
fileName = OUTPUT_DIR + String.format("frame_%05d.jpg", outputFrameCount);
compressToJpeg(fileName, image);
break;
}
}
image.close();
decoder.releaseOutputBuffer(outputBufferId, true);
}
}
}
上述代码是MediaCodec解码的一般框架,不作过多解释。 不同于bigflake的是MediaCodec解码的输出没有指定一个Surface
,而是利用API 21新功能,直接通过getOutputImage()
将视频帧以Image
的形式取出。
而我们现在得到的Image
就可以确定是YUV420Flexible格式,而得益于Image
类的抽象,我们又可以非常方便地将其转换为NV21或I420格式。关于具体的转换和写入文件的细节,参见我的另一篇文章Android: YUV_420_888编码Image转换为I420和NV21格式byte数组。
总结
这篇文章饼画的很大,但写的很短,因为还有一大部分内容在如上链接中的文章中讲到。对于仅仅需要将视频切分为一帧一帧并保存为图片的用户来说,使用这种方法比bigflake的方法会快10倍左右,因为没有OpenGL渲染,以及转换为Bitmap的开销。而对于需要获得视频帧YUV格式数据的用户来说,这种方法能够直接得到YUV格式数据,中间没有数学运算,不会出现不必要的精度损失,而且,也是效率最高的。
此方法的核心原理就是通过指定解码器参数,保证了解码得到的帧格式一定是YUV420Flexible;通过Image
实现了健壮且高效的YUV格式转换方法;通过YuvImage
实现了快速的JPEG格式图片生成和写入的方法。
Demo
依照上面的描述,本文附带了一个Android APP Demo,指定输入视频文件和输出文件夹名,此APP可将视频帧保存为I420、NV21或JPEG格式。如有需要,请点击zhantong/Android-VideoToImages。
Update 2017.12.13
修复了Android 6.0及以上的读写权限问题,以及选择视频文件时可能路径出错的问题。
主要代码
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.media.Image;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingQueue;
public class New {
private static final String TAG = "VideoToFrames";
private static final boolean VERBOSE = true;
private static final long DEFAULT_TIMEOUT_US = 10000;
private static final int COLOR_FormatI420 = 1;
private static final int COLOR_FormatNV21 = 2;
public static final int FILE_TypeI420 = 1;
public static final int FILE_TypeNV21 = 2;
public static final int FILE_TypeJPEG = 3;
private final int decodeColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;
private int outputImageFileType = -1;
private String OUTPUT_DIR;
public void setSaveFrames(String dir, int fileType) throws IOException {
if (fileType != FILE_TypeI420 && fileType != FILE_TypeNV21 && fileType != FILE_TypeJPEG) {
throw new IllegalArgumentException("only support FILE_TypeI420 " + "and FILE_TypeNV21 " + "and FILE_TypeJPEG");
}
outputImageFileType = fileType;
File theDir = new File(dir);
if (!theDir.exists()) {
theDir.mkdirs();
} else if (!theDir.isDirectory()) {
throw new IOException("Not a directory");
}
OUTPUT_DIR = theDir.getAbsolutePath() + "/";
}
public void videoDecode(String videoFilePath) throws IOException {
MediaExtractor extractor = null;
MediaCodec decoder = null;
try {
File videoFile = new File(videoFilePath);
extractor = new MediaExtractor();
extractor.setDataSource(videoFile.toString());
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + videoFilePath);
}
extractor.selectTrack(trackIndex);
MediaFormat mediaFormat = extractor.getTrackFormat(trackIndex);
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
showSupportedColorFormat(decoder.getCodecInfo().getCapabilitiesForType(mime));
if (isColorFormatSupported(decodeColorFormat, decoder.getCodecInfo().getCapabilitiesForType(mime))) {
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, decodeColorFormat);
Log.i(TAG, "set decode color format to type " + decodeColorFormat);
} else {
Log.i(TAG, "unable to set decode color format, color format type " + decodeColorFormat + " not supported");
}
decodeFramesToImage(decoder, extractor, mediaFormat);
decoder.stop();
} finally {
if (decoder != null) {
decoder.stop();
decoder.release();
decoder = null;
}
if (extractor != null) {
extractor.release();
extractor = null;
}
}
}
private void showSupportedColorFormat(MediaCodecInfo.CodecCapabilities caps) {
System.out.print("supported color format: ");
for (int c : caps.colorFormats) {
System.out.print(c + "\t");
}
System.out.println();
}
private boolean isColorFormatSupported(int colorFormat, MediaCodecInfo.CodecCapabilities caps) {
for (int c : caps.colorFormats) {
if (c == colorFormat) {
return true;
}
}
return false;
}
private void decodeFramesToImage(MediaCodec decoder, MediaExtractor extractor, MediaFormat mediaFormat) {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
decoder.configure(mediaFormat, null, null, 0);
decoder.start();
final int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
final int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
int outputFrameCount = 0;
while (!sawOutputEOS) {
if (!sawInputEOS) {
int inputBufferId = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufferId);
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
decoder.queueInputBuffer(inputBufferId, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
sawInputEOS = true;
} else {
long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(inputBufferId, 0, sampleSize, presentationTimeUs, 0);
extractor.advance();
}
}
}
int outputBufferId = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US);
if (outputBufferId >= 0) {
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
boolean doRender = (info.size != 0);
if (doRender) {
outputFrameCount++;
Image image = decoder.getOutputImage(outputBufferId);
System.out.println("image format: " + image.getFormat());
if (outputImageFileType != -1) {
String fileName;
switch (outputImageFileType) {
case FILE_TypeI420:
fileName = OUTPUT_DIR + String.format("frame_%05d_I420_%dx%d.yuv", outputFrameCount, width, height);
dumpFile(fileName, getDataFromImage(image, COLOR_FormatI420));
break;
case FILE_TypeNV21:
fileName = OUTPUT_DIR + String.format("frame_%05d_NV21_%dx%d.yuv", outputFrameCount, width, height);
dumpFile(fileName, getDataFromImage(image, COLOR_FormatNV21));
break;
case FILE_TypeJPEG:
fileName = OUTPUT_DIR + String.format("frame_%05d.jpg", outputFrameCount);
compressToJpeg(fileName, image);
break;
}
}
image.close();
decoder.releaseOutputBuffer(outputBufferId, true);
}
}
}
}
private static int selectTrack(MediaExtractor extractor) {
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
if (VERBOSE) {
Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format);
}
return i;
}
}
return -1;
}
private static boolean isImageFormatSupported(Image image) {
int format = image.getFormat();
switch (format) {
case ImageFormat.YUV_420_888:
case ImageFormat.NV21:
case ImageFormat.YV12:
return true;
}
return false;
}
private static byte[] getDataFromImage(Image image, int colorFormat) {
if (colorFormat != COLOR_FormatI420 && colorFormat != COLOR_FormatNV21) {
throw new IllegalArgumentException("only support COLOR_FormatI420 " + "and COLOR_FormatNV21");
}
if (!isImageFormatSupported(image)) {
throw new RuntimeException("can't convert Image to byte array, format " + image.getFormat());
}
Rect crop = image.getCropRect();
int format = image.getFormat();
int width = crop.width();
int height = crop.height();
Image.Plane[] planes = image.getPlanes();
byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
byte[] rowData = new byte[planes[0].getRowStride()];
if (VERBOSE) Log.v(TAG, "get data from " + planes.length + " planes");
int channelOffset = 0;
int outputStride = 1;
for (int i = 0; i < planes.length; i++) {
switch (i) {
case 0:
channelOffset = 0;
outputStride = 1;
break;
case 1:
if (colorFormat == COLOR_FormatI420) {
channelOffset = width * height;
outputStride = 1;
} else if (colorFormat == COLOR_FormatNV21) {
channelOffset = width * height + 1;
outputStride = 2;
}
break;
case 2:
if (colorFormat == COLOR_FormatI420) {
channelOffset = (int) (width * height * 1.25);
outputStride = 1;
} else if (colorFormat == COLOR_FormatNV21) {
channelOffset = width * height;
outputStride = 2;
}
break;
}
ByteBuffer buffer = planes[i].getBuffer();
int rowStride = planes[i].getRowStride();
int pixelStride = planes[i].getPixelStride();
if (VERBOSE) {
Log.v(TAG, "pixelStride " + pixelStride);
Log.v(TAG, "rowStride " + rowStride);
Log.v(TAG, "width " + width);
Log.v(TAG, "height " + height);
Log.v(TAG, "buffer size " + buffer.remaining());
}
int shift = (i == 0) ? 0 : 1;
int w = width >> shift;
int h = height >> shift;
buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
for (int row = 0; row < h; row++) {
int length;
if (pixelStride == 1 && outputStride == 1) {
length = w;
buffer.get(data, channelOffset, length);
channelOffset += length;
} else {
length = (w - 1) * pixelStride + 1;
buffer.get(rowData, 0, length);
for (int col = 0; col < w; col++) {
data[channelOffset] = rowData[col * pixelStride];
channelOffset += outputStride;
}
}
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length);
}
}
if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i);
}
return data;
}
private static void dumpFile(String fileName, byte[] data) {
FileOutputStream outStream;
try {
outStream = new FileOutputStream(fileName);
} catch (IOException ioe) {
throw new RuntimeException("Unable to create output file " + fileName, ioe);
}
try {
outStream.write(data);
outStream.close();
} catch (IOException ioe) {
throw new RuntimeException("failed writing data to file " + fileName, ioe);
}
}
private void compressToJpeg(String fileName, Image image) {
FileOutputStream outStream;
try {
outStream = new FileOutputStream(fileName);
} catch (IOException ioe) {
throw new RuntimeException("Unable to create output file " + fileName, ioe);
}
Rect rect = image.getCropRect();
YuvImage yuvImage = new YuvImage(getDataFromImage(image, COLOR_FormatNV21), ImageFormat.NV21, rect.width(), rect.height(), null);
yuvImage.compressToJpeg(rect, 100, outStream);
}
}
参考
- MediaCodec | Android Developers
- MediaCodecInfo.CodecCapabilities | Android Developers
- Image | Android Developers
- tests/tests/media/src/android/media/cts/ImageReaderDecoderTest.java - platform/cts - Git at Google
- Android MediaCodec stuff
- android - Why doesn't the decoder of MediaCodec output a unified YUV format(like YUV420P)? - Stack Overflow
- Android MediaCodec Formats
- How to use OpenGL fragment shader to convert RGB to YUV420 - Stack Overflow
感谢 帮了android小白的大忙
博主你好,请教下,我用Mediacodec去解码一个mp4文件,用getOutputBuffer拿到ByteBuffer并转换为byte[]数组后写入了文件,是要写入.yuv文件么,我能否看到这些数据帧对应的profile等帧相关的信息呢。当前写入的文件我完全无法解析..
您好!请教使用MediaCode从4k中提取出图片有什么方案吗?
谢谢!
你好,先给你点赞。请教一下,用这个demo解码后会丢帧,没有原始视频一样的帧数?是配置问题吗
显示和存图无法同时满足的,可参考以下解答:
https://stackoverflow.com/questions/34996433/decoding-video-and-encoding-again-by-mediacodec-gets-a-corrupted-file
https://github.com/zolad/VideoSlimmer
我的场景是:mediaCodec 编码 h64裸流,电脑显示实现远程桌面,现在的问题主要是显示的颜色完全不对(怀疑是yuv格式编解码不一致),还有就是花屏。花屏有丢包影响,我做了udp自定义保障稳定传输,还是有些花屏。 电脑端用的是ffmpeg 解码 rgb显示,yuv显示完全错乱。我在手机端设置 mediaCodec 编码输出流的时候,好像只能用 COLOR_FormatSurface ,其他都会报错。想向你请教我的问题出在哪里?非常感谢
你好,能否加个微信,请教下 mediaCodec 编码的问题
学长好,我也正好有需要在Android解码拆帧得到YUV格式的帧数据,也是搜寻了很多,才偶然间,见到学长这篇宝贵的文章,让我一口气解决了问题,其中的畅快溢于言表。这篇短小的博客,太赞了,随后,看了学长的GitHub,真的很叹服,学长只比我大三岁,但是我心目中的偶像!谢谢。
你好,我想请问一下是否有可能在转化yuv的过程中,是否有可能将图片且成原来的一半,因为我的原图是左右完全相同的一张图,来自于vr眼镜,因此我想是否有可能只取出其中的一半。
DEFAULT_TIMEOUT_US=10000 这个变量是什么意思呢?decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US); 这个方法用到这个变量具体是代表什么呢
感谢回复
博主这个demo如何兼容到21以下呢?decoder.getOutputImage是21才有的 21之前 应该如何操作呢
博主,如何指定取某些帧呢?
博主 支持网络视频吗?放一个视频的url是否可以取出帧呢?如果不能 有什么好的方案吗?现在要做一个需求 就是加载视频列表,然后取出对应视频的某些帧 生成一个gif做封面 目前还没头绪呢?望回复
这位高手,我想问一下关于那个BigFlake的实例中用GL来做颜色转换的问题。我使用了他们的代码,发现绝大多数视频都能够分解出帧图像来,但是某些视频只能输出色块(同样的设备同样的代码)。不知道到底是哪里不兼容?详细问题我Post在这里:https://stackoverflow.com/questions/57504443/mediacodec-render-on-offscreen-gl-texture-notworking-on-some-specific-video
谢谢!
博主请问一下,我用mediacodec.getoutputImage在部分机型,比如小米,上面一直为null是怎么回事
部分机型上是null?你需要考虑APP有没有真的读到mp4文件,另外需要考虑在此行代码前没有把buffer读给其他变量~
博主您好,我用您的程序运行时,提取到的图片人脸皮肤都变成了青色,请问这是什么原因造成的呢,是哪个参数设置的问题吗
这个多半是U和V的数据有问题,图片能看到灰度数据说明Y是正确的,你要是提取到的是JPEG图片的话,可以试试导出NV21格式的YUV图片,用如YUView之类的软件看看是不是NV21格式的~再或者你可以换个手机试试来排除CPU硬解码不支持的问题。
你好,最近有幸拜读了大佬的解码程序,并且成功跑通了,很赞。
但是,我在Unity调用Android studio 解码就出问题了,程序运行到extractor.setDataSource(videoFile.toString())就跳过decoder退出了。你知道是什么原因吗?
抱歉没有接触过Unity哟~不过之前有小伙伴评论说是因为动态权限的问题,不知道你的是不是这种情况~
小伙伴有提过解决方法吗?
在附上的GitHub链接里的Demo是加入了动态权限检查的,你可以参考看看~
就是动态权限的问题。Uniyu发布aqk时就赋给了存储权限,会出现setDataSource无法实例化的问题。
还有,博主我想请问一下,在getOutputImage的结果后面,不是有一个image.getFormat的输出是35,请问这个是能说明什么吗?
这个是表示这个手机CPU硬解码支持并解码成的YUV格式,35对应YUV_420_888也就是I420,在https://developer.android.com/reference/android/graphics/ImageFormat有说明。
需要注意的是这个值会随手机硬件的不同而不同,所以本文就采用的Image抽象了这个细节。
博主感谢您,我的问题解决了,是channeloffset的参数设置问题,有一篇博客跟您的代码比较类似,他在channeloffset的参数上设置与您有所差别,我最开始参照的是他的博客,导致抽出来的帧有问题。非常感谢您的帮助。
哈哈哈~我的代码很久以前channeloffset上有个bug会导致生成的NV21格式数据实际是NV12,就会导致图片颜色不对的问题,后来就把这个问题修复了~
博主您好,非常感谢您的回复。经过您的提醒,我回去查看了3种格式的文件,发现首先 jpeg格式的图片颜色有了变化,就是出现了皮肤变成青色的情况,NV21格式的图像偏灰色,比较奇怪,但是I420格式的图像是对的,抽出来的YUv文件颜色是正确的。现在我想问一下,因为我自己的任务是对NV21格式的数据进行操作,我可以将getOutPutImage的数据先转成I420,再转成NV21,这样的操作是合理的吗?因为自己以前没有接触过Android相关的操作,请楼主指教。
那你的问题就是拿到的NV21有问题咯,因为输出JPEG用到的是NV21所以导致JPEG也有问题。先拿到I420再转成NV21是可行的,但是如果你是用的我贴的那个代码的话,估计你得自己写转换函数(其实就是数组元素重排),然后再交给compressToJpeg API就能拿到JPEG图片了。
话说I420正确但NV21不正确挺诡异的,你在把I420重排成NV21后可以把两份数据对比一下,我猜是哪个地方没对齐。
楼主你好,最近使用你的方法过程中发现小米2s(5.0.2系统)在竖屏输出时转换出来的图片出现图像错位,而横屏时一切正常,很奇怪的问题,请问这个是什么原因造成的呢?有什么解决办法? 非常感谢
横屏图片:http://i4.bvimg.com/643543/88c961d520adfb51.jpg
竖屏图片:http://i4.bvimg.com/643543/62ce8cac4867004a.jpg
04-28 11:18:42.879: E/====>(31734): Finished reading data from plane 0
04-28 11:18:42.879: E/====>(31734): pixelStride 2
04-28 11:18:42.879: E/====>(31734): rowStride 480
04-28 11:18:42.879: E/====>(31734): width 480
04-28 11:18:42.879: E/====>(31734): height 720
04-28 11:18:42.879: E/====>(31734): BufferSize 172799
04-28 11:18:42.879: E/====>(31734): Finished reading data from plane 1
04-28 11:18:42.879: E/====>(31734): pixelStride 2
04-28 11:18:42.879: E/====>(31734): rowStride 480
04-28 11:18:42.879: E/====>(31734): width 480
04-28 11:18:42.879: E/====>(31734): height 720
04-28 11:18:42.879: E/====>(31734): BufferSize 172799
04-28 11:18:42.879: E/====>(31734): Finished reading data from plane 2
似乎是UV分量有个整体的偏移,挺奇怪的~这些天忙毕业论文可能没时间,要不你把原始视频发我邮箱我有空看看?
发现只要加上下面code速度就变得非常慢了,不加转出来的nv21又总是那一帧,请问如何解决好:
//decoder.releaseOutputBuffer(outputBufferId, true);
有花屏现象哦
MediaCodec的工作模式是循环使用buffer的,不release的话很有可能造成数据混乱之类的问题~
你好,如果我配置解码后的数据输出到Surface上,然后从Surface上获取数据并调用opencv进行处理,可是我不知道怎么从surface上获取数据,是不能获取吗?
抱歉这个我没有试过,不过下面有评论说,在设置数据输出到surface后,在surface上调用getOutImage()会返回null。不太确定有没有其他的方法~
我主要是想在渲染出画面之前对数据流中的数据拼接一下,拼接技术需要用到openCV,感觉没有必要获取到Image,楼主对openCV有了解吗?
拿到Image是为了抽象YUV格式问题,不然拿到byte[]数据不能预先判定格式,会导致OpenCV跑不起来。
我只是短暂接触过OpenCV,当时测试过拿OpenCV把YUV转RGB了显示在屏幕上,感觉对效率(fps)要求不是太高的话可以将就用~
你好,好文,好文。
null, 0);有个疑问想请教一下;
按照你的方法,把 mCodec.configure(mediaFormat, surface.getHolder().getSurface(),
的第二个参数改为null,可以把图片保存下来。
但是现在要求是一遍播放视频,一边截图(每隔一段时间)。这个功能该如何实现呢?如果mCodec.configure()中设置了surface 的话 getOutImage(index)将返回null
要是我没理解错的话,你是想MediaCodec解码直接投到surface,同时又想能够拿到一些帧数据。
这个似乎现在都还没有太简单的解决方法,主要就是拿到Image了就不能再交给surface了。成本最高的实现方法就是你自己实现Image向surface的传输,这样你就可以从MediaCodec拿到每一帧的Image,然后一份给surface,一份干其他的事情。另外我想到是一个方法就是既然你不是需要每一帧的数据,那干脆需要截图的时候拿到视频当前帧时间,再去seek视频文件的位置,抽出那帧就好了,不过这样做有个时间偏差,就是截图发生的那个时刻的帧往往不是seek到的那帧。
我用这个代码工程,完成30秒钟的解析,得出810帧,保存JPEG 共耗时92s钟,在三星Note5 手机上;相当于每得到一张图片,耗时100ms
但是我seek 到某一个时间戳上,然后保存一张图片,耗时了300-400ms , 这个怎么那么明显呢?是代码问题,还是其他原因?
求指教,有微信吗?或者QQ 吗?
需要把数据渲染到一个surface上,然后从surface上读取数据,这样既可以显示也可以截屏
抱歉关于seek这个不太了解,不过我猜是seek耗时较多,你可以测试看看seek的耗时,和保存图片的耗时~
还有一个问题是,如何才能输入一个随机事件,然后得到具体的某一帧呢? 你的工程都是从开始到MP4 文件结束的;
主要是想得到任意一帧,用mediacodec
您好,我现在有这样的一个需求:
MediaPlayer播放视频-->ImageReader.getSurface(),
onImageAvailable(ImageReader reader)这个回调里面可以获取视频的帧数据,就是YUV_420_888,
这个帧数据经过分析是YUV420SemiPlanar格式,
接下来我要转换成YUV420Planar,好给我的OpenCV使用;并且还要转换成RGB,给SurfaceView显示。
对于这样的过程,您有没有什么建议,优化下流程?
你好 能请教一下吗 ?
你好 我也在使用imageReader 获取解码后的数据,但我现在拿到的数据有问题,能加QQ请教一下吗 ?
ImageReader里似乎可以读出来Image?这样的话用文章代码里的Image转I420就拿到YUV420的数据了。另外想再把YUV420转RGB的话,我目前知道的最快的方法是用OpenCV,或者走OpenGL ES转RGB;不过你可以找找API 21以上有没有提供直接显示YUV的接口(我怀疑没有)。
PS:你说分析是YUV420SemiPlanar,这个可能换个设备就不是了。
感谢回复!
是的,虽然我现在经过各种搜索,解决了流程中涉及到的转码问题,但是估计移植性不够好,而且发现跟原始视频文件格式、分辨率都有关系。
可是好像找不到更好的流程,我想最好是,视频文件解码后,一方面扔给Surfaceview直接显示,另外一方面转成YUV给图像处理算法。
这个你得权衡一下利弊,如果你的首要目标是OpenCV进行逐帧处理,且要求视频解码效率高,且具有通用性(非针对专一设备),那么像文章中说的拿到Image再简单处理成常见YUV格式是目前唯一的办法,其他方法要么没有通用性,要么效率太低。所以我感觉你可以首先尝试从ImageReader拿到Image,然后转I420,解决通用性问题;然后OpenCV处理完后,寻找高效地Surface直接渲染I420数据的方法(我记得有些博客分享过一些方法),解决效率问题。
我在android开发者平台上面看的,通过Bytebuffer获取数据效率比较低,imageReader获取mediacodec解码后的数据 效率最高。
请问你解码后的数据是放在哪儿?Surface上还是其他地方?
我觉得这个要看应用场景,如果你是需要硬解码了直接交给surface渲染的话,ImageReader当然更好,因为可以直接对接surface。但如果希望能够拿到每一帧的数据并进行处理的话,就势必需要转换成RGB/YUV的帧,而Image向RGB/YUV的转换是避免不了操作Buffer的(因为MediaCodec实际是以Buffer的串入串出驱动的),也就必须要损失掉这部分的效率了。
您好,请问一下 怎么通过imageReader高效的拿到mediacodec解码出来的数据 ,您有研究过吗 有这样的demo吗
抱歉当时只是看过ImageReader,但感觉自己用不上就没看了,不过貌似Android CTS上有MediaCodec结合ImageReader解码的例子,你可以看看。
虽然已经2018年,但博主这篇是我目前为止看到的最新最详细的关于Camera2与MediaCodec编码器的结合经验。
好文,回應一下
若用 decoder.configure(mediaFormat, null, null, 0);
拿來存入 4k 影片時(在支援4k decode 裝置上), 會發生 V/MediaCodec: codec output format changed
然後實際存下只有 1920x1088, 不知道是不是限制?
刚才搜了下确实好多设备在解码4k视频时都出现了各种奇怪的问题,抱歉最近比较忙,手上也没有4k视频做测试,所以暂时不太好回答,还请见谅。
另外“V/MediaCodec: codec output format changed”是正常的提示,指的是输出帧格式由YUV420Flexible自动转变成了芯片支持的格式,这是一个透明的过程。(同样不确定是不是这个透明格式转换有问题)
感謝回覆。btw 要試 4k video 需要 4k decoder device 才行 (e.g. Amlogic S905 chip)
你好,我用了你上面测试的demo,然后转换了一个背景大部分都是白色的视频,结果出来的jpg图片背景是灰色的。请问这个是哪里的问题?
可否提供一下视频文件我来看看?你可以直接发到发给你通知邮件的邮箱~
我遇到了一个错误,网上都没找到答案
E/WVMExtractor: Failed to open libwvm.so: dlopen failed: library "libwvm.so" not found
权限都有了,用得是小米6,安卓版本7.1.1
噢,只有小米的设备才这样?
这个不太确定,但搜了一下发现几乎全都是关于小米手机遇到这个问题的~
似乎是在某些小米手机上使用MediaExtractor()会间接用到libwvm.so库,不太确定有什么解决方法~
不过我也遇到类似的提示,但不影响使用...
谢谢,very helpful! saved my ass.
请问必须要API21才可以吗?运行时不显示帧数。。
因为用到了API 21的新特性,所以Android是必须要支持到API 21的。不显示帧数的话也可能是DEMO的问题,貌似有些权限问题~
我想问一下,这个地方,我如何获取到最大的编解码视频分辨率,因为有些设备硬件达不到
MediaFormat outputVideoFormat = MediaFormat.createVideoFormat(OUTPUT_VIDEO_MIME_TYPE, mWidth, mHeight);
我设置的宽 高是原视频的宽高,过大,导致我每次编解码的时候都会报错,我如何才能获取到设备能编解码的最大值呢
抱歉,我不太清楚“芯片支持的最大编解码分辨率”,貌似我也没听说过,可能是孤陋寡闻了...翻MediaCodec文档看到一个API boolean isSizeSupported (int width, int height)),你可以试试~
想问一下,解码的时候那个outputbuffer里面的数据不是yuv数据吗?不是的话,它是什么?
你是说MediaCodec的OutputBuffer?对于这段代码来说拿到的是一个Image对象,这个对象抽象了真实的YUV420数据为三个分量,屏蔽了细节格式的区别。
ByteBuffer buffer = decoder.getOutputBuffer(outIndex);最近刚开始看着mediacodec,想拿到yuv帧数据,不明白的是这里buffer里是什么,outIndex又是什么,不是很理解。谢谢你回复我。
这个是MediaCodec的工作模式,简单来说是以buffer的形式喂给解码器数据,解码器也以buffer的形式吐出数据,你可以看MediaCodec的官方文档,那里有一些示意图画的挺形象的。
还有一个问题就是,我的输出一直显示运行中,这种属于什么原因
这个我在小米手机上碰到过,但一直没时间调试,你可以在Android Studio里看看,我估计是权限问题。
精选好文!!
博主我想问一下现在我有连续的视频流byte[]并不是完整视频文件,MediaExtractor 似乎不合适用。不知道对流文件您的方法合不合适用呢
抱歉我没流式编解码的经验,不过理论上来说MediaCodec是能支持到stream的(因为MediaExtractor也是拿出一个一个数组喂给MediaCodec。不过鉴于MediaCodec的API并不丰富,你可能需要解决如构造文件头(即让MediaCodec判定文件编码等信息),构造视频帧的问题,我看Google搜“MediaCodec stream”有比较多相关结果,祝你好运咯~
你好,请问我想把裸流h.264解码保存为图片时发现用 Image image = mCodec.getOutputImage(outIndex);去获取YUV数据时是空的这是为什么呢?
decoder.configure(mediaFormat, null, null, 0); 第二个参数需要为null,不能为surface。
不能给surface,怎么播放视频流
抱歉回复晚了。出现这个问题的情况原因可以有很多的,最常见的是MediaCodec的处理流程漏了哪一步,导致buffer没有送进去,或者取出的时候没有取到正确的,还请参考MediaCodec官方文档,里面有很多注意事项的
你好,我想请教一个问题,在使用硬解码成功得到YUV数据后,我想同步输出到屏幕显示,使用YUV转bitmap的效率有点低,网上说可以通过SurfaceTextture去渲染,但是网上没有找到太多的资料,请问有什么好的方法嘛推荐吗?
用SurfaceTexture其实就是用OpenGL把YUV转RGB吧?如果你是要转RGB的话,OpenGL确实是一个很好的选择~
如果你是想拿到Bitmap的话,可以试试compressToJpeg(),直接写到流,然后读成Bitmap,这样节约掉IO操作,应该会比较快。
楼主,你好。在运行这个代码的时候,感觉会出现卡顿。请问您知道原因吗?
你是指UI上的计数会突然卡一下然后继续跑是吧?这个我也见到过,调试也没看到日志,所以现在是没找到原因的。
但MediaCodec本身就是异步的,所以我也没太往“实时性”考虑,只希望MediaCodec能够尽可能快解码。
您好,我想把图片转成视频应该怎么做?
YUV格式的话似乎也是可以直接用MediaCodec进行硬件编码的,jpeg等格式图片就比较麻烦了,那部分我自己也没有深入研究,抱歉咯
你好,请问如何能用Camera2 API直接获得NV21的数据帧?我通过ImageReader只能拿到YUV420格式,转换成NV21需要大约20ms,我想省掉这时间,能不能直接拿到NV21格式的。ImageReader设置NV21直接抛异常我看了安卓源代码,ImageReader构造函数里直接检查了如果format == NV21就直接抛异常,无语。。。
Camera2似乎取消了之前Camera设置raw image输出格式的API,意思就是不再支持指定输出NV21格式了(Camera有这个API的)。从API21不仅引入了Camera2,而且引入了一整套抽象,如Image,ImageReader,其中一个目的似乎就是抽象具体的YUV420格式,从目前来看的话只能是从ImageReader拿到Image再自己构造成NV21格式。
多谢回复。不过好失望,我们的图像处理都是基于NV21的,看起来省不掉了
你好,看了你的实现之后,我发现你是把所有的帧都保存成相应的图片,如jpg,由此我想是否可以保存特定的某一帧,进而我想实现一个在播放视频时截图的功能,按照你的这个实现,我在videoDecode方法中extractor.selectTrack(trackIndex);语句之后添加
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { sawOutputEOS = true; } if (presentationTimeUs == timeUs) {extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);将其定位到指定位置的前一个关键帧,其中timeUs是上层传递下来的时间戳,然后让他从这个关键帧开始解码,同时在decodeFramesToImage中我添加了一个判断
if (outputBufferId >= 0) {
如果与传递下来的时间戳匹配就将其保存输出,进而实现截图功能。大神帮我看看,这种想法是可行的不?非常感谢
我没往这方面做过,只能凭我的经验来谈谈咯。
首先你的想法应该是可行的,但需要注意MediaExtractor和MediaCodec中time的概念有些区别(我印象中是这样),很容易弄混出现不必要的bug;貌似你知道关键帧的概念,这里的潜在问题就不用细说咯。你可以动手试试啦~
不过我的想法是可不可以直接用MediaCodec播放视频,然后触发截图的时候直接就从MediaCodec把实时的那帧保存下来?这样似乎就不用再这么绕一圈了,一点想法~
你好,又来请教你了,上次我在你程序的基础上通过seekTo以及对TimeUs进行对比操作实现了截取某一帧数据的功能,但是我在调试的过程中发现一个问题。我在一开始用extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)将其定位到timeUs所代表时间戳的前一个关键帧,然后直接调用extractor.readSampleData获取数据,调用extractor.getSampleTime()获取时间time,按理说此时获得的时间time应该是定位的前一个关键帧的时间戳,也就是说time应该小于timeUs,但是结果却不是这样。然后这两天经过格式调试,以及查看源代码都是这样,我猜测应该是seekTo的问题,不知大神知道不,望指点一下,感激不尽。
抱歉我没往这方面做过,也不太好猜原因...这些天比较忙也很难挤出时间帮你咯,要不你再搜搜解决方法,上StackOverflow看看?或者等我闲下来再回复你...
非常感谢你的解答,我也是刚学不久,我再去好好调试下
你好,我在测试的时候,运行到extractor.setDataSource(videoFile.toString())这一步时,就出错了,提示failed to instantiate extractor。你有遇到过吗
抱歉你这个是第一次碰到...要不要考虑没有申请读写权限,或者手机没有允许权限申请?或者视频文件路径有没有出错?另外我看到stackoverflow上也有类似的问题,不过我不太好排查,还麻烦你自己再尝试下。
经过检查,发现了就是因为动态权限的问题,我在你程序的基础上加入了动态权限检查,然后就能运行了。再次感谢。
你好,我也遇到了同样的问题。请问你加的是哪些动态权限?万分感谢。
应该是READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE,可以参考Google提供的动态权限工具EasyPermissions来适配动态权限。
Android 6.0引入的动态权限?我有空也把这个检查加进去,谢谢反馈~
好的,还是非常感谢,我自己再好好检查一下
精品好文!!
我有个问题:
COLOR_FormatYUV420Flexible
YV12 和 NV21都是YUV420,是怎么区分的呢?不区分不可能,因为UV的位置都不确定。
不知博主有没有研究?
你没有仔细看哟,我写了的呢!
就是用到了API21新增的Image类(文中我说详细格式介绍的链接专门讲这个),Image类自己把YUV三个分量分离了,所以要转成YV12和NV21的话实际就是按对应格式标准重新把YUV分量混起来啦!
少有的 关于 mediaCodec 好文
谢谢你的肯定!
这是我见过最原创,最不复制张贴的android硬件加速的文章,非常感谢