Знаючи, коли об’єкт AVPlayer готовий до відтворення


79

Я намагаюся відтворити 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**

Я бачу, що ДРУГИЙ РАЗ, player.statusздається, тримає 1, що, на мою думку, є AVPlayerReadyToPlay.

Що я можу зробити, щоб гра працювала належним чином при першому натисканні клавіші UIButton? (тобто як я можу переконатись, що AVPlayerвін не просто створений, а й готовий до гри?)

Відповіді:


127

Ви відтворюєте віддалений файл. Це може зайняти деякий час, поки 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
        }
    }
}

Дякую!! Це спрацювало як шарм. (хоч би мав здогадатися, коли я побачив, що він без проблем відтворює офлайн-файли)
mvishnu

Є деякі URL-адреси, які просто не відтворюються, вони існують, але вони не працюють (наприклад, iTunes також не відтворюватиме їх). Як ти керуєш такою поведінкою? У AVPlayer немає часу очікування.
Фабріціо

10
З мого досвіду player.currentItem.status, точний, коли player.statusні. Не знаю, в чому різниця.
bendytree

1
@iOSAppDev На IOS7 використовуйте AVPlayerItem addObserver
Пітер Чжао

4
вау, цей AVPlayer настільки погано розроблений, що змушує мене плакати. Чому б не додати блок обробника onLoad? Давай, Apple, спрости свої речі!
Качка

30

У мене було багато проблем, намагаючись з'ясувати статус AVPlayer. statusВласність не завжди , здається, дуже корисно, і це привело до нескінченного розчарування , коли я намагався обробляти аудіо - сеанси переривання. Іноді AVPlayerмені говорили, що готовий до гри (зAVPlayerStatusReadyToPlay ), коли цього насправді не було. Я використовував метод KVO Jilouc, але він працював не у всіх випадках.

На додаток, коли властивість status не було корисною, я запитував кількість потоку, який завантажив AVPlayer, переглядаючи loadedTimeRangesвластивість AVPlayer's 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
}

2
Є доповнення до типу NSValue, що постачаються з AV Foundation. Деякі з цих помічників дозволяють вам перетворювати назад і вперед із NSValue значення CMTimeXxx. Як CMTimeRangeValue .
superjos

Подібна історія для отримання секунд (мабуть, це те, що timeLoadedє) з CMTime: CMTimeGetSeconds
superjos

2
На жаль, це має бути прийнятою відповіддю. AVPlayerздається, status == AVPlayerStatusReadyToPlayзанадто рано, коли він не готовий грати насправді. Щоб зробити це, ви можете, наприклад, обернути наведений вище код у NSTimerвиклик.
maxkonovalov

Чи може це бути так, коли завантажений діапазон часу перевищує (влог) 2 секунди, але статус гравця або playerItem не є ReadyToPlay? IOW, чи слід це також підтвердити?
danielhadar

29

Швидке рішення

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's 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/ Зміст.swift


Дякую, цей метод дуже простий для видалення KVO.
ZAFAR007

11

Дослідивши багато і спробувавши багато способів, я помітив, що зазвичай 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
    }
}

дякую, це спрацювало і на мене. Однак я не використовував оцінку "currentBufferDuration == секунд". Не могли б ви сказати мені, для чого він використовується?
andrei

Для випадків, колиcurrentBufferDuration < 2
jose920405

Чи може це бути так, коли завантажений діапазон часу перевищує (влог) 2 секунди, але статус гравця або playerItem не є ReadyToPlay? IOW, чи слід це також підтвердити?
danielhadar

11
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")
}

Ключовим моментом у коді є властивість екземпляра isPlaybackLikelyToKeepUp.


3
Хороша відповідь. Я б покращив KVO за допомогоюforKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)
Miroslav Hrivik

У 2019 році це працює ідеально - скопіюйте та вставте :) Я використовував мод @MiroslavHrivik, дякую!
Fattie

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
    }
}

З розширенням, я думаю, неможливо спостерігати за готовим майном KVO. Як-небудь?
Джонні

Я слухаю повідомлення AVPlayerItemNewAccessLogEntryта AVPlayerItemDidPlayToEndTimeв своєму проекті. Afaik це працює.
Аксель Гільмін

Добре, я закінчив слухати loadedTimeRanges.
Джонні,

5

У мене були проблеми з відсутністю зворотних дзвінків.

Виявляється, це залежить від того, як ви створюєте потік. У моєму випадку я використовував 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"];
}

Якщо не слід бути з AVPlayerItemStatusReadyToPlay?
jose920405

@ jose920405 Я можу підтвердити, що рішення вище працює, але це хороше питання. Я справді не знаю. Повідомте мене, якщо ви його протестуєте.
dac2009

3

Перевірте статус поточного пункту гравця:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)

2
player.currentItem.status повертає AVPlayerItemStatusUnkown. Я не знаю, що робити далі. :(
mvishnu

Спочатку це значення AVPlayerItemStatusUnkown. Тільки через якийсь - то час, він буде мати можливість знати , якщо він AVPlayerItemStatusReadyToPlayабоAVPlayerItemStatusFailed
Густаво Барбоса

2

Свіфт 4:

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!!
        }
}

1

Відповідь @ JoshBernfeld для мене не спрацювала. Не знаю, чому. Він спостерігав playerItem.observe(\.status. Довелося спостерігати player?.observe(\.currentItem?.status. Здається, це те саме, playerItem statusвласність.

var playerStatusObserver: NSKeyValueObservation?

player?.automaticallyWaitsToMinimizeStalling = false // starts faster

playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) { (player, change) in
        
    switch (player.status) {
    case .readyToPlay:
            // here is where it's ready to play so play player
            DispatchQueue.main.async { [weak self] in
                self?.player?.play()
            }
    case .failed, .unknown:
            print("Media Failed to Play")
    @unknown default:
         break
    }
}

коли ви закінчите використовувати програвач playerStatusObserver = nil

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.