阿杰,使用 AVPlayer 播放在线音频 时,和视频类似,但有一些特别需要注意的点,我帮你整理如下:


1. URL 与网络要求

  • 确保 URL 是 有效的音频文件地址,常见格式:mp3, aac, m4a
  • URL 支持 HTTPS,iOS 9+ 默认要求 ATS (App Transport Security) 支持 HTTPS,如果是 HTTP,需要在 Info.plist 添加例外:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

2. AVPlayer 的创建

import AVFoundation

let url = URL(string: "https://example.com/audio.mp3")!
let playerItem = AVPlayerItem(url: url)
let player = AVPlayer(playerItem: playerItem)
player.play()

⚠️ 注意:

  • AVPlayer 默认不会缓冲完整音频,需要依赖网络和系统缓存。
  • 对于长音频,建议观察缓冲状态,避免播放中断。

3. 缓冲状态监控

playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "loadedTimeRanges" {
        if let timeRanges = player.currentItem?.loadedTimeRanges,
           let timeRange = timeRanges.first?.timeRangeValue {
            let bufferedTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration)
            print("已缓冲: \(bufferedTime) 秒")
        }
    }
}
  • 可根据缓冲时间调整播放逻辑,比如 等待缓冲完成再播放

4. 后台播放支持

  • 如果希望音频在 后台播放,需配置:
  1. Info.plist 添加:
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>
  1. 设置音频会话:
import AVFoundation
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try? AVAudioSession.sharedInstance().setActive(true)

5. 处理中断和网络变化

  • 注册通知处理中断(如来电、锁屏):
NotificationCenter.default.addObserver(self,
                                       selector: #selector(handleInterruption),
                                       name: AVAudioSession.interruptionNotification,
                                       object: nil)
@objc func handleInterruption(notification: Notification) {
    // 判断中断类型,暂停或恢复播放
}
  • 网络切换或断网可能导致播放失败,需要监听:
playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)

并根据 AVPlayerItem.Status.failed 做重试或提示用户。


6. 音频格式和缓存

  • 常见格式推荐:mp3, aac, m4a,兼容性最好。
  • 如果需要频繁播放同一音频,可以先下载到本地再播放,提高流畅度和减少网络依赖。

7. 总结

注意点建议做法
网络缓冲监听 loadedTimeRanges,避免卡顿
后台播放配置 UIBackgroundModes + AVAudioSession
中断处理监听 AVAudioSession 中断通知,暂停/恢复
网络失败监听 status,做重试或提示
URL 安全尽量使用 HTTPS,ATS 配置合适
音频格式推荐 mp3/aac/m4a,避免不支持格式

阿杰,如果你愿意,我可以帮你写一个 完整 AVPlayer 在线音频播放器封装类,支持:

  • 播放/暂停
  • 缓冲进度显示
  • 后台播放
  • 网络中断重试

这样就可以直接在项目里使用。

明白阿杰,我帮你写一个 Swift 封装类 OnlineAudioPlayer,实现 在线音频播放、缓冲监控、后台播放、网络中断处理,方便直接使用。


import AVFoundation
import UIKit

class OnlineAudioPlayer: NSObject {

    static let shared = OnlineAudioPlayer()
    
    private var player: AVPlayer?
    private var playerItem: AVPlayerItem?
    
    /// 播放音频
    /// - Parameter urlString: 在线音频 URL
    func play(urlString: String) {
        guard let url = URL(string: urlString) else { return }

        // 创建 AVPlayerItem
        playerItem = AVPlayerItem(url: url)
        
        // 监听缓冲状态
        playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
        playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
        
        // 初始化 AVPlayer
        player = AVPlayer(playerItem: playerItem)
        player?.automaticallyWaitsToMinimizeStalling = true
        
        // 后台播放设置
        try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
        try? AVAudioSession.sharedInstance().setActive(true)
        
        // 播放
        player?.play()
        
        // 监听播放结束
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(audioDidFinish),
                                               name: .AVPlayerItemDidPlayToEndTime,
                                               object: playerItem)
    }
    
    /// 暂停播放
    func pause() {
        player?.pause()
    }
    
    /// 停止播放并释放
    func stop() {
        player?.pause()
        removeObservers()
        player = nil
        playerItem = nil
    }
    
    // MARK: - KVO 监听
    override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let item = object as? AVPlayerItem else { return }
        
        if keyPath == "loadedTimeRanges" {
            if let timeRange = item.loadedTimeRanges.first?.timeRangeValue {
                let bufferedTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration)
                print("已缓冲: \(bufferedTime) 秒")
            }
        } else if keyPath == "status" {
            switch item.status {
            case .readyToPlay:
                print("准备播放")
            case .failed:
                print("播放失败: \(item.error?.localizedDescription ?? "未知错误")")
            default:
                break
            }
        }
    }
    
    @objc private func audioDidFinish() {
        print("音频播放完成")
        stop()
    }
    
    // 移除 KVO 和通知
    private func removeObservers() {
        if let item = playerItem {
            item.removeObserver(self, forKeyPath: "loadedTimeRanges")
            item.removeObserver(self, forKeyPath: "status")
        }
        NotificationCenter.default.removeObserver(self)
    }
}

使用示例

// 播放音频
OnlineAudioPlayer.shared.play(urlString: "https://www.example.com/audio.mp3")

// 暂停播放
OnlineAudioPlayer.shared.pause()

// 停止播放
OnlineAudioPlayer.shared.stop()

功能特点

  1. 支持 在线音频播放(mp3 / aac / m4a 等)
  2. 缓冲进度监控,可根据需要更新 UI
  3. 后台播放支持(来电或锁屏仍能继续播放)
  4. 播放完成自动停止
  5. 错误监听,支持网络失败提示

明白阿杰,我帮你升级 OnlineAudioPlayer,增加以下功能:

  1. 网络中断自动重试
  2. 缓冲进度回调(可更新 UI)
  3. 播放进度回调(可更新进度条)
  4. 支持外部监听播放状态(开始/暂停/完成/失败)

import AVFoundation
import UIKit

protocol OnlineAudioPlayerDelegate: AnyObject {
    func playbackDidStart()
    func playbackDidPause()
    func playbackDidFinish()
    func playbackDidFail(error: Error?)
    func playbackBufferProgress(buffered: Double)
    func playbackTimeProgress(current: Double, duration: Double)
}

class OnlineAudioPlayer: NSObject {

    static let shared = OnlineAudioPlayer()
    
    private var player: AVPlayer?
    private var playerItem: AVPlayerItem?
    private var timeObserver: Any?
    private var retryCount = 0
    private let maxRetry = 3
    
    weak var delegate: OnlineAudioPlayerDelegate?
    
    // MARK: - 播放音频
    func play(urlString: String) {
        guard let url = URL(string: urlString) else { return }
        retryCount = 0
        startPlayer(with: url)
    }
    
    private func startPlayer(with url: URL) {
        // 创建 AVPlayerItem
        playerItem = AVPlayerItem(url: url)
        
        // 监听缓冲和状态
        playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
        playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
        
        // 初始化 AVPlayer
        player = AVPlayer(playerItem: playerItem)
        player?.automaticallyWaitsToMinimizeStalling = true
        
        // 后台播放
        try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
        try? AVAudioSession.sharedInstance().setActive(true)
        
        // 播放
        player?.play()
        delegate?.playbackDidStart()
        
        // 监听播放结束
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(audioDidFinish),
                                               name: .AVPlayerItemDidPlayToEndTime,
                                               object: playerItem)
        
        // 播放进度回调
        timeObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1),
                                                       queue: DispatchQueue.main) { [weak self] time in
            guard let self = self,
                  let duration = self.playerItem?.duration else { return }
            let current = CMTimeGetSeconds(time)
            let total = CMTimeGetSeconds(duration)
            self.delegate?.playbackTimeProgress(current: current, duration: total)
        }
    }
    
    // MARK: - 暂停播放
    func pause() {
        player?.pause()
        delegate?.playbackDidPause()
    }
    
    // MARK: - 停止播放
    func stop() {
        player?.pause()
        removeObservers()
        player = nil
        playerItem = nil
    }
    
    // MARK: - KVO 监听
    override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let item = object as? AVPlayerItem else { return }
        
        if keyPath == "loadedTimeRanges" {
            if let timeRange = item.loadedTimeRanges.first?.timeRangeValue {
                let bufferedTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration)
                delegate?.playbackBufferProgress(buffered: bufferedTime)
            }
        } else if keyPath == "status" {
            switch item.status {
            case .readyToPlay:
                print("准备播放")
            case .failed:
                print("播放失败: \(item.error?.localizedDescription ?? "未知错误")")
                retryOrFail()
            default:
                break
            }
        }
    }
    
    // MARK: - 播放完成
    @objc private func audioDidFinish() {
        delegate?.playbackDidFinish()
        stop()
    }
    
    // MARK: - 网络中断自动重试
    private func retryOrFail() {
        guard retryCount < maxRetry, let urlAsset = playerItem?.asset as? AVURLAsset else {
            delegate?.playbackDidFail(error: playerItem?.error)
            stop()
            return
        }
        retryCount += 1
        print("播放失败,正在重试 \(retryCount)/\(maxRetry)")
        startPlayer(with: urlAsset.url)
    }
    
    // MARK: - 移除 KVO 和通知
    private func removeObservers() {
        if let item = playerItem {
            item.removeObserver(self, forKeyPath: "loadedTimeRanges")
            item.removeObserver(self, forKeyPath: "status")
        }
        if let observer = timeObserver {
            player?.removeTimeObserver(observer)
            timeObserver = nil
        }
        NotificationCenter.default.removeObserver(self)
    }
}

使用示例

class ViewController: UIViewController, OnlineAudioPlayerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        OnlineAudioPlayer.shared.delegate = self
        OnlineAudioPlayer.shared.play(urlString: "https://www.example.com/audio.mp3")
    }
    
    func playbackDidStart() {
        print("播放开始")
    }
    
    func playbackDidPause() {
        print("播放暂停")
    }
    
    func playbackDidFinish() {
        print("播放完成")
    }
    
    func playbackDidFail(error: Error?) {
        print("播放失败: \(error?.localizedDescription ?? "未知错误")")
    }
    
    func playbackBufferProgress(buffered: Double) {
        print("已缓冲: \(buffered) 秒")
    }
    
    func playbackTimeProgress(current: Double, duration: Double) {
        print("播放进度: \(current)/\(duration)")
    }
}

功能亮点

  1. 网络中断自动重试(最大 3 次)
  2. 缓冲进度实时回调
  3. 播放进度实时回调
  4. 后台播放支持
  5. 播放完成自动停止
  6. 播放状态回调给外部 UI 使用

阿杰,这个封装类几乎可以直接在项目中使用,并且可以方便地绑定 UI,比如缓冲条、播放进度条等。

如果你愿意,我还可以帮你加一个 缓存音频到本地的功能,让重复播放同一个 URL 时无需再次下载,提高体验和节省流量。

你希望我加缓存功能吗?