From 15fa04ceb959700207494663ccaa9bac95a4d80e Mon Sep 17 00:00:00 2001 From: BilalKhanWDI Date: Tue, 9 Jul 2024 19:46:30 +0530 Subject: [PATCH] Research on Karaoke --- WOKA.xcodeproj/project.pbxproj | 12 + WOKA/Constants K/StoryBoardID.swift | 2 +- WOKA/Info.plist | 2 + WOKA/Karaoke/Controller/AVAssetMods.swift | 110 ++++++ WOKA/Karaoke/Controller/AVPlayerVC.swift | 132 +++++-- .../Controller/JWKaraokePlayerVC.swift | 352 ++++++++++++++++++ .../Karaoke/Controller/KaraokeDetailsVC.swift | 71 +++- .../Karaoke/Controller/TestingKaraokeVC.swift | 120 ++++++ WOKA/Karaoke/Karaoke.storyboard | 215 ++++++++++- 9 files changed, 954 insertions(+), 62 deletions(-) create mode 100644 WOKA/Karaoke/Controller/AVAssetMods.swift create mode 100644 WOKA/Karaoke/Controller/JWKaraokePlayerVC.swift create mode 100644 WOKA/Karaoke/Controller/TestingKaraokeVC.swift diff --git a/WOKA.xcodeproj/project.pbxproj b/WOKA.xcodeproj/project.pbxproj index 70c0306..955a7e2 100644 --- a/WOKA.xcodeproj/project.pbxproj +++ b/WOKA.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ 5259545A2BEB67D200191286 /* DateFormatterLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525954592BEB67D200191286 /* DateFormatterLib.swift */; }; 5259545C2BEBB80400191286 /* AvatarDM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5259545B2BEBB80400191286 /* AvatarDM.swift */; }; 5259545E2BEBBA1A00191286 /* LoadingIndicatorImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5259545D2BEBBA1A00191286 /* LoadingIndicatorImageView.swift */; }; + 525FC61D2C3D3DC30049145D /* AVAssetMods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525FC61C2C3D3DC30049145D /* AVAssetMods.swift */; }; + 525FC65D2C3D57D80049145D /* TestingKaraokeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525FC65C2C3D57D80049145D /* TestingKaraokeVC.swift */; }; 525FD79B2C2AFB990062C80F /* JWPlayerKit in Frameworks */ = {isa = PBXBuildFile; productRef = 525FD79A2C2AFB990062C80F /* JWPlayerKit */; }; 52663FF52BDFAB830001D8CE /* TextFieldErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52663FF42BDFAB830001D8CE /* TextFieldErrorView.swift */; }; 52663FF72BDFACF60001D8CE /* ShadowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52663FF62BDFACF60001D8CE /* ShadowView.swift */; }; @@ -169,6 +171,7 @@ 52DAC6482C21762900E2F85B /* WebSeries.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 52DAC6472C21762900E2F85B /* WebSeries.storyboard */; }; 52DAC64E2C21775300E2F85B /* WebSeriesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52DAC64D2C21775300E2F85B /* WebSeriesVC.swift */; }; 52E214C72C2AD47F00BC2D29 /* EpisodeDetailsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E214C62C2AD47F00BC2D29 /* EpisodeDetailsVC.swift */; }; + 52F4E8662C3D123B00778FBC /* JWKaraokePlayerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F4E8652C3D123B00778FBC /* JWKaraokePlayerVC.swift */; }; 52FB2D8F2BDF898F0009B0C7 /* TextFieldPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FB2D8E2BDF898F0009B0C7 /* TextFieldPadding.swift */; }; 52FDBA782BFF23F4009D7AC7 /* TimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FDBA772BFF23F4009D7AC7 /* TimePeriod.swift */; }; 52FDBA7B2BFF2712009D7AC7 /* AuthFuncTimeHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FDBA7A2BFF2712009D7AC7 /* AuthFuncTimeHandling.swift */; }; @@ -340,6 +343,8 @@ 525954592BEB67D200191286 /* DateFormatterLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterLib.swift; sourceTree = ""; }; 5259545B2BEBB80400191286 /* AvatarDM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarDM.swift; sourceTree = ""; }; 5259545D2BEBBA1A00191286 /* LoadingIndicatorImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorImageView.swift; sourceTree = ""; }; + 525FC61C2C3D3DC30049145D /* AVAssetMods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAssetMods.swift; sourceTree = ""; }; + 525FC65C2C3D57D80049145D /* TestingKaraokeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingKaraokeVC.swift; sourceTree = ""; }; 52663FF42BDFAB830001D8CE /* TextFieldErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldErrorView.swift; sourceTree = ""; }; 52663FF62BDFACF60001D8CE /* ShadowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowView.swift; sourceTree = ""; }; 52663FF82BDFAF110001D8CE /* EmailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailVM.swift; sourceTree = ""; }; @@ -451,6 +456,7 @@ 52E214C62C2AD47F00BC2D29 /* EpisodeDetailsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeDetailsVC.swift; sourceTree = ""; }; 52E7E0F62BDF7DD500C86E10 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AuthenticationSB.strings; sourceTree = ""; }; 52E7E0F82BDF7DD900C86E10 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/AuthenticationSB.strings; sourceTree = ""; }; + 52F4E8652C3D123B00778FBC /* JWKaraokePlayerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWKaraokePlayerVC.swift; sourceTree = ""; }; 52FB2D8E2BDF898F0009B0C7 /* TextFieldPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldPadding.swift; sourceTree = ""; }; 52FDBA772BFF23F4009D7AC7 /* TimePeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePeriod.swift; sourceTree = ""; }; 52FDBA7A2BFF2712009D7AC7 /* AuthFuncTimeHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFuncTimeHandling.swift; sourceTree = ""; }; @@ -1382,6 +1388,9 @@ 9CB3D08A2C37BBA50062869D /* KaraokeListingVC.swift */, 9CB3D0902C37D6930062869D /* KaraokeDetailsVC.swift */, 9C21F81D2C37E3CA0050BFCC /* AVPlayerVC.swift */, + 52F4E8652C3D123B00778FBC /* JWKaraokePlayerVC.swift */, + 525FC61C2C3D3DC30049145D /* AVAssetMods.swift */, + 525FC65C2C3D57D80049145D /* TestingKaraokeVC.swift */, ); path = Controller; sourceTree = ""; @@ -1708,12 +1717,14 @@ 52D6A2512C22B58200145908 /* WebSeriesShowListingCell.swift in Sources */, 9C007F232C25603800F798C2 /* WebSeriesEpisodeCell.swift in Sources */, 5272FCE32BDFDB05000ECB1D /* UserDetailsRegisterVC.swift in Sources */, + 525FC65D2C3D57D80049145D /* TestingKaraokeVC.swift in Sources */, 525954102BE8B72900191286 /* FontCustom.swift in Sources */, 5202AAFE2BDF90590043B7BD /* TextFieldImage.swift in Sources */, 9C7939152C0F23AA00F5D6E6 /* NsNotificationExtension.swift in Sources */, 528E5F1B2C24531200E33E4E /* SeasonListingDM.swift in Sources */, 52FDDAB52BF34DC300E037C1 /* YesNoAlertVC.swift in Sources */, 52C6E0232BE3B3E300E22D59 /* SelectAvatarVC.swift in Sources */, + 52F4E8662C3D123B00778FBC /* JWKaraokePlayerVC.swift in Sources */, 529B0DD62C070C0F00CFC54B /* GuestDataDM.swift in Sources */, 5259545C2BEBB80400191286 /* AvatarDM.swift in Sources */, 52C8B06C2BDA6E87003B51D0 /* LocalizedString.swift in Sources */, @@ -1820,6 +1831,7 @@ 52C8EC7D2C3536E5002DC35C /* ContinueAudioCell.swift in Sources */, 525954252BE8F01600191286 /* ValueWrapper.swift in Sources */, 52A3F6A82BECBF2A0000BB0B /* LinkedChildCell.swift in Sources */, + 525FC61D2C3D3DC30049145D /* AVAssetMods.swift in Sources */, 52BC3BEE2C16FBDB002FACA6 /* MoreVC.swift in Sources */, 52C6E01E2BE3847F00E22D59 /* BorderView.swift in Sources */, 52FDBA7D2BFF481A009D7AC7 /* ThemeOneVM.swift in Sources */, diff --git a/WOKA/Constants K/StoryBoardID.swift b/WOKA/Constants K/StoryBoardID.swift index 6ce013c..4b5d46b 100644 --- a/WOKA/Constants K/StoryBoardID.swift +++ b/WOKA/Constants K/StoryBoardID.swift @@ -86,7 +86,7 @@ extension K{ struct Karaoke{ static let karaokeListingVC = "KaraokeListingVC" static let karaokeDetailsVC = "KaraokeDetailsVC" - static let aVPlayerVC = "AVPlayerVC" + static let jwKaraokePlayerVC = "JWKaraokePlayerVC" } } diff --git a/WOKA/Info.plist b/WOKA/Info.plist index d08457d..337af21 100644 --- a/WOKA/Info.plist +++ b/WOKA/Info.plist @@ -2,6 +2,8 @@ + NSMicrophoneUsageDescription + Give Permissions for Karaoke API_KEY_ID $(API_KEY_ID) API_KEY_PASS diff --git a/WOKA/Karaoke/Controller/AVAssetMods.swift b/WOKA/Karaoke/Controller/AVAssetMods.swift new file mode 100644 index 0000000..0dc6046 --- /dev/null +++ b/WOKA/Karaoke/Controller/AVAssetMods.swift @@ -0,0 +1,110 @@ +// +// AVAssetMods.swift +// WOKA +// +// Created by MacBook Pro on 09/07/24. +// + +import Foundation +import AVKit + +extension AVAsset { + + func writeAudioTrackToURL(_ url: URL, completion: @escaping (Bool, Error?, URL?) -> ()) { + do { + let audioAsset = try self.audioAsset() + audioAsset.writeToURL(url, completion: completion) + } catch (let error as NSError){ + completion(false, error, nil) + } + } + + func writeToURL(_ url: URL, completion: @escaping (Bool, Error?, URL?) -> ()) { + guard let exportSession = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetAppleM4A) else { + completion(false, nil , nil) + return + } + + Utilities.startProgressHUD(msg: "Preparing") + // Create an AVMutableAudioMix to adjust the volume + let audioMix = AVMutableAudioMix() + var inputParameters = [AVMutableAudioMixInputParameters]() + + // Decrease the volume by setting the volume to a value less than 1.0 + let volume : Float = 0.4 // Adjust the volume level as needed (e.g., 0.5 for half volume) + + // Create an AVMutableAudioMixInputParameters instance for each audio track + for track in self.tracks(withMediaType: .audio) { + let audioInputParams = AVMutableAudioMixInputParameters(track: track) + audioInputParams.setVolume(volume, at: .zero) // Set the volume for the audio track + inputParameters.append(audioInputParams) + } + + // Assign the input parameters to the audio mix + audioMix.inputParameters = inputParameters + + // Set the audio mix for the export session + exportSession.audioMix = audioMix + + // Configure export session and start exporting + exportSession.outputFileType = .m4a + exportSession.outputURL = url + + exportSession.exportAsynchronously { + switch exportSession.status { + case .completed: + Utilities.dismissProgressHUD() + completion(true, nil, url) + case .unknown, .waiting, .exporting, .failed, .cancelled: + Utilities.dismissProgressHUD() + completion(false, nil, nil) + } + } +// guard let exportSession = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetAppleM4A) else { +// completion(false, nil , nil) +// return +// } +// +// exportSession.outputFileType = .m4a +// exportSession.outputURL = url +// +// exportSession.exportAsynchronously { +// switch exportSession.status { +// case .completed: +// completion(true, nil, url) +// case .unknown, .waiting, .exporting, .failed, .cancelled: +// completion(false, nil, nil) +// } +// } + } + + func audioAsset() throws -> AVAsset { + + let composition = AVMutableComposition() + let audioTracks = tracks(withMediaType: .audio) + + for track in audioTracks { + + let compositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) + try compositionTrack?.insertTimeRange(track.timeRange, of: track, at: track.timeRange.start) + compositionTrack?.preferredTransform = track.preferredTransform + } + return composition + } +} + + +extension FileManager { + func clearTmpDirectory() { + do { + let tmpDirURL = FileManager.default.temporaryDirectory + let tmpDirectory = try contentsOfDirectory(atPath: tmpDirURL.path) + try tmpDirectory.forEach { file in + let fileUrl = tmpDirURL.appendingPathComponent(file) + try removeItem(atPath: fileUrl.path) + } + } catch { + //catch the error somehow + } + } +} diff --git a/WOKA/Karaoke/Controller/AVPlayerVC.swift b/WOKA/Karaoke/Controller/AVPlayerVC.swift index d3292b5..a5e9ee3 100644 --- a/WOKA/Karaoke/Controller/AVPlayerVC.swift +++ b/WOKA/Karaoke/Controller/AVPlayerVC.swift @@ -9,9 +9,9 @@ import UIKit import AVFoundation class AVPlayerVC: UIViewController { - + @IBOutlet weak var videoPlayer: UIView! -// @IBOutlet weak var videoPlayerHeight: NSLayoutConstraint! + // @IBOutlet weak var videoPlayerHeight: NSLayoutConstraint! @IBOutlet weak var viewControll: UIView! @IBOutlet weak var stackCtrView: UIStackView! @@ -47,7 +47,7 @@ class AVPlayerVC: UIViewController { self.seekSlider.addTarget(self, action: #selector(onTapToSlide), for: .valueChanged) } } - + var videoURL : String? var timer : Timer? @@ -60,6 +60,7 @@ class AVPlayerVC: UIViewController { vm.initView() self.videoTitle.text = titleVideo startTimer() + self.setObserverToPlayer() viewControll.addTapGesture { self.timer?.invalidate() @@ -87,6 +88,11 @@ class AVPlayerVC: UIViewController { self.player = nil } + deinit { + NotificationCenter.default.removeObserver(self) + player?.currentItem?.removeObserver(self, forKeyPath: "status") + } + // MARK: - ShowHideControls func showHideControls(){ @@ -98,8 +104,8 @@ class AVPlayerVC: UIViewController { startTimer() } } - - + + private var player : AVPlayer? = nil private var playerLayer : AVPlayerLayer? = nil @@ -107,40 +113,45 @@ class AVPlayerVC: UIViewController { guard let videoURL, let url = URL(string: videoURL) else { return } if self.player == nil { - 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) + // 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?.play() - self.imgPlay.image = UIImage(systemName: "pause.circle") + + + // 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") } - self.setObserverToPlayer() } - -// private var windowInterface : UIInterfaceOrientation? { -// return self.view.window?.windowScene?.interfaceOrientation -// } - -// override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { -// super.willTransition(to: newCollection, with: coordinator) -// guard let windowInterface = self.windowInterface else { return } -// if windowInterface.isPortrait == true { -// self.videoPlayerHeight.constant = 300 -// } else { -// self.videoPlayerHeight.constant = self.view.layer.bounds.width -// } -// print(self.videoPlayerHeight.constant) -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { -// self.playerLayer?.frame = self.videoPlayer.bounds -// }) -// } - - private var timeObserver : Any? = nil private func setObserverToPlayer() { @@ -150,6 +161,55 @@ class AVPlayerVC: UIViewController { }) } + + // 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 } @@ -232,7 +292,7 @@ class AVPlayerVC: UIViewController { if completed { self.isThumbSeek = false self.startTimer() -// print("Completed") + // print("Completed") } }) } diff --git a/WOKA/Karaoke/Controller/JWKaraokePlayerVC.swift b/WOKA/Karaoke/Controller/JWKaraokePlayerVC.swift new file mode 100644 index 0000000..e198a63 --- /dev/null +++ b/WOKA/Karaoke/Controller/JWKaraokePlayerVC.swift @@ -0,0 +1,352 @@ +// +// JWKaraokePlayerVC.swift +// WOKA +// +// Created by MacBook Pro on 09/07/24. +// + +import UIKit +import JWPlayerKit +import AVFAudio +import AVFoundation + +class JWKaraokePlayerVC: JWPlayerViewController, JWPlayerViewControllerDelegate { + + @IBOutlet weak var outerStack: UIStackView! + @IBOutlet weak var backButton: UIButton! + @IBOutlet weak var startRecordBtn: LocalisedElementsButton! + @IBOutlet weak var playBtn: LocalisedElementsButton! + @IBOutlet weak var downloadRecordingBtn: LocalisedElementsButton! + + var config: JWPlayerConfiguration! + var dismissTapped: (() -> Void)? + var videoIndex : Int? + var documentAudioUrl : URL? + var recordedAudioURL: URL? + var audioRecorder: AVAudioRecorder? + var isRecording = false + var playerAV : AVAudioPlayer? + + var mixedAudioURL: URL? { + didSet{ + do{ + let sPlayer = try AVAudioPlayer(contentsOf: self.mixedAudioURL!) + self.playerAV = sPlayer + self.playerAV?.prepareToPlay() +// self.playerAV?.play() + }catch{ + + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + player.configurePlayer(with: config) + + self.delegate = self + + //Disable Picture in Picture + playerView.allowsPictureInPicturePlayback = false + playerView.captionStyle = .none + + self.view.bringSubviewToFront(outerStack) + self.view.bringSubviewToFront(backButton) + setupRecorder() + } + + func setupRecorder(){ + let audioSession = AVAudioSession.sharedInstance() + do { + try audioSession.setCategory(.playAndRecord, mode: .default,options: .defaultToSpeaker) + try audioSession.setActive(true) + + + // Define settings for the audio recorder + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let outputURL = documentsDirectory.appendingPathComponent("recordedAudio.m4a") + let settings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVSampleRateKey: 44100, + AVNumberOfChannelsKey: 2, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue + ] + + // Initialize AVAudioRecorder with the output URL and settings + audioRecorder = try AVAudioRecorder(url: outputURL, settings: settings) + audioRecorder?.prepareToRecord() + recordedAudioURL = outputURL // Store the recorded audio URL + } catch { + print("Error setting up audio: \(error.localizedDescription)") + } + } + + func startRecording() { + guard let player = player, let audioRecorder = audioRecorder else { return } + player.play() + audioRecorder.record() + } + + func stopRecording() { + guard let player = player, let audioRecorder = audioRecorder else { return } + player.pause() // Pause playback instead of stopping it + audioRecorder.stop() + + // Mix the recorded audio with the downloaded M4A file + mixAudio() + } + + func mixAudio() { + guard let recordedAudioURL = recordedAudioURL else { return } +// guard let playerURL = Bundle.main.url(forResource: "Sample_audio", withExtension: "m4a") else { return } + guard let playerURL = documentAudioUrl else{return} + let composition = AVMutableComposition() + + // Add the recorded audio + let recordedAudioAsset = AVURLAsset(url: recordedAudioURL) + let recordedAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) + do { + try recordedAudioTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: recordedAudioAsset.duration), of: recordedAudioAsset.tracks(withMediaType: .audio)[0], at: CMTime.zero) + } catch { + print("Error adding recorded audio track: \(error.localizedDescription)") + } + + // Add the downloaded M4A file + let playerAsset = AVURLAsset(url: playerURL) + let playerTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) + + do { + try playerTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: playerAsset.duration), of: playerAsset.tracks(withMediaType: .audio)[0], at: CMTime.zero) + } catch { + print("Error adding player audio track: \(error.localizedDescription)") + } + + // Export the mixed audio + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + mixedAudioURL = documentsDirectory.appendingPathComponent("mixedAudio.m4a") + guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A) else { return } + exportSession.outputURL = mixedAudioURL + exportSession.outputFileType = .m4a + exportSession.exportAsynchronously { + if exportSession.status == .completed { + print("Mixing audio completed.") + + self.saveToFilesApp() + // Play the mixed audio if needed + } else if exportSession.status == .failed { + print("Mixing audio failed.") + } + } + } + + func saveToFilesApp() { + guard let mixedAudioURL = mixedAudioURL else { return } + DispatchQueue.main.async { + let documentPicker = UIDocumentPickerViewController(url: mixedAudioURL, in: .exportToService) + documentPicker.delegate = self + self.present(documentPicker, animated: true, completion: nil) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // self.navigationController?.isNavigationBarHidden = true + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + player.stop() + } + + @IBAction func startRecordingBtnTapped(_ sender: LocalisedElementsButton) { +// if sender.titleLabel?.text?.lowercased() == "start recording"{ +// guard let audioRecorder else { return } +// startRecordBtn.setTitle("Stop Recording", for: .normal) +// audioRecorder.record() +// }else{ +// guard let audioRecorder else { return } +// startRecordBtn.setTitle("Stop Recording", for: .normal) +// audioRecorder.record() +// } + + if isRecording { + stopRecording() + sender.setTitle("Start Recording", for: .normal) + } else { + startRecording() + sender.setTitle("Stop Recording", for: .normal) + } + isRecording.toggle() + } + + @IBAction func playBtnTapped(_ sender: LocalisedElementsButton) { + playerAV?.play() + } + + + @IBAction func downloadRecording(_ sender: LocalisedElementsButton) { + print("DownloadRecording") + } + @IBAction func backBtnTapped(_ sender: UIButton) { + self.player.stop() + self.dismiss(animated: true) + } + + // MARK: - JWPlayerViewControllerDelegate + + override func jwplayer(_ player: any JWPlayer, didFinishLoadingWithTime loadTime: TimeInterval) { + super.jwplayer(player, didFinishLoadingWithTime: loadTime) + print("LoadTime", loadTime) + DispatchQueue.main.async { + self.startRecordBtn.isEnabled = true + } + } + + //Playlist Functions + + // override func jwplayerHasSeeked(_ player: any JWPlayer) { + //// if player.getState() != .playing{ + //// print("Again Play") + //// player.play() + //// } + // print("Seeked " , player.getState()) + // } + + override func jwplayerIsReady(_ player: JWPlayer) { + super.jwplayerIsReady(player) + player.seek(to: 0) + + 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) + // if no internet then add network observer + print("Error: \(code) - \(message)") + } + + override func jwplayer(_ player: JWPlayer, encounteredWarning code: UInt, message: String) { + super.jwplayer(player, encounteredWarning: code, message: message) + 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) + // player.play() + print("Buffering Reason:", reason) + } + + override func jwplayer(_ player: JWPlayer, didPauseWithReason reason: JWPauseReason) { + super.jwplayer(player, didPauseWithReason: reason) + // Implement custom behavior + } +} + +extension JWKaraokePlayerVC: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + // Handle document picking completion if needed + } +} +// MARK: - Full Screen Handling + +extension JWKaraokePlayerVC { + + func playerViewControllerWillGoFullScreen(_ controller: JWPlayerViewController) -> JWFullScreenViewController? { + // controller.shouldEnterFullScreen = false +// self.setDeviceOrientation(orientation: .portrait) + self.player.stop() + self.dismiss(animated: true) + print("playerViewControllerWillGoFullScreen") + return nil + } + + func playerViewControllerDidGoFullScreen(_ controller: JWPlayerViewController) { + print("playerViewControllerDidGoFullScreen") + + return + } + + func playerViewControllerWillDismissFullScreen(_ controller: JWPlayerViewController) { + print("playerViewControllerWillDismissFullScreen") + + } + + func playerViewControllerDidDismissFullScreen(_ controller: JWPlayerViewController) { + print("playerViewControllerDidDismissFullScreen") + + self.dismissTapped?() + Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in + self.player.stop() + controller.dismiss(animated: true) + } + // self.navigationController?.popViewController(animated: true) + } + // func playerViewControllerWillGoFullScreen(_ controller: JWPlayerViewController) -> JWFullScreenViewController? { + // print("playerViewControllerWillGoFullScreen") + // return nil + // } + // + // func playerViewControllerDidGoFullScreen(_ controller: JWPlayerViewController) { + // print("playerViewControllerDidGoFullScreen") + // } + // + // func playerViewControllerWillDismissFullScreen(_ controller: JWPlayerViewController) { + // print("playerViewControllerWillDismissFullScreen") + // self.player.stop() + // self.dismissTapped?() + // self.setDeviceOrientation(orientation: .portrait) + // } + // + // func playerViewControllerDidDismissFullScreen(_ controller: JWPlayerViewController) { + // print("playerViewControllerDidDismissFullScreen") + // Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in + // self.navigationController?.popViewController(animated: true) + // } + // } +} + +// MARK: - JWPlayerViewController Delegate Functions + +extension JWKaraokePlayerVC { + + func playerViewController(_ controller: JWPlayerKit.JWPlayerViewController, controlBarVisibilityChanged isVisible: Bool, frame: CGRect) { + self.backButton.isHidden = !isVisible + // 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) + } +} diff --git a/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift b/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift index c28b279..29018ae 100644 --- a/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift +++ b/WOKA/Karaoke/Controller/KaraokeDetailsVC.swift @@ -6,6 +6,8 @@ // import UIKit +import JWPlayerKit +import AVFoundation class KaraokeDetailsVC: UIViewController { @@ -157,25 +159,74 @@ class KaraokeDetailsVC: UIViewController { } @IBAction func playNowBtnTapped(_ sender: LocalisedElementsButton) { - let sb = UIStoryboard(name: K.StoryBoard.Karaoke, bundle: nil) - let vcPush = sb.instantiateViewController(withIdentifier: K.StoryBoardID.Karaoke.aVPlayerVC) as! AVPlayerVC + +// let sb = UIStoryboard(name: K.StoryBoard.Karaoke, bundle: nil) +// let vcPush = sb.instantiateViewController(withIdentifier: "TestingKaraokeVC") as! TestingKaraokeVC +// self.present(vcPush, animated: true) +// return + var itemBuild = JwPlayerItemCreate(url: "") + if AuthFunc.shareInstance.getDefaultLanguage() == .english{ if let englishData = karaokeData?.contentMoreDetails?.filter({$0.languageMasterID == 1}).first{ - vcPush.titleVideo = englishData.title - vcPush.videoURL = englishData.url + guard let url = englishData.url , let title = englishData.title else{return} + itemBuild = JwPlayerItemCreate(url: url, poster: karaokeData?.thumbnailPath, titles: title) } }else{ if let hindiData = karaokeData?.contentMoreDetails?.filter({$0.languageMasterID == 2}).first{ - vcPush.titleVideo = hindiData.title - vcPush.videoURL = hindiData.url + guard let url = hindiData.url , let title = hindiData.title else{return} + itemBuild = JwPlayerItemCreate(url: url, poster: karaokeData?.thumbnailPath, titles: title) + } + } + + 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.jwKaraokePlayerVC) as! JWKaraokePlayerVC + 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() + } } - self.present(vcPush, animated: true) } @IBAction func closeBtnTapped(_ sender: UIButton) { - self.dismiss(animated: true) { - - } + self.dismiss(animated: true) } } diff --git a/WOKA/Karaoke/Controller/TestingKaraokeVC.swift b/WOKA/Karaoke/Controller/TestingKaraokeVC.swift new file mode 100644 index 0000000..a6627d7 --- /dev/null +++ b/WOKA/Karaoke/Controller/TestingKaraokeVC.swift @@ -0,0 +1,120 @@ +// +// TestingKaraokeVC.swift +// WOKA +// +// Created by MacBook Pro on 09/07/24. +// + +import AVFAudio +import UIKit +import AVFoundation + +class TestingKaraokeVC : UIViewController{ + + var player: AVPlayer! + var audioEngine: AVAudioEngine! + var micInput: AVAudioInputNode! + var playerNode: AVAudioPlayerNode! + var audioFile: AVAudioFile! + var fileURL: URL! + + override func viewDidLoad() { + super.viewDidLoad() + + let videoURL = URL(string: "https://content.jwplatform.com/videos/DOhtETio-Ysj2G4DQ.mp4")! + setupPlayer(with: videoURL) + setupAudioEngine() + setupAudioFile() + } + + func setupPlayer(with url: URL) { + player = AVPlayer(url: url) + let playerLayer = AVPlayerLayer(player: player) + playerLayer.frame = view.bounds + view.layer.addSublayer(playerLayer) + player.play() + } + + func setupAudioEngine() { + audioEngine = AVAudioEngine() + micInput = audioEngine.inputNode + playerNode = AVAudioPlayerNode() + + audioEngine.attach(playerNode) + let format = micInput.inputFormat(forBus: 0) + + audioEngine.connect(micInput, to: audioEngine.mainMixerNode, format: format) + audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: format) + + try! audioEngine.start() + } + + func setupAudioFile() { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + fileURL = documentsDirectory.appendingPathComponent("output.m4a") + let format = audioEngine.mainMixerNode.outputFormat(forBus: 0) + + do { + audioFile = try AVAudioFile(forWriting: fileURL, settings: format.settings) + } catch { + print("Error creating audio file: \(error)") + } + } + + func startRecording() { + let mixer = audioEngine.mainMixerNode + let format = mixer.outputFormat(forBus: 0) + + mixer.installTap(onBus: 0, bufferSize: 1024, format: format) { (buffer, time) in + do { + try self.audioFile.write(from: buffer) + } catch { + print("Error writing audio buffer: \(error)") + } + } + } + + func stopRecording() { + let mixer = audioEngine.mainMixerNode + mixer.removeTap(onBus: 0) + + // Save the file to Files app + presentDocumentPicker() + } + + func presentDocumentPicker() { + // let documentPicker = UIDocumentPickerViewController(forExporting: [fileURL]) + // documentPicker.delegate = self + // present(documentPicker, animated: true, completion: nil) + + guard let mixedAudioURL = fileURL else { return } + DispatchQueue.main.async { + let documentPicker = UIDocumentPickerViewController(url: mixedAudioURL, in: .exportToService) + documentPicker.delegate = self + self.present(documentPicker, animated: true, completion: nil) + } + } + + @IBAction func startButtonPressed(_ sender: UIButton) { + startRecording() + } + + @IBAction func stopButtonPressed(_ sender: UIButton) { + stopRecording() + } +} + +extension TestingKaraokeVC: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let selectedURL = urls.first else { return } + do { + try FileManager.default.moveItem(at: fileURL, to: selectedURL) + } catch { + print("Error saving file to Files app: \(error)") + } + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + print("Document picker was cancelled") + } +} diff --git a/WOKA/Karaoke/Karaoke.storyboard b/WOKA/Karaoke/Karaoke.storyboard index 7fb8004..4d0dc0a 100644 --- a/WOKA/Karaoke/Karaoke.storyboard +++ b/WOKA/Karaoke/Karaoke.storyboard @@ -581,17 +581,17 @@ - + - + - + - + - + - + @@ -707,16 +707,16 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +