diff --git a/Podfile b/Podfile index e6eb8ab..fdc3ff5 100644 --- a/Podfile +++ b/Podfile @@ -14,7 +14,7 @@ target 'WOKA' do pod 'Alamofire' , '~> 5.9.1' # Image Loading & Caching - pod 'SDWebImage', '~> 5.19.1' + pod 'SDWebImage' , '~> 5.19.4' #JwPlayer # pod 'JWPlayerKit', '>= 4.0.0' diff --git a/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/Contents.json b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/Contents.json new file mode 100644 index 0000000..e5d57b0 --- /dev/null +++ b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "refresh-arrow (1).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "refresh-arrow (1)@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "refresh-arrow (1)@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1).png b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1).png new file mode 100644 index 0000000..5c0ba00 Binary files /dev/null and b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1).png differ diff --git a/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@2x.png b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@2x.png new file mode 100644 index 0000000..c7d8009 Binary files /dev/null and b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@2x.png differ diff --git a/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@3x.png b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@3x.png new file mode 100644 index 0000000..ea7621c Binary files /dev/null and b/WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@3x.png differ diff --git a/WOKA/Karaoke/Controller/AVPlayerVC.swift b/WOKA/Karaoke/Controller/AVPlayerVC.swift index a5e9ee3..f9db629 100644 --- a/WOKA/Karaoke/Controller/AVPlayerVC.swift +++ b/WOKA/Karaoke/Controller/AVPlayerVC.swift @@ -9,16 +9,20 @@ import UIKit import AVFoundation class AVPlayerVC: UIViewController { - - @IBOutlet weak var videoPlayer: UIView! - // @IBOutlet weak var videoPlayerHeight: NSLayoutConstraint! + @IBOutlet weak var viewControll: UIView! @IBOutlet weak var stackCtrView: UIStackView! + @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! @IBOutlet weak var sliderStack: UIStackView! @IBOutlet weak var tintView: UIView! @IBOutlet weak var videoTitle: UILabel! + @IBOutlet weak var karaokeStack: UIStackView! + @IBOutlet weak var playPauseRecordStack: UIStackView! + @IBOutlet weak var karaokeLoading: UIActivityIndicatorView! + @IBOutlet weak var reloadBtn: UIButton! + @IBOutlet weak var img10SecBack: UIImageView! { didSet { self.img10SecBack.isUserInteractionEnabled = true @@ -48,276 +52,214 @@ class AVPlayerVC: UIViewController { } } - var videoURL : String? - var timer : Timer? var titleVideo : String? var vm = AVPlayerVM() - + + // MARK: - LifeCycle + override func viewDidLoad() { super.viewDidLoad() vm.vc = self vm.initView() - self.videoTitle.text = titleVideo - startTimer() - self.setObserverToPlayer() - + hideShowIndicator(isLoading: true) + viewControll.addTapGesture { - self.timer?.invalidate() - self.showHideControls() + self.vm.timer?.invalidate() + self.vm.showHideControls() } } override func viewDidAppear(_ animated: Bool) { - self.setVideoPlayer() + self.vm.setVideoPlayer() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.vm.player?.pause() + self.vm.player = nil + } + + deinit { + NotificationCenter.default.removeObserver(self) + self.vm.player?.currentItem?.removeObserver(self, forKeyPath: "status") + + vm.player?.removeObserver(self, forKeyPath: "rate") + if let playerItem = vm.player?.currentItem { + playerItem.removeObserver(self, forKeyPath: "isPlaybackBufferEmpty") + playerItem.removeObserver(self, forKeyPath: "isPlaybackLikelyToKeepUp") + } + } + + // MARK: - Button Clicks & Tap Handler + + @IBAction func reloadBtnTapped(_ sender: UIButton) { + vm.isFinished = false + + vm.reloadVideo() } @IBAction func closeBtnTapped(_ sender: UIButton) { self.dismiss(animated: true) } - - func startTimer(){ - timer = Timer.scheduledTimer(withTimeInterval: 3.5, repeats: false) { _ in - self.showHideControls() - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - self.player?.pause() - self.player = nil - } - - deinit { - NotificationCenter.default.removeObserver(self) - player?.currentItem?.removeObserver(self, forKeyPath: "status") - } - - // MARK: - ShowHideControls - - func showHideControls(){ - stackCtrView.isHidden.toggle() - sliderStack.isHidden.toggle() - tintView.isHidden.toggle() - if !stackCtrView.isHidden{ - startTimer() - } + @IBAction func startStopRecording(_ sender: LocalisedElementsButton) { } - - private var player : AVPlayer? = nil - private var playerLayer : AVPlayerLayer? = nil - - private func setVideoPlayer() { - guard let videoURL, let url = URL(string: videoURL) else { return } - - if self.player == nil { - // Initialize the player - let playerItem = AVPlayerItem(url: url) - player = AVPlayer(playerItem: playerItem) - - // Add observer for AVPlayerItemFailedToPlayToEndTimeNotification - NotificationCenter.default.addObserver(self, - selector: #selector(playerItemFailedToPlayToEndTime(_:)), - name: .AVPlayerItemFailedToPlayToEndTime, - object: playerItem) - - // Add observer for AVPlayerItem's status - playerItem.addObserver(self, - forKeyPath: "status", - options: [.new, .initial], - context: nil) - - // Set up the player layer - playerLayer = AVPlayerLayer(player: player) - playerLayer?.videoGravity = .resizeAspectFill - playerLayer?.frame = videoPlayer.bounds - - if let playerLayer = playerLayer { - videoPlayer.layer.addSublayer(playerLayer) - } - - - // self.player = AVPlayer(url: url) - // self.playerLayer = AVPlayerLayer(player: self.player) - // self.playerLayer?.videoGravity = .resizeAspectFill - // self.playerLayer?.frame = self.videoPlayer.bounds - // self.playerLayer?.addSublayer(self.viewControll.layer) - // if let playerLayer = self.playerLayer { - // self.videoPlayer.layer.addSublayer(playerLayer) - // } - // self.player?.play() - // self.imgPlay.image = UIImage(systemName: "pause.circle") - } + @IBAction func playPauseBtn(_ sender: LocalisedElementsButton) { } - private var timeObserver : Any? = nil - - private func setObserverToPlayer() { - let interval = CMTime(seconds: 0.3, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsed in - self.updatePlayerTime() - }) - } - - - // Handle notification - @objc func playerItemFailedToPlayToEndTime(_ notification: Notification) { - if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error { - print("Error: \(error.localizedDescription)") - handlePlayerError(error) - } - } - - // Observe for player item status changes - override func observeValue(forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey : Any]?, - context: UnsafeMutableRawPointer?) { - if keyPath == "status" { - if let playerItem = object as? AVPlayerItem { - switch playerItem.status { - case .readyToPlay: - // Ready to play - print("Player item is ready to play.") - player?.play() - imgPlay.image = UIImage(systemName: "pause.circle") - case .failed: - // Failed - if let error = playerItem.error { - print("Player item error: \(error.localizedDescription)") - handlePlayerError(error) - } - case .unknown: - // Unknown status - print("Player item status is unknown.") - @unknown default: - // Handle unexpected cases - print("Player item status is unknown.") - } - } - } - } - - func handlePlayerError(_ error: Error) { - // Update the UI or show an alert to the user - Utilities.alertWithBtnCompletion(title: "Error", msgBody: error.localizedDescription, okBtnStr: "Retry?", vc: self) { [weak self] isDone in - guard let self else{return} - player?.play() - } - } - - - - private func updatePlayerTime() { - guard let currentTime = self.player?.currentTime() else { return } - guard let duration = self.player?.currentItem?.duration else { return } - - let currentTimeInSecond = CMTimeGetSeconds(currentTime) - let durationTimeInSecond = CMTimeGetSeconds(duration) - - if self.isThumbSeek == false { - self.seekSlider.value = Float(currentTimeInSecond/durationTimeInSecond) - } - - let value = Float64(self.seekSlider.value) * CMTimeGetSeconds(duration) - - var hours = value / 3600 - var mins = (value / 60).truncatingRemainder(dividingBy: 60) - var secs = value.truncatingRemainder(dividingBy: 60) - var timeformatter = NumberFormatter() - timeformatter.minimumIntegerDigits = 2 - timeformatter.minimumFractionDigits = 0 - timeformatter.roundingMode = .down - guard let hoursStr = timeformatter.string(from: NSNumber(value: hours)), let minsStr = timeformatter.string(from: NSNumber(value: mins)), let secsStr = timeformatter.string(from: NSNumber(value: secs)) else { - return - } - self.lbCurrentTime.text = "\(hoursStr):\(minsStr):\(secsStr)" - - hours = durationTimeInSecond / 3600 - mins = (durationTimeInSecond / 60).truncatingRemainder(dividingBy: 60) - secs = durationTimeInSecond.truncatingRemainder(dividingBy: 60) - timeformatter = NumberFormatter() - timeformatter.minimumIntegerDigits = 2 - timeformatter.minimumFractionDigits = 0 - timeformatter.roundingMode = .down - guard let hoursStr = timeformatter.string(from: NSNumber(value: hours)), let minsStr = timeformatter.string(from: NSNumber(value: mins)), let secsStr = timeformatter.string(from: NSNumber(value: secs)) else { - return - } - self.lbTotalTime.text = "\(hoursStr):\(minsStr):\(secsStr)" - } - - - @objc private func onTap10SecNext() { - guard let currentTime = self.player?.currentTime() else { return } + @objc func onTap10SecNext() { + guard let currentTime = self.vm.player?.currentTime() else { return } let seekTime10Sec = CMTimeGetSeconds(currentTime).advanced(by: 10) let seekTime = CMTime(value: CMTimeValue(seekTime10Sec), timescale: 1) - self.player?.seek(to: seekTime, completionHandler: { completed in - + vm.timer?.invalidate() + vm.timer = nil + hideShowIndicator(isLoading: true) + + self.vm.player?.seek(to: seekTime, completionHandler: { [weak self] completed in + if completed{ + guard let self else{return} + hideShowIndicator(isLoading: false) + vm.startTimer() + } }) } - @objc private func onTap10SecBack() { - guard let currentTime = self.player?.currentTime() else { return } + @objc func onTap10SecBack() { + guard let currentTime = self.vm.player?.currentTime() else { return } + vm.timer?.invalidate() + vm.timer = nil + hideShowIndicator(isLoading: true) + let seekTime10Sec = CMTimeGetSeconds(currentTime).advanced(by: -10) let seekTime = CMTime(value: CMTimeValue(seekTime10Sec), timescale: 1) - self.player?.seek(to: seekTime, completionHandler: { completed in - + self.vm.player?.seek(to: seekTime, completionHandler: { [weak self] completed in + if completed{ + guard let self else{return} + hideShowIndicator(isLoading: false) + vm.startTimer() + } }) } - @objc private func onTapPlayPause() { - if self.player?.timeControlStatus == .playing { - self.imgPlay.image = UIImage(systemName: "play.circle") - self.player?.pause() - } else { - self.imgPlay.image = UIImage(systemName: "pause.circle") - self.player?.play() + func hideShowIndicator(isLoading : Bool){ + if isLoading{ + self.imgPlay.isHidden = true + self.loadingIndicator.startAnimating() + }else{ + self.imgPlay.isHidden = false + self.loadingIndicator.stopAnimating() + self.loadingIndicator.hidesWhenStopped = true } } - private var isThumbSeek : Bool = false - @objc private func onTapToSlide() { - if timer != nil{ - timer?.invalidate() - timer = nil + @objc func onTapPlayPause() { + if self.vm.player?.timeControlStatus == .playing { + self.imgPlay.image = UIImage(systemName: "play.circle") + self.vm.player?.pause() + } else { + self.imgPlay.image = UIImage(systemName: "pause.circle") + self.vm.player?.play() } - self.isThumbSeek = true - guard let duration = self.player?.currentItem?.duration else { return } + } + + @objc func onTapToSlide() { + if vm.timer != nil{ + vm.timer?.invalidate() + vm.timer = nil + } + self.vm.isThumbSeek = true + guard let duration = self.vm.player?.currentItem?.duration else { return } let value = Float64(self.seekSlider.value) * CMTimeGetSeconds(duration) if value.isNaN == false { let seekTime = CMTime(value: CMTimeValue(value), timescale: 1) - self.player?.seek(to: seekTime, completionHandler: { completed in + self.vm.player?.seek(to: seekTime, completionHandler: { completed in if completed { - self.isThumbSeek = false - self.startTimer() - // print("Completed") + self.vm.isThumbSeek = false + self.vm.startTimer() } }) } } - @objc private func onTapToggleScreen() { - if #available(iOS 16.0, *) { - guard let windowSceen = self.view.window?.windowScene else { return } - if windowSceen.interfaceOrientation == .portrait { - windowSceen.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) { error in - print(error.localizedDescription) - } - } else { - windowSceen.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { error in - print(error.localizedDescription) + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "rate" { + if let player = object as? AVPlayer { + if player.rate == 0 { + print("Player is paused") + } else { + print("Player is playing") + hideShowIndicator(isLoading: false) + imgPlay.image = UIImage(systemName: "pause.circle") } } - } else { - if UIDevice.current.orientation == .portrait { - let orientation = UIInterfaceOrientation.landscapeRight.rawValue - UIDevice.current.setValue(orientation, forKey: "orientation") - } else { - let orientation = UIInterfaceOrientation.portrait.rawValue - UIDevice.current.setValue(orientation, forKey: "orientation") + } else if let playerItem = object as? AVPlayerItem { + if keyPath == "isPlaybackBufferEmpty" { + if playerItem.isPlaybackBufferEmpty { + print("Loading") + hideShowIndicator(isLoading: true) + } + } else if keyPath == "isPlaybackLikelyToKeepUp" { + if playerItem.isPlaybackLikelyToKeepUp { + print("finished") + hideShowIndicator(isLoading: false) + } } } } + +// // Observe for player item status changes +// override func observeValue(forKeyPath keyPath: String?, +// of object: Any?, +// change: [NSKeyValueChangeKey : Any]?, +// context: UnsafeMutableRawPointer?) { +// if keyPath == "status" { +// if let playerItem = object as? AVPlayerItem { +// switch playerItem.status { +// case .readyToPlay: +// // Ready to play +// print("Player item is ready to play.") +// vm.player?.play() +// hideShowIndicator(isLoading: false) +// imgPlay.image = UIImage(systemName: "pause.circle") +// case .failed: +// // Failed +// if let error = playerItem.error { +// print("Player item error: \(error.localizedDescription)") +// vm.handlePlayerError(error) +// } +// case .unknown: +// // Unknown status +// print("Player item status is unknown.") +// @unknown default: +// // Handle unexpected cases +// print("Player item status is unknown.") +// } +// } +// } +// } + +// @objc private func onTapToggleScreen() { +// if #available(iOS 16.0, *) { +// guard let windowSceen = self.view.window?.windowScene else { return } +// if windowSceen.interfaceOrientation == .portrait { +// windowSceen.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) { error in +// print(error.localizedDescription) +// } +// } else { +// windowSceen.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { error in +// print(error.localizedDescription) +// } +// } +// } else { +// if UIDevice.current.orientation == .portrait { +// let orientation = UIInterfaceOrientation.landscapeRight.rawValue +// UIDevice.current.setValue(orientation, forKey: "orientation") +// } else { +// let orientation = UIInterfaceOrientation.portrait.rawValue +// UIDevice.current.setValue(orientation, forKey: "orientation") +// } +// } +// } } diff --git a/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift b/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift index 1b05a36..ef031dc 100644 --- a/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift +++ b/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift @@ -178,55 +178,21 @@ class KaraokeDetailsVC: UIViewController { } } - let avURL = URL(string: itemBuild.url)! - let asset = AVAsset(url: avURL) - - let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(karaokeData?.title?.trimmingCharacters(in: .whitespaces) ?? "extractedAudio").m4a") - FileManager.default.clearTmpDirectory() - asset.writeAudioTrackToURL(outputURL) { isDone, error,url in - print(isDone, error , url) + DispatchQueue.main.async { [weak self] in + guard let self else{return} + Utilities.startProgressHUD() + let sb = UIStoryboard(name: K.StoryBoard.Karaoke, bundle: nil) + let vc = sb.instantiateViewController(withIdentifier: K.StoryBoardID.Karaoke.aVPlayerVC) as! AVPlayerVC + vc.videoURL = itemBuild.url + vc.titleVideo = itemBuild.titles + vc.modalPresentationStyle = .fullScreen + self.present(vc, animated: true) + // Dismiss the progress HUD after the view controller presentation + Utilities.dismissProgressHUD() - DispatchQueue.main.async { [weak self] in - guard let self else{return} - Utilities.startProgressHUD() - let sb = UIStoryboard(name: K.StoryBoard.Karaoke, bundle: nil) - let vc = sb.instantiateViewController(withIdentifier: K.StoryBoardID.Karaoke.aVPlayerVC) as! AVPlayerVC - vc.videoURL = itemBuild.url - vc.modalPresentationStyle = .fullScreen - self.present(vc, animated: true) -// do { -// -// // Create a JWPlayerItem -// let item = try JWPlayerItemBuilder() -// .file(URL(string: itemBuild.url)!) -// .posterImage(URL(string: itemBuild.poster ?? "")!) -// .title(itemBuild.titles ?? "Karaoke") -// .build() -// -// // Create a JWPlayerConfiguration -// let config = try JWPlayerConfigurationBuilder() -// .playlist(items: [item]) -// .autostart(true) -// .build() -// -// vc.config = config -// // vc.dismissTapped = self.tapped -// vc.modalPresentationStyle = .overFullScreen -// vc.modalTransitionStyle = .crossDissolve -// vc.documentAudioUrl = url -// Utilities.dismissProgressHUD() -// // Present the PlayerVC -// self.present(vc, animated: true) -// } catch { -// print("Error creating JWPlayer configuration: \(error)") -// Utilities.dismissProgressHUD() -// } - - // Dismiss the progress HUD after the view controller presentation - Utilities.dismissProgressHUD() - - } } + + } @IBAction func closeBtnTapped(_ sender: UIButton) { diff --git a/WOKA/Karaoke/Karaoke.storyboard b/WOKA/Karaoke/Karaoke.storyboard index cf27a55..e4c89ef 100644 --- a/WOKA/Karaoke/Karaoke.storyboard +++ b/WOKA/Karaoke/Karaoke.storyboard @@ -602,10 +602,12 @@ - + - + + + + - + - + @@ -648,15 +663,24 @@ - - - - - - - + + + + + + + + + + + + + - + @@ -703,7 +727,9 @@ + + @@ -732,91 +758,92 @@ - + - - + + - - @@ -838,13 +865,17 @@ + + + + + - @@ -1045,10 +1076,13 @@ + + + @@ -1062,5 +1096,8 @@ + + + diff --git a/WOKA/Karaoke/ViewModel/AVPlayerVM.swift b/WOKA/Karaoke/ViewModel/AVPlayerVM.swift index 314c959..4136bba 100644 --- a/WOKA/Karaoke/ViewModel/AVPlayerVM.swift +++ b/WOKA/Karaoke/ViewModel/AVPlayerVM.swift @@ -5,13 +5,243 @@ // Created by Bilal on 05/07/2024. // -import Foundation +import UIKit +import AVKit class AVPlayerVM{ weak var vc : AVPlayerVC! + var player : AVPlayer? = nil + var isThumbSeek : Bool = false + var timer : Timer? + var isFinished = false{ + didSet{ + if isFinished{ + timer?.invalidate() + timer = nil + vc.reloadBtn.isHidden = false + vc.stackCtrView.isHidden = true + vc.tintView.isHidden = false + vc.sliderStack.isHidden = true + }else{ + vc.reloadBtn.isHidden = true + vc.stackCtrView.isHidden = false + vc.sliderStack.isHidden = false + } + } + } + + private var playerLayer : AVPlayerLayer? = nil + private var timeObserver : Any? = nil + func initView(){ + vc.seekSlider.transform = CGAffineTransform(scaleX: 0.85, y: 0.85) + + self.vc.videoTitle.text = vc.titleVideo + setupKaraoke() + + } + + @objc func videoDidFinish() { + self.isFinished = true } + + func setupKaraoke(){ + guard let url = vc.videoURL else{return} + hideShowKaraoke(isLoading: true) + let avURL = URL(string: url)! + let asset = AVAsset(url: avURL) + + let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(vc.titleVideo?.trimmingCharacters(in: .whitespaces) ?? "extractedAudio").m4a") + FileManager.default.clearTmpDirectory() + asset.writeAudioTrackToURL(outputURL) { [weak self] isDone, error,url in + guard let self else{return} + print(isDone, error , url) + if error == nil{ + hideShowKaraoke(isLoading: false) + } + } + } + + func hideShowKaraoke(isLoading : Bool){ + DispatchQueue.main.async { [weak self] in + guard let self else{return} + if isLoading{ + vc.karaokeLoading.startAnimating() + vc.karaokeStack.isHidden = false + vc.playPauseRecordStack.isHidden = true + }else{ + vc.karaokeLoading.stopAnimating() + vc.karaokeLoading.hidesWhenStopped = true + vc.karaokeStack.isHidden = true + vc.playPauseRecordStack.isHidden = false + } + } + } + + // MARK: - Setup Video Player + + func setVideoPlayer() { + guard let videoURL = vc.videoURL, let url = URL(string: videoURL) else { return } + + if self.player == nil { + + // Create a subview for the video player + let playerView = UIView(frame: vc.viewControll.bounds) + playerView.backgroundColor = .clear // Make sure the background is clear + vc.viewControll.addSubview(playerView) + vc.viewControll.sendSubviewToBack(playerView) // Ensure the player view is below other UI elements + + // Initialize the player + let playerItem = AVPlayerItem(url: url) + player = AVPlayer(playerItem: playerItem) + + // Add observer for AVPlayerItemFailedToPlayToEndTimeNotification + NotificationCenter.default.addObserver(self, + selector: #selector(playerItemFailedToPlayToEndTime(_:)), + name: .AVPlayerItemFailedToPlayToEndTime, + object: playerItem) + + // Add observer for AVPlayerItem's status + playerItem.addObserver(self.vc, + forKeyPath: "status", + options: [.new, .initial], + context: nil) + + // Set up the player layer + playerLayer = AVPlayerLayer(player: player) + playerLayer?.videoGravity = .resizeAspectFill + playerLayer?.frame = playerView.bounds + + if let playerLayer = playerLayer { + playerView.layer.addSublayer(playerLayer) + } + + // Add observer for play/pause + player?.addObserver(self.vc, forKeyPath: "rate", options: [.new, .initial], context: nil) + + // Add observer for buffering + playerItem.addObserver(self.vc, forKeyPath: "isPlaybackBufferEmpty", options: [.new, .initial], context: nil) + playerItem.addObserver(self.vc, forKeyPath: "isPlaybackLikelyToKeepUp", options: [.new, .initial], context: nil) + + // Add observer for video finished playing + NotificationCenter.default.addObserver(self, selector: #selector(videoDidFinish), name: .AVPlayerItemDidPlayToEndTime, object: playerItem) + self.setObserverToPlayer() + startTimer() + player?.play() + } + } + + func reloadVideo() { + guard let videoURL = vc.videoURL, let url = URL(string: videoURL) else { return } + + // Remove existing observers + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem) + player?.currentItem?.removeObserver(vc, forKeyPath: "status") + player?.currentItem?.removeObserver(vc, forKeyPath: "isPlaybackBufferEmpty") + player?.currentItem?.removeObserver(vc, forKeyPath: "isPlaybackLikelyToKeepUp") + + // Create a new player item + let playerItem = AVPlayerItem(url: url) + player?.replaceCurrentItem(with: playerItem) + + // Add observers again + NotificationCenter.default.addObserver(self, selector: #selector(videoDidFinish), name: .AVPlayerItemDidPlayToEndTime, object: playerItem) + playerItem.addObserver(vc, forKeyPath: "status", options: [.new, .initial], context: nil) + playerItem.addObserver(vc, forKeyPath: "isPlaybackBufferEmpty", options: [.new, .initial], context: nil) + playerItem.addObserver(vc, forKeyPath: "isPlaybackLikelyToKeepUp", options: [.new, .initial], context: nil) + + player?.play() + } + + func setObserverToPlayer() { + let interval = CMTime(seconds: 0.3, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsed in + self.updatePlayerTime() + }) + } + + + // Handle notification + @objc func playerItemFailedToPlayToEndTime(_ notification: Notification) { + if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error { + print("Error: \(error.localizedDescription)") + handlePlayerError(error) + } + } + + func handlePlayerError(_ error: Error) { + // Update the UI or show an alert to the user + Utilities.alertWithBtnCompletion(title: "Error", msgBody: error.localizedDescription, okBtnStr: "Retry?", vc: vc.self) { [weak self] isDone in + guard let self else{return} + player?.play() + } + } + + // MARK: - ShowHideControls + func startTimer(){ + timer = Timer.scheduledTimer(withTimeInterval: 4.5, repeats: false) { _ in + self.showHideControls() + } + } + + func showHideControls(){ + if isFinished{ + vc.stackCtrView.isHidden = true + vc.reloadBtn.isHidden.toggle() + vc.sliderStack.isHidden = true + }else{ + vc.reloadBtn.isHidden = true + vc.stackCtrView.isHidden.toggle() + vc.sliderStack.isHidden.toggle() + } + + vc.tintView.isHidden.toggle() + + if !vc.stackCtrView.isHidden{ + startTimer() + } + } + + // MARK: - Update the player time Label with Slider + + private func updatePlayerTime() { + guard let currentTime = self.player?.currentTime() else { return } + guard let duration = self.player?.currentItem?.duration else { return } + + let currentTimeInSecond = CMTimeGetSeconds(currentTime) + let durationTimeInSecond = CMTimeGetSeconds(duration) + + if self.isThumbSeek == false { + self.vc.seekSlider.value = Float(currentTimeInSecond/durationTimeInSecond) + } + + let value = Float64(self.vc.seekSlider.value) * CMTimeGetSeconds(duration) + + var hours = value / 3600 + var mins = (value / 60).truncatingRemainder(dividingBy: 60) + var secs = value.truncatingRemainder(dividingBy: 60) + var timeformatter = NumberFormatter() + timeformatter.minimumIntegerDigits = 2 + timeformatter.minimumFractionDigits = 0 + timeformatter.roundingMode = .down + guard let hoursStr = timeformatter.string(from: NSNumber(value: hours)), let minsStr = timeformatter.string(from: NSNumber(value: mins)), let secsStr = timeformatter.string(from: NSNumber(value: secs)) else { + return + } + self.vc.lbCurrentTime.text = "\(hoursStr):\(minsStr):\(secsStr)" + + hours = durationTimeInSecond / 3600 + mins = (durationTimeInSecond / 60).truncatingRemainder(dividingBy: 60) + secs = durationTimeInSecond.truncatingRemainder(dividingBy: 60) + timeformatter = NumberFormatter() + timeformatter.minimumIntegerDigits = 2 + timeformatter.minimumFractionDigits = 0 + timeformatter.roundingMode = .down + guard let hoursStr = timeformatter.string(from: NSNumber(value: hours)), let minsStr = timeformatter.string(from: NSNumber(value: mins)), let secsStr = timeformatter.string(from: NSNumber(value: secs)) else { + return + } + self.vc.lbTotalTime.text = "\(hoursStr):\(minsStr):\(secsStr)" + } } diff --git a/WOKA/Theme/Controller/PlayerVC.swift b/WOKA/Theme/Controller/PlayerVC.swift index ac9d35e..af59624 100644 --- a/WOKA/Theme/Controller/PlayerVC.swift +++ b/WOKA/Theme/Controller/PlayerVC.swift @@ -49,14 +49,8 @@ class PlayerVC: JWPlayerViewController, JWPlayerViewControllerDelegate { override func viewDidLoad() { super.viewDidLoad() - player.configurePlayer(with: config) - self.rotateToLandsScapeDevice() - self.delegate = self - //Disable Picture in Picture - playerView.allowsPictureInPicturePlayback = false - playerView.captionStyle = .none } @objc func applicationDidBecomeActive() { @@ -75,6 +69,12 @@ class PlayerVC: JWPlayerViewController, JWPlayerViewControllerDelegate { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + player.configurePlayer(with: config) + self.delegate = self + + //Disable Picture in Picture + playerView.allowsPictureInPicturePlayback = false + playerView.captionStyle = .none // self.navigationController?.isNavigationBarHidden = true }