How To Get Time of Video Playback When User Closes AVPlayer - swift

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

Related

SwfitUI play sound on a button event

so i got this function for playing sound:
func playSound(){
let url = Bundle.main.url(forResource: "kaiBark", withExtension:"mp3")
guard url != nil else{
return
}
do{
player = try AVAudioPlayer(contentsOf: url!)
player?.play()
}catch{
print("sounds error")
}
}
for my app the user probably hit this button few times per second and when i tap the button more then 1 time the sounds only playing when i stop tapping the button meanwhile i need it to play each time i press not after i done pressing.
what am i missing for making this work?
thank you!

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.

ObservedObject class function not being called when protocol fires

While implementing the Google Interactive Media Ads (IMA) SDK protocols, my mediaPlayer/audioManager, which is an AVPlayer object, is not pausing during the adsManagerDidRequestContentPause delegate method. My mediaPlayer conforms to ObservableObject, which is where I think the problem is coming from but i'm not 100% positive.
When I press the play button, I play the audio and request the ads from my adsManager class. The issue is the preroll video plays but the audio from the content player plays over the preroll. The audio from the content player is supposed to pause when the preroll is playing and resume after it finishes. As you'll see in the code, AudioManager is also a singleton class.
Here's the code for when a user presses play.
#ObservedObject var manager = AudioManager.sharedInstance
func didTapPlayButton(){
isPlaying.toggle()
if isPlaying {
audioManager.playLiveStream(with: pageInfo.tritonMount)
adManager.requestAds()
} else {
audioManager.pause()
}
}
And here's the adManager class with the Google IMA delegate methods. After setting breakpoints at each delegate method, I found that each call to audioManager is being called successfully, however, the audio from the audioManager doesn't actually pause.
#ObservedObject var manager = AudioManager.sharedInstance
func setUpContentPlayer() {
contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: player)
// Create a player layer for the player.
playerLayer = AVPlayerLayer(player: player)
playerLayer!.frame = videoView.underlyingView.layer.bounds
videoView.underlyingView.layer.addSublayer(playerLayer!)
}
func setUpAdsLoader() {
adsLoader = IMAAdsLoader(settings: nil)
adsLoader!.delegate = self
}
func requestAds() {
// Create an ad display container for ad rendering.
let adDisplayContainer = IMAAdDisplayContainer(adContainer: videoView.underlyingView, companionSlots: nil)
// Create an ad request with our ad tag, display container, and optional user context.
let request = IMAAdsRequest(
adTagUrl: kLivePrerollVastTag,
adDisplayContainer: adDisplayContainer,
contentPlayhead: contentPlayhead,
userContext: nil)
adsLoader!.requestAds(with: request)
}
func adsLoader(_ loader: IMAAdsLoader!, adsLoadedWith adsLoadedData: IMAAdsLoadedData!) {
// Grab the instance of the IMAAdsManager and set ourselves as the delegate
adsManager = adsLoadedData.adsManager
adsManager!.delegate = self
// Create ads rendering settings and tell the SDK to use the in-app browser.
let adsRenderingSettings = IMAAdsRenderingSettings()
adsRenderingSettings.webOpenerPresentingController = viewController
// Initialize the ads manager.
adsManager!.initialize(with: adsRenderingSettings)
}
//Ads Manager recieved ad request and is loading and starting ad
func adsManager(_ adsManager: IMAAdsManager!, didReceive event: IMAAdEvent!) {
if (event.type == IMAAdEventType.LOADED) {
// When the SDK notifies us that ads have been loaded, play them.
adsManager.start()
}
}
//Ads manager failed to receive ad request
func adsLoader(_ loader: IMAAdsLoader!, failedWith adErrorData: IMAAdLoadingErrorData!) {
print("Error loading ads: \(String(describing: adErrorData.adError.message))")
audioManager.resume()
}
func adsManager(_ adsManager: IMAAdsManager!, didReceive error: IMAAdError!) {
// Something went wrong with the ads manager after ads were loaded. Log the
// error and play the content.
NSLog("AdsManager error: \(String(describing: error.message))")
audioManager.resume()
}
/*Ads manager received request, initiated avplayer and is now
requesting player be paused in order or the Ads manager to play preroll*/
func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager!) {
// The SDK is going to play ads, so pause the content.
audioManager.pause()
}
/*Ads manager received request, played the preroll and is now
requesting avplayer to resume live stream*/
func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager!) {
// The SDK is done playing ads (at least for now), so resume the content.
audioManager.resume()
}
AudioManagerClass:
/// Upon setting this property any observers for 'currentItem' as well as any time observers will
/// be removed from the old value where applicable and added to the new value where applicable.
var player: AVPlayer? {
didSet {
oldValue?.removeObserver(self, forKeyPath: "currentItem", context: &AudioManager.ObserveAVPlayerCurrentItem)
oldValue?.removeObserver(self, forKeyPath: "rate", context: &AudioManager.ObserveAVPlayerRate)
guard let player = self.player else { return }
player.addObserver(self, forKeyPath: "currentItem", options: [.initial, .new], context: &AudioManager.ObserveAVPlayerCurrentItem)
player.addObserver(self, forKeyPath: "rate", options: [.initial, .new], context: &AudioManager.ObserveAVPlayerRate)
player.automaticallyWaitsToMinimizeStalling = false
if let tmpObs = timeObserver {
print("Already have timeobserver, removing it")
oldValue?.removeTimeObserver(tmpObs)
timeObserver = nil
}
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1,preferredTimescale: 1),
queue: nil,
using: timeObserverCallback) as AnyObject?
}
}

Using an AVPlayer returns a "non-Multipath connection" error

I'm using AVKit to play a youtube URL.
I have this code inside a button action:
#IBAction func trailerButtonAction(_ sender: Any) {
guard let youtubeUrl = youtubeURL else { return }
let player = AVPlayer(url: youtubeUrl)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
present(playerViewController, animated: true) {
player.play()
}
}
The URL is valid, but when I press the button, the video doesn't stop loading and I'm getting this message on Debug area:
nw_endpoint_flow_copy_multipath_subflow_counts Called on non-Multipath connection
Edit:
I found that AVPlayer doesn't support youtube URL
I would say this log isn't necessarily relevant. I was getting this error when trying to playback on the simulator but it wasn't happening on a real device.
One workaround would be to use a 12.4.x simulator as it does not exhibit this issue. Only the 13.x simulators are showing this error. It happens to repeatedly that it slows down Simulator to a craw until all requested tracks have been buffered.
To combat this while testing, I am either not turning on AVPlayer or I am only buffering a short track.
To cut down on the number of errors try initing your AVPlayer like so:
var avPlayer : AVPlayer = AVPlayer()
This may cut down the errors by 30%.

TVOS getting ReferenceError: Can't find variable when calling SWIFT function

I am trying to playback youtube videos with tvml on my appleTV. It is based on:
https://gist.github.com/nickv2002/b7bb28cdccc000bdb910
The first time I start it, it is working, but after I play around (leaving the app), I get:
ReferenceError: Can't find variable: playYTblock
After rebooting/exit(0)ing the app the ATV it is working again... it seems, the context between the app<>tvjs is lost - anyone with ideas?
Here is my code:
in AppDelegate.swift
let playerVC = YTPlayerViewController()
in the application function:
playerVC.createPlayYT( appController! )
in presenter.js
if (youtubeUrl && (event.type === "play")) {
playYTblock(youtubeUrl);
}
in the template.xml.js
<listItemLockup youtubeUrl="H4O6oEaIDrs">
btw has anyone an idea why the event.type === select is fired right after loading the template (without clicking on my side)
Can't answer about that exact problem but,
Here is what i'm using is to play youtube video on tvos :
pod "AKYoutubeParser"
A parser to retrieve the actual youtube video urls.
And the video playback is handled by apple core stuff (AVFoundation, AVKit)
you can delete all the hud part if you don't use it.
import AKYoutubeParser
import AVFoundation
import AVKit
func displayTrailer(trailer: String) {
AKYoutubeParser.h264Streams(trailer) { [weak self] streams, error in
if let s = streams, v = AKYoutubeParser.getBestQuality(s) {
let playerVC = AVPlayerViewController()
let playerItem = AVPlayerItem(asset: AVAsset(URL: v.url))
let videoPlayer = AVPlayer(playerItem: playerItem)
playerVC.player = videoPlayer
self?.presentViewController(playerVC, animated: true) {
playerVC.player?.play()
}
}
}
}