315 lines
13 KiB
Swift
315 lines
13 KiB
Swift
//
|
|
// JWKaraokePlayerVM.swift
|
|
// WOKA
|
|
//
|
|
// Created by Bilal on 09/08/2024.
|
|
//
|
|
|
|
import UIKit
|
|
import AVFAudio
|
|
import AVFoundation
|
|
import GoogleMobileAds
|
|
|
|
class JWKaraokePlayerVM{
|
|
|
|
weak var vc : JWKaraokePlayerVC!
|
|
var mixedAudioURL: URL?
|
|
var audioURLFromMP4 : URL?
|
|
var recordedAudioURL: URL?
|
|
var audioRecorder: AVAudioRecorder?
|
|
var videoTitle : String?
|
|
var videoUrl : String?
|
|
|
|
//for mapping the post
|
|
var postID : Int?
|
|
|
|
//Start and end time for trimming the audio
|
|
var startTime : TimeInterval?
|
|
var endTime : TimeInterval?
|
|
|
|
var startTimeStamp = Date()
|
|
var headerBannerView = GADBannerView()
|
|
|
|
var playerKAraoke = AVPlayer()
|
|
|
|
func initView(){
|
|
if let adsData = AuthFunc.shareInstance.adsData, let karaokePlayerAd = adsData.result?.filter({$0.slug == AdsEnum.karaokePlayer.rawValue}).first, karaokePlayerAd.googleAd != nil{
|
|
/*
|
|
Show google ads with dispatch queue.
|
|
*/
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8, execute: { [weak self] in
|
|
guard let self else{return}
|
|
AdReusable.sharedInstance.setupBannerAd(bannerView: self.headerBannerView, in: vc.adView, adUnitID: K.GoogleAdIDs.themeTwo, viewController: self.vc)
|
|
})
|
|
}
|
|
|
|
startTimeStamp = Date()
|
|
vc.downloadRecordingBtn.isEnabled = false
|
|
hideShowKaraoke(isLoading: true)
|
|
}
|
|
|
|
// MARK: - Document Directory Save & Fetch audio for recording
|
|
|
|
func saveToFilesApp() {
|
|
guard let mixedAudioURL = mixedAudioURL else { return }
|
|
DispatchQueue.main.async {
|
|
let documentPicker = UIDocumentPickerViewController(url: mixedAudioURL, in: .exportToService)
|
|
documentPicker.delegate = self.vc
|
|
self.vc.present(documentPicker, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
func setupKaraoke(){
|
|
guard let videoUrl else{return}
|
|
// let avURL = URL(string: "https://content.jwplatform.com/videos/699dmCGz-UwFX5S0R.mp4")!
|
|
let avURL = URL(string: videoUrl)!
|
|
let asset = AVAsset(url: avURL)
|
|
hideShowKaraoke(isLoading: true)
|
|
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(videoTitle?.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)
|
|
DispatchQueue.main.async {
|
|
if error == nil && isDone{
|
|
self.hideShowKaraoke(isLoading: false)
|
|
self.audioURLFromMP4 = url
|
|
self.setupAudio()
|
|
}else{
|
|
print("errrrrr", error)
|
|
self.vc.karaokeLoading.stopAnimating()
|
|
self.vc.karaokeStack.isHidden = true
|
|
self.vc.startStopRecordingStack.isHidden = true
|
|
|
|
self.vc.retryKaraokeBtn.isHidden = 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.startStopRecordingStack.isHidden = true
|
|
}else{
|
|
vc.karaokeLoading.stopAnimating()
|
|
vc.karaokeLoading.hidesWhenStopped = true
|
|
vc.karaokeStack.isHidden = true
|
|
vc.startStopRecordingStack.isHidden = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - App LifeCycle HAndler
|
|
|
|
func updateKaraokeVideoView(){
|
|
if let postID {
|
|
let duration = DateFormatterLib.dateDifferenceINT(date1: startTimeStamp, date2: Date())
|
|
AuthFunc.shareInstance.userVideoView(postID: postID, postType: PostType.karaokeVideo.rawValue, duration: duration, catID: 0) { isDone in
|
|
if isDone{
|
|
K.GVar.reloadContinueKaraoke = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - AudioRecording
|
|
|
|
func setupAudio() {
|
|
// FileManager.default.clearTmpDirectory()
|
|
|
|
let audioSession = AVAudioSession.sharedInstance()
|
|
do {
|
|
try audioSession.setCategory(.playAndRecord, mode: .default,options: .defaultToSpeaker)
|
|
try audioSession.setActive(true)
|
|
|
|
// // URL of the downloaded M4A file
|
|
// guard let audioURL = Bundle.main.url(forResource: "Sample_audio", withExtension: "m4a") else {
|
|
// print("Audio file not found.")
|
|
// return
|
|
// }
|
|
|
|
// Initialize AVAudioPlayer with the downloaded M4A file
|
|
// player = try AVAudioPlayer(contentsOf: audioURLFromMP4!)
|
|
// player?.prepareToPlay()
|
|
|
|
// 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() {
|
|
startTime = vc.player.time.position.round(to: 1)
|
|
vc.interfaceBehavior = .hidden
|
|
guard let audioRecorder = audioRecorder else { return }
|
|
audioRecorder.record()
|
|
}
|
|
|
|
func stopRecording() {
|
|
endTime = vc.player.time.position.round(to: 1)
|
|
vc.interfaceBehavior = .normal
|
|
guard let audioRecorder = audioRecorder else { return }
|
|
audioRecorder.stop()
|
|
vc.player.pause()
|
|
// Mix the recorded audio with the downloaded M4A file
|
|
guard let startTime , let endTime else{return}
|
|
mixAudio(start: startTime, stop: endTime)
|
|
}
|
|
|
|
func playMixedAudio() {
|
|
guard let mixedAudioURL = mixedAudioURL else { return }
|
|
|
|
do {
|
|
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
let destinationURL = documentsDirectoryURL.appendingPathComponent("xyze.m4a")
|
|
|
|
// Check if file already exists
|
|
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
// Delete the existing file
|
|
try FileManager.default.removeItem(at: destinationURL)
|
|
}
|
|
|
|
// Copy the new file
|
|
try FileManager.default.copyItem(at: mixedAudioURL, to: destinationURL)
|
|
|
|
// Play the audio
|
|
playerKAraoke = AVPlayer(url: destinationURL)
|
|
|
|
// Adding a completion handler to check if the player starts playing
|
|
let playerObserver = playerKAraoke.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { time in
|
|
print("Playing audio at time: \(time.seconds)")
|
|
}
|
|
|
|
|
|
playerKAraoke.volume = 1.0
|
|
playerKAraoke.play()
|
|
print("Audio is playing...")
|
|
// Uncomment this block if you need to configure the audio session and play using AVAudioPlayer
|
|
/*
|
|
// Configure the audio session
|
|
let audioSession = AVAudioSession.sharedInstance()
|
|
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
|
|
try audioSession.setActive(true)
|
|
|
|
let audioPlayer = try AVAudioPlayer(contentsOf: mixedAudioURL)
|
|
audioPlayer.volume = 1.0
|
|
audioPlayer.play()
|
|
*/
|
|
|
|
// Observing when playback finishes
|
|
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerKAraoke.currentItem, queue: .main) { [weak self] _ in
|
|
print("Audio finished playing")
|
|
guard let self else{return}
|
|
self.vc.playBtn.setTitle("Play", for: .normal)
|
|
self.vc.playBtn.setImage(UIImage(named: "PlayButtonSmall"), for: .normal)
|
|
|
|
// Disable Recording while playing, hide controls for jwplayer
|
|
self.vc.interfaceBehavior = .normal
|
|
self.vc.startRecordBtn.isEnabled = true
|
|
self.vc.downloadRecordingBtn.isEnabled = true
|
|
// Remove observer
|
|
playerKAraoke.removeTimeObserver(playerObserver)
|
|
self.vc.isPlaying = false
|
|
}
|
|
} catch {
|
|
print("Error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
func mixAudio(start : TimeInterval, stop : TimeInterval) {
|
|
let totalTime = stop - start
|
|
guard let recordedAudioURL = recordedAudioURL else { return }
|
|
guard let playerURL = audioURLFromMP4 else { return }
|
|
Utilities.startProgressHUD(msg: "Mixing Audio")
|
|
let composition = AVMutableComposition()
|
|
let compositionDuration = CMTime(seconds: totalTime, preferredTimescale: 1)
|
|
|
|
// Add the recorded audio from 0 to 10 seconds
|
|
let recordedAudioAsset = AVURLAsset(url: recordedAudioURL)
|
|
let recordedAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
|
|
|
|
do {
|
|
try recordedAudioTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: compositionDuration), of: recordedAudioAsset.tracks(withMediaType: .audio)[0], at: CMTime.zero)
|
|
} catch {
|
|
Utilities.dismissProgressHUD()
|
|
print("Error adding recorded audio track: \(error.localizedDescription)")
|
|
}
|
|
|
|
// Add the downloaded M4A file from 10 to 20 seconds
|
|
let playerAsset = AVURLAsset(url: playerURL)
|
|
let playerTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
|
|
|
|
do {
|
|
try playerTrack?.insertTimeRange(CMTimeRangeMake(start: CMTime(seconds: start, preferredTimescale: 1), duration: compositionDuration), of: playerAsset.tracks(withMediaType: .audio)[0], at: CMTime.zero)
|
|
} catch {
|
|
Utilities.dismissProgressHUD()
|
|
print("Error adding player audio track: \(error.localizedDescription)")
|
|
}
|
|
|
|
// Example usage:
|
|
|
|
|
|
// Export the mixed audio
|
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
let fileName = "\((self.videoTitle?.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: ".", with: "") ?? "Audio") + Date().timeIntervalSince1970.toString()).m4a"
|
|
let filePath = documentsDirectory.appendingPathComponent(fileName)
|
|
deleteFileIfExist(at: filePath)
|
|
|
|
mixedAudioURL = filePath
|
|
|
|
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.")
|
|
Utilities.dismissProgressHUD()
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else{return}
|
|
vc.playBtn.isEnabled = true
|
|
vc.downloadRecordingBtn.isEnabled = true
|
|
}
|
|
|
|
|
|
} else if exportSession.status == .failed {
|
|
print("Mixing audio failed.", exportSession.error?.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
func deleteFileIfExist(at url: URL) {
|
|
let fileManager = FileManager.default
|
|
|
|
do {
|
|
// Check if the file exists
|
|
if fileManager.fileExists(atPath: url.path) {
|
|
// Attempt to delete the file
|
|
try fileManager.removeItem(at: url)
|
|
print("File deleted successfully.")
|
|
} else {
|
|
print("File does not exist at path: \(url.path)")
|
|
}
|
|
} catch {
|
|
print("Error deleting file: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|