// // PlayerVC.swift // WOKA // // Created by MacBook Pro on 30/05/24. import UIKit import JWPlayerKit import AVKit class PlayerVC: JWPlayerViewController, JWPlayerViewControllerDelegate { @IBOutlet weak var backButton: UIButton! var previousScale: CGFloat = 1.0 var contentType : VideoContentType? var config: JWPlayerConfiguration! var dismissTapped: (() -> Void)? var videoIndex : Int? var errorCount = 0 var isFullScreenBtn = false var vm = PlayerVM() deinit { NotificationCenter.default.removeObserver(self,name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.removeObserver(self,name: UIApplication.willEnterForegroundNotification, object: nil) } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .allButUpsideDown } override var shouldAutorotate: Bool { return true } override var prefersStatusBarHidden: Bool { if #available(iOS 16.0, *) { // Code for iOS 16.0 and above return false } else { // Fallback code for earlier iOS versions return true } } override func viewDidLoad() { super.viewDidLoad() self.delegate = self player.configurePlayer(with: config) vm.vc = self vm.initView() //bring back button to the front self.view.bringSubviewToFront(backButton) NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) let skinStylingBuilder = JWPlayerSkinBuilder() .adCueColor(.yellow) // .buttonsColor(.blue) // .backgroundColor(.cyan) let skinStyling = try? skinStylingBuilder.build() self.styling = skinStyling self.setVisibility(.hidden, for: [.fullscreenButton, .pictureInPictureButton,.languagesButton]) print("Quality:- ", player.visualQualityLevels) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) setNeedsStatusBarAppearanceUpdate() } // for ios 15 and below func rotateView(to angle: CGFloat) { // Apply rotation to the view's transform view.transform = CGAffineTransform(rotationAngle: angle) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if #available(iOS 16.0, *) { // Code for iOS 15.0 and above print("Running on iOS 15.0 or later") DispatchQueue.main.async { appDelegate.deviceOrientation = .landscapeRight let value = UIInterfaceOrientation.landscapeRight.rawValue UIDevice.current.setValue(value, forKey: "orientation") } } else { // Fallback code for earlier iOS versions rotateView(to: .pi / 2) // Example: 90 degrees rotation print("Running on a version earlier than iOS 15.0") } //Disable Picture in Picture playerView.allowsPictureInPicturePlayback = false playerView.captionStyle = .none } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) player.stop() } @IBAction func backBtnTapped(_ sender: UIButton) { switch contentType { case .liveStream: if let postID = vm.videoIDs.first { let duration = DateFormatterLib.dateDifferenceINT(date1: vm.startTimeStamp, date2: Date()) let totalDuration = duration + vm.totalVideoViewTime AuthFunc.shareInstance.userVideoView(postID: postID, postType: PostType.liveTV.rawValue, duration: totalDuration, catID: 0) { _ in} vm.handleBackAction() } case .webSeries: if let catID = vm.catID, vm.currentPlayingIndex >= 0 && vm.currentPlayingIndex < (vm.videoIDs.count - 1) { let postID = vm.videoIDs[vm.currentPlayingIndex] let duration = DateFormatterLib.dateDifferenceINT(date1: vm.startTimeStamp, date2: Date()) let totalDuration = duration + vm.totalVideoViewTime Utilities.startProgressHUD(msg: K.ConstantString.sync) AuthFunc.shareInstance.userVideoView(postID: postID, postType: PostType.episode.rawValue, duration: totalDuration, catID: catID) { [weak self] isDone in guard let self else{return} if isDone{ Utilities.dismissProgressHUD() vm.handleBackAction() K.GVar.reloadContinueWebSeries = true }else{ Utilities.dismissProgressHUD() vm.handleBackAction() } } } else { vm.handleBackAction() } case .trailer: let duration = DateFormatterLib.dateDifferenceINT(date1: vm.startTimeStamp, date2: Date()) let totalDuration = duration + vm.totalVideoViewTime AuthFunc.shareInstance.userVideoView(postID: 0, postType: PostType.episode.rawValue, duration: totalDuration, catID: 0) { _ in} vm.handleBackAction() case .continueWatching: if let catID = vm.catID,let postID = vm.videoIDs.first{ let duration = DateFormatterLib.dateDifferenceINT(date1: vm.startTimeStamp, date2: Date()) let totalDuration = duration + vm.totalVideoViewTime Utilities.startProgressHUD(msg: K.ConstantString.sync) AuthFunc.shareInstance.userVideoView(postID: postID, postType: PostType.episode.rawValue, duration: totalDuration, catID: catID) { [weak self] isDone in guard let self else{return} if isDone{ Utilities.dismissProgressHUD() vm.handleBackAction() K.GVar.reloadContinueWebSeries = true }else{ Utilities.dismissProgressHUD() vm.handleBackAction() } } } else { vm.handleBackAction() } case .audioBooks: if let postID = vm.videoIDs.first { let duration = DateFormatterLib.dateDifferenceINT(date1: vm.startTimeStamp, date2: Date()) let totalDuration = duration + vm.totalVideoViewTime Utilities.startProgressHUD(msg: K.ConstantString.sync) AuthFunc.shareInstance.userVideoView(postID: postID, postType: PostType.audio.rawValue, duration: totalDuration, catID: 0) { [weak self] isDone in guard let self else{ Utilities.dismissProgressHUD() return } if isDone{ Utilities.dismissProgressHUD() K.GVar.reloadContinueAudioBooks = true vm.handleBackAction() }else{ Utilities.dismissProgressHUD() vm.handleBackAction() } } } case .games: break case .songs: break default: vm.handleBackAction() } } // MARK: - App LifeCycle Handling @objc func appDidEnterBackground() { print("App entered background PlayerVC") player.pause() //Update uservideo view switch contentType { case .liveStream: vm.updateUserView() case .webSeries: vm.updateUserView() case .trailer: vm.updateUserView() case .continueWatching: vm.updateUserView() case .audioBooks: vm.updateUserView() case .games: break case .songs: break default: break } } @objc func appWillEnterForeground() { print("App will enter foreground PlayerVC") //Reset StartTimestamp vm.startTimeStamp = Date() player.play() } // MARK: - Handle Screen Transition override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: nil) { [weak self] _ in guard let self else{return} vm.checkOrientation() } } // MARK: - JWPlayerViewControllerDelegate override func jwplayerPlaylistHasCompleted(_ player: any JWPlayer) { super.jwplayerPlaylistHasCompleted(player) // if playlist is finised make sure to add it to user video view vm.updateUserView() print("PlayList Over", vm.currentPlayingIndex) } override func jwplayer(_ player: any JWPlayer, didLoadPlaylist playlist: [JWPlayerItem]) { super.jwplayer(player, didLoadPlaylist: playlist) print("Playlist loaded") } // this will give index override func jwplayer(_ player: any JWPlayer, didLoadPlaylistItem item: JWPlayerItem, at index: UInt) { super.jwplayer(player, didLoadPlaylistItem: item, at: index) print("didLoadPlaylistItem ", index) // if currentplaylist index is not set, and the video index that we got from back screen check with the load index if contentType == .webSeries{ if let videoIndex,vm.currentPlayingIndex == -1 && (index != videoIndex){ return } if vm.currentPlayingIndex != -1 && vm.currentPlayingIndex != index{ // so update the loaded index and increase the current playing index vm.updateUserView() vm.currentPlayingIndex = Int(index) }else{ vm.currentPlayingIndex = Int(index) } } } override func jwplayer(_ player: any JWPlayer, didFinishLoadingWithTime loadTime: TimeInterval) { super.jwplayer(player, didFinishLoadingWithTime: loadTime) print("LoadTime", loadTime) // if let videoIndex , contentType == .webSeries, videoIndex != 0{ // player.nextUpPlaylistIndex = videoIndex // player.next() // self.videoIndex = nil // } } override func jwplayerIsReady(_ player: JWPlayer) { super.jwplayerIsReady(player) switch contentType { case .liveStream: player.play() case .webSeries: break // player.loadPlayerItemAt(index: videoIndex ?? 0) // self.player.play(relatedContent: videoIndex ?? 0) // self.player.loadPlayerItemAt(index: videoIndex ?? 0) // player.play() // if videoIndex == 0{ // player.seek(to: 0) // }else{ // player.nextUpPlaylistIndex = videoIndex ?? 0 // player.next() // } case .trailer,.songs, .masilaSongs: break case .continueWatching,.audioBooks, .games: player.seek(to: 0) player.play() case nil: break } print("IsReady") } override func jwplayer(_ player: JWPlayer, failedWithSetupError code: UInt, message: String) { super.jwplayer(player, failedWithSetupError: code, message: message) print("Setup Error: \(code) - \(message)") } override func jwplayer(_ player: JWPlayer, failedWithError code: UInt, message: String) { super.jwplayer(player, failedWithError: code, message: message) print("Error: \(code) - \(message)") // try reconnect 3 times else show retry if errorCount < 4{ errorCount += 1 self.player.configurePlayer(with: config) self.player.play() return } DispatchQueue.main.async { Utilities.alertWithBtnCancelCompletion(title: "Error", msgBody: message, okBtnStr: "Connect", vc: self) { [weak self] isDone in guard let self else{ self?.vm.handleBackAction() return } errorCount = 0 if isDone{ self.player.configurePlayer(with: config) self.player.play() }else{ self.vm.handleBackAction() } } } } override func jwplayer(_ player: JWPlayer, encounteredWarning code: UInt, message: String) { super.jwplayer(player, encounteredWarning: code, message: message) //Handle the reconnecting of video here print("Warning: \(code) - \(message)") } override func jwplayer(_ player: JWPlayer, encounteredAdError code: UInt, message: String) { super.jwplayer(player, encounteredAdError: code, message: message) print("Ad Error: \(code) - \(message)") } override func jwplayer(_ player: JWPlayer, encounteredAdWarning code: UInt, message: String) { super.jwplayer(player, encounteredAdWarning: code, message: message) print("Ad Warning: \(code) - \(message)") } override func jwplayer(_ player: JWPlayer, isBufferingWithReason reason: JWBufferReason) { super.jwplayer(player, isBufferingWithReason: reason) print("Buffering Reason:", reason) } //When Player is Paused override func jwplayer(_ player: JWPlayer, didPauseWithReason reason: JWPauseReason) { super.jwplayer(player, didPauseWithReason: reason) if reason == .interaction{ vm.updateClicks() } // Implement custom behavior } //When Player is Play override func jwplayer(_ player: JWPlayer, isPlayingWithReason reason: JWPlayReason) { super.jwplayer(player, isPlayingWithReason: reason) if reason == .interaction{ vm.updateClicks() } } } // MARK: - Full Screen Handling extension PlayerVC { func playerViewControllerWillGoFullScreen(_ controller: JWPlayerViewController) -> JWFullScreenViewController? { print("playerViewControllerWillGoFullScreen") self.interfaceBehavior = .hidden self.player.stop() controller.player.stop() if contentType == .liveStream{ self.dismissTapped?() } return nil } func playerViewControllerDidGoFullScreen(_ controller: JWPlayerViewController) { print("playerViewControllerDidGoFullScreen") self.player.pause() self.dismissFullScreen(animated: false) self.dismiss(animated: true) return } func playerViewControllerWillDismissFullScreen(_ controller: JWPlayerViewController) { switch contentType { case .audioBooks,.liveStream,.webSeries, .continueWatching, .trailer: vm.updateUserView() default: break } print("playerViewControllerWillDismissFullScreen") } func playerViewControllerDidDismissFullScreen(_ controller: JWPlayerViewController) { print("playerViewControllerDidDismissFullScreen") vm.updateClicks() if #available(iOS 16.0, *) { // Code for iOS 16.0 and above DispatchQueue.main.async { appDelegate.deviceOrientation = .portrait let value = UIInterfaceOrientation.portrait.rawValue UIDevice.current.setValue(value, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() } } else { // Fallback code for earlier iOS versions DispatchQueue.main.async { self.dismiss(animated: true) } } } } // MARK: - Handle ADs extension PlayerVC{ // Reports when an event is emitted by the player. // override func jwplayer(_ player: JWPlayer, adEvent event: JWAdEvent) { // super.jwplayer(player, adEvent: event) // // switch event.type { // case .adBreakStart: // print("The ad break has begun") // case .schedule: // print("The ad(s) has been scheduled") // case .request: // print("The ad has been requested") // case .started: // print("The ad playback has started") // case .impression: // print("The ad impression has been fulfilled") // case .meta: // print("The ad metadata is ready") // case .clicked: // print("The ad has been tapped") // case .pause: // print("The ad playback has been paused") // case .play: // print("The ad playback has been resumed") // case .skipped: // print("The ad has been skipped") // case .complete: // print("The ad playback has finished") // case .adBreakEnd: // print("The ad break has finished") // default: // break // } // } // // // This method is triggered when a time event fires for a currently playing ad. // override func onAdTimeEvent(_ time: JWTimeData) { // super.onAdTimeEvent(time) // // // If you are not interested in the ad time data, avoid overriding this method due to performance reasons. // } // // // When the player encounters an ad warning within the SDK, this method is called on the delegate. // // Ad warnings do not prevent the ad from continuing to play. // override func jwplayer(_ player: JWPlayer, encounteredAdWarning code: UInt, message: String) { // super.jwplayer(player, encounteredAdWarning: code, message: message) // // print("An ad warning has been encountered: (\(code))-\(message)") // } // // // When the player encounters an ad error within the SDK, this method is called on the delegate. // // Ad errors prevent ads from playing, but do not prevent media playback from continuing. // override func jwplayer(_ player: JWPlayer, encounteredAdError code: UInt, message: String) { // super.jwplayer(player, encounteredAdError: code, message: message) // // print("An ad error has been encountered: (\(code))-\(message)") // } } // MARK: - JWPlayerViewController Delegate Functions extension PlayerVC { func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, controlBarVisibilityChanged isVisible: Bool, frame: CGRect) { self.backButton.isHidden = !isVisible } func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, sizeChangedFrom oldSize: CGSize, to newSize: CGSize) { // Handle size change if necessary } func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, screenTappedAt position: CGPoint) { // Handle screen tap if necessary } func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, relatedMenuOpenedWithItems items: [JWPlayerKit.JWPlayerItem], withMethod method: JWPlayerKit.JWRelatedInteraction) { print("Related items:", items) } func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, relatedMenuClosedWithMethod method: JWRelatedInteraction) { print("Related menu closed") } func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, relatedItemBeganPlaying item: JWPlayerKit.JWPlayerItem, atIndex index: Int, withMethod method: JWPlayerKit.JWRelatedMethod) { print("Item ", item, index) } }