Detect when, iframe(AVPlayer) of video player inside wkwebkit - swift

When I am open any website with movie inside WKWebKit and press on players this movie will be open inside some player, where I can pause, remove etc.
My question How I can detect when this iframe(window or player, I don't know how it is named) is open or closed and do something in background if window open or closed.
For clarity I am attached screenshot of simulator where this player was opened if I am press on player with movie on website.

I am solved this problem using NotificationCenter using UIWindowDidBecomeVisibleNotification & UIWindowDidBecomeHiddenNotification.
My code:
override func viewDidLoad() {
super.viewDidLoad()
// listen for videos playing in fullscreen
NotificationCenter.default.addObserver(self, selector: #selector(onDidEnterFullscreen(_:)), name: UIWindow.didBecomeVisibleNotification, object: view.window)
// listen for videos stopping to play in fullscreen
NotificationCenter.default.addObserver(self, selector: #selector(onDidLeaveFullscreen(_:)), name: UIWindow.didBecomeHiddenNotification, object: view.window)
}
#objc func onDidEnterFullscreen(_ notification: Notification) {
print("Enter Fullscreen")
}
#objc func onDidLeaveFullscreen(_ notification: Notification) {
print("Leave Fullscreen")
}

Related

Swift Multiplayer Calls Present Game Multiple Times

I am writing several Swift multiplayer games based on the Ray Wenderlich tutorial for Nine Knights. (https://www.raywenderlich.com/7544-game-center-for-ios-building-a-turn-based-game)
I use pretty much the same GameCenterHelper file except that I change to a segue instead of present scene since I am using UIKit instead of Sprite Kit with the following important pieces:
present match maker:
func presentMatchmaker() {
guard GKLocalPlayer.local.isAuthenticated else {return}
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.inviteMessage = "Would you like to play?"
let vc = GKTurnBasedMatchmakerViewController(matchRequest: request)
vc.turnBasedMatchmakerDelegate = self
currentMatchmakerVC = vc
print(vc)
viewController?.present(vc, animated: true)
}
the player listener function:
extension GameCenterHelper: GKLocalPlayerListener {
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
if let vc = currentMatchmakerVC {
currentMatchmakerVC = nil
vc.dismiss(animated: true)
}
guard didBecomeActive else {return}
NotificationCenter.default.post(name: .presentGame, object: match)
}
}
The following extension for Notification Center:
extension Notification.Name {
static let presentGame = Notification.Name(rawValue: "presentGame")
static let authenticationChanged = Notification.Name(rawValue: "authenticationChanged")
}
In the viewdidload of the menu I call the following:
override func viewDidLoad() {
super.viewDidLoad()
createTitleLabel()
createGameImage()
createButtons()
GameCenterHelper.helper.viewController = self
GameCenterHelper.helper.currentMatch = nil
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
}
and tapping the multi device buttons calls the following:
#objc func startMultiDeviceGame() {
multiPlayer = true
GameCenterHelper.helper.presentMatchmaker()
}
and the notifications call the following:
#objc func presentGame(_ notification: Notification) {
// 1
print("present game")
guard let match = notification.object as? GKTurnBasedMatch else {return}
loadAndDisplay(match: match)
}
// MARK: - Helpers
private func loadAndDisplay(match: GKTurnBasedMatch) {
match.loadMatchData { [self] data, error in
if let data = data {
do {
gameModel = try JSONDecoder().decode(GameModel.self, from: data)
} catch {gameModel = GameModel()}
} else {gameModel = GameModel()}
GameCenterHelper.helper.currentMatch = match
print("load and display")
performSegue(withIdentifier: "gameSegue", sender: nil)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("prepare to segue")
if let vc = segue.destination as? GameVC {vc.gameModel = gameModel}
}
Which is hopefully easy to follow.
The game starts and the menu scene adds the observer for present game
The player taps multi device, which presents the matchmaker
The player selects their game from the match maker, which I think activates the player listener function
This posts to the Notification Center for present game
The notification center observer calls present game, which calls load and display, with a little help from prepare segue
My issue is that the first time I do this it works perfectly, and per the framework from that tutorial that I can't figure out how to change (an issue for a different question I think) after a player takes their turn they are returned to the menu. The second time they enter present matchmaker and select a game the present game function is called twice, and the third time they take their turn without shutting down the app it is called 3 times, etc. (I have the print statements in both the present game and load and display functions and they are called back to back the 2nd time through and back to back to back the 3rd time etc. even though they are only called once the first time a game is selected from the matchmaker)
Console messages
present matchmaker true
<GKTurnBasedMatchmakerViewController: 0x104810000>
present game
present game
present game
load and display
prepare to segue
load and display
prepare to segue
load and display
prepare to segue
2021-03-20 22:32:26.838680-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103894c00> on <Game.MenuVC: 0x103814800> (from < Game.MenuVC: 0x103814800>) whose view is not in the window hierarchy.
(419.60100000000006, 39.0)
2021-03-20 22:32:26.877943-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103898e00> on < Game.MenuVC: 0x10501c800> (from < Game.MenuVC: 0x10501c800>) whose view is not in the window hierarchy.
I had thought that this was due to me not removing the Notification Center observers, but I tried the following in the view did load for the menu screen (right before I added the .presentGame observer):
NotificationCenter.default.removeObserver(self, name: .presentGame, object: nil)
and that didn't fix the issue, so I tried the following (in place of the above):
NotificationCenter.default.removeObserver(self)
and that didn't work so I tried them each, one at a time in the view did disappear of the game view controller (which I didn't think would work since self refers to the menu vc, but I was getting desperate) and that didn't work either.
I started thinking that maybe I'm not adding multiple observers that are calling present game more than once, since the following didn't work at all the second time (I'm just using a global variable to keep track of the first run through that adds the observers and then not adding them the second time):
if addObservers {
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
addObservers = false
}
since it is trying to add a view that is not in the view hierarchy. (Although the background music for that screen starts playing, the menu remains and the game board is not shown...)
I wasn't sure if I'm removing the Notification Center observers incorrectly or if they aren't really the source of the problem so I decided to ask for help :)
Thank you!
I figured it out. I was trying to remove the Notifications from a deallocated instance of the view controller per the below link (The bottom most answer):
How to avoid adding multiple NSNotification observer?
The correct way to remove the notifications is in the view will disappear function like this:
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self, name: Notification.Name.presentGame, object: nil)
NotificationCenter.default.removeObserver(self, name: Notification.Name.authenticationChanged, object: nil)
}
After implementing that I stopped making multiple calls to the notification center.

Add Observer for end of short, high resolution video

I am trying to play a 4k Video in the AVPlayer with Swift 3 for iOS12 and it works totally fine, but i want this video to play inside a loop. I found articles that stated, that you should use this method to register for the end of the video playback:
NotificationCenter.default.addObserver(self, selector: #selector(self.replay),
name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
In this case self.replay is beeing called once the player reached the end of the video. This works fine for normal videos, but once i try to play a ~4 second long video it does not loop.
The replay function looks like this:
#objc func replay() {
self.playerViewController!.player?.seek(to: CMTime.zero)
self.playerViewController!.player!.playImmediately(atRate: 1)
}
Im adding the Observer in the presenting ViewController's viewDidLoad.
Is this a race-condition, because as stated longer videos work fine? How can i prevent this behaviour.
(Sidenote: Not a regular poster, so please tell me if my question is asked wrong or hard to understand)
NotificationCenter.default.addObserver(self, selector: #selector(replay),
name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
#objc func replay() {
self.playerViewController!.player!.seek(to: CMTime.zero)
self.playerViewController!.player!.play()
}
Not sure what playImmediately(atRate:) does, however, I know in my experience playing videos on loop is to reset the player to time zero, as you have done. Then, just naturally play the video.
ADDITION: On another note, you can WAIT until the video is ready to play to add the observer AND start the video.
override func viewDidLoad() {
player.addObserver(self, forKeyPath: "status", options: [], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if(keyPath == "status" && self.playerViewController!.player!.status == .readyToPlay) {
NotificationCenter.default.addObserver(self, selector: #selector(replay), name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
self.playerViewController!.player!.play()
}
}

How To Get Time of Video Playback When User Closes AVPlayer

In the app, the user is supposed to watch a setup video. If they don't finish watching and click the 'close' button, I want to know where they stopped so I can have the video start at that spot once they start watching again.
I want to know what the time is that they stopped watching so I can save that number to the server for later use, using the following code:
player.seek(to: CMTimeMakeWithSeconds(637, 1))
I tried the following:
func showUnplayedVideo() {
// 1. check to see if user has already watched the app overview video
ref.child("videos")
.child("appOverview")
.child("watched")
.observeSingleEvent(of: .value) { (snapshot) in
let watched = snapshot.value as? Bool ?? false
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: VideoURL.appOverview.rawValue) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
self.playerVC.player = player
// 3. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerDidFinishPlaying),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.playerVC.player?.currentItem)
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerWasStoppedPrematurely),
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object: self.playerVC.player?.currentItem)
self.present(self.playerVC, animated: true) {
self.playerVC.player?.play()
}
}
}
}
#objc func playerWasStoppedPrematurely(note: NSNotification) {
self.playerVC.dismiss(animated: true)
print(playerVC.player?.currentTime())
}
The first notification 'AVPlayerItemDidPlayToEndTime' works great (for users who watch the video all the way through), but my other notification 'AVPlayerItemFailedToPayToEndTime' doesn't work (for users who stop part way through the video).
How do I get the stopping point of my users if they don't watch the whole video?
EDIT #1
This is what the screen looks like when the video is playing:
You are presenting the playerVC from a view controller. When the playerVC dismisses, viewDidAppear() in your view controller will be called. This is a choice to put the time checking code.
Original Answer
You are already using player.currentTime(). Why not also put it in the close button handler? Something like this
let current = Double(CMTimeGetSeconds(avPlayer.currentTime()))

Notifications and pause mode

I'm making some game with Swift and SpriteKit.
When my app is going to background it calls a function pause but it automatically unpause when the game resumes.
func pauseTheGame()
{
self.scene?.isPaused = true
}
AppDelegate
func applicationWillResignActive(_ application: UIApplication)
{
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "goToBackground"), object: self)
GameScene
NotificationCenter.default.addObserver(self, selector: #selector(GameScene.pauseTheGame), name: NSNotification.Name("goToBackground"), object: nil)
How can I fix it?
I think its not ideal to pause the whole scene, its better to have a worldNode and pause that node. This will also make your life easier for overlaying Menu nodes etc.
Apple also does this in their sample game DemoBots.
Create a world node in your scene and a isGamePause property
var isGamePaused = false
let worldNode = SKNode()
and add it in didMoveToView
addChild(worldNode)
Than add all your sprites to that node
worldNode.addChild(someSprite1)
worldNode.addChild(someSprite2)
Than in your pause function you say
func pauseTheGame() {
isGamePaused = true
worldNode.paused = true
physicsWorld.speed = 0
/// show pause menu
}
Your resume function should say
func resumeTheGame() {
isGamePaused = false
worldNode.paused = false
physicsWorld.speed = 1
// remove pause menu
}
To make extra sure that your game does not resume when paused I add a check in the update method to keep the game paused.
override func update(_ currentTime: TimeInterval) {
guard !isGamePaused else {
worldNode.paused = true
physicsWorld.speed = 0
return
}
...
}
As a tip you should always organise string keys into properties to avoid typos e.g Notification centre names, UserDefaults keys, SKAction keys etc.
With Swift 3 for Notification Center names you can now create an extension and handle them in a very neat way.
extension NSNotification.Name {
static let goToBackground = Notification.Name(rawValue: "goToBackground")
}
Now you can say
NotificationCenter.default.post(name: .goToBackground, object: self)
NotificationCenter.default.addObserver(self, selector: #selector(pauseTheGame), name: .goToBackground, object: nil)
Hope this helps

how to detect tap home-button twice in ios9

In my app which am writing to learn swift and iOS9, I'm trying to pause my NStimer when user double click the home button and becomes at app switcher, accoridng to programming ios9 matt neuberg, when The user double-clicks the Home button, The user can now work in the app switcher interface. If your app is frontmost, your app delegate receives this message:
applicationWillResignActive:
But my timer only pauses when I tap home button once and when I tap twice and have the app switcher, I see my timer counting, any ideas?
Try to add this lines in your AppDelegate.swift:
static let kAppDidBecomeActive = "kAppDidBecomeActive"
static let kAppWillResignActive = "kAppWillResignActive"
func applicationDidBecomeActive(application: UIApplication) {
// Your application is now the active one
// Take into account that this method will be called when your application is launched and your timer may not initialized yet
NSNotificationCenter.defaultCenter().postNotificationName("kAppDidBecomeActive", object: nil)
}
func applicationWillResignActive(application: UIApplication) {
// Home button is pressed twice
NSNotificationCenter.defaultCenter().postNotificationName("kAppWillResignActive", object: nil)
}
In addition, set your view controller as the observer to those notifications:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(pauseGame), name: AppDelegate.kAppWillResignActive, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(resumeGame), name: AppDelegate.AppDelegate.kAppDidBecomeActive, object: nil)
}