用OpenGLES實(shí)現(xiàn)yuv420p視頻播放界面

背景

例子TFLive這個(gè)項(xiàng)目里,是我按著ijkPlayer寫(xiě)的直播播放器,要運(yùn)行需要編譯ffmpeg的庫(kù),網(wǎng)盤(pán)里存了一份, 提取碼:vjce。OpenGL ES播放相關(guān)的在在OpenGLES的文件夾里。

learnOpenGL學(xué)到會(huì)使用紋理就可以了。

播放視頻,就是把畫(huà)面一副一副的顯示,跟幀動(dòng)畫(huà)那樣。在解碼視頻幀數(shù)據(jù)之后得到的就是某種格式的一段內(nèi)存,這段數(shù)據(jù)構(gòu)成了一副畫(huà)面所需的顏色信息,比如yuv420p。圖文詳解YUV420數(shù)據(jù)格式這篇寫(xiě)的很好。

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

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

要顯示YUV420p的圖像,需要轉(zhuǎn)化yuv到rgba,因?yàn)镺penGL輸出只認(rèn)rgba。

iOS上準(zhǔn)備工作

OpenGL部分在各平臺(tái)邏輯是一致的,不在iOS上的可以跳過(guò)這段。

使用frameBuffer來(lái)顯示:

  • 新建一個(gè)UIView子類(lèi),修改layer為CAEAGLLayer:
+(Class)layerClass{
    return [CAEAGLLayer class];
}
  • 開(kāi)始繪制前構(gòu)建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設(shè)為YES是為了不做圖層混合,去掉不必要的性能消耗。
  • contentsScale保持跟手機(jī)主屏幕一致,在不同手機(jī)上自適應(yīng)。
  • kEAGLDrawablePropertyRetainedBacking為YES的時(shí)候會(huì)保存渲染之后數(shù)據(jù)不變,我們不需要這個(gè),一幀視頻數(shù)據(jù)顯示完就沒(méi)用了,所以這個(gè)功能關(guān)閉,去掉不必要的性能消耗。

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

  • 構(gòu)建frameBuffer,它是輸出結(jié)果:
-(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);
    }
}
  • 建一個(gè)framebuffer
  • 建一個(gè)存儲(chǔ)顏色的renderBuffer,但是它的內(nèi)存是由contex來(lái)分配:[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_renderLayer];這一句比較關(guān)鍵。因?yàn)樗?,renderBuffer、context和layer才聯(lián)系到了一起。根據(jù)Apple文檔,負(fù)責(zé)顯示的layer和renderbuffer是共用內(nèi)存的,這樣輸出到renderBuffer里的內(nèi)容,layer才顯示。

OpenGL部分

分為兩部分:第一次繪制開(kāi)始前準(zhǔn)備數(shù)據(jù)和每次繪制循環(huán)。

準(zhǔn)備部分

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

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

    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個(gè)角的頂點(diǎn)坐標(biāo)數(shù)據(jù),每個(gè)點(diǎn)5個(gè)float數(shù),前3個(gè)是xyz坐標(biāo),后兩個(gè)是紋理坐標(biāo)(uv)。xyz范圍[-1, 1], uv范圍[0, 1]。
  • 加載shader、編譯,鏈接program,都在TFOPGLProgram這個(gè)類(lèi)里做了。
  • 然后生成一個(gè)VAO和VBO綁定數(shù)據(jù)。
  • 最后構(gòu)建幾個(gè)紋理,雖然這時(shí)還沒(méi)有數(shù)據(jù),先占個(gè)位置。

繪制

先上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然后把紋理坐標(biāo)傳給fragment shader。

  • fragment shader是重點(diǎn),因?yàn)橐谶@里完成從yuv到rgb的轉(zhuǎn)換。

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

所以采用每個(gè)分量單獨(dú)的紋理。這樣厲害的地方就是他們可以共用同一個(gè)紋理坐標(biāo):

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個(gè)紋理,y的紋理和圖像大小一樣,u和v的高寬都減半。
  • overlay只是用來(lái)打包視頻幀數(shù)據(jù)的一個(gè)結(jié)構(gòu)體,pixels的0、1、2分別就是yuv3個(gè)分量的平面的開(kāi)始位置。
  • 有一個(gè)關(guān)鍵點(diǎn)是紋理格式使用GL_LUMINANCE,也就是單顏色通道。看網(wǎng)上的例子,之前寫(xiě)的是GL_RED的是不行的。
  • 因?yàn)橥ψ鴺?biāo)是一個(gè)相對(duì)坐標(biāo),是映射到[0, 1]范圍內(nèi)的。所以對(duì)于紋理坐標(biāo)[x, y],在u和v紋理的上取到的點(diǎn)跟y紋理坐標(biāo)上[2x, 2y]是對(duì)應(yīng)的,而這正是yuv420需要的:4個(gè)y對(duì)應(yīng)一組uv。

最后用的把yuv轉(zhuǎn)成rgb,用的公式:

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

這里還有一個(gè)注意的就是,YUV和YCrCb的區(qū)別
YCrCb是YUV的一個(gè)偏移版本,所以需要減去0.5(因?yàn)槎加成涞?-1范圍了128就是0.5)。當(dāng)然我覺(jué)得這個(gè)公式還是要看編碼的時(shí)候設(shè)置了什么格式,視頻拍攝的時(shí)候是怎么把rgb轉(zhuǎn)成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];
  • 開(kāi)啟program,并把三個(gè)紋理輸入
  • 使用GL_TRIANGLE_STRIP繪制,這樣可以更簡(jiǎn)單些,用GL_TRIANGLES就得兩個(gè)三角形了。因?yàn)檫@個(gè),所以vertices的4個(gè)點(diǎn)是左上、左下、右上、右下的順序,具體規(guī)律看【OpenGL】理解GL_TRIANGLE_STRIP等繪制三角形序列的三種方式。

細(xì)節(jié)處理

  • 監(jiān)測(cè)一下app前后臺(tái)切換,后臺(tái)就不要渲染了:
[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。關(guān)鍵是context構(gòu)建、數(shù)組準(zhǔn)備(VAO texture等)、渲染這些得在一個(gè)線程里,當(dāng)然也可以多線程操作,但對(duì)于視屏播放而言沒(méi)有必要,去除沒(méi)必要的性能消耗吧,鎖都不用加了。

  • 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的內(nèi)存
  • 為了在同一個(gè)線程里處理,所以沒(méi)有直接在layoutSubviews里重新分配render buffer,這里肯定是主線程。所以只是做了個(gè)標(biāo)記
  • 在渲染的方法里,先查看_needReallocRenderBuffer,然后realloc render buffer.

最后

重點(diǎn)是fragment shader里對(duì)yuv分量的讀?。?/p>

  1. 采取3個(gè)紋理
  2. 使用同一個(gè)紋理坐標(biāo)
  3. 構(gòu)建紋理是使用GL_LUMINANCE, u、v紋理寬高相對(duì)y都減半。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容