iOS 9和10中AVPlayer的status属性KVO不触发问题求助
解决iOS 9/10中AVPlayer status属性KVO不触发的问题
我之前也踩过这个坑!在iOS 9和10上AVPlayer的status KVO死活不触发,但iOS 11及以上完全正常,核心问题出在KVO注册时机和AVPlayer的状态流转逻辑上,结合你的代码我来拆解下:
问题原因
1. KVO添加时机太晚
在iOS 9/10的AVFoundation实现中,当你用AVPlayerItem初始化AVPlayer后,系统会立刻开始加载媒体资源,如果这时候你才去给AVPlayer添加status的KVO,很可能此时status已经变成AVPlayerStatusReadyToPlay了——KVO只会在状态变化发生后发送通知,错过初始状态变化的话自然收不到回调。而iOS 11调整了状态更新的时机,允许初始化后补加KVO仍能收到通知,所以没这个问题。
2. 提前获取未加载完成的资源时长
你代码里直接通过item.asset.duration获取时长,这时候asset的元数据还没加载完成,得到的是无效的CMTime(值为NaN),这种异常值可能间接干扰AVPlayer的状态流转逻辑,进一步导致KVO通知异常。
解决方案
方案一:调整KVO注册顺序(优先推荐)
先创建空的AVPlayer,先注册status的KVO,再关联AVPlayerItem,确保KVO在状态变化前就已经生效:
- (void)loadVideo { AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:VIDEO_URL]]; // 1. 先创建空的AVPlayer self.player = [[AVPlayer alloc] init]; // 2. 先添加KVO监听,确保不会错过状态变化 [self.player addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; // 3. 再关联playerItem [self.player replaceCurrentItemWithPlayerItem:item]; AVPlayerLayer *layer = (AVPlayerLayer *)self.playerView.layer; [layer setPlayer:self.player]; __weak ViewController *weakSelf = self; self.token = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.1, NSEC_PER_SEC) queue:nil usingBlock:^(CMTime time) { Float64 currentTime = CMTimeGetSeconds(time); // 实时从currentItem获取有效时长 Float64 duration = CMTimeGetSeconds(weakSelf.player.currentItem.asset.duration); if (!isnan(duration)) { [weakSelf updateTimeLabel:currentTime duration:duration]; } }]; }
方案二:监听AVPlayerItem的status(备选)
AVPlayer的status其实依赖于当前AVPlayerItem的状态,直接监听AVPlayerItem的status有时候更可靠:
- (void)loadVideo { AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:VIDEO_URL]]; // 先给playerItem添加status监听 [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; self.player = [[AVPlayer alloc] initWithPlayerItem:item]; AVPlayerLayer *layer = (AVPlayerLayer *)self.playerView.layer; [layer setPlayer:self.player]; __weak ViewController *weakSelf = self; self.token = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.1, NSEC_PER_SEC) queue:nil usingBlock:^(CMTime time) { Float64 currentTime = CMTimeGetSeconds(time); Float64 duration = CMTimeGetSeconds(item.asset.duration); if (!isnan(duration)) { [weakSelf updateTimeLabel:currentTime duration:duration]; } }]; } // 对应的observeValueForKeyPath修改 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"status"]) { if ([object isKindOfClass:[AVPlayerItem class]]) { AVPlayerItem *item = (AVPlayerItem *)object; switch (item.status) { case AVPlayerItemStatusReadyToPlay: { NSLog(@"player item's status changed to AVPlayerItemStatusReadyToPlay"); // 此时获取的时长才是有效的 Float64 duration = CMTimeGetSeconds(item.asset.duration); [self updateTimeLabel:0.0 duration:duration]; } break; case AVPlayerItemStatusFailed: NSLog(@"player item load failed: %@", item.error); break; default: NSLog(@"player item's status changed"); break; } [item removeObserver:self forKeyPath:@"status"]; } // 如果同时用了方案一的AVPlayer监听,保留对应的处理逻辑 } }
额外注意事项
- 务必在销毁时移除所有观察者:避免内存泄漏或崩溃,在控制器的
dealloc方法中清理:
- (void)dealloc { if (self.player) { [self.player removeObserver:self forKeyPath:@"status" error:nil]; if (self.token) { [self.player removeTimeObserver:self.token]; } } if (self.player.currentItem) { [self.player.currentItem removeObserver:self forKeyPath:@"status" error:nil]; } }
- 不要提前获取未加载的资源元数据:时长、分辨率等元数据需要等asset加载完成后才能拿到,必须在
status变为ReadyToPlay后再获取。
内容的提问来源于stack exchange,提问作者inexcii
相关产品推荐
相关产品推荐

