BFRecordExport.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. //
  2. // BFRecordExport.swift
  3. // BFRecordScreenKit
  4. //
  5. // Created by 胡志强 on 2021/11/25.
  6. // 录屏视频导出
  7. import AVFoundation
  8. import BFCommonKit
  9. import BFMediaKit
  10. import Foundation
  11. import GPUImage
  12. import Photos
  13. import UIKit
  14. public enum ExportError: Int {
  15. case FileNotExist = -31001
  16. case DataLost = -31002
  17. case VoiceLost = -31003
  18. case TotalDurError = -31004
  19. case ExportExcept = -31005
  20. case DiskNoSpace = -31006
  21. }
  22. public class BFRecordExport {
  23. public var progress: ((Float) -> Void)?
  24. public var exportCompletion: ((Error?, URL?) -> Void)?
  25. public var originSoundVolumn: Float = 1.0 // 无录音时原声大小
  26. public var originSoundInRecordVolumn: Float = 0.0 // 录音时原声大小
  27. var titleStickers = [PQEditSubTitleModel]()
  28. var voiceList = [PQEditVisionTrackMaterialsModel]()
  29. var videoStickers = [PQEditVisionTrackMaterialsModel]()
  30. public var data: [BFRecordItemModel]? {
  31. didSet {
  32. if data?.count ?? 0 > 0 {
  33. for item in data! {
  34. item.generationTimeRanges()
  35. }
  36. }
  37. }
  38. }
  39. var count = 0
  40. var stickerRanges = [CMTimeRange]()
  41. var exporter: PQCompositionExporter?
  42. // var mStickers = [PQEditVisionTrackMaterialsModel]()
  43. deinit {}
  44. public init() {}
  45. // MARK: -
  46. /// synthesisAll: 合成所有还是只合成录音部分
  47. public func startExprot(synthesisAll: Bool) {
  48. // 1,背景视频素材
  49. if let itemModels = data {
  50. var totalDur = 0.0
  51. titleStickers.removeAll()
  52. voiceList.removeAll()
  53. videoStickers.removeAll()
  54. // 切割视频素材
  55. for (_, itemModel) in itemModels.enumerated() {
  56. // 保留录音部分时,是否按录音顺序合成最终视频;
  57. // 如果需要排序,则排视频的顺序;否则排音频的顺序
  58. let needSort = false
  59. if needSort {
  60. } else {
  61. // 音频排序
  62. var useVoices = itemModel.getUsedVoices()
  63. useVoices.sort { m1, m2 in
  64. m1.startCMTime.seconds < m2.startCMTime.seconds
  65. }
  66. // 字幕排序
  67. itemModel.titleStickers.sort { model1, model2 in
  68. model1.timelineIn < model2.timelineIn
  69. }
  70. }
  71. switch itemModel.mediaType {
  72. case .Image:
  73. totalDur += parseImage(synthesisAll: synthesisAll, itemModel: itemModel, totalDur: totalDur)
  74. case .some(.Video):
  75. let dur = parseVideo(synthesisAll: synthesisAll, itemModel: itemModel, totalDur: totalDur)
  76. if dur >= 0 {
  77. totalDur += dur
  78. }else{
  79. // -1 时出错
  80. return
  81. }
  82. case .some(.Camera):
  83. totalDur += parseCamera(synthesisAll: synthesisAll, itemModel: itemModel, totalDur: totalDur)
  84. case .none:
  85. break
  86. }
  87. // 视频处理
  88. }
  89. beginExport(synthesisAll: synthesisAll)
  90. }
  91. }
  92. func parseImage(synthesisAll: Bool, itemModel: BFRecordItemModel, totalDur:Double) -> Double{
  93. if !synthesisAll, itemModel.getUsedVoices().count == 0 {
  94. // 图片无录音在保留模式里不合成
  95. return 0
  96. }
  97. var duration = itemModel.materialDuraion.seconds
  98. if itemModel.getUsedVoices().count == 0 {
  99. // 图片无录音保持2s
  100. duration = 2
  101. let voice = PQEditVisionTrackMaterialsModel()
  102. voice.model_in = 0.0
  103. voice.out = 2.0
  104. voice.aptDuration = 2.0
  105. // voice.voiceType = VOICETYPT.None.rawValue
  106. voice.volumeGain = 100
  107. voiceList.append(voice)
  108. } else {
  109. //
  110. for mod in itemModel.getUsedVoices() {
  111. let sticker = PQEditVisionTrackMaterialsModel()
  112. sticker.model_in = 0
  113. sticker.out = mod.endCMTime.seconds - mod.startCMTime.seconds
  114. sticker.timelineIn = totalDur + mod.startCMTime.seconds
  115. sticker.timelineOut = totalDur + mod.endCMTime.seconds
  116. sticker.aptDuration = sticker.out
  117. sticker.duration = sticker.out
  118. sticker.locationPath = mod.wavFilePath
  119. sticker.volumeGain = 100 // Float64(model.volume)
  120. voiceList.append(sticker)
  121. }
  122. }
  123. let sticker = splitBaseMaterial(timelineIn: totalDur, model_in: 0, duration: duration)
  124. sticker.originalUImage = itemModel.coverImg
  125. sticker.volumeGain = 0
  126. sticker.type = StickerType.IMAGE.rawValue
  127. videoStickers.append(sticker)
  128. BFLog(1, message: "image sticker - timIn:\(sticker.timelineIn), modIn:\(sticker.model_in), dur:\(duration)")
  129. for titleS in itemModel.titleStickers {
  130. // let leng = titleS.timelineOut - titleS.timelineIn
  131. let newTitleSticker = PQEditSubTitleModel()
  132. titleStickers.append(newTitleSticker)
  133. newTitleSticker.text = titleS.text
  134. newTitleSticker.setting = titleS.setting
  135. newTitleSticker.timelineIn = titleS.timelineIn + CMTime(seconds: totalDur, preferredTimescale: 1000)
  136. newTitleSticker.timelineOut = titleS.timelineOut + CMTime(seconds: totalDur, preferredTimescale: 1000)
  137. }
  138. return duration
  139. }
  140. func parseVideo(synthesisAll: Bool, itemModel: BFRecordItemModel, totalDur:Double) -> Double {
  141. var subDur = 0.0
  142. if let localPath = itemModel.localPath {
  143. if !FileManager.default.fileExists(atPath: localPath) {
  144. let error = NSError(domain: "err", code: ExportError.FileNotExist.rawValue, userInfo: ["msg": "file not exist"])
  145. exportCompletion?(error as Error, nil)
  146. return subDur
  147. }
  148. if synthesisAll {
  149. let drangs = itemModel.dealedDurationRanges
  150. for srange in drangs {
  151. let range = srange.range
  152. let sticker = splitBaseMaterial(timelineIn: totalDur + subDur, model_in: range.start.seconds, duration: range.duration.seconds)
  153. sticker.locationPath = localPath
  154. sticker.volumeGain = Float64(srange.isRecord ? originSoundInRecordVolumn * 100 : originSoundVolumn * 100)
  155. videoStickers.append(sticker)
  156. subDur += range.duration.seconds
  157. if srange.isRecord {
  158. itemModel.getUsedVoices().forEach { mod in
  159. let sticker = PQEditVisionTrackMaterialsModel()
  160. sticker.model_in = 0
  161. sticker.out = mod.endCMTime.seconds - mod.startCMTime.seconds
  162. sticker.timelineIn = totalDur + mod.startCMTime.seconds
  163. sticker.timelineOut = totalDur + mod.endCMTime.seconds
  164. sticker.aptDuration = sticker.out
  165. sticker.duration = sticker.out
  166. sticker.locationPath = mod.wavFilePath
  167. sticker.volumeGain = 100 // Float64(model.volume)
  168. voiceList.append(sticker)
  169. let ttsAudioAsset = AVURLAsset(url: URL(fileURLWithPath: sticker.locationPath), options: avAssertOptions)
  170. BFLog(message: "导出使用的音频文件 timelineIn:\(sticker.timelineIn) timelineOut\(sticker.timelineOut) sticker.locationPath\( sticker.locationPath) tss语音时长\(ttsAudioAsset.duration.seconds)")
  171. }
  172. }
  173. }
  174. for titleS in itemModel.titleStickers {
  175. let newTitleSticker = PQEditSubTitleModel()
  176. titleStickers.append(newTitleSticker)
  177. newTitleSticker.text = titleS.text
  178. newTitleSticker.setting = titleS.setting
  179. newTitleSticker.timelineIn = titleS.timelineIn + CMTime(seconds: totalDur, preferredTimescale: 1000)
  180. newTitleSticker.timelineOut = titleS.timelineOut + CMTime(seconds: totalDur, preferredTimescale: 1000)
  181. }
  182. } else {
  183. // 只保留录音部分
  184. var drangs = itemModel.dealedDurationRanges.filter { srange in
  185. srange.isRecord == true
  186. }
  187. let needSort = false
  188. if needSort {
  189. drangs.sort { range1, range2 in
  190. range1.index < range2.index
  191. }
  192. }
  193. for (index, srange) in drangs.enumerated() {
  194. let range = srange.range
  195. let sticker = splitBaseMaterial(timelineIn: totalDur + subDur, model_in: range.start.seconds, duration: range.duration.seconds)
  196. sticker.locationPath = localPath
  197. sticker.volumeGain = Float64(srange.isRecord ? originSoundInRecordVolumn * 100 : originSoundVolumn * 100)
  198. videoStickers.append(sticker)
  199. let voiceSticker = itemModel.voiceStickers[index]
  200. // mdf by ak
  201. if(itemModel.voiceChangeStickers.count > 0){//有变音
  202. let voiceChangeModels = itemModel.voiceChangeStickers.filter { mod in
  203. mod.recordId == voiceSticker.recordId
  204. }
  205. for voice in voiceChangeModels{
  206. BFLog(2, message: "当前变音所属:\(voiceSticker.recordId ?? "") text:\(voice.wavFilePath ?? "")")
  207. let voiceChangeSt = PQEditVisionTrackMaterialsModel()
  208. voiceChangeSt.model_in = 0
  209. voiceChangeSt.out = voice.endCMTime.seconds - voice.startCMTime.seconds
  210. voiceChangeSt.timelineIn = totalDur + subDur + (voice.startCMTime.seconds - voiceSticker.startCMTime.seconds)
  211. voiceChangeSt.timelineOut = voiceChangeSt.timelineIn + voiceChangeSt.out
  212. voiceChangeSt.aptDuration = voiceChangeSt.out
  213. voiceChangeSt.duration = voiceChangeSt.out
  214. voiceChangeSt.locationPath = voice.wavFilePath
  215. voiceChangeSt.volumeGain = 100 // Float64(model.volume)
  216. voiceList.append(voiceChangeSt)
  217. }
  218. }else{//无变音
  219. let voice = PQEditVisionTrackMaterialsModel()
  220. voice.model_in = 0
  221. voice.out = voiceSticker.endCMTime.seconds - voiceSticker.startCMTime.seconds
  222. voice.timelineIn = totalDur + subDur
  223. voice.timelineOut = totalDur + subDur + voice.out
  224. voice.aptDuration = voice.out
  225. voice.duration = voice.out
  226. voice.locationPath = voiceSticker.wavFilePath
  227. voice.volumeGain = 100 // Float64(model.volume)
  228. voiceList.append(voice)
  229. }
  230. let titleModels = itemModel.titleStickers.filter { mod in
  231. mod.recordId == voiceSticker.recordId
  232. }
  233. // 字幕的时间点是以相关录音的原点为坐标计算的。
  234. for titleS in titleModels {
  235. let newTitleSticker = PQEditSubTitleModel()
  236. titleStickers.append(newTitleSticker)
  237. newTitleSticker.text = titleS.text
  238. newTitleSticker.setting = titleS.setting
  239. newTitleSticker.timelineIn = titleS.timelineIn + CMTime(seconds: totalDur + subDur , preferredTimescale: 1000) - voiceSticker.startCMTime
  240. newTitleSticker.timelineOut = titleS.timelineOut + CMTime(seconds: totalDur + subDur, preferredTimescale: 1000) - voiceSticker.startCMTime
  241. BFLog(1, message: "timein - \(newTitleSticker.timelineIn), out - \(newTitleSticker.timelineOut)")
  242. }
  243. subDur += range.duration.seconds
  244. }
  245. }
  246. }
  247. return subDur
  248. }
  249. func parseCamera(synthesisAll: Bool, itemModel: BFRecordItemModel, totalDur:Double) -> Double {
  250. var duration = itemModel.materialDuraion.seconds
  251. // 视频分解
  252. for mod in itemModel.videoStickers {
  253. let sticker = PQEditVisionTrackMaterialsModel()
  254. sticker.type = StickerType.VIDEO.rawValue
  255. sticker.canvasFillType = stickerContentMode.aspectFitStr.rawValue
  256. sticker.model_in = 0
  257. sticker.out = (mod.timelineCMOut - mod.timelineCMIn).seconds
  258. sticker.timelineIn = totalDur + mod.timelineCMIn.seconds
  259. sticker.timelineOut = totalDur + mod.timelineCMOut.seconds
  260. sticker.aptDuration = sticker.out
  261. sticker.duration = sticker.out
  262. sticker.locationPath = mod.locationPath.replacingOccurrences(of: documensDirectory, with: "")
  263. sticker.volumeGain = 1 // Float64(model.volume)
  264. videoStickers.append(sticker)
  265. }
  266. // 音频分解
  267. for mod in itemModel.getUsedVoices() {
  268. let sticker = PQEditVisionTrackMaterialsModel()
  269. sticker.model_in = 0
  270. sticker.type = StickerType.VOICE.rawValue
  271. sticker.out = mod.endCMTime.seconds - mod.startCMTime.seconds
  272. sticker.timelineIn = totalDur + mod.startCMTime.seconds
  273. sticker.timelineOut = totalDur + mod.endCMTime.seconds
  274. sticker.aptDuration = sticker.out
  275. sticker.duration = sticker.out
  276. sticker.locationPath = mod.wavFilePath
  277. sticker.volumeGain = 100 // Float64(model.volume)
  278. voiceList.append(sticker)
  279. }
  280. // 字幕分解
  281. for titleS in itemModel.titleStickers {
  282. let newTitleSticker = PQEditSubTitleModel()
  283. titleStickers.append(newTitleSticker)
  284. newTitleSticker.text = titleS.text
  285. newTitleSticker.setting = titleS.setting
  286. newTitleSticker.timelineIn = titleS.timelineIn + CMTime(seconds: totalDur, preferredTimescale: 1000)
  287. newTitleSticker.timelineOut = titleS.timelineOut + CMTime(seconds: totalDur, preferredTimescale: 1000)
  288. }
  289. return duration
  290. }
  291. public func cancelExport() {
  292. exporter?.cancel()
  293. }
  294. public func clearFileCache() {
  295. data?.forEach { itemModel in
  296. itemModel.getUsedVoices().forEach { model in
  297. if let localPath = model.wavFilePath {
  298. try? FileManager.default.removeItem(atPath: localPath)
  299. }
  300. }
  301. }
  302. }
  303. enum DispatchError: Error {
  304. case timeout
  305. }
  306. func getOutputFilePath() -> URL {
  307. var outPutMP4Path = exportVideosDirectory
  308. if !directoryIsExists(dicPath: outPutMP4Path) {
  309. createDirectory(path: outPutMP4Path)
  310. }
  311. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  312. return URL(fileURLWithPath: outPutMP4Path)
  313. }
  314. // 切割素材
  315. func splitBaseMaterial(timelineIn: Double, model_in: Double, duration: Double) -> PQEditVisionTrackMaterialsModel {
  316. let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
  317. bgMovieInfo.type = StickerType.VIDEO.rawValue
  318. bgMovieInfo.timelineIn = timelineIn
  319. bgMovieInfo.timelineOut = timelineIn + duration
  320. bgMovieInfo.model_in = model_in
  321. bgMovieInfo.out = model_in + duration
  322. bgMovieInfo.canvasFillType = stickerContentMode.aspectFitStr.rawValue
  323. bgMovieInfo.volumeGain = 1
  324. bgMovieInfo.aptDuration = bgMovieInfo.timelineOut
  325. bgMovieInfo.duration = bgMovieInfo.timelineOut
  326. BFLog(1, message: "hhh- timIn:\(timelineIn), modIn:\(model_in), dur:\(duration)")
  327. return bgMovieInfo
  328. }
  329. // 因为titleStickers 是传递过来的,会修改timelinein,需要重新生成,以免影响原来的数据
  330. // voiceList是考虑到图片有时候没有录音,在保留全部时,需要添加一个2秒的空sticker
  331. func beginExport(synthesisAll: Bool) {
  332. // 输出视频地址
  333. // exprotVideo()
  334. // return;
  335. var outPutMP4Path = exportVideosDirectory
  336. if !directoryIsExists(dicPath: outPutMP4Path) {
  337. createDirectory(path: outPutMP4Path)
  338. }
  339. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  340. let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
  341. BFLog(1, message: "导出视频地址 \(outPutMP4URL)")
  342. guard let itemData = data else {
  343. let error = NSError(domain: "err", code: ExportError.DataLost.rawValue, userInfo: ["msg": "compose_fail_lost".BFLocale])
  344. exportCompletion?(error as Error, nil)
  345. return
  346. }
  347. // 处理导出
  348. guard let voiceCount = data?.reduce(0, { partialResult, itemModell in
  349. itemModell.getUsedVoices().count + partialResult
  350. }) else {
  351. BFLog(1, message: "getUsedVoices() count += nil")
  352. let error = NSError(domain: "err", code: ExportError.VoiceLost.rawValue, userInfo: ["msg": "getUsedVoices() count += nil"])
  353. exportCompletion?(error as Error, nil)
  354. return
  355. }
  356. guard let totalDuration = data?.reduce(0.0, { partialResult, itemModell in
  357. var modelDuraion = 0.0
  358. if itemModell.mediaType == .Image {
  359. if itemModell.getUsedVoices().count == 0, synthesisAll {
  360. modelDuraion += 2
  361. } else {
  362. modelDuraion = itemModell.materialDuraion.seconds
  363. }
  364. } else if itemModell.mediaType == .Video {
  365. modelDuraion = itemModell.dealedDurationRanges.reduce(0.0) { partialResult, srange in
  366. // partialResult + (!synthesisAll && srange.isRecord) ?
  367. if synthesisAll {
  368. return partialResult + srange.range.duration.seconds
  369. } else {
  370. return partialResult + (srange.isRecord ? srange.range.duration.seconds : 0)
  371. }
  372. }
  373. }else if itemModell.mediaType == .Camera {
  374. modelDuraion = itemModell.materialDuraion.seconds
  375. }
  376. return (partialResult ?? 0) + modelDuraion
  377. }) else {
  378. let error = NSError(domain: "err", code: ExportError.TotalDurError.rawValue, userInfo: ["msg": "时长计算出错"])
  379. exportCompletion?(error as Error, nil)
  380. return
  381. }
  382. // MARK: - 声音合成
  383. // 有录音操作或者多个视频,就会进入合成步骤,否则就是一个没有处理的素材,直接导出就行了
  384. if voiceCount > 0 || videoStickers.count > 1 {
  385. let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList, synthesisAll: synthesisAll)
  386. let outputSize: CGSize = CGSize(width: 1080, height: Int(1080 * CGFloat(UIScreen.main.bounds.size.height / UIScreen.main.bounds.size.width)))
  387. BFLog(message: "输出视频大小:\(outputSize)")
  388. // add by ak 有字幕数据 & 显示字幕开关打开 添加字幕filter
  389. var subtitleFilter:PQSubTitleFilter?
  390. if titleStickers.count > 0,titleStickers.first?.setting.subtitleIsShow ?? true {
  391. subtitleFilter = PQSubTitleFilter(st: titleStickers, inputSize: outputSize)
  392. }
  393. exporter = PQCompositionExporter(asset: composition, videoComposition: nil, audioMix: audioMix, filters:nil,stickers: videoStickers, animationTool: nil, exportURL: outPutMP4URL,subTitleFilter:subtitleFilter)
  394. var orgeBitRate = Int(outputSize.width * outputSize.height * 3)
  395. for stick in videoStickers {
  396. if stick.type == StickerType.VIDEO.rawValue, stick.locationPath.count > 0 {
  397. let asset = AVURLAsset(url: URL(fileURLWithPath: stick.locationPath), options: avAssertOptions)
  398. let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
  399. if Int(cbr ?? 0) > orgeBitRate {
  400. orgeBitRate = Int(cbr ?? 0)
  401. }
  402. }
  403. }
  404. BFLog(1, message: String(format: "导出设置的码率为:%.3f MB", Double(orgeBitRate) / 1024.0 / 1024.0 / 8.0))
  405. let preSize = Double(orgeBitRate) * totalDuration / (1024 * 1024 * 8)
  406. let freeSize = PQBridgeObject.getPhoneDiskFreeSize()
  407. if preSize + 100.0 > freeSize { // 存储完后磁盘剩余至少100M
  408. let error = NSError(domain: "err", code: ExportError.DiskNoSpace.rawValue, userInfo: ["msg": "\("option_need".BFLocale)\(Int(preSize))MB,\("option_available".BFLocale)\(Int(freeSize))MB"])
  409. exportCompletion?(error as Error, nil)
  410. return
  411. }
  412. let tempBeginExport = Date().timeIntervalSince1970
  413. if exporter!.prepare(videoSize: outputSize, videoAverageBitRate: orgeBitRate) {
  414. exporter!.start(playeTimeRange: CMTimeRange(start: CMTime.zero, end: CMTime(seconds: totalDuration, preferredTimescale: 1000)))
  415. }
  416. exporter?.progressClosure = { [weak self] _, _, progress in
  417. // BFLog(message: "正片合成进度 \(progress * 100)%")
  418. let useProgress = progress > 1 ? 1 : progress
  419. if progress > 0 { // 更新进度
  420. self?.progress?(useProgress)
  421. }
  422. }
  423. exporter?.completion = { [weak self] url in
  424. // 输出视频时长
  425. if let url = url {
  426. let outSeconds = CMTimeGetSeconds(AVAsset(url: url).duration)
  427. let exportEndTime = Date().timeIntervalSince1970
  428. BFLog(1, message: "生成视频时长为:\(outSeconds) 总用时:\(exportEndTime - tempBeginExport)")
  429. if(PQENVUtil.shared.channel == "Development"){
  430. cShowHUB(superView: nil, msg: (outSeconds == 0) ? "compose_retry3".BFLocale : String(format: "总用时: %.2f", exportEndTime - tempBeginExport))
  431. }
  432. self?.exportCompletion?(nil, url)
  433. } else {
  434. let error = NSError(domain: "err", code: ExportError.ExportExcept.rawValue, userInfo: ["msg": "compose_fail_export".BFLocale])
  435. self?.exportCompletion?(error as Error, nil)
  436. cShowHUB(superView: nil, msg: "compose_fail_export".BFLocale)
  437. }
  438. // 导出完成后取消导出
  439. self?.exporter?.cancel()
  440. }
  441. } else {
  442. // 没有处理,直接copy原文件
  443. if let localPath = data?.first?.localPath {
  444. exportCompletion?(nil, URL(fileURLWithPath: localPath))
  445. }
  446. }
  447. }
  448. func dealAsset() {
  449. // asset?.tracks.first(where: { track in
  450. // if track.mediaType == .audio{
  451. //
  452. // }
  453. // })
  454. }
  455. func getVideoSize(asset: AVURLAsset) -> CGSize {
  456. var size = CGSize.zero
  457. asset.tracks.forEach { track in
  458. if track.mediaType == .video {
  459. let realSize = __CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform)
  460. size = CGSize(width: ceil(abs(realSize.width)), height: ceil(abs(realSize.height)))
  461. }
  462. }
  463. return size
  464. }
  465. }
  466. extension BFRecordExport {
  467. func mergeAudio(videoStickers: [PQEditVisionTrackMaterialsModel], audios: [PQEditVisionTrackMaterialsModel]?, synthesisAll: Bool) -> (AVMutableAudioMix, AVMutableComposition) {
  468. let composition = AVMutableComposition()
  469. let audioMix = AVMutableAudioMix()
  470. var tempParameters = [AVMutableAudioMixInputParameters]()
  471. var totalDuration: Float64 = 0
  472. for sticker in videoStickers {
  473. if sticker.volumeGain == 0 {
  474. // 如果添加了会有刺啦音
  475. BFLog(message: "音频音量 为0 不添加")
  476. continue
  477. }
  478. // sticker.volumeGain = 50
  479. totalDuration = max(totalDuration, sticker.duration)
  480. tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  481. }
  482. if let voices = audios {
  483. if synthesisAll {
  484. tempParameters += mergeRecordVoiceAll(voices: voices, composition)
  485. } else {
  486. tempParameters += mergeRecordVoiceOnly(voices: voices, composition)
  487. }
  488. }
  489. audioMix.inputParameters = tempParameters
  490. return (audioMix, composition)
  491. }
  492. func mergeRecordVoiceOnly(voices: [PQEditVisionTrackMaterialsModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  493. var tempParameters = [AVMutableAudioMixInputParameters]()
  494. var totalDur: Double = 0.0
  495. for model in voices {
  496. if model.volumeGain == 0 {
  497. // 如果添加了会有刺啦音
  498. BFLog(message: "音频音量 为0 不添加")
  499. continue
  500. }
  501. tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: model, composition: composition)
  502. totalDur += model.aptDuration
  503. }
  504. return tempParameters
  505. }
  506. func mergeRecordVoiceAll(voices: [PQEditVisionTrackMaterialsModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  507. var tempParameters = [AVMutableAudioMixInputParameters]()
  508. var totalDur = 0.0
  509. for model in voices {
  510. if model.volumeGain == 0 {
  511. // 如果添加了会有刺啦音
  512. BFLog(message: "音频音量 为0 不添加")
  513. continue
  514. }
  515. tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: model, composition: composition)
  516. }
  517. return tempParameters
  518. }
  519. }