SwfitUI play sound on a button event - swift

so i got this function for playing sound:
func playSound(){
let url = Bundle.main.url(forResource: "kaiBark", withExtension:"mp3")
guard url != nil else{
return
}
do{
player = try AVAudioPlayer(contentsOf: url!)
player?.play()
}catch{
print("sounds error")
}
}
for my app the user probably hit this button few times per second and when i tap the button more then 1 time the sounds only playing when i stop tapping the button meanwhile i need it to play each time i press not after i done pressing.
what am i missing for making this work?
thank you!

Related

Playing 2 sounds in succession ... but before 2nd sound finishes trying to stop the entire 2 sound sequence

Playing 2 sounds in succession ... but before 2nd sound finishes, trying to stop the entire 2 sound sequence.
Note that it does not matter where I choose to stop the sequence - even, e.g., while the 1st sound is playing.
Here are the playSound and stopSound + 1 helper func code snippets:
func playSound(theSoundName: String) {
guard let url = Bundle.main.url(forResource: "audio/" + theSoundName,
withExtension: "mp3") else {
print("sound not found")
return
}
itsSoundPlayer = setupAudioPlayer(theURL: url)
if theSoundName == "roar" {
itsSoundPlayer?.numberOfLoops = -1 // forever
}
itsSoundPlayer?.play()
} // playSound
func stopSound() {
itsSoundPlayer?.stop() // stops whatever is playing via playSound(...)
} // stopSound
func setupAudioPlayer(theURL: URL) -> AVAudioPlayer? {
do {
// Make this App ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
let soundPlayer = try AVAudioPlayer(contentsOf: theURL)
return soundPlayer
}
catch let error as NSError {
print("error: \(error.localizedDescription)")
return nil
}
} // setupAudioPlayer
Okay, so the above are the basic building blocks ... now for a specific example of their use:
func attaBoy() {
stopSound() // stop whatever is playing via playSound(...)
playSound(theSoundName: "attaboy") // then play the new sound
// give "attaboy" time to finish before returning to "roar"
let theDelay = Double(2.0)
DispatchQueue.main.asyncAfter(deadline: .now() + theDelay) {
self.playSound(theSoundName: "roar") // replay
}
} // attaBoy
At an undetermined point when either attaboy or roar is playing, I want to call stopSound(). This stoppage could occur while attaboy is playing or while roar is playing.
I've tried to use the above code as is ... but when I try to stop the 2-sound sequence while attaboy is playing, attaboy stops as it should, but roar still plays.
Is there some other approach I should try ?

Using an AVPlayer returns a "non-Multipath connection" error

I'm using AVKit to play a youtube URL.
I have this code inside a button action:
#IBAction func trailerButtonAction(_ sender: Any) {
guard let youtubeUrl = youtubeURL else { return }
let player = AVPlayer(url: youtubeUrl)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
present(playerViewController, animated: true) {
player.play()
}
}
The URL is valid, but when I press the button, the video doesn't stop loading and I'm getting this message on Debug area:
nw_endpoint_flow_copy_multipath_subflow_counts Called on non-Multipath connection
Edit:
I found that AVPlayer doesn't support youtube URL
I would say this log isn't necessarily relevant. I was getting this error when trying to playback on the simulator but it wasn't happening on a real device.
One workaround would be to use a 12.4.x simulator as it does not exhibit this issue. Only the 13.x simulators are showing this error. It happens to repeatedly that it slows down Simulator to a craw until all requested tracks have been buffered.
To combat this while testing, I am either not turning on AVPlayer or I am only buffering a short track.
To cut down on the number of errors try initing your AVPlayer like so:
var avPlayer : AVPlayer = AVPlayer()
This may cut down the errors by 30%.

setting sharedInstance of audiosessions category options not working

I'm building an App where I want to play music from the local library AND use the AVQueuePlayer, to play a list of tracks, where once in a while there's a break between the track. The music works totally fine, now, Since I want everything to work in the background, my only option when playing the AVQueuePlayer and want a break, is to play a silent AVPlayerItem (an empty audiofile.) I want the Music to play normally when silent AVPlayerItems are playing, I achieved that by setting AVAudioSession category to .playback and with options: .mixWithOthers, and when a regular track (not silent) is played by the AVQueuePlayer, I want the music to be dimmed a little.
I've tried changing the the audio session like this: AVAudioSession.sharedInstance().setCategory(.playback, options: [.mixWithOthers, .duckOthers])
but it doesn't change anything. When I check if it's changed like this: if AVAudioSession.sharedInstance().categoryOptions == .mixWithOthers {
print("Music mixing with silence")
}
the session category options seem to have changed, but it doesn't affect the audio in the end.
#objc func playerDidFinishPlaying() {
print("Player finished!")
guard let queuePlayer = queuePlayer else { return }
if let index = queuePlayer.items().lastIndex(where: { (playerItem) -> Bool in
return playerItem == queuePlayer.currentItem
}) {
if let nextItem = queuePlayer.items()[index + 1].asset as? AVURLAsset {
if nextItem.url.absoluteString != oneSecondSilenceUrl?.absoluteString {
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.mixWithOthers, .duckOthers])
//When narrator is speaking
} else {
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
AVAudioSession.sharedInstance()
//When there's silence and only music should be playing
}
}
}
if AVAudioSession.sharedInstance().categoryOptions == .mixWithOthers {
print("Music mixing with silence")
}
try? AVAudioSession.sharedInstance().setActive(true)
}
enter code here
This method runs whenever the the AVQueuePlayer has finished playing one of its items. I use it to check and see if the NEXT item is a silent track or not. If its a silent track, I want .mixWithOthers, If its a track that is NOT silent and the track actually plays audio, I want category options: .duckOthers :)
Any help, response or answer is very much appreciated! :)
Switching to main thread seemed to fix the problem. Weird.
DispatchQueue.main.async {
if AVAudioSession.sharedInstance().categoryOptions != .duckOthers {
try? AVAudioSession.sharedInstance().setActive(false)
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [ .duckOthers])
}
}

How To Get Time of Video Playback When User Closes AVPlayer

In the app, the user is supposed to watch a setup video. If they don't finish watching and click the 'close' button, I want to know where they stopped so I can have the video start at that spot once they start watching again.
I want to know what the time is that they stopped watching so I can save that number to the server for later use, using the following code:
player.seek(to: CMTimeMakeWithSeconds(637, 1))
I tried the following:
func showUnplayedVideo() {
// 1. check to see if user has already watched the app overview video
ref.child("videos")
.child("appOverview")
.child("watched")
.observeSingleEvent(of: .value) { (snapshot) in
let watched = snapshot.value as? Bool ?? false
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: VideoURL.appOverview.rawValue) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
self.playerVC.player = player
// 3. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerDidFinishPlaying),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.playerVC.player?.currentItem)
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerWasStoppedPrematurely),
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: self.playerVC.player?.currentItem)
self.present(self.playerVC, animated: true) {
self.playerVC.player?.play()
}
}
}
}
#objc func playerWasStoppedPrematurely(note: NSNotification) {
self.playerVC.dismiss(animated: true)
print(playerVC.player?.currentTime())
}
The first notification 'AVPlayerItemDidPlayToEndTime' works great (for users who watch the video all the way through), but my other notification 'AVPlayerItemFailedToPayToEndTime' doesn't work (for users who stop part way through the video).
How do I get the stopping point of my users if they don't watch the whole video?
EDIT #1
This is what the screen looks like when the video is playing:
You are presenting the playerVC from a view controller. When the playerVC dismisses, viewDidAppear() in your view controller will be called. This is a choice to put the time checking code.
Original Answer
You are already using player.currentTime(). Why not also put it in the close button handler? Something like this
let current = Double(CMTimeGetSeconds(avPlayer.currentTime()))

How to play the same sound overlapping with AVAudioPlayer?

This code plays the sound when the button is tapped but cancels the previous if it is pressed again. I do not want this to happen I want the same sound to overlap when repeatedly pressed. I believe it might be due to using the same AVAudioPlayer as I have looked on the internet but I am new to swift and want to know how to create a new AVAudioPlayer everytime the method runs so the sounds overlap.
func playSound(sound:String){
// Set the sound file name & extension
let soundPath = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource(sound, ofType: "mp3")!)
do {
//Preperation
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch _{
}
do {
try AVAudioSession.sharedInstance().setActive(true)
}
catch _ {
}
//Play the sound
var error:NSError?
do{
audioPlayer = try AVAudioPlayer(contentsOfURL: soundPath)
}catch let error1 as NSError {
error = error1
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
To play two sounds simultaneously with AVAudioPlayer you just have to use a different player for each sound.
In my example I've declared two players, playerBoom and playerCrash, in the Viewcontroller, and I'm populating them with a sound to play via a function, then trigger the play at once:
import AVFoundation
class ViewController: UIViewController {
var playerBoom:AVAudioPlayer?
var playerCrash:AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
playerBoom = preparePlayerForSound(named: "sound1")
playerCrash = preparePlayerForSound(named: "sound2")
playerBoom?.prepareToPlay()
playerCrash?.prepareToPlay()
playerBoom?.play()
playerCrash?.play()
}
func preparePlayerForSound(named sound: String) -> AVAudioPlayer? {
do {
if let soundPath = NSBundle.mainBundle().pathForResource(sound, ofType: "mp3") {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
return try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: soundPath))
} else {
print("The file '\(sound).mp3' is not available")
}
} catch let error as NSError {
print(error)
}
return nil
}
}
It works very well but IMO is not suitable if you have many sounds to play. It's a perfectly valid solution for just a few ones, though.
This example is with two different sounds but of course the idea is exactly the same for two identic sounds.
I could not find a solution using just AVAudioPlayer.
Instead, I have found a solution to this problem with a library that is built on top of AVAudioPlayer.
The library allows same sounds to be played overlapped with each other.
https://github.com/adamcichy/SwiftySound