音視頻學習 (八) 掌握視頻基礎知識並使用 OpenGL ES 2.0 渲染 YUV 數據

簡介

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

視頻的基礎知識

圖像的物理現象

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

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

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

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

因此在黑暗的環境下也能看到手機屏幕上的內容,是由於手機屏幕是自發光的,而不是經過光的反射才被人們看到的。github

圖像的數值表示

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 信號亮度值之間的差別。bash

之因此採用 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.26一、H.26二、H.26三、H.264 一系列視頻編碼標準是一套單獨的體系。其中,H.264 集中了以往標準的全部優勢,並吸收了以往標準的經驗,採樣的是簡潔設計,這使得它比 Mpeg4 更容易推廣。如今使用最多的就是 H.264 標準, H.264 創造了多參考幀、多塊類型、整數變換、幀內預測等新的壓縮技術,使用了更精準的分像素運動矢量(1/四、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_TEXTURE一、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 倉庫,須要的能夠自行查看 在提供一個 Java 端實現 OpenGL ES 實時渲染 YUV 的 DEMO,注意: 測試的時候須要把 raw/*.yuv 放入 sdcard/ 根目錄中。

總結

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

相關文章
相關標籤/搜索