so here is my use case - we are loading a video using avplayer and playing it, and user can click on the default fullscreen button to take the video fullscreen. The user may be viewing the video in 2 different conditions, if he is logged in and if he is not logged in.
If the user is logged in (determined by a variables value), he can watch full video, otherwise, playback should stop after playing a defined number of seconds (depending on the video the no. of seconds changes), and a banner comes up over the player asking him to login.
Everything is working fine while the video is being played inline. However, when the video is playing fullscreen, then even if we stop playback using didTapPause(), the fullscreen window does not get dismissed. I have even tried dismissing it using self.playerController.dismiss(animated: true, completion: nil), the fullscreen modal is not dismissed. The code snippet it as follows -
playerController.player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.playerController.player?.currentItem?.status == .readyToPlay {
self.videoCurrentTimeDuration = CMTimeGetSeconds((self.playerController.player?.currentItem!.currentTime())!);
self.videoTimeDuration = CMTimeGetSeconds((self.playerController.player?.currentItem?.duration)!);
if self.moveToTime != nil{
let timeWithSecond = CMTimeMakeWithSeconds(self.videoTimeDuration! * self.moveToTime! / 100, Int32(kCMTimeMaxTimescale))
self.playerController.player?.seek(to: timeWithSecond, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
self.moveToTime = nil
}
guard let videoD = self.videoData else { return }
let timeTToPlay: Double = Double(videoD.freeDuration)
if videoD.isFree {
if videoD.registrationNeeded && !CurrentLoginUser.shared.isLogin{
if self.videoCurrentTimeDuration! > timeTToPlay {
self.didTapPause()
self.playerController.dismiss(animated: true, completion: nil)
self.loginNeedView = UINib.get(withNib: self)
self.loginNeedView?.frame = self.bounds
self.loginNeedView?.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
self.addSubview(self.loginNeedView!)
AppUtility.lockOrientation(UIInterfaceOrientationMask.portrait, andRotateTo: UIInterfaceOrientation.portrait)
}
}
else{
self.loginNeedView?.removeFromSuperview()
AppUtility.lockOrientation(UIInterfaceOrientationMask.all)
}
}
The player controller is added onto the view by calling the setupView function which is as follows -
private func setUpView() {
self.backgroundColor = .black
addVideoPlayerView()
configurateControls()
}
fileprivate func addVideoPlayerView() {
playerController.view.frame = self.bounds
playerController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
playerController.showsPlaybackControls = true
playerController.addObserver(self, forKeyPath: "videoBounds", options: NSKeyValueObservingOptions.new, context: nil)
self.insertSubview(playerController.view, at: 0)
}
I am not sure if this is the proper way to do it, any ideas ?
EDIT:
Next as per anbu.karthik's advice I tried to forcibly remove the fullscreen view by locating the top view controller as :
func currentTopViewController() -> UIViewController {
var topVC: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController
while ((topVC?.presentedViewController) != nil) {
topVC = topVC?.presentedViewController
}
return topVC!
}
and then, used it as follows-
let currentTopVC: UIViewController? = self.currentTopViewController()
if (currentTopVC?.description.contains("AVFullScreenViewController"))! {
print("found it")
currentTopVC?.dismiss(animated: true) { _ in }
}
And it works but crashes the app with the following exception -
Terminating app due to uncaught exception 'UIViewControllerHierarchyInconsistency', reason: 'child view controller:<AVEmbeddedPlaybackControlsViewController: 0x7fdcc2264400> should have parent view controller:<AVPlayerViewController: 0x7fdcc222a800> but actual parent is:<AVFullScreenViewController: 0x7fdcce2f3c50>'
To my knowledge it is not possible to play full screen directly. It seems that AVPlayerViewController comes pretty much as is and does not offer much in the way of customization of UI or behavior. If you wanted to play full screen directly you would need to either present the controller containing your AVPlayerViewController modally in full screen or change the frame yourself to make it full screen. But there is no API to control full screen programmatically on AVPlayerViewController...
Related
I am having a couple of problems with player. I know there are many player examples but almost all of them are designed to play just one url and does not have the functionalities that I want.
First of all this is my existing code for the player:
struct PlayerViewController: UIViewControllerRepresentable {
var video: Video
private let player = AVPlayer()
func makeUIViewController(context: Context) -> AVPlayerViewController {
let playerVC = AVPlayerViewController()
playerVC.player = self.player
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
} catch(let error) {
print(error.localizedDescription)
}
playerVC.player?.play()
return playerVC
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
guard let url = URL(string: video.url) else { return }
resetPlayer(uiViewController)
let item = AVPlayerItem(url: url)
uiViewController.player?.replaceCurrentItem(with: item)
uiViewController.player?.play()
if((uiViewController.view.window) == nil)
{
uiViewController.player?.pause()
}
print("Player::UpdatePlayer: Player updated")
}
func resetPlayer(_ vc: AVPlayerViewController) {
vc.player?.pause()
vc.player?.replaceCurrentItem(with: nil)
print("Player::ResetPlayer: Player reseted.!")
}
}
struct Video: Identifiable, Equatable {
var id: Int
var url: String
}
And I use it as follows. This is just example I replace the str with button action in the actual project.
struct TestTestView: View {
#State var str: String = ""
var body: some View {
PlayerViewController(video: Video(id: 2, url: (str == "") ? chan.getDVR()[0].getHlsStreamURL() : self.str ))
}
}
Now the code above works up to a point. It plays, you can update the url with button click and it plays the new url. But when it enters the fullscreen the video stops, because it updates the whole player although nothing changed. The following lines makes it pause when entering the fullscreen mode:
if((uiViewController.view.window) == nil)
{
uiViewController.player?.pause()
}
However, If I remove those lines when I navigate to another screen it continues to play you keep hearing the sound. I tried to detect the fullscreen to update my code but I was not able to do it. I tried to detect I the player is actually in the screen but it did not work as well. So what is the correct way to implement it. Basically I want to do:
1 - Be able to update the player and play different streams
2 - Starts auto play
3 - Smoothly continue to play when switching in and out of full screen mode
4 - Stop when navigated to another screen.
By the way if it is not in full screen mode I cannot click on play button or progress bar but somehow I can click on the mute or fullscreen button. I need help!!
I am playing videos that are in my app bundle.
They are playing correctly.
However, when I call to dismiss the AVPlayerViewController, it visibly is removed from the view hierarchy but, if I turn off the iOS device and turn it back on again, on the lock screen there is a media control showing that video and a 'play' button.
If you touch play you only get the audio and no video.
My problem is I don't understand why the 'dismiss' is not completely 'killing' the player when I'm done with it.
Here is the presentation code:
let path = Bundle.main.path(forResource: filename, ofType: type)
let url = NSURL(fileURLWithPath: path!)
let player = AVPlayer(url: url as URL)
NotificationCenter.default.addObserver(self,
selector: #selector(VideoLibraryViewController.didFinishPlaying(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: player.currentItem)
self.playerController = AVPlayerViewController()
self.playerController?.player = player
self.playerController?.allowsPictureInPicturePlayback = true
self.playerController?.showsPlaybackControls = YES
self.playerController?.delegate = self
self.playerController?.player?.play()
self.present(self.playerController!, animated: true, completion : nil)
Here is the dismissal code:
// Delegate can implement this method to be notified when Picture in Picture will start.
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
self.playerController?.dismiss(animated: NO, completion: nil )
}
And here's what's remaining in the systemwide media player that is shown on the lock screen / control centre:
iOS 13 SDK ONLY: Here's the solution, but the answer is that despite dismissing the AVPlayerViewController, the AVPlayer object that it's knows about is persistent and that needs to be set to nil.
private func killVideoPlayer()
{
self.playerController?.player?.pause()
self.playerController?.player = nil
self.playerController?.dismiss(animated: YES, completion: { self.playerController = nil })
}
Previous SDK's, this still isn't working.
Neither is setting the AVAudioSession.active to false... ?!?! Still need a pre iOS 13 SDK solution.
We load an MP4 video from a URL into an AVPlayer. The AVPlayer is behind a skeleton image which we hide when the AVPlayer gets to status "Ready To Play".
We expect to see the first frame of the video as soon as we hide the skeleton image. However, that first frame of the video appears after a slight delay. What is the status that indicates that the video in the AVPlayer is loaded and ready?
func prepareVideo(videoUrl: URL?, owner: UIView, autoStart: Bool = false) {
self.player = AVPlayer(url: videoUrl!)
let playerController = AVPlayerViewController()
playerController.player = player
playerController.view.layer.shouldRasterize = true
playerController.view.frame = owner.frame
playerController.view.isOpaque = true
playerController.view.backgroundColor = UIColor.clear
playerController.view.layer.borderColor = UIColor.clear.cgColor
playerController.view.layer.borderWidth = 0
playerController.showsPlaybackControls = false
playerController.updatesNowPlayingInfoCenter = false
owner.addSubview(playerController.view)
owner.sendSubview(toBack: playerController.view)
timerStart() // this starts a timer that checks the AVPlayer status
if autoStart {
playVideo()
}
}
#objc func timerStatusCheck() {
// function called by a Timer and checks the status of AVPlayer
if player!.status == AVPlayerStatus.readyToPlay {
print("ready to play")
timerStop()
if (readyToPlayHandler != nil) {
self.readyToPlayHandler!() // here we hide the skeleton image that shows while video is loading
}
} else if player!.status == AVPlayerStatus.failed {
timerStop()
MessageBox.showError("Video Failed to start")
}
}
When the AVPlayer reports rate = 1, it's playing the video. However, that doesn't mean the video is visible in the AVPlayerViewController. For that, you need the AVPlayerViewController's property "isReadyForDisplay" to be true.
https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-isreadyfordisplay
Note both AVPlayerViewController.isReadyForDisplay and AVPlayer.status are both KVO observable, which will be more responsive than using a Timer.
Also note if you use an AVPlayerLayer to display the video (instead of AVPlayerViewController), you need to observe the AVPlayerLayer's "isReadyForDisplay" for the same reason.
#objc func timerStatusCheck() {
// function called by a Timer and checks the status of AVPlayer
if player!.status == AVPlayerStatus.readyToPlay {
print("ready to play")
// check that controller's view is showing video frames from player
if playerController.isReadyForDisplay == false {
print("view not yet showing video")
return
}
timerStop()
if (readyToPlayHandler != nil) {
self.readyToPlayHandler!() // here we hide the skeleton image that shows while video is loading
}
} else if player!.status == AVPlayerStatus.failed {
timerStop()
MessageBox.showError("Video Failed to start")
}
}
I am trying to video capture an ARKIt app using ReplayKit. I have a record button, when pressed turned red and start recording, then pressed again to turn white and stop recording.
But the stopRecording method never worked on the first time.
if recorder.isAvailable {
recorder.delegate = self
if recorder.isRecording {
print("Recorder is recording...")
// Stop recording
recorder.stopRecording { previewController, error in
print("Stop recording...")
self.recordImage.color = UIColor.white
self.recordImage.colorBlendFactor = 1
if let controller = previewController {
controller.previewControllerDelegate = self
self.present(controller, animated:true, completion:nil)
}
}
}
else {
// Start recording
recorder.startRecording { error in
print("Starting to record…")
if error == nil {
print("Start Recording…")
self.recordImage.color = UIColor.red
self.recordImage.colorBlendFactor = 1
}
}
}
When first pressed, I can see the recording started. Then when I pressed again, I can see that recorder.isRecording is entered, but the block in recorder.stopRecording does not work. I have to press again to start recording, then stop again before the recorder.stopRecording block is entered.
Any idea? Help is appreciated.
Press Record!
Starting to record…
Start Recording…
Press Record!
Recorder is recording...
I fixed this issue based on the replies at https://forums.developer.apple.com/thread/62624
This is definitely a bug in iOS; but removing the "Localization native development region" entry from the Info.plist seems to solve this issue.
Which iOS version are you using? I've seen cases where the completion handler doesn't get called, often on the first try, but then works thereafter. This happened a lot on iOS 9 and again in 11.0, but seems to be better in 11.0.3.
I'm not sure if you are trying this on an iPad, but your code above won't work on an iPad. You need to set a presentation style.
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.phone {
self.present(controller, animated: true, completion: nil)
}
else {
controller.popoverPresentationController?.sourceRect = self.recordingButton.bounds
controller.popoverPresentationController?.sourceView = self.view
controller.modalPresentationStyle = UIModalPresentationStyle.popover
controller.preferredContentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height)
self.present(controller, animated: true, completion: nil)
}
When i launch my app, a background music starts to play. But when i press start and play the game (in another viewcontroller) and then go back to menu (the first view controller) the song starts again but while the same song is still playing (result = hearing it twice at the same time).Very annoying. This is my viewDidLoad function (probably where my problem is). Can someone help me (by explaining or giving code) playing the music only the first time that the view loads ?
override func viewDidLoad() {
super.viewDidLoad()
// get path of audio file
let myFilePathString = NSBundle.mainBundle().pathForResource("Background Music Loop (Free to Use)", ofType: "mp3")
if let myFilePathString = myFilePathString {
let myFilePathURL = NSURL(fileURLWithPath: myFilePathString)
do { try myAudioPlayer = AVAudioPlayer(contentsOfURL: myFilePathURL)
myAudioPlayer.play()
myAudioPlayer.numberOfLoops = -1
}catch{
print("error")
}
}
}
myAudioPlayer.play will simply be called again, since the VC runs the viewDidLoad() method again.
My idea:
declare a variable
var x = false
next step - make a condition on your audio player to play.
if x == false { // your code here }
when triggering the Segue from the other VC try to use the prepareForSegue method.
So on 2nd ViewController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
if segue.identifier == "yourIdentifier" {
let destinationController = segue.destinationViewController as! // Controller Name here
destinationController.x = true
}
}
This will set the x to true and the music doesn't run again.
It all depends on the functionality you want.
If you only want the background music to play when the menu view controller is on screen you could make the file name and player variables properties on the view controller and then in viewWillAppear do
Player.play
And then when a user navigates away from the page stop it playing in the viewDidDissappear function.
If you want the music to always play in the background then it might be worth creating your own class for it and providing a sharedInstance on the app delegate so that you can manage a single instance of the player throughout the application.