TSBusinessAudioPlayer.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. //
  2. // TSBusinessAudioPlayer.swift
  3. // AIRingtone
  4. //
  5. // Created by 100Years on 2025/3/7.
  6. //
  7. import AVFoundation
  8. class TSBusinessAudioPlayer {
  9. static let shared = TSBusinessAudioPlayer()
  10. enum PlayerState:Equatable {
  11. case play
  12. case pause
  13. case stop
  14. case loading(Float)
  15. case volume(Float)
  16. case currentTime(Double)
  17. }
  18. private var audioPlayer: TSAudioPlayer?
  19. var stateChangedHandle:((PlayerState) -> Void)?
  20. var currentTimeChangedHandle:((Double,Double) -> Void)?
  21. var currentPlayerState:PlayerState = .stop
  22. var duration:Double{
  23. if let audioPlayer = audioPlayer {
  24. return audioPlayer.duration
  25. }
  26. return 0.0
  27. }
  28. var isPlaying:Bool{
  29. if let audioPlayer = audioPlayer {
  30. return audioPlayer.isPlaying
  31. }
  32. return false
  33. }
  34. var isLoading:Bool{
  35. switch currentPlayerState {
  36. case .loading(let float):
  37. return float < 1.0 ? true : false
  38. default:
  39. return false
  40. }
  41. }
  42. var currentTime:Double{
  43. if let audioPlayer = audioPlayer {
  44. return audioPlayer.currentTime
  45. }
  46. return 0.0
  47. }
  48. /// 跳转到指定时间
  49. /// - Parameter time: 目标时间(秒)
  50. func seek(to time: Double) {
  51. audioPlayer?.seek(to: time)
  52. }
  53. var playProgress:Double{
  54. let playProgress = currentTime / duration
  55. // dePrint("TSAudioPlayer playProgress = \(playProgress)")
  56. return playProgress
  57. }
  58. //播放器是否可用
  59. var playerUsable:Bool {
  60. if let audioPlayer = audioPlayer {
  61. return audioPlayer.playerUsable
  62. }
  63. return false
  64. }
  65. var currentURLString:String = ""
  66. var currentLocalURL:URL? = nil
  67. var currentIndexPath:IndexPath? = nil
  68. //加载音乐可能 2-3 秒有结果,停止加载后播放.
  69. private var isStopPlayingAfterLoading:Bool = false
  70. func isPlayURLString(string:String,localURL:URL? = nil,indexPath:IndexPath? = nil) -> Bool {
  71. if currentURLString == string {
  72. if let currentIndexPath = currentIndexPath,
  73. let indexPath = indexPath,
  74. indexPath != currentIndexPath
  75. {
  76. return false
  77. }else if let currentLocalURL = currentLocalURL,
  78. let localURL = localURL,
  79. currentLocalURL != localURL
  80. {
  81. return false
  82. }else{
  83. return true
  84. }
  85. }
  86. return false
  87. }
  88. func loadLoactionURL(url:URL){
  89. self.audioPlayer = TSAudioPlayer(url: url)
  90. }
  91. func playUrlString(_ urlString:String?,localURL:URL? = nil,loop:Bool = false,indexPath:IndexPath? = nil) {
  92. self.stop()
  93. if let urlString = urlString {
  94. // if self.currentURLString == urlStrin {
  95. // self.play()
  96. // return
  97. // }
  98. self.currentURLString = urlString
  99. self.currentLocalURL = localURL
  100. self.currentIndexPath = indexPath
  101. let palyFile:(URL)->Void = { [weak self] url in
  102. guard let self = self else { return }
  103. debugPrint("TSAudioPlayer 正在播放url:\(currentURLString)")
  104. debugPrint("TSAudioPlayer 正在播放path:\(url)")
  105. self.audioPlayer = TSAudioPlayer(url: url)
  106. self.audioPlayer?.setLoop(loop)
  107. if self.audioPlayer?.volume == 0 {
  108. setVolume(volume: 1.0)
  109. }
  110. self.audioPlayer?.currentTimeChanged = { [weak self] currentTime,duration in
  111. guard let self = self else { return }
  112. currentTimeChangedHandle?(currentTime,duration)
  113. changePlayerState(.currentTime(currentTime))
  114. }
  115. self.play()
  116. dePrint(self.audioPlayer?.duration)
  117. self.audioPlayer?.audioPlayerDidFinishHandle = { [weak self] flag in
  118. guard let self = self else { return }
  119. if flag == true, self.audioPlayer?.isLooping == false{
  120. stop()
  121. }
  122. }
  123. }
  124. isStopPlayingAfterLoading = false
  125. if let path = self.currentLocalURL,TSFileManagerTool.fileExists(at: path){
  126. palyFile(path) //播放
  127. }else if let path = TSDownloadManager.getRingLocalURL(urlString: urlString) {
  128. // if let path = TSCommonTool.getCachedURLString(from: urlString,missingEx: "mp3") {
  129. palyFile(path) //播放
  130. }else{
  131. self.changePlayerState(.loading(0.0))
  132. _ = TSDownloadManager.downloadFile(urlString: urlString,missingEx: "mp3") {[weak self] url, error in
  133. guard let self = self else { return }
  134. self.changePlayerState(.loading(1.0))
  135. if isStopPlayingAfterLoading == true || currentURLString != urlString{
  136. isStopPlayingAfterLoading = false
  137. return
  138. }
  139. if let url = url {
  140. palyFile(url) //播放
  141. }else{
  142. //暂停
  143. self.stop()
  144. }
  145. }
  146. // TSCommonTool.downloadAndCacheFile(from: urlString,missingEx: "mp3") { [weak self] path, error in
  147. // guard let self = self else { return }
  148. // self.changePlayerState(.loading(1.0))
  149. //
  150. // if isStopPlayingAfterLoading == true || currentURLString != urlString{
  151. // isStopPlayingAfterLoading = false
  152. // return
  153. // }
  154. //
  155. // if let path = path {
  156. // palyFile(URL(fileURLWithPath: path)) //播放
  157. // }else{
  158. // //暂停
  159. // self.stop()
  160. // }
  161. // }
  162. }
  163. }
  164. }
  165. func play() {
  166. self.audioPlayer?.play()
  167. changePlayerState(.play)
  168. }
  169. func stop() {
  170. self.audioPlayer?.currentTimeChanged = nil
  171. isStopPlayingAfterLoading = true
  172. currentURLString = ""
  173. self.audioPlayer?.stop()
  174. changePlayerState(.stop)
  175. }
  176. func pause() {
  177. isStopPlayingAfterLoading = true
  178. self.audioPlayer?.pause()
  179. changePlayerState(.pause)
  180. }
  181. func setVolume(volume:Float){
  182. self.audioPlayer?.volume = volume
  183. // self.audioPlayer?.setVolume(volume)
  184. changePlayerState(.volume(volume))
  185. }
  186. func changeAudioSwitch()->Float {
  187. let volume:Float = self.audioPlayer?.volume == 0.0 ? 1.0 : 0.0
  188. setVolume(volume: volume)
  189. return volume
  190. }
  191. func changePlayerState(_ state:PlayerState){
  192. if case .currentTime(let time) = state {} else {
  193. debugPrint("TSAudioPlayer changePlayerState=\(state)")
  194. }
  195. currentPlayerState = state
  196. kExecuteOnMainThread{
  197. self.stateChangedHandle?(state)
  198. // NotificationCenter.default.post(name: .kBusinessAudioStateChange, object: nil, userInfo: ["PlayerState": state])
  199. }
  200. }
  201. deinit {
  202. dePrint("TSAudioPlayer TSBusinessAudioPlayer deinit")
  203. }
  204. }
  205. extension TSBusinessAudioPlayer{
  206. struct AudioFileInfo {
  207. let sizeInBytes: UInt64? // 文件大小(字节)
  208. let durationInSeconds: Double? // 音频时长(秒)
  209. // // 计算属性:格式化显示
  210. // var formattedSize: String {
  211. // guard let size = sizeInBytes else { return "未知大小" }
  212. // let formatter = ByteCountFormatter()
  213. // formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB]
  214. // return formatter.string(fromByteCount: Int64(size))
  215. // }
  216. //
  217. // var formattedDuration: String {
  218. // guard let duration = durationInSeconds else { return "未知时长" }
  219. // let formatter = DateComponentsFormatter()
  220. // formatter.unitsStyle = .positional
  221. // formatter.allowedUnits = [.hour, .minute, .second]
  222. // formatter.zeroFormattingBehavior = .pad
  223. // return formatter.string(from: duration) ?? "00:00"
  224. // }
  225. }
  226. static func getAudioFileInfo(path: String) -> AudioFileInfo? {
  227. // 1. 检查URL有效性
  228. guard let url = URL(string: path) else {
  229. print("getAudioFileInfo 无效的URL字符串")
  230. return nil
  231. }
  232. // 2. 检查文件是否存在(仅限本地文件)
  233. guard FileManager.default.fileExists(atPath: url.path) else {
  234. print("getAudioFileInfo 文件不存在或不是本地路径")
  235. return nil
  236. }
  237. // 3. 获取文件大小
  238. let fileSize: UInt64? = {
  239. do {
  240. let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
  241. return attributes[.size] as? UInt64
  242. } catch {
  243. print("获取文件大小失败: \(error.localizedDescription)")
  244. return nil
  245. }
  246. }()
  247. // 4. 获取音频时长
  248. let duration: Double? = {
  249. return getAudioDurationWithAudioFile(url: url)
  250. // let asset = AVURLAsset(url: url)
  251. // let seconds = Double(CMTimeGetSeconds(asset.duration))
  252. // return seconds.isNaN ? nil : seconds
  253. }()
  254. return AudioFileInfo(sizeInBytes: fileSize, durationInSeconds: duration)
  255. }
  256. /// 同步获取音频时长(可能阻塞线程!)
  257. /// 使用 AudioFile 同步获取音频时长
  258. static func getAudioDurationWithAudioFile(url: URL) -> TimeInterval? {
  259. var audioFile: AudioFileID?
  260. let status = AudioFileOpenURL(url as CFURL, .readPermission, 0, &audioFile)
  261. guard status == noErr, let file = audioFile else {
  262. print("⚠️ 打开音频文件失败: \(status)")
  263. return nil
  264. }
  265. // 获取音频时长(单位:秒)
  266. var duration: Float64 = 0
  267. var propertySize = UInt32(MemoryLayout.size(ofValue: duration))
  268. let durationStatus = AudioFileGetProperty(
  269. file,
  270. kAudioFilePropertyEstimatedDuration,
  271. &propertySize,
  272. &duration
  273. )
  274. AudioFileClose(file)
  275. return durationStatus == noErr ? duration : nil
  276. }
  277. }