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-Pannel
和UV-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_EXT
和GL_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_RGBA
和GL_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
的时候才可以调用play
和seek
等函数,可以使用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即可满足需求。