PQPlayerViewModel.swift 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. //
  2. // PQPlayerViewModel.swift
  3. // PQSpeed
  4. //
  5. // Created by ak on 2021/1/27.
  6. // Copyright © 2021 BytesFlow. All rights reserved.
  7. // 视频渲染相关逻辑方法
  8. import RealmSwift
  9. import UIKit
  10. open class PQPlayerViewModel: NSObject {
  11. /// 根据贴纸信息转成种 fitler ,编辑 ,总览,导出共用
  12. /// - Parameter parts: filter 组
  13. public class func partModelToFilters(sections: [PQEditSectionModel], inputSize: CGSize = .zero) -> ([ImageProcessingOperation], [URL]) {
  14. // 所有段的声音位置
  15. var audioFiles: Array = Array<URL>.init()
  16. // 所有滤镜数组
  17. var filters: Array = Array<ImageProcessingOperation>.init()
  18. /*
  19. 一, 默认素材时长
  20. 图片:2S
  21. 视频: X1倍速 播一边
  22. GIF: X1倍速 播一边
  23. 二,资源适配规则
  24. 1,有配音声音 也就是有文字
  25. 适配系数 = 配音时长/视觉总时长
  26. 视觉元素最终时长 = 视觉元素原时长 * 适配系数
  27. 2,无配音无文字
  28. 使用素材的默认时长
  29. 3,无配音有文字
  30. 适配系数 = 视频总时长/文字总时长
  31. 文字每一句的实际时长 = 文字分段落的原始时长 * 适配系统
  32. */
  33. // 返回时自动预览开始播放 添加有贴纸开始自动播放
  34. var partTotaDuration: Float64 = 0
  35. for section in sections {
  36. autoreleasepool {
  37. // 优先使用 mix audio
  38. if section.mixEmptyAuidoFilePath.count > 0 {
  39. audioFiles.append(URL(fileURLWithPath: documensDirectory + section.mixEmptyAuidoFilePath.replacingOccurrences(of: documensDirectory, with: "")))
  40. BFLog(message: "add mixEmptyAuidoFilePath mixEmptyAuidoFilePath")
  41. } else {
  42. if section.audioFilePath.count > 0 {
  43. audioFiles.append(URL(fileURLWithPath: documensDirectory + section.audioFilePath.replacingOccurrences(of: documensDirectory, with: "")))
  44. BFLog(message: "add audioFilePath audioFilePath")
  45. }
  46. }
  47. var totalDuration: Float64 = 0
  48. // 根据已经选择的贴纸类型创建各自filters
  49. for sticker in section.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
  50. autoreleasepool {
  51. sticker.timelineIn = totalDuration + partTotaDuration
  52. totalDuration = totalDuration + sticker.aptDuration
  53. sticker.timelineOut = totalDuration + partTotaDuration
  54. BFLog(message: "创建 filter start :\(sticker.timelineIn) end :\(sticker.timelineOut) type is \(sticker.type)")
  55. if sticker.type == StickerType.IMAGE.rawValue {
  56. let imageFilter = PQImageFilter(sticker: sticker)
  57. filters.append(imageFilter)
  58. } else if sticker.type == StickerType.VIDEO.rawValue {
  59. let videoFilter = PQMovieFilter(movieSticker: sticker)
  60. filters.append(videoFilter)
  61. } else if sticker.type == StickerType.GIF.rawValue {
  62. let gifFilter = PQGifFilter(sticker: sticker)
  63. filters.append(gifFilter)
  64. }
  65. }
  66. }
  67. // 字幕如果是多段的 ,字幕的开始时间是 前几段 part duration 总时长 所以要重新计算
  68. var newSubtitleData: [PQEditSubTitleModel] = Array()
  69. // 如果有录制声音转的字幕优先使用,在使用人工输入文字字幕s
  70. let recorderSubtitle = List<PQEditSubTitleModel>()
  71. if section.sectionTimeline?.visionTrack?.getSubtitleMatraislInfo() != nil {
  72. for subtitleMatraislInfo in section.sectionTimeline!.visionTrack!.getSubtitleMatraislInfo() {
  73. BFLog(message: "有录音字幕")
  74. let editSubTitleModel = PQEditSubTitleModel()
  75. editSubTitleModel.text = subtitleMatraislInfo.subtitleInfo?.text ?? ""
  76. editSubTitleModel.timelineIn = subtitleMatraislInfo.timelineIn
  77. editSubTitleModel.timelineOut = subtitleMatraislInfo.timelineOut
  78. recorderSubtitle.append(editSubTitleModel)
  79. }
  80. }
  81. for (index, subTitle) in recorderSubtitle.count > 0 ? recorderSubtitle.enumerated() : section.subTitles.enumerated() {
  82. BFLog(message: "有配音字幕")
  83. let newSubtitle = PQEditSubTitleModel()
  84. newSubtitle.timelineIn = subTitle.timelineIn
  85. newSubtitle.timelineOut = subTitle.timelineOut
  86. newSubtitle.text = subTitle.text.replacingOccurrences(of: "\n", with: "")
  87. BFLog(message: "第\(index)个字幕 subTitle old start : \(newSubtitle.timelineIn) end: \(newSubtitle.timelineOut) text: \(newSubtitle.text)")
  88. // subtitle duration
  89. let duration: Float64 = (newSubtitle.timelineOut - newSubtitle.timelineIn)
  90. newSubtitle.timelineIn = partTotaDuration + newSubtitle.timelineIn
  91. newSubtitle.timelineOut = newSubtitle.timelineIn + duration
  92. BFLog(message: "第\(index)个字幕 subTitle new start : \(newSubtitle.timelineIn) end: \(newSubtitle.timelineOut) text: \(newSubtitle.text)")
  93. newSubtitleData.append(newSubtitle)
  94. // let subTitle = PQSubTitleFilter(st: [newSubtitle], isBig: section.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count == 0, inputSize: inputSize)
  95. // filters.append(subTitle)
  96. }
  97. // 无视觉素材是大字幕方式 有数据在初始字幕filter
  98. // for subtitle in newSubtitleData{
  99. // let subTitleFilter = PQSubTitleFilter(st: [newSubtitleData[0]], isBig: section.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count == 0,inputSize: inputSize)
  100. // filters.append(subTitleFilter)
  101. // }
  102. if newSubtitleData.count > 0 {
  103. let subTitleFilter = PQSubTitleFilter(st: newSubtitleData, isBig: section.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count == 0, inputSize: inputSize)
  104. filters.append(subTitleFilter)
  105. // DispatchQueue.main.async {
  106. // }
  107. }
  108. var tempDuration = section.allStickerAptDurationNoRound() == 0 ? section.sectionDuration : section.allStickerAptDurationNoRound()
  109. BFLog(message: "tempDuration 1 is \(tempDuration)")
  110. // 如果音频时长是经过加空音频 加长后的 要使用长音频
  111. if section.mixEmptyAuidoFilePath.count > 0 {
  112. BFLog(message: "有拼接的数据")
  113. let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + section.mixEmptyAuidoFilePath), options: avAssertOptions)
  114. if tempDuration <= audioAsset.duration.seconds {
  115. tempDuration = audioAsset.duration.seconds
  116. } else {
  117. BFLog(message: "音频文件时长为0?")
  118. }
  119. }
  120. BFLog(message: "tempDuration 2 is \(tempDuration)")
  121. partTotaDuration = partTotaDuration + tempDuration
  122. }
  123. BFLog(message: "audioFiles 声音文件总数\(audioFiles.count)")
  124. }
  125. return (filters, audioFiles)
  126. }
  127. public class func calculationStickAptDurationReal(currentPart: PQEditSectionModel, completeHander: @escaping (_ returnPart: PQEditSectionModel?) -> Void) {
  128. // XXXXXX如果 没有选择发音人 就算有自动的转的声音文件也不按声音时长计算,都是素材原有时长
  129. // let audioTotalDuration: Float64 = Float64(currentPart.sectionDuration)
  130. // 1,计算贴纸所有原始时长
  131. var stickerTotalDuration: Float64 = 0
  132. for sticker in currentPart.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
  133. var stikcerDuration: Float64 = sticker.duration
  134. if sticker.videoIsCrop() {
  135. BFLog(message: "这个视频有裁剪 \(sticker.locationPath)")
  136. stikcerDuration = sticker.out - sticker.model_in
  137. }
  138. stickerTotalDuration = stickerTotalDuration + stikcerDuration
  139. }
  140. // 真人声音时长
  141. var realAudioDuration = 0.0
  142. BFLog(message: "currentPart.audioFilePath is \(currentPart.audioFilePath)")
  143. if currentPart.audioFilePath.count > 0 {
  144. let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + currentPart.audioFilePath), options: avAssertOptions)
  145. realAudioDuration = audioAsset.duration.seconds
  146. }
  147. BFLog(message: "所有素材的总时 \(stickerTotalDuration) 文字转语音的时长:\(realAudioDuration)")
  148. if stickerTotalDuration == 0 && realAudioDuration == 0 {
  149. DispatchQueue.main.async {
  150. completeHander(currentPart)
  151. }
  152. return
  153. }
  154. // 所有视频素材原有时长 > 音频文件(字幕时长 有可能有声音,有可能没有声音自动转的)
  155. if stickerTotalDuration - realAudioDuration > 0.01 {
  156. // 要创建空文件加长原有声音
  157. let tool = PQCreateEmptyWAV(sampleRate: 8000,
  158. channel: 1,
  159. duration: stickerTotalDuration - realAudioDuration,
  160. bit: 16)
  161. let timeInterval: TimeInterval = Date().timeIntervalSince1970
  162. var audioFileTempPath = exportAudiosDirectory
  163. if !directoryIsExists(dicPath: audioFileTempPath) {
  164. BFLog(message: "文件夹不存在 \(audioFileTempPath)")
  165. createDirectory(path: audioFileTempPath)
  166. }
  167. audioFileTempPath.append("empty_\(timeInterval).wav")
  168. tool.createEmptyWAVFile(url: URL(fileURLWithPath: audioFileTempPath)) { _ in
  169. var tempUrls: Array = NSArray() as! [URL]
  170. if currentPart.audioFilePath.count > 0 {
  171. BFLog(message: "currentPart.audioFilePath is \(String(describing: currentPart.audioFilePath))")
  172. tempUrls.append(URL(fileURLWithPath: documensDirectory + currentPart.audioFilePath))
  173. }
  174. tempUrls.append(URL(fileURLWithPath: audioFileTempPath))
  175. PQPlayerViewModel.mergeAudios(urls: tempUrls) { completURL in
  176. if completURL == nil {
  177. BFLog(message: "合并文件有问题!")
  178. return
  179. }
  180. // file:///var/mobile/Containers/Data/Application/2A008644-31A6-4D7E-930B-F1099F36D577/Documents/Resource/ExportAudios/merge_1618817019.789495.m4a
  181. let audioAsset = AVURLAsset(url: completURL!, options: avAssertOptions)
  182. BFLog(message: "completURL mix : \(String(describing: completURL)) audioFilePath durtion \(audioAsset.duration.seconds)")
  183. currentPart.mixEmptyAuidoFilePath = completURL!.absoluteString.replacingOccurrences(of: documensDirectory, with: "").replacingOccurrences(of: "file://", with: "")
  184. currentPart.sectionDuration = audioAsset.duration.seconds
  185. BFLog(message: "stickerTotalDuration is \(stickerTotalDuration) mixEmptyAuidoFilePath 设置后 是\(currentPart.mixEmptyAuidoFilePath) 时长是:\(currentPart.sectionDuration)")
  186. // 1.2)计算贴纸的逻辑显示时长
  187. for sticker in currentPart.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
  188. var tempDuration = sticker.duration
  189. if sticker.videoIsCrop() {
  190. tempDuration = sticker.out - sticker.model_in
  191. BFLog(message: "这个视频有裁剪后:\(tempDuration) \(String(describing: sticker.locationPath))")
  192. }
  193. sticker.aptDuration = tempDuration
  194. }
  195. DispatchQueue.main.async {
  196. completeHander(currentPart)
  197. }
  198. }
  199. }
  200. } else {
  201. // 这种情况下 mixEmptyAuidoFilePath 应该为空
  202. currentPart.mixEmptyAuidoFilePath = ""
  203. // currentPart.audioFilePath = ""
  204. currentPart.sectionDuration = realAudioDuration
  205. // 1.1)计算系数
  206. let coefficient: Float64 = realAudioDuration / stickerTotalDuration
  207. BFLog(message: "系数 is: \(coefficient) stickerTotalDuration is \(stickerTotalDuration) audioTotalDuration is :\(realAudioDuration)")
  208. // 1.2)计算贴纸的逻辑显示时长
  209. for sticker in currentPart.sectionTimeline!.visionTrack!.getEnableVisionTrackMaterials() {
  210. // 如果是视频素材有过裁剪 就使用裁剪时长
  211. var tempDuration = sticker.duration
  212. if sticker.videoIsCrop() {
  213. tempDuration = sticker.out - sticker.model_in
  214. BFLog(message: "这个视频有裁剪后:\(tempDuration) \(String(describing: sticker.locationPath))")
  215. }
  216. // 如果没有音频 系数为0时 使用素材的原始时长
  217. sticker.aptDuration = (coefficient == 0) ? tempDuration : tempDuration * coefficient
  218. }
  219. DispatchQueue.main.async {
  220. completeHander(currentPart)
  221. }
  222. }
  223. }
  224. // 计算所有贴纸的逻辑时长
  225. public class func calculationStickAptDuration(currentPart: PQEditSectionModel, createFirst: Bool = true, completeHander: @escaping (_ returnPart: PQEditSectionModel?) -> Void) {
  226. if currentPart.sectionType == "global" {
  227. BFLog(message: "音频段落不处理计算")
  228. return
  229. }
  230. // 从素材详细界面返回 有可能是删除素材操作 这时如果没有选择发音人同时没有录音和导入数据要重新计算空文件时长
  231. let speeckAudioTrackModel = currentPart.sectionTimeline?.audioTrack?.getAudioTrackModel(voiceType: VOICETYPT.SPEECH.rawValue)
  232. let localAudioTrackModel = currentPart.sectionTimeline?.audioTrack?.getAudioTrackModel(voiceType: VOICETYPT.LOCAL.rawValue)
  233. if !currentPart.haveSelectVoice(), speeckAudioTrackModel == nil, localAudioTrackModel == nil, createFirst {
  234. // 只有视觉素材 没有文字
  235. if currentPart.sectionText.count == 0 {
  236. // 根据视觉的总时长生成空音频数据
  237. var timeCount: Double = 0
  238. for sticker in (currentPart.sectionTimeline!.visionTrack?.getEnableVisionTrackMaterials())! {
  239. if sticker.out != 0 || sticker.model_in == 0 {
  240. timeCount = timeCount + (sticker.out - sticker.model_in)
  241. } else {
  242. timeCount = timeCount + sticker.aptDuration
  243. }
  244. }
  245. BFLog(message: "计算视觉的总时长 \(timeCount)")
  246. if timeCount > 0 {
  247. let tool = PQCreateEmptyWAV(sampleRate: 8000,
  248. channel: 1,
  249. duration: timeCount,
  250. bit: 16)
  251. let timeInterval: TimeInterval = Date().timeIntervalSince1970
  252. var audioFileTempPath = exportAudiosDirectory
  253. if !directoryIsExists(dicPath: audioFileTempPath) {
  254. BFLog(message: "文件夹不存在 \(audioFileTempPath)")
  255. createDirectory(path: audioFileTempPath)
  256. }
  257. audioFileTempPath.append("empty_\(timeInterval).wav")
  258. tool.createEmptyWAVFile(url: URL(fileURLWithPath: audioFileTempPath)) { _ in
  259. currentPart.audioFilePath = audioFileTempPath.replacingOccurrences(of: documensDirectory, with: "")
  260. calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
  261. }
  262. } else {
  263. calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
  264. }
  265. } else {
  266. calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
  267. }
  268. } else {
  269. calculationStickAptDurationReal(currentPart: currentPart, completeHander: completeHander)
  270. }
  271. }
  272. // 首尾拼接音频文件
  273. /*
  274. 因为在对音频做合并或者裁切的时候生成的音频格式是m4a的,但是m4a转成mp3会损坏音频格式,所以我当时采用先把m4a转为wav,再用wav转成mp3。
  275. */
  276. /// 合并声音
  277. /// - Parameter urls: 所有音频的URL 是全路径方便复用
  278. /// - Parameter completeHander: 返回的 URL 全路径的 URL 如果要保存替换掉前缀
  279. public class func mergeAudios(urls: [URL], completeHander: @escaping (_ fileURL: URL?) -> Void) {
  280. let timeInterval: TimeInterval = Date().timeIntervalSince1970
  281. let composition = AVMutableComposition()
  282. var totalDuration: CMTime = .zero
  283. BFLog(message: "合并文件总数 \(urls.count)")
  284. for urlStr in urls {
  285. BFLog(message: "合并的文件地址: \(urlStr)")
  286. let audioAsset = AVURLAsset(url: urlStr, options: avAssertOptions)
  287. let tracks1 = audioAsset.tracks(withMediaType: .audio)
  288. if tracks1.count == 0 {
  289. BFLog(message: "音频数据无效不进行合并,所有任务结束要确保输入的数据都正常! \(urlStr)")
  290. break
  291. }
  292. let assetTrack1: AVAssetTrack = tracks1[0]
  293. let duration1: CMTime = assetTrack1.timeRange.duration
  294. BFLog(message: "每一个文件的 duration \(CMTimeGetSeconds(duration1))")
  295. let timeRange1 = CMTimeRangeMake(start: .zero, duration: duration1)
  296. let compositionAudioTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID())!
  297. do {
  298. //
  299. try compositionAudioTrack.insertTimeRange(timeRange1, of: assetTrack1, at: totalDuration)
  300. } catch {
  301. BFLog(message: "error is \(error)")
  302. }
  303. totalDuration = CMTimeAdd(totalDuration, audioAsset.duration)
  304. }
  305. if CMTimeGetSeconds(totalDuration) == 0 {
  306. BFLog(message: "所有数据无效")
  307. completeHander(nil)
  308. return
  309. } else {
  310. // 拼接声音文件 完成
  311. BFLog(message: "totalDuration is \(CMTimeGetSeconds(totalDuration))")
  312. }
  313. let assetExport = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
  314. BFLog(message: "assetExport.supportedFileTypes is \(String(describing: assetExport?.supportedFileTypes))")
  315. assetExport?.outputFileType = .m4a
  316. // XXXX 注意文件名的后缀要和outputFileType 一致 否则会导出失败
  317. var audioFilePath = exportAudiosDirectory
  318. if !directoryIsExists(dicPath: audioFilePath) {
  319. BFLog(message: "文件夹不存在")
  320. createDirectory(path: audioFilePath)
  321. }
  322. audioFilePath.append("merge_\(timeInterval).m4a")
  323. let fileUrl = URL(fileURLWithPath: audioFilePath)
  324. assetExport?.outputURL = fileUrl
  325. assetExport?.exportAsynchronously {
  326. if assetExport!.status == .completed {
  327. // 85.819125
  328. let audioAsset = AVURLAsset(url: fileUrl, options: avAssertOptions)
  329. BFLog(message: "拼接声音文件 完成 \(fileUrl) 时长is \(CMTimeGetSeconds(audioAsset.duration))")
  330. completeHander(fileUrl)
  331. } else {
  332. print("拼接出错 \(String(describing: assetExport?.error))")
  333. completeHander(URL(string: ""))
  334. }
  335. }
  336. }
  337. /// 根据选择的画布类型计算播放器显示的位置和大小
  338. /// - Parameters:
  339. /// - editProjectModel: 项目数据
  340. /// - showType: 显示类型 1, 编辑界面 2,总览界面
  341. /// - Returns: 显示的坐标和位置
  342. public class func getShowCanvasRect(editProjectModel: PQEditProjectModel?, showType: Int, playerViewHeight: CGFloat = 216 / 667 * cScreenHeigth) -> CGRect {
  343. if editProjectModel == nil {
  344. BFLog(message: "editProjectModel is error")
  345. return CGRect()
  346. }
  347. // UI播放器的最大高度,同时最大宽度为设备宽度
  348. var showRect: CGRect = CGRect(x: (cScreenWidth - playerViewHeight) / 2, y: 0, width: playerViewHeight, height: playerViewHeight)
  349. let canvasType: Int = editProjectModel!.sData!.videoMetaData!.canvasType
  350. if showType == 1 { // 编辑界面
  351. switch canvasType {
  352. case videoCanvasType.origin.rawValue:
  353. // 使用有效素材第一位
  354. var firstModel: PQEditVisionTrackMaterialsModel?
  355. for part in editProjectModel!.sData!.sections {
  356. if part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
  357. firstModel = part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().first
  358. break
  359. }
  360. }
  361. if firstModel != nil {
  362. if firstModel?.width == 0 || firstModel?.height == 0 {
  363. BFLog(message: "!!!!!!!!!!!素材宽高有问题!!!!!!!!!!!")
  364. }
  365. BFLog(message: "第一个有效素材的大小 \(String(describing: firstModel?.width)) \(String(describing: firstModel?.height))")
  366. let ratioMaterial: Float = (firstModel?.width ?? 0) / (firstModel?.height ?? 0)
  367. if ratioMaterial > 1 {
  368. // 横屏
  369. var tempPlayerHeight = cScreenWidth * CGFloat(firstModel!.height / firstModel!.width)
  370. var scale: CGFloat = 1.0
  371. if tempPlayerHeight > playerViewHeight {
  372. scale = CGFloat(playerViewHeight) / CGFloat(tempPlayerHeight)
  373. tempPlayerHeight = tempPlayerHeight * scale
  374. }
  375. showRect = CGRect(x: (cScreenWidth - cScreenWidth * scale) / 2, y: (playerViewHeight - tempPlayerHeight) / 2, width: cScreenWidth * scale, height: tempPlayerHeight)
  376. } else {
  377. // 竖屏
  378. let playerViewWidth = (CGFloat(firstModel!.width) / CGFloat(firstModel!.height)) * playerViewHeight
  379. showRect = CGRect(x: (cScreenWidth - playerViewWidth) / 2, y: 0, width: playerViewWidth, height: playerViewHeight)
  380. }
  381. } else {
  382. // 没有视觉素材时,只有文字,语音时,默认为原始但显示的 VIEW 为 1:1
  383. showRect = CGRect(x: (cScreenWidth - playerViewHeight) / 2, y: 0, width: playerViewHeight, height: playerViewHeight)
  384. }
  385. case videoCanvasType.oneToOne.rawValue:
  386. showRect = CGRect(x: (cScreenWidth - playerViewHeight) / 2, y: 0, width: playerViewHeight, height: playerViewHeight)
  387. case videoCanvasType.nineToSixteen.rawValue:
  388. showRect = CGRect(x: (cScreenWidth - playerViewHeight * (9.0 / 16.0)) / 2, y: 0, width: playerViewHeight * (9.0 / 16.0), height: playerViewHeight)
  389. case videoCanvasType.sixteenToNine.rawValue:
  390. showRect = CGRect(x: 0, y: 0 + (playerViewHeight - cScreenWidth * (9.0 / 16.0)) / 2, width: cScreenWidth, height: cScreenWidth * (9.0 / 16.0))
  391. default:
  392. break
  393. }
  394. } else if showType == 2 { // 总览界面
  395. switch canvasType {
  396. case videoCanvasType.origin.rawValue:
  397. BFLog(message: "总览时画布的大小 \(String(describing: editProjectModel!.sData!.videoMetaData?.videoWidth)) \(String(describing: editProjectModel!.sData!.videoMetaData?.videoHeight))")
  398. // 画布的宽高 和宽高比值
  399. let materialWidth = editProjectModel!.sData!.videoMetaData?.videoWidth ?? 0
  400. let materialHeight = editProjectModel!.sData!.videoMetaData?.videoHeight ?? 1
  401. let ratioMaterial: Float = Float(materialWidth) / Float(materialHeight)
  402. if ratioMaterial > 1 {
  403. // 横屏
  404. showRect = CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenWidth * CGFloat(materialHeight) / CGFloat(materialWidth))
  405. } else if ratioMaterial < 1 {
  406. // 竖屏
  407. showRect = CGRect(x: (cScreenWidth - cScreenWidth * CGFloat(materialWidth) / CGFloat(materialHeight)) / 2, y: 0, width: cScreenWidth * (CGFloat(materialWidth) / CGFloat(materialHeight)), height: cScreenWidth)
  408. BFLog(message: "showRect is \(showRect)")
  409. } else {
  410. showRect = CGRect(x: 0, y: 0, width: cScreenWidth - 2, height: cScreenWidth - 2)
  411. }
  412. case videoCanvasType.oneToOne.rawValue:
  413. showRect = CGRect(x: 0, y: 0, width: cScreenWidth - 2, height: cScreenWidth - 2)
  414. case videoCanvasType.nineToSixteen.rawValue:
  415. showRect = CGRect(x: (cScreenWidth - cScreenWidth * (9.0 / 16.0)) / 2, y: 0, width: cScreenWidth * (9.0 / 16.0), height: cScreenWidth)
  416. case videoCanvasType.sixteenToNine.rawValue:
  417. showRect = CGRect(x: 0, y: 0, width: cScreenWidth, height: cScreenWidth * (9.0 / 16.0))
  418. default:
  419. break
  420. }
  421. }
  422. return showRect
  423. }
  424. /*
  425. 1, 加工入口进入编辑界面 默认画布?默认为 原始
  426. 2,进入编辑界面如果选了一个素材 画布就是实际大小,
  427. 3,没视觉素材时 点击原始显示1:1
  428. 4, 上传入口进入编辑界面 默认画布为原始
  429. 5, 从草稿箱进来时,使用恢复的画布大小
  430. 6, 如果选择了原始,移动素材后都按最新的第一个素材修改画布
  431. */
  432. /// sdata json canvastype 转到 UI 所使用类型
  433. /// - Parameter projectModel: project sdata
  434. /// - Returns: UI 使用类型
  435. public class func videoCanvasTypeToAspectRatio(projectModel: PQEditProjectModel?) -> aspectRatio? {
  436. // add by ak 给素材详情界面传比例参数如果是原始大小的要传 size
  437. var aspectRatioTemp: aspectRatio?
  438. if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.origin.rawValue {
  439. var firstModel: PQEditVisionTrackMaterialsModel?
  440. for part in projectModel!.sData!.sections {
  441. if part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().count ?? 0 > 0 {
  442. firstModel = part.sectionTimeline?.visionTrack?.getEnableVisionTrackMaterials().first
  443. break
  444. }
  445. }
  446. if firstModel != nil {
  447. aspectRatioTemp = .origin(width: CGFloat(firstModel!.width), height: CGFloat(firstModel!.height))
  448. } else {
  449. aspectRatioTemp = .origin(width: CGFloat(projectModel?.sData?.videoMetaData?.videoWidth ?? 0), height: CGFloat(projectModel?.sData?.videoMetaData?.videoHeight ?? 0))
  450. }
  451. } else if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.oneToOne.rawValue {
  452. aspectRatioTemp = .oneToOne
  453. } else if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.nineToSixteen.rawValue {
  454. aspectRatioTemp = .nineToSixteen
  455. } else if projectModel?.sData?.videoMetaData?.canvasType == videoCanvasType.sixteenToNine.rawValue {
  456. aspectRatioTemp = .sixteenToNine
  457. }
  458. return aspectRatioTemp
  459. }
  460. public class func getCanvasBtnName(canvasType: videoCanvasType) -> (String, String) {
  461. var btnText: String = "自适应"
  462. var btnImageName: String = "settingZoom_origin_h"
  463. if canvasType == .origin {
  464. btnText = "自适应"
  465. btnImageName = "settingZoom_origin_h"
  466. } else if canvasType == .oneToOne {
  467. btnText = "1:1"
  468. btnImageName = "settingZoom_oneToOne_h"
  469. } else if canvasType == .sixteenToNine {
  470. btnText = "16:9"
  471. btnImageName = "settingZoom_sixteenToNine_h"
  472. } else if canvasType == .nineToSixteen {
  473. btnText = "9:16"
  474. btnImageName = "settingZoom_nineToSixteen_h"
  475. }
  476. return (btnText, btnImageName)
  477. }
  478. }
  479. // MARK: - 混音相关
  480. /// 混音相关
  481. extension PQPlayerViewModel {
  482. /// 混音合成
  483. /// - Parameters:
  484. /// - originAsset: 空音乐文件素材
  485. /// - bgmData: 背景音乐
  486. /// - videoStickers: 视频素材
  487. /// - originMusicDuration : 要播放的时长
  488. /// - lastSecondPoint : 音频长度不够时,拼接音频文件时的结束时间,推荐卡点的倒数第二位
  489. /// - startTime: 裁剪的开始位置。
  490. /// - Returns:
  491. public class func setupAudioMix(originAsset: AVURLAsset, bgmData: PQVoiceModel?, videoStickers: [PQEditVisionTrackMaterialsModel]?,originMusicDuration:Float = 0,clipAudioRange: CMTimeRange = CMTimeRange.zero,startTime:CMTime = .zero ) -> (AVMutableAudioMix, AVMutableComposition) {
  492. let composition = AVMutableComposition()
  493. let audioMix = AVMutableAudioMix()
  494. var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
  495. // 处理选择的主音乐
  496. if(originMusicDuration > Float(CMTimeGetSeconds(clipAudioRange.duration))){
  497. BFLog(message: "要播放的时间长,比原音频要长进行拼接originMusicDuration:\(originMusicDuration) originAsset.duration \(CMTimeGetSeconds(clipAudioRange.duration))")
  498. let originaParameters = dealWithOriginAssetTrack(originAsset: originAsset, totalDuration: Float64(originMusicDuration), composition: composition,clipAudioRange: clipAudioRange,mStartTime: startTime)
  499. BFLog(message: "originaParameters count \(originaParameters.count)")
  500. if originaParameters.count > 0 {
  501. tempParameters = tempParameters + originaParameters
  502. }
  503. }else{
  504. BFLog(message: "音频不用拼接:\(CMTimeGetSeconds(originAsset.duration))")
  505. let parameters = mixAudioTrack(audioAsset: originAsset, trackTimeRange: CMTimeRange(start: .zero, end: originAsset.duration), composition: composition)
  506. if parameters != nil {
  507. tempParameters.append(parameters!)
  508. }else{
  509. BFLog(message: "parameters is error \(CMTimeGetSeconds(originAsset.duration))")
  510. }
  511. }
  512. // 处理背景音乐
  513. if bgmData != nil, bgmData?.localPath != nil {
  514. let bgmParameters = dealWithBGMTrack(bgmData: bgmData!, totalDuration: originAsset.duration.seconds, composition: composition)
  515. if bgmParameters.count > 0 {
  516. tempParameters = tempParameters + bgmParameters
  517. }
  518. }
  519. // 处理素材音乐
  520. if videoStickers != nil, (videoStickers?.count ?? 0) > 0 {
  521. for sticker in videoStickers! {
  522. if sticker.volumeGain == 0 {
  523. // 如果添加了会有刺啦音
  524. BFLog(message: "音频音量 为0 不添加")
  525. continue
  526. }
  527. let stickerParameters = dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  528. if stickerParameters.count > 0 {
  529. tempParameters = tempParameters + stickerParameters
  530. }
  531. }
  532. }
  533. audioMix.inputParameters = tempParameters
  534. // 导出音乐
  535. // exportAudio(comosition: composition)
  536. return (audioMix, composition)
  537. }
  538. /// 处理原主音乐音轨 e.g. 原音频时长只有30s 要播放 250s 的音频 拼接原音频音轨
  539. /// - Parameters:
  540. /// - originAsset: 原音频文件地址
  541. /// - composition:
  542. /// - Returns:
  543. public class func dealWithOriginAssetTrack(originAsset: AVURLAsset, totalDuration: Float64, composition: AVMutableComposition,clipAudioRange: CMTimeRange = CMTimeRange.zero,mStartTime:CMTime = .zero ) -> [AVMutableAudioMixInputParameters] {
  544. var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
  545. let volume:Float = 1.0
  546. let originaDuration = CMTimeGetSeconds(clipAudioRange.duration)
  547. BFLog(message: "处理主音频 原始时长startTime = \(originaDuration) 要显示时长totalDuration = \(totalDuration)")
  548. //整倍数
  549. let count = Int(totalDuration) / Int(originaDuration)
  550. // count = count + 1
  551. //有余数多 clip 一整段
  552. let row = totalDuration - Double(count) * originaDuration
  553. //已经拼接的总时长
  554. var clipTotalDuration:Float = 0.0
  555. if count > 0 {
  556. for index in 0 ..< count {
  557. BFLog(message: "this is running running")
  558. //第一段是用户选择的开始时间 到倒数第二个卡点, 其它段都是从推荐卡点到倒数第二个卡点
  559. var startTime = CMTime.zero
  560. var trackTimeRange = clipAudioRange
  561. if(index == 0){
  562. startTime = mStartTime
  563. trackTimeRange = CMTimeRange(start: startTime, end: CMTime(value: CMTimeValue(CMTimeGetSeconds(clipAudioRange.end)), timescale: playerTimescaleInt))
  564. clipTotalDuration = clipTotalDuration + Float(CMTimeGetSeconds(trackTimeRange.duration))
  565. }else{
  566. // (CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime))为用户选择的第一段时长
  567. startTime = CMTime(value: CMTimeValue((CMTimeGetSeconds( clipAudioRange.duration) * Double(index) + (CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime))) * Float64(playerTimescaleInt)), timescale: playerTimescaleInt)
  568. trackTimeRange = clipAudioRange
  569. clipTotalDuration = clipTotalDuration + Float(CMTimeGetSeconds(trackTimeRange.duration))
  570. }
  571. BFLog(1, message: "原音频时长短:count = \(count),startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
  572. let parameters = mixAudioTrack(audioAsset: originAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  573. if parameters != nil {
  574. tempParameters.append(parameters!)
  575. }else{
  576. BFLog(message: "接拼出现错误!!!!")
  577. }
  578. }
  579. }
  580. if(row > 0){
  581. let startTime = CMTime(value: CMTimeValue(clipTotalDuration * Float(playerTimescaleInt)), timescale: playerTimescaleInt)
  582. let trackTimeRange = CMTimeRange(start: startTime, end: CMTime(value: CMTimeValue((CMTimeGetSeconds(startTime) + row) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
  583. BFLog(1, message: "最后一小段音乐时长短:count = \(count),startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
  584. let parameters = mixAudioTrack(audioAsset: originAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  585. if parameters != nil {
  586. tempParameters.append(parameters!)
  587. }
  588. clipTotalDuration = clipTotalDuration + Float(row)
  589. }
  590. BFLog(message: "拼接的音频总时长: \(clipTotalDuration)")
  591. return tempParameters
  592. }
  593. /// 处理背景音乐音轨
  594. /// - Parameters:
  595. /// - stickerModel: <#stickerModel description#>
  596. /// - composition: <#composition description#>
  597. /// - Returns: <#description#>
  598. public class func dealWithBGMTrack(bgmData: PQVoiceModel, totalDuration: Float64, composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  599. var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
  600. let bgmAsset = AVURLAsset(url: URL(fileURLWithPath: bgmData.localPath ?? ""), options: avAssertOptions)
  601. let volume = Float(bgmData.volume) / 100.0
  602. let bgmDuration = (Float64(bgmData.duration ?? "0") ?? 0) - bgmData.startTime
  603. BFLog(message: "处理背景音乐:startTime = \(bgmData.startTime),bgmDuration = \(bgmDuration),totalDuration = \(totalDuration)")
  604. if bgmDuration < totalDuration {
  605. let count = Int(totalDuration) / Int(bgmDuration)
  606. let row = totalDuration - Double(count) * bgmDuration
  607. if count > 0 {
  608. for index in 0 ..< count {
  609. let startTime = CMTime(value: CMTimeValue(bgmDuration * Double(index) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
  610. let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(bgmData.startTime * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((bgmData.startTime + bgmDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
  611. BFLog(message: "背景音乐时长短:count = \(count),startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
  612. let parameters = mixAudioTrack(audioAsset: bgmAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  613. if parameters != nil {
  614. tempParameters.append(parameters!)
  615. }
  616. }
  617. }
  618. if row > 0 {
  619. let startTime = CMTime(value: CMTimeValue(bgmDuration * Double(count) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
  620. let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(bgmData.startTime * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((bgmData.startTime + row) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
  621. BFLog(message: "背景音乐时长短:count = \(count),startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
  622. let parameters = mixAudioTrack(audioAsset: bgmAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  623. if parameters != nil {
  624. tempParameters.append(parameters!)
  625. }
  626. }
  627. } else {
  628. let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(bgmData.startTime * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((bgmData.startTime + totalDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
  629. BFLog(message: "背景音乐时长长:trackTimeRange = \(trackTimeRange)")
  630. let bgmParameters = mixAudioTrack(audioAsset: bgmAsset, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  631. if bgmParameters != nil {
  632. tempParameters.append(bgmParameters!)
  633. }
  634. }
  635. return tempParameters
  636. }
  637. /// 处理素材音轨
  638. /// - Parameters:
  639. /// - stickerModel: <#stickerModel description#>
  640. /// - composition: <#composition description#>
  641. /// - Returns: <#description#>
  642. public class func dealWithMaterialTrack(stickerModel: PQEditVisionTrackMaterialsModel, composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  643. var tempParameters: [AVMutableAudioMixInputParameters] = [AVMutableAudioMixInputParameters].init()
  644. let audioAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + stickerModel.locationPath), options: avAssertOptions)
  645. let volume = Float(stickerModel.volumeGain) / 100
  646. let rangeStart = stickerModel.model_in
  647. var rangeEnd = stickerModel.out
  648. if rangeEnd == 0 {
  649. rangeEnd = audioAsset.duration.seconds
  650. }
  651. var originDuration = (rangeEnd - rangeStart)
  652. if stickerModel.aptDuration < originDuration {
  653. originDuration = stickerModel.aptDuration
  654. }
  655. if stickerModel.aptDuration > originDuration, stickerModel.materialDurationFit?.fitType == adapterMode.loopAuto.rawValue {
  656. let count = originDuration == 0 ? 0 : Int(stickerModel.aptDuration) / Int(originDuration)
  657. let row = stickerModel.aptDuration - Double(count) * originDuration
  658. if count > 0 {
  659. for index in 0 ..< count {
  660. let startTime = CMTime(value: CMTimeValue((stickerModel.timelineIn + originDuration * Double(index)) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
  661. let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(rangeStart * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((rangeStart + originDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
  662. let parameters = mixAudioTrack(audioAsset: audioAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  663. if parameters != nil {
  664. tempParameters.append(parameters!)
  665. }
  666. }
  667. }
  668. if row > 0 {
  669. let startTime = CMTime(value: CMTimeValue((stickerModel.timelineIn + originDuration * Double(count)) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
  670. let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(rangeStart * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((rangeStart + row) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
  671. let parameters = mixAudioTrack(audioAsset: audioAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  672. if parameters != nil {
  673. tempParameters.append(parameters!)
  674. }
  675. }
  676. } else {
  677. let startTime = CMTime(value: CMTimeValue(stickerModel.timelineIn * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
  678. let trackTimeRange = CMTimeRange(start: CMTime(value: CMTimeValue(rangeStart * Double(playerTimescaleInt)), timescale: playerTimescaleInt), end: CMTime(value: CMTimeValue((rangeStart + originDuration) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
  679. let parameters = mixAudioTrack(audioAsset: audioAsset, startTime: startTime, trackTimeRange: trackTimeRange, volume: volume, composition: composition)
  680. if parameters != nil {
  681. tempParameters.append(parameters!)
  682. }
  683. }
  684. return tempParameters
  685. }
  686. /// 混音添加音轨
  687. /// - Parameters:
  688. /// - audioAsset: 素材资源
  689. /// - startTime: 从什么时间开始播放
  690. /// - trackTimeRange: 播放素材范围
  691. /// - volume:音轨音量
  692. /// - composition: <#composition description#>
  693. /// - Returns: <#description#>
  694. public class func mixAudioTrack(audioAsset: AVURLAsset, startTime: CMTime = CMTime.zero, trackTimeRange: CMTimeRange, volume: Float = 1, composition: AVMutableComposition) -> AVMutableAudioMixInputParameters? {
  695. BFLog(message: "startTime = \(startTime),trackTimeRange = \(trackTimeRange)")
  696. // 第一个音轨
  697. // let assetTrack : AVAssetTrack? = audioAsset.tracks(withMediaType: .audio).first
  698. // 所有音轨
  699. let assetTracks: [AVAssetTrack]? = audioAsset.tracks(withMediaType: .audio)
  700. if assetTracks != nil, (assetTracks?.count ?? 0) > 0 {
  701. let audioTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
  702. let mixInputParameters = AVMutableAudioMixInputParameters(track: audioTrack)
  703. mixInputParameters.setVolume(volume, at: startTime)
  704. do {
  705. // 第一个音轨插入到原音的开始和结束位置
  706. // try audioTrack?.insertTimeRange(trackTimeRange, of: assetTrack!, at: startTime)
  707. // 所有音轨插入到原音的开始和结束位置
  708. let timeRanges = Array(repeating: NSValue(timeRange: trackTimeRange), count: assetTracks!.count)
  709. try audioTrack?.insertTimeRanges(timeRanges, of: assetTracks!, at: startTime)
  710. } catch {
  711. BFLog(message: "error is \(error)")
  712. }
  713. return mixInputParameters
  714. }
  715. return nil
  716. }
  717. // 导出音频
  718. /// - Parameter comosition: <#comosition description#>
  719. /// - Returns: <#description#>
  720. public class func exportAudio(comosition: AVAsset) {
  721. let outPutFilePath = URL(fileURLWithPath: tempDirectory + "/temp.mp4")
  722. // 删除以创建地址
  723. try? FileManager.default.removeItem(at: outPutFilePath)
  724. let assetExport = AVAssetExportSession(asset: comosition, presetName: AVAssetExportPresetMediumQuality)
  725. assetExport?.outputFileType = .mp4
  726. assetExport?.outputURL = outPutFilePath
  727. assetExport?.exportAsynchronously(completionHandler: {
  728. print("assetExport == \(assetExport?.status.rawValue ?? 0),error = \(String(describing: assetExport?.error))")
  729. DispatchQueue.main.async {}
  730. })
  731. }
  732. }