BFRecordExport.swift 24 KB

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