背景
例子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>
- 采取3個(gè)紋理
- 使用同一個(gè)紋理坐標(biāo)
- 構(gòu)建紋理是使用
GL_LUMINANCE, u、v紋理寬高相對(duì)y都減半。