Add Observer for end of short, high resolution video - swift

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()
}
}

Related

AVPlayer addObserver message was received but not handled

Ive been trying to add an observer to listen to AVPlayer's "timeControlStatus", mostly taken dirrectly from Apple's example;
https://developer.apple.com/documentation/avfoundation/media_playback_and_selection/observing_playback_state
I created a sperate class called Play and im calling the below from the ViewController
Play().playMusic(url: url!)
Class Play()
import Foundation
import AVFoundation
var player: AVPlayer! = nil
var playerItemContext = 0
class Play: AVPlayer {
func playMusic(url : URL) {
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
if player == nil {
player = AVPlayer(playerItem: playerItem)
if player.status.rawValue == 0 {
player.play()
player.addObserver(player, forKeyPath: "timeControlStatus", options: [.old, .new], context: &playerItemContext)
}
} else {
player.replaceCurrentItem(with: playerItem)
player.play()
}
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations
guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == "timeControlStatus" { print("Result") }
}
}
The above always crashes with;
<AVPlayer: 0x6000030a4770>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: timeControlStatus
Observed object: <AVPlayer: 0x6000030a4770>
Change: {
kind = 1;
new = 1;
old = 1;
}
Context: 0x1003f3e98'
If I remove the 'addObserver', the code acts as intended and plays the audio file, the weird thing is, if move all the observer code from the Play class over to ViewContoller it works? what gives?.
The difference is merely that when you move the code to the view controller, the view controller persists. It is a stable object living in the view controller hierarchy. So it lives long enough to do some work.
But on the other hand, in this line:
Play().playMusic(url: url!)
...the Play instance is created and immediately goes out of existence again, like a quantum virtual particle. It doesn't live long enough to be there when the playing proceeds. Hence the crash: you have allowed the observer to go out of existence too soon.
If you wanted your Play instance to persist, you would need to assign it to some long-lived variable, such as a property of your view controller.

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.

observe when AVplayer is ready to play - swift - programmatically

trying to play a video with AVPlayer like this:
if let video = card.pageImageVideoController.controllers[0] as? VideoController{
video.player.play()
}
I noticed that the video doesn't play. So I inspected deeper and found out that when I call the function .play() the AVPlayer current Item is nil.
I thought that the solution for this should be to add KVO observer for the player to see when the item is ready to play. I used this stack overflow question.
And I modified the previous code like this:
var playbackLikelyToKeepUpContext = 0
if let video = card.pageImageVideoController.controllers[0] as? VideoController{
video.player.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp",
options: .new, context: &playbackLikelyToKeepUpContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let videoController = topCard!.pageImageVideoController.controllers[0] as? VideoController else { return }
if context == &playbackLikelyToKeepUpContext {
if videoController.player.currentItem!.isPlaybackLikelyToKeepUp {
// loadingIndicatorView.stopAnimating() or something else
print("ready")
} else {
// loadingIndicatorView.startAnimating() or something else
print("not ready")
}
}
}
But the function observeValue is never called. I don't know why.
If your idea is to check if the item is ready to play or not. Then better you put observer for status. And check error in the observer function. As mentioned in the following document:
https://developer.apple.com/documentation/avfoundation/avplayeritem

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

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")
}

avplayer with slow-internet connection not working after first frame

my problem is that when having very bad connection,the activtiy indicator start animating till first frame in video is shown,then disappear thinking is the video is playing,but the video stops playing stuck on loaded first frame,until the whole video is loaded then its resume playing,how to show activity indicator while video is stuck on frame and buffering ,then play until next loaded frame ?
notes:
it's working when internet connetion is off ,video is played until the loaded frame and activity indicator is shown,then when turn on video us resumed to play and activity indicator is hidden
it's working when normal internet connection is present
removing and showing indicator using override observevalue for key path
"currentItem.loadedTimeRanges"/"currentItem.playbackBufferEmpty"
i made a uiview class with avplayer in it
import UIKit
import AVKit
import AVFoundation
class videoplaying: UIView {
override static var layerClass: AnyClass {
return AVPlayerLayer.self;
}
var playerlayer: AVPlayerLayer{
return layer as! AVPlayerLayer;
}
var player: AVPlayer?{
get{
return playerlayer.player
}
set {
playerlayer.player = newValue
}
}
var playetitem: AVPlayerItem?
}
i assigned a uivew in uicollectioncell to this class(using storyboard)
avplayer starts playing and adding observes when pressing play in uicollectioncell
#IBAction func play(_ sender: Any) {
activityindicator.isHidden = false
activityindicator.startAnimating()
self.butttoonheight.isHidden = true
self.postimage.isHidden = true
let url2 = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
let avplayer = AVPlayer(url: url2! )
let playeritem = AVPlayerItem(url: url2!)
//videoss is class of type uiview
videoss.playetitem = playeritem
videoss.playerlayer.player = avplayer
videoss.player?.play()
videoss.player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
videoss.player?.addObserver(self, forKeyPath: "rate", options: .new
, context: nil)
videoss.player?.addObserver(self, forKeyPath: "currentItem.playbackBufferEmpty", options: .new, context: nil)
playying.isHidden = false
}
//observing when video is playing
//playpause button to play or pause video while bad network is present video is stuck on first frame and playorpause is not changing while pressed
#IBAction func playorpause(_ sender: Any) {
if videoss.player?.timeControlStatus == AVPlayerTimeControlStatus.paused{
videoss.player?.play()
playying.setImage(UIImage(named: "pas50"), for: .normal)
}
if videoss.player?.timeControlStatus == AVPlayerTimeControlStatus.playing{
videoss.player?.pause()
playying.setImage(UIImage(named: "p24"), for: .normal)
}
}
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate"{
print(videoss.player?.rate)
if videoss.player?.rate == 0.0 {
print("dawdaopwdaopwdipo")
}
}
if keyPath == "currentItem.loadedTimeRanges"{
print("its is working")
activityindicator.stopAnimating()
activityindicator.isHidden = true
}
if keyPath == "currentItem.playbackBufferEmpty"
{
activityindicator.startAnimating()
activityindicator.isHidden = false
print("pkawdawdawd")
}
}
I solved this problem by adding a timer that runs ever 0.3 seconds,evrry 0.3seconds it checks the current time of the video if current time equals previous time then the video is not playing activity indicator is shown,if not video if playing activity indicators was hidden,also you need to check if the users pause the video,an advantage is you also get the current time also in the same function.
What i tried also was seeing rate of the video ,but over slow internet the rate was always giving that is playing,but it wasn’t,which didn’t help