前言
前面介绍了opengl es上下文环境搭建,如何渲染RGB图片。基本上有了这些基础,那么做出一个能播放YUV文件的播放就易如反掌了。为什么要写一个播放YUV的播放器呢?离写一个万能播放器的距离还有多远呢?答案是不远了。学习要讲究一个循序渐进,一个播放器需要解复用,音视频解码,音视频同步,音视频渲染等等很多知识模块,我们先一个个模块的完成,学完本章你就完成了学会了视频渲染功能。至于音频渲染功能,请参考前面文章
ios渲染音频PCM之AudioUnit
安卓渲染音频PCM之OpenSL ES
安卓渲染音频PCM之AudioTrack
opengl es系列文章
opengl es之-基础概念(一)
opengl es之-GLSL语言(二)
opengl es之-GLSL语言(三)
opengl es之-常用函数介绍(四)
opengl es之-渲染两张图片(五)
opengl es之-在图片上添加对角线(六)
opengl es之-离屏渲染简介(七)
opengl es之-CVOpenGLESTextureCache介绍(八)
opengl es之-播放YUV文件(九)
1、先简单介绍下YUV
RGB我们都熟悉,它可以表示任何颜色,一张图片最后到了内存中也是RGB的像素数据。那YUV是什么呢?它也是类似于RGB的另外一种颜色空间,但是它比RGB更加节省空间,通分辨率一张RGB图片大小是YUV图片的2倍。视频是由连续的图片组成,所以每一帧(一张)视频其实就是一张"图片",所以显然用YUV来表示视频的颜色显然比RGB节省空间。YUV的另外一个用处是它能很好的兼容电视系统的黑白和彩色制式,它有很多格式比如YUV420P YUV422等等,YUV和RGB还可以相互转换,不同的YUV的制式标准转换系数也不一样,这里以BT.709为例转换公式如下:
R 1.1644, 1.1644, 1.1644 Y
G = 0.0, -0.2132, 2.1124 U
B 1.7927, -0.5329, 0.0 V
总之:我们的得到两个结论
1、视频的原始颜色数据用的是YUV颜色空间(因为省空间,当然也可以RGB,空间多的土豪另说)
2、YUV的纹理和RGB纹理加载流程一样
3、至于着色器中的渲染流程后面介绍
2、YUV播放器流程图如下:
顺着上面的流程图分析一下实现思路
1、yuv数据队列:YUV文件中有很多连续的YUV视频帧数据,所以我们得实现一个视频队列缓冲专门保存视频数据
2、yuv视频数据提供者:yuv视频数据来源于文件,所以还需要一个线程专门从文件中读取yuv数据然后发送给yuv数据队列
3、渲染:以前渲染一张图片等等都是在主线程中实现的,这里是渲染连续的视频数据,显然不能在主线程做了,所以要单独开一个线程做渲染。渲染线程根据设定的帧率(比如24fps)那么每隔50ms 就向yuv数据队列获取一帧视频数据进行渲染
以上就是实现一个yuv播放器的设计,其实就是一个简单的生产者-消费者模型下面详细讲解实现的关键步骤
实现思路
1、yuv视频数据源提供类
该类要实现如下功能:
一、向外暴露接口,用于提供yuv视频数据
二、开始提供数据和停止提供数据的接口供外部调用
三、提供初始化方法,初始化方法中要指定yuv文件地址,视频的宽和高,宽高必须指定,否则无法正确读取yuv数据
四、读取yuv数据的工作要在一个独立的线程中
头文件代码如下:
@protocol VideoFileSourceProtocol
- (void)pushYUVFrame:(VideoFrame*)video;
// 视频流没有了
- (void)didFinishVideoData;
@end
@interface VideoFileSource : NSObject
@property (assign, nonatomic) iddelegate;
@property (strong, nonatomic) NSURL *fURL;
@property (strong, nonatomic) NSThread *workThread;
@property (assign, nonatomic) BOOL isPull;
@property (assign, nonatomic) int width;
@property (assign, nonatomic) int height;
- (id)initWithFileUrl:(NSURL*)fileUrl;
// 设置yuv中的视频宽和高 很重要
- (void)setVideoWidth:(int)width height:(int)height;
- (void)beginPullVideo;
- (void)stop;
@end
VideoFrame是一个结构体,它包含了一帧视频的YUV数据,宽 高等基本信息
struct VideoFrame_ {
uint8_t *luma; // Y
uint8_t *chromaB; // U
uint8_t *chromaR; // V
// 视频帧的长宽
int width;
int height;
// 当使用CVOpenGLESTextureCacheRef的缓冲区时该字段有效。此时前面三个字段为NULL
void *cv_pixelbuffer;
int full_range; // 是否是full range的视频
};
那如何从YUV文件中读取数据呢?YUV文件中YUV三个分量是如何排列的呢?
不管是什么格式的YUV,都是按照一帧一帧的顺序存储的,只是每一帧YUV三个分量的存储顺序略有不同,这里以常用的YUV420格式来说明。假如YUV文件的宽和高分别为width和height,那么一帧的存储顺序为:
y(widthheight个字节)u(width/2height/2个字节)v(width/2*height/2个字节)
这样读取的逻辑就很清晰了,只需要用一个while循环不停的一帧帧读取就行了
while (![NSThread currentThread].isCancelled) {
// 读取YUV420 planner格式的视频数据,其一帧视频数据的大小为 宽*高*3/2;
VideoFrame *frame = (VideoFrame*)malloc(sizeof(VideoFrame));
frame->luma = (uint8_t*)malloc(self.width * self.height);
frame->chromaB = (uint8_t*)malloc(self.width * self.height/4);
frame->chromaR = (uint8_t*)malloc(self.width * self.height/4);
frame->width = self.width;
frame->height = self.height;
frame->cv_pixelbuffer = NULL;
frame->full_range = 0;
size_t size = fread(frame->luma, 1, self.width * self.height, yuvFile);
size = fread(frame->chromaB, 1, self.width * self.height/4, yuvFile);
size = fread(frame->chromaR, 1, self.width * self.height/4, yuvFile);
if (size == 0) {
NSLog(@"读取的数据字节为0");
if ([self.delegate respondsToSelector:@selector(didFinishVideoData)]) {
[self.delegate didFinishVideoData];
}
break;
}
if ([self.delegate respondsToSelector:@selector(pushYUVFrame:)]) {
[self.delegate pushYUVFrame:frame];
}
usleep(usec_per_fps);
}
读取完一帧,则通过接口pushYUVFrame推送出去
2、yuv数据队列
这个队列用于保存来自VideoFileSource提供的yuv数据,同时向渲染模块提供yuv数据,它要是线程安全的,同时提供增加和移除VideoFrame的接口,这里用一个数组加pthread_mutex_t和pthread_cond_t实现
/** 这里设计一个队列,用于保存原始的视频帧,作为管理视频帧的缓冲队列
* 因为这里是直接播放YUV裸数据,所以缓冲的是原始视频帧
* tips:一般做视频播放器的时候缓冲队列不会保存原始的视频帧,一般都是保存压缩的视频帧,因为原始的视频帧数据过大,
* 可以算一下,1080P 30fps 的视频,缓冲一秒占用内存 1080x1920x1.5x30 = 90M
*/
// 用于缓存原始视频帧的数组队列
VideoFrame *_videoFrame[Video_cache_lenght];
int _count;
int _head;
int _tail;
// 保证该队列安全性的锁
pthread_mutex_t _videoMutex;
pthread_cond_t _videoCond;
3、渲染
首先渲染要在单独的渲染线程中进行,同时按照指定的渲染帧率进行渲染。
基本流程就是,先到yuv数据队列中获取数据,如果没有则等待2秒,有则进行渲染
- (void)renderThreadRunloop
{
NSLog(@"decodeThreadRunloop begin");
[self.lock lock];
self.mRenderThreadRun = YES;
while (![NSThread currentThread].isCancelled &&(![self isEmpty] || !self.mVideoDidFnish)) {
@autoreleasepool {
NSLog(@"开始渲染");
VideoFrame *frame = NULL;
[self pullVideo:&frame];
if (frame && frame->luma == NULL) { //说明暂时没有视频数据
NSLog(@"没有数据");
continue;
}
[self.renderView rendyuvFrame:frame];
NSLog(@"结束渲染");
// 用完后释放内存
if (frame) {
[self freeVideoFrame:frame];
}
usleep(usec_per_fps);
}
}
[self.lock unlock];
NSLog(@"decodeThreadRunloop end");
}
具体的渲染代码是GLVideoView类的
- (void)rendyuvFrame:(VideoFrame*)yuvFrame;
方法中
渲染流程与前面文章介绍的一模一样,包括
创建opengl es上下文,创建FBO帧缓冲,渲染缓冲,加载着色器,上传纹理、调用glDrawArrays()函数,最后调用
[_context presentRenderbuffer:GL_RENDERBUFFER];将渲染结果呈现到屏幕上,具体代码可以参考前面文章的Demo或者本项目的后面的Demo。这里说一下不同的地方
不同点:
一、随时切换opengl es上下文到当前线程
由于这里的渲染是在单独的子线程,而opengl es上下文的创建等等是在主线程的(因为GLVideoView是在主线程中初始化的),所以进行渲染之前一定要调用一下[EAGLContext setCurrentContext:_context];上下文环境切换到当前线程
二、上传YUV纹理
与上传RGB纹理略有不同,一帧RGB的纹理可以一次性上传给opengl es,但是一帧YUV的纹理要YUV分别传给opengl es
for (int i=0; i<3; i++) {
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, textureyuvs[i]);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
/**
* glTexSubImage2D与glTexImage2D区别就是:
* 前者不会创建用于传输纹理图片的内存,直接使用由glTexImage2D创建的内存,这样避免了内存的重复创建。
*/
if (!hasGenTexutre) {
if (i == 0) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame->width, frame->height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->luma);
} else if (i==1) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame->width/2, frame->height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->chromaB);
} else {
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame->width/2, frame->height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->chromaR);
}
} else {
if (i == 0) {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->width, frame->height, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->luma);
} else if (i==1) {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->width/2, frame->height/2, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->chromaB);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->width/2, frame->height/2, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->chromaR);
}
}
}
三、YUV片元着色器
YUV纹理变量要有三个,分别用于保存应用端传递过来的Y/U/V三个纹理分量,同时opengl es最终只能渲染RGB颜色,所以还需要将YUV颜色格式转换成RGB颜色格式
NSString *const yuvcolorFS = SHADER_STRING
(
varying highp vec2 v_texcoord;
uniform sampler2D texture_y;
uniform sampler2D texture_u;
uniform sampler2D texture_v;
uniform highp mat4 yuvToRGBmatrix;
uniform highp vec4 luminanceScale;
void main(){
highp vec4 color_yuv = vec4(texture2D(texture_y,v_texcoord).r + luminanceScale.x,
texture2D(texture_u,v_texcoord).r - 0.5,
texture2D(texture_v,v_texcoord).r - 0.5,
1.0)*luminanceScale.y;
highp vec4 color_rgb = yuvToRGBmatrix * color_yuv;
gl_FragColor = color_rgb;
}
);
这里的yuvToRGBmatrix就是前面说锁的YUV到RGB的转换矩阵了
其它步骤则与渲染RGB没有区别了
项目参考地址
Demo