Зная, когда объект AVPlayer готов к игре
Я пытаюсь воспроизвести файл MP3
, который передается в UIView
из предыдущего UIView
(сохраняется в переменной NSURL *fileURL
).
Я инициализирую AVPlayer
с помощью:
player = [AVPlayer playerWithURL:fileURL];
NSLog(@"Player created:%d",player.status);
NSLog
печатает Player created:0,
, который, как я понял, означает, что он еще не готов играть.
Когда я нажимаю кнопку воспроизведения UIButton
, я запускаю код:
-(IBAction)playButtonClicked
{
NSLog(@"Clicked Play. MP3:%@",[fileURL absoluteString]);
if(([player status] == AVPlayerStatusReadyToPlay) && !isPlaying)
// if(!isPlaying)
{
[player play];
NSLog(@"Playing:%@ with %d",[fileURL absoluteString], player.status);
isPlaying = YES;
}
else if(isPlaying)
{
[player pause];
NSLog(@"Pausing:%@",[fileURL absoluteString]);
isPlaying = NO;
}
else {
NSLog(@"Error in player??");
}
}
Когда я запускаю это, я всегда получаю Error in player??
в консоли.
Если я, однако, заменил условие if
, которое проверяет, готова ли игра AVPlayer
, с простым if(!isPlaying)
..., тогда музыка воспроизводит ВТОРОЕ ВРЕМЯ Я нажимаю кнопку воспроизведения UIButton
.
Журнал консоли:
Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 0**
Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Pausing:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
2011-03-23 11:06:43.674 Podcasts[2050:207] Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 1**
Я вижу, что SECOND TIME, player.status
, кажется, содержит 1, что, я думаю, AVPlayerReadyToPlay
.
Что я могу сделать, чтобы игра правильно работала в первый раз, когда я нажимаю кнопку воспроизведения UIButton
?
(т.е. как я могу убедиться, что AVPlayer
не только создан, но также готов к воспроизведению?)
Ответы
Ответ 1
Вы воспроизводите удаленный файл. AVPlayer
может потребоваться некоторое время для буферизации достаточного количества данных и готовности к воспроизведению файла (см. Руководство по программированию AV Foundation)
Но вы, кажется, не ждете, когда игрок будет готов, прежде чем нажать кнопку воспроизведения. Я отключил бы эту кнопку и включал ее только тогда, когда игрок готов.
Используя KVO, можно получать уведомления об изменениях статуса игрока:
playButton.enabled = NO;
player = [AVPlayer playerWithURL:fileURL];
[player addObserver:self forKeyPath:@"status" options:0 context:nil];
Этот метод вызывается при изменении статуса:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (object == player && [keyPath isEqualToString:@"status"]) {
if (player.status == AVPlayerStatusReadyToPlay) {
playButton.enabled = YES;
} else if (player.status == AVPlayerStatusFailed) {
// something went wrong. player.error should contain some information
}
}
}
Ответ 2
У меня было много проблем, пытаясь выяснить статус AVPlayer
. Свойство status
не всегда казалось очень полезным, и это приводило к бесконечным разочарованиям, когда я пытался справиться с прерываниями сеанса аудио. Иногда AVPlayer
сказал мне, что он готов играть (с AVPlayerStatusReadyToPlay
), когда это действительно не похоже. Я использовал метод Jilouc KVO, но он не работал во всех случаях.
Чтобы добавить, когда свойство status не было полезным, я запросил объем потока, который загрузил AVPlayer, просмотрев свойство loadedTimeRanges
AVPlayer
currentItem
(которое является AVPlayerItem
).
Все это немного запутывает, но вот как это выглядит:
NSValue *val = [[[audioPlayer currentItem] loadedTimeRanges] objectAtIndex:0];
CMTimeRange timeRange;
[val getValue:&timeRange];
CMTime duration = timeRange.duration;
float timeLoaded = (float) duration.value / (float) duration.timescale;
if (0 == timeLoaded) {
// AVPlayer not actually ready to play
} else {
// AVPlayer is ready to play
}
Ответ 3
Быстрое решение
var observer: NSKeyValueObservation?
func prepareToPlay() {
let url = <#Asset URL#>
// Create asset to be played
let asset = AVAsset(url: url)
let assetKeys = [
"playable",
"hasProtectedContent"
]
// Create a new AVPlayerItem with the asset and an
// array of asset keys to be automatically loaded
let playerItem = AVPlayerItem(asset: asset,
automaticallyLoadedAssetKeys: assetKeys)
// Register as an observer of the player item status property
self.observer = playerItem.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
if playerItem.status == .readyToPlay {
//Do your work here
}
})
// Associate the player item with the player
player = AVPlayer(playerItem: playerItem)
}
Также вы можете аннулировать наблюдателя таким образом
self.observer.invalidate()
Важно: Вы должны оставить переменную наблюдателя сохраненной, иначе она будет освобождена, и changeHandler больше не будет вызываться. Поэтому не определяйте наблюдателя как переменную функции, а определяйте его как переменную экземпляра, как в данном примере.
Этот синтаксис наблюдателя значения ключа является новым для Swift 4.
Для получения дополнительной информации см. здесь https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/Contents.swift
Ответ 4
После многозначительного исследования и многих попыток я заметил, что обычно наблюдатель status
не лучше знает, когда объект AVPlayer
готов к воспроизведению, поскольку объект может быть готовым к игре, но это не означает, что он будет играть сразу.
Лучшая идея знать это с loadedTimeRanges
.
Для наблюдателя регистра
[playerClip addObserver:self forKeyPath:@"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
Прослушать наблюдателя
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == playerClip && [keyPath isEqualToString:@"currentItem.loadedTimeRanges"]) {
NSArray *timeRanges = (NSArray*)[change objectForKey:NSKeyValueChangeNewKey];
if (timeRanges && [timeRanges count]) {
CMTimeRange timerange=[[timeRanges objectAtIndex:0]CMTimeRangeValue];
float currentBufferDuration = CMTimeGetSeconds(CMTimeAdd(timerange.start, timerange.duration));
CMTime duration = playerClip.currentItem.asset.duration;
float seconds = CMTimeGetSeconds(duration);
//I think that 2 seconds is enough to know if you're ready or not
if (currentBufferDuration > 2 || currentBufferDuration == seconds) {
// Ready to play. Your logic here
}
} else {
[[[UIAlertView alloc] initWithTitle:@"Alert!" message:@"Error trying to play the clip. Please try again" delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil] show];
}
}
}
Для удаления наблюдателя (dealloc, viewWillDissapear или до наблюдателя регистра) его хорошие места для вызываемых
- (void)removeObserverForTimesRanges
{
@try {
[playerClip removeObserver:self forKeyPath:@"currentItem.loadedTimeRanges"];
} @catch(id anException){
NSLog(@"excepcion remove observer == %@. Remove previously or never added observer.",anException);
//do nothing, obviously it wasn't attached because an exception was thrown
}
}
Ответ 5
private var playbackLikelyToKeepUpContext = 0
Для наблюдателя регистра
avPlayer.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp",
options: .new, context: &playbackLikelyToKeepUpContext)
Прослушать наблюдателя
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &playbackLikelyToKeepUpContext {
if avPlayer.currentItem!.isPlaybackLikelyToKeepUp {
// loadingIndicatorView.stopAnimating() or something else
} else {
// loadingIndicatorView.startAnimating() or something else
}
}
}
Для удаления наблюдателя
deinit {
avPlayer.removeObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp")
}
Ключевым моментом в коде является свойство экземпляра isPlaybackLiciousToKeepUp.
Ответ 6
У меня были проблемы с отсутствием обратных вызовов.
Оказывается, это зависит от того, как вы создаете поток. В моем случае я использовал элемент playerItem для инициализации, и поэтому мне пришлось добавить наблюдателя к элементу вместо этого.
Например:
- (void) setup
{
...
self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
...
// add callback
[self.player.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil];
}
// the callback method
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
NSLog(@"[VideoView] player status: %i", self.player.status);
if (object == self.player.currentItem && [keyPath isEqualToString:@"status"])
{
if (self.player.currentItem.status == AVPlayerStatusReadyToPlay)
{
//do stuff
}
}
}
// cleanup or it will crash
-(void)dealloc
{
[self.player.currentItem removeObserver:self forKeyPath:@"status"];
}
Ответ 7
Основываясь на ответе Тима Камбер, вот функция Swift, которую я использую:
private func isPlayerReady(_ player:AVPlayer?) -> Bool {
guard let player = player else { return false }
let ready = player.status == .readyToPlay
let timeRange = player.currentItem?.loadedTimeRanges.first as? CMTimeRange
guard let duration = timeRange?.duration else { return false } // Fail when loadedTimeRanges is empty
let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
let loaded = timeLoaded > 0
return ready && loaded
}
Или, как расширение
extension AVPlayer {
var ready:Bool {
let timeRange = currentItem?.loadedTimeRanges.first as? CMTimeRange
guard let duration = timeRange?.duration else { return false }
let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
let loaded = timeLoaded > 0
return status == .readyToPlay && loaded
}
}
Ответ 8
Проверьте статус текущего игрока:
if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
Ответ 9
Свифт 5:
var player:AVPlayer!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidReadyToPlay(notification:)),
name: .AVPlayerItemNewAccessLogEntry,
object: player?.currentItem)
}
@objc func playerItemDidReadyToPlay(notification: Notification) {
if let _ = notification.object as? AVPlayerItem {
// player is ready to play now!!
}
}
Ответ 10
Я думаю, что лучше использовать игрока, используя initWithUrl
. Я не думаю, что это решит вашу проблему, но лучше.