摘要:isPlay) return。//設置紋理的格式和大小 glTexImage2D(GL_TEXTURE_2D, 0,//細節基本 默認0 GL_LUMINANCE,//gpu內部格式 亮度,灰度圖(這裏就是隻取一個顏色通道的意思) width / 2,//u數據數量爲屏幕的4分之1 height / 2, 0,//邊框 GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 GL_UNSIGNED_BYTE,//像素點存儲的數據類型 NULL //紋理的數據(先不傳) )。

簡介

上一篇文章我們學習了音頻的基礎知識和音頻的渲染之後,該篇我們學習視頻的知識,與上一篇學習方式一樣,基礎 + demo ,主打渲染,採集跟編碼我們後面學習播放器和錄屏在來研究。

視頻的基礎知識

圖像的物理現象

做過 Camera 採集或者做過幀動畫其實應該知道,視頻是由一幅幅圖像或者說一幀幀 YUV 數據組成,所以要學習視頻還得從圖像開始學習。

我們回顧一下,應該是初中的時候做過一個三棱鏡實驗,內容是如何利用三棱鏡將太陽光分解成彩色的光帶?第一個做這個實驗者是 牛頓 ,各色光因其所形成的折射角不同而彼此分離,就像彩虹一樣,所以白光能夠分解成多種色彩的光。後來人們通過實驗證明,紅綠藍三種色光無法被分解,故稱爲三原色光,等量的三原色光相加會變爲白光,即白光中含有等量的紅光(R),綠光(G),藍光(B)。

在日常生活中,由於光的反射,我們才能看到各類物體的輪廓和顏色。但是如果將這個理論應用到手機中,那麼該結論還成立嗎?答案是否定的,因爲在黑暗中我們也可以看到手機屏幕中的內容,實際上人眼能看到手機屏幕上的內容的原理如下。

假設一部手機屏幕的分辨率是 1920 * 1080 說明水平方向有 1080 個像素點,垂直方向有 1920 個像素點,所以整個屏幕就有 1920 * 1080 個像素點(這也是分辨率的含義)。每個像素點都由三個子像素點組成,如下圖所示,這些密密麻麻的子像素點在圖像放大或者在顯微鏡下可以看得一清二楚。當要顯示某篇文字或者某幅圖像時,就會把這幅圖像的每一個像素點的 RGB 通道分別對應的屏幕位置上的子像素點繪製到屏幕上,從而顯示整個圖像。

所以在黑暗的環境下也能看到手機屏幕上的內容,是因爲手機屏幕是自發光的,而不是通過光的反射才被人們看到的。

圖像的數值表示

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 壓縮 :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

YUV 和 RGB 的轉化

前面已經講過,凡是渲染到屏幕上的文字、圖片、或者其它,都需要轉爲 RGB 的表示形式,那麼 YUV 的表示形式和 RGB 的表示形式之間是如何進行轉換的呢?可以參考該篇文章 YUV <——> RGB 轉換算法 , 相互轉換 C++ 代碼可以參考 地址

視頻的編碼方式

視頻編碼

還記得上一篇文章我們學習的音頻編碼方式嗎?音頻的編碼主要是去除冗餘信息,從而實現數據量的壓縮。那麼對於視頻壓縮,又該從哪幾個方面來對數據進行壓縮呢?其實與之前提到的音頻編碼類似,視頻壓縮也是通過去除冗餘信息來進行壓縮的。相較於音頻數據,視頻數據有極強的相關性,也就是說有大量的冗餘信息,包括空間上的冗餘信息和時間上的冗餘信息,具體包括以下幾個部分。

  • 運動補償: 運動補償是通過先前的局部圖像來預測,,補償當前的局部圖像,它是減少幀序列冗餘信息的有效方法。
  • 運動表示: 不同區域的圖像需要使用不同的運動矢量來描述運動信息。
  • 運動估計: 運動估計是從視頻序列中抽取運動信息的一整套技術。

使用幀內編碼技術可以去除空間上的冗餘信息。

大家還記得之前提到的圖像編碼 JPEG 嗎?對於視頻, 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 (Open Graphics Lib) 定義了一個跨編程語言、跨平臺編程的專業圖形程序接口。可用於二維或三維圖像的處理與渲染,它是一個功能強大、調用方便的底層圖形庫。對於嵌入式的設備,其提供了 OpenGL ES (OpenGL for Embedded System) 版本,該版本是針對手機、Pad 等嵌入式設備而設計的,是 OpenGL 的一個子集。到目前爲止,OpenGL ES 已經經歷過很多版本的迭代與更新,到目前爲止運用最廣泛的還是 OpenGL ES 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;
      }
    1. 初始化 EGL

      //初始化egl,後兩個參數爲主次版本號
          if (EGL_TRUE != eglInitialize(display, 0, 0)) {
              LOGD("eglInitialize failed");
              showMessage(env, "eglInitialize failed", false);
              return;
          }
    1. 確定可用的渲染表面( 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;
          }
    1. 創建渲染表面 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;
          }
    1. 創建渲染上下文 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;
          }
    1. 指定某個 EGLContext 爲當前上下文, 關聯起來

      //將egl和opengl關聯
          //兩個surface一個讀一個寫。第二個一般用來離線渲染
          if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
              LOGD("eglMakeCurrent failed");
              showMessage(env, "eglMakeCurrent failed", false);
              return;
          }
    1. 使用 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);
          }
    1. 交換 EGL 的 Surface 的內部緩衝和 EGL 創建的和平臺無關的窗口 diaplay

      //窗口顯示,交換雙緩衝區
      eglSwapBuffers(display, winSurface);
    1. 釋放資源

      /**
       * 銷燬數據
       */
      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 倉庫,需要的可以自行查看 ,注意: 測試的時候需要把 raw/*.yuv 放入 sdcard/ 根目錄中。

總結

本章的概念比較多,難免會枯燥一些,但是瞭解這些概念是必須的。下一篇將帶來 FFmpeg + LibRtmp 播放器開發練習,支持 rtmp 拉流、本地視頻播放(該篇文章和上一篇文章都分別講解了音頻視頻基礎和渲染就是爲了播放器開發做準備),可以先看一下效果(如下圖)。是不是有那麼一點小小的期待 :stuck_out_tongue_winking_eye: ,預計在 2 月下旬發佈文章,在等一等。

相關文章