TSBusinessAudioPlayer.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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 playUrlString(_ urlString:String?,localURL:URL? = nil,loop:Bool = false,indexPath:IndexPath? = nil) {
  89. self.stop()
  90. if let urlString = urlString {
  91. // if self.currentURLString == urlStrin {
  92. // self.play()
  93. // return
  94. // }
  95. self.currentURLString = urlString
  96. self.currentLocalURL = localURL
  97. self.currentIndexPath = indexPath
  98. let palyFile:(URL)->Void = { [weak self] url in
  99. guard let self = self else { return }
  100. debugPrint("TSAudioPlayer 正在播放url:\(currentURLString)")
  101. debugPrint("TSAudioPlayer 正在播放path:\(url)")
  102. self.audioPlayer = TSAudioPlayer(url: url)
  103. self.audioPlayer?.setLoop(loop)
  104. if self.audioPlayer?.volume == 0 {
  105. setVolume(volume: 1.0)
  106. }
  107. self.audioPlayer?.currentTimeChanged = { [weak self] currentTime,duration in
  108. guard let self = self else { return }
  109. currentTimeChangedHandle?(currentTime,duration)
  110. changePlayerState(.currentTime(currentTime))
  111. }
  112. self.play()
  113. dePrint(self.audioPlayer?.duration)
  114. self.audioPlayer?.audioPlayerDidFinishHandle = { [weak self] flag in
  115. guard let self = self else { return }
  116. if flag == true, self.audioPlayer?.isLooping == false{
  117. stop()
  118. }
  119. }
  120. }
  121. isStopPlayingAfterLoading = false
  122. if let path = self.currentLocalURL,TSFileManagerTool.fileExists(at: path){
  123. palyFile(path) //播放
  124. }else if let path = TSDownloadManager.getRingLocalURL(urlString: urlString) {
  125. // if let path = TSCommonTool.getCachedURLString(from: urlString,missingEx: "mp3") {
  126. palyFile(path) //播放
  127. }else{
  128. self.changePlayerState(.loading(0.0))
  129. _ = TSDownloadManager.downloadFile(urlString: urlString,missingEx: "mp3") {[weak self] url, error in
  130. guard let self = self else { return }
  131. self.changePlayerState(.loading(1.0))
  132. if isStopPlayingAfterLoading == true || currentURLString != urlString{
  133. isStopPlayingAfterLoading = false
  134. return
  135. }
  136. if let url = url {
  137. palyFile(url) //播放
  138. }else{
  139. //暂停
  140. self.stop()
  141. }
  142. }
  143. // TSCommonTool.downloadAndCacheFile(from: urlString,missingEx: "mp3") { [weak self] path, error in
  144. // guard let self = self else { return }
  145. // self.changePlayerState(.loading(1.0))
  146. //
  147. // if isStopPlayingAfterLoading == true || currentURLString != urlString{
  148. // isStopPlayingAfterLoading = false
  149. // return
  150. // }
  151. //
  152. // if let path = path {
  153. // palyFile(URL(fileURLWithPath: path)) //播放
  154. // }else{
  155. // //暂停
  156. // self.stop()
  157. // }
  158. // }
  159. }
  160. }
  161. }
  162. func play() {
  163. self.audioPlayer?.play()
  164. changePlayerState(.play)
  165. }
  166. func stop() {
  167. self.audioPlayer?.currentTimeChanged = nil
  168. isStopPlayingAfterLoading = true
  169. currentURLString = ""
  170. self.audioPlayer?.stop()
  171. changePlayerState(.stop)
  172. }
  173. func pause() {
  174. isStopPlayingAfterLoading = true
  175. self.audioPlayer?.pause()
  176. changePlayerState(.pause)
  177. }
  178. func setVolume(volume:Float){
  179. self.audioPlayer?.volume = volume
  180. // self.audioPlayer?.setVolume(volume)
  181. changePlayerState(.volume(volume))
  182. }
  183. func changeAudioSwitch()->Float {
  184. let volume:Float = self.audioPlayer?.volume == 0.0 ? 1.0 : 0.0
  185. setVolume(volume: volume)
  186. return volume
  187. }
  188. func changePlayerState(_ state:PlayerState){
  189. if case .currentTime(let time) = state {} else {
  190. debugPrint("TSAudioPlayer changePlayerState=\(state)")
  191. }
  192. currentPlayerState = state
  193. kExecuteOnMainThread{
  194. self.stateChangedHandle?(state)
  195. // NotificationCenter.default.post(name: .kBusinessAudioStateChange, object: nil, userInfo: ["PlayerState": state])
  196. }
  197. }
  198. deinit {
  199. dePrint("TSAudioPlayer TSBusinessAudioPlayer deinit")
  200. }
  201. }
  202. extension TSBusinessAudioPlayer{
  203. struct AudioFileInfo {
  204. let sizeInBytes: UInt64? // 文件大小(字节)
  205. let durationInSeconds: Double? // 音频时长(秒)
  206. // // 计算属性:格式化显示
  207. // var formattedSize: String {
  208. // guard let size = sizeInBytes else { return "未知大小" }
  209. // let formatter = ByteCountFormatter()
  210. // formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB]
  211. // return formatter.string(fromByteCount: Int64(size))
  212. // }
  213. //
  214. // var formattedDuration: String {
  215. // guard let duration = durationInSeconds else { return "未知时长" }
  216. // let formatter = DateComponentsFormatter()
  217. // formatter.unitsStyle = .positional
  218. // formatter.allowedUnits = [.hour, .minute, .second]
  219. // formatter.zeroFormattingBehavior = .pad
  220. // return formatter.string(from: duration) ?? "00:00"
  221. // }
  222. }
  223. static func getAudioFileInfo(path: String) -> AudioFileInfo? {
  224. // 1. 检查URL有效性
  225. guard let url = URL(string: path) else {
  226. print("无效的URL字符串")
  227. return nil
  228. }
  229. // 2. 检查文件是否存在(仅限本地文件)
  230. guard FileManager.default.fileExists(atPath: url.path) else {
  231. print("文件不存在或不是本地路径")
  232. return nil
  233. }
  234. // 3. 获取文件大小
  235. let fileSize: UInt64? = {
  236. do {
  237. let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
  238. return attributes[.size] as? UInt64
  239. } catch {
  240. print("获取文件大小失败: \(error.localizedDescription)")
  241. return nil
  242. }
  243. }()
  244. // 4. 获取音频时长
  245. let duration: Double? = {
  246. return getAudioDurationWithAudioFile(url: url)
  247. // let asset = AVURLAsset(url: url)
  248. // let seconds = Double(CMTimeGetSeconds(asset.duration))
  249. // return seconds.isNaN ? nil : seconds
  250. }()
  251. return AudioFileInfo(sizeInBytes: fileSize, durationInSeconds: duration)
  252. }
  253. /// 同步获取音频时长(可能阻塞线程!)
  254. /// 使用 AudioFile 同步获取音频时长
  255. static func getAudioDurationWithAudioFile(url: URL) -> TimeInterval? {
  256. var audioFile: AudioFileID?
  257. let status = AudioFileOpenURL(url as CFURL, .readPermission, 0, &audioFile)
  258. guard status == noErr, let file = audioFile else {
  259. print("⚠️ 打开音频文件失败: \(status)")
  260. return nil
  261. }
  262. // 获取音频时长(单位:秒)
  263. var duration: Float64 = 0
  264. var propertySize = UInt32(MemoryLayout.size(ofValue: duration))
  265. let durationStatus = AudioFileGetProperty(
  266. file,
  267. kAudioFilePropertyEstimatedDuration,
  268. &propertySize,
  269. &duration
  270. )
  271. AudioFileClose(file)
  272. return durationStatus == noErr ? duration : nil
  273. }
  274. }