http://ivanyuan.farbox.com/post/shi-pin-bo-fang-de-na-xie-keng
最近一直在做视频动态挂件以及一个视频播放的功能,在开始做之前,先学习了苹果的官方文档RosyWriter,熟悉了短视频拍摄、滤镜处理的一些小技巧,同学也学习了下GPUImage,最后在踩了很多坑以后才实现了视频挂件的处理。
这次主要是总结和记录下视频播放遇到的坑,视频播放采用的是AVPlayer
这个控件,语法大致如下:
NSURL * url = [NSURL fileURLWithPath:@"视频地址"]; AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url]; self.player = [AVPlayer playerWithPlayerItem:playerItem]; [self.player addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; self.player.actionAtItemEnd = AVPlayerActionAtItemEndNone; self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; self.playerLayer.frame = self.view.bounds; [self.view.layer addSublayer:self.playerLayer];
这里要监听一下AVPlayer
的status
属性,当status
的状态变为AVPlayerStatusReadyToPlay
时,说明视频就可以播放了,此时我们调用[self.player play];
就好了。
如果是AVPlayerStatusFailed
说明视频加载失败,这时可以通过self.player.error.description
属性来找出具体的原因。
status
变为AVPlayerStatusReadyToPlay
后,我们调用play
方法真的就能保证视频正常播放吗? 众所周知,AVPlayer
支持的视频、音频格式非常广泛,抛开那些无法正常编解码的情况,在某些情况下其可能就是无法正常播放。
AVPlayer
在进行播放时,会预先解码一些内容,而此时如果我们的App使用CPU过多,I/O读写过多时,有可能导致视频播放声/画不同步,这点尤其在iPhone4上面表现更为明显。
而如果是发生在AVPlayer
初始化解码视频的时候,有可能导致视频直接无法播放,这时,我们再调用play
或者seekToTime:
方法都无法正常播放。
建议不要在CPU或者I/O很频繁的情况下使用AVPlayer,例如刚登录App加载各种数据的情况下,可以等App预热以后再使用。
rate
属性的值大于0后,真的就在播放视频了吗?答案是否定的,当发生上面所讲的情况时,我打印了当前的rate情况,是大于0的,但是页面上显示的情况却还是什么也没有。
有时候我们如果想要在视频一播放的时候去做一些事情,例如设置一下播放器的背景色,如果我们仅仅是监听这个rate可能无法100%保证有效,而如果我们真的要监听这种情况的话,有一个取巧的方法
id _timerObserver = [self.player addBoundaryTimeObserverForTimes:@[[NSValue valueWithCMTime:CMTimeMake(1, 30)]] queue:dispatch_get_main_queue() usingBlock:^{ //do something }];
另外如果不需要监听播放进度的时候可以调
[self.player removeTimeObserver:_timerObserver];
AVPlayer
前后台播放的那些问题当我们切换到后台后,这时AVPlayer通常会自动暂停,当然如果设置了后台播放音频的话,是可以在后台继续播放声音的,正如苹果自己的WWDC这个App一样。
如果我们想要在程序切回来前台继续播放的话,我们需要监听两个通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
先在appWillResignActive:方法中记录当前播放的时间CMTime
- (void)appWillResignActive:(NSNotification *)notification { if (self.player) { [self.player pause]; self.time = self.player.currentTime; } }
等到切回前台的时候再继续播放
@try { [self.player seekToTime:self.time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { if (finished) { [self.player play]; } }]; } @catch (NSException *exception) { [self.player play]; }
这里如果我们只是调用[self.player play];
,则在继续播放的时候可能会后退一定的时间,而如果我们想要精准地继续播放则需要下面这个方法,toleranceBefore:
与toleranceAfter:
均设置成kCMTimeZero
.
[self.player seekToTime:self.time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:
当然这个方法也会有一些问题,例如在刚启动播放的时候,以及在播放到最后一帧的时候,首先是其有可能会出现异常并crash.
所以我们用了@try@catch来捕获这个异常,当出现异常的时候直接调用play
让播放器自己决定播放的进度。
另外一个问题是其在最后一帧的时候有可能会白屏,因为最后一帧的内容有可能是空的,或者其它一些特殊的中间情况,所以在视频快要播放结束的时候建议,直接使用play
方法。
iOS系统有如下几种声音播放模式
enum { kAudioSessionCategory_AmbientSound = 'ambi', kAudioSessionCategory_SoloAmbientSound = 'solo', kAudioSessionCategory_MediaPlayback = 'medi', kAudioSessionCategory_RecordAudio = 'reca', kAudioSessionCategory_PlayAndRecord = 'plar', kAudioSessionCategory_AudioProcessing = 'proc' };
App运行的时候通常只能使用一种声音播放模式,而如果我们在录制视频或者录制声音的时候,把模式设置成了kAudioSessionCategory_RecordAudio
,这个时候如果我们使用AVPlayer播放视频,可能就无法播放视频。
这个时候我们需要把模式切换成kAudioSessionCategory_MediaPlayback
或者其它合适的模式,切换模式的代码如下:
UInt32 category = kAudioSessionCategory_MediaPlayback; UInt32 size = sizeof(category); AudioSessionGetProperty(kAudioSessionProperty_AudioCategory, &size, &category); if (category != kAudioSessionCategory_MediaPlayback) { category = kAudioSessionCategory_MediaPlayback; AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, size, &category); QLog_Event(MODULE_IMPB_RICHMEDIA,"change route category to media play back."); }
关于上面这几个模式的作用这儿有比较详细的解释,如果我们需要在用户静音时,不播放声音,可以选择kAudioSessionCategory_SoloAmbientSound
.
如果用户当时在后台听音乐,如QQ音乐,或者喜马拉雅这些App,这个时候播放视频后,其会被我们打断,当我们不再播放视频的时候,自然需要继续这些后台声音的播放。
首先,我们需要先向设备注册激活声音打断AudioSessionSetActive(YES);
,当然我们也可以通过[AVAudioSession sharedInstance].otherAudioPlaying;
这个方法来判断还有没有其它业务的声音在播放。
当我们播放完视频后,需要恢复其它业务或App的声音,这时我们可以调用如下方法:
OSStatus ret = AudioSessionSetActiveWithFlags(NO, kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation);
1、在用户插入和拔出耳机时,有可能也会导致视频暂停。
其实插、拔耳机是属性改变声音输出设备的一种方式,其次还有修改为听筒、扬声器,或者其它蓝牙设备输出。相关代码如下:
AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, audioRouteChangeListenerCallback, (__bridge void*)self); void audioRouteChangeListenerCallback ( void *inUserData, AudioSessionPropertyID inPropertyID, UInt32 inPropertyValueS, const void *inPropertyValue ) { UInt32 propertySize = sizeof(CFStringRef); AudioSessionInitialize(NULL, NULL, NULL, NULL); CFStringRef state = nil; //获取音频路线 AudioSessionGetProperty(kAudioSessionProperty_AudioRoute ,&propertySize,&state);//kAudioSessionProperty_AudioRoute:音频路线 NSLog(@"%@",(NSString *)state);//Headphone 耳机 Speaker 喇叭. }
如果是输出设备发生变化,我们如果要继续播放视频的话,我们只需监听到设备变化时调用play
就好了.
2、性能问题
其实在UITableView中使用AVPlayer播放多个视频时,是很容易出现性能问题的,当然这个时候我们也通常是静音的,不然多个视频一起播声音,没有人会承受得了。
当然你可以选择muted
以及把volume
设置为0来达到目的。在TableViewCell重用时,我们也可以使用pause
方法来暂停之前的视频,并使用- (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item
方法来加载一个新的视频。
使用这样的一个套路,可能仍然无法解决切换视频时带来的卡顿,尤其在视频内容比较多的时候。关于这个问题,微信内部自己写了一个简易版的AVAssetReader+AVAssetReaderTrackOutput组件,在静音模式下播放列表里面的视频,同时也不用考虑播放模式了,微信博客链接为iOS小视频优化心得。
3、内存泄漏问题
当我们释放一个正在播放的视频时,需要先调用pause
方法,如果由于某些原因,例如切前后台时,导致又调用了play
方法,那么有可能会hold住内存空间而导致内存泄漏。
4、获取视频缩略图
获取首帧视频截图的方法如下:
AVAssetImageGenerator *imageGen = [[AVAssetImageGenerator alloc] initWithAsset:self.source]; if (imageGen) { imageGen.appliesPreferredTrackTransform = YES; CMTime actualTime; CGImageRef cgImage = [imageGen copyCGImageAtTime:CMTimeMakeWithSeconds(0, 30) actualTime:&actualTime error:NULL]; if (cgImage) { UIImage *image = [UIImage imageWithCGImage:cgImage]; CGImageRelease(cgImage); return image; } }
视频播放只是整个富媒体的一小部分,在拍摄短视频时,各种参数的应用,对于视频的后期美颜、滤镜、着色、人脸识别,视频压缩等技术这些才是真正的难点。