我正在尝试播放一个
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
不仅已创建,而且已准备好播放?)
您正在播放远程文件。
AVPlayer
可能需要一些时间来缓冲足够的数据并准备好播放文件(请参阅 AV 基础编程指南)
但是您似乎没有等到播放器准备好才点击播放按钮。我想要的是禁用此按钮并仅在播放器准备好时才启用它。
使用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
}
}
}
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 中的新语法。
我在尝试弄清楚
AVPlayer
的状态时遇到了很多麻烦。 status
属性似乎并不总是很有帮助,当我尝试处理音频会话中断时,这给我带来了无尽的挫败感。有时,AVPlayer
告诉我它已准备好播放(使用 AVPlayerStatusReadyToPlay
),但实际上似乎并未准备好。我使用了 Jilouc 的 KVO 方法,但它并不是在所有情况下都有效。
补充一下,当 status 属性没有用时,我通过查看
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
}
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。
经过大量研究并尝试多种方法后,我注意到通常
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
}
}
基于 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
}
}
我遇到了没有收到任何回电的问题。
事实证明,这取决于您创建流的方式。在我的例子中,我使用了一个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"];
}
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!!
}
}
@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
检查玩家当前物品的状态:
if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)