在后台使用 AVAudioPlayer [英] Using AVAudioPlayer in background

查看:40
本文介绍了在后台使用 AVAudioPlayer的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的应用中,我添加了后台音频和后台处理功能.

In my app, I've added the capabilities of background audio and background processing.

我的代码目前使用 AVAudioPlayer 来播放音频.虽然当应用在前台时播放效果很好,但在锁定屏幕的情况下,音频会有一些静态抖动.

My code presently uses AVAudioPlayer to play audio. While playback is good when the app in the foreground, with a locked screen, the audio is has some static jitteriness to it.

我的应用程序是使用 SwiftUI 和 Combine 编写的.有没有人遇到过这个问题,你有什么建议作为解决方法?

My app is written using SwiftUI and Combine. Has anyone encountered this issue and what would you suggest as a workaround?

这是play方法:

    /// Play an `AudioFile`
    /// - Parameters:
    ///   - audioFile: an `AudioFile` struct
    ///   - completion: optional completion, default is `nil`
    func play(_ audioFile: AudioFile,
              completion: (() -> Void)? = nil) {
        if audioFile != currentAudioFile {
            resetPublishedValues()
        }
        currentAudioFile = audioFile
        setupCurrentAudioFilePublisher()
        guard let path = Bundle.main.path(forResource: audioFile.filename, ofType: "mp3") else {
            return
        }
        
        let url = URL(fileURLWithPath: path)
        
        // everybody STFU
        stop()
        
        do {
            // make sure the sound is one
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            // instantiate instance of AVAudioPlayer
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer.prepareToPlay()
            // play the sound
            let queue = DispatchQueue(label: "audioPlayer", qos: .userInitiated, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
            
            queue.async {
                self.audioPlayer.play()
            }
            audioPlayer.delegate = self
        } catch {
            // Not much to go wrong, so leaving alone for now, but need to make `throws` if we handle errors
            print(String(format: "play() error: %@", error.localizedDescription))
        }
    }

这是类定义:

import AVFoundation
import Combine
import Foundation

/// A `Combine`-friendly wrapper for `AVAudioPlayer` which utilizes `Combine` `Publishers` instead of `AVAudioPlayerDelegate`
class CombineAudioPlayer: NSObject, AVAudioPlayerDelegate, ObservableObject {
    static let sharedInstance = CombineAudioPlayer()
    private var audioPlayer = AVAudioPlayer()
    /*
     FIXME: For now, gonna leave this timer on all the time, but need to refine
     down the road because it's going to generate a fuckload of data on the
     current interval.
     */
    // MARK: - Publishers
    private var timer = Timer.publish(every: 0.1,
                                      on: RunLoop.main,
                                      in: RunLoop.Mode.default).autoconnect()
    @Published public var currentAudioFile: AudioFile?
    public var isPlaying = CurrentValueSubject<Bool, Never>(false)
    public var currentTime = PassthroughSubject<TimeInterval, Never>()
    public var didFinishPlayingCurrentAudioFile = PassthroughSubject<AudioFile, Never>()
    
    private var cancellables: Set<AnyCancellable> = []
    
    // MARK: - Initializer
    private override init() {
        super.init()
        // set it up with a blank audio file
        setupPublishers()
        audioPlayer.setVolume(1.0, fadeDuration: 0)
    }
    
    // MARK: - Publisher Methods
    private func setupPublishers() {
        timer.sink(receiveCompletion: { completion in
            // TODO: figure out if I need anything here
            // Don't think so, as this will always be initialized
        },
        receiveValue: { value in
            self.isPlaying.send(self.audioPlayer.isPlaying)
            self.currentTime.send(self.currentTimeValue)
        })
        .store(in: &cancellables)
        
        didFinishPlayingCurrentAudioFile.sink(receiveCompletion: { _ in
            
        },
        receiveValue: { audioFile in
            self.resetPublishedValues()
        })
        .store(in: &cancellables)
    }
    
    private func setupCurrentAudioFilePublisher() {
        self.isPlaying.send(false)
        self.currentTime.send(0.0)
    }
    
    // MARK: - Playback Methods
    
    /// Play an `AudioFile`
    /// - Parameters:
    ///   - audioFile: an `AudioFile` struct
    ///   - completion: optional completion, default is `nil`
    func play(_ audioFile: AudioFile,
              completion: (() -> Void)? = nil) {
        if audioFile != currentAudioFile {
            resetPublishedValues()
        }
        currentAudioFile = audioFile
        setupCurrentAudioFilePublisher()
        guard let path = Bundle.main.path(forResource: audioFile.filename, ofType: "mp3") else {
            return
        }
        
        let url = URL(fileURLWithPath: path)
        
        // everybody STFU
        stop()
        
        do {
            // make sure the sound is one
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            // instantiate instance of AVAudioPlayer
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer.prepareToPlay()
            // play the sound
            let queue = DispatchQueue(label: "audioPlayer", qos: .userInitiated, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
            
            queue.async {
                self.audioPlayer.play()
            }
            audioPlayer.delegate = self
        } catch {
            // Need to make `throws` if we handle errors
            print(String(format: "play error: %@", error.localizedDescription))
        }
    }
    
    func stop() {
        audioPlayer.stop()
        resetPublishedValues()
    }
    
    private func resetPublishedValues() {
        isPlaying.send(false)
        currentTime.send(0.0)
    }
    
    private var currentTimeValue: TimeInterval {
        audioPlayer.currentTime
    }
    
    /// Use the `Publisher` to determine when a sound is done playing.
    /// - Parameters:
    ///   - player: an `AVAudioPlayer` instance
    ///   - flag: a `Bool` indicating whether the sound was successfully played
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        if let currentAudioFile = currentAudioFile {
            didFinishPlayingCurrentAudioFile.send(currentAudioFile)
        }
        resetPublishedValues()
    }
}

推荐答案

所以我想通了.我有几个问题需要解决.基本上,我需要在应用程序处于后台时的特定时间播放音频文件.如果在应用程序处于活动状态时播放声音,这可以正常工作,但如果音频播放尚未进行,AVAudioPlayer 不会让我在应用程序处于后台后启动某些内容.

So I got it figured out. I had a few issues to contend with. Basically, I needed to play audio files at a specific time when the app was in the background. While this works fine if the sound is playing when the app is active, AVAudioPlayer won't let me start something after the app is in the background if audio playback is not already in progress.

我不会深入细节,但我最终使用了 AVQueuePlayer,我将其初始化为我的 CombineAudioPlayer 类的一部分.

I won't go into the nitty gritty details, but I ended up making use of AVQueuePlayer, which I initialized as part of my CombineAudioPlayer class.

  1. 更新 AppDelegate.swift

我将以下几行添加到 AppDelegatedidFinishLaunchingWithOptions 方法中.

I added the following lines to AppDelegate's didFinishLaunchingWithOptions method.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    do {
        try AVAudioSession.sharedInstance().setCategory(.playback,
                                                        mode: .default)
        try AVAudioSession.sharedInstance().setActive(true)
    } catch {
        print(String(format: "didFinishLaunchingWithOptions error: %@", error.localizedDescription))
    }
    
    return true
}

  1. 在我的 AudioPlayer 类中,我声明了一个 AVQueuePlayer.使用 AudioPlayer 类初始化这一点很重要,而不是在方法内部.
  1. In my AudioPlayer class, I declared an AVQueuePlayer. It is critical this be initialized with the AudioPlayer class, not inside of a method.

我的 ViewModel 订阅了一个通知,该通知监听应用程序即将退出前台,它会快速生成一个播放列表并在应用程序退出之前触发它.

My ViewModel subscribes to a notification that listens for the app about to exit the foreground, it quickly generates a playlist and fires it just before the app exits.

NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification).sink { _ in
    self.playBackground()
}
.store(in: &cancellables)

private var bgAudioPlayer = AVQueuePlayer()

然后,我创建了一个方法来为 AVQueuePlayer 生成一个播放列表,如下所示:

Then, I created a method to generate a playlist for the AVQueuePlayer that looks something like this:

func backgroundPlaylist(from audioFiles: [AudioFile]) -> [AVPlayerItem] {
    guard let firstFile = audioFiles.first else {
        // return empty array, don't wanna unwrap optionals
        return []
    }
    // declare a silence file
    let silence = AudioFile(displayName: "Silence",
                            filename: "1sec-silence")
    // start at zero
    var currentSeconds: TimeInterval = 0
    
    var playlist: [AVPlayerItem] = []
    
    // while currentSeconds is less than firstFile's fire time...
    while currentSeconds < firstFile.secondsInFuture {
        // add 1 second of silence to the playlist
        playlist.append(AVPlayerItem(url: silence.url!))
        // increment currentSeconds and we loop over again, adding more silence
        currentSeconds += 1
    }
    
    // once we're done, add the file we want to play
    playlist.append(AVPlayerItem(url: audioFiles.first!.url!))
                    
    return playlist
}

最后播放的声音如下:

func playInBackground() {
    do {
        // make sure the sound is one
        try AVAudioSession.sharedInstance().setCategory(.playback,
                                                        mode: .default,
                                                        policy: .longFormAudio,
                                                        options: [])
        try AVAudioSession.sharedInstance().setActive(true)
        let playlist = backgroundPlaylist(from: backgroundPlaylist)
        bgAudioPlayer = AVQueuePlayer(items: playlist)
        bgAudioPlayer.play()
    } catch {
        // Not much to mess up, so leaving alone for now, but need to make
        // `throws` if we handle errors
        print(String(format: "playInBackground error: %@",
                        error.localizedDescription))
    }
}

这篇关于在后台使用 AVAudioPlayer的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆