From 6a96b8746536ce7186cecd9086d2ceff61c43446 Mon Sep 17 00:00:00 2001 From: BilalKhanWDI Date: Thu, 11 Jul 2024 20:31:25 +0530 Subject: [PATCH] - Av player enhancements --- Podfile | 2 +- .../Karaoke/Reload.imageset/Contents.json | 26 ++ .../Reload.imageset/refresh-arrow (1).png | Bin 0 -> 2039 bytes .../Reload.imageset/refresh-arrow (1)@2x.png | Bin 0 -> 4692 bytes .../Reload.imageset/refresh-arrow (1)@3x.png | Bin 0 -> 4420 bytes WOKA/Karaoke/Controller/AVPlayerVC.swift | 392 ++++++++---------- .../Karaoke/Controller/KaraokeDetailsVC.swift | 60 +-- WOKA/Karaoke/Karaoke.storyboard | 203 +++++---- WOKA/Karaoke/ViewModel/AVPlayerVM.swift | 232 ++++++++++- WOKA/Theme/Controller/PlayerVC.swift | 12 +- 10 files changed, 564 insertions(+), 363 deletions(-) create mode 100644 WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/Contents.json create mode 100644 WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1).png create mode 100644 WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@2x.png create mode 100644 WOKA/Assets/Assets.xcassets/Karaoke/Reload.imageset/refresh-arrow (1)@3x.png 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 0000000000000000000000000000000000000000..5c0ba00de47f05176d6e79033194595330b82b6e GIT binary patch literal 2039 zcmVPx+u}MThR9HvVS$(J+R~0|^&fb}~Z)W%H>`sz(pX3#qqK%1yqLoBQjP{QfTdB18 z{fDu%6pf;ypyC$_V)}UvBc+a>z+@F3k zio1N-B%oP^;a&vDGb)4x;FERz2#z#R{1&BB>4UN?U)SsP9_;mct(d8CX7NyDJbLo^ z{2K`&`xQmGMwVrrb1pdNw(t85A;jyh>z)SyKXvlR{w&NSK%h8LW5Tkti6uobOC??z))4y zx0OnzpD@M_AnQU1pAaG^AcPR8h#f1oNC;67Osouo4k@J!*ZaQz(4|Y4KHu;6o3U~m zt~80>aiHJQH0|3l(5`8kdtKK>prTwZA6;5nx-SgFMIppGf}oTloBtjR2B*R>eA028 zvy3qdfd)aKF~(|)u^aOF{Ef0K9~MF|&N%|&2v`WQ+UayY?s?wx01#Pqnh$IS)LdO% zeG>qf)oS(l!otFrV;0vbrO4EOT-QBrn&t^yGt0?TRo$;?+Od2-|7p%SO)1;6ZToi1 zvYyO;@kND;wOZ{#S(a}N!!Yslf0?HF_Sn@>!AP;>naC*T z9EBkWd=Q94Ddo0p-)ULaFX96e8JR;&O1UU{)M~Xy8DqC_&Lem7egBbWvw8a@R%Zc4 zkQiQU*L9DYruk@=8#>EhV<4!Ywz9HvS_rWh-5{6C$);)EY}@t|CNFgybh8#-iIm@!{@jUNhyl%u?GXO{NGoR1juyg0mKjB5Cl;(Wj|8uk1JP_~Q zkT)sde-(W)Hhq*a_0=;&@#tJRZ=q8!C@B}ro4ZuhY3 zx_?NsIsz0^qOR+|M(rj&$YT1DBnceH`C6yb`Tm?P5v5U0)81?t#w%eMQc02|&+|^S zTCKY$0mbtp(3MK%9!ZkkMF`6uz{97&z!`DpH{86gl$XXVr^^NL>TCMhb#@G=g zLkMxcUa!9;mK$01O3J(pcP7)4D8!Y^z+v>z_h|S(+Meu zpNGlhjObEnns&%AjAz3zjQrFz&D$KuIhB}yJ1`oVUQv{5tJUgjoO32g68&E>^jXt1 z?@uo2b^sdrpsK1>!!XWp&UL(`yI*;B1n z|F)e?Ki6f7{6*LGk1@uo?RNWT0N}*xU&+nhjO{Ilr=-EVt5hoe6eSM^gQ?Zbymp?j zFA4ZhGsfN<2mEvOdVSxtfS=^u`xQmGJIT9^Mq{7v`>)SYbY#&bNuk#phVe3{P)e!f z`~Hb$vpF?|MvRt9rNgCC=@|^X*w^^Jf3n$Z-Z^K{F$-)$&d(J^`BbVXIpn(TAE)Jd z6p9+c^78U&?A5WW#G_ zK@hbGl+rU7E?jsw)}>d;1(M-|wtM&P(*O_^&XOd_mSx>!+xBIR-lTvZIrsAN@*}xi z?n6nNFdPmaZ!{XW#f9^xt?dkZCws9KImHSk z`zWQd@B5E68jVlJLk2u>qr05tjImoK&D~P1R{JG3cbxO669s^}Wm)f-)!bcHyd#K0 zp>Uv5sXT>42SP~X1_1C^Y(E^wc_H4EbeH4f#cA#Gd&=eV0|2mZtX+P;=XvM`>G{?M zuFZ0N5?60pT3ULTbDo}%gr4V}bY1tyycdRti=5kw$GFhGQ=s}U@&;f_x+cK!(k(r%V8$Nkxn5!(qW7p!WmY2qyqr|Y`5E= z@H`K#lWC;087WB^S=M#^hq5f+5f8nv0Zaf#`-Kn#0O)5Kk_`rf-`KYOg{=szUtx?LjME(gOr|A?zoNY4qK%9lgCKZrZEfwi<2b*K58cw(aTZYgJv~PP zP1BC3s`}xDg@wa}kUeA5le9b@1i{%b44>`y`&T^3%>r~JDWoS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c7d80095195416858c54b4c053aa7516270bbd10 GIT binary patch literal 4692 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 KMNUMnLSTY=98)_0 literal 0 HcmV?d00001 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 }