How to properly make background audio playback on iOS work? - ios

I'm trying to build an Radio Streaming app. I have created a Singleton class for may RadioPlayer as described bellow and I have turned on Background Modes > Audio, AirPlay and Picture in Picture.
However, when my App goes into background mode, the audio stops playing. What am I missing here?
Appreciate any help! Thanks!
import Foundation
import AVFoundation
class RadioPlayer {
static let sharedInstance = RadioPlayer()
var player = AVPlayer(playerItem: RadioPlayer.radioPlayerItem())
var isPlaying = false
class func radioPlayerItem() -> AVPlayerItem {
return AVPlayerItem(URL: urlRadio())
}
class func urlRadio() -> NSURL {
let roRadio = Repository.realm.objects(RORadio)
let url: NSURL = NSURL(string: roRadio[0].streaming)!
return url
}
func toggle() {
if isPlaying == true {
pause()
} else {
play()
}
}
func play() {
player.play()
isPlaying = true
}
func pause() {
player.pause()
isPlaying = false
}
func currentlyPlaying() -> Bool {
return isPlaying
}
}

The application should be allowed to continue running while in the background. Open your Info.plist file and add the key of UIBackgroundModes. There will be only one string "audio" for your aim.

Related

How to monitor for AVAudioPlayerNode finished playing

Oh hey! I've been stuck trying to figure out how to have my AVAudioPlayerNode() automatically stop playback when it reaches the end of the file in my SwiftUI project. As you can see below, my audio files that don't go through the AVAudioEngine but rather the AVAudioPlayer have an audioPlayerDidFinishPlaying function and that works great. However for the audio files that go through the engine using the play function don't have anything like this, and I can't figure out how to get it to behave similarly.
I've tried building something like
func audioPlayerDidFinishPlaying2(_ player: AVAudioPlayerNode, successfully flag: Bool) {
if flag {
isPlaying = false
print("Playback Engine Stopped")
}
}
But this doesn't appear to do anything.
Stopping the engine's playback manually by calling stopPlayback2() works just fine.
I'm calling the engine player using
do {
try self.audioPlayer.play(self.audioURL)
}
catch let error as NSError {
print(error.localizedDescription)
}
I've looked at other SO posts [here][1] and [here][2] but neither solutions are working for me. I would really appreciate your input if you have any suggestions! Thanks!!
AudioPlayer.swift
class AudioPlayer: NSObject, ObservableObject, AVAudioPlayerDelegate {
let objectWillChange = PassthroughSubject<AudioPlayer, Never>()
var isPlaying = false {
didSet {
objectWillChange.send(self)
}
}
var audioPlayer: AVAudioPlayer!
func startPlayback (audio: URL) {
let playbackSession = AVAudioSession.sharedInstance()
do {
try playbackSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
} catch {
print("Playing over the device's speakers failed")
}
do {
audioPlayer = try AVAudioPlayer(contentsOf: audio)
audioPlayer.delegate = self
audioPlayer.play()
isPlaying = true
} catch {
print("Playback failed.")
}
}
func stopPlayback() {
audioPlayer.stop()
isPlaying = false
}
func stopPlayback2() {
engine.stop()
isPlaying = false
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if flag {
isPlaying = false
print("Playback Stopped")
}
}
let engine = AVAudioEngine()
let speedControl = AVAudioUnitVarispeed()
let pitchControl = AVAudioUnitTimePitch()
func play(_ url: URL) throws {
let file = try! AVAudioFile(forReading: url)
let avPlayer = AVAudioPlayerNode()
engine.attach(avPlayer)
engine.attach(pitchControl)
engine.attach(speedControl)
engine.connect(avPlayer, to: speedControl, format: nil)
engine.connect(speedControl, to: pitchControl, format: nil)
engine.connect(pitchControl, to: engine.mainMixerNode, format: nil)
avPlayer.scheduleFile(file, at: nil)
isPlaying = true
try engine.start()
avPlayer.play()
}
```
[1]: https://stackoverflow.com/questions/34238432/avaudioengine-avaudioplayernode-didfinish-method-like-avaudioplayer
[2]: https://stackoverflow.com/questions/59080708/calling-stop-on-avaudioplayernode-after-finished-playing-causes-crash
Instead of using avPlayer.scheduleFile(file, at: nil), use the form of the method with a completion handler:
avPlayer.scheduleFile(file, at: nil) {
//call your completion function here
print("Done playing")
}

Unable to get AVAudioPlayer to stop playing

I have a class defined as Music.swift coded as follows:
import Foundation
import AVFoundation
class Music {
var isPlaying: Bool = false
public var backgrndSound = AVAudioPlayer()
func isMusicPlaying() -> Bool {
isPlaying = UserDefaults.standard.bool(forKey: "isPlaying")
return isPlaying
}
func StartPlaying() {
let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do {
self.backgrndSound = try AVAudioPlayer(contentsOf: url)
self.backgrndSound.numberOfLoops = -1
self.backgrndSound.play()
UserDefaults.standard.setValue(true, forKey: "isPlaying")
} catch {
// couldn't load file :(
}
}
func StopPlaying() {
self.backgrndSound.pause()
self.backgrndSound.stop()
}
}
On first load of the app, the music is automatically started with a call to StartPlaying(). That works just fine. Afterwards I have a settings menu that has a switch for the music play :
import UIKit
import AVFoundation
class SettingsViewController: UIViewController {
#IBOutlet weak var swMusic: UISwitch!
var myMusic = Music()
override func viewDidLoad() {
super.viewDidLoad()
swMusic.isOn = UserDefaults.standard.bool(forKey: "isPlaying")
}
#IBAction func musicSwitch(_ sender: Any) {
if swMusic.isOn == true {
// turn on music
myMusic.StartPlaying()
} else {
myMusic.StopPlaying()
}
}
}
When I tap the switch it does fire StopPlaying() but the music in the background continues to play despite the tap.
I am not sure why that is, unless the AV object isn't accessible from the original creation and therefore can't stop it properly; but so far I have been unable to figure that out either.
Any help or suggestions would be greatly appreciated.
By instantiating a new instance of the Music class in SettingsViewController you're effectively creating a new AVAudioPlayer instance that knows nothing about the one already instantiated.
Consider this code, which contains static properties and class methods:
import Foundation
import AVFoundation
class Music
{
public static var backgrndSound: AVAudioPlayer?
// AVAudioPlayer already has an isPlaying property
class func isMusicPlaying() -> Bool
{
return backgrndSound?.isPlaying ?? false
}
class func StartPlaying()
{
let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do
{
backgrndSound = try AVAudioPlayer(contentsOf: url)
backgrndSound?.numberOfLoops = -1
backgrndSound?.play()
}
catch
{
// couldn't load file :(
}
}
class func StopPlaying()
{
backgrndSound?.pause()
backgrndSound?.stop()
}
}
then access this using:
Music.isMusicPlaying()
Music.startPlaying()
Music.stopPlaying()
i.e. you'd not do var myMusic = Music()
This way there will always be a single instance of the AVAudioPlayer, Music.backgrndSound
This sample code changes backgrndSound to an optional ... you're effectively creating an unused AVAudioPlayer instance that is discarded as soon as you startPlaying.
It also removes the unnecessary isPlaying property, as AVAudioPlayer already has a property for this purpose.

Handling AVPlayerLayer when wrapping AVPlayer behaviour

I'm trying to wrap AVPlayer in my own class so I can provide a nicer API to use throughout my app and so I can mock player behaviour for testing with other objects (and because the AVPlayer KVOs are quite ugly to use!). Here's a simplified model of what I'm trying to do with just the play and pause functionality:
protocol VideoPlayerProtocol {
func play()
func pause()
}
class AVPlayerWrapped: VideoPlayerProtocol {
private let player = AVPlayer()
init(playerItem: AVPlayerItem) {
self.player.replaceCurrentItem(with: playerItem)
}
func play() {
player.play()
}
func pause() {
player.pause()
}
}
I also have a PlayerView which adds an AVPlayerLayer to a view. From the Apple docs, this is set by providing the view an AVPlayer:
class PlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
}
The problem is that when I setup an AVPlayerWrapped object, in order to display the playback in a view I need to reveal the underlying AVPlayer to the player property on PlayerView which defeats the purpose of me wrapping the player.
Is there a way for me to somehow use an AVPlayerLayer without my AVPlayerWrapped having to reveal its underlying player please? Or am I taking the wrong approach?
Any guidance much appreciated!
class AVPlayerWrapped: VideoPlayerProtocol {
fileprivate let player = AVPlayer()
init(playerItem: AVPlayerItem) {
self.player.replaceCurrentItem(with: playerItem)
}
func play() {
player.play()
}
func pause() {
player.pause()
}
}
extension AVPlayerLayer {
func setPlayerWrapper(_ playerWrapped: AVPlayerWrapped) {
player = playerWrapped.player
}
}
And
class PlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
func setPlayerWrapper(_ playerWrapped: AVPlayerWrapped) {
playerLayer.setPlayerWrapper(playerWrapped)
}
}
I believe you don't need a getter for your view - in my practice I haven't used it. But in case you do, you can do it with an associatedObject, but it's much slower than a real property and I would advise you to use this approach only in special cases.

How to track when song is finished AVAudioPlayer

I want to track when playing song is finished. I tried different solutions from the web but they could not solve my problem.
I implemented audioPlayerDidFinishPlaying method but it is not working.
How can I understand if playing song is finished?
I am playing songs with playSound function
playSound func:
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
audioPlayerDidFinishPlaying func:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is not working, not printing "finished"
}
How can I solve my problem? How to track when playing song is finished
EDIT: I am adding whole code.
//
// ViewController.swift
import UIKit
import SwiftVideoBackground
import AudioToolbox
import AVFoundation
class ViewController: UIViewController,AVAudioPlayerDelegate {
var player: AVAudioPlayer?
#IBOutlet weak var backgroundVideo: BackgroundVideo!
#IBOutlet weak var initialLabel: UILabel!
#IBOutlet weak var statementLabel: UILabel!
var mp3: [String] = ["turk_milleti_demokrattir","xyz"]
var fav: [String] = ["0","0"]
var name: [String] = ["Türk milleti demokrattır","xy"]
var toggleState = 1
#IBOutlet weak var playB: UIButton!
var counter = 0
var duration = 0.1
override func viewDidLoad() {
super.viewDidLoad()
player?.delegate = self
playB.setImage(UIImage(named: "playbtn.png"), for: .normal)
statementLabel.text = name[counter]
backgroundVideo.createBackgroundVideo(name: "abc", type: "mp4")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func likeButton(_ sender: Any) {
fav[counter] = "1"
print(fav[0...1])
}
#IBAction func playButton(_ sender: Any) {
let name = mp3[counter]
playSound(name: name)
let playBtn = sender as! UIButton
if toggleState == 1 {
player?.play()
toggleState = 2
playBtn.setImage(UIImage(named: "pausebtn.png"), for: .normal)
} else {
player?.pause()
toggleState = 1
playBtn.setImage(UIImage(named:"playbtn.png"),for: .normal)
}
}
#IBAction func nextButton(_ sender: Any) {
counter = counter + 1
if counter == mp3.count {
counter = 0
}
toggleState = 2
playB.setImage(UIImage(named: "pausebtn.png"), for: .normal)
playSound(name: mp3[counter])
statementLabel.text = name[counter]
}
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is not working, not printing "finished"
}
}
I solved my problem with help of Leo Dabus.
I changed my edited code. I moved player?.delegate = self
to playSound func. Finally, it is working.
playSound & audioPlayerDidFinishPlaying function:
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
player?.delegate = self
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is working now! printed "finished"!
}
Do not forget to add AVAudioPlayerDelegate to ViewController!
class ViewController: UIViewController,AVAudioPlayerDelegate {
You are not setting the player's delegate correctly.
In viewDidLoad, your player is going to be nil, so this line:
player?.delegate = self
Will do nothing (The question mark is optional chaining, so if player == nil, it does nothing.)
You need to set the delegate after loading the player.

Change of AVPlayer current item (item url) and start playback

I have a UITableView with a posts that contain URLs to a mp3 files. With a tap on a button in a UITableViewCell I plan to fetch a singleton instance of AVPlayer. How to assign new URL to a AVPlayer and start playback? AVPlayer's currentItem is only a { get } property.
This is what I have now.
import Foundation
import AVFoundation
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static func shared() -> AVPlayer{
return AVPlayer()
}
func playItem(at itemURL: URL) {
if StreamMusicPlayer.shared().isPlaying {
StreamMusicPlayer.shared().pause()
}
//Change url of AVPlayerItem
//And and assign it to shared() instance then begin playback
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
This might be what you want. It's not perfect.
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static var shared = AVPlayer()
static func playItem(at itemURL: URL) {
StreamMusicPlayer.shared = AVPlayer(url: itemURL)
StreamMusicPlayer.shared.play()
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
I hade to change the static function to a normal variable. Every time you access .shared() it would initialize and return a new instance.
Then you can do:
StreamMusicPlayer.playItem(at: URL(string: "http://antoon.io/e/mp3/")!)
// Delay so you can hear it changing
StreamMusicPlayer.playItem(at: URL(string: "http://antoon.io/e/mp3/2.mp3")!)

Resources