如何在 iOS 中使用 AVPlayer 缓冲音频?

Posted

技术标签:

【中文标题】如何在 iOS 中使用 AVPlayer 缓冲音频?【英文标题】:How to buffer audio using AVPlayer in iOS? 【发布时间】:2016-08-08 10:50:55 【问题描述】:

我想播放来自 Internet 的流式音频。我编写了播放流的代码,但它没有任何缓冲区,因此如果信号较弱,应用程序停止播放音频。这是我的代码:

import UIKit
import AVFoundation
import MediaPlayer
import AudioToolbox

class ViewController: UIViewController 

var playerItem:AVPlayerItem?
var player:AVPlayer?

@IBOutlet weak var PlayButton: UIButton!

override func viewDidLoad() 
    super.viewDidLoad()

    var buffer = AVAudioBuffer ()
    let url = NSURL (string: "http://radio.afera.com.pl/afera64.aac")
    playerItem = AVPlayerItem(URL: url!)
    player = AVPlayer(playerItem: playerItem!)



override func didReceiveMemoryWarning() 
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.


@IBAction func PlayButtonTapped(sender: AnyObject)

    if ((player!.rate != 0) && (player!.error == nil))
    
        player!.pause()
        PlayButton.setImage(UIImage(named:"Play"), forState: UIControlState.Normal)
    
    else
    
        player!.play()
        PlayButton.setImage(UIImage(named:"Stop"), forState:UIControlState.Normal)
    


我不知道如何缓冲这个流。我搜索了苹果文档,但在 Swift 中找不到任何内容。

我找到了类似AVAudioBuffer 的东西,但我不知道如何使用它,也不知道它是否正确解决了这个问题。

附: C#MSDN 上有文档,如果是 Swift,Apple 上是否有类似的东西?

【问题讨论】:

Swift 文档:developer.apple.com/library/ios/documentation/Swift/Conceptual/… // iOS 文档:developer.apple.com/library/ios/navigation 【参考方案1】:

答案是创建一个错误委托,每次播放器停止时都会启动一个选择器(当网络连接中断或流未正确加载时,错误会发生变化):

这里是代表,就在我的 RadioPlayer 类之外和之上:

protocol errorMessageDelegate 
func errorMessageChanged(newVal: String)

类:

import Foundation
import AVFoundation
import UIKit

class RadioPlayer : NSObject 

static let sharedInstance = RadioPlayer()
var instanceDelegate:sharedInstanceDelegate? = nil
var sharedInstanceBool = false 
    didSet 
        if let delegate = self.instanceDelegate 
            delegate.sharedInstanceChanged(self.sharedInstanceBool)
        
    

private var player = AVPlayer(URL: NSURL(string: Globals.radioURL)!)
private var playerItem = AVPlayerItem?()
private var isPlaying = false

var errorDelegate:errorMessageDelegate? = nil
var errorMessage = "" 
    didSet 
        if let delegate = self.errorDelegate 
            delegate.errorMessageChanged(self.errorMessage)
        
    


override init() 
    super.init()

    errorMessage = ""

    let asset: AVURLAsset = AVURLAsset(URL: NSURL(string: Globals.radioURL)!, options: nil)

    let statusKey = "tracks"

    asset.loadValuesAsynchronouslyForKeys([statusKey], completionHandler: 
        var error: NSError? = nil

        dispatch_async(dispatch_get_main_queue(), 
            let status: AVKeyValueStatus = asset.statusOfValueForKey(statusKey, error: &error)

            if status == AVKeyValueStatus.Loaded

                let playerItem = AVPlayerItem(asset: asset)

                self.player = AVPlayer(playerItem: playerItem)
                self.sharedInstanceBool = true

             else 
                self.errorMessage = error!.localizedDescription
                print(error!)
            

        )


    )

    NSNotificationCenter.defaultCenter().addObserverForName(
        AVPlayerItemFailedToPlayToEndTimeNotification,
        object: nil,
        queue: nil,
        usingBlock:  notification in
            print("Status: Failed to continue")
            self.errorMessage = "Stream was interrupted"
    )

    print("Initializing new player")



func resetPlayer() 
    errorMessage = ""

    let asset: AVURLAsset = AVURLAsset(URL: NSURL(string: Globals.radioURL)!, options: nil)

    let statusKey = "tracks"

    asset.loadValuesAsynchronouslyForKeys([statusKey], completionHandler: 
        var error: NSError? = nil

        dispatch_async(dispatch_get_main_queue(), 
            let status: AVKeyValueStatus = asset.statusOfValueForKey(statusKey, error: &error)

            if status == AVKeyValueStatus.Loaded

                let playerItem = AVPlayerItem(asset: asset)
                //playerItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: &ItemStatusContext)

                self.player = AVPlayer(playerItem: playerItem)
                self.sharedInstanceBool = true

             else 
                self.errorMessage = error!.localizedDescription
                print(error!)
            

        )
    )


func bufferFull() -> Bool 
    return bufferAvailableSeconds() > 45.0


func bufferAvailableSeconds() -> NSTimeInterval 
    // Check if there is a player instance
    if ((player.currentItem) != nil) 

        // Get current AVPlayerItem
        let item: AVPlayerItem = player.currentItem!
        if (item.status == AVPlayerItemStatus.ReadyToPlay) 

            let timeRangeArray: NSArray = item.loadedTimeRanges
            if timeRangeArray.count < 1  return(CMTimeGetSeconds(kCMTimeInvalid)) 
            let aTimeRange: CMTimeRange = timeRangeArray.objectAtIndex(0).CMTimeRangeValue
            //let startTime = CMTimeGetSeconds(aTimeRange.end)
            let loadedDuration = CMTimeGetSeconds(aTimeRange.duration)

            return (NSTimeInterval)(loadedDuration);
        
        else 
            return(CMTimeGetSeconds(kCMTimeInvalid))
        
     
    else 
        return(CMTimeGetSeconds(kCMTimeInvalid))
    


func play() 
    player.play()
    isPlaying = true
    print("Radio is \(isPlaying ? "" : "not ")playing")


func pause() 
    player.pause()
    isPlaying = false
    print("Radio is \(isPlaying ? "" : "not ")playing")


func currentlyPlaying() -> Bool 
    return isPlaying



protocol sharedInstanceDelegate 
func sharedInstanceChanged(newVal: Bool)

RadioViewController:

import UIKit
import AVFoundation

class RadioViewController: UIViewController, errorMessageDelegate, sharedInstanceDelegate 

// MARK: Properties

var firstErrorSkip = true
var firstInstanceSkip = true

@IBOutlet weak var listenLabel: UILabel!
@IBOutlet weak var radioSwitch: UIImageView!

@IBAction func back(sender: AnyObject) 
    print("Dismissing radio view")
    if let navigationController = self.navigationController
    
        navigationController.popViewControllerAnimated(true)
    


@IBAction func switched(sender: AnyObject) 
    toggle()


override func viewDidLoad() 
    super.viewDidLoad()

    do 
        try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
        print("AVAudioSession Category Playback OK")
        do 
            try AVAudioSession.sharedInstance().setActive(true)
            print("AVAudioSession is Active")

         catch let error as NSError 
            print(error.localizedDescription)
        
     catch let error as NSError 
        print(error.localizedDescription)
    

    RadioPlayer.sharedInstance.errorDelegate = self
    RadioPlayer.sharedInstance.instanceDelegate = self

    if RadioPlayer.sharedInstance.currentlyPlaying() 
        radioSwitch.image = UIImage(named: "Radio_Switch_Active")
        listenLabel.text = "Click to Pause Radio Stream:"
    



override func didReceiveMemoryWarning() 
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.


func toggle() 
    if RadioPlayer.sharedInstance.currentlyPlaying() 
        pauseRadio()
     else 
        playRadio()
    


func playRadio() 
    firstErrorSkip = false
    firstInstanceSkip = false

    if RadioPlayer.sharedInstance.errorMessage != "" || RadioPlayer.sharedInstance.bufferFull() 
        resetStream()
     else 
        radioSwitch.image = UIImage(named: "Radio_Switch_Active")
        listenLabel.text = "Click to Pause Radio Stream:"
        RadioPlayer.sharedInstance.play()
    


func pauseRadio() 
    RadioPlayer.sharedInstance.pause()
    radioSwitch.image = UIImage(named: "Radio_Switch_Inactive")
    listenLabel.text = "Click to Play Radio Stream:"


func resetStream() 
    print("Reloading interrupted stream");
    RadioPlayer.sharedInstance.resetPlayer()
    //RadioPlayer.sharedInstance = RadioPlayer();
    RadioPlayer.sharedInstance.errorDelegate = self
    RadioPlayer.sharedInstance.instanceDelegate = self
    if RadioPlayer.sharedInstance.bufferFull() 
        radioSwitch.image = UIImage(named: "Radio_Switch_Active")
        listenLabel.text = "Click to Pause Radio Stream:"
        RadioPlayer.sharedInstance.play()
     else 
        playRadio()
    


func errorMessageChanged(newVal: String) 
    if !firstErrorSkip 
        print("Error changed to '\(newVal)'")
        if RadioPlayer.sharedInstance.errorMessage != "" 
            print("Showing Error Message")
            let alertController = UIAlertController(title: "Stream Failure", message: RadioPlayer.sharedInstance.errorMessage, preferredStyle: UIAlertControllerStyle.Alert)
            alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default, handler: nil))

            self.presentViewController(alertController, animated: true, completion: nil)

            pauseRadio()

        
     else 
        print("Skipping first init")
        firstErrorSkip = false
    


func sharedInstanceChanged(newVal: Bool) 
    if !firstInstanceSkip 
    print("Detected New Instance")
        if newVal 
            RadioPlayer.sharedInstance.play()
        
     else 
        firstInstanceSkip = false
    



希望这会有所帮助:)

【讨论】:

谢谢! :) 我不明白一件事。这是什么:“Globals.radioURL”?项目中的一些全局设置?我可以在哪里编辑它们? 不客气。 :) 它只是 url 没有更多声明可以在任何地方使用。 如何声明全局使用带有“Global”前缀的变量? global 是自定义类,radioURL 是它的属性。 好的。现在我明白了一切。问题已解决,感谢您的耐心等待。【参考方案2】:

改变

playerItem = AVPlayerItem?()

playerItem:AVPlayerItem?

【讨论】:

以上是关于如何在 iOS 中使用 AVPlayer 缓冲音频?的主要内容,如果未能解决你的问题,请参考以下文章

iOS音频篇:AVPlayer的缓存实现

使用 AVPlayer 的 iOS 音频可视化

使用 AVPlayer 在 iOS 中音频流期间的播放速度

iOS SDK AVPlayer - 如何检测音频流是不是可搜索?

具有缓冲进度的 UISlider - AVPlayer

iOS 13 - 崩溃的 avPlayer.currentTime().seconds 检查