TSBusinessAudioPlayer.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. self.currentURLString = urlString
  95. self.currentLocalURL = localURL
  96. self.currentIndexPath = indexPath
  97. let palyFile:(URL)->Void = { [weak self] url in
  98. guard let self = self else { return }
  99. debugPrint("TSAudioPlayer 正在播放url:\(currentURLString)")
  100. debugPrint("TSAudioPlayer 正在播放path:\(url)")
  101. self.audioPlayer = TSAudioPlayer(url: url)
  102. self.audioPlayer?.setLoop(loop)
  103. if self.audioPlayer?.volume == 0 {
  104. setVolume(volume: 1.0)
  105. }
  106. self.audioPlayer?.currentTimeChanged = { [weak self] currentTime,duration in
  107. guard let self = self else { return }
  108. currentTimeChangedHandle?(currentTime,duration)
  109. changePlayerState(.currentTime(currentTime))
  110. }
  111. self.play()
  112. dePrint(self.audioPlayer?.duration)
  113. self.audioPlayer?.audioPlayerDidFinishHandle = { [weak self] flag in
  114. guard let self = self else { return }
  115. if flag == true, self.audioPlayer?.isLooping == false{
  116. stop()
  117. }
  118. }
  119. }
  120. isStopPlayingAfterLoading = false
  121. if let path = self.currentLocalURL,TSFileManagerTool.fileExists(at: path){
  122. palyFile(path) //播放
  123. }else{
  124. self.changePlayerState(.loading(0.0))
  125. _ = ASDownloadManager.getDownLoadRing(urlString: urlString, progressHandler: { progress in
  126. dePrint("ASDownloadManager.etDownLoadRing progress = \(progress)")
  127. }, complete: {[weak self] url, success in
  128. guard let self = self else { return }
  129. self.changePlayerState(.loading(1.0))
  130. if isStopPlayingAfterLoading == true || currentURLString != urlString{
  131. isStopPlayingAfterLoading = false
  132. return
  133. }
  134. if let url = url {
  135. palyFile(url) //播放
  136. }else{
  137. //暂停
  138. self.stop()
  139. }
  140. })
  141. }
  142. }
  143. }
  144. func play() {
  145. self.audioPlayer?.play()
  146. changePlayerState(.play)
  147. }
  148. func stop() {
  149. self.audioPlayer?.currentTimeChanged = nil
  150. isStopPlayingAfterLoading = true
  151. currentURLString = ""
  152. self.audioPlayer?.stop()
  153. changePlayerState(.stop)
  154. }
  155. func pause() {
  156. isStopPlayingAfterLoading = true
  157. self.audioPlayer?.pause()
  158. changePlayerState(.pause)
  159. }
  160. func setVolume(volume:Float){
  161. self.audioPlayer?.volume = volume
  162. changePlayerState(.volume(volume))
  163. }
  164. func changeAudioSwitch()->Float {
  165. let volume:Float = self.audioPlayer?.volume == 0.0 ? 1.0 : 0.0
  166. setVolume(volume: volume)
  167. return volume
  168. }
  169. func changePlayerState(_ state:PlayerState){
  170. if case .currentTime(let time) = state {} else {
  171. debugPrint("TSAudioPlayer changePlayerState=\(state)")
  172. }
  173. currentPlayerState = state
  174. kExecuteOnMainThread{
  175. self.stateChangedHandle?(state)
  176. // NotificationCenter.default.post(name: .kBusinessAudioStateChange, object: nil, userInfo: ["PlayerState": state])
  177. }
  178. }
  179. deinit {
  180. dePrint("TSAudioPlayer TSBusinessAudioPlayer deinit")
  181. }
  182. }
  183. extension TSBusinessAudioPlayer{
  184. struct AudioFileInfo {
  185. let sizeInBytes: UInt64? // 文件大小(字节)
  186. let durationInSeconds: Double? // 音频时长(秒)
  187. let songName: String? // 音频时长(秒)
  188. }
  189. static func getAudioFileInfo(path: String) -> AudioFileInfo? {
  190. // 1. 检查URL有效性
  191. guard let url = URL(string: path) else {
  192. print("getAudioFileInfo 无效的URL字符串")
  193. return nil
  194. }
  195. // 2. 检查文件是否存在(仅限本地文件)
  196. guard FileManager.default.fileExists(atPath: url.path) else {
  197. print("getAudioFileInfo 文件不存在或不是本地路径")
  198. return nil
  199. }
  200. // 3. 获取文件大小
  201. let fileSize: UInt64? = {
  202. do {
  203. let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
  204. return attributes[.size] as? UInt64
  205. } catch {
  206. print("获取文件大小失败: \(error.localizedDescription)")
  207. return nil
  208. }
  209. }()
  210. // 4. 获取音频时长
  211. let duration: Double? = {
  212. return getAudioDurationWithAudioFile(url: url)
  213. }()
  214. // 5. 获取音频名称
  215. let songName: String? = {
  216. return getSongNameFromLocalFile(fileURL: url)
  217. }()
  218. return AudioFileInfo(sizeInBytes: fileSize, durationInSeconds: duration,songName: songName)
  219. }
  220. /// 同步获取音频时长(可能阻塞线程!)
  221. /// 使用 AudioFile 同步获取音频时长
  222. static func getAudioDurationWithAudioFile(url: URL) -> TimeInterval? {
  223. var audioFile: AudioFileID?
  224. let status = AudioFileOpenURL(url as CFURL, .readPermission, 0, &audioFile)
  225. guard status == noErr, let file = audioFile else {
  226. print("⚠️ 打开音频文件失败: \(status)")
  227. return nil
  228. }
  229. // 获取音频时长(单位:秒)
  230. var duration: Float64 = 0
  231. var propertySize = UInt32(MemoryLayout.size(ofValue: duration))
  232. let durationStatus = AudioFileGetProperty(
  233. file,
  234. kAudioFilePropertyEstimatedDuration,
  235. &propertySize,
  236. &duration
  237. )
  238. AudioFileClose(file)
  239. return durationStatus == noErr ? duration : nil
  240. }
  241. //获取音频的名称
  242. static func getSongNameFromLocalFile(fileURL: URL) -> String? {
  243. // 1. 检查文件是否存在
  244. guard FileManager.default.fileExists(atPath: fileURL.path) else {
  245. print("文件不存在")
  246. return nil
  247. }
  248. // 2. 创建 AVAsset 对象(代表音频文件)
  249. let asset = AVAsset(url: fileURL)
  250. // 3. 同步读取元数据(适用于本地文件)
  251. var songName: String? = nil
  252. let metadataFormats = asset.availableMetadataFormats
  253. for format in metadataFormats {
  254. let metadata = asset.metadata(forFormat: format)
  255. // 4. 遍历元数据项,查找歌曲标题
  256. for item in metadata {
  257. if item.commonKey == .commonKeyTitle, // 标准键:标题
  258. let value = item.value as? String { // 确保值是字符串
  259. songName = value
  260. break
  261. }
  262. // 额外检查 ID3 标签(某些 MP3 文件可能用非标准键)
  263. if item.key as? String == "TIT2", // ID3v2 标题键
  264. let value = item.value as? String {
  265. songName = value
  266. break
  267. }
  268. }
  269. if songName != nil { break }
  270. }
  271. // 5. 返回结果(若未找到,则尝试从文件名推断)
  272. return songName ?? fileURL.deletingPathExtension().lastPathComponent
  273. }
  274. }