iOS下 WebRTC 視頻渲染

前言

今天爲你們介紹一下 iOS 下 WebRTC是如何渲染視頻的。在iOS中有兩種加速渲染視頻的方法。一種是使用OpenGL;另外一種是使用 Metal。bash

OpenGL的好處是跨平臺,推出時間比較長,所以比較穩定。兼容性也比較好。而Metal是iOS最近才推出的技術,理論上來講比OpenGL ES效率更高。app

WebRTC中這兩種渲染方式都支持。它首先會判斷當前iOS系統是否支持Metal,若是支持的話,優先使用Metal。若是不支持的話,就使用 OpenGL ES。框架

咱們今天介紹的是 OpenGL ES的方案。ide


建立 OpenGL 上下文

在iOS中使用OpenGL ES作視頻渲染時,首先要建立EAGLContext對象。這是由於,EAGLContext管理着 OpengGL ES 渲染上下文。該上下文中,包括了狀態信息,渲染命令以及OpenGL ES繪製資源(如紋理和renderbuffers)。爲了執行OpenGL ES命令,你須要將建立的EAGLContext設置爲當前渲染上下文。函數

EAGLContext並不直接管理繪製資源,它經過與上下文相關的EAGLSharegroup對象來管理。當建立EAGLContext時,你能夠選擇建立一個新的sharegroup或與以前建立的EAGLContext共享EAGLSharegroup。ui

EAGLContext與EAGLSharegroup的關係以下圖所示:spa


WebRTC中並無使用共享EAGLSharegroup的狀況,因此對於這種狀況咱們這裏就不作特別講解了。有興趣的同窗能夠在網上查找相關資料。3d

目前,OpenGL ES有3個版本,主要使用版本2和版本3 。因此咱們在建立時要對其做判斷。首先看是否支持版本3,若是不支持咱們就使用版本2。代理

代碼以下:code

//首先使用版本3,若是不支持則使用版本2
EAGLContext *glContext =
[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (!glContext) {
    glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
}

if (!glContext) {
    RTCLogError(@"Failed to create EAGLContext");
    return NO;
}
複製代碼

建立完上下文後,咱們還要將它設置爲當前上下文,這樣它才能真正起做用。

代碼以下:

//若是當前上下文不是OpenGL上下文,則將OpenGL上下文設置爲當前上下文。
if ([EAGLContext currentContext] != _glContext) {
    [EAGLContext setCurrentContext:_glContext];
}
複製代碼

須要注意的是,因爲應用切換到後臺後,上下文就發生了切換。因此當它切換到前臺時,也要作上面那個判斷。

OpenGL ES上下文建立好後,下面咱們看一下如何建立View。

建立 OpenGL View

在iOS中,有兩種展現層,一種是 GLKView,另外一種是 CAEAGLLayer。WebRTC中使用GLKView進行展現。CAEAGLLayer暫不作介紹。

GLKit框架提供了View和View Controller類以減小創建和維護繪製 OpenGL ES 內容的代碼。GLKView類用於管理展現部分;GLKViewController類用於管理繪製的內容。它們都是繼承自UIKit。GLKView的好處是,開發人員能夠將本身的精力聚焦在OpenGL ES渲染的工做上。

GLKView展現的基本流程以下:


如上圖所示,繪製 OpenGL ES 內容有三步:

  • 準備 OpenGL ES 環境;
  • 發送繪製命令;
  • 展現渲染內容。

GLKView類本身實現了第一步和第三步。第二步由開發人員來完成,也就是要實現drawRect函數。GLKView之因此能爲OpenGL ES提供簡單的繪製接口,是由於它管理了OpenGL ES渲染過程的標準部分:

  • 在調用繪製方法以前:

    • 使用 EAGLContext 做爲當前上下文。
    • 根據size, 縮放因子和繪製屬性,建立 FBO 和 renderbuffer。
    • 綁定 FBO,做爲繪製命令的當前目的地。
    • 匹配 OpenGL ES viewport與 framebuffer size 。
  • 在繪製方法返回以後:

    • 解決多采樣 buffers(若是開啓了多采樣)。
    • 當內容不在須要時,丟掉 renderbuffers。
    • 展現renderbuffer內容。

使用GLKView有兩種方法,一種是實現一個類,直接繼承自GLKView,並實現drawRect方法。另外一種是實現GLKView的代理,也就是GLKViewDelegate,並實現drawInRect方法。

在WebRTC中,使用的是第二種方法。RTCEAGLVideoView 是GLKView的包裹類,而且繼承自GLKViewDelegate。

首先,建立GLKView.

// GLKView manages a framebuffer for us.
//建立GLKView,在建立時,就將 EAGLContext 設置好。
_glkView = [[GLKView alloc] initWithFrame:CGRectZero
                                context:_glContext];
_glkView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
_glkView.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
_glkView.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
_glkView.drawableMultisample = GLKViewDrawableMultisampleNone;

//設置GLKView的delegate
_glkView.delegate = self;

_glkView.layer.masksToBounds = YES;

//將該值設置爲NO,這樣咱們就能夠本身控制OpenGL的展現了
_glkView.enableSetNeedsDisplay = NO;

[self addSubview:_glkView];

複製代碼

建立好GLKView後,須要將glkView.delegate設置爲RTCEAGLVideoView,這樣就能夠將繪製工做交由RTCEAGLVideoView來完成了。另外,glkView.enableSetNeedsDisplay 設置爲 NO,由咱們本身來控制什麼時候進行繪製。

而後,實現drawInRect方法。

...

if (!_nv12TextureCache) {
  _nv12TextureCache = [[RTCNV12TextureCache alloc] initWithContext:_glContext];
}
if (_nv12TextureCache) {
  [_nv12TextureCache uploadFrameToTextures:frame];
  [_shader applyShadingForFrameWithWidth:frame.width
                                  height:frame.height
                                rotation:frame.rotation
                                  yPlane:_nv12TextureCache.yTexture
                                 uvPlane:_nv12TextureCache.uvTexture];
  [_nv12TextureCache releaseTextures];
}
                                  
...

複製代碼

上面的代碼就是經過Shader來繪製NV12的YUV數據到View中。這段代碼的基本意思是將一個解碼後的視頻幀分解成Y數據紋理,UV數據紋理。而後調用Shader程序將紋理轉成rgb數據,最終渲染到View中。

Shader程序

OpenGL ES 有兩種 Shader。一種是頂點(Vetex)Shader; 另外一種是片元(fragment )Shader。

  • Vetex Shader: 用於繪製頂點。
  • Fragment Shader:用於繪製像素點。

Vetex Shader

Vetex Shader用於繪製圖形的頂點。咱們都知道,不管是2D仍是3D圖形,它們都是由頂點構成的。

在OpenGL ES中,有三種基本圖元,分別是點,線,三角形。由它們再構成更復雜的圖形。而點、線、三角形又都是由點組成的。

視頻是在一個矩形裏顯示,因此咱們要經過基本圖元構建一個矩形。理論上,距形能夠經過點、線繪製出來,但這樣作的話,OpenGL ES就要繪製四次。而經過三角形繪製只須要兩次,因此使用三角形執行速度更快。

下面的代碼就是 WebRTC 中的Vetex Shader程序。該程序的做用是每一個頂點執行一次,將用戶輸入的頂點輸出到 gl_Position中,並將頂點的紋理做標點轉做爲 Fragment Shader 的輸入。

  1. OpenGL座標原點是屏幕的中心。紋理座標的原點是左下角。
  2. gl_Position是Shader的內部變量,存放一個項點的座標。
// Vertex shader doesn't do anything except pass coordinates through. const char kRTCVertexShaderSource[] = SHADER_VERSION VERTEX_SHADER_IN " vec2 position;\n" VERTEX_SHADER_IN " vec2 texcoord;\n" VERTEX_SHADER_OUT " vec2 v_texcoord;\n" "void main() {\n" " gl_Position = vec4(position.x, position.y, 0.0, 1.0);\n" " v_texcoord = texcoord;\n" "}\n"; 複製代碼

OpenGL ES Shader語法請見個人另外一篇文章着色器

fragment Shader

fragment Shader程序是對片元着色,每一個片元執行一次。片元與像素差很少。能夠簡單的把片元理解爲像素。

下面的代碼是WebRTC中的 fragment Shader程序。WebRTC收到遠端傳來的H264視頻幀後,解碼成YUV數據。以後,對YUV數據進行分解,如移動端使用的YUV數據格式爲NV12, 因此就被分紅了兩部分,一部分是Y數據紋理,另外一部分是UV數據紋理。

YUV有多種格式,能夠參見個人另外一篇文章YUV

在代碼中,使用FRAGMENT_SHADER_TEXTURE命令,也就是OpenGL ES中的 texture2D 函數,分別從 Y 數據紋理中取出 y值,從 UV 數據紋理中取出 uv值,而後經過公式計算出每一個像素(實際是片元)的 rgb值。

static const char kNV12FragmentShaderSource[] =
  SHADER_VERSION
  "precision mediump float;"
  FRAGMENT_SHADER_IN " vec2 v_texcoord;\n"
  "uniform lowp sampler2D s_textureY;\n"
  "uniform lowp sampler2D s_textureUV;\n"
  FRAGMENT_SHADER_OUT
  "void main() {\n"
  " mediump float y;\n"
  " mediump vec2 uv;\n"
  " y = " FRAGMENT_SHADER_TEXTURE "(s_textureY, v_texcoord).r;\n"
  " uv = " FRAGMENT_SHADER_TEXTURE "(s_textureUV, v_texcoord).ra -\n"
  " vec2(0.5, 0.5);\n"
  " " FRAGMENT_SHADER_COLOR " = vec4(y + 1.403 * uv.y,\n"
  " y - 0.344 * uv.x - 0.714 * uv.y,\n"
  " y + 1.770 * uv.x,\n"
  " 1.0);\n"
  " }\n";
複製代碼

有了頂點數據和片元的RGB值後,就能夠調用OpenGL ES的 draw 方法進行視頻的繪製了。

Shader的編譯、連接與使用

上面介紹了 WebRTC下 Vetex Shader 和 Fragment Shader程序。要想讓程序運行起來,還要額外作一些工做。

OpenGL ES的 shader程序與C程序差很少。想像一下C程序,要想讓一個C程序運行起來,要有如下幾個步驟:

  • 寫好程序代碼
  • 編譯
  • 連接
  • 執行

Shader程序的運行也是如此。咱們看看 WebRTC是如何作的。

...

GLuint shader = glCreateShader(type);
if (!shader) {
    return 0;
}
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint compileStatus = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus == GL_FALSE) {
    ...
}
...
  
複製代碼

它首先建立一個 Shader, 而後將上面的 Shader 程序與 Shader 綁定。以後編譯 Shader。

...

GLuint program = glCreateProgram();
if (!program) {
    return 0;
}
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
GLint linkStatus = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
    ...
}
...

複製代碼

編譯成功後,建立 program 對象。 將以前建立的 Shader 與program綁定到一塊兒。以後作連接工做。一切準備就緒後,就可使用Shader程序繪製視頻了。

...

glUseProgram(_nv12Program);

...

glDrawArrays(GL_TRIANGLE_FAN, 0, 4)

...
複製代碼

WebRTC中視頻渲染相關文件

  • RTCEAGLVideoView.m/h:建立 EAGLContext及OpenGL ES View,並將視頻數據顯示出來。
  • RTCShader.mm/h:OpenGL ES Shader 程序的建立,編譯與連接相關的代碼。
  • RTCDefaultShader.mm/h: Shader 程序,繪製相關的代碼。
  • RTCNV12TextureCache.mm/h: 用於生成 YUV NV12 相關紋理的代碼。
  • RTCI420TexutreCache.mm/h: 用於生成 I420 相關紋理的代碼。

小結

本文對 WebRTC 中 OpenGL ES 渲染作了介紹。經過本篇文章你們能夠了解到WebRTC是如何將視頻渲染出來的。包括:

  • 上下文的建立與初始化。
  • GLKView的建立。
  • 繪製方法的實現。
  • Shader代碼的分析。
  • Shader的編譯與執行。

對於 OpenGL ES 是一個至關大的主題,若是沒有相應的基礎,看本篇文章仍是比較因難的。你們能夠參考我前面寫的幾篇關於 OpenGL 的文章。

謝謝!

相關文章
相關標籤/搜索