每天一点点_音视频_OpenGL_着色器管道

着色器语言(GLSL, GL shader language)

它是一种类 C 的语言,编译后运行在 GPU 上的每个单元上

着色器是一段 GLSL 代码, 有输入有输出, 多个着色器的输出和输入相连,就形成了着色器管线。

我们一般都会写两个着色器: 顶点着色器 和 片着色器

顶点着色器的输入是之前提到过的上传到 GPU 的定点数据里的一个点的坐标,顶点着色器就是负责将这些坐标进行变换的。

片着色器的, 输出则是那个顶点坐标对应的颜色

以上两者是固定的,另外 GLSL 提供了让我们自定义变量来串起着色器的功能,方法就是在两个着色器里定义类型和名称相同的变量。在前一个着色器作为输出在后一个作为输入。

另外还有一种方式,定义 uniform 的变量,它相对于是全局变量,在所有着色器里都可见,在应用里向它传值时,所有着色器都可以得到这个值。

每天一点点_音视频_OpenGL_光照

每天一点点_音视频_OpenGL_基本步骤

在 Android 平台上, 我们使用 OpenGL 的步骤如下:

  1. 首先确认使用哪个版本,因为不同的版本有不同的特性, 一般我们使用 2.0 以上, 因为此版本开始支持管道,可以更灵活方便的操作图像数据。

  2. 然后确认是用 Java API 还是使用 NDK

  3. 在 AndroidManifest 里声明需要的版本(因为某些设备不支持某些版本,这样声明,会在 Google 市场进行检验,如果手机支持需要的版本,才会支持下载)

  4. 同样,如果使用了纹理压缩, 需要在 AndroidManifest 里声明纹理压缩的格式

  5. 使用 SurfaceView, TextureView, 或者其他能提供 Surface 的组建构建 OpenGL 的上下文,至此,以后的步骤基本就是通用的 OpenGL 的操作了。

接下来是通用的 OpenGL 的操作步骤:

  1. 将 Bitmap 加载到纹理
  2. 绘制纹理

这里绘制纹理包含了以下操作:

  1. 上传顶点组,相当于构造一个模型
  2. 构造 Program 并加载着色器代码并编译
  3. 绘制

以上的步骤是适用于对视频处理的时候

但是让我们在来考虑以下, OpenGL API 是一套跟 GPU 对话的接口。GPU 有自己的内存, 它要处理我们的任务, 需要我们给它数据, 这些数据包括 纹理数据, 纹理数据就是一个图片只不过对 GPU 来说它们叫纹理。另一块数据是模型数据,模型数据实际是一些点的数据。另外,除了这些数据外,还有着色器代码,什么叫着色器代码呢,这部分代码是要在 GPU 执行的。

明天: 每天一点点_音视频_OpenGL_着色器管道

每天一点点_音视频_OpenGL_概述

我之前写过一个关于入门 OpenGL 的系列文章

那时候,我也是刚开始学,也没有用过,现在,我有了一定的 OpenGL 的开发实践经验,应该可以写一个进阶的 OpenGL 教程了。

OpenGL 是什么

OpenGL 是一套 API, 它定义了一套抽象的接口,从而给我们提供了通过 GPU 来实现图像处理绘制的能力。作为抽象的接口,它独立于平台,独立于语言, 这意味着,不同的语言,可能每个接口的样子不太一样,毕竟语法不尽相同,但是基本的名字,参数,都是类似的。这样,在一个语言一个平台会了 OpenGL, 在其他语言其他平台学习的成本就很少了。

OpenGL ES 是什么

OpenGL ES 是 OpenGL 的一个子集, 是针对 手机, 平板等嵌入式设备设计的。我们在之后的学习都是基于 OpenGL ES 的

EGL 是什么

EGL 是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system,在 Android 平台就是 Android 提供 EGL 的实现)之间的一个中间接口层,它主要由系统制造商实现

主要提供了下面三个概念的实现:

  1. Display, 它代表了一个显示器
  2. Surface, 带代表了 画布
  3. EGLContext, 保存一些状态信息

明天: 每天一点点_音视频_OpenGL_基本步骤

每天一点点_音视频_MP4_视频数据BOX1

之前的文章,想要读取 moov box, 发现我要读取的 mp4 的 moov 在最后, 而不是紧跟 ftype box。

一般情况下(限于篇幅,本文只讲解常见的MP4文件结构),“moov”中会包含1个“mvhd”和若干个“trak”。其中“mvhd”为header box,一般作为“moov”的第一个子box出现(对于其他container box来说,header box都应作为首个子box出现)。“trak”包含了一个track的相关信息,是一个container box。

我修改了代码,使得可以打印出 moov box 的内部的 box, 结果是这样的:

mvhd: 108
trak: 7209
trak: 8122
udta: 834

还多了一个 udta, 有两个 trak box, 应该就是音频和视频的。

接下来不打算对每一个 Box 的结构介绍了,完全可以用代码来表现出来。

附上参考的文章的链接

明天: 每天一点点_音视频_MP4_学习总结

每天一点点_音视频_MP4_视频数据BOX

Movie Box (moov)

该box包含了文件媒体的metadata信息,“moov”是一个container box,具体内容信息由子box诠释。同File Type Box一样,该box有且只有一个,且只被包含在文件层。一般情况下,“moov”会紧随“ftyp”出现。

以下是读取下一个bax的大小和类型的代码:

1
2
3
4
5
6
7

it.read(buf)
val moovBoxSize = buf.toInt()
Log.e("JIN", "size = $moovBoxSize")
it.read(buf)
Log.e("JIN", "type = ${buf.toCharString()}")

然而类型却不是 moov 而是 free, 上面说了,一般情况下 ftype 后面是 moov。

那就跳过当前的box读取下一个box吧。

我又读取了下一个box, 这个 box 是 mdat, 看来,我要读取完整个文件来看看到底这个 mp4 文件有没有 moov 的 box 了。

重构了代码:

1
2
3
4
5
6
7

f.inputStream().also {
while (it.available() > 0) {
val box = Box(it, buf)
Log.e("JIN", box.toString())
}
}

Box 对象的代码:

1
2
3
4
5
6
7
8
9
10
11
12

class Box(val size: Int, val type: String) {
constructor(inStream: InputStream, byterArray: ByteArray)
: this(inStream.read(byterArray).let { byterArray.toInt() },
inStream.read(byterArray).let { byterArray.toCharString() }) {
inStream.skip(size - 8L)
}

override fun toString(): String {
return "$type: $size"
}
}

这里, 只读取了 size 和 type, 然后跳过了其他数据。

按照大端的方式转化4个字节为 Int 的方法:

1
2
3
4
5
6
7

fun ByteArray.toInt(): Int {
val a = this[0].toUByte().toInt() * Math.pow(2.0, 8.0 * 3)
val b = this[1].toUByte().toInt() * Math.pow(2.0, 8.0 * 2)
val c = this[2].toUByte().toInt() * Math.pow(2.0, 8.0)
return (this[3].toUByte().toInt() + a + b + c).toInt()
}

这里, 除了点问题, 每个字节,要转化成无符号数,再转成 Int, 之前没有转, 因为数小没出问题。

这样打印出了这个 mp4 的所有文件级别的 box:

ftyp: 32
free: 8
mdat: 11703979
moov: 16281

这就有意思了,下一步该绘制 UI 了

明天: 每天一点点_音视频_MP4_视频数据BOX1

每天一点点_音视频_MP4_BOX

首先, Box 里的字节序为网络字节序,也就是大端字节序(Big-Endian),简单的说,就是一个32位的4字节整数存储方式为高位字节在内存的低端。

Box 由 header 和 body 组成,其中 header 统一指明 box 的大小和类型, body 根据类型有不同的意义和格式。

标准的box开头的4个字节(32位)为box size,该大小包括box header和box body整个box的大小,这样我们就可以在文件中定位各个box。如果size为1,则表示这个box的大小为large size,真正的size值要在largesize域上得到。(实际上只有“mdat”类型的box才有可能用到large size。)如果size为0,表示该box为文件的最后一个box,文件结尾即为该box结尾。(同样只存在于“mdat”类型的box中。

size后面紧跟的32位为box type,一般是4个字符,如“ftyp”、“moov”等,这些box type都是已经预定义好的,分别表示固定的意义。如果是“uuid”,表示该box为用户扩展类型。如果box type是未定义的,应该将其忽略。

File Type Box(ftyp)

该box有且只有1个,并且只能被包含在文件层,而不能被其他box包含。该box应该被放在文件的最开始,指示该MP4文件应用的相关信息。

“ftyp” 的body依次包括1个32位的major brand(4个字符),1个32位的minor version(整数)和1个以32位(4个字符)为单位元素的数组compatible brands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val buf = ByteArray(4)
f.inputStream().also {
it.read(buf)
val boxSize = buf.toInt()
Log.e("JIN", "size = $boxSize")
it.read(buf)
Log.e("JIN", "type = ${buf.toCharString()}")
it.read(buf)
Log.e("JIN", "major brand = ${buf.toCharString()}")
it.read(buf)
Log.e("JIN", "minor version = ${buf.toInt()}")
val compatableBrandsArraySize = boxSize - 4 * 4 // 以上的四个 四字节
(0 until (compatableBrandsArraySize / 4)).forEach {i ->
it.read(buf)
Log.e("JIN", "minor version = ${buf.toCharString()}")
}
}

代码写得很顺利,果然看着说明书一步一步弄是最简单的事情呀。

我查看了一个 MP4 文件, 它的 major brand 是 isom, 它兼容的 brand 是 isom, iso2, avc1, mp41

看这个意思, mp4 是个封装标准, 但是还有很多子规范。

明天: 每天一点点_音视频_MP4_视频数据BOX

每天一点点_音视频_MP4_概述

Box

MP4文件中的所有数据都装在box(QuickTime中为atom)中,也就是说MP4文件由若干个box组成,每个box有类型和长度,可以将box理解为一个数据对象块。

box中可以包含另一个box,这种box称为container box。

一个MP4文件首先会有且只有一个“ftyp”类型的box,作为MP4格式的标志并包含关于文件的一些信息;

之后会有且只有一个“moov”类型的box(Movie Box),它是一种container box,子box包含了媒体的metadata信息

MP4文件的媒体数据包含在“mdat”类型的box(Midia Data Box)中,该类型的box也是container box,可以有多个,也可以没有(当媒体数据全部引用其他文件时),媒体数据的结构由metadata进行描述。

track

表示一些 sample 的集合, 对于媒体数据来说, track 表示一个视频或音频的序列

hint track

是个特殊的 track, 不包含媒体数据, 而是包含一些将其他数据 track 打包成流媒体的信息指示

sample

对于非 hint track,也就是包含媒体数据的 track, video track 即为一个或者一组视频侦, audio track 为一段连续的压缩音频。 对于 hint track, sample 定义一个或多个流媒体包的格式

sample table

指明 sample 时序和物理布局的表

chunk

一个 track 中几个 sample 组成的单元

基本的大体结构


前段时间我哥住我这工作, 还带来两个孩子, 虽然对原来的习惯有影响,但是基本的习惯都还艰难的保持着。然后他们最近走了,几个坚持的习惯反而崩塌了,奇奇怪怪

明天: 每天一点点_音视频_MP4_BOX

每天一点点_音视频_MP4_结构查看器

MP4 的标准说明无法看,我就打算参考一下网上别人的文章了,但是刚一看就看到了下面这个图片,萌生了要做么个软件的念头,这个年头是如此强烈,让我忽视掉当前我的处境, 不得不开个新坑。

或者这样子的

写这么个软件,必然对 MP4 文件格式有透彻的了解, 而且一款这种 UI 比较好看的 Android 软件, 必然填补了市场空白。关键我有中感觉,我能把它做完,做好。

风险也是有的

  1. 兼容性,可能这种结构兼容性会比较好,没有的结构不显示,不过还是要有个提示
  2. 图形的现实可能会比较费劲,不过好像也不太难,就是 View 的嵌套嘛。
  3. 要做到对结构的检查,还是需要标准文档的

大的架构是这样的:

  1. 每种文件格式有特定的格式,有一层抽象
  2. UI 层有一层结构的抽象
  3. 实现从文件结构的抽象到现实结构的抽象的转换

额,一不小心就成了所有文件,一开始还是要现实 MP4 文件的结构

已经上传 GIT: MediaFileStructureViewer

明天: 每天一点点_音视频_MP4_概述

每天一点点音视频_MP4_了解

MP4, 我们通常了解到的是,它是一种视频文件格式,这种视频比较流行, 甚至还有个播放视频的设备就叫 MP4。

但是, 严格来说, MP4 是叫 MPEG-4 Part 14, 是一种数字多媒体容器格式。所谓容器的格式,就是它封装保存数据, 里面的数据可能是视频数据, 音频数据, 字幕数据, 图片等。它支持通过网络进行流传输,也就是说,不用完全下载下来就可以播放。

之前我们了解过 解包,封包 的功能,可以使用 Android 上的 MediaExtractor/MediaMuxer, 也可以使用 ffmpeg 来做。

MPEG-4 是啥, 为啥还是 Part 14?

MPEG-4 是对音视频的压缩方法, 包括了一些列的技术, 既然有 4, 那就会有 MPEG-1, MPEG-2, 反正就是这么更新来的。

它分成,几个部分,每个部分可以说是一个功能,比如 part3 是音频的压缩格式的说明, part10 是视频编码器的说明, part14 是把压缩后的数据打包处理的说明。

另外需要说明的是, MPEG-4 part14 还有个编号 ISO/IEC 14496-14:2003, 这个编号是 ISO 认证, 给分配的。

我想下载完整的描述,竟然还要付费购买,这是我没想到的。

以上内容来自 wiki


MPEG-4

MPEG-4 Part14

每天一点点音视频_bmp_实践

现在依然会有那种错误的冲动,就是学完一个只是以后,就觉得明白了,很简单,这么简单,以后都记住了。然而事实上,这种明白的感觉也没有那么明白,另外,就算当时明白了,以后也会忘,甚至能忘得一干二净。

所以,我也有意识的在学完了一个东西以后,用一下,检测一下。

最近学西的 BMP 格式, 这种只是的检测方式,手写一个读取或者写入 BMP 的程序就很好。

以下是一个 4*2, 每个像素 32 位, 带有透明度的 BMP 的示意图。DIP 头的版本是 BITMAPV4HEADER。

我打算生成一个这样的 BMP 文件,数据都来自 wiki

第一步确定 DIP 头的版本

使用 BITMAPV4HEADER

第二步确定像素格式

使用 32 位, 压缩方法为 BI_BITFIELDS, 使用掩码来确定像素的格式

掩码名称 掩码
红色 00 00 FF 00
绿色 00 FF 00 00
蓝色 FF 00 00 00
透明 00 00 00 FF

第三步确定像素数据

位置 颜色数据
(0,0) FF0000FF
(0,1) 00FF00FF
(0,2) 0000FFFF
(0,3) FFFFFFFF
(1,0) FF00007F
(1,1) 00FF007F
(1,2) 0000FF7F
(1,3) FFFFFF7F

第四步写入文件头数据

第五步写入 DIP 数据

第六步写入像素数据

写不完了,以后补 :)

明天: 每天一点点音视频_MP4_了解