AVFoundationSimplePlayer 是 Apple 官方提供的一个使用 AVFoundation 进行视频播放的 Demo。麻雀虽小,五脏俱全,该 Demo 提供了包括视频加载,视频快进,视频快退,视频播放/暂停,播放进度显示功能。
加载视频
AVAsset 是一个用于表示定时视听媒体的抽象类,比如视频,音频。AVURLAsset 是 AVAsset 的具体子类,AVURLAsset 可以使用一个 local URL 或者 remote URL 来初始化 asset 。我们来看看 AAPLPlayerViewController 的 asynchronouslyLoadURLAsset: 方法,该方法调用 AVURLAsset 的 异步加载方法。
- (void)loadValuesAsynchronouslyForKeys:(NSArray *)keys completionHandler:(nullable void (^)(void))handler;
加载 asset 是耗时操作,该方法在子线程中操作 assert,并根据我们给定字段的值加载 assert 的属性,比如在这里需要处理 [ @"playable", @"hasProtectedContent" ] 的属性值。加载完成之后通过 completionHandler 回调处理结果。在 completionHandler 里面我们需要切换到主线程来进行工作。我们可以在根据 [ @"playable", @"hasProtectedContent" ] 的属性值来判断 assert 的资源情况。若是 assert 加载成功,且 assert 的 playable 和 hasProtectedContent 属性值都是成功的。那么我们可以利用 AVURLAsset 来生成一个 AVPlayerItem。
- (void)asynchronouslyLoadURLAsset:(AVURLAsset *)newAsset {
/*
Using AVAsset now runs the risk of blocking the current thread
(the main UI thread) whilst I/O happens to populate the
properties. It's prudent to defer our work until the properties
we need have been loaded.
*/
[newAsset loadValuesAsynchronouslyForKeys:AAPLPlayerViewController.assetKeysRequiredToPlay completionHandler:^{
/*
The asset invokes its completion handler on an arbitrary queue.
To avoid multiple threads using our internal state at the same time
we'll elect to use the main thread at all times, let's dispatch
our handler to the main queue.
*/
dispatch_async(dispatch_get_main_queue(), ^{
if (newAsset != self.asset) {
/*
self.asset has already changed! No point continuing because
another newAsset will come along in a moment.
*/
return;
}
/*
Test whether the values of each of the keys we need have been
successfully loaded.
*/
for (NSString *key in self.class.assetKeysRequiredToPlay) {
NSError *error = nil;
if ([newAsset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed) {
NSString *message = [NSString localizedStringWithFormat:NSLocalizedString(@"error.asset_key_%@_failed.description", @"Can't use this AVAsset because one of it's keys failed to load"), key];
[self handleErrorWithMessage:message error:error];
return;
}
}
// We can't play this asset.
if (!newAsset.playable || newAsset.hasProtectedContent) {
NSString *message = NSLocalizedString(@"error.asset_not_playable.description", @"Can't use this AVAsset because it isn't playable or has protected content");
[self handleErrorWithMessage:message error:nil];
return;
}
/*
We can play this asset. Create a new AVPlayerItem and make it
our player's current item.
*/
self.playerItem = [AVPlayerItem playerItemWithAsset:newAsset];
});
}];
}
播放视频
AVPlayer 是一个可以控制视频播放的对象,值的注意的是 AVPlayer 一次有且只能有一个 asset 在播放,若是要复用 AVPlayer 对象来播放另一个 asset ,可以使用 AVPlayer 的 replaceCurrentItem(with:) 方法。
AVPlayerItem 用来表示 AVPlayer 播放器中 asset 的时间和表现状态。
在 asset 加载完成之后,并且生成了 AVPlayerItem 对象。接下来就是使用 AVPlayer 来播放 AVPlayerItem
- (void)setPlayerItem:(AVPlayerItem *)newPlayerItem {
if (_playerItem != newPlayerItem) {
_playerItem = newPlayerItem;
// If needed, configure player item here before associating it with a player
// (example: adding outputs, setting text style rules, selecting media options)
[self.player replaceCurrentItemWithPlayerItem:_playerItem];
}
}
暂停/播放视频
暂停视频和重新播放视频都是利用 AVPlayer 的一个 rate 属性来操作的,通常情况下,当 rate 等于 0 的时候,视频是处于暂停状态的,当 rate 等于 1 的时候,视频是处于正常播放状态。
- (IBAction)playPauseButtonWasPressed:(UIButton *)sender {
if (self.player.rate != 1.0) {
// not playing foward so play
if (CMTIME_COMPARE_INLINE(self.currentTime, ==, self.duration)) {
// at end so got back to begining
self.currentTime = kCMTimeZero;
}
[self.player play];
} else {
// playing so pause
[self.player pause];
}
}
视频快进和视频快退
前面提到暂停/播放视频都是利用 AVPlayer 的 rate 属性。视频的快进/快退操作也是利用 rate 属性。当 rate 等于 2 的时候,表示 2 倍速播放。rate 也可以取负值,当 rate 等于 -2 的时候,表示 2 倍速回退。这里我们不能无限度倍速,我们取 rate 的绝对值为 2 。
- (IBAction)rewindButtonWasPressed:(UIButton *)sender {
self.rate = MAX(self.player.rate - 2.0, -2.0); // rewind no faster than -2.0
}
- (IBAction)fastForwardButtonWasPressed:(UIButton *)sender {
self.rate = MIN(self.player.rate + 2.0, 2.0); // fast forward no faster than 2.0
}
拖动进度播放
除了视频快进/快退的视频进度操作方法,我们还可以采用更加准确的视频进度控制方式。通过调用 AVPlayer 的以下方法我们可以准确控制视频的播放进度。
- (void)setCurrentTime:(CMTime)newCurrentTime {
[self.player seekToTime:newCurrentTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
而 CMTime 的值可以来自 UISlider 的 value
- (IBAction)timeSliderDidChange:(UISlider *)sender {
self.currentTime = CMTimeMakeWithSeconds(sender.value, 1000);
}
视频的播放进度
AVPlayer 的视频播放进度的回调时间单位可以由外界来控制,正常情况下,我们选择 1s 回调一次视频播放进度就可以了。
AAPLPlayerViewController __weak *weakSelf = self;
_timeObserverToken = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:
^(CMTime time) {
weakSelf.timeSlider.value = CMTimeGetSeconds(time);
}];
这里值得注意的是当需要 AVPlayer 不断回调视频播放进度,需要持有 _timeObserverToken。当不需要 _timeObserverToken 这个 observer 的时候,需要手动移除 _timeObserverToken。
if (_timeObserverToken) {
[self.player removeTimeObserver:_timeObserverToken];
_timeObserverToken = nil;
}
结束播放
当 AVPlayer 播放完成之后,需要释放资源。
[self.player pause];
参考
- https://developer.apple.com/documentation/avfoundation/avplayer
- https://developer.apple.com/documentation/avfoundation/avasset
- https://developer.apple.com/documentation/avfoundation/avplayeritem
- https://developer.apple.com/library/content/samplecode/AVFoundationSimplePlayer-iOS/Introduction/Intro.html