AVPlayer
。我有所有的播放/暂停控件以及所有的代理和观察者,它们都工作得很好。当播放小型音频文件时,一切都很顺利。当播放长音频文件时,它也开始正常播放,但在几秒钟后,
AVPlayer
会暂停播放(很可能是为了缓冲)。问题是它不会自动恢复播放。它保持在暂停状态,如果我手动再次按下播放按钮,它就会再次平稳地播放。我想知道为什么
AVPlayer
不会自动恢复播放,以及如何在没有用户再次按下播放按钮的情况下管理恢复音频?谢谢。AVPlayer
。我有所有的播放/暂停控件以及所有的代理和观察者,它们都工作得很好。当播放小型音频文件时,一切都很顺利。AVPlayer
会暂停播放(很可能是为了缓冲)。问题是它不会自动恢复播放。它保持在暂停状态,如果我手动再次按下播放按钮,它就会再次平稳地播放。AVPlayer
不会自动恢复播放,以及如何在没有用户再次按下播放按钮的情况下管理恢复音频?谢谢。是的,它停止了,因为缓冲区为空,所以必须等待加载更多视频。之后,您必须手动要求重新开始。为了解决这个问题,我按照以下步骤进行:
1)检测:为了检测播放器何时停止,我使用了KVO与值的速率属性:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"rate"] )
{
if (self.player.rate == 0 && CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime) && self.videoPlaying)
{
[self continuePlaying];
}
}
}
这个条件:CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime)
是用来检测视频是否已经播放到结尾或者停止在中途。
2)等待视频加载 - 如果你直接继续播放,你将没有足够的缓存来无间断地播放下去。要知道何时开始播放,您必须观察 playerItem 中的 playbackLikelytoKeepUp
值(这里我使用一个库来观察 blocks,但是这并不影响重点):
-(void)continuePlaying
{
if (!self.playerItem.playbackLikelyToKeepUp)
{
self.loadingView.hidden = NO;
__weak typeof(self) wSelf = self;
self.playbackLikelyToKeepUpKVOToken = [self.playerItem addObserverForKeyPath:@keypath(_playerItem.playbackLikelyToKeepUp) block:^(id obj, NSDictionary *change) {
__strong typeof(self) sSelf = wSelf;
if(sSelf)
{
if (sSelf.playerItem.playbackLikelyToKeepUp)
{
[sSelf.playerItem removeObserverForKeyPath:@keypath(_playerItem.playbackLikelyToKeepUp) token:self.playbackLikelyToKeepUpKVOToken];
sSelf.playbackLikelyToKeepUpKVOToken = nil;
[sSelf continuePlaying];
}
}
}];
}
就是这样!问题解决了。
编辑:顺便说一下,使用的库是libextobjc。
AVPlayer *player;
int playerTryCount = -1; // this should get set to 0 when the AVPlayer starts playing
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
partial .m:
- (AVPlayer *)initializePlayerFromURL:(NSURL *)movieURL {
// create AVPlayer
AVPlayerItem *videoItem = [AVPlayerItem playerItemWithURL:movieURL];
AVPlayer *videoPlayer = [AVPlayer playerWithPlayerItem:videoItem];
// add Observers
[videoItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
[self startNotificationObservers]; // see method below
// I observe a bunch of other stuff, but this is all you need for this to work
return videoPlayer;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
if (self.player.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.player.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.player.currentTime, !=, self.player.currentItem.duration)) {
// if so, post the playerHanging notification
[self.notificationCenter postNotificationName:PlayerHangingNotification object:self.videoPlayer];
}
}
}
- (void)startNotificationObservers {
[self.notificationCenter addObserver:self
selector:@selector(playerContinue)
name:PlayerContinueNotification
object:nil];
[self.notificationCenter addObserver:self
selector:@selector(playerHanging)
name:PlayerHangingNotification
object:nil];
}
// playerHanging simply decides whether to wait 0.5 seconds or not
// if so, it pauses the player and sends a playerContinue notification
// if not, it puts us out of our misery
- (void)playerHanging {
if (playerTryCount <= 10) {
playerTryCount += 1;
[self.player pause];
// start an activity indicator / busy view
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.player];
} else { // this code shouldn't actually execute, but I include it as dummyproofing
[self stopPlaying]; // a method where I clean up the AVPlayer,
// which is already paused
// Here's where I'd put up an alertController or alertView
// to say we're sorry but we just can't go on like this anymore
}
}
// playerContinue does the actual waiting and restarting
- (void)playerContinue {
if (CMTIME_COMPARE_INLINE(self.player.currentTime, ==, self.player.currentItem.duration)) { // we've reached the end
[self stopPlaying];
} else if (playerTryCount > 10) // stop trying
[self stopPlaying];
// put up "sorry" alert
} else if (playerTryCount == 0) {
return; // protects against a race condition
} else if (self.player.currentItem.playbackLikelyToKeepUp == YES) {
// Here I stop/remove the activity indicator I put up in playerHanging
playerTryCount = 0;
[self.player play]; // continue from where we left off
} else { // still hanging, not at end
// create a 0.5-second delay to see if buffering catches up
// then post another playerContinue notification to call this method again
// in a manner that attempts to avoid any recursion or threading nightmares
playerTryCount += 1;
double delayInSeconds = 0.5;
dispatch_time_t executeTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(executeTime, dispatch_get_main_queue(), ^{
// test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
if (playerTryCount > 0) {
if (playerTryCount <= 10) {
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.videoPlayer];
} else {
[self stopPlaying];
// put up "sorry" alert
}
}
});
}
接受的答案提供了解决问题的可能方案,但缺乏灵活性,阅读起来也很困难。这里提供一种更灵活的解决方案。
添加观察者:
//_player is instance of AVPlayer
[_player.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil];
[_player addObserver:self forKeyPath:@"rate" options:0 context:nil];
处理程序:
-(void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if ([keyPath isEqualToString:@"status"]) {
if (_player.status == AVPlayerStatusFailed) {
//Possibly show error message or attempt replay from tart
//Description from the docs:
// Indicates that the player can no longer play AVPlayerItem instances because of an error. The error is described by
// the value of the player's error property.
}
}else if ([keyPath isEqualToString:@"rate"]) {
if (_player.rate == 0 && //if player rate dropped to 0
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, >, kCMTimeZero) && //if video was started
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, <, _player.currentItem.duration) && //but not yet finished
_isPlaying) { //instance variable to handle overall state (changed to YES when user triggers playback)
[self handleStalled];
}
}
}
魔法:
-(void)handleStalled {
NSLog(@"Handle stalled. Available: %lf", [self availableDuration]);
if (_player.currentItem.playbackLikelyToKeepUp || //
[self availableDuration] - CMTimeGetSeconds(_player.currentItem.currentTime) > 10.0) {
[_player play];
} else {
[self performSelector:@selector(handleStalled) withObject:nil afterDelay:0.5]; //try again
}
}
"[self availableDuration]"是可选的,但是您可以根据视频可用的数量手动启动播放。您可以更改代码检查视频缓冲区是否足够的频率。如果您决定使用可选部分,这里是方法实现:
- (NSTimeInterval) availableDuration
{
NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
别忘了清理工作。移除观察者:
[_player.currentItem removeObserver:self forKeyPath:@"status"];
[_player removeObserver:self forKeyPath:@"rate"];
还可能存在待处理的卡顿视频通话:
[UIView cancelPreviousPerformRequestsWithTarget:self selector:@selector(handleStalled) object:nil];
playbackBufferFull
也很重要。 - Anjan Biswas我曾遇到类似的问题。我有一些本地文件想要播放,配置了AVPlayer并调用[player play],但是播放器在第0帧停止播放,直到我手动再次调用play才能继续播放。由于错误的解释,无法按照被采纳的答案进行实现,因此我尝试延迟播放,神奇地解决了问题。
[self performSelector:@selector(startVideo) withObject:nil afterDelay:0.2];
-(void)startVideo{
[self.videoPlayer play];
}
对于Web视频,我也遇到了问题,我使用Wallace的答案解决了它。
在创建AVPlayer时添加一个观察者:
[self.videoItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
if (self.videoPlayer.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, !=, self.videoPlayer.currentItem.duration)) {
NSLog(@"hanged");
[self performSelector:@selector(startVideo) withObject:nil afterDelay:0.2];
}
}
记得在关闭视图之前移除观察者
[self.videoItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]
我认为使用AVPlayerItemPlaybackStalledNotification
来检测卡顿是一种更好的方式。
首先,我会观察播放是否卡顿
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerStalled),
name: AVPlayerItemPlaybackStalledNotification, object: videoPlayer.currentItem)
func playerStalled(note: NSNotification) {
let playerItem = note.object as! AVPlayerItem
if let player = playerItem.valueForKey("player") as? AVPlayer{
player.play()
}
}
这可能不是最好的方法,但在我找到更好的方法之前,我会继续使用它 :)
player.play()
可能会在不同的线程上被调用。不确定这是否可以接受... - tonymontana我也遇到了像这里描述的问题。
我多次测试了下面的答案,并且到目前为止每次都有效。
这是我为Swift 5版本的@wallace的答案编写的代码:
1- 不再观察keyPath "playbackLikelyToKeepUp",而是使用.AVPlayerItemPlaybackStalled通知,然后在其中通过if !playerItem.isPlaybackLikelyToKeepUp {...}检查缓冲区是否已满
2- 不再使用他的PlayerHangingNotification,而是使用名为playerIsHanging()的函数
3- 不再使用他的PlayerContinueNotification,而是使用名为checkPlayerTryCount()的函数
4- 在checkPlayerTryCount()内部,我执行与他的(void)playerContinue函数相同的所有操作,除了当我遇到} else if playerTryCount == 0 {时,什么也不会发生。为了避免这种情况,我在return语句上面添加了两行代码
5- 如@PranoyC在@wallace的评论中建议的那样,我将playerTryCount设置为最大值20,而不是10。 我还将其设置为类属性let playerTryCountMaxLimit = 20
您必须在注释建议的位置添加/删除活动指示器/旋转器
代码:
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemPlaybackStalled(_:)),
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: playerItem)
@objc func playerItemPlaybackStalled(_ notification: Notification) {
// The system may post this notification on a thread other than the one used to registered the observer: https://developer.apple.com/documentation/foundation/nsnotification/name/1387661-avplayeritemplaybackstalled
guard let playerItem = notification.object as? AVPlayerItem else { return }
// playerItem.isPlaybackLikelyToKeepUp == false && if the player's current time is greater than zero && the player's current time is not equal to the player's duration
if (!playerItem.isPlaybackLikelyToKeepUp) && (CMTimeCompare(playerItem.currentTime(), .zero) == 1) && (CMTimeCompare(playerItem.currentTime(), playerItem.duration) != 0) {
DispatchQueue.main.async { [weak self] in
self?.playerIsHanging()
}
}
}
var playerTryCount = -1 // this should get set to 0 when the AVPlayer starts playing
let playerTryCountMaxLimit = 20
func playerIsHanging() {
if playerTryCount <= playerTryCountMaxLimit {
playerTryCount += 1
// show spinner
checkPlayerTryCount()
} else {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("1.-----> PROBLEM")
}
}
func checkPlayerTryCount() {
guard let player = player, let playerItem = player.currentItem else { return }
// if the player's current time is equal to the player's duration
if CMTimeCompare(playerItem.currentTime(), playerItem.duration) == 0 {
// show spinner or better yet remove spinner and show a replayButton or auto rewind to the beginning ***BE SURE TO RESET playerTryCount = 0 ***
} else if playerTryCount > playerTryCountMaxLimit {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("2.-----> PROBLEM")
} else if playerTryCount == 0 {
// *** in his answer he has nothing but a return statement here but when it would hit this condition nothing would happen. I had to add these 2 lines of code for it to continue ***
playerTryCount += 1
retryCheckPlayerTryCountAgain()
return // protects against a race condition
} else if playerItem.isPlaybackLikelyToKeepUp {
// remove spinner and reset playerTryCount to zero
playerTryCount = 0
player?.play()
} else { // still hanging, not at end
playerTryCount += 1
/*
create a 0.5-second delay using .asyncAfter to see if buffering catches up
then call retryCheckPlayerTryCountAgain() in a manner that attempts to avoid any recursion or threading nightmares
*/
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
DispatchQueue.main.async { [weak self] in
// test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
if self!.playerTryCount > 0 {
if self!.playerTryCount <= self!.playerTryCountMaxLimit {
self!.retryCheckPlayerTryCountAgain()
} else {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("3.-----> PROBLEM")
}
}
}
}
}
}
func retryCheckPlayerTryCountAgain() {
checkPlayerTryCount()
}
playbackLikelyToKeepUp
很可能为 false。kvo
观察 playbackBufferEmpty
更好,更敏感地判断是否存在可用于播放的缓冲区数据。如果值更改为 true,则可以调用播放方法继续播放。playbackLikelyToKeepUp
和playbackBufferEmpty
上使用KVO。苹果公司表示 - “当playbackLikelyToKeepUp指示NO而属性playbackBufferFull指示YES时,这是可能的。在这种情况下,播放缓冲区已达到容量,但没有统计数据支持未来播放可能跟上的预测。由您决定是否继续媒体播放。” - Anjan Biswas就我的情况而言,
我试图使用imagePickerController录制视频,并使用AVPlayerController播放录制的视频。但是它开始播放视频,1秒后就停止了。不知何故,它需要时间来保存视频,如果立即回放,则无法播放。-(void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self performSelector:@selector(playVideo) withObject:self
afterDelay:0.5];
}
-(void) playVideo {
self.avPlayerViewController = [[AVPlayerViewController alloc] init];
if(self.avPlayerViewController != nil)
{
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:Vpath];
AVPlayer* player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
self.avPlayerViewController.player = player;
self.avPlayerViewController.showsPlaybackControls = NO;
[self.avPlayerViewController setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.avPlayerViewController.view setFrame:[[UIScreen mainScreen] bounds]];
self.avPlayerViewController.view.clipsToBounds = YES;
self.avPlayerViewController.delegate = self;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerDidFinishPlaying) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
[self.viewVideoHolder addSubview:self.avPlayerViewController.view];
[self.avPlayerViewController.player play];
}
}
-(void) playerDidFinishPlaying
{
[avPlayer pause];
}