前言
說來慚愧,本來是打算使用SDL的,網上的SDL資料也是相當的多,但是由于編譯一直不成功,所以轉向了另外一個跨平臺,使用OpenGL,以后等OpenGL做完后我還是會對SDL渲染進行嘗試,OpenGL我打算寫三篇博客來試著對FFmpeg解碼后的yuv420p數據進行渲染,今天的這一篇是參考自別人的代碼,我對別人的代碼進行了相當程度的注釋,注釋如果有不對的地方歡迎探討,在下一期打算使用雷博的這篇文章作為資料來寫一個demo,后期可能還要更深入的學習OpenGL的相關知識,這方面的學習成本還是很高的,歡迎大家一起交流,一起進步.
抽取類的百度云盤下載地址
鏈接: https://pan.baidu.com/s/1nuUp9nb 密碼: u242
使用前切記要給對幀的寬和高,否則會提示參數錯誤以及視頻顯示不對,由于時間過的就遠了一些,所以一些參數不對就被我改掉了,同時加入了像素對齊,否則看到的圖像就會感覺只渲染出了Y數據一樣.這是原文鏈接,需要登錄查看.
源代碼以及注釋
- 使用前需要導入相關依賴庫
基本上每一步都已經在代碼中注釋了,所以我在此便不再仔細敘述了.
- 這是頭文件外界可以調用的三個接口:
- (void)displayYUV420pData:(void *)data width:(NSInteger)w height:(NSInteger)h;
- (void)setVideoSize:(GLuint)width height:(GLuint)height;
/**
清除畫面
*/
- (void)clearFrame;
- 源碼
#import "OpenglView.h"
enum AttribEnum
{
ATTRIB_VERTEX,
ATTRIB_TEXTURE,
ATTRIB_COLOR,
};
//YUV數據枚舉
enum TextureType
{
TEXY = 0,
TEXU,
TEXV,
TEXC
};
@implementation OpenglView
#pragma mark - 初始化等操作
- (BOOL)doInit{
//用來顯示opengl的圖形
CAEAGLLayer *eaglLayer = (CAEAGLLayer*) self.layer;
//eaglLayer.opaque = YES;
//設為不透明
eaglLayer.opaque = YES;
//設置描繪屬性
// [eaglLayer setDrawableProperties:[NSDictionary dictionaryWithObjectsAndKeys:
// [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,
// kEAGLColorFormatRGB565, kEAGLDrawablePropertyColorFormat,
// //[NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
// nil]];
eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGB565, kEAGLDrawablePropertyColorFormat,
//[NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
nil];
//設置分辨率
self.contentScaleFactor = [UIScreen mainScreen].scale;
_viewScale = [UIScreen mainScreen].scale;
//創建上下文
_glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
//[self debugGlError];
//上下文創建失敗則直接返回no
if(!_glContext || ![EAGLContext setCurrentContext:_glContext])
{
return NO;
}
//創建紋理
[self setupYUVTexture];
//加載著色器
[self loadShader];
//像素數據對齊,第二個參數默認為4,一般為1或4
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
//使用著色器
glUseProgram(_program);
//獲取一致變量的存儲位置
GLuint textureUniformY = glGetUniformLocation(_program, "SamplerY");
GLuint textureUniformU = glGetUniformLocation(_program, "SamplerU");
GLuint textureUniformV = glGetUniformLocation(_program, "SamplerV");
//對幾個紋理采樣器變量進行設置
glUniform1i(textureUniformY, 0);
glUniform1i(textureUniformU, 1);
glUniform1i(textureUniformV, 2);
return YES;
}
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
//沒有初始化成功
if (![self doInit]) {
self = nil;
}
}
return self;
}
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if (self) {
//沒有初始化成功
if (![self doInit]) {
self = nil;
}
}
return self;
}
-(void)layoutSubviews{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//互斥鎖
@synchronized (self) {
[EAGLContext setCurrentContext:_glContext];
//清除緩沖區
[self destoryFrameAndRenderBuffer];
//創建緩沖區
[self createFrameAndRenderBuffer];
}
//把數據顯示在這個視窗上
glViewport(1, 1, self.bounds.size.width*_viewScale - 2, self.bounds.size.height*_viewScale - 2);
});
}
#pragma mark - 設置opengl
/**
不寫的話,設置描繪屬性會崩潰
@return <#return value description#>
*/
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
/**
創建緩沖區
@return <#return value description#>
*/
- (BOOL)createFrameAndRenderBuffer
{
//創建幀緩沖綁定
glGenFramebuffers(1, &_framebuffer);
//創建渲染緩沖
glGenRenderbuffers(1, &_renderBuffer);
//將之前用glGenFramebuffers創建的幀緩沖綁定為當前的Framebuffer(綁定到context上?).
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
//Renderbuffer綁定到context上,此時當前Framebuffer完全由renderbuffer控制
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//分配空間
if (![_glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer])
{
NSLog(@"attach渲染緩沖區失敗");
}
//這個函數看起來有點復雜,但其實它很好理解的。它要做的全部工作就是把把前面我們生成的深度緩存對像與當前的FBO對像進行綁定,當然我們要注意一個FBO有多個不同綁定點,這里是要綁定在FBO的深度緩沖綁定點上。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
//檢查當前幀緩存的關聯圖像和幀緩存參數
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
NSLog(@"創建緩沖區錯誤 0x%x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
return NO;
}
return YES;
}
/**
清除緩沖區
*/
- (void)destoryFrameAndRenderBuffer
{
if (_framebuffer)
{
//刪除FBO
glDeleteFramebuffers(1, &_framebuffer);
}
if (_renderBuffer)
{
//刪除渲染緩沖區
glDeleteRenderbuffers(1, &_renderBuffer);
}
_framebuffer = 0;
_renderBuffer = 0;
}
/**
創建紋理
*/
- (void)setupYUVTexture{
if (_textureYUV[TEXY])
{
//刪除紋理
glDeleteTextures(3, _textureYUV);
}
//生成紋理
glGenTextures(3, _textureYUV);
if (!_textureYUV[TEXY] || !_textureYUV[TEXU] || !_textureYUV[TEXV])
{
NSLog(@"<<<<<<<<<<<<紋理創建失敗!>>>>>>>>>>>>");
return;
}
//選擇當前活躍單元
glActiveTexture(GL_TEXTURE0);
//綁定Y紋理
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXY]);
//紋理過濾函數
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_S, GL_CLAMP_TO_EDGE);//水平方向
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);//垂直方向
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXU]);
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_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXV]);
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_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
#define FSH @"varying lowp vec2 TexCoordOut;\
\
uniform sampler2D SamplerY;\
uniform sampler2D SamplerU;\
uniform sampler2D SamplerV;\
\
void main(void)\
{\
mediump vec3 yuv;\
lowp vec3 rgb;\
\
yuv.x = texture2D(SamplerY, TexCoordOut).r;\
yuv.y = texture2D(SamplerU, TexCoordOut).r - 0.5;\
yuv.z = texture2D(SamplerV, TexCoordOut).r - 0.5;\
\
rgb = mat3( 1, 1, 1,\
0, -0.39465, 2.03211,\
1.13983, -0.58060, 0) * yuv;\
\
gl_FragColor = vec4(rgb, 1);\
\
}"
#define VSH @"attribute vec4 position;\
attribute vec2 TexCoordIn;\
varying vec2 TexCoordOut;\
\
void main(void)\
{\
gl_Position = position;\
TexCoordOut = TexCoordIn;\
}"
/**
加載著色器
*/
- (void)loadShader{
/**
1 編譯著色
*/
GLuint vertexShader = [self compileShader:VSH withType:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShader:FSH withType:GL_FRAGMENT_SHADER];
/**
2
*/
//創建程序容器
_program = glCreateProgram();
//綁定shader到program
glAttachShader(_program, vertexShader);
glAttachShader(_program, fragmentShader);
/**
綁定需要在link之前
*/
//把頂點屬性索引綁定到頂點屬性名
glBindAttribLocation(_program, ATTRIB_VERTEX, "position");
glBindAttribLocation(_program, ATTRIB_TEXTURE, "TexCoordIn");
//鏈接
glLinkProgram(_program);
/**
3
*/
GLint linkSuccess;
//查詢相關信息,并將數據返回到linkSuccess
glGetProgramiv(_program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(_program, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"<<<<著色器連接失敗 %@>>>", messageString);
//exit(1);
}
if (vertexShader)
/*
釋放內存不能立刻刪除
If a shader object to be deleted is attached to a program object, it will be flagged for deletion, but it will not be deleted until it is no longer attached to any program object…
shader 刪除該著色器對象(如果一個著色器對象在刪除前已經鏈接到程序對象中,那么當執行glDeleteShader函數時不會立即被刪除,而是該著色器對象將被標記為刪除,器內存被釋放一次,它不再鏈接到其他任何程序對象)
*/
glDeleteShader(vertexShader);
if (fragmentShader)
glDeleteShader(fragmentShader);
}
/**
編譯著色代碼,使用著色器
@param shaderString 代碼
@param shaderType 類型
@return 成功返回著色器,失敗返回-1
*/
- (GLuint)compileShader:(NSString*)shaderString withType:(GLenum)shaderType
{
/**
1
*/
if (!shaderString) {
// NSLog(@"Error loading shader: %@", error.localizedDescription);
exit(1);
}
else
{
//NSLog(@"shader code-->%@", shaderString);
}
/**
2 分別創建一個頂點著色器對象和一個片段著色器對象
*/
GLuint shaderHandle = glCreateShader(shaderType);
/**
3 分別將頂點著色程序的源代碼字符數組綁定到頂點著色器對象,將片段著色程序的源代碼字符數組綁定到片段著色器對象
*/
const char * shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = [shaderString length];
glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
/**
4 分別編譯頂點著色器對象和片段著色器對象
*/
glCompileShader(shaderHandle);
/**
5
*/
GLint compileSuccess;
//獲取編譯情況
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
exit(1);
}
return shaderHandle;
}
#pragma mark - 接口
/**
設置大小
@param width 界面寬
@param height 界面高
*/
-(void)setVideoSize:(GLuint)width height:(GLuint)height{
//給寬高賦值
_videoH = height;
_videoW = width;
//開辟內存空間
//為什么乘1.5而不是1: width * hight =Y(總和) U = Y / 4 V = Y / 4
void *blackData = malloc(width * height * 1.5);
if (blackData) {
/**
對內存空間清零,作用是在一段內存塊中填充某個給定的值,它是對較大的結構體或數組進行清零操作的一種最快方法
@param __b#> 源數據 description#>
@param __c#> 填充數據 description#>
@param __len#> 長度 description#>
@return <#return value description#>
*/
memset(blackData, 0x0, width * height * 1.5);
}
/*
Apple平臺不允許直接對Surface進行操作.這也就意味著在Apple中,不能通過調用eglSwapBuffers函數來直接實現渲染結果在目標surface上的更新.
在Apple平臺中,首先要創建一個EAGLContext對象來替代EGLContext (不能通過eglCreateContext來生成), EAGLContext的作用與EGLContext是類似的.
然后,再創建相應的Framebuffer和Renderbuffer.
Framebuffer象一個Renderbuffer集(它可以包含多個Renderbuffer對象).
Renderbuffer有三種: color Renderbuffer, depth Renderbuffer, stencil Renderbuffer.
渲染結果是先輸出到Framebuffer中,然后通過調用context的presentRenderbuffer,將Framebuffer上的內容提交給之前的CustumView.
*/
//設置當前上下文
[EAGLContext setCurrentContext:_glContext];
/*
target —— 紋理被綁定的目標,它只能取值GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D或者GL_TEXTURE_CUBE_MAP;
texture —— 紋理的名稱,并且,該紋理的名稱在當前的應用中不能被再次使用。
glBindTexture可以讓你創建或使用一個已命名的紋理,調用glBindTexture方法,將target設置為GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D或者GL_TEXTURE_CUBE_MAP,并將texture設置為你想要綁定的新紋理的名稱,即可將紋理名綁定至當前活動紋理單元目標。當一個紋理與目標綁定時,該目標之前的綁定關系將自動被打破。紋理的名稱是一個無符號的整數。在每個紋理目標中,0被保留用以代表默認紋理。紋理名稱與相應的紋理內容位于當前GL rendering上下文的共享對象空間中。
*/
//綁定Y紋理
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXY]);
/**
根據像素數據,加載紋理
@param target#> 指定目標紋理,這個值必須是GL_TEXTURE_2D。 description#>
@param level#> 執行細節級別。0是最基本的圖像級別,n表示第N級貼圖細化級別 description#>
@param internalformat#> 指定紋理中的顏色格式。可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等幾種。 description#>
@param width#> 紋理的寬度 description#>
@param height#> 高度 description#>
@param border#> 紋理的邊框寬度,必須為0 description#>
@param format#> 像素數據的顏色格式, 不需要和internalformatt取值必須相同。可選的值參考internalformat。 description#>
@param type#> 指定像素數據的數據類型。可以使用的值有GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,GL_UNSIGNED_SHORT_5_5_5_1等。 description#>
@param pixels#> 指定內存中指向圖像數據的指針 description#>
@return <#return value description#>
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED_EXT, width, height, 0, GL_RED_EXT, GL_UNSIGNED_BYTE, blackData);
//綁定U紋理
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXU]);
//加載紋理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED_EXT, width/2, height/2, 0, GL_RED_EXT, GL_UNSIGNED_BYTE, blackData + width * height);
//綁定V數據
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXV]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED_EXT, width/2, height/2, 0, GL_RED_EXT, GL_UNSIGNED_BYTE, blackData + width * height * 5 / 4);
//釋放malloc分配的內存空間
free(blackData);
}
/**
清除畫面
*/
-(void)clearFrame{
if ([self window])
{
[EAGLContext setCurrentContext:_glContext];
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
[_glContext presentRenderbuffer:GL_RENDERBUFFER];
}
}
/**
顯示YUV數據
@param data YUV數據
@param w <#w description#>
@param h <#h description#>
*/
-(void)displayYUV420pData:(void *)data width:(NSInteger)w height:(NSInteger)h{
if (!self.window) {
return;
}
//加互斥鎖,防止其他線程訪問
@synchronized (self) {
if (w != _videoW || h != _videoH) {
[self setVideoSize:(GLuint)w height:(GLuint)h];
}
//設置當前上下文
[EAGLContext setCurrentContext:_glContext];
//綁定
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXY]);
/**
更新紋理
https://my.oschina.net/sweetdark/blog/175784
@param target#> 指定目標紋理,這個值必須是GL_TEXTURE_2D。 description#>
@param level#> 執行細節級別。0是最基本的圖像級別,n表示第N級貼圖細化級別 description#>
@param xoffset#> 紋理數據的偏移x值 description#>
@param yoffset#> 紋理數據的偏移y值 description#>
@param width#> 更新到現在的紋理中的紋理數據的規格寬 description#>
@param height#> 高 description#>
@param format#> 像素數據的顏色格式, 不需要和internalformatt取值必須相同。可選的值參考internalformat。 description#>
@param type#> 顏色分量的數據類型 description#>
@param pixels#> 指定內存中指向圖像數據的指針 description#>
@return <#return value description#>
*/
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)w, (GLsizei)h, GL_RED_EXT, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXU]);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)w/2, (GLsizei)h/2, GL_RED_EXT, GL_UNSIGNED_BYTE, data + w * h);
glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXV]);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)w/2, (GLsizei)h/2, GL_RED_EXT, GL_UNSIGNED_BYTE, data + w * h * 5 / 4);
//渲染
[self render];
}
#ifdef DEBUG
GLenum err = glGetError();
if (err != GL_NO_ERROR)
{
printf("GL_ERROR=======>%d\n", err);
}
struct timeval nowtime;
gettimeofday(&nowtime, NULL);
if (nowtime.tv_sec != _time.tv_sec)
{
printf("視頻 %ld 幀率: %d\n", self.tag, _frameRate);
memcpy(&_time, &nowtime, sizeof(struct timeval));
_frameRate = 1;
}
else
{
_frameRate++;
}
#endif
}
/**
渲染
*/
-(void)render{
//設置上下文
[EAGLContext setCurrentContext:_glContext];
CGSize size = self.bounds.size;
//把數據顯示在這個視窗上
glViewport(1, 1, size.width * _viewScale -2, size.height * _viewScale -2);
/*
我們如果選定(0, 0), (0, 1), (1, 0), (1, 1)四個紋理坐標的點對紋理圖像映射的話,就是映射的整個紋理圖片。如果我們選擇(0, 0), (0, 1), (0.5, 0), (0.5, 1) 四個紋理坐標的點對紋理圖像映射的話,就是映射左半邊的紋理圖片(相當于右半邊圖片不要了),相當于取了一張320x480的圖片。但是有一點需要注意,映射的紋理圖片不一定是“矩形”的。實際上可以指定任意形狀的紋理坐標進行映射。下面這張圖就是映射了一個梯形的紋理到目標物體表面。這也是紋理(Texture)比上一篇文章中記錄的表面(Surface)更加靈活的地方。
*/
static const GLfloat squareVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
static const GLfloat coordVertices[] = {
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};
//更新屬性值
glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, squareVertices);
//開啟定點屬性數組
glEnableVertexAttribArray(ATTRIB_VERTEX);
glVertexAttribPointer(ATTRIB_TEXTURE, 2, GL_FLOAT, 0, 0, coordVertices);
glEnableVertexAttribArray(ATTRIB_TEXTURE);
//繪制
//當采用頂點數組方式繪制圖形時,使用該函數。該函數根據頂點數組中的坐標數據和指定的模式,進行繪制。
//繪制方式,從數組的哪一個點開始繪制(一般為0),頂點個數
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//將該渲染緩沖區對象綁定到管線上
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//把緩沖區(render buffer和color buffer)的顏色呈現到UIView上
[_glContext presentRenderbuffer:GL_RENDERBUFFER];
}
@end