AVPlayer初体验之视频解纹理

文章已发布在我的博客上,如需转载,请注明原文出处

AVPlayer是苹果提供的用来管理多媒体播放的控制器,提供了播放所需要的控制接口和支持KVO的属性,支持播放本地和网络视频,以及实时视频流。它一次只能播放一个AVPlayerItem,如果需要切换媒体源,需要使用replaceCurrentItem(with:)函数。如果需要播放多个视频,可以考虑使用AVQueuePlayer。在不同性能的设备上,甚至相同设备的不同iOS版本上,AVPlayer的最大支持清晰度都会不一样,例如在iOS10的某些机器上不支持4k播放,但是到iOS11就支持了,关于测定视频是否可以用AVPlayer来解码,可以直接在safari中输入视频网址来测试。

如果只需要播放视频,可以直接使用CALayer的子类AVPlayerLayer。这里不做过多的说明,可以查看苹果的Demo代码
这里主要说明从AVPlayerOutput中获取视频纹理的以用于OpenGl的下一步处理。

进度、播放状态控制

播放信息监听

利用KVO和通知中心监听以下Key即可,虽然KVO的机制不太推荐使用,但是看了官方文档,确实说这么用。

//已缓存进度
self.playerItem!.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.new, context: nil)
//状态改变
self.playerItem!.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
//缓冲
self.playerItem!.addObserver(self, forKeyPath: "playbackBufferEmpty", options: NSKeyValueObservingOptions.new, context: nil)
//缓冲可播
self.playerItem!.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: NSKeyValueObservingOptions.new, context: nil)
//播放完成
NotificationCenter.default.addObserver(self, selector: #selector(didPlayToEnd(notify:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)

状态控制

所有的状态控制都需要在AVPlayerItemStatus变成readyToPlay的时候才可以使用,并且只有这个时候可以取到视频的Size,所以在KVO的回调里

if keyPath == "status"{
    switch (object as! AVPlayerItem).status {
        case .readyToPlay:
            // 只有在这个状态下才能播放
            //准备就绪
            let pixelBuffer:CVPixelBuffer? = self.videoOutPut.copyPixelBuffer(forItemTime:(self.playerItem!.currentTime()), itemTimeForDisplay: nil)
            if(pixelBuffer != nil){
                //获取size
                let width:Int = CVPixelBufferGetWidth(pixelBuffer!)
                let height:Int = CVPixelBufferGetHeight(pixelBuffer!)
                self.playerItem?.videoSize = CGSize.init(width: width, height: height)
            }
            self.notify(state: .prepared)
            if(self.shouldPlayAfterPrepared)
            {
                self.play()
            }
        case .unknown:
                self.notify(state: .unknown)
                //print("视频加载未知错误")
        case .failed:
                self.notify(state: .failed,error: self.avPlayer?.error)
                //print("视频加载错误,\(String(describing: self.avPlayer?.error))")
            }
}

如果播放遇到错误可以用self.avPlayer?.error来查看错误类型。

输出纹理

YUV纹理

由于视频的编码格式基本都是YUV420,可以查看苹果的Demo代码 ,通过AVPlayerItemVideoOutput获取Y-PannelUV-Pannel两张纹理,最后在Shader中对两种纹理组合处理。

设置AVPlayerItemVideoOutput的部分代码

NSDictionary *pixBuffAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)};
self.videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];

输出纹理的部分代码

//Y-Plane
glActiveTexture(GL_TEXTURE0);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _videoTextureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RED_EXT, frameWidth, frameHeight, GL_RED_EXT, GL_UNSIGNED_BYTE, 0, &_lumaTexture);
//UV-plane
glActiveTexture(GL_TEXTURE1);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _videoTextureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RG_EXT, frameWidth / 2, frameHeight / 2, GL_RG_EXT, GL_UNSIGNED_BYTE, 1, &_chromaTexture);

其中的kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange是CoreVideo中指定的Pixel Format Identifiers 类型,在OpenGLES2环境下其对应的参数是GL_RED_EXTGL_RG_EXT。视频支持的PixelFormat格式如下

获取纹理之后,还要使用Shader混合两张纹理,片元着色器(.fsh)代码如下

void main()
{
    mediump vec3 yuv;
    lowp vec3 rgb;
    
    // Subtract constants to map the video range start at 0
    yuv.x = (texture2D(SamplerY, texCoordVarying).r - (16.0/255.0))* lumaThreshold;
    yuv.yz = (texture2D(SamplerUV, texCoordVarying).rg - vec2(0.5, 0.5))* chromaThreshold;
    
    rgb = colorConversionMatrix * yuv;

    gl_FragColor = vec4(rgb,1);
}

RGB纹理

首先要明白一点,上图中明确说明,BGRA的输出格式是420v的两倍多带宽(More than 2x bandwidth),并且在该图来源,WWDC的这个视频27:00位置明确说明420v的输出格式效率会明显高于BGRA的输出格式(It does come across if you can avoid using BGRA and doing your work in YUV, it's more efficient from bandwidth standpoint),但是反过来,对于OpenGL来说,两张纹理的性能又会低于一张纹理。而且直接使用使用BGRA毕竟会方便很多,因为输出的直接就是一张纹理,个人认为在iOS5时代可能需要考虑420和BGRA的输出效率,但是现在毕竟都iOS11时代了,所以影响可以忽略不计。

设置AVPlayerItemVideoOutput的代码

NSDictionary *pixBuffAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
self.videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];

输出纹理的代码

CVReturn textureRet = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.videoTextureCache, pixelBuffer, nil, GL_TEXTURE_2D, GL_RGBA, width, height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &_textureOutput);

BGRA对应的输出格式是kCVPixelFormatType_32BGRA,其对应的从Buffer读纹理的参数是GL_RGBAGL_BGRA

完整的从VideoOutput中获取纹理的代码如下

-(CVOpenGLESTextureRef)getVideoTextureWithOpenGlContext:(EAGLContext *)context{
    if(self.videoOutput == nil){
        NSLog(@"ferrisxie: 输出对象为空");
        return nil;
    }
    //step1:构造缓存
    if(self.videoTextureCache == nil){
        CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, nil, context, nil, &_videoTextureCache);
        if(ret != 0){
            NSLog(@"构造缓存失败");
            return nil;
        }
    }
    //step2: 取纹理
    CMTime currentTime = self.currentItem.currentTime;
    if(![self.videoOutput hasNewPixelBufferForItemTime:currentTime]){
        //没有新的纹理 返回上一帧
        return self.textureOutput;
    }
    CVPixelBufferRef pixelBuffer = [self.videoOutput copyPixelBufferForItemTime:currentTime itemTimeForDisplay:nil];
    CGFloat width = CVPixelBufferGetWidth(pixelBuffer);
    CGFloat height = CVPixelBufferGetHeight(pixelBuffer);
    if(CGSizeEqualToSize(CGSizeZero, self.videoSize)){
        self.videoSize = CGSizeMake(width, height);
    }
    CVOpenGLESTextureCacheFlush(self.videoTextureCache, 0);
    if(self.textureOutput != nil){
        //释放上一帧
        CFRelease(self.textureOutput);
        self.textureOutput = nil;
    }
    CVReturn textureRet = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.videoTextureCache, pixelBuffer, nil, GL_TEXTURE_2D, GL_RGBA, width, height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &_textureOutput);
    if(textureRet != 0){
        NSLog(@"解析纹理失败%u,%@",textureRet,self.textureOutput);
        if(self.textureOutput != nil){
            //解析纹理失败不需要Release
            //CFRelease(self.textureOutput);
            self.textureOutput = nil;
        }
        return nil;
    }
    if(pixelBuffer != nil){
        CVPixelBufferRelease(pixelBuffer);
    }
    
    return self.textureOutput;
}
//usage
CVOpenGLESTextureRef texureRef = [self.player getVideoTextureWithOpenGlContext:[EAGLContext currentContext]];
GLuint target = CVOpenGLESTextureGetTarget(texureRef);
GLuint name = CVOpenGLESTextureGetName(texureRef);
    //用完记得释放
CFRelease(texureRef);

Swift由于取消了CFRelease等CoreFoundation的内存管理接口,在取纹理的时候需要使用Unmanaged对象,利用takeUnretainedValue,可以不需要释放代码了。

if let videoPlayer = self.videoPlayer{
    if let unmangaed:Unmanaged<CVOpenGLESTexture> = videoPlayer.getVideoTexture(withOpen: self.context){
        let testure:CVOpenGLESTexture = unmangaed.takeUnretainedValue()
        let target:GLuint = CVOpenGLESTextureGetTarget(testure)
        let name:GLuint = CVOpenGLESTextureGetName(testure)
    }
}
//不再需要释放了

其他

切换播放源

针对需要切换播放源的场景,重新构造播放器显然是最简单易行的,但是测试发现,频繁的构造和销毁AVPlayer对象虽然不会导致内存增加,但是很奇怪的是,会导致OtherProccesses的内存增大,从而导致Free内存减小,减小到某个值的时候,就会触发didReceiveMemeoryWarning内存警告,暂时还没有发现原因,因此这种方法不可取。

其实AVPlayer本身提供了切换播放源的函数。

func replaceCurrentItem(with item: AVPlayerItem?)

当要切换播放源时,需要指定新的AVPlayerItem,这时候又会面临状态问题,之前说过只有在AVPlayerItemStatus变成readyToPlay的时候才可以调用playseek等函数,可以使用AVUrlAsset来预加载这个Item:

func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -> Void)? = nil)

通过预加载duration(视频总进度)来判断视频是否可播放,当加载完成后再replaceCurrentItem

// Load the asset's "playable" key
asset.loadValuesAsynchronously(forKeys: ["duration"]) {
    var error: NSError? = nil
    let status = asset.statusOfValue(forKey: "duration", error: &error)
    switch status {
    case .loaded:
    // Sucessfully loaded, continue processing
    //在这里替换播放源,并且直接开始播放
    let playerItem = AVPlayerItem.init(asset: asset)
    self.videoPlayer?.replaceCurrentItem(with: playerItem)
    self.resumePlay()
    case .failed:
    // Examine NSError pointer to determine failure
    case .cancelled:
    // Loading cancelled
    default:
        // Handle all other cases
    }
}

如果实在需要控制多个播放源,可以考虑使用AVQueuePlayer来处理。

声音优先级

默认的声音优先级为视频播放的默认优先级AVAudioSessionCategoryAmbient,静音状态不会有声音,退出后台就停止播放。AudioSessionCategoriesandModes有关于声音优先级的介绍。
使用如下函数切换

AVAudioSession.sharedInstance().setCategory(_ category: String)

一般的,如果需要静音状态下也有声音可以直接使用AVAudioSessionCategoryPlayback这个Value。

硬件加速

iOS6以后可以使用底层框架VideoToolbox来实现硬解码,具体视频工具箱和硬件加速有很清楚的解释,基本的场景,使用AVPlayer即可满足需求。

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

推荐阅读更多精彩内容