用OpenGLES實現yuv420p視頻播放界面

背景

例子TFLive這個項目里,是我按著ijkPlayer寫的直播播放器,要運行需要編譯ffmpeg的庫,網盤里存了一份, 提取碼:vjce。OpenGL ES播放相關的在在OpenGLES的文件夾里。

learnOpenGL學到會使用紋理就可以了。

播放視頻,就是把畫面一副一副的顯示,跟幀動畫那樣。在解碼視頻幀數據之后得到的就是某種格式的一段內存,這段數據構成了一副畫面所需的顏色信息,比如yuv420p。圖文詳解YUV420數據格式這篇寫的很好。

YUV和RGB這些都叫顏色空間,我的理解便是:它們是一種約定好的顏色值的排列方式。比如RGB,便是紅綠藍三種顏色分量依次排列,一般每個顏色分量就占一個字節(jié),值為0-255。

YUV420p, 是YUV三個分量分別三層,就像:YYYYUUVV。就是Y全部在一起,而RGB是RGBRGBRGB這樣混合的。每個分量各自在一起的就是有平面(Plane)的。而420樣式是4個Y分量和一對UV分量組合,節(jié)省空間。

要顯示YUV420p的圖像,需要轉化yuv到rgba,因為OpenGL輸出只認rgba。

iOS上準備工作

OpenGL部分在各平臺邏輯是一致的,不在iOS上的可以跳過這段。

使用frameBuffer來顯示:

  • 新建一個UIView子類,修改layer為CAEAGLLayer:
+(Class)layerClass{
    return [CAEAGLLayer class];
}
  • 開始繪制前構建Context:
-(BOOL)setupOpenGLContext{
    _renderLayer = (CAEAGLLayer *)self.layer;
    _renderLayer.opaque = YES;
    _renderLayer.contentsScale = [UIScreen mainScreen].scale;
    _renderLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
                                       [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,
                                       kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
                                       nil];
    
    _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    //_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    if (!_context) {
        NSLog(@"alloc EAGLContext failed!");
        return false;
    }
    EAGLContext *preContex = [EAGLContext currentContext];
    if (![EAGLContext setCurrentContext:_context]) {
        NSLog(@"set current EAGLContext failed!");
        return false;
    }
    [self setupFrameBuffer];
    
    [EAGLContext setCurrentContext:preContex];
    return true;
}
  • opaque設為YES是為了不做圖層混合,去掉不必要的性能消耗。
  • contentsScale保持跟手機主屏幕一致,在不同手機上自適應。
  • kEAGLDrawablePropertyRetainedBacking為YES的時候會保存渲染之后數據不變,我們不需要這個,一幀視頻數據顯示完就沒用了,所以這個功能關閉,去掉不必要的性能消耗。

有了這個context,并且把它設為CurrentContext,那么在繪制過程里的那些OpenGL代碼才能在這個context生效,它才能把結果輸出到需要的地方。

  • 構建frameBuffer,它是輸出結果:
-(void)setupFrameBuffer{
    glGenBuffers(1, &_frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
    
    glGenRenderbuffers(1, &_colorBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorBuffer);
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_renderLayer];
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBuffer);
    
    GLint width,height;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);
    
    _bufferSize.width = width;
    _bufferSize.height = height;
    
    glViewport(0, 0, _bufferSize.width, _bufferSize.height);

    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER) ;
    if(status != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"failed to make complete framebuffer object %x", status);
    }
}
  • 建一個framebuffer
  • 建一個存儲顏色的renderBuffer,但是它的內存是由contex來分配:[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_renderLayer];這一句比較關鍵。因為它,renderBuffer、context和layer才聯系到了一起。根據Apple文檔,負責顯示的layer和renderbuffer是共用內存的,這樣輸出到renderBuffer里的內容,layer才顯示。

OpenGL部分

分為兩部分:第一次繪制開始前準備數據和每次繪制循環(huán)。

準備部分

使用OpenGL顯示的邏輯是:畫一個正方形,然后把輸出的視頻幀數據制作成紋理(texture)給這個正方形,把紋理顯示處理就OK里。

所以繪制的圖形是不變的,那么shader和數據(AVO等)都是固定的,在第一次開始前搞定后面就不需要變了。

    if (!_renderConfiged) {
        [self configRenderData];
    }
-(BOOL)configRenderData{
    if (_renderConfiged) {
        return true;
    }
    
    GLfloat vertices[] = {
        -1.0f, 1.0f, 0.0f, 0.0f, 0.0f,  //left top
        -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, //left bottom
        1.0f, 1.0f, 0.0f, 1.0f, 0.0f,   //right top
        1.0f, -1.0f, 0.0f, 1.0f, 1.0f,  //right bottom
    };
    
//    NSString *vertexPath = [[NSBundle mainBundle] pathForResource:@"frameDisplay" ofType:@"vs"];
//    NSString *fragmentPath = [[NSBundle mainBundle] pathForResource:@"frameDisplay" ofType:@"fs"];
    //_frameProgram = new TFOPGLProgram(std::string([vertexPath UTF8String]), std::string([fragmentPath UTF8String]));
    _frameProgram = new TFOPGLProgram(TFVideoDisplay_common_vs, TFVideoDisplay_yuv420_fs);
    
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);
    
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GL_FLOAT), 0);
    glEnableVertexAttribArray(0);
    
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GL_FLOAT), (void*)(3*(sizeof(GL_FLOAT))));
    glEnableVertexAttribArray(1);
    
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    
    
    //gen textures
    glGenTextures(TFMAX_TEXTURE_COUNT, textures);
    for (int i = 0; i<TFMAX_TEXTURE_COUNT; i++) {
        glBindTexture(GL_TEXTURE_2D, textures[i]);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    }
    _renderConfiged = YES;
    
    return YES;
}
  • vertices 是正方形4個角的頂點坐標數據,每個點5個float數,前3個是xyz坐標,后兩個是紋理坐標(uv)。xyz范圍[-1, 1], uv范圍[0, 1]。
  • 加載shader、編譯,鏈接program,都在TFOPGLProgram這個類里做了。
  • 然后生成一個VAO和VBO綁定數據。
  • 最后構建幾個紋理,雖然這時還沒有數據,先占個位置。

繪制

先上shader:

const GLchar *TFVideoDisplay_common_vs ="               \n\
#version 300 es                                         \n\
                                                        \n\
layout (location = 0) in highp vec3 position;           \n\
layout (location = 1) in highp vec2 inTexcoord;         \n\
                                                        \n\
out highp vec2 texcoord;                                \n\
                                                        \n\
void main()                                             \n\
{                                                       \n\
gl_Position = vec4(position, 1.0);                      \n\
texcoord = inTexcoord;                                  \n\
}                                                       \n\
";
const GLchar *TFVideoDisplay_yuv420_fs ="               \n\
#version 300 es                                         \n\
precision highp float;                                  \n\
                                                        \n\
in vec2 texcoord;                                       \n\
out vec4 FragColor;                                     \n\
uniform lowp sampler2D yPlaneTex;                       \n\
uniform lowp sampler2D uPlaneTex;                       \n\
uniform lowp sampler2D vPlaneTex;                       \n\
                                                        \n\
void main()                                             \n\
{                                                       \n\
    // (1) y - 16 (2) rgb * 1.164                       \n\
    vec3 yuv;                                           \n\
    yuv.x = texture(yPlaneTex, texcoord).r;             \n\
    yuv.y = texture(uPlaneTex, texcoord).r - 0.5f;      \n\
    yuv.z = texture(vPlaneTex, texcoord).r - 0.5f;      \n\
                                                        \n\
    mat3 trans = mat3(1, 1 ,1,                          \n\
                      0, -0.34414, 1.772,               \n\
                      1.402, -0.71414, 0                \n\
                      );                                \n\
                                                        \n\
    FragColor = vec4(trans*yuv, 1.0);                   \n\
}                                                       \n\
";
  • vertex shader就是輸出一下gl_Position然后把紋理坐標傳給fragment shader。

  • fragment shader是重點,因為要在這里完成從yuv到rgb的轉換

  • 因為yuv420p是yuv3個分量分層存放的,如果將整個yuv數據作為整個紋理加載進來,那么用一個紋理坐標想取到3個分量,計算起來就比較麻煩了,每個fragment都需要計算。
    YyYYYYYY
    YYYYYYYY
    uUUUvVVV
    yuv420p的樣子是這樣的,加入你要取(2,1)這個坐標的顏色信息,那么y在(2,1),u在(1,3),v在(5,3)。而且高寬比例會影響布局:
    YyYYYYYY
    YYYYYYYY
    YyYYYYYY
    YYYYYYYY
    uUUUuUUU
    vVVVvVVV
    這樣uv不在同一行了。

所以采用每個分量單獨的紋理。這樣厲害的地方就是他們可以共用同一個紋理坐標:

glBindTexture(GL_TEXTURE_2D, textures[0]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, overlay->pixels[0]);
    glGenerateMipmap(GL_TEXTURE_2D);
    
    glBindTexture(GL_TEXTURE_2D, textures[1]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width/2, height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, overlay->pixels[1]);
    glGenerateMipmap(GL_TEXTURE_2D);
    
    glBindTexture(GL_TEXTURE_2D, textures[2]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width/2, height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, overlay->pixels[2]);
    glGenerateMipmap(GL_TEXTURE_2D);
  • 3個紋理,y的紋理和圖像大小一樣,u和v的高寬都減半。
  • overlay只是用來打包視頻幀數據的一個結構體,pixels的0、1、2分別就是yuv3個分量的平面的開始位置。
  • 有一個關鍵點是紋理格式使用GL_LUMINANCE,也就是單顏色通道??淳W上的例子,之前寫的是GL_RED的是不行的。
  • 因為威力坐標是一個相對坐標,是映射到[0, 1]范圍內的。所以對于紋理坐標[x, y],在u和v紋理的上取到的點跟y紋理坐標上[2x, 2y]是對應的,而這正是yuv420需要的:4個y對應一組uv。

最后用的把yuv轉成rgb,用的公式:

R = Y + 1.402 (Cr-128)
G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128)
B = Y + 1.772 (Cb-128)

這里還有一個注意的就是,YUV和YCrCb的區(qū)別
YCrCb是YUV的一個偏移版本,所以需要減去0.5(因為都映射到0-1范圍了128就是0.5)。當然我覺得這個公式還是要看編碼的時候設置了什么格式,視頻拍攝的時候是怎么把rgb轉成yuv的,兩者配套就ok了!

繪制正方形

glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
    
    _frameProgram->use();
    
    _frameProgram->setTexture("yPlaneTex", GL_TEXTURE_2D, textures[0], 0);
    _frameProgram->setTexture("uPlaneTex", GL_TEXTURE_2D, textures[1], 1);
    _frameProgram->setTexture("vPlaneTex", GL_TEXTURE_2D, textures[2], 2);
    
    glBindVertexArray(VAO);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    
    glBindRenderbuffer(GL_RENDERBUFFER, self.colorBuffer);
    [self.context presentRenderbuffer:GL_RENDERBUFFER];

細節(jié)處理

  • 監(jiān)測一下app前后臺切換,后臺就不要渲染了:
[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(catchAppResignActive) name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(catchAppBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
......
-(void)catchAppResignActive{
    _appIsUnactive = YES;
}

-(void)catchAppBecomeActive{
    _appIsUnactive = NO;
}
.......
if (self.appIsUnactive) {
    return;    //繪制之前檢查,直接取消
}
  • 把繪制移到副線程
    iOS中OpenGL ES的的這些操縱是可以全部放到副線程處理的,包括最后的presentRenderbuffer。關鍵是context構建、數組準備(VAO texture等)、渲染這些得在一個線程里,當然也可以多線程操作,但對于視屏播放而言沒有必要,去除沒必要的性能消耗吧,鎖都不用加了。

  • layer的frame改變處理

-(void)layoutSubviews{
    [super layoutSubviews];
    
    //If context has setuped and layer's size has changed, realloc renderBuffer.
    if (self.context && !CGSizeEqualToSize(self.layer.frame.size, self.bufferSize)) {
 _needReallocRenderBuffer = YES;
    }
}
...........
if (_needReallocRenderBuffer) {
   [self reallocRenderBuffer];
   _needReallocRenderBuffer = NO;
}
.........
-(void)reallocRenderBuffer{
    glBindRenderbuffer(GL_RENDERBUFFER, _colorBuffer);
    
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_renderLayer];
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBuffer);
    ......
}

  • 改變之后,重新分配render buffer的內存
  • 為了在同一個線程里處理,所以沒有直接在layoutSubviews里重新分配render buffer,這里肯定是主線程。所以只是做了個標記
  • 在渲染的方法里,先查看_needReallocRenderBuffer,然后realloc render buffer.

最后

重點是fragment shader里對yuv分量的讀?。?/p>

  1. 采取3個紋理
  2. 使用同一個紋理坐標
  3. 構建紋理是使用GL_LUMINANCE, u、v紋理寬高相對y都減半。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容