## 音視頻學習 (八) 掌握視頻基礎知識并使用 OpenGL ES 2.0 渲染 YUV 數據
## 簡介
上一篇文章我們學習了音頻的基礎知識和音頻的渲染之后,該篇我們學習視頻的知識,與上一篇學習方式一樣,基礎 + demo ,主打渲染,采集跟編碼我們后面學習播放器和錄屏在來研究。
## 視頻的基礎知識
### 圖像的物理現象
做過 Camera 采集或者做過幀動畫其實應該知道,視頻是由一幅幅圖像或者說一幀幀 YUV 數據組成,所以要學習視頻還得從圖像開始學習。
我們回顧一下,應該是初中的時候做過一個三棱鏡實驗,內容是如何利用三棱鏡將太陽光分解成彩色的光帶?第一個做這個實驗者是**牛頓**,各色光因其所形成的折射角不同而彼此分離,就像彩虹一樣,所以白光能夠分解成多種色彩的光。后來人們通過實驗證明,紅綠藍三種色光無法被分解,故稱為三原色光,等量的三原色光相加會變為白光,即白光中含有等量的紅光(R),綠光(G),藍光(B)。
在日常生活中,由于光的反射,我們才能看到各類物體的輪廓和顏色。但是如果將這個理論應用到手機中,那么該結論還成立嗎?答案是否定的,因為在黑暗中我們也可以看到手機屏幕中的內容,實際上人眼能看到手機屏幕上的內容的原理如下。
假設一部手機屏幕的分辨率是 1920 \* 1080 說明水平方向有 1080 個像素點,垂直方向有 1920 個像素點,所以整個屏幕就有 1920 \* 1080 個像素點(這也是分辨率的含義)。每個像素點都由三個子像素點組成,如下圖所示,這些密密麻麻的子像素點在圖像放大或者在顯微鏡下可以看得一清二楚。當要顯示某篇文字或者某幅圖像時,就會把這幅圖像的每一個像素點的 RGB 通道分別對應的屏幕位置上的子像素點繪制到屏幕上,從而顯示整個圖像。
所以在黑暗的環境下也能看到手機屏幕上的內容,是因為手機屏幕是自發光的,而不是通過光的反射才被人們看到的。

[圖片來源](https://glumes.com/post/ffmpeg/understand-yuv-format/)
### 圖像的數值表示
#### RGB 表示方式
通過上一小節我們清楚的知道任何一個圖像都是由 RGB 組成,那么一個像素點的 RGB 該如何表示呢?音頻里面的每一個采樣 (sample) 均使用 16 bit 來表示,那么像素里面的子像素又該如何表示呢?通常的表示方式有以下幾種。
* 浮點表示: 取值范圍在 0.0 ~ 1.0 之間,比如在 OpenGL ES 中對每一個子像素點的表示使用的就是這種方式。
* 整數表示: 取值范圍為 0 ~ 255 或者 00 ~ FF , 8 個 bit 表示一個子像素點,32 個 bit 表示一個像素,這就是類似某些平臺上表示圖像格式的 RGBA\_8888 數據格式。比如 Android 平臺上的 RGB\_565 的表示方法為 16 個 bit 模式表示一個像素, R 用 5 個 bit , G 用 6 個 bit, B 用 5 個 bit 來表示。
對于一幅圖像,一般使用整數表示方法進行描述,比如計算一張 1920 \* 1080 的 RGB\_8888 的圖像大小,可采用如下計算方式:
~~~
1920 * 1080 * 4 / 1024 / 1024 ≈ 7.910 MB
復制代碼
~~~
這也是 Bitmap 在內存中所占用的大小,所以每一張圖像的裸數據都是很大的。對于圖像的裸數據來說,直接來網絡中進行傳輸也是不大可能的,所以就有了圖像的壓縮格式,比如我之前開源過一個基于[JPEG 壓縮](https://github.com/yangkun19921001/LIBJPEG_SAMPLE):JPEG 是靜態圖像壓縮標準,由 ISO 制定。 JPEG 圖像壓縮算法在提供良好的壓縮性能的同時,具有較好的重建質量。這種算法被廣泛應用于圖像處理領域,當然它也是一種有損壓縮。在很多網站如淘寶上使用的都是這種壓縮之后的圖像,但是,這種壓縮不能直接應用于視頻壓縮,因為對于視頻來講,還有一個時域上的因素需要考慮,也就是說不僅僅要考慮幀內編碼,還要考慮幀間編碼。視頻采用的是更加成熟的算法,關于視頻壓縮算法的相關內容我們會在后面進行介紹。
#### YUV 表示方式
對于視頻幀的裸數據表示,其實更多的是 YUV 數據格式的表示, YUV 主要應用于優化彩色視頻信號的傳輸,使其向后兼容老式黑白電視。在 RGB 視頻信號傳輸相比,它最大的優點在于只需要占用極少的頻寬(RGB 要求三個獨立的視頻信號同時傳輸)。其中 Y 表示明亮度,而 “U”,"V" 表示的則是色度值,它們的作用是描述影像的色彩及飽和度,用于指定像素的顏色。“亮度” 是透過 RGB 輸入信號來建立的,方法時將 RGB 信號的特定部分疊加到一起。“色度” 則定義了顏色的兩個方面 - 色調與飽和度,分別用 Cr 和 Cb 來表示。其中,Cr 反應了 RGB 輸入信號紅色部分與 RGB 信號亮度值之間的差異,而 Cb 反映的則是 RGB 輸入信號藍色部分與 RGB 信號亮度值之間的差異。
之所以采用 YUV 色彩空間,是因為它的亮度信號 Y 和色度信號 U、V 是分離的。如果只有 Y 信號分量而沒有 U 、V 分量,那么這樣表示的圖像就是黑白灰圖像。彩色電視采用 YUV 空間正是為了用亮度信號 Y 解決彩色電視機與黑白電視機的兼容問題,使黑白電視機也能接收彩色電視信號,最常用的表示形式是 Y、U、V 都使用 8 字節來表示,所以取值范圍是 0 ~ 255 。 在廣播電視系統中不傳輸很低和很高的數值,實際上是為了防止信號變動造成過載, Y 的取值范圍都是 16 ~ 235 ,UV 的取值范圍都是 16 ~ 240。
YUV 最常用的采樣格式是 4:2:0 , 4:2:0 并不意味著只有 Y 、Cb 而沒有 Cr 分量。它指的是對每行掃描線來說,只有一種色度分量是以 2:1 的抽樣率來存儲的。相鄰的掃描行存儲著不同的色度分量,也就是說,如果某一行是 4:2:0,那么下一行就是 4:0:2,在下一行是 4:2:0,以此類推。對于每個色度分量來說,水平方向和豎直方向的抽象率都是 2:1,所以可以說色度的抽樣率是 4:1。對非壓縮的 8 bit 量化的視頻來說,8\*4 的一張圖片需要占用 48 byte 內存。
相較于 RGB ,我們可以計算一幀為 1920 \* 1080 的視頻幀,用 YUV420P 的格式來表示,其數據量的大小如下:
~~~
(1920 * 1080 * 1 + 1920 * 1080 * 0.5 ) / 1024 /1024 ≈ 2.966MB
復制代碼
~~~
如果 fps(1 s 的視頻幀數量)是 25 ,按照 5 分鐘的一個短視頻來計算,那么這個短視頻用 YUV420P 的數據格式來表示的話,其數據量的大小就是 :
~~~
2.966MB * 25fps * 5min * 60s / 1024 ≈ 21GB
復制代碼
~~~
可以看到僅僅 5 分鐘的視頻數據量就能達到 21 G, 像抖音,快手這樣短視頻領域的代表這樣的話還不卡死,那么如何對短視頻進行存儲以及流媒體播放呢?答案肯定是需要進行視頻編碼,后面會介紹視頻編碼的內容。
如果對 YUV 采樣或者存儲不明白的可以看這篇文章:[音視頻基礎知識---像素格式YUV](https://zhuanlan.zhihu.com/p/68532444)
#### YUV 和 RGB 的轉化
前面已經講過,凡是渲染到屏幕上的文字、圖片、或者其它,都需要轉為 RGB 的表示形式,那么 YUV 的表示形式和 RGB 的表示形式之間是如何進行轉換的呢?可以參考該篇文章[YUV RGB 轉換算法](http://blog.shenyuanluo.com/ColorConverter.html), 相互轉換 C++ 代碼可以參考[地址](https://github.com/shenyuanluo/SYKit/tree/master/SYKit/SYConverter)
### 視頻的編碼方式
#### 視頻編碼
還記得上一篇文章我們學習的音頻編碼方式嗎?音頻的編碼主要是去除冗余信息,從而實現數據量的壓縮。那么對于視頻壓縮,又該從哪幾個方面來對數據進行壓縮呢?其實與之前提到的音頻編碼類似,視頻壓縮也是通過去除冗余信息來進行壓縮的。相較于音頻數據,視頻數據有極強的相關性,也就是說有大量的冗余信息,包括空間上的冗余信息和時間上的冗余信息,具體包括以下幾個部分。
* 運動補償: 運動補償是通過先前的局部圖像來預測,,補償當前的局部圖像,它是減少幀序列冗余信息的有效方法。
* 運動表示: 不同區域的圖像需要使用不同的運動矢量來描述運動信息。
* 運動估計: 運動估計是從視頻序列中抽取運動信息的一整套技術。
使用幀內編碼技術可以去除空間上的冗余信息。
大家還記得之前提到的圖像編碼[JPEG](https://github.com/libjpeg-turbo/libjpeg-turbo)嗎?對于視頻, ISO 同樣也制定了標準: Motion JPEG 即 MPEG ,MPEG 算法是適用于動態視頻的壓縮算法,它除了對單幅圖像進行編碼外,還利用圖像序列中的相關原則去除冗余,這樣可以大大提高視頻的壓縮比,截至目前,MPEG 的版本一直在不斷更新中,主要包括這樣幾個版本: Mpeg1(用于 VCD)、Mpeg2(用于 DVD)、Mpeg4 AVC(現在流媒體使用最多的就是它了)。
想比較 ISO 指定的 MPEG 的視頻壓縮標準,ITU-T 指定的 H.261、H.262、H.263、H.264 一系列視頻編碼標準是一套單獨的體系。其中,H.264 集中了以往標準的所有優點,并吸取了以往標準的經驗,采樣的是簡潔設計,這使得它比 Mpeg4 更容易推廣。現在使用最多的就是 H.264 標準, H.264 創造了多參考幀、多塊類型、整數變換、幀內預測等新的壓縮技術,使用了更精準的分像素運動矢量(1/4、1/8) 和新一代的環路濾波器,這使得壓縮性能得到大大提高,系統也變得更加完善。
#### 編碼概念
視頻編碼中,每幀都代表著一幅靜止的圖像。而在進行實際壓縮時,會采取各種算法以減少數據的容量,其中 IPB 幀就是最常見的一種。
##### IPB 幀
* I 幀: 表示關鍵幀,你可以理解為這一幀畫面的完整保留,解碼時只需要本幀數據就可以完成(包含完整畫面)。
* P 幀: 表示的是當前 P 幀與上一幀( I 幀或者 P幀)的差別,解碼時需要用之前緩存的畫面疊加上本幀定義的差別生成最終畫面。(也就是差別幀, P 幀沒有完整畫面數據,只有與前一幀的畫面差別的數據。)
* B 幀: 表示雙向差別幀,也就是 B 幀記錄的是當前幀與前后幀(前一個 I 幀或 P 幀和后面的 P 幀)的差別(具體比較復雜,有 4 種情況), 換言之,要解碼 B 幀,不僅要取得之前的緩存畫面,還要解碼之后的畫面,通過前后畫面數據與本幀數據的疊加取得最終的畫面。B 幀壓縮率高,但是解碼時 CPU 會比較吃力。
##### IDR 幀與 I 幀的理解
在 H264 的概念中有一個幀稱為 IDR 幀,那么 IDR 幀與 I 幀的區別是什么呢 ? 首先要看下 IDR 的英文全稱 instantaneous decoding refresh picture , 因為 H264 采用了多幀預測,所以 I 幀之后的 P 幀有可能會參考 I 幀之前的幀,這就使得在隨機訪問的時候不能以找到 I 幀作為參考條件,因為即使找到 I 幀,I 幀之后的幀還是有可能解析不出來,而 IDR 幀就是一種特殊的 I 幀,即這一幀之后的所有參考幀只會參考到這個 IDR 幀,而不會再參考前面的幀。在解碼器中,一旦收到第一個 IDR 幀,就會立即清理參考幀緩沖區,并將 IDR 幀作為被參考的幀。
##### PTS 與 DTS
DTS 主要用視頻的解碼,全稱為(Decoding Time Stamp), PTS 主要用于解碼階段進行視頻的同步和輸出, 全稱為 (Presentation Time Stamp) 。在沒有 B 幀的情況下, DTS 和 PTS 的輸出順序是一樣的。因為 B 幀打亂了解碼和顯示的順序,所以一旦存在 B 幀, PTS 與 DTS 勢必就會不同。在大多數編解碼標準(H.264 或者 HEVC) 中,編碼順序和輸入順序并不一致,于是才會需要 PTS 和 DTS 這兩種不同的時間戳。
##### GOP 的概念
兩個 I 幀之間形成的一組圖片,就是 GOP (Group Of Picture) 的概念。通常在為編碼器設置參數的時候,必須要設置 gop\_size 的值,其代表的是兩個 I 幀之間的幀數目。一個 GOP 中容量最大的幀就是 I 幀,所以相對來講,gop\_size 設置得越大,整個畫面的質量就會越好,但是在解碼端必須從接收到的第一個 I 幀開始才可以正確的解碼出原始圖像,否則會無法正確解碼,在提高視頻質量的技巧中,還有個技巧是多使用 B 幀,一般來說,I 的壓縮率是 7 (與 JPG 差不多),P 是 20 ,B 可以達到 50 ,可見使用 B 幀能節省大量空間,節省出來的空間可以用來更多地保存 I 幀,這樣就能在相同的碼率下提供更好的畫質,所以我們要根據不同的業務場景,適當地設置 gop\_size 的大小,以得到更高質量的視頻。
結合下圖,希望可以幫組大家更好的理解 DTS 和 PTS 的概念。

## 視頻渲染
### OpenGL ES
#### 實現效果

#### 介紹
[OpenGL](https://www.khronos.org/opengl/)(Open Graphics Lib) 定義了一個跨編程語言、跨平臺編程的專業圖形程序接口。可用于二維或三維圖像的處理與渲染,它是一個功能強大、調用方便的底層圖形庫。對于嵌入式的設備,其提供了[OpenGL ES](https://developer.android.com/guide/topics/graphics/opengl?hl=zh-cN)(OpenGL for Embedded System) 版本,該版本是針對手機、Pad 等嵌入式設備而設計的,是 OpenGL 的一個子集。到目前為止,OpenGL ES 已經經歷過很多版本的迭代與更新,到目前為止運用最廣泛的還是[OpenGL ES](https://www.khronos.org/opengles/)2.0 版本。我們接下來所實現的 Demo 就是基于 OpenGL ES 2.0 接口進行編程并實現圖像的渲染。
由于 OpenGL ES 是基于跨平臺的設計,所以在每個平臺上都要有它的具體實現,既要提供 OpenGL ES 的上下文環境以及窗口的管理。在 OpenGL 的設計中,OpenGL 是不負責管理窗口的。那么在 Android 平臺上其實是使用 EGL 提供本地平臺對 OpenGL ES 的實現。
#### 使用
要在 Android 平臺下使用 OpenGL ES , 第一種方式是直接使用 GLSurfaceView ,通過這種方式使用 OpenGL ES 比較簡單,因為不需要開發者搭建 OpenGL ES 的上下文環境,以及創建 OpenGL ES 的顯示設備。但是凡事都有兩面,有好處也有壞處,使用 GLSurfaceView 不夠靈活,很多真正的 OpenGL ES 的核心用法(比如共享上下文來達到多線程使用 EGL 的 API 來搭建的,并且是基于 C++ 的環境搭建的。因為如果僅僅在 Java 層編寫 ,那么對于普通的應用也許可行,但是對于要進行解碼或者使用第三方庫的場景(比如人臉識別),則需要到 C++ 層來實施。處于效率和性能的考慮,這里的架構將直接使用 Native 層的 EGL 搭建一個 OpenGL ES 的開發環境。要想在 Native 層使用 EGL ,那么就必須在 CmakeLists.txt 中添加 EGL 庫(可以參考如下提供的 CMakeLists 文件配置),并在使用該庫的 C++ 文件中引入對應的頭文件,需要引如的頭文件地址如下:
~~~
//1. 在開發中如果要使用 EGL 需要在 CMakeLists.txt 中添加 EGL 庫,并指定頭文件
//使用 EGL 需要添加的頭文件
#include <EGL/egl.h>
#include <EGL/eglext.h>
//2. 使用 OpenGL ES 2.0 也需要在 CMakeLists.txt 中添加 GLESv2 庫,并指定頭文件
//使用 OpenGL ES 2.0 需要添加的頭文件
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
復制代碼
~~~
CMakeLists 文件配置:
~~~
cmake_minimum_required(VERSION 3.4.1)
#音頻渲染
set(OpenSL ${CMAKE_SOURCE_DIR}/opensl)
#視頻渲染
set(OpenGL ${CMAKE_SOURCE_DIR}/gles)
#批量添加自己編寫的 cpp 文件,不要把 *.h 加入進來了
file(GLOB ALL_CPP ${OpenSL}/*.cpp ${OpenGL}/*.cpp)
#添加自己編寫 cpp 源文件生成動態庫
add_library(audiovideo SHARED ${ALL_CPP})
#找系統中 NDK log庫
find_library(log_lib
log)
#最后才開始鏈接庫
target_link_libraries(
#最后生成的 so 庫名稱
audiovideo
#音頻渲染
OpenSLES
# OpenGL 與 NativeWindow 連接本地窗口的中間者
EGL
#視頻渲染
GLESv2
#添加本地庫
android
${log_lib}
)
復制代碼
~~~
至此,對于 OpenGL 的開發需要用到的頭文件以及庫文件就引入完畢了,下面再來看看如何使用 EGL 搭建出 OpenGL 的上下文環境以及渲染視頻數據。
* 1. 使用 EGL 首先必須創建,建立本地窗口系統和 OpenGL ES 的連接
~~~
//1.獲取原始窗口
nativeWindow = ANativeWindow_fromSurface(env, surface);
//獲取Display
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
LOGD("egl display failed");
showMessage(env, "egl display failed", false);
return;
}
復制代碼
~~~
* 2. 初始化 EGL
~~~
//初始化egl,后兩個參數為主次版本號
if (EGL_TRUE != eglInitialize(display, 0, 0)) {
LOGD("eglInitialize failed");
showMessage(env, "eglInitialize failed", false);
return;
}
復制代碼
~~~
* 3. 確定可用的渲染表面( Surface )的配置。
~~~
//surface 配置,可以理解為窗口
EGLConfig eglConfig;
EGLint configNum;
EGLint configSpec[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};
if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) {
LOGD("eglChooseConfig failed");
showMessage(env, "eglChooseConfig failed", false);
return;
}
復制代碼
~~~
* 4. 創建渲染表面 surface(4/5步驟可互換)
~~~
//創建surface(egl和NativeWindow進行關聯。最后一個參數為屬性信息,0表示默認版本)
winSurface = eglCreateWindowSurface(display, eglConfig, nativeWindow, 0);
if (winSurface == EGL_NO_SURFACE) {
LOGD("eglCreateWindowSurface failed");
showMessage(env, "eglCreateWindowSurface failed", false);
return;
}
復制代碼
~~~
* 5. 創建渲染上下文 Context
~~~
//4 創建關聯上下文
const EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};
//EGL_NO_CONTEXT表示不需要多個設備共享上下文
context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);
if (context == EGL_NO_CONTEXT) {
LOGD("eglCreateContext failed");
showMessage(env, "eglCreateContext failed", false);
return;
}
復制代碼
~~~
* 6. 指定某個 EGLContext 為當前上下文, 關聯起來
~~~
//將egl和opengl關聯
//兩個surface一個讀一個寫。第二個一般用來離線渲染
if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
LOGD("eglMakeCurrent failed");
showMessage(env, "eglMakeCurrent failed", false);
return;
}
復制代碼
~~~
* 7. 使用 OpenGL 相關的 API 進行繪制操作
~~~
GLint vsh = initShader(vertexShader, GL_VERTEX_SHADER);
GLint fsh = initShader(fragYUV420P, GL_FRAGMENT_SHADER);
//創建渲染程序
GLint program = glCreateProgram();
if (program == 0) {
LOGD("glCreateProgram failed");
showMessage(env, "glCreateProgram failed", false);
return;
}
//向渲染程序中加入著色器
glAttachShader(program, vsh);
glAttachShader(program, fsh);
//鏈接程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == 0) {
LOGD("glLinkProgram failed");
showMessage(env, "glLinkProgram failed", false);
return;
}
LOGD("glLinkProgram success");
//激活渲染程序
glUseProgram(program);
//加入三維頂點數據
static float ver[] = {
1.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f
};
GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPosition"));
glEnableVertexAttribArray(apos);
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, ver);
//加入紋理坐標數據
static float fragment[] = {
1.0f, 0.0f,
0.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f
};
GLuint aTex = static_cast<GLuint>(glGetAttribLocation(program, "aTextCoord"));
glEnableVertexAttribArray(aTex);
glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);
//紋理初始化
//設置紋理層對應的對應采樣器?
/**
* //獲取一致變量的存儲位置
GLint textureUniformY = glGetUniformLocation(program, "SamplerY");
GLint textureUniformU = glGetUniformLocation(program, "SamplerU");
GLint textureUniformV = glGetUniformLocation(program, "SamplerV");
//對幾個紋理采樣器變量進行設置
glUniform1i(textureUniformY, 0);
glUniform1i(textureUniformU, 1);
glUniform1i(textureUniformV, 2);
*/
//對sampler變量,使用函數glUniform1i和glUniform1iv進行設置
glUniform1i(glGetUniformLocation(program, "yTexture"), 0);
glUniform1i(glGetUniformLocation(program, "uTexture"), 1);
glUniform1i(glGetUniformLocation(program, "vTexture"), 2);
//紋理ID
GLuint texts[3] = {0};
//創建若干個紋理對象,并且得到紋理ID
glGenTextures(3, texts);
//綁定紋理。后面的的設置和加載全部作用于當前綁定的紋理對象
//GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是紋理單元,GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP為紋理目標
//通過 glBindTexture 函數將紋理目標和紋理綁定后,對紋理目標所進行的操作都反映到對紋理上
glBindTexture(GL_TEXTURE_2D, texts[0]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//放大的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
// 加載紋理到 OpenGL,讀入 buffer 定義的位圖數據,并把它復制到當前綁定的紋理對象
// 當前綁定的紋理對象就會被附加上紋理圖像。
//width,height表示每幾個像素公用一個yuv元素?比如width / 2表示橫向每兩個像素使用一個元素?
glTexImage2D(GL_TEXTURE_2D,
0,//細節基本 默認0
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖(這里就是只取一個亮度的顏色通道的意思)
width,//加載的紋理寬度。最好為2的次冪(這里對y分量數據當做指定尺寸算,但顯示尺寸會拉伸到全屏?)
height,//加載的紋理高度。最好為2的次冪
0,//紋理邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖
GL_UNSIGNED_BYTE,//像素點存儲的數據類型
NULL //紋理的數據(先不傳)
);
//綁定紋理
glBindTexture(GL_TEXTURE_2D, texts[1]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0,//細節基本 默認0
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖(這里就是只取一個顏色通道的意思)
width / 2,//u數據數量為屏幕的4分之1
height / 2,
0,//邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖
GL_UNSIGNED_BYTE,//像素點存儲的數據類型
NULL //紋理的數據(先不傳)
);
//綁定紋理
glBindTexture(GL_TEXTURE_2D, texts[2]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0,//細節基本 默認0
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖(這里就是只取一個顏色通道的意思)
width / 2,
height / 2,//v數據數量為屏幕的4分之1
0,//邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖
GL_UNSIGNED_BYTE,//像素點存儲的數據類型
NULL //紋理的數據(先不傳)
);
unsigned char *buf[3] = {0};
buf[0] = new unsigned char[width * height];//y
buf[1] = new unsigned char[width * height / 4];//u
buf[2] = new unsigned char[width * height / 4];//v
showMessage(env, "onSucceed", true);
FILE *fp = fopen(data_source, "rb");
if (!fp) {
LOGD("oepn file %s fail", data_source);
return;
}
while (!feof(fp)) {
//解決異常退出,終止讀取數據
if (!isPlay)
return;
fread(buf[0], 1, width * height, fp);
fread(buf[1], 1, width * height / 4, fp);
fread(buf[2], 1, width * height / 4, fp);
//激活第一層紋理,綁定到創建的紋理
//下面的width,height主要是顯示尺寸?
glActiveTexture(GL_TEXTURE0);
//綁定y對應的紋理
glBindTexture(GL_TEXTURE_2D, texts[0]);
//替換紋理,比重新使用glTexImage2D性能高多
glTexSubImage2D(GL_TEXTURE_2D, 0,
0, 0,//相對原來的紋理的offset
width, height,//加載的紋理寬度、高度。最好為2的次冪
GL_LUMINANCE, GL_UNSIGNED_BYTE,
buf[0]);
//激活第二層紋理,綁定到創建的紋理
glActiveTexture(GL_TEXTURE1);
//綁定u對應的紋理
glBindTexture(GL_TEXTURE_2D, texts[1]);
//替換紋理,比重新使用glTexImage2D性能高
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_LUMINANCE,
GL_UNSIGNED_BYTE,
buf[1]);
//激活第三層紋理,綁定到創建的紋理
glActiveTexture(GL_TEXTURE2);
//綁定v對應的紋理
glBindTexture(GL_TEXTURE_2D, texts[2]);
//替換紋理,比重新使用glTexImage2D性能高
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_LUMINANCE,
GL_UNSIGNED_BYTE,
buf[2]);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//8. 窗口顯示,交換雙緩沖區
eglSwapBuffers(display, winSurface);
}
復制代碼
~~~
* 8. 交換 EGL 的 Surface 的內部緩沖和 EGL 創建的和平臺無關的窗口 diaplay
~~~
//窗口顯示,交換雙緩沖區
eglSwapBuffers(display, winSurface);
復制代碼
~~~
* 9. 釋放資源
~~~
/**
* 銷毀數據
*/
void Gles_play::release() {
if (display || winSurface || context) {
//銷毀顯示設備
eglDestroySurface(display, winSurface);
//銷毀上下文
eglDestroyContext(display, context);
//釋放窗口
ANativeWindow_release(nativeWindow);
//釋放線程
eglReleaseThread();
//停止
eglTerminate(display);
eglMakeCurrent(display, winSurface, EGL_NO_SURFACE, context);
context = EGL_NO_CONTEXT;
display = EGL_NO_SURFACE;
winSurface = nullptr;
winSurface = 0;
nativeWindow = 0;
isPlay = false;
}
}
復制代碼
~~~
到這里整個 OpenGL ES 渲染工作都完成了,代碼已上傳到[GitHub 倉庫,需要的可以自行查看](https://github.com/yangkun19921001/NDK_AV_SAMPLE/tree/master/audio_video)在提供一個[Java 端實現 OpenGL ES 實時渲染 YUV 的 DEMO](https://github.com/yangkun19921001/YUVPlay),注意: 測試的時候需要把 raw/\*.yuv 放入 sdcard/ 根目錄中。
## 總結
本章的概念比較多,難免會枯燥一些,但是了解這些概念是必須的。下一篇將帶來 FFmpeg + LibRtmp 播放器開發練習,支持 rtmp 拉流、本地視頻播放(該篇文章和上一篇文章都分別講解了音頻視頻基礎和渲染就是為了播放器開發做準備),可以先看一下效果(如下圖)。是不是有那么一點小小的期待 ?? ,預計在 2 月下旬發布文章,在等一等。

- 前言
- JNI基礎知識
- C語言知識點總結
- ①基本語法
- ②數據類型
- 枚舉類型
- 自定義類型(類型定義)
- ③格式化輸入輸出
- printf函數
- scanf函數
- 編程規范
- ④變量和常量
- 局部變量和外部變量
- ⑤類型轉換
- ⑥運算符
- ⑦結構語句
- 1、分支結構(選擇語句)
- 2、循環結構
- 退出循環
- break語句
- continue語句
- goto語句
- ⑧函數
- 函數的定義和調用
- 參數
- 函數的返回值
- 遞歸函數
- 零起點學通C語言摘要
- 內部函數和外部函數
- 變量存儲類別
- ⑨數組
- 指針
- 結構體
- 聯合體(共用體)
- 預處理器
- 預處理器的工作原理
- 預處理指令
- 宏定義
- 簡單的宏
- 帶參數的宏
- 預定義宏
- 文件包含
- 條件編譯
- 內存中的數據
- C語言讀文件和寫文件
- JNI知識點總結
- 前情回顧
- JNI規范
- jni開發
- jni開發中常見的錯誤
- JNI實戰演練
- C++(CPP)在Android開發中的應用
- 掘金網友總結的音視頻開發知識
- 音視頻學習一、C 語言入門
- 1.程序結構
- 2. 基本語法
- 3. 數據類型
- 4. 變量
- 5. 常量
- 6. 存儲類型關鍵字
- 7. 運算符
- 8. 判斷
- 9. 循環
- 10. 函數
- 11. 作用域規則
- 12. 數組
- 13. 枚舉
- 14. 指針
- 15. 函數指針與回調函數
- 16. 字符串
- 17. 結構體
- 18. 共用體
- 19. typedef
- 20. 輸入 & 輸出
- 21.文件讀寫
- 22. 預處理器
- 23.頭文件
- 24. 強制類型轉換
- 25. 錯誤處理
- 26. 遞歸
- 27. 可變參數
- 28. 內存管理
- 29. 命令行參數
- 總結
- 音視頻學習二 、C++ 語言入門
- 1. 基本語法
- 2. C++ 關鍵字
- 3. 數據類型
- 4. 變量類型
- 5. 變量作用域
- 6. 常量
- 7. 修飾符類型
- 8. 存儲類
- 9. 運算符
- 10. 循環
- 11. 判斷
- 12. 函數
- 13. 數學運算
- 14. 數組
- 15. 字符串
- 16. 指針
- 17. 引用
- 18. 日期 & 時間
- 19. 輸入輸出
- 20. 數據結構
- 21. 類 & 對象
- 22. 繼承
- 23. 重載運算符和重載函數
- 24. 多態
- 25. 數據封裝
- 26. 接口(抽象類)
- 27. 文件和流
- 28. 異常處理
- 29. 動態內存
- 30. 命名空間
- 31. 預處理器
- 32. 多線程
- 總結
- 音視頻學習 (三) JNI 從入門到掌握
- 音視頻學習 (四) 交叉編譯動態庫、靜態庫的入門學習
- 音視頻學習 (五) Shell 腳本入門
- 音視頻學習 (六) 一鍵編譯 32/64 位 FFmpeg 4.2.2
- 音視頻學習 (七) 掌握音頻基礎知識并使用 AudioTrack、OpenSL ES 渲染 PCM 數據
- 音視頻學習 (八) 掌握視頻基礎知識并使用 OpenGL ES 2.0 渲染 YUV 數據
- 音視頻學習 (九) 從 0 ~ 1 開發一款 Android 端播放器(支持多協議網絡拉流/本地文件)
- 音視頻學習 (十) 基于 Nginx 搭建(rtmp、http)直播服務器
- 音視頻學習 (十一) Android 端實現 rtmp 推流
- 音視頻學習 (十二) 基于 FFmpeg + OpenSLES 實現音頻萬能播放器
- 音視頻學習 (十三) Android 中通過 FFmpeg 命令對音視頻編輯處理(已開源)