i'm working on a audio player where i have to allow the audio to play in background, i have done with audio sessions but i stuck in progress bar when i play audio in background and again open my app the progress bar and timer stoped, i need to run them also in background. i mean progress bar should be updated with timer if i move to other views or minimise the app.
#IBAction func btnPlayMusicAction(_ sender: Any)
{
if isPlaying == false {
self.btnPlayMusicPro.setImage(#imageLiteral(resourceName: "PauseIcon"), for: .normal)
self.isPlaying = true
self.audioPlayer?.play()
timer = Timer.scheduledTimer(timeInterval:1.0, target: self,selector: (#selector(updateRemainingTime)), userInfo: nil, repeats:true)
}else{
self.btnPlayMusicPro.setImage(#imageLiteral(resourceName: "PlayIcon"), for: .normal)
self.isPlaying = false
if (audioPlayer?.isPlaying)!{
self.audioPlayer?.pause()
}
self.timer?.invalidate()
}
}
her is the method updateRemainingTime()
#objc func updateRemainingTime(){
let currentTime = Int((audioPlayer?.currentTime)!)
let duration = Int((audioPlayer?.duration)!)
let seconds = currentTime % 60
let minutes = (currentTime / 60) % 60
let hours = (currentTime / 3600)
var remainingTime:String = ""
if currentTime >= 3600{
remainingTime = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}else{
remainingTime = String(format: "%02d:%02d", minutes, seconds)
}
self.lblPlayMusicTimePro.text = remainingTime
Tray.remainingAudioTime = remainingTime
if currentTime == duration-1{
timer?.invalidate()
if (audioPlayer?.isPlaying)!{
self.audioPlayer?.currentTime = 0
self.audioPlayer?.stop()
}
self.btnPlayMusicPro.setImage(#imageLiteral(resourceName: "PlayIcon"), for: .normal)
self.isPlaying = false
self.musicProgressViewPro.setProgress(0.0, animated: true)
self.lblPlayMusicTimePro.text = "00:00"
self.lblMusicTimePro.text = "00:00"
}else {
let currentProgress = Float((audioPlayer?.currentTime)! / (audioPlayer?.duration)!)
Tray.progressTime = currentProgress
self.musicProgressViewPro.setProgress(currentProgress, animated: true)
}
}
Related
I have a countdown Timer that shows seconds and milliseconds. The user can start/stop recording multiple times until the timer hits zero. The user can also delete a previous recording at which point I have to re-add that deleted time back into the initial 20 secs. There are 2 issues.
The first issue is when the timer is stopped, the remaining time that shows on the timer label doesn't match the time culmination of the recordings. From my understanding this might be a RunLoop issue and I don't think there is anything that I can do about the inaccuracies.
let initialTime = 20.0
var cumulativeTimeForAllAssests = 0.0
for asset in arrOfAssets {
let assetDuration = CMTimeGetSeconds(asset.duration)
print("assetDuration: ", assetDuration)
cumulativeTimeForAllAssests += assetDuration
}
print("\ncumulativeTimeForAllAssests: ", cumulativeTimeForAllAssests)
After starting/stopping 5 times, the remaining time on the timer label says 16.5 but the culmination of the assets time is 4.196666.... The timer label should say 15.8, it's 0.7 milli off. The more I start/stop the recording, the more inaccurate/further off the culmination time - the initial time and the timer label time is.
assetDuration: 0.7666666666666667
assetDuration: 0.9666666666666667
assetDuration: 0.7983333333333333
assetDuration: 0.7333333333333333
assetDuration: 0.9316666666666666
cumulativeTimeForAllAssests: 4.196666666666667
The second issue is because I'm using seconds and milliseconds in my timerLabel, when I add re-add the subtracted time back in via deleteAssetAndUpdateTimer(...), I use the parts of modf() to update the seconds and milliseconds. I couldn't think of another way to update the timer. I know there has to be a more accurate way to do it.
Timer code:
weak var timer: Timer?
var seconds = 20
var milliseconds = 0
let initialTime = 20.0
func startTimer() {
invalidateTimer()
if seconds == Int(initalTime) && milliseconds == 0 {
timerIsRunning()
}
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
self?.timerIsRunning()
})
}
func timerIsRunning() {
updateTimerLabel()
if milliseconds == 0 {
seconds -= 1
}
milliseconds -= 1
if milliseconds < 0 {
milliseconds = 9
}
if seconds == 0 && milliseconds == 0 {
invalidateTimer()
updateTimerLabel()
}
}
func invalidateTimer() {
timer?.invalidate()
timer = nil
}
func updateTimerLabel() {
let milisecStr = "\(milliseconds)"
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = "\(secondsStr).\(milisecStr)"
}
Delete asset and update timer code:
// the timer is stopped when this is called
func deleteAssetAndUpdateTimer(_ assetToDelete: AVURLAsset) {
var cumulativeTimeForAllAssests = 0.0
for asset in arrOfAssets {
let assetDuration = CMTimeGetSeconds(asset.duration)
cumulativeTimeForAllAssests += assetDuration
}
let timeFromAssetToDelete = CMTimeGetSeconds(assetToDelete.duration)
let remainingTime = self.initialTime - cumulativeTimeForAllAssests
let updatedTime = remainingTime + timeFromAssetToDelete
let mod = modf(updatedTime)
self.seconds = Int(mod.0)
self.milliseconds = Int(mod.1 * 10)
updateTimerLabel()
// remove assetToDelete from array
}
The big issue here was I was using a Timer to countdown which was incorrect. Following #LeoDabus' comments, I instead used CACurrentMediaTime():
let timerLabel = UILabel()
let maxRecordingTime = 30.0
lazy var elapsedTime = maxRecordingTime
var startTime: CFTimeInterval?
var endTime: CFTimeInterval?
weak var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
updateTimerLabel(with: Int(maxRecordingTime))
}
#IBAction func recordButtonPressed(_ sender: UIButton) {
if startTime == nil {
startTimer()
} else {
stopTimer(updateElapsed: true)
}
}
func startTimer() {
if elapsedTime == 0 { return }
stopTimer()
startTime = CACurrentMediaTime()
endTime = startTime! + elapsedTime
print("startTime: \(startTime!) | endTime: \(endTime!)")
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
self?.timerIsRunning()
}
}
func timerIsRunning() {
guard let startTime = startTime, let endTime = endTime else { return }
let currentTime = CACurrentMediaTime()
let remainingTime = currentTime - startTime
print("%2d %.3lf", elapsedTime, remainingTime)
if currentTime >= endTime {
print("stopped at - currentTime: \(currentTime) | endTime: \(endTime)")
stopTimer(updateElapsed: true, currentTime: currentTime)
return
}
let countDownTime: Double = elapsedTime - remainingTime
let seconds = Int(countDownTime)
updateTimerLabel(with: seconds)
}
func updateTimerLabel(with seconds: Int) {
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = secondsStr
}
func stopTimer(updateElapsed: Bool = false, currentTime: Double? = nil) {
timer?.invalidate()
timer = nil
if updateElapsed {
updateElapsedTime(using: currentTime)
}
startTime = nil
endTime = nil
}
func updateElapsedTime(using currentTime: Double? = nil) {
guard let startTime = startTime else { return }
var timeNow = CACurrentMediaTime()
if let currentTime = currentTime {
timeNow = currentTime
}
var updatedTime = elapsedTime - (timeNow - startTime)
if updatedTime < 0 {
updatedTime = 0
}
elapsedTime = updatedTime
}
func resetElapsedTime() { // This is for a resetButton not shown here
elapsedTime = maxRecordingTime
}
audio link
I uploaded a recording in my google drive link(mentioned above) and set that to public. I am trying to play the audio using audio kit and other audio player(cocoapods) with url, downloading the file and play and I tried converting to other formats nothing worked for me. I am unable to play it. This audio is not playing in safari browser also. The recording is done from the web platform and the generated audio format is .opus
Try please the code below and tell me if is working for you.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player: AVPlayer?
var playerItem:AVPlayerItem?
fileprivate let seekDuration: Float64 = 10
#IBOutlet weak var labelCurrentTime: UILabel!
#IBOutlet weak var labelOverallDuration: UILabel!
#IBOutlet weak var playbackSlider: UISlider!
#IBOutlet weak var playButtonOutlet: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
initAudioPlayer()
}
//call this mehtod to init audio player
func initAudioPlayer() {
let url = URL(string: "https://argaamplus.s3.amazonaws.com/eb2fa654-bcf9-41de-829c-4d47c5648352.mp3")
let playerItem:AVPlayerItem = AVPlayerItem(url: url!)
player = AVPlayer(playerItem: playerItem)
playbackSlider.minimumValue = 0
//To get overAll duration of the audio
let duration : CMTime = playerItem.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)
labelOverallDuration.text = self.stringFromTimeInterval(interval: seconds)
//To get the current duration of the audio
let currentDuration : CMTime = playerItem.currentTime()
let currentSeconds : Float64 = CMTimeGetSeconds(currentDuration)
labelCurrentTime.text = self.stringFromTimeInterval(interval: currentSeconds)
playbackSlider.maximumValue = Float(seconds)
playbackSlider.isContinuous = true
player!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.player!.currentItem?.status == .readyToPlay {
let time : Float64 = CMTimeGetSeconds(self.player!.currentTime());
self.playbackSlider.value = Float ( time );
self.labelCurrentTime.text = self.stringFromTimeInterval(interval: time)
}
let playbackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp
if playbackLikelyToKeepUp == false{
print("IsBuffering")
self.playButtonOutlet.isHidden = true
} else {
//stop the activity indicator
print("Buffering completed")
self.playButtonOutlet.isHidden = false
}
}
//change the progress value
playbackSlider.addTarget(self, action: #selector(playbackSliderValueChanged(_:)), for: .valueChanged)
//check player has completed playing audio
NotificationCenter.default.addObserver(self, selector: #selector(self.finishedPlaying(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)}
#objc func playbackSliderValueChanged(_ playbackSlider:UISlider) {
let seconds : Int64 = Int64(playbackSlider.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
player!.seek(to: targetTime)
if player!.rate == 0 {
player?.play()
}
}
#objc func finishedPlaying( _ myNotification:NSNotification) {
playButtonOutlet.setImage(UIImage(named: "play"), for: UIControl.State.normal)
//reset player when finish
playbackSlider.value = 0
let targetTime:CMTime = CMTimeMake(value: 0, timescale: 1)
player!.seek(to: targetTime)
}
#IBAction func playButton(_ sender: Any) {
print("play Button")
if player?.rate == 0
{
player?.play()
self.playButtonOutlet.isHidden = true
playButtonOutlet.setImage(UIImage(systemName: "pause"), for: UIControl.State.normal)
} else {
player?.pause()
playButtonOutlet.setImage(UIImage(systemName: "play"), for: UIControl.State.normal)
}
}
func stringFromTimeInterval(interval: TimeInterval) -> String {
let interval = Int(interval)
let seconds = interval % 60
let minutes = (interval / 60) % 60
let hours = (interval / 3600)
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
#IBAction func seekBackWards(_ sender: Any) {
if player == nil { return }
let playerCurrenTime = CMTimeGetSeconds(player!.currentTime())
var newTime = playerCurrenTime - seekDuration
if newTime < 0 { newTime = 0 }
player?.pause()
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player?.seek(to: selectedTime)
player?.play()
}
#IBAction func seekForward(_ sender: Any) {
if player == nil { return }
if let duration = player!.currentItem?.duration {
let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
let newTime = playerCurrentTime + seekDuration
if newTime < CMTimeGetSeconds(duration)
{
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player!.seek(to: selectedTime)
}
player?.pause()
player?.play()
}
}
}
Here is a DEMO:
https://github.com/florentin89/PlaySongFromExternalSource
I got two problems, the first one is I'm getting the error message described above (it includes a beautiful crash) every time a Timer calls this function:
func updateTime() {
let currentTime = Int(player.currentTime)
let minutes = currentTime/60
let seconds = currentTime - minutes * 60
if seconds < 10 {
tempo.text = String(minutes) + ":0" + String(seconds)
} else {
tempo.text = String(minutes) + ":" + String(seconds)
}
durationSliderOutlet.value = Float(player.currentTime)
if currentTime == Int(player.duration - 1) {
playpauseOutlet.setTitle("Play", for: .normal)
isPlaying = false
}
print("currentTime: \(currentTime)")
print(Int(player.duration))
print("minutes: \(minutes)")
print("seconds: \(seconds)")
print(isPlaying)
}
I'm just trying to display the current time of an audio file in a label, and if I comment out this code
if seconds < 10 {
tempo.text = String(minutes) + ":0" + String(seconds)
} else {
tempo.text = String(minutes) + ":" + String(seconds)
}
...everything works just fine!
The second one throws the same crash and error when I press a button...
#IBAction func playpauseButton(_ sender: UIButton) {
if isPlaying == false {
playpauseOutlet.setImage(#imageLiteral(resourceName: "pause.png"), for: .normal)
player.play()
isPlaying = true
} else {
playpauseOutlet.setImage(#imageLiteral(resourceName: "play.png"), for: .normal)
player.pause()
isPlaying = false
}
}
Note: (#imageLiteral(resourceName: "play.png") appears as a little white rectangle. And if I try something like playpauseOutlet.setImage(UIImage(named: "play.png"), for: .normal) it also crashes.
What I'm pretending to do with this code is to change the "play" image of a button to "pause" image or vice versa depending of the isPlaying state. Thank in advance!
I am working on a stopwatch app that begins flashing when approaching limit and again when overtime. Everything works great except for when the app is pushed to the background and then brought to the foreground again. At that time the animation will not start.
I have tried moving the code to applicationWillEnterForeground and applicationDidBecomeActive but it produces a fatal error:
unexpectedly found nil while unwrapping an Optional value
Am I missing something basic here?
There is more to the code of course (calculations, etc.), but this is relevant section. (Edit: I've expanded this to provide a better view of what I'm doing.)
#IBAction func start(_ sender: AnyObject) {
if timerOn == 0 {
myTimer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate
timerOn = 1
redOrYellow = "white"
startButton.superview?.sendSubview(toBack: startButton)
background.superview?.sendSubview(toBack: background)
pickerMask.isHidden = false
}
}
....
func animateLight() {
if redOrYellow == "red" {
print("red")
//background.superview?.sendSubview(toBack: background)
UIView.animate(withDuration: 0.3, delay: 0.01, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.allowUserInteraction], animations: {
self.background.backgroundColor = UIColor.red
self.background.backgroundColor = UIColor.white
}, completion: nil)
} else if redOrYellow == "yellow" {
print("yellow")
//background.superview?.sendSubview(toBack: background)
UIView.animate(withDuration: 1, delay: 0.1, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.allowUserInteraction], animations: {
self.background.backgroundColor = UIColor.yellow
self.background.backgroundColor = UIColor.white
}, completion: nil)
}
}
func updateCounter() {
let currentTime = NSDate.timeIntervalSinceReferenceDate
var elapsedTime: TimeInterval = currentTime - startTime
totalTime = elapsedTime
if totalTime >= partBaseTimes[numberOfSelectedPart] {
if redOrYellow != "red" {
redOrYellow = "red"
animateLight()
}
} else if totalTime >= (partBaseTimes[numberOfSelectedPart] - 30) {
if redOrYellow != "yellow" {
redOrYellow = "yellow"
animateLight()
}
}
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (TimeInterval(minutes) * 60)
let seconds = UInt8(elapsedTime)
elapsedTime -= TimeInterval(seconds)
let fractionOfSecond = UInt8(elapsedTime * 10)
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
let strFractionOfSecond = String(format: "%01d", fractionOfSecond)
talkTimer.text = "\(strMinutes):\(strSeconds).\(strFractionOfSecond)"
}
Hello I've been trying to figure out this issue and don't know where to look. I'm using AVPlayer to play videos, and I have a UISlider whose value updates every second based on the progresss of the video using addPeriodicTimeObserverForInterval.
When I press pause, the UISlider thumb stops immediately as expected. However, immediately after I press play to resume the video, the thumb moves slightly before it continues progressing as normal every second.
I can't figure out why it's doing this. I'd like to have the UISlider thumb progress fluently, i.e. if I paused the video at 1.5 seconds in, and I play the video again, it SHOULD wait 0.5 seconds before the thumb moves again.
Here's a snippet of my code:
override func viewDidLoad()
{
super.viewDidLoad()
....
isPlaybackSliderTouched = false
isPauseButtonTouched = false
setUpPlayerControls()
....
}
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(true)
let timeIntervalOne: CMTime = CMTimeMakeWithSeconds(1.0, 10)
playbackSliderTimer = avPlayer.addPeriodicTimeObserverForInterval(timeIntervalOne,
queue: dispatch_get_main_queue()) { (elapsedTime: CMTime) -> Void in
self.observeTime(elapsedTime)
}
....
playPauseButton.addTarget(self, action: "onClick:", forControlEvents: .TouchUpInside)
}
func observeTime(elapsedTime: CMTime)
{
let duration = CMTimeGetSeconds(avPlayer.currentItem!.duration)
if isfinite(duration) && avPlayer.rate == 1
{
print("ENNNTNTTERRR")
let elapsedTime = CMTimeGetSeconds(elapsedTime)
updatePlaybackSlider(elapsedTime: elapsedTime, duration: duration)
}
}
func updatePlaybackSlider(elapsedTime: Float64, duration: Float64)
{
if isPlaybackSliderTouched == false
{
let sliderValue: Float = Float(elapsedTime / duration)
print("sliderValue = \(sliderValue)")
self.playbackSlider.setValue(sliderValue, animated: true)
self.currentTimeLabel.text = self.convertSecondsToHHMMSS(elapsedTime)
self.endTimeLabel.text = self.convertSecondsToHHMMSS(duration - elapsedTime)
print("currentTimeLabel.text = \(currentTimeLabel.text)")
print("endTimeLabel.text = \(endTimeLabel.text)")
}
}
func convertSecondsToHHMMSS(seconds: Float64) -> String
{
let time: Int = Int( floor(seconds) )
print("time = \(time)")
let hh: Int = time / 3600
let mm: Int = (time / 60) % 60
let ss: Int = time % 60
print("seconds = \(ss)")
if hh > 0
{
return String(format: "%02d:%02d:%02d", hh, mm, ss)
}
else
{
return String(format: "%02d:%02d", mm, ss )
}
}
deinit
{
avPlayer.removeTimeObserver(playbackSliderTimer)
}
func onClick(sender: UIButton)
{
print("onClick")
if sender == playPauseButton
{
print("playPauseButton touched")
let playerIsPlaying:Bool = avPlayer.rate > 0
if (playerIsPlaying)
{
isPauseButtonTouched = true
avPlayer.pause()
sender.selected = true
}
else
{
isPauseButtonTouched = false
avPlayer.play()
sender.selected = false
}
}
}
Here's a sample output immediately after I press pause from play state:
setUpPlayerControls
viewDidAppear
ENNNTNTTERRR
sliderValue = 0.0
time = 0
seconds = 0
time = 39
seconds = 39
currentTimeLabel.text = Optional("00:00")
endTimeLabel.text = Optional("-00:39")
ENNNTNTTERRR
sliderValue = 4.76564e-05
time = 0
seconds = 0
time = 39
seconds = 39
currentTimeLabel.text = Optional("00:00")
endTimeLabel.text = Optional("-00:39")
ENNNTNTTERRR
sliderValue = 0.0252767
time = 1
seconds = 1
time = 38
seconds = 38
currentTimeLabel.text = Optional("00:01")
endTimeLabel.text = Optional("-00:38")
ENNNTNTTERRR
sliderValue = 0.0505207
time = 2
seconds = 2
time = 37
seconds = 37
currentTimeLabel.text = Optional("00:02")
endTimeLabel.text = Optional("-00:37")
handleSingleTap
onClick
playPauseButton touched
pause touched
Here's the continuing output when I tap the play from pause state:
onClick
playPauseButton touched
play touched
ENNNTNTTERRR
sliderValue = 0.0718539
time = 2
seconds = 2
time = 36
seconds = 36
currentTimeLabel.text = Optional("00:02")
endTimeLabel.text = Optional("-00:36")
ENNNTNTTERRR
sliderValue = 0.0722224
time = 2
seconds = 2
time = 36
seconds = 36
currentTimeLabel.text = Optional("00:02")
endTimeLabel.text = Optional("-00:36")
ENNNTNTTERRR
sliderValue = 0.0757659
time = 3
seconds = 3
time = 36
seconds = 36
currentTimeLabel.text = Optional("00:03")
endTimeLabel.text = Optional("-00:36")
ENNNTNTTERRR
sliderValue = 0.101012
time = 4
seconds = 4
time = 35
seconds = 35
currentTimeLabel.text = Optional("00:04")
endTimeLabel.text = Optional("-00:35")
As you can see the end of the first output when I press pause to the beginning of the second output when I press play, there is 2 unnecessary calls for time 00:02 to observeTime(elapsedTime), which in turns call my updatePlaybackSlider, which in turn slightly shifts the slider value to the right a bit before it goes back to calling observeTime(elapsedTime) like normal, i.e. every 1 second.
What are some suggestions for me to fix this?
Thanks.