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
}