前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AVPlayer初体验之视频解纹理

AVPlayer初体验之视频解纹理

作者头像
xferris
发布2018-06-01 15:41:27
3.4K0
发布2018-06-01 15:41:27
举报
文章被收录于专栏:慎独

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

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

进度、播放状态控制

播放信息监听

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

代码语言:javascript
复制
//已缓存进度
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的回调里

代码语言:javascript
复制
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的部分代码

代码语言:javascript
复制
NSDictionary *pixBuffAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)};
self.videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];

输出纹理的部分代码

代码语言:javascript
复制
//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)代码如下

代码语言:javascript
复制
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的代码

代码语言:javascript
复制
NSDictionary *pixBuffAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
self.videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];

输出纹理的代码

代码语言:javascript
复制
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中获取纹理的代码如下

代码语言:javascript
复制
-(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,可以不需要释放代码了。

代码语言:javascript
复制
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本身提供了切换播放源的函数。

代码语言:javascript
复制
func replaceCurrentItem(with item: AVPlayerItem?)

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

代码语言:javascript
复制
func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -> Void)? = nil)

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

代码语言:javascript
复制
// 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有关于声音优先级的介绍。 使用如下函数切换

代码语言:javascript
复制
AVAudioSession.sharedInstance().setCategory(_ category: String)

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

硬件加速

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进度、播放状态控制
    • 播放信息监听
      • 状态控制
      • 输出纹理
        • YUV纹理
          • RGB纹理
          • 其他
            • 切换播放源
              • 声音优先级
                • 硬件加速
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档