Research on Karaoke

This commit is contained in:
2024-07-09 19:46:30 +05:30
parent 23ae82968d
commit 15fa04ceb9
9 changed files with 954 additions and 62 deletions

View File

@@ -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 = "<group>"; };
5259545B2BEBB80400191286 /* AvatarDM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarDM.swift; sourceTree = "<group>"; };
5259545D2BEBBA1A00191286 /* LoadingIndicatorImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorImageView.swift; sourceTree = "<group>"; };
525FC61C2C3D3DC30049145D /* AVAssetMods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAssetMods.swift; sourceTree = "<group>"; };
525FC65C2C3D57D80049145D /* TestingKaraokeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingKaraokeVC.swift; sourceTree = "<group>"; };
52663FF42BDFAB830001D8CE /* TextFieldErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldErrorView.swift; sourceTree = "<group>"; };
52663FF62BDFACF60001D8CE /* ShadowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowView.swift; sourceTree = "<group>"; };
52663FF82BDFAF110001D8CE /* EmailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailVM.swift; sourceTree = "<group>"; };
@@ -451,6 +456,7 @@
52E214C62C2AD47F00BC2D29 /* EpisodeDetailsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeDetailsVC.swift; sourceTree = "<group>"; };
52E7E0F62BDF7DD500C86E10 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AuthenticationSB.strings; sourceTree = "<group>"; };
52E7E0F82BDF7DD900C86E10 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/AuthenticationSB.strings; sourceTree = "<group>"; };
52F4E8652C3D123B00778FBC /* JWKaraokePlayerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWKaraokePlayerVC.swift; sourceTree = "<group>"; };
52FB2D8E2BDF898F0009B0C7 /* TextFieldPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldPadding.swift; sourceTree = "<group>"; };
52FDBA772BFF23F4009D7AC7 /* TimePeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePeriod.swift; sourceTree = "<group>"; };
52FDBA7A2BFF2712009D7AC7 /* AuthFuncTimeHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFuncTimeHandling.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 */,

View File

@@ -86,7 +86,7 @@ extension K{
struct Karaoke{
static let karaokeListingVC = "KaraokeListingVC"
static let karaokeDetailsVC = "KaraokeDetailsVC"
static let aVPlayerVC = "AVPlayerVC"
static let jwKaraokePlayerVC = "JWKaraokePlayerVC"
}
}

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSMicrophoneUsageDescription</key>
<string>Give Permissions for Karaoke</string>
<key>API_KEY_ID</key>
<string>$(API_KEY_ID)</string>
<key>API_KEY_PASS</key>

View File

@@ -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
}
}
}

View File

@@ -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")
}
})
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}

View File

@@ -581,17 +581,17 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="s55-L0-QBA" userLabel="PlayerView">
<rect key="frame" x="0.0" y="330" width="414" height="250"/>
<rect key="frame" x="0.0" y="330" width="418" height="250"/>
<subviews>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l7y-7N-uz2">
<rect key="frame" x="0.0" y="0.0" width="414" height="250"/>
<rect key="frame" x="0.0" y="0.0" width="418" height="250"/>
<subviews>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7Lc-Yp-x8G">
<rect key="frame" x="0.0" y="0.0" width="414" height="250"/>
<rect key="frame" x="0.0" y="0.0" width="418" height="250"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Mhh-bR-zKm" userLabel="SliderStack">
<rect key="frame" x="5" y="218" width="404" height="30"/>
<rect key="frame" x="5" y="218" width="408" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00:00" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="PZq-WO-H32">
<rect key="frame" x="0.0" y="0.0" width="50" height="30"/>
@@ -603,12 +603,12 @@
<nil key="highlightedColor"/>
</label>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="VKq-Xn-4Nn">
<rect key="frame" x="54" y="0.0" width="296" height="31"/>
<rect key="frame" x="54" y="0.0" width="300" height="31"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="thumbTintColor" red="0.80000000000000004" green="0.29411764709999999" blue="0.1137254902" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</slider>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" usesAttributedText="YES" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="0xZ-En-mzk">
<rect key="frame" x="354" y="0.0" width="50" height="30"/>
<rect key="frame" x="358" y="0.0" width="50" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="13R-ht-Ooc"/>
</constraints>
@@ -626,7 +626,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="6xv-b4-3da">
<rect key="frame" x="112" y="100" width="190" height="50"/>
<rect key="frame" x="114" y="100" width="190" height="50"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="M0h-b5-LXf">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
@@ -707,16 +707,16 @@
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="hXe-Fv-Zk9">
<rect key="frame" x="10" y="58" width="394" height="30"/>
<rect key="frame" x="10" y="58" width="398" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="NA" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="alm-nS-A2I">
<rect key="frame" x="0.0" y="0.0" width="354" height="30"/>
<rect key="frame" x="0.0" y="0.0" width="358" height="30"/>
<fontDescription key="fontDescription" name="Exo2-Bold" family="Exo 2" pointSize="18"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jlb-0g-59l">
<rect key="frame" x="364" y="0.0" width="30" height="30"/>
<rect key="frame" x="368" y="0.0" width="30" height="30"/>
<constraints>
<constraint firstAttribute="width" secondItem="jlb-0g-59l" secondAttribute="height" multiplier="1:1" id="9KW-Ku-fD3"/>
<constraint firstAttribute="width" secondItem="jlb-0g-59l" secondAttribute="height" multiplier="1:1" id="Ig1-El-cep"/>
@@ -732,13 +732,13 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="jTd-8Q-0y7">
<rect key="frame" x="25" y="658" width="364" height="96"/>
<rect key="frame" x="25" y="658" width="368" height="96"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="gLz-mh-ln0">
<rect key="frame" x="0.0" y="0.0" width="364" height="50"/>
<rect key="frame" x="0.0" y="0.0" width="368" height="50"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="4KA-5y-qg7" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="172" height="50"/>
<rect key="frame" x="0.0" y="0.0" width="174" height="50"/>
<color key="backgroundColor" red="0.80000000000000004" green="0.29411764709999999" blue="0.1137254902" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="3ZS-GJ-VKY"/>
@@ -758,7 +758,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dz2-DJ-auS" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="192" y="0.0" width="172" height="50"/>
<rect key="frame" x="194" y="0.0" width="174" height="50"/>
<color key="backgroundColor" name="TextDarkBlue"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="7d9-YY-TdQ"/>
@@ -781,7 +781,7 @@
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" hasAttributedTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k45-7s-jnN" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="0.0" y="65" width="364" height="31"/>
<rect key="frame" x="0.0" y="65" width="368" height="31"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="titleEdgeInsets" minX="5" minY="0.0" maxX="0.0" maxY="0.0"/>
@@ -854,8 +854,193 @@
</objects>
<point key="canvasLocation" x="1657.9710144927537" y="-27.455357142857142"/>
</scene>
<!--Karaoke PlayerVC-->
<scene sceneID="76g-qy-5ia">
<objects>
<viewController storyboardIdentifier="JWKaraokePlayerVC" id="9gy-Qq-XHU" customClass="JWKaraokePlayerVC" customModule="WOKA" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="h1e-2a-Kjb">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="lIa-J9-DOG">
<rect key="frame" x="20" y="606.5" width="374" height="97"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="hIr-xj-FN5">
<rect key="frame" x="0.0" y="0.0" width="374" height="50"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yv1-I1-YGZ" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="177" height="50"/>
<color key="backgroundColor" red="1" green="0.044904738164268321" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="N39-39-T3E"/>
</constraints>
<fontDescription key="fontDescription" name="Exo2-Bold" family="Exo 2" pointSize="14"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="titleEdgeInsets" minX="5" minY="0.0" maxX="0.0" maxY="0.0"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="5" maxY="0.0"/>
<state key="normal" title="Start Recording" image="Microphone"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<integer key="value" value="25"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="playNowBtnTapped:" destination="fax-bi-Mb9" eventType="touchUpInside" id="Ruq-Xc-nQi"/>
<action selector="startRecordingBtnTapped:" destination="9gy-Qq-XHU" eventType="touchUpInside" id="SOw-KK-zIp"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="249" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mHM-7B-XXx" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="197" y="0.0" width="177" height="50"/>
<color key="backgroundColor" name="TextDarkBlue"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="nIb-2q-pnZ"/>
</constraints>
<fontDescription key="fontDescription" name="Exo2-Bold" family="Exo 2" pointSize="16"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="titleEdgeInsets" minX="5" minY="0.0" maxX="0.0" maxY="0.0"/>
<inset key="imageEdgeInsets" minX="-10" minY="0.0" maxX="10" maxY="0.0"/>
<state key="normal" title="Play" image="PlayButtonSmall"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<integer key="value" value="25"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="playBtnTapped:" destination="9gy-Qq-XHU" eventType="touchUpInside" id="tLm-vf-dht"/>
<action selector="playNowBtnTapped:" destination="fax-bi-Mb9" eventType="touchUpInside" id="VzV-ZD-BbA"/>
</connections>
</button>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3hw-ks-w1o" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="0.0" y="65" width="374" height="32"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" name="Exo2-Regular" family="Exo 2" pointSize="16"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="titleEdgeInsets" minX="5" minY="0.0" maxX="0.0" maxY="0.0"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="5" maxY="0.0"/>
<state key="normal" title="Download Recording">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<integer key="value" value="25"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="downloadRecording:" destination="9gy-Qq-XHU" eventType="touchUpInside" id="dRc-xY-3VG"/>
<action selector="playNowBtnTapped:" destination="fax-bi-Mb9" eventType="touchUpInside" id="abN-kZ-2DQ"/>
</connections>
</button>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Nuf-TE-5LZ">
<rect key="frame" x="10" y="63" width="45" height="45"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="45" id="Llr-tl-LTD"/>
<constraint firstAttribute="height" constant="45" id="SlF-Fz-fuA"/>
</constraints>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="BackArrow"/>
<connections>
<action selector="backBtnTapped:" destination="9gy-Qq-XHU" eventType="touchUpInside" id="oPW-n3-xjb"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="fhs-yV-fEZ"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="lIa-J9-DOG" firstAttribute="centerY" secondItem="fhs-yV-fEZ" secondAttribute="centerY" constant="200" id="2hX-z2-lum"/>
<constraint firstAttribute="trailing" secondItem="lIa-J9-DOG" secondAttribute="trailing" constant="20" id="7WR-NN-0zd"/>
<constraint firstItem="Nuf-TE-5LZ" firstAttribute="top" secondItem="fhs-yV-fEZ" secondAttribute="top" constant="15" id="7jB-UT-Xqq"/>
<constraint firstItem="Nuf-TE-5LZ" firstAttribute="leading" secondItem="fhs-yV-fEZ" secondAttribute="leading" constant="10" id="Tcl-yf-D2V"/>
<constraint firstItem="lIa-J9-DOG" firstAttribute="leading" secondItem="fhs-yV-fEZ" secondAttribute="leading" constant="20" id="yH2-iw-QCt"/>
</constraints>
</view>
<connections>
<outlet property="backButton" destination="Nuf-TE-5LZ" id="syG-po-xEv"/>
<outlet property="downloadRecordingBtn" destination="3hw-ks-w1o" id="bWR-kY-K1G"/>
<outlet property="outerStack" destination="lIa-J9-DOG" id="uBJ-C7-rCI"/>
<outlet property="playBtn" destination="mHM-7B-XXx" id="qso-dg-U5b"/>
<outlet property="startRecordBtn" destination="yv1-I1-YGZ" id="Xf6-hb-4Qd"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="s0j-fC-RFe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2443" y="-27"/>
</scene>
<!--Testing KaraokeVC-->
<scene sceneID="ISg-0i-f5X">
<objects>
<viewController storyboardIdentifier="TestingKaraokeVC" id="Ixs-R0-bqs" customClass="TestingKaraokeVC" customModule="WOKA" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="SEn-mb-vM4">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="ApB-WQ-WAf">
<rect key="frame" x="20" y="712" width="374" height="50"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="K2c-bI-iaW" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="177" height="50"/>
<color key="backgroundColor" red="1" green="0.044904738159999999" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="uXi-mu-IQ8"/>
</constraints>
<fontDescription key="fontDescription" name="Exo2-Bold" family="Exo 2" pointSize="14"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="titleEdgeInsets" minX="5" minY="0.0" maxX="0.0" maxY="0.0"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="5" maxY="0.0"/>
<state key="normal" title="Start Recording" image="Microphone"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<integer key="value" value="25"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="startButtonPressed:" destination="Ixs-R0-bqs" eventType="touchUpInside" id="zGF-yq-0o2"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="srX-WU-cle" customClass="LocalisedElementsButton" customModule="WOKA" customModuleProvider="target">
<rect key="frame" x="197" y="0.0" width="177" height="50"/>
<color key="backgroundColor" name="TextDarkBlue"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="wwk-CU-vet"/>
</constraints>
<fontDescription key="fontDescription" name="Exo2-Bold" family="Exo 2" pointSize="16"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="titleEdgeInsets" minX="5" minY="0.0" maxX="0.0" maxY="0.0"/>
<inset key="imageEdgeInsets" minX="-10" minY="0.0" maxX="10" maxY="0.0"/>
<state key="normal" title="Play" image="PlayButtonSmall"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<integer key="value" value="25"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="stopButtonPressed:" destination="Ixs-R0-bqs" eventType="touchUpInside" id="TlD-dh-StE"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="t9O-c6-dcl"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="ApB-WQ-WAf" firstAttribute="leading" secondItem="SEn-mb-vM4" secondAttribute="leading" constant="20" id="aa2-wO-FVT"/>
<constraint firstItem="t9O-c6-dcl" firstAttribute="trailing" secondItem="ApB-WQ-WAf" secondAttribute="trailing" constant="20" id="cV0-l6-keq"/>
<constraint firstItem="t9O-c6-dcl" firstAttribute="bottom" secondItem="ApB-WQ-WAf" secondAttribute="bottom" constant="100" id="cxI-Ia-Ggd"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="2b3-eU-9bD" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3207" y="-27"/>
</scene>
</scenes>
<resources>
<image name="BackArrow" width="21.333333969116211" height="21.333333969116211"/>
<image name="CloseIconEmpty" width="30" height="30"/>
<image name="FavouriteRemove" width="42.5" height="42.5"/>
<image name="LikeRemove" width="42.5" height="42.5"/>