How to show paused video instead of black screen when starting AVPlayer - ios

I am able to successfully create a player but was annoyed with the initial black screen. I decided to overlay a UIImageView and hide it once the player started. This worked, but I didn't want the hassle of creating and maintaining images for all my videos.
I was able to achieve the exact results I wanted by playing and immediately pausing the player after instantiating it. The only issue was that sometimes the state of the player was getting recorded incorrectly, so when I went to start the player again, the status was listed as already "playing" even though the player was paused.
I starting looking into using AVPlayerItem seekToTime but haven't found any feasible solutions. Is there a "non hacky" way of achieving this?

If you're using an AVPlayerViewController, this is a perfect use of the player controller's contentOverlayView property. This is a UIView between the player layer and the controls exposed exactly for this purpose:
First, create the screenshot:
let asset = AVAsset(URL: URL(string: "")!) // link to some video
let imageGenerator = AVAssetImageGenerator(asset: asset)
let screenshotTime = CMTime(seconds: 1, preferredTimescale: 1)
if let imageRef = try? imageGenerator.copyCGImageAtTime(screenshotTime, actualTime: nil) {
let image = UIImage(CGImage: imageRef)
// see part 2 below
}
Now, add the image as a subview of the contentOverlayView in the player controller:
// in the same try block
let imageView = UIImageView(image: image)
let playerVC = AVPlayerViewController()
let playerItem = AVPlayerItem(asset: asset)
playerVC.player = AVPlayer(playerItem: playerItem)
self.presentViewController(playerVC, animated: true) {
playerVC.contentOverlayView?.addSubview(imageView)
// adjust the frame of your imageView to fit on the player controller's contentOverlayView
}
Then, remove the imageView subview when the player starts playing the asset, or when buffering completes.

The AVPlayerLayer associated with your player has a readyForDisplay property.
You can try to make your host view an observer for this value, and do a refresh as soon as it is set to true.
It is set to true when the first frame is ready to be rendered, so before the player has enough data to play and set its status to readyToPlay.

Related

Updating a timer label whilst an animation is playing?

I run the code
DispatchQueue.main.async { [weak self] in
self?.sliderView.elapsedLabel.text = self?.player.currentTime.secondsToString()
}
Which every second of an observer firing that watches the audio file progression, will update a label of remaning seconds. It forces main thread because its UI related...
player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
Meanwhile I have an animation loop playing in the background using AVKit
I am finding that, when the label updates, it stops the animation, as both are real time main thread updates?
How is this handled to allow 2 processes to update the UI at the same time without clashing with each other?
Updated code for the animating video:
let playerItem = AVPlayerItem(url: file)
videoPlayer = AVQueuePlayer(items: [playerItem])
playerLooper = AVPlayerLooper(player: videoPlayer!, templateItem: playerItem)
videoPlayerLayer = AVPlayerLayer(player: videoPlayer)
videoPlayerLayer!.frame = inView.bounds
videoPlayerLayer!.videoGravity = AVLayerVideoGravity.resizeAspectFill
then I call the standard
videoPlayer?.play()

Stop AVPlayer video when a new UITableViewCell is selected

I want to show a video accordingly to the selected cell in my tableview. I want to show the video in a view in my vc.
The following code is showing the correct videos when the cells are tapped but whenever i select a new cell the audio from the old video is still there. How can i remove it ?
Setting up the view in my vc:
func setupView(for url: URL) {
let player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.videoView.bounds
self.videoView.layer.addSublayer(playerLayer)
player.play()
}
didSelectRow code:
let selectedExercise = exercises[indexPath.row]
setupView(for: URL(string: selectedExercise.videoUrl)!)
My guess is that i should pause/stop the previous video before showing the new one. But i don't know how to.
Thanks a lot!
It's happend because you create new instance of AVPlayer every tap.
To solve your problem, you need to move
let player = AVPlayer(url: url)
outside of setupView() and call it only once. You could move it in init() for example.
For updating asset (video url) in setupView() you could use
player.replaceCurrentItem(with: AVPlayerItem(url: url))
.

iOS: AVPlayer plays multiple instances of audio when using controls on lock screen

I have set up a function that creates an AVPlayerViewController instance in order to play an audio track (stored in our server).
func playAudio(_ url: URL) {
let avAssest = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: avAssest)
audioPlayer = AVPlayer(playerItem: playerItem)
let playerViewController = AVPlayerViewController()
playerViewController.player = audioPlayer
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
self.present(playerViewController, animated: true, completion: {
self.audioPlayer.play()
})
}
The player works fine while in the foreground. If I go to the lock screen, the audio continues to play. If I press the Pause button in the lock screen, the audio pauses. The problem is, if I then click play on the lock screen, the audio I was listening to resumes, but a second instance of the audio starts playing from the start, so now i have 2 instances of the audio playing simultaneously. If I pause again and press play again, yet another instance of the audio starts playing.
How can I fix this so the lock screen buttons only affect the original audio?
I'm working on Xcode 11, with Swift 4.2.
The problem cannot be reproduced based on the code you showed. Therefore it sounds like you have an extra reference somewhere to self.audioPlayer, and the problem is caused by code you did not reveal to us.
To test that theory, let's start by not making it a global. There's no need for that within the scope of the question.
Modify your code to look like this (note the changes to make this a local):
let avAssest = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: avAssest)
let audioPlayer = AVPlayer(playerItem: playerItem) // *
let playerViewController = AVPlayerViewController()
playerViewController.player = audioPlayer
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
self.present(playerViewController, animated: true, completion: {
audioPlayer.play() // *
})
Now delete the audioPlayer property and, if your code fails to compile, comment out everything that refers to it until the code does compile. Test the app; all is well. Now go back and figure out where in the code you did not show us the problem is coming from.

Set opacity of AVPlayerLayer

I have seen quite a few posts of people wanting to play videos with transparent backgrounds. That is not what I am trying to accomplish here.
I am trying to set the opacity of the whole layer. Below is how I am setting the opacity of an image layer. I have tried the same approach to set opacity for the video layer but nothing happened when I attempted to fade
#IBAction func imageFadeAction(_ sender: Any) {
let rounded = Int(round(imageFader.value))
imageLabel.text = "\(rounded.description)%"
let fade = imageFader.value / 100
SecondScreen.main?.backgroundImage.alpha = CGFloat(fade)
SecondScreen.main?.viewDidLoad()
}
This is how I am playing the video and generating the layer
func playVideo() {
let videoURL = SecondScreen.videoPath
self.player = AVPlayer(url: videoURL!)
self.avpController = AVPlayerLayer(player: player)
avpController.frame = self.view.bounds
self.view.layer.addSublayer(avpController)
player.play()
}
Can anyone tell me if its possible or point me in the right direction
To answer my own question, Use a container view. play your video in a separate ViewController and embed it in the container view. THEN you can set the opacity of the container view.
I don't know if this is the correct way to do it, but it works for me and what I needed. If someone has a better answer please feel free to share
You can set its opacity to 0. something like this:
guard let path = Bundle.main.path(forResource: "SomeVideo", ofType: ".Someformat") else {
return nil
}
let player = AVPlayer(url: URL(fileURLWithPath: path))
let playerLayer = AVPlayerLayer(player: player)
playerLayer.opacity = 0
.
.
.

Why does Swift's AVPlayer loads the playerItem for twice on one play?

I'm using AVFoundation's AVPlayer for streaming external mp3 files. I have a counter on the back-end that counts how many times a file loaded. The only client for this service is only me and whenever I trigger to play the AVPlayer, the counter increases two which means AVPlayer makes the request twice. Is there a reason for this, or how can I prevent that from happening? Here is my code:
#IBAction func listen(sender: UIButton) {
let urlstring = "http://api.server.com/endpoint-to-mp3"
let url = NSURL(string: urlstring)
let playerItem = AVPlayerItem(URL: url!)
let player = AVPlayer(playerItem: playerItem)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = CGRectMake(0, 0, 300, 50)
self.view.layer.addSublayer(playerLayer)
player.volume = 1.0
player.play()
}
AVPlayer is making a network request to the URL whenever you initialize the player with AVPlayerItem. This call only fetches the file information and file size. (At this point I am able to observe 2 requests sometime, which could increase your count to 3)
Later when you are attaching the player to any view, another call is happening to fetch the complete file. (You can use Charles to observe your network traffic, fyi)
This behaviour is same when you init the player with init(url:) So I don't see any way that could prevent this from happening at the moment.

Resources