细数AVPlayer的那些坑

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];

这里要监听一下AVPlayerstatus属性,当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.


其它App播放声音打断问题

如果用户当时在后台听音乐,如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;
            }
        }

总结:

视频播放只是整个富媒体的一小部分,在拍摄短视频时,各种参数的应用,对于视频的后期美颜、滤镜、着色、人脸识别,视频压缩等技术这些才是真正的难点。


  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值