I'm playing a song using AVAudioPlayer. I need a progress bar to show the progress of the song.
My issue is that the progress bar's progress isn't working properly. Within 2-3 seconds, it finishes its progress.
func playMusic() {
do {
player = try AVAudioPlayer(contentsOf: (currentSong?.mediaURL)!)
guard let player = player else { return }
player.prepareToPlay()
player.play()
updater = CADisplayLink(target: self, selector: #selector(self.musicProgress))
updater.frameInterval = 1
updater.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
playButton.setImage(UIImage.init(named: "pause"), for: .normal)
} catch let error as NSError {
print(error.description)
}
}
#objc func musicProgress() {
let normalizedTime = Float(self.player?.currentTime as! Double * 100.0 / (self.player?.duration as! Double) )
self.progressMusic.progress = normalizedTime
}
The issue is here:
let normalizedTime = Float(self.player?.currentTime as! Double * 100.0 / (self.player?.duration as! Double) )
With this you will get a value between 0.0 and 100.0, but according to UIProgressView documentation, progress must be between 0.0 and 1.0. Try
let normalizedTime = Float(self.player?.currentTime as! Double / (self.player?.duration as! Double) )
Related
On a screen inside my app I have both an AVAudioPlayer for music and an AVPlayer for videos. The user can swap out different songs and different videos but can only play one at a time. They can play either the audioPlayer or watch videos on the avPlayer.
I have MPRemoteCommandCenter that works fine for both when using pause/play/ff/rewind. The issue is I can't display the currentTime or duration for either on the lock screen. I tried this but it doesn't say where to put the code.
This is what I tried so that every time the user switches songs or videos I have all of the available data for the new items:
Audio-
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioTrack)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
audioPlayer?.play()
setupNowPlayingForAudio()
} catch {
}
func setupNowPlayingForAudio() {
guard let audioPlayer = audioplayer else { return }
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "My App Name"
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Float(audioPlayer.currentTime)
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Float(audioPlayer.duration)
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = audioPlayer.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
Video-
playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) {
switch (player.status) {
case .readyToPlay:
player?.play()
setupNowPlayingForVideo()
}
}
func setupNowPlayingForVideo() {
guard let player = player, let playerItem = player.currentItem else { return }
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "My App Name"
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playerItem.currentTime().seconds
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem.asset.duration.seconds
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
The MPRemoteCommandCenter is set in viewDidLoad along with the AVAudioSession
I followed this answer which says that you have to add it to any pause/play/ff/rewind buttons, slider, and any observers that listen to the player playing/stopping. Here is the way that I did it. This works fine for me on the Lock Screen.
Here is where I used the below function for the AudioPlayer-
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioTrack)
// ...
audioPlayer?.play()
setupNowPlaying(musicPlayer: audioPlayer)
} catch { }
func audioPlayerPausePlayButton() {
// ...
setupNowPlaying(musicPlayer: audioPlayer)
}
func audioPlayerFastFowardAndRewindButton() {
// ... ff or rewind functions
setupNowPlaying(musicPlayer: audioPlayer)
}
Here is where I used the below function for the AVPlayer-
playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) { // ...
switch (player.status) {
case .readyToPlay:
// ... this should occur on the MainQueue
self?.player?.play()
self?.setupNowPlaying(videoPlayer: self?.player)
}
}
// .. I also added it to any other observers that listen to the player stopping
func videoPlayerPausePlayButton() {
// ...
setupNowPlaying(videoPlayer: player)
}
func videoPlayerFastFowardAndRewindButton() {
// ...
player?.seek(to: whateverSeekTime) { [weak self](_) in
self?.setupNowPlaying(videoPlayer: self?.player)
}
}
Dictionary values for the CommandCenter to show on the Lock Screen
func setupNowPlaying(musicPlayer: AVAudioPlayer? = nil, videoPlayer: AVPlayer? = nil) {
var nowPlayingInfo = [String : Any]()
// audio
if let musicPlayer = musicPlayer, let musicUrl = musicPlayer.url {
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = musicUrl
nowPlayingInfo[MPMediaItemPropertyTitle] = musicUrl.lastPathComponent
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.audio.rawValue
let currentTime: TimeInterval = musicPlayer.currentTime
let musicCurrentTimeCMTime = CMTime(seconds: currentTime, preferredTimescale: 1000)
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(musicCurrentTimeCMTime)
let musicDuration: TimeInterval = musicPlayer.duration
let musicDurationCMTime = CMTime(seconds: musicDuration, preferredTimescale: 1000)
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = CMTimeGetSeconds(musicDurationCMTime)
if musicPlayer.isPlaying {
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1
} else {
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0
}
}
// video
if let videoPlayer = videoPlayer, let currentItem = videoPlayer.currentItem {
if let videoAsset = currentItem.asset as? AVURLAsset {
let videoUrl = videoAsset.url
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = videoUrl
nowPlayingInfo[MPMediaItemPropertyTitle] = videoUrl.lastPathComponent
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
}
if let videoDuration: CMTime = videoPlayer.currentItem?.duration {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = CMTimeGetSeconds(videoDuration)
}
let videoCurrentTime: CMTime = videoPlayer.currentTime()
let videoCurrentTimeAsSecs = CMTimeGetSeconds(videoCurrentTime)
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = videoCurrentTimeAsSecs
print("videoCurrentTimeAsSecs: ", videoCurrentTimeAsSecs)
if videoPlayer.isPlaying {
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1
} else {
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0
}
}
nowPlayingInfo[MPMediaItemPropertyTitle] = "Your App Name"
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
I am playing videos from the user's library in my app. I use this method in ViewDidLoad() to get the video:
fileprivate let imageManager = PHImageManager()
fileprivate var playerLayer: AVPlayerLayer?
fileprivate var player:AVPlayer?
fileprivate var videoView:UIView?
imageManager.requestPlayerItem(forVideo: videoAsset, options: options, resultHandler: { playerItem, info in
DispatchQueue.main.sync {
guard self.playerLayer == nil else { return }
self.player = AVPlayer(playerItem: playerItem)
self.playerLayer = AVPlayerLayer(player: self.player)
self.videoView = UIView(frame: self.view.frame)
self.videoView?.contentMode = .scaleAspectFit
self.playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
self.playerLayer?.frame = self.videoView!.layer.bounds
self.videoView!.layer.addSublayer(self.playerLayer!)
self.photoScrollView.addSubview(self.videoView!)
self.addObserversForVideo()
}
})
Inside the addObserversForVideo() I set different observers to update a slider that controls the video and also set its min and max values:
guard let currentPlayer = player else {return}
guard let currentItem = currentPlayer.currentItem else {return}
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerFinishedPlayingVideo),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: nil)
let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
_ = playerLayer?.player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { [weak self] (time) in
self?.videoSlider.maximumValue = Float(currentItem.duration.seconds)
self?.videoSlider.minimumValue = 0.0
self?.videoSlider.value = Float(currentItem.currentTime().seconds)
self?.videoElapsedTimeLabel.text = self?.getStringFromCMTime(time: currentItem.currentTime())
let remainingTime = currentItem.duration - currentItem.currentTime()
self?.videoRemainingTimeLabel.text = self?.getStringFromCMTime(time: remainingTime)
})
Now, the problem is that I sometimes get this error which crashes my app:
'NSInternalInconsistencyException', reason: 'Attempting to set a
slider's minimumValue (0.000000) to be larger than the maximumValue
(nan)'
I don't understand why this happens as I am checking the currentItem to be non-nil with a guard statement at the beginning, and also I am setting minimum value after the maximum value. I would appreciate it if someone can help me out.
Thanks to #TonyNguyen I could fix the problem in one line:
guard currentItem.status.rawValue == AVPlayerItem.Status.readyToPlay.rawValue else {return}
You need to guard against two additional things:
currentPlayer.currentItem.status == .readyToPlay
currentPlayer.currentItem.duration >= CMTime.zero
In my periodicTimeObserver I change the (time) to seconds then check if the value is NaN or Infinite before updating anything.
_ = player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { [weak self] (time) in
let seconds = CMTimeGetSeconds(time)
guard !(seconds.isNaN || seconds.isInfinite) else {
return
}
// the rest of your code
})
I am trying to play videos on avplayer uing the following code...but get nothing as result as it shows duration nan.
func setUpPlayer(fileURL:URL){
let playerItem:AVPlayerItem = AVPlayerItem(url: fileURL)
player = AVPlayer(playerItem: playerItem)
let playerLayer=AVPlayerLayer(player: player!)
playerLayer.frame=CGRect(x:self.videoContainer.frame.origin.x, y:self.videoContainer.frame.origin.y+20, width:self.videoContainer.frame.size.width, height:self.videoContainer.frame.size.height-40)
player?.addObserver(
self, forKeyPath:"currentItem", options:.initial, context:nil)
self.view.layer.addSublayer(playerLayer)
rangeSlider.setVideoURL(videoURL:fileURL)
rangeSlider.delegate = self
self.endTime = CMTimeGetSeconds((player?.currentItem?.duration)!)
let timeInterval: CMTime = CMTimeMakeWithSeconds(0.01, 100)
// let asset:AVURLAsset = AVURLAsset.init(url:videoURL)
// let videoDuration:CMTime = asset.duration;
//
//
// let timeInterval: CMTime = CMTimeMakeWithSeconds(videoDuration,100)
//CMTimeGetSeconds(videoDuration)
timeObserver = player?.addPeriodicTimeObserver(forInterval: timeInterval,
queue: DispatchQueue.main) { (elapsedTime: CMTime) -> Void in
self.observeTime(elapsedTime: elapsedTime)
} as AnyObject!
}
I am doing this first time.Kindly give some solution to resolve this problem.Thanks in advance!
you missed to write "player.play()" in your code
let timeRange = self.avPlayer.currentItem.loadedTimeRanges[0].CMTimeRangeValue
let duration = CMTimeGetSeconds(timeRange.duration)
try this it will definiyely helpful
I have a uiView where I am playing a video with custom controls.
When I pause the video , it goes to the beginning of the video and pauses instead of pausing at that particular frame
Following is my Code
func playButtonTapped(cell: ViewTableCell) {
guard let indexPath = self.tableView.indexPath(for: cell) else {
return
}
let url = Bundle.main.path(forResource:ArtistFeeds.sharedInstance.videoFeeds[indexPath.row].videoUrl, ofType:"mp4")
let path = NSURL.fileURL(withPath: url!)
currentCell = tableView.cellForRow(at: indexPath) as! ViewTableCell
currentCell.videoPlayerItem = AVPlayerItem.init(url: path)
let playImage = UIImage(named: "image_video_play") as UIImage?
let pauseImage = UIImage(named: "image_video_pause") as UIImage?
if currentCell.avPlayer?.rate == 1.0 {
currentCell.stopPlayback()
currentCell.playButton.isHidden = false
currentCell.playButton.setImage(playImage, for: .normal)
} else {
currentCell.startPlayback()
currentCell.playButton.isHidden = true
currentCell.playButton.setImage(pauseImage, for: .normal)
}
}
I am setting up the videoPlayer in the ViewTableCell class as follows
var avPlayer: AVPlayer?
var avPlayerLayer: AVPlayerLayer?
var videoPlayerItem: AVPlayerItem? = nil {
didSet {
avPlayer?.replaceCurrentItem(with: self.videoPlayerItem)
}
}
#IBAction func playButtonAction(_ sender: UIButton) {
self.delegate?.playButtonTapped(cell: self)
}
func setupMoviePlayer(){
self.avPlayer = AVPlayer.init(playerItem: self.videoPlayerItem)
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer?.videoGravity = AVLayerVideoGravityResizeAspect
avPlayer?.volume = 3
avPlayer?.actionAtItemEnd = .none
avPlayerLayer?.frame = videoView.bounds
self.backgroundColor = .clear
videoView.layer.insertSublayer(avPlayerLayer!, at: 0)
let interval = CMTime(value: 1, timescale: 2)
avPlayer?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using :{ (progressTime) in
let seconds = CMTimeGetSeconds(progressTime)
if let duration = self.avPlayer?.currentItem?.duration{
let durationSeconds = CMTimeGetSeconds(duration)
self.videoSlider.value = Float(seconds/durationSeconds)
}
})
}
func stopPlayback(){
self.avPlayer?.pause()
}
func startPlayback(){
self.avPlayer?.play()
}
The problem is on every on click of the play button "currentCell.videoPlayerItem = AVPlayerItem.init(url: path)" is being called which is replacing the videoPlayerItem.
I want to pause the video at that particular frame and not go back to the beginning of the video.
How can I overcome this problem. Any help will be appreciated.
You replace the videoPlayerItem every times you press the button. That's why it resets:
avPlayer?.replaceCurrentItem(with: self.videoPlayerItem)
In my application I have used AVPlayer to play videos, If we play a video and go bock to previous page video is playing in backgraound well but if we play the same video again we have to show continuation of the video and it should not begin from start , Here I can able to set slider position according to the video duration but the screen not showing video content sound is coming.
For first time to play any video i called below method
func setup() {
UserDefaults.standard.set(videoString as NSString, forKey: CurrentURL)
UserDefaults.standard.synchronize()
let targetTime:CMTime = CMTimeMake(0, 1)
mediaPlayer.seek(to: targetTime)
mediaPlayer.pause()
print("Video URL \(videoString)")
let url = NSURL(string: videoString)
let playerItem = AVPlayerItem(url: url! as URL)
mediaPlayer=AVPlayer(playerItem: playerItem)
let playerLayer=AVPlayerLayer(player: mediaPlayer)
playerLayer.frame = self.view.bounds
playerLayer.backgroundColor = UIColor.black.cgColor
//playerLayer.videoGravity = AVLayerVideoGravityResize
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
self.view.layer.addSublayer(playerLayer)
AppDelegate.shared().showLoading()
videoView.isHidden = false
mediaPlayer.play()
let duration : CMTime = mediaPlayer.currentItem!.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)
videoSlider.maximumValue = Float(seconds)
videoPlayButton.setImage(UIImage(named: "Pause"), for: .normal)
videoPlaying = true
//Adding Default Periodic Observer On Player
mediaPlayer.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if mediaPlayer.currentItem?.status == .readyToPlay {
AppDelegate.shared().removeLoading()
self.videoSlider.value = Float(CMTimeGetSeconds(mediaPlayer.currentTime()))
let currentTime : Int = Int(CMTimeGetSeconds(mediaPlayer.currentTime()))
let minutes = currentTime/60
let seconds = currentTime - minutes * 60
self.durationLabel.text = NSString(format: "%02d:%02d", minutes,seconds) as String
}
}
}
If user plays same video second time i'm checking current url which is stored in userdefaults if it is playing same video i did like following
let runningSrtring = UserDefaults.standard.object(forKey: CurrentURL) as! NSString
if runningSrtring.isEqual(to: videoString) {
//self.view.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
videoPlayButton.setImage(UIImage(named: "Pause"), for: .normal)
let duration : CMTime = mediaPlayer.currentItem!.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)
self.videoSlider.value = Float(CMTimeGetSeconds((mediaPlayer.currentTime())))
videoSlider.minimumValue = 0
videoSlider.maximumValue = Float(seconds)
let playerLayer = AVPlayerLayer(player: mediaPlayer)
playerLayer.frame = self.view.frame
playerLayer.backgroundColor = UIColor.black.cgColor
//playerLayer.videoGravity = AVLayerVideoGravityResize
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
self.view.layer.addSublayer(playerLayer)
mediaPlayer.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if mediaPlayer.currentItem?.status == .readyToPlay {
self.videoSlider.value = Float(CMTimeGetSeconds((mediaPlayer.currentTime())))
let currentTime : Int = Int(CMTimeGetSeconds(mediaPlayer.currentTime()))
let minutes = currentTime/60
let seconds = currentTime - minutes * 60
self.durationLabel.text = NSString(format: "%02d:%02d", minutes,seconds) as String
}
}
}
But the lauer is not showing video content if i play same video again , here if the user play same video which is already running it should continue from current duration instead of restarting