了解 AVPlayer 对象何时准备好播放

发布于 2024-10-25 09:38:36 字数 1804 浏览 6 评论 0原文

我正在尝试播放从先前的 UIView 传递到 UIViewMP3 文件(存储在 NSURL *fileURL 中) 变量)。

我正在初始化一个 AVPlayer

player = [AVPlayer playerWithURL:fileURL];

NSLog(@"Player created:%d",player.status);

NSLog 打印 Playercreated: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??");
    }

}

当我运行此代码时,我总是在控制台中收到播放器错误?。 但是,如果我用简单的 if(!isPlaying)... 替换检查 AVPlayer 是否准备好播放的 if 条件,那么我第二次点击播放 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 不仅已创建,而且已准备好播放?)

I'm trying to play an MP3 file that is passed to an UIView from a previous UIView (stored in a NSURL *fileURL variable).

I'm initializing an AVPlayer with:

player = [AVPlayer playerWithURL:fileURL];

NSLog(@"Player created:%d",player.status);

The NSLog prints Player created:0, which i figured means it is not ready to play yet.

When i click the play UIButton, the code i run is:

-(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??");
    }

}

When i run this, I always get Error in player?? in the console.
If i however replace the if condition that checks if AVPlayer is ready to play, with a simple if(!isPlaying)..., then the music plays the SECOND TIME I click on the play UIButton.

The console log is:

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

I see that the SECOND TIME, the player.status seems to hold 1, which I'm guessing is AVPlayerReadyToPlay.

What can I do to have the playing to work properly the first time i click the play UIButton?
(ie, how can i make sure the AVPlayer is not just created, but also ready to play?)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

撩人痒 2024-11-01 09:38:36

您正在播放远程文件。 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
        }
    }
}

You are playing a remote file. It may take some time for the AVPlayer to buffer enough data and be ready to play the file (see AV Foundation Programming Guide)

But you don't seem to wait for the player to be ready before tapping the play button. What I would to is disable this button and enable it only when the player is ready.

Using KVO, it's possible to be notified for changes of the player status:

playButton.enabled = NO;
player = [AVPlayer playerWithURL:fileURL];
[player addObserver:self forKeyPath:@"status" options:0 context:nil];   

This method will be called when the status changes:

- (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
        }
    }
}
汐鸠 2024-11-01 09:38:36

Swift 解决方案

var observer: NSKeyValueObservation?

func prepareToPlay() {
    let url = <#Asset URL#>
    // Create asset to be played.
    let asset = AVAsset(url: url)
    
    // Create a new AVPlayerItem with the asset.
    let playerItem = AVPlayerItem(asset: asset)
    
    // 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/Contents.swift

Swift Solution

var observer: NSKeyValueObservation?

func prepareToPlay() {
    let url = <#Asset URL#>
    // Create asset to be played.
    let asset = AVAsset(url: url)
    
    // Create a new AVPlayerItem with the asset.
    let playerItem = AVPlayerItem(asset: asset)
    
    // 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)
}

Also you can invalidate the observer this way

self.observer.invalidate()

Important: You must keep the observer variable retained otherwise it will deallocate and the changeHandler will no longer get called. So don't define the observer as a function variable but define it as a instance variable like the given example.

This key value observer syntax is new to Swift 4.

For more information, see here https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/Contents.swift

心房的律动 2024-11-01 09:38:36

我在尝试找出 AVPlayer 的状态时遇到了很多麻烦。 status 属性似乎并不总是很有帮助,当我尝试处理音频会话中断时,这导致了无尽的挫败感。有时,AVPlayer 告诉我它已准备好播放(使用 AVPlayerStatusReadyToPlay),但实际上似乎并未准备好。我使用了 Jilouc 的 KVO 方法,但它并不是在所有情况下都有效。

作为补充,当 status 属性没有用时,我通过查看 AVPlayerloadedTimeRanges 属性来查询 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
}

I had a lot of trouble trying to figure out the status of an AVPlayer. The status property didn't always seem to be terribly helpful, and this led to endless frustration when I was trying to handle audio session interruptions. Sometimes the AVPlayer told me it was ready to play (with AVPlayerStatusReadyToPlay) when it didn't actually seem to be. I used Jilouc's KVO method, but it didn't work in all cases.

To supplement, when the status property wasn't being useful, I queried the amount of the stream that the AVPlayer had loaded by looking at the loadedTimeRanges property of the AVPlayer's currentItem (which is an AVPlayerItem).

It's all a little confusing, but here's what it looks like:

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
}
简单 2024-11-01 09:38:36
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。

private var playbackLikelyToKeepUpContext = 0

For register observer

avPlayer.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp",
        options: .new, context: &playbackLikelyToKeepUpContext)

Listen the observer

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

For remove observer

deinit {
    avPlayer.removeObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp")
}

The key point in the code is instance property isPlaybackLikelyToKeepUp.

月竹挽风 2024-11-01 09:38:36

经过大量研究并尝试多种方法后,我注意到通常 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
    }
}

After researching a lot and try many ways I've noticed that normally the status observer is not the better for know really when AVPlayer object is ready to play, because the object can be ready for play but this not that mean it will be play immediately.

The better idea for know this is with loadedTimeRanges.

For Register observer

[playerClip addObserver:self forKeyPath:@"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

Listen the observer

- (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];
        }
    }
}

For remove observer (dealloc, viewWillDissapear or before register observer) its a good places for called

- (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
    }
}
年少掌心 2024-11-01 09:38:36

基于 Tim Camber 回答,这是我使用的 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
    }
}

Based on Tim Camber answer, here is the Swift function I use :

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
}

Or, as an extension

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
    }
}
聊慰 2024-11-01 09:38:36

我遇到了没有收到任何回调的问题。

事实证明,这取决于您创建流的方式。在我的例子中,我使用了一个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"];
}

I had issues with not getting any callbacks.

Turns out it depends on how you create the stream. In my case I used a playerItem to initialize, and thus I had to add the observer to the item instead.

For example:

- (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"];
}
︶ ̄淡然 2024-11-01 09:38:36

斯威夫特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!!
        }
}

Swift 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!!
        }
}
转身以后 2024-11-01 09:38:36

@JoshBernfeld 的答案对我不起作用。不知道为什么。他观察了playerItem.observe(\.status)。我必须观察player?.observe(\.currentItem?.status。看起来它们是同一件事, playerItem status 属性设置playerStatusObserver = nil

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

当您使用完播放器后,

@JoshBernfeld's answer didn't work for me. Not sure why. He observed playerItem.observe(\.status. I had to observe player?.observe(\.currentItem?.status. Seems like they're the same thing, the playerItem status property.

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

when you are finished using the player set playerStatusObserver = nil

紙鸢 2024-11-01 09:38:36

检查玩家当前物品的状态:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)

Check the status of the player's currentItem:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文