AVPlayer layer showing blank screen when application running or idle for 15 minutes - ios

I would like to create a custom video player using AVPlayer() and AVPlayerLayer() classes.
My code working fine for application fresh start but goes wrong and showing blank screen when the application running or idle for more than 15 minutes.
When the occurrences this issue all classes of my application having AVPlayerLayer showing blank screen. I don't know why this happens.
AVPlayer() and AVPlayerlayer() instances are initialized as below.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
dispatch_async(dispatch_get_main_queue(), {
let playerItem = AVPlayerItem(URL: self.itemUrl)
self.avPlayer = AVPlayer(playerItem: playerItem)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(VideoPreviewView.restartVideoFromBeginning),
name: AVPlayerItemDidPlayToEndTimeNotification,
object: self.avPlayer.currentItem)
self.avPlayerLayer = AVPlayerLayer(player: self.avPlayer)
self.view.layer.addSublayer(self.avPlayerLayer)
self.avPlayerLayer.frame = self.view.bounds
self.avPlayer.pause()
})
}
}
Play function
func playVideo(sender: AnyObject) {
avPlayer.play()
if (avPlayer.rate != 0 && avPlayer.error == nil) {
print("playing")
}
slider.hidden=false
myTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(VideoPreviewView.updateSlider), userInfo: nil, repeats: true)
slider.addTarget(self, action: #selector(VideoPreviewView.sliderValueDidChange(_: )), forControlEvents: UIControlEvents.ValueChanged)
slider.minimumValue = 0.0
slider.continuous = true
pauseButton.hidden=false
playButton.hidden=true
closeViewButton.hidden = false
}
Restart video
func restartVideoFromBeginning() {
let seconds: Int64 = 0
let preferredTimeScale: Int32 = 1
let seekTime: CMTime = CMTimeMake(seconds, preferredTimeScale)
avPlayer?.seekToTime(seekTime)
avPlayer?.pause()
pauseButton.hidden=true
playButton.hidden = false
closeViewButton.hidden=false
}
func updateSlider() {
if (avPlayer.rate != 0 && avPlayer.error == nil) {
print("playing")
}
else{
print("Not playing.")
}
currentTime = Float(CMTimeGetSeconds(avPlayer.currentTime()))
duration = avPlayer.currentItem!.asset.duration
totalDuration = Float(CMTimeGetSeconds(duration))
slider.value = currentTime // Setting slider value as current time
slider.maximumValue = totalDuration // Setting maximum value as total duration of the video
}
func sliderValueDidChange(sender: UISlider!) {
timeInSecond=slider.value
newtime = CMTimeMakeWithSeconds(Double(timeInSecond), 1)// Setting new time using slider value
avPlayer.seekToTime(newtime)
}
#IBAction func closeViewAction(sender: AnyObject) {
pauseButton.hidden=true
self.avPlayer.pause()
self.avPlayerLayer.removeFromSuperlayer()
self.dismissViewControllerAnimated(true, completion: nil)
}

Related

Video player: Play,Pause, forward and backward methods IOS Swift 3

How to get this buttons event/delegate(Play,Pause, forward and backward) with time difference.
Here i have put the code below. Please suggest we can use other player for this events
player = AVPlayer(url:URL(fileURLWithPath: objpdfURL));
NotificationCenter.default.addObserver(self, selector: #selector(VedioPlayVC.didfinishplaying(note:)),name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
NotificationCenter.default.addObserver(self, selector:#selector(VedioPlayVC.Stop(note:)), name: .kAVPlayerViewControllerDismissingNotification, object: nil);
NotificationCenter.default.addObserver(self, selector: #selector(VedioPlayVC.Jump(note:)),name:NSNotification.Name.AVPlayerItemTimeJumped, object: nil)
playerController.player = player
playerController.allowsPictureInPicturePlayback = true
playerController.delegate = self
playerController.player?.play()
self.present(playerController,animated:true,completion:nil)
Hope this will help
#IBAction func btnPlayAction(_ sender: UIButton) {
if isVideoPlaying{
player.pause()
sender.setTitle("Play", for: .normal)
}else{
player.play()
sender.setTitle("Pause", for: .normal)
}
isVideoPlaying = !isVideoPlaying
}
#IBAction func btnForwardAction(_ sender: UIButton) {
guard let duration = player.currentItem?.duration else { return }
let currentTime = CMTimeGetSeconds(player.currentTime())
let newTime = currentTime + 5.0
if newTime < (CMTimeGetSeconds(duration) - 5.0){
let time: CMTime = CMTimeMake(value: Int64(newTime*1000), timescale: 1000)
player.seek(to: time)
}
}
#IBAction func btnBackwardAction(_ sender: UIButton) {
let currentTime = CMTimeGetSeconds(player.currentTime())
var newTime = currentTime - 5.0
if newTime < 0{
newTime = 0
}
let time: CMTime = CMTimeMake(value: Int64(newTime*1000), timescale: 1000)
player.seek(to: time)
}

Swift timer won't work after invalidating and reinitiating

I'm using Timer() for indicating current audio position in audio player
func startTimer() {
print("PlayerController: startTimer()")
if itemPlayTimer != nil {
return
}
itemPlayTimer = Timer.scheduledTimer(timeInterval: 0.001,
target: self,
selector: #selector(updateItemPlayerTimer),
userInfo: nil,
repeats: true)
}
#objc func updateItemPlayerTimer() {
guard let currentTime = player?.currentTime else {
return
}
updateTimeDescription?(currentTime)
}
when user pause player app invalidating timer
func stopTimer() {
itemPlayTimer?.invalidate()
itemPlayTimer = nil
}
But after calling startTimer() again selector won't call
The reason was that the timer starts executing in other thread, not in main thread.
change your functions in this way:
func startTimer() {
print("PlayerController: startTimer()")
if itemPlayTimer == nil {
itemPlayTimer = Timer.scheduledTimer(timeInterval: 0.001,
target: self,
selector: #selector(updateItemPlayerTimer),
userInfo: nil,
repeats: true)
}
}
#objc func updateItemPlayerTimer() {
guard let currentTime = player?.currentTime else {
return
}
updateTimeDescription?(currentTime)
}
func stopTimer() {
if itemPlayTimer != nil {
itemPlayTimer?.invalidate()
itemPlayTimer = nil
}
}
Use the following code. I have used An AVPlayer with a sample Video to demonstrate pausing/playing with Timer. Logic is same for audio player.
Just replace AVPlayer with your audioPlayer.
Key logic here is to properly manage the state of Playing/not playing and checking timer for nil etc.
As indicated in this Answer
startTimer() starts the timer only if it's nil and stopTimer() stops
it only if it's not nil.
You have only to take care of stopping the timer before
creating/starting a new one.
I have implemented this in a sample project and is working 100%.
Carefully See the function pauseTapped(_ sender: UIButton)
See sample gif At end of Code
import UIKit
import AVKit
class TimerVC: UIViewController {
///A container view for displaying the AVPlayer.
#IBOutlet weak var playerView: UIView!
/// A button to play and pause the video
#IBOutlet weak var btnPause: UIButton!
///to maintain the status of AVPlayer playing or not
var flagPlaying = true
///An AVPlayer for displaying and playing the video
var player: AVPlayer?
///to show the current time to user
#IBOutlet weak var lblCurrentTime: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
//add an AVPlayer with sample URL link
addVideoPlayer( playerView: playerView)
}
///Timer
var itemPlayTimer: Timer?
#objc func startTimer() {
if itemPlayTimer != nil {
return
}
itemPlayTimer = Timer.scheduledTimer(timeInterval: 0.001,
target: self,
selector: #selector(updateItemPlayerTimer),
userInfo: nil,
repeats: true)
}
///update time label
#objc func updateItemPlayerTimer() {
guard let currentTime = player?.currentTime else {
return
}
updateTimeDescription(currentTime())
}
func updateTimeDescription( _ currentTime :CMTime ) {
self.lblCurrentTime.text = "\(currentTime.seconds)"
}
///To Pause and play the video
#IBAction func pauseTapped(_ sender: UIButton) {
if flagPlaying {
//pause
if itemPlayTimer != nil {
player?.pause()
itemPlayTimer?.invalidate()
itemPlayTimer = nil
flagPlaying = false
DispatchQueue.main.async {
self.btnPause.setTitle("Play", for: .normal)
}
}
}else {
//not playing
if itemPlayTimer == nil {
player?.play()
startTimer()
flagPlaying = true
DispatchQueue.main.async {
self.btnPause.setTitle("Pause", for: .normal)
}
}
}
}
private func addVideoPlayer(playerView: UIView) {
//let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer.init(url: URL.init(string: "http://techslides.com/demos/sample-videos/small.mp4")!)
let layer: AVPlayerLayer = AVPlayerLayer(player: player)
layer.backgroundColor = UIColor.white.cgColor
layer.frame = CGRect(x: 0, y: 0, width: playerView.frame.width, height: playerView.frame.height)
layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerView.layer.sublayers?.forEach({$0.removeFromSuperlayer()})
playerView.layer.addSublayer(layer)
flagPlaying = true
player?.play()
startTimer()
}
}
Working Example
Let me know if you need any help

Add tone to Countdown Timer in XCODE 8 Swift 3

I am new to coding. I am trying to learn iOS development and I have created a simple countdown timer, that uses a slider to select the amount of time in seconds. I would like to add a tone to the final 9 secs of the countdown. I know how to add audio files, but I have "NO IDEA" how to add a countdown tone to the last 9 seconds of the timer.
#IBAction func startBtnTapped(_ sender: Any) {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.counter), userInfo: nil, repeats: true)
SoundPlayer4.play()
//Hide Start and Slider Buttons
startBtn.isHidden = true
sliderCtrl.isHidden = true
stopBtn.isHidden = false
}
#IBAction func stopBtnTapped(_ sender: Any) {
timer.invalidate()
seconds = 180
sliderCtrl.setValue(180, animated: true)
timerLbl.text = "180"
SoundPlayer3.play()
//Show Start and Slider Buttons
sliderCtrl.isHidden = false
startBtn.isHidden = false
stopBtn.isHidden = true
}
#IBAction func sliderCrtlUsed(_ sender: UISlider) {
seconds = Int(sender.value)
timerLbl.text = String(seconds)
}
func counter(){
seconds -= 1
timerLbl.text = String(seconds)
if (seconds == 0){
timer.invalidate()
//Show Start and Slider Buttons
startBtn.isHidden = false
sliderCtrl.isHidden = false
stopBtn.isHidden = true
//Play Audio
SoundPlayer1.play()
}
}
So can anyone help me add a countdown tone to my project?
Thank you
Put this code in counter function:
if seconds < 10 {
var AudioURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Dog bark", ofType: "mp3")!)
var AudioPlayer = AVAudioPlayer()
do {
AudioPlayer = try AVAudioPlayer(contentsOfURL: AudioURL)
} catch _ as NSError {
fatalError()
}
AudioPlayer.play()
}
Then put the file here:
And that's it!
You can read more on this question here.

Jumpy UISlider when scrubbing - Using UISlider with AVPlayer

I am using AvPlayer and am trying to set up a slider to allow scrubbing of audio files. Im having a problem with the slider jumping all over the place when its selected. It then goes back to the origin position for a second before going back to the location it was dragged to.
You cant see my cursor on the Gif, but the smooth elongated drags are me moving the knob, then the quick whips are the slider misbehaving.
Ive spent hours googling and combing through Stack Overflow and cant figure out what I'm doing wrong here, a lot of similar questions are quite old and in ObjC.
This is the section of code i think is responsible for the problem, it does handle the event of the slider being moved: Ive tried it without the if statement also and didn't see a different result.
#IBAction func horizontalSliderActioned(_ sender: Any) {
horizontalSlider.isContinuous = true
if self.horizontalSlider.isTouchInside {
audioPlayer?.pause()
let seconds : Int64 = Int64(horizontalSlider.value)
let preferredTimeScale : Int32 = 1
let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
audioPlayerItem?.seek(to: seekTime)
audioPlayer?.play()
} else {
let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
let seconds : Float64 = CMTimeGetSeconds(duration)
self.horizontalSlider.value = Float(seconds)
}
}
I will include my entire class below for reference.
import UIKit
import Parse
import AVFoundation
import AVKit
class PlayerViewController: UIViewController, AVAudioPlayerDelegate {
#IBOutlet var horizontalSlider: UISlider!
var selectedAudio: String!
var audioPlayer: AVPlayer?
var audioPlayerItem: AVPlayerItem?
var timer: Timer?
func getAudio() {
let query = PFQuery(className: "Part")
query.whereKey("objectId", equalTo: selectedAudio)
query.getFirstObjectInBackground { (object, error) in
if error != nil || object == nil {
print("The getFirstObject request failed.")
} else {
print("There is an object now get the Audio. ")
let audioFileURL = (object?.object(forKey: "partAudio") as! PFFile).url
self.audioPlayerItem = AVPlayerItem(url: NSURL(string: audioFileURL!) as! URL)
self.audioPlayer = AVPlayer(playerItem: self.audioPlayerItem)
let playerLayer = AVPlayerLayer(player: self.audioPlayer)
playerLayer.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
self.view.layer.addSublayer(playerLayer)
let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
let seconds : Float64 = CMTimeGetSeconds(duration)
let maxTime : Float = Float(seconds)
self.horizontalSlider.maximumValue = maxTime
self.audioPlayer?.play()
self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PlayerViewController.audioSliderUpdate), userInfo: nil, repeats: true)
}
}
}
#IBOutlet var playerButton: UIButton!
func playerButtonTapped() {
if audioPlayer?.rate == 0 {
audioPlayer?.play()
self.playerButton.setImage(UIImage(named: "play"), for: UIControlState.normal)
} else {
audioPlayer?.pause()
self.playerButton.setImage(UIImage(named: "pause"), for: UIControlState.normal)
}
}
override func viewDidLoad() {
super.viewDidLoad()
horizontalSlider.minimumValue = 0
horizontalSlider.value = 0
self.playerButton.addTarget(self, action: #selector(PlayerViewController.playerButtonTapped), for: UIControlEvents.touchUpInside)
getAudio()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(PlayerViewController.finishedPlaying), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.audioPlayerItem)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillAppear(animated)
// remove the timer
self.timer?.invalidate()
// remove the observer when leaving page
NotificationCenter.default.removeObserver(audioPlayer?.currentItem! as Any)
}
func finishedPlaying() {
// need option to play next track
self.playerButton.setImage(UIImage(named: "play"), for: UIControlState.normal)
let seconds : Int64 = 0
let preferredTimeScale : Int32 = 1
let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
audioPlayerItem!.seek(to: seekTime)
}
#IBAction func horizontalSliderActioned(_ sender: Any) {
horizontalSlider.isContinuous = true
if self.horizontalSlider.isTouchInside {
audioPlayer?.pause()
let seconds : Int64 = Int64(horizontalSlider.value)
let preferredTimeScale : Int32 = 1
let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
audioPlayerItem?.seek(to: seekTime)
audioPlayer?.play()
} else {
let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
let seconds : Float64 = CMTimeGetSeconds(duration)
self.horizontalSlider.value = Float(seconds)
}
}
func audioSliderUpdate() {
let currentTime : CMTime = (self.audioPlayerItem?.currentTime())!
let seconds : Float64 = CMTimeGetSeconds(currentTime)
let time : Float = Float(seconds)
self.horizontalSlider.value = time
}
}
Swift 5, Xcode 11
I faced the same issue, it was apparently periodicTimeObserver which was causing to return incorrect time which caused lag or jump in the slider. I solved it by removing periodic time observer when the slider was changing and adding it back when seeking completion handler was called.
#objc func sliderValueChanged(_ playbackSlider: UISlider, event: UIEvent){
let seconds : Float = Float(playbackSlider.value)
let targetTime:CMTime = CMTimeMake(value: Int64(seconds), timescale: 1)
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
// handle drag began
//Remove observer when dragging is in progress
self.removePeriodicTimeObserver()
break
case .moved:
// handle drag moved
break
case .ended:
// handle drag ended
//Add Observer back when seeking got completed
player.seek(to: targetTime, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] (value) in
self?.addTimeObserver()
}
break
default:
break
}
}
}
you need to remove observers and invalidate timers as soon as user selects the thumb on slider and add them back again when dragging is done
to do add targets like this where you load your player:
mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderBeganTracking(_:)),
forControlEvents:.TouchDown)
mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderEndedTracking(_:)),
forControlEvents: .TouchUpInside)
mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderEndedTracking(_:)),
forControlEvents: .TouchUpOutside )
and remove observers and invalidate timers in mySliderBeganTracking then add observers in mySliderEndedTracking
for better control on what happens in your player write 2 functions : addObservers and removeObservers and call them when needed
Make sure to do the following:
isContinuous for the slider is NOT set to false.
Pause the player before seeking.
Seek to the position and use the completion handler to resume playing.
Example code:
#objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
let roundedValue = sender.value.rounded()
guard let touchEvent = event.allTouches?.first else { return }
switch touchEvent.phase {
case .began:
PlayerManager.shared.pause()
case .moved:
print("Slider moved")
case .ended:
PlayerManager.shared.seek(to: roundedValue, playAfterSeeking: true)
default: ()
}
}
And here is the function for seeking:
func seek(to: Float, playAfterSeeking: Bool) {
player?.seek(to: CMTime(value: CMTimeValue(to), timescale: 1), completionHandler: { [weak self] (status) in
if playAfterSeeking {
self?.play()
}
})
}
Try using the time slider value like below:
#IBAction func timeSliderDidChange(_ sender: UISlider) {
AVPlayerManager.sharedInstance.currentTime = Double(sender.value)
}
var currentTime: Double {
get {
return CMTimeGetSeconds(player.currentTime())
}
set {
if self.player.status == .readyToPlay {
let newTime = CMTimeMakeWithSeconds(newValue, 1)
player.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { ( _ ) in
self.updatePlayerInfo()
}
}
}
}
and pass the value of slider when user release the slider, also don't update the slider value of current playing while user interaction happening on the slider
This is a temporary solution for me, I observed that the rebound is only once, so I set an int value isSeekInProgress:
When sliderDidFinish, isSeekInProgress = 0
In reply to avplayer time change:
if (self.isSeekInProgress > 1) {
float sliderValue = 1.f / (self.slider.maximumValue - self.slider.minimumValue) * progress;
// if (sliderValue > self.slider.value ) {
self.slider.value = sliderValue;
}else {
self.isSeekInProgress += 1;
}

Audio playback progress as UISlider in Swift

I've seen some posts about accomplishing this in Objective-C but I've been unable to do the same via Swift.
Specifically, I can't figure out how to implement addPeriodicTimeObserverForInterval in the below.
var player : AVAudioPlayer! = nil
#IBAction func playAudio(sender: AnyObject) {
playButton.selected = !(playButton.selected)
if playButton.selected {
let fileURL = NSURL(string: toPass)
player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
player.delegate = self
player.play()
startTime.text = "\(player.currentTime)"
endTime.text = NSString(format: "%.1f", player.duration)
} else {
player.stop()
}
Any assistance would be appreciated.
Thanks to the suggestion of Dare above, here's how I accomplished this:
var updater : CADisplayLink! = nil
#IBAction func playAudio(sender: AnyObject) {
playButton.selected = !(playButton.selected)
if playButton.selected {
updater = CADisplayLink(target: self, selector: Selector("trackAudio"))
updater.frameInterval = 1
updater.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
let fileURL = NSURL(string: toPass)
player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
player.delegate = self
player.play()
startTime.text = "\(player.currentTime)"
theProgressBar.minimumValue = 0
theProgressBar.maximumValue = 100 // Percentage
} else {
player.stop()
}
}
func trackAudio() {
var normalizedTime = Float(player.currentTime * 100.0 / player.duration)
theProgressBar.value = normalizedTime
}
#IBAction func cancelClicked(sender: AnyObject) {
player.stop()
updater.invalidate()
dismissViewControllerAnimated(true, completion: nil)
}
Just to elaborate on my previous comment, this is how I implemented it and it seems to work pretty well. Any Swift corrections are more than welcome, I'm still an Obj-C guy for now.
#IBAction func playAudio(sender: AnyObject) {
var playing = false
if let currentPlayer = player {
playing = player.playing;
}else{
return;
}
if !playing {
let filePath = NSBundle.mainBundle().pathForResource("3e6129f2-8d6d-4cf4-a5ec-1b51b6c8e18b", ofType: "wav")
if let path = filePath{
let fileURL = NSURL(string: path)
player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
player.delegate = self
player.play()
displayLink = CADisplayLink(target: self, selector: ("updateSliderProgress"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode!)
}
} else {
player.stop()
displayLink.invalidate()
}
}
func updateSliderProgress(){
var progress = player.currentTime / player.duration
timeSlider.setValue(Float(progress), animated: false)
}
*I set time slider's range between 0 and 1 on a storyboard
Specifically for Swift I was able to handle it like this:
I set the maximum value of the scrubSlider to the duration of the music file(.mp3) that was loaded in this method
override func viewDidLoad() {
super.viewDidLoad()
do {
try player = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("bach", ofType: "mp3")!))
scrubSlider.maximumValue = Float(player.duration)
} catch {
//Error
}
_ = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(ViewController.updateScrubSlider), userInfo: nil, repeats: true)
}
I player was set to play the music at the time set by the value of the scrubber.
#IBAction func scrub(sender: AnyObject) {
player.currentTime = NSTimeInterval(scrubSlider.value)
}
Syntax has now changed in Swift 4:
updater = CADisplayLink(target: self, selector: #selector(self.trackAudio))
updater.preferredFramesPerSecond = 1
updater.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)
And the function (I have previously set the progressSlider.maxValue to player.duration):
#objc func trackAudio() {
progressSlider.value = Float(player!.currentTime)
}

Resources