IOS播放YUV文件OpenGLES(九)

前言

前面介绍了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播放器流程图如下:


1560652048790.jpg

顺着上面的流程图分析一下实现思路
1、yuv数据队列:YUV文件中有很多连续的YUV视频帧数据,所以我们得实现一个视频队列缓冲专门保存视频数据
2、yuv视频数据提供者:yuv视频数据来源于文件,所以还需要一个线程专门从文件中读取yuv数据然后发送给yuv数据队列
3、渲染:以前渲染一张图片等等都是在主线程中实现的,这里是渲染连续的视频数据,显然不能在主线程做了,所以要单独开一个线程做渲染。渲染线程根据设定的帧率(比如24fps)那么每隔50ms 就向yuv数据队列获取一帧视频数据进行渲染

以上就是实现一个yuv播放器的设计,其实就是一个简单的生产者-消费者模型下面详细讲解实现的关键步骤

实现思路

1、yuv视频数据源提供类
该类要实现如下功能:
一、向外暴露接口,用于提供yuv视频数据
二、开始提供数据和停止提供数据的接口供外部调用
三、提供初始化方法,初始化方法中要指定yuv文件地址,视频的宽和高,宽高必须指定,否则无法正确读取yuv数据
四、读取yuv数据的工作要在一个独立的线程中
头文件代码如下:

@protocol VideoFileSourceProtocol <NSObject>

- (void)pushYUVFrame:(VideoFrame*)video;

// 视频流没有了
- (void)didFinishVideoData;
@end

@interface VideoFileSource : NSObject
@property (assign, nonatomic) id<VideoFileSourceProtocol>delegate;
@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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345