每天一点点音视频_Graphics架构

再以后的一段日子的,我们将会学习 Android 上 Graphics 架构的一些组件, 将会从地层到上层的顺序。今天是一个概述。

底层的组件

BufferQueue 和 Gralloc

BufferQueue 实现连接图形缓冲的生产者和消费者,消费者主要时接受了数据后,显示出来或者做进一步的处理。

gralloc 负责分配缓冲,通过 HAL 接口实现

SurfaceFlinger, Hardware Composer 和 virtual displays

SurfaceFlinger 从多个源接受数据, 混合后发送显示

HWC(Hardware Composer) 决定使用最有效的方式来混合图像

virtual display 让混合后的图像再系统内可用,比如录屏

Surface, Canvas 和 SurfaceHolder

Surface 生成一个对应的缓冲队列, 当再一个 Surface 上绘制时,最后结果会被传给消费者

Canvas API 是一套软件实现的绘制接口(支持硬件加速),负责往 Surface 上绘制

所有与视图相关的的操作,都会有一个 SurfaceHolder, 通过它来获取或者设置 Surface 的参数比如 大小, 格式

EGLSurface, OpenGL ES

OpenGL ES(GLES) 定义了一套图形渲染的 API

EGL 是一个库,它被用来通过操作系统来创建 OpenGL 里的窗口

Vulkan

Vulkan 是有一个图形渲染的 API, 类似于 OpenGL, 它提供了工具再应用里创建高质量,实时的图像, 减少了 CPU 负载, 支持 SPIR-V Binary Intermediate 语言

上层的组件

SurfaceView 和 GLSurfaceView

SurfaceView 将一个 Surface 和 View 绑定, SurfaceView 的 View 由 SurfaceFlinger 混合。允许再单独的线程做渲染操作。

GLSurfaceView 提供了辅助类管理 EGL 上下文,线程间通讯,与 Activity 生命周期的交互。

SurfaceTexture

SurfaceTexture 将一个 Surface 和一个 Texture 绑定在一块来创建一个缓冲队列,应用作为这个缓冲队列的消费者。

当一个生产者产生了一个新缓存,它会将它入队,这时候你的应用就会得到通知,应用就会释放旧的缓冲区,获取新的。然后执行 EGL 操作,让新缓冲区成为一个 GLES 中的外部 texture。

TextureView

TexureView 将一个 View 和一个 SurfaceTexture 绑定。

TextureView 包裹了一个 SuraceTexture, 负责监听它,从其中获取新的缓冲区,作为它的数据源, 然后按照 View 的配置来渲染数据

View 的混合总是使用 GLES, 这意味着更新内容会导致其他 View 的重绘(这里不太明白)

明天: 每天一点点音视频_BufferQueue


参考 官方文档

每天一点点音视频_Graphics数据流

先来个图,看图说话:

现在我才突然明白, 显示屏幕和GPU是两个东西,之前我以为,输入到GPU就是输出到显示屏了,这种错误认知导致了很多纠结的地方。

上图可以看出, 一个正在显示的屏幕, 包括几个部分, 分别对应着一个 Surface, 每个 Surface 后都有一个缓冲队列, 而这其中的某些缓冲队列会经过 GPU 合并成一个缓冲队列, 最后再给 HWCOMPOSER, 这是后才真正在屏幕上显示出来。

SurfaceFlinger 负责排版,对这些不同的部分排列, 昨天的图中可以看到 WindowManager, 就负责提供提供排列的信息。

BufferQueue

这个模型根 MediaCodec 的模型很相似, 生产者负责获取空的缓存装入数据,再送入, 消费者负责获取由数据的缓冲,使用后再还回去。

它是一个数据结构, 内部是一个缓存池和一个队列, 使用 Binder IPC 再进程间传递数据。生产这的接口是 IGraphicBufferProducer, 它是 SurfaceTexture 的一部分,BufferQueue经常备用来渲染到 Surfacew(这里还是没搞明白, Surface 和 SurfaceTexture, BufferQueue 的关系)

BufferQueue 分三种模式:

  1. 类同步模式, 这时,不会丢帧,生产者太快的话会等待空缓存

  2. 非阻塞模式, 这时,也不会丢帧,但是生产太快的话,不是等待,而是抛出异常, 这可以避免死锁。

  3. 丢弃模式, 这时,如果生产者太快,就会丢掉某些帧

每天一点点音视频_Graphics

这个系列是讲音视频的,但是这几天会讲 Android 的 Graphics 显示,因为在项目中用到了 Surface, TextureView, SurfaceTexture 这些东西,搞不懂, 所以来学习一下。

内容主要是参考了官方文档,是对 Android上图像显示的架构的详细说明。

Android上,应用开发者主要使用三种 API 绘制图形: Canvas, OpenGL ES, Vulkan

无论使用那种 API, 都是绘制到 Surface, Surface 代表了一个缓冲队列的生产者, 消费者是 SurfaceFlinger。Android 上的每一个 Window 后面都有一个 Surface, 所有可见的 Surface 都被 SurfaceFlinger 渲染到显示屏上。

Surface作为一种数据传递的载体, 连通者消费者和生产者, 生产者有很多, 比如 Camera, OpenGL, 解码器。消费者也很多, 比如 SurfaceFlinger, OpenGL, 编码器。

这里的 Image Stream Producers 就是那些能够产生一帧一帧的图像, 可以往 Surface 写入的模块

Image Stream Consumer 就是 Surface 的消费者

Hardware Composer 是硬件抽象层的显示子系统。

Gralloc (graphic memory allocator) 被图形生产者用来分配内存。

以下是我的理解或者是猜测:

Surface 在应用层,代表了底层的一个图形队列, 连接了生产者和消费者。

生产者通过 Gralloc 分配内存, 然后通过绘制 API 向上面绘制,绘制完传出,如果消费者是 SurfaceFlinger, 它就会给 Hardware Composer

明天: 每天一点点音视频_Graphics数据流

Android上实现预览录制OpenGL处理过的相机数据

在 Android 上, Camera2 支持以 Surface 的方式将视频数据传出。

我们可以通过创建 OpenGL 上下文, 并在其冲分配一个 texture, 接着创建 SurfaceTexture, 继而创建 Surface, 从而可以接受相机的数据。

这样相机的数据以 texture 的形式进入 OpenGL, 就可以使用 OpenGL 处理了,特别注意的是, 这时候的 texture 不应该绑定到 GLES20.GL_TEXTURE_2D, 而是绑定到 GLES11Ext.GL_TEXTURE_EXTERNAL_OES。

至于预览, 可以使用 TextureView, 继而得到 Surface, 继而得到 EGLSurface, 就可以使用 OpenGL 向上面绘制东西了。

可以从编码器或者 MediaRecorder 获取 Surface, 进而得到 EGLSurface, 这样,在 OpenGL 上下文, 可以切换两个 EGLSurface, 分别绘制。

这样数据流向为 Camera -> OpenGL -> TextureView 或者 编码器

Camera 的输出分辨率由在 OpenGL 上下文创建的 SurfaceTexture 的 defaultBuffer 分辨率决定。TextureView有显示在View上的分辨率, 也有背后 SurfaceTexture 的分辨率,前者由 View 分辨率决定, 后者由 defaultBuffer 分辨率决定。编码器 MediaRecorder 可以直接配置视频的分辨率分辨率。

TextureView 内部的 Surface 如何显示到 View 上, 是由 TextureView 的 setTransform() 来决定的

在使用 SurfaceTexture 接受数据的时候, updateTexImage() 后,可以通过getTransformMatrix() 得到一个变换矩阵, 但是还是要叠加变换,因为 Camera 的输出帧分辨率,与 OpenGL 的输出分辨率不同。

好吧,我自己感觉很清楚,因为花了好长时间弄明白了,但是我也很清楚,读者肯能是懵比状态。

每天一点点音视频_Surface

这是一个大问题, 可能一个问题能不能完成并不在于它是不是很大, 而是能不能将它分解成小问题,再就是排期,一个大问题,可能需要一个月,却排了一天,那结果肯定是失败。问题是如何排期呢,排不准呀。我打算避开排期的问题,因为对于我来说,一个问题之所以成为问题,是因为有很多未知, 有未知就没法排期。但是我可以每天一点点,探索这个问题空间,我的目标是解决这个问题,不在乎花多少时间。而每天有一点补充,我相信一定能解决这个问题,或者说接近答案。

Surface 是什么

想想现在 Surface 的使用,好像就只有用 Surface(aSurfaceTexture), 然后将它给 OpenGL, 创建一个 EGLSurface, 然后就是对 EGLSurface 的操作了。

正如文档所说: Surface 通常来自(Image Buffer)的消费者,如 SurfaceTexture, MediaRecorder, Allocation, 然后被传给数据生产者,如 EGLSurface, MediaPlayer, Camera。Surface 表现的像一个对消费者的弱引用, 保持消费者不被回收。

文档还说: 它是一个句柄,用来引用被 screen compositor 管理的 raw buffer, 这就完全不知所云了, 牵扯到底层框架了吧。

这篇文章可以
不过还是官方文档讲的明白

本想来个系统的文章, 找了个太大的概念, 还是细水长流吧, 坚持每天一点点。

明天: 每天一点点音视频_Graphics

每天一点点音视频_MediaCodec的C位出道

说 MediaCodec 是 C 位出道,是因为, MediaCodec 在 Android 平台上, 硬件音视频编码的的关键模块。我们当前了解的只是 Java 层的, 实际上 ndk 也提供了 jni 层的。

MediaCodec 输入输出数据可以是三种:

  1. 编码的数据, 这种数据可以保存到文件中,这时候需要 MediaMuxer 对数据打包, 可以从文件中读取,这时候需要 MediaExtractor
  2. 原始音频数据, PCM, 之前提到过, 可以从 AudioReord 读取, 也可以从 AudioTrack 输出
  3. 原始的视频数据, 有几种格式, 在 Android 上比较方便的操作方式是使用 Surface, 这个 Surface 可以从 Camera 来, 从 OpenGL 来, 录屏幕来, 可以输出到 OpenGL, 输出到 UI等等。

我的最初的想法是想把音视频的大的概念铺开, 但是, 现在感觉是这种蜻蜓点水不是说不好, 只是如果能于工作联系起来,把那些花好久解决的问题记录下来,可能更好,当然也可以都进行。

明天: 每天一点点音视频_Surface

每天一点点音视频_MediaCodec使用示例

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();

以下是同步模式:

MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
    int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
    if (inputBufferId >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer(…);
        // fill inputBuffer with valid data
        …
        codec.queueInputBuffer(inputBufferId, …);
    }
    int outputBufferId = codec.dequeueOutputBuffer(…);
    if (outputBufferId >= 0) {
        ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
        MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
        // bufferFormat is identical to outputFormat
        // outputBuffer is ready to be processed or rendered.
        …
        codec.releaseOutputBuffer(outputBufferId, …);
    } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // Subsequent data will conform to new format.
        // Can ignore if using getOutputFormat(outputBufferId)
        outputFormat = codec.getOutputFormat(); // option B
    }
}
codec.stop();
codec.release();

这是官方文档的例子, 这样写出看起来还挺简单的, 但是当考虑到多线程, 流程上的控制,比如流结束终止循环等,就比较复杂了。

不过这样官方提供的示例还是给人一种标准的感觉,不像看项目中的代码,会有很多小问号,比如为什么会用 for(;;), 一次输入不需要循环多次输出吗(这个问题,可以通过外层这个大循环解决),输入数据的填入也受数据源的影响,怎么处理的。

所以还是有必要多看官方示例,然后多问为什么,想想它设计背后的依据。

明天: 每天一点点音视频_MediaCodec的C位出道

每天一点点音视频_MediaCodec创建初始化

首先要来看一下 MediaCodec 的创建需要解决的问题:

  1. MediaCodec 分为编码器和解码器
  2. 编解码特定的音视频格式,需要特定的编码器,可能有多个
  3. 对于某中编码格式, 不同的编码器支持的特性可能不同

针对第一个问题:

MediaCodec 提供了区分编解码的方法: createDecoder/EncoderByType(java.lang.String), MediaCodecList#findEncoder/DecoderForFormat

针对第二个问题,第三个:

MediaCodecList#findEncoder/DecoderForFormat 会根据格式去查找最适合的, 我们可以对 MediaFormat 添加特定的需求的属性查找特定的编解码器。

明天: 每天一点点音视频_MediaCodec使用示例

每天一点点音视频_MediaCodec状态转换

状态, 对于我来说总是比较有兴趣的, 为什么呢?

因为, 状态意味着复杂, 这样说好像我很喜欢复杂一样,但实际上,我只喜欢稍微复杂。我讨厌简单,重复。更准确点, 我喜欢那中对复杂的掌控感, 而如果一个类牵扯到了状态, 它的复杂度就比较大了, 使用状态机, 可以一个复杂的问题空间,分成小的问题空间,再这个问题空间中,它限制了只能进行某些操作,很明确,而且还限制了它只能转向某几个状态。

对于一个状态机, 它的难度编成了,要记住,有哪些状态,状态的转换,再某个状态支持那些操作。

下面, 对 MediaCodec 这个状态机做个说明:

  1. 分两个状态组, 停止的 和 执行中的, 两组里的状态,都可以调用 release 进入 释放状态, 进入释放状态后, 这个对象就不能用了, 要想重新使用需要 new 个新的。

  2. 一个刚创建的对象,处于未初始化的状态,下一步只能执行 configure, 然后进入配置好的状态,这时候就可以随时开始,进入运行态了。

  3. 在除释放状态, 其他状态都可以执行重置,进入为初始化的状态,从而可以重新使用该对象

  4. 在运行态停止的话,会进入未初始的状态

  5. 在运行态出错的话,会进入错误状态

  6. 运行状态包括三个状态: Flushed, Running, End of Stream, (为了明确,就不翻译)

  7. 当进入运行态时, 首先是 Flushed, 这时候,还没有往里面输入数据, 当然也没有输出数据, 也就是说输入输出缓冲区都是空的

  8. 当在 Flushed 状态, 在填入数据后就进入了 Running 状态

  9. 这时候, 可以 一直填入数据, 或者填入结束标志,结束这个流,从而进入 End Of Stream 状态

  10. 在 End of Stream, 也就是一个流结束了。

超越设计:

  1. 为什么会有个 Released 状态, 它跟 Uninitialized 状态很像, 其他状态都可以转到它们?

    应该是在创建 MediaCodec 对象是分配比较大的数据空间, 为了重复利用, 有了 Uninitialized, 这样在做完一个任务, 如果还有其他的任务,可以重复使用,只需要 reset 以下。而 release 手动释放占用的空间。

  2. 既然创建的时候会配置,为什么还需要 configure?

    这还是为了复用, 创建的时候,确定了是解码器还是编码器, 是那种编码器,那种解码器。但是配置的时候是配置数据类型, 和编码器的属性,如果都放在创建的时候配置,那重用的时候就不能改变了编码格式和编码器属性了。

  3. 没有 End Of Stream 状态可以吗?

    好像也可以, 但是让我们想一下, 当我们把最后一帧送入 MediaCodec, 它并不知道有没有结束, 如果它的实现机制并不是进去一帧处理一帧,而是满了才会处理,那这时候如果没有满就不会处理了,就需要我们告诉它没有了,不满也要处理。

明天: 每天一点点音视频_MediaCodec创建初始化

每天一点点音视频_MediaCodec

MediaCodec 的模型如下:

MediaCodec作为一个模块, 通过输入队列和输出队列来与外界传递数据。

它的使用方式类似于使用一排电烤箱, 使用的时候,我们找到一个空的,然后填入生的事物,然后再查找熟了的拿出。

MediaCodec 分为编码器和解码器, 编码器输入原始数据输出压缩后的数据, 解码器输入压缩后的数据输出原始数据。

原始数据又分为原始音频数据和原始视频数据, 原始数据和压缩数据都可以使用 ByteBuffer 保存, 但是原始视频数据还可以使用 Surface 来保存, 使用 Surface 效率会更高, 因为 MediaCodec 是本地实现的, Surface 直接使用了本地数据没有复制到 ByteBuffer 的操作。通常不能直接访问 Surface 的数据,但是可以使用 ImageReader 访问 Surface。这样效率也会很高,因为写本地缓冲可以直接映射到 ByteBuffer, 也就是说, ByteBuffer 不用复制本地缓冲到 Java 层, 这是 ByteBuffer 的一种使用模式(直接模式, 使用本地数据)。当使用 ByteBuffer 模式的时候, 还可以使用 Image API 来访问数据(信息量太大了)。

压缩的视频数据,一个缓冲区,一般是一个压缩的视频帧。

压缩的音频数据,一个缓冲区,一般是一个单独的访问单元(也就是一个编码的音频片,通常包含几毫秒的音频数据,至于多少于音频类型有关)。但是也可以包含多个访问单元,这种情况下Buffer的开始和结束都是在访问单元的边界,除非他们标记了 BUFFER_FLAG_PARTIAL_FRAME.

原始音频数据包含 PCM 音频数据, 它是每一个声道的采样数据,每一次的采样按声道的顺序排列为一帧。每一个 PCM 的采样格式要么是16位的符号整数,要么是一个float。编码的时候可以通过configure配置采样数据的格式。可以通过 MediaFormat 获取采样的格式。

原始视频数据的布局格式有几种格式,可以通过 MediaFormat#KEY_COLOR_FORMAT 来确定。 视频编码器可能支持三种类型的格式:

  1. CodecCapabilities#COLOR_FormatSurface 可以被用来输入输出 Surface
  2. 灵活的 YUV 缓存, 比如 CodecCapabilities#COLOR_FormatYUV420Flexible, 既可以使用Surface访问, 也可以使用 ByteBuffer 模式,使用 Image 访问
  3. 其他的, 只支持 ByteBuffer 模式

就这样吧, 今天内容比较无聊,信息量有点大。现在只要有个大的印象就行, 就是那个图, 分为输入输出, 输入一个魂冲队列,输出一个缓冲队列, 对于输入队列,先请求空缓冲,然后填充数据,填充完再放回。对于输出缓冲队列,先请求一个,把数据拿走,然后归还。

明天: 每天一点点音视频_MediaCodec状态转换