How to detect when someone cancels AVPLayer? - swift

How can I detect when someone cancels AVPlayer?
Here's my code to show the video. How can I detect when someone exits the video screen?
// Create a new AVPlayerViewController and pass it a reference to the player.
let controller = AVPlayerViewController()
controller.player = player
NotificationCenter.default.addObserver(self, selector: #selector(videoDidEnded), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
NotificationCenter.default.addObserver(self, selector: #selector(videoDidCancel), name: NSNotification.Name.kAVPlayerViewControllerDismissingNotification, object: player.currentItem)
// Modally present the player and call the player's play() method when complete.
present(controller, animated: true) {
Amplitude.instance().logEvent("ac_content_video_start", withEventProperties: [
"Name": self.book.title,
"Length": 10,
"No_upvotes": self.book.starCount,
"Category": self.book.categories as Any
])
player.play()
}

Unfortunately that is not possible and there is no such event.
What you could do is to associate a UITapGestureRecognizer with the view of the AVPlayerViewController where you can capture the stop/pause/cancel events of your AVPlayerViewController. Then in your selector you can handle the pause/stop/cancel.

There is no specific delegate or notification that I know of, however I could offer this workaround.
1. Conformance to the UIViewControllerTransitioningDelegate
Make the UIViewController that presents the AVPlayerViewController conform to the transitioning delegate
class YourVC: UIViewController, UIViewControllerTransitioningDelegate
2. Add a var to keep track of video status
// Again, in the VC that presents the AVPlayerViewController
var hasMovieFinished = false
3. AVPlayerController SetUp
Make the view controller presenting the AVPlayerController the transitioningDelegate of the AVPlayerController
let controller = AVPlayerViewController()
// Add this
controller.transitioningDelegate = self
controller.player = player
4. Add this to your movie finish notification handler
This way you know, the movie finished
#objc
func videoDidEnded() {
print("Video finished")
hasMovieFinished = true
// up to you if you dismiss the controller or not
dismiss(animated: true) {
// do what you want
}
}
5. Finally, implement the transition delegate function
// MARK: UIViewControllerTransitioningDelegate
// Gets called when a modal was dismissed
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning?
{
// The dismissal was before the movie ended
if !hasMovieFinished
{
print("Movie was cancelled")
// DO WHAT YOU WANT, MOVIE WAS CANCELLED
}
return nil
}

Related

Call Controller of an autoplayed video in ViewController

I have set it up so that when a ViewController is displayed, a video starts automatically and at the end it switches to a different ViewController.
The problem is that if the app is put in the background while viewing it, the video freezes and you have to restart the application.
I thought about setting the classic pause / play controllers to appear when you press the screen so you can continue watching, but I don't know how to do that.
Or do you have another solution to prevent the video from freezing?
import UIKit
import AVKit
import AVFoundation
class View8BaController: UIViewController {
func setupAVPlayer() {
let videoURL = Bundle.main.url(forResource: "8B-A", withExtension: "mp4") // Get video url
let avAssets = AVAsset(url: videoURL!) // Create assets to get duration of video.
let avPlayer = AVPlayer(url: videoURL!) // Create avPlayer instance
let avPlayerLayer = AVPlayerLayer(player: avPlayer) // Create avPlayerLayer instance
avPlayerLayer.frame = self.view.bounds // Set bounds of avPlayerLayer
self.view.layer.addSublayer(avPlayerLayer) // Add avPlayerLayer to view's layer.
avPlayer.play() // Play video
// Add observer for every second to check video completed or not,
// If video play is completed then redirect to desire view controller.
avPlayer.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1) , queue: .main) { [weak self] time in
if time == avAssets.duration {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "SCENA7") as! SCENA7ViewController
self?.navigationController?.pushViewController(vc, animated: false)
}
}
}
//------------------------------------------------------------------------------
override func viewDidLoad() {
super.viewDidLoad()
}
//------------------------------------------------------------------------------
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.setupAVPlayer() // Call method to setup AVPlayer & AVPlayerLayer to play video
}
}
have you tried telling the video to play in the sceneDelegate?
add to the class, right above func setupAVPlayer(),
var avPlayer: AvPlayer!
then in your sceneDelegate outside any functions
let view8Ba = View8BaController()
to create an instance of the view controller. Then you can access the properties in the following:
func sceneWillEnterForeground(_ scene: UIScene) {
if view8Ba.viewIfLoaded?.window != nil {
view8Ba.avPlayer.play()
}
}
This will tell your video to start playing again when the app comes back from the background.
If you want to add a play/pause when you tap the screen you can add a tap gesture recognizer and another view to the current view controller and set the background to clear (in storyboard drag the new view to the white bar on top of the view controller)
then call
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
pauseScreenView.frame = View8BaController.bounds
}
This tells the new view you added where to be on the viewController.
In the IBAction of the tap gesture recognizer
#IBAction func screenTapped(_ sender: Any) {
View8BaController.addSubview(pauseScreenView)
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
self.pauseScreenView.removeFromSuperview()
}
}
This adds the new view to the top of the viewController and then removes it after 10 seconds.
In that new view you can add a button that will play/pause your video
#IBAction func pauseVideo(_ sender: UIButton) {
if avPlayer.timeControlStatus == .playing {
avPlayer.pause()
pauseButton.setImage(playImage, for: .normal)
}else {
avPlayer.play()
pauseButton.setImage(pauseImage, for: .normal)
}
}

What is the best way of updating a variable in a view controller from scene delegate?

I am using Spotify SDK. I want to change labels in some view controllers when a user changes his/her player state. Here is my scene delegate:
var playerViewController = MatchViewController()
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
playerViewController.stateChanged(playerState)
}
A view controller:
func stateChanged(_ playerState: SPTAppRemotePlayerState) {
// aLabel.text = playerState.track.name
}
The problem is labels or other outlets are nil when the state is changed because the view controllers are not loaded at that time. How can I fix that? (I tried isViewLoaded)
If you have a more than a few places to update according to a change that occurs at one place use observers. Here's how,
Post notification in SceneDelegate like this:
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "stateChanged"), object: nil, userInfo: ["playerState": playerState])
}
Observe in ViewControllers like this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(stateChanged), name: NSNotification.Name("stateChanged"), object: nil)
}
#objc func stateChanged(_ notification: Notification) {
if let playerState = notification.userInfo?["playerState"] as? SPTAppRemotePlayerState {
print(playerState)
}
}
}

Returning to a previous view controller

I have a view controller which I made through coding rather than storyboard to display a video.
once the video has finished playing I cannot get the view controller to return to the previous one. I have a button which takes the user to the video but when the video finish it doesn't go back to the previous view controller.
import AVKit
import AVFoundation
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "tour", ofType: "mov")!))
let vc = AVPlayerViewController()
vc.player = player
present(vc, animated: true)
}
}
this is my code for my video. I have done everything else to build the app on my storyboard
To go back to the previous VC after the video finishes, you need to -
add an observer which could detect when the video stops playing and then -
dismiss the AVPlayerViewController
To accomplish task 1 - this answer by Channel helps set up the observer -https://stackoverflow.com/a/40056529/13451699
To do task 2 - just dismiss the AVPlayerViewController from inside the playerDidFinishPlaying function, like so :
func playerDidFinishPlaying(note: NSNotification){
// Video finished playing
self.dismiss(animated: true, completion: nil)
}

How can I show same video as a background in all View Controllers without any interruption during the pages transition?

There are 2 pages(ViewController) in Main.storyboard: HomeViewController and DetailViewController
I gave a storyboard ID for the DetailViewController:DetailPage
I have added a button into first page to take user to the second page.
I also have added a back button into second page to take user to the first page back.
I need to show a video in the background in all pages.
So I have added an UIView component in all pages to show a video.
I gave 0,0,0,0 constraint values for these 2 UIView components in each pages.
First let me share source codes and then I would like to ask my questions.
HomeViewController.swift file
import UIKit
import AVFoundation
class HomeViewController: UIViewController {
private var player: AVPlayer!
#IBOutlet weak var outlet4TheVideoUiView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
configureVideoIssue()
}
override func viewWillAppear(_ animated: Bool) {
configureVideoIssue()
}
func configureVideoIssue()
{
// BACKGROUND VIDEO SCOPE STARTS
let path = URL(fileURLWithPath: Bundle.main.path(forResource: "clouds", ofType: "mp4")!)
let player = AVPlayer(url: path)
self.player = player
let newLayer = AVPlayerLayer(player: player)
newLayer.frame = self.view.bounds
outlet4TheVideoUiView.frame = self.view.bounds
outlet4TheVideoUiView.layer.addSublayer(newLayer)
newLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
player.play()
// video bitince tekrar oynatmak için
player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none
NotificationCenter.default.addObserver(self, selector: #selector(self.videoDidPlayToEnd(notification:)),
name: NSNotification.Name(rawValue: "AVPlayerItemDidPlayToEndTimeNotification"), object: player.currentItem)
NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(enteredForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
// BACKGROUND VIDEO SCOPE ENDS
}
#objc func enteredBackground() {
player.pause()
}
#objc func enteredForeground() {
player.play()
}
#objc func videoDidPlayToEnd(notification: Notification)
{
let player: AVPlayerItem = notification.object as! AVPlayerItem
player.seek(to: .zero, completionHandler: nil)
}
#IBAction func linkBtnClick(_ sender: UIButton) {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DetailPage") as! DetailViewController
controller.modalPresentationStyle = .fullScreen
//present(controller, animated: true, completion: nil)
show(controller, sender: nil)
}
}
DetailViewController.swift file
import UIKit
import AVFoundation
class DetailViewController: UIViewController {
private var player: AVPlayer!
#IBOutlet weak var outlet4TheVideoUiView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
configureVideoIssue()
// Do any additional setup after loading the view.
}
func configureVideoIssue()
{
// BACKGROUND VIDEO SCOPE STARTS
let path = URL(fileURLWithPath: Bundle.main.path(forResource: "clouds", ofType: "mp4")!)
let player = AVPlayer(url: path)
self.player = player
let newLayer = AVPlayerLayer(player: player)
newLayer.frame = self.view.bounds
outlet4TheVideoUiView.frame = self.view.bounds
outlet4TheVideoUiView.layer.addSublayer(newLayer)
newLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
player.play()
// video bitince tekrar oynatmak için
player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none
NotificationCenter.default.addObserver(self, selector: #selector(self.videoDidPlayToEnd(notification:)),
name: NSNotification.Name(rawValue: "AVPlayerItemDidPlayToEndTimeNotification"), object: player.currentItem)
NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(enteredForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
// BACKGROUND VIDEO SCOPE ENDS
}
#objc func enteredBackground() {
player.pause()
}
#objc func enteredForeground() {
player.play()
}
#objc func videoDidPlayToEnd(notification: Notification)
{
let player: AVPlayerItem = notification.object as! AVPlayerItem
player.seek(to: .zero, completionHandler: nil)
}
#IBAction func btnBackClick(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
First Question: I am sure that it shouldn't be like this because same codes are written in both ViewControllers. So how can I avoid this? Any suggestions? I should use same video ui view object as a background for all view controllers. Does it make sense? If it does not make sense. What should I do? For example, I use asp.net user control component in Visual Studio for this scenario. I create one video component(user control) and I can use this component for all pages as a background.
Second Question: (if we can't find a solution to the first question)Let's assume that app user sees Homepage right now. And let's assume that user clicks button to see secondpage(detail page)after 10th second. Button click action takes user to the second page but video starts from the first second. Video should continue from 10th second or 11th second. How can I do this?
Third Question: Second page comes from the bottom after i click button in first page. Can second page comes with fading first page out animation and appearing second page animation?
Important details: Video should stop after app is minimized and should continue after app is maximized by app user. And other important detail is: buttons are not inside of the video view. User can see buttons above the video with this way. Let me show layers for the components in order with an image:
One way that you can do this is set your view on the rootWindow and then set all your views' background color to clear.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let window=self.window!
backgroundVideoView.bounds = window.bounds
window.addSubview(backgroundVideoView)
playVideo()
return true
}
Here, backgroundVideoView refers to the view which will be playing your video.
Although, I must warn you, this will consume a lot of memory but will get you the desired behavior.

AVPlayerViewController pops entire stack of view controllers when closed

I am using an AVPlayerViewController to display video content within my application. This player can be reached via a sequence of views. The sequence is embedded in a navigation view controller. The problem I have is that whenever I close the player the entire stack of view controllers is popped from the list of view controllers in the navigation controller which means that I am sent back to my home screen (however I only want to pop the AVPlayerViewController from the list and return to the screen before). I tried to find a way to override the close Button but did not find a way. Moreover I tried to push a notification and handle it in the home screen by reinitializing the entire stack of view controllers - this solution works but does not seem like the appropriate solution. I attached the class that inherits from AVPlayerViewController and the code that implements the viewController. Thankful for any hint.
import UIKit
import AVKit
import AVFoundation
// MARK: - EduMediaVideoViewController
class EduMediaVideoViewController: AVPlayerViewController, EduMediaViewController {
// MARK: Stored Type Properties
weak var eduMediaDelegate: EduMediaElementDelegate?
var videoMedia: VideoMediaElement?
// MARK: Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setUpVideo()
}
// MARK: Instance Methods
func setContent(content: MediaElement) {
guard let videoMedia = content as? VideoMediaElement else {
return
}
self.videoMedia = videoMedia
}
// MARK: Private Instance Methods
private func setUpVideo() {
let playerItem = videoMedia?.video
let player = AVPlayer(playerItem: playerItem)
self.player = player
player.play()
}
private func updateVideoProgress() {
self.eduMediaDelegate?.updateProgress(id: 1, progress: 1)
}
}
// create an extension of AVPlayerViewController
extension EduMediaVideoViewController {
// override 'viewWillDisappear'
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let category = eduMediaDelegate?.getCategory() else {
return
}
let dict = ["category": category]
NotificationCenter.default.post(name: .kAVDismissNotification, object: nil, userInfo: dict)
}
}
extension Notification.Name {
static let kAVDismissNotification = Notification.Name.init("dismissing")
}
The code that initializes the video controller:
let eduVideoViewController = EduMediaVideoViewController()
eduVideoViewController.setContent(content: mediaElement)
eduVideoViewController.eduMediaDelegate = self
navigationController?.pushViewController(eduVideoViewController, animated: false)