如果还没看过前面两篇博文,还是建议看一下,由于本文和前两篇是有很大关联的。

代码运行log剖析

首先点击第一个item:

进入到这个界面:

phpgrafika转bmpAndroid硬编解码MediaCodec解析从猪肉餐馆的故事讲起三 SQL

看下此时的Log:

log打印位置在com.android.grafika.PlayMovieActivity,紧张看下“SurfaceTexture ready (984x1384)”这一行:

@Overridepublic void onSurfaceTextureAvailable(SurfaceTexture st, int width, int height) { // There's a short delay between the start of the activity and the initialization // of the SurfaceTexture that backs the TextureView. We don't want to try to // send a video stream to the TextureView before it has initialized, so we disable // the "play" button until this callback fires. Log.d(TAG, "SurfaceTexture ready (" + width + "x" + height + ")"); mSurfaceTextureReady = true; updateControls();}

还记得Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(二)画的整体流程图么:

onSurfaceTextureAvailable这个回调方法便是见告我们,TextureView的SurfaceTexture已经初始化好了,可以开始渲染了。
此时才会将播放按钮置为可点击。
log“SurfaceTexture ready (984x1384)” 中的“(984x1384)”即为TextureView的尺寸。

C++学习资料免费获取方法:关注音视频开拓T哥,+「链接」即可免费获取2023年最新C++音视频开拓进阶独家学习资料!

此时,轻轻点击播放按钮,于是视频开始动起来了,可谓是穿梭韶光旳画面的钟,从反方向开始移动~:

首先输出了这条log:

D/fuyao-Grafika: Extractor selected track 0 (video/avc): {track-id=1, level=32, mime=video/avc, profile=1, language=``` , color-standard=4, display-width=320, csd-1=java.nio.HeapByteBuffer[pos=0 lim=8 cap=8], color-transfer=3, durationUs=2033333, display-height=240, width=320, color-range=2, max-input-size=383, frame-rate=16, height=240, csd-0=java.nio.HeapByteBuffer[pos=0 lim=38 cap=38]}

它是在MediaExtractor选中媒体轨道的时候打印的,打印出详细当前视频轨道格式干系信息:

/ Selects the video track, if any. @return the track index, or -1 if no video track is found. /private static int selectTrack(MediaExtractor extractor) { // Select the first video track we find, ignore the rest. //当前媒体文件共有多少个轨道(视频轨道、音频轨道、字幕轨道等等) int numTracks = extractor.getTrackCount(); for (int i = 0; i < numTracks; i++) { //第i个轨道的MediaFormat MediaFormat format = extractor.getTrackFormat(i); //format对应的mime类型 String mime = format.getString(MediaFormat.KEY_MIME); //找到视频轨道的index if (mime.startsWith("video/")) { if (VERBOSE) { //把稳这行的log打印 Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format); } return i; } } return -1;}

轻微阐明下log中的几个关键参数:

1.log中的level和profile指的是画质级别,以下阐明引用于# H264编码profile & level掌握

H.264有四种画质级别,分别是baseline, extended, main, high: 1、Baseline Profile:基本画质。
支持I/P 帧,只支持无交错(Progressive)和CAVLC; 2、Extended profile:进阶画质。
支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;(用的少) 3、Main profile:主流画质。
供应I/P/B 帧,支持无交错(Progressive)和交错(Interlaced), 也支持CAVLC 和CABAC 的支持; 4、High profile:高等画质。
在main Profile 的根本上增加了8x8内部预测、自定义量化、 无损视频编码和更多的YUV 格式; H.264 Baseline profile、Extended profile和Main profile都是针对8位样本数据、4:2:0格式(YUV)的视频序列。
在相同配置情形下,High profile(HP)可以比Main profile(MP)降落10%的码率。
根据运用领域的不同,Baseline profile多运用于实时通信领域,Main profile多运用于流媒体领域,High profile则多运用于广电和存储领域。

2.mime为video/avc,这个上篇文章已经讲过,video/avc即为H264。

3.color-standard:指的是视频的颜色格式,

/ An optional key describing the color primaries, white point and luminance factors for video content. The associated value is an integer: 0 if unspecified, or one of the COLOR_STANDARD_ values. /public static final String KEY_COLOR_STANDARD = "color-standard";/ BT.709 color chromacity coordinates with KR = 0.2126, KB = 0.0722. /public static final int COLOR_STANDARD_BT709 = 1;/ BT.601 625 color chromacity coordinates with KR = 0.299, KB = 0.114. /public static final int COLOR_STANDARD_BT601_PAL = 2;/ BT.601 525 color chromacity coordinates with KR = 0.299, KB = 0.114. /public static final int COLOR_STANDARD_BT601_NTSC = 4;/ BT.2020 color chromacity coordinates with KR = 0.2627, KB = 0.0593. /public static final int COLOR_STANDARD_BT2020 = 6;

还记得# 音视频开拓根本知识之YUV颜色编码 里面说过,RGB到YUV有不同的转化标准:

目前一样平常解码后的视频格式为yuv,但是一样平常显卡渲染的格式是RGB,以是须要把yuv转化为RGB。

这里涉及到 Color Range 这个观点。
Color Range 分为两种,一种是 Full Range,一种是 Limited Range。
Full Range 的 R、G、B 取值范围都是 0~255。
而 Limited Range 的 R、G、B 取值范围是 16~235。

而对付每种Color Range来说,还有不同的转换标准,常见的标准紧张是 BT601 和 BT709(BT601 是标清的标准,而 BT709 是高清的标准)。

这里该视频的color-standard为4,即转换标准为BT.601 525。

4.color-range: 上面引用部分已经提及,当前color-range为2,看下谷歌文档的常量值解释:

/ Limited range. Y component values range from 16 to 235 for 8-bit content. Cr, Cy values range from 16 to 240 for 8-bit content. This is the default for video content. /public static final int COLOR_RANGE_LIMITED = 2;/ Full range. Y, Cr and Cb component values range from 0 to 255 for 8-bit content. /public static final int COLOR_RANGE_FULL = 1;

以是当前视频的color-range为Limited range。

其他参数由于数量太多,大家也大部分可以看明白,就不一一阐明了。

看接下来的log:

第一行是这里打印的:

//拿到可用的ByteBuffer的indexint inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);//根据index得到对应的输入ByteBufferByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];Log.d(TAG, "decoderInputBuffers inputBuf:" + inputBuf + ",inputBufIndex:" + inputBufIndex);

打印的是inputBuffer的情形,上一篇已经讲过,这里就犹如生猪肉采购员讯问厨师有没有空篮子,厨师在TIMEOUT_USEC微秒韶光内见告了采购员篮子的编号,然后采购员根据编号找到对应的空篮子。

根据log可以看出:

decoderInputBuffers inputBuf:java.nio.DirectByteBuffer[pos=0 lim=6291456 cap=6291456],inputBufIndex:2

这个空Buffer大小为6291456字节(pos表示当前操作指针指向的位置,lim表示当前可读或者可写的最大数量,cap表示其容量),inputBufIndex为2,即该Buffer在MediaCodec的输入Buffer数组的位置是2。

submitted frame 0 to dec, size=339

这个log的frame 0表示MediaExtractor的readSampleData读取出来的第几块数据,在这里便是第几帧,size=339表示该帧大小为339字节,当然这是压缩的数据大小。

下面一条log输出端取数据的,即顾客讯问厨师猪肉炒好了没有:

D/fuyao-Grafika: dequeueOutputBuffer decoderBufferIndex:-1,mBufferInfo:android.media.MediaCodec$BufferInfo@fcbc6e2

D/fuyao-Grafika: no output from decoder available

这条log来源:

int outputBufferIndex = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);Log.d(TAG, "dequeueOutputBuffer decoderBufferIndex:" + outputBufferIndex + ",mBufferInfo:" + mBufferInfo);

decoderBufferIndex为-1,则即是MediaCodec.INFO_TRY_AGAIN_LATER,即当前输出端还没有数据,即厨师见告顾客,猪肉还没做好。

如果看过之前我写的解析H264视频编码事理——从孙艺珍的电影提及(一)和 解析H264视频编码事理——从孙艺珍的电影提及(二),就知道视频编码是一个非常繁芜的过程,涉及大量的数学算法,以是解码也不会大略,基本不会刚放一帧数据到input端,output端就立马拿到解码后的数据。

从后面的log可以看到,经由很多次在input端放入数据,又考试测验在output端取出数据的循环之后,终于在第一次在input端放入数据的77ms秒之后,在output端拿到了数据:

startup lag是官方demo已经有的统计从第一次在input端放入数据到第一次从output端拿到数据的韶光长。

接下来便是取到详细数据的log:

decoderBufferIndex为0,即取到的解码数据所在的buffer在output端buffer数组第0个。

ecoderOutputBuffers.length:8是我专门把output数组数量打印出来:

ByteBuffer[] decoderOutputBuffers = decoder.getOutputBuffers();Log.d(TAG, "ecoderOutputBuffers.length:" + decoderOutputBuffers.length);

可见output端buffer数组大小为8个(经由实践创造,该数值并不是固定的)。

outputBuffer:java.nio.DirectByteBuffer[pos=0 lim=115200 cap=115200]表示该buffer的可用数据和容量都为115200。
后面解码出来的数据也是这个大小,由于解码之后的数据便是一帧画面的yuv数据,由于画面的分辨率固定,yuv格式也是固定,以是大小自然也是一样的。

而在output拿到数据之前的上一次取数据的log须要把稳下:

D/fuyao-Grafika:dequeueOutputBuffer decoderBufferIndex:-2,mBufferInfo:android.media.MediaCodec$BufferInfo@9bec00c

D/fuyao-Grafika: decoder output format changed: {crop-right=319, color-format=21, slice-height=240, image-data=java.nio.HeapByteBuffer[pos=0 lim=104 cap=104], mime=video/raw, stride=320, color-standard=4, color-transfer=3, crop-bottom=239, crop-left=0, width=320, color-range=2, crop-top=0, height=240}

decoderBufferIndex为2,即MediaCodec.INFO_OUTPUT_FORMAT_CHANGED。
在拿到数据之后,会现有一个关照输出数据格式变革的关照,我们可以在这里拿到输出数据的格式。

1.crop-left=0,crop-right=319,crop-top=0,crop-bottom=239表示的是真正的视频区域的4个顶点在全体视频帧的坐标位置。

有读者可能会问,视频不是充满一帧么?实在不是的,看下官网的解读 developer.android.google.cn/reference/a… :

The MediaFormat#KEY_WIDTH and MediaFormat#KEY_HEIGHT keys specify the size of the video frames; however, for most encondings the video (picture) only occupies a portion of the video frame. This is represented by the 'crop rectangle'.

You need to use the following keys to get the crop rectangle of raw output images from the output format. If these keys are not present, the video occupies the entire video frame.The crop rectangle is understood in the context of the output frame before applying any rotation.

详细key的意义:

Format Key

Type

Description

MediaFormat#KEY_CROP_LEFT

Integer

The left-coordinate (x) of the crop rectangle

MediaFormat#KEY_CROP_TOP

Integer

The top-coordinate (y) of the crop rectangle

MediaFormat#KEY_CROP_RIGHT

Integer

The right-coordinate (x) MINUS 1 of the crop rectangle

MediaFormat#KEY_CROP_BOTTOM

Integer

The bottom-coordinate (y) MINUS 1 of the crop rectangle

官网又给了一段通过这4个值打算视频有效区域的代码:

MediaFormat format = decoder.getOutputFormat(…); int width = format.getInteger(MediaFormat.KEY_WIDTH); if (format.containsKey(MediaFormat.KEY_CROP_LEFT) && format.containsKey(MediaFormat.KEY_CROP_RIGHT)) { width = format.getInteger(MediaFormat.KEY_CROP_RIGHT) + 1 - format.getInteger(MediaFormat.KEY_CROP_LEFT); } int height = format.getInteger(MediaFormat.KEY_HEIGHT); if (format.containsKey(MediaFormat.KEY_CROP_TOP) && format.containsKey(MediaFormat.KEY_CROP_BOTTOM)) { height = format.getInteger(MediaFormat.KEY_CROP_BOTTOM) + 1 - format.getInteger(MediaFormat.KEY_CROP_TOP); }

2.color-format:颜色编码格式。
21即为COLOR_FormatYUV420SemiPlanar,也常叫做叫作NV21。
关于yuv详细魄式在音视频开拓根本知识之YUV颜色编码 已有阐述,不过文章并没有详细讲NV21,NV21的于半平面格式(semi planner),y独立放一个数组,uv放一个数组,先V后U交错存放(图来自: 浅析 YUV 颜色空间)

比如一个44的画面,分布如下图所示:

Y Y Y YY Y Y YY Y Y YY Y Y YV U V UV U V U

3.slice-height:指的是帧的高度,即有多少行,不过这个行数可能是内存对齐过的,有时候为了提高读取速率,视频帧高度会添补到2的次幂数值。

4.stride:跨距,是图像存储的时候有的一个观点。
它指的是图像存储时内存中每行像素所占用的空间。
同样的,这个也是经由内存对齐的,所以是大于即是原视频的每行像素个数。
很多视频花屏问题的根源便是忽略了stride这个属性。

其他参数上面已讲过,就不赘述。

拿到输出的解码数据就通过releaseOutputBuffer渲染到Surface:

//将输出buffer数组的第outputBufferIndex个buffer绘制到surface。
doRender为true绘制到配置的surfacedecoder.releaseOutputBuffer(outputBufferIndex, doRender);

我们看到log的output末了一帧数据是:

output EOS

当调用:

int outputBufferIndex = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);

得到的mBufferInfo.flags为MediaCodec.BUFFER_FLAG_END_OF_STREAM(intput端在视频末了一帧的时候传入)的时候,解释该帧已经是视频末了一帧了,此时就跳出解码的大循环,准备开释资源:

finally { // release everything we grabbed if (decoder != null) { //Call stop() to return the codec to the Uninitialized state, whereupon it may be configured again. decoder.stop(); decoder.release(); decoder = null; } if (extractor != null) { extractor.release(); extractor = null; }}

还记得Android硬编解码利器MediaCodec解析——从猪肉餐馆的故事讲起(一) 提及过得MediaCodec的状态机么:

先调用了stop方法,就进入了Uninitialized状态,即猪肉餐馆要整顿桌椅了,整顿完桌椅之后,再调用release就开释资源,即猪肉餐馆关门了。

将解码输出数据保存下来

接下来来做一件有趣的事情,便是将每次输出的解码数据保存为图片。

创建一个方法吸收输出的一帧数据,然后通过系统供应的YuvImage可以将yuv数据转化为jpeg数据,然后通过BitmapFactory.decodeByteArray将jpeg数据转化为Bitmap,再保存到本地文件夹中。

private void outputFrameAsPic(byte[] ba, int i) { Log.d(TAG, "outputBuffer i:" + i); YuvImage yuvImage = new YuvImage(ba, ImageFormat.NV21, mVideoWidth, mVideoHeight, null); ByteArrayOutputStream baos = new ByteArrayOutputStream(); //将yuv转化为jpeg yuvImage.compressToJpeg(new Rect(0, 0, mVideoWidth, mVideoHeight), 100, baos); byte[] jdata = baos.toByteArray();//rgb Bitmap bmp = BitmapFactory.decodeByteArray(jdata, 0, jdata.length); if (bmp != null) { try { File parent = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/moviePlayer/"); if (!parent.exists()){ parent.mkdirs(); } File myCaptureFile = new File(parent.getAbsolutePath(),String.format("img%s.png", i)); if (!myCaptureFile.exists()){ myCaptureFile.createNewFile(); } BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(myCaptureFile)); bmp.compress(Bitmap.CompressFormat.JPEG, 80, bos); Log.d(TAG, "bmp.compress myCaptureFile:" + myCaptureFile.getAbsolutePath()); bos.flush(); bos.close(); } catch (Exception e) { e.printStackTrace(); Log.d(TAG, "outputFrameAsPic Exception:" + e); } }}

然后在每次得到output端Buffer的地方调用该方法:

ByteBuffer outputBuffer = decoderOutputBuffers[outputBufferIndex];Log.d(TAG, "outputBuffer:" + outputBuffer);outputBuffer.position(mBufferInfo.offset);outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);byte[] ba = new byte[outputBuffer.remaining()];//byteBuffer数据放入baoutputBuffer.get(ba);//输出的一帧保存为本地的一张图片outputFrameAsPic(ba, decodeFrameIndex);

再运行下程序,得到以下图片:

可见每一帧都成功截图并保存到本地~~

同步与异步模式

末了说下,MediaCodec编解码是分为同步和异步模式的(Android 5.0开始支持异步状态),同步便是比如生猪肉采购员和顾客必须 在Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(二)的关于MediaCodec的解码流程代码,是属于同步,所谓的同步,是相对付异步而言的。
同步和异步最大的不同,个人认为便是前者是哀求我们主动去咨询MeidaCodec有没有可用的Buffer可以用,后者是MeidaCodec来关照我们已经有有了可用的buffer。
就像原来是猪肉采购员主动讯问厨师有没有空篮子可以用,现在变为厨师发个微信见告采购员现在有空篮子可以用。

对付异步来说,MediaCodec的事情状态和同步有一点不同:

异步的情形下从Configured会直接进入Running状态,然后等待MediaCodec的回调关照再处理数据即可,以下为官方给的代码模板:

MediaCodec codec = MediaCodec.createByCodecName(name); MediaFormat mOutputFormat; // member variable codec.setCallback(new MediaCodec.Callback() { @Override void onInputBufferAvailable(MediaCodec mc, int inputBufferId) { ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } @Override void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is equivalent to mOutputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } @Override void onOutputFormatChanged(MediaCodec mc, MediaFormat format) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) mOutputFormat = format; // option B } @Override void onError(…) { … } }); codec.configure(format, …); mOutputFormat = codec.getOutputFormat(); // option B codec.start(); // wait for processing to complete codec.stop(); codec.release();总结

本文在上一文章剖析代码的根本上运行了代码,通过剖析log剖析解码流程的细节,让各位对解码流程有更清晰的认识。
并将解码出来的每帧截图保存到本地,验证了视频解码的output端每次获取的数据确实是表示一帧的数据。

末了讲了一下MediaCodec编解码异步模式干系。

美好的光阴总是过得很快,不知不觉已经用了三篇博文讲MediaCodec了,我已经迫不及待地想进入下一个系列了——OpenGL系列。

由于解码成功后,便是渲染到屏幕了,而当前Android平台最主流的渲染工具,便是OpenGL了。

作者:半岛铁盒里的猫 链接:https://juejin.cn/post/7113767096512675870 来源:稀土掘金 著作权归作者所有。
商业转载请联系作者得到授权,非商业转载请注明出处。

在开拓的路上你不是一个人,欢迎加入C++音视频开拓互换群大家庭正在跳转谈论互换

#程序员##C++##音视频开拓#