BFRecordExport.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. public enum ExportError : Int {
  14. case FileNotExist = -31001
  15. case DataLost = -31002
  16. case VoiceLost = -31003
  17. case TotalDurError = -31004
  18. case ExportExcept = -31005
  19. case DiskNoSpace = -31006
  20. }
  21. public class BFRecordExport {
  22. public var progress: ((Float) -> Void)?
  23. public var exportCompletion: ((Error?, URL?) -> Void)?
  24. public var originSoundVolumn : Float = 1.0 // 无录音时原声大小
  25. public var originSoundInRecordVolumn : Float = 0.0 // 录音时原声大小
  26. public var data: [BFRecordItemModel]? {
  27. didSet {
  28. if data?.count ?? 0 > 0 {
  29. for item in data! {
  30. item.generationTimeRanges()
  31. }
  32. }
  33. }
  34. }
  35. var count = 0
  36. var stickerRanges = [CMTimeRange]()
  37. var exporter: PQCompositionExporter?
  38. // var mStickers = [PQEditVisionTrackMaterialsModel]()
  39. deinit {}
  40. public init() {}
  41. // MARK: -
  42. /// synthesisAll: 合成所有还是只合成录音部分
  43. public func startExprot(synthesisAll: Bool) {
  44. // 1,背景视频素材
  45. if let itemModels = data {
  46. var totalDur = 0.0
  47. // 切割视频素材
  48. for (_, itemModel) in itemModels.enumerated() {
  49. if let localPath = itemModel.localPath {
  50. if !FileManager.default.fileExists(atPath: localPath) {
  51. let error = NSError(domain: "err", code: ExportError.FileNotExist.rawValue, userInfo: ["msg": "file not exist"])
  52. exportCompletion?(error as Error, nil)
  53. return
  54. }
  55. itemModel.videoStickers.removeAll()
  56. if synthesisAll {
  57. var subDur = 0.0
  58. let drangs = itemModel.dealedDurationRanges
  59. for srange in drangs {
  60. let range = srange.range
  61. let sticker = splitBaseMaterial(timelineIn: totalDur + subDur, model_in: range.start.seconds, duration: range.duration.seconds)
  62. sticker.locationPath = localPath
  63. sticker.volumeGain = Float64(srange.isRecord ? originSoundInRecordVolumn*100 : originSoundVolumn*100)
  64. itemModel.videoStickers.append(sticker)
  65. subDur += range.duration.seconds
  66. }
  67. totalDur += subDur
  68. } else {
  69. var subDur = 0.0
  70. var drangs = itemModel.dealedDurationRanges.filter { srange in
  71. srange.isRecord == true
  72. }
  73. // 是否按录音顺序排列
  74. let needSort = false
  75. if needSort {
  76. drangs.sort { range1, range2 in
  77. range1.index < range2.index
  78. }
  79. }
  80. for srange in drangs {
  81. let range = srange.range
  82. let sticker = splitBaseMaterial(timelineIn: totalDur + subDur, model_in: range.start.seconds, duration: range.duration.seconds)
  83. sticker.locationPath = localPath
  84. sticker.volumeGain = Float64(srange.isRecord ? originSoundInRecordVolumn*100 : originSoundVolumn*100)
  85. itemModel.videoStickers.append(sticker)
  86. subDur += range.duration.seconds
  87. }
  88. totalDur += subDur
  89. }
  90. }
  91. }
  92. beginExport(synthesisAll: synthesisAll)
  93. }
  94. }
  95. public func cancelExport() {
  96. exporter?.cancel()
  97. }
  98. public func clearFileCache() {
  99. data?.forEach { itemModel in
  100. itemModel.voiceStickers.forEach { model in
  101. if let localPath = model.wavFilePath {
  102. try? FileManager.default.removeItem(atPath: localPath)
  103. }
  104. }
  105. }
  106. }
  107. enum DispatchError: Error {
  108. case timeout
  109. }
  110. func getOutputFilePath() -> URL {
  111. var outPutMP4Path = exportVideosDirectory
  112. if !directoryIsExists(dicPath: outPutMP4Path) {
  113. createDirectory(path: outPutMP4Path)
  114. }
  115. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  116. return URL(fileURLWithPath: outPutMP4Path)
  117. }
  118. // 切割素材
  119. func splitBaseMaterial(timelineIn: Double, model_in: Double, duration: Double) -> PQEditVisionTrackMaterialsModel {
  120. let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
  121. bgMovieInfo.type = StickerType.VIDEO.rawValue
  122. bgMovieInfo.timelineIn = timelineIn
  123. bgMovieInfo.timelineOut = timelineIn + duration
  124. bgMovieInfo.model_in = model_in
  125. bgMovieInfo.out = model_in + duration
  126. bgMovieInfo.canvasFillType = stickerContentMode.aspectFitStr.rawValue
  127. bgMovieInfo.volumeGain = 1
  128. bgMovieInfo.aptDuration = bgMovieInfo.timelineOut
  129. bgMovieInfo.duration = bgMovieInfo.timelineOut
  130. BFLog(1, message: "hhh- timIn:\(timelineIn), modIn:\(model_in), dur:\(duration)")
  131. return bgMovieInfo
  132. }
  133. func beginExport(synthesisAll: Bool) {
  134. // 输出视频地址
  135. // exprotVideo()
  136. // return;
  137. var outPutMP4Path = exportVideosDirectory
  138. if !directoryIsExists(dicPath: outPutMP4Path) {
  139. createDirectory(path: outPutMP4Path)
  140. }
  141. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  142. let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
  143. BFLog(1, message: "导出视频地址 \(outPutMP4URL)")
  144. guard let itemData = data else {
  145. let error = NSError(domain: "err", code: ExportError.DataLost.rawValue, userInfo: ["msg": "所有数据丢失"])
  146. exportCompletion?(error as Error, nil)
  147. return
  148. }
  149. // 处理导出
  150. var voiceList = [PQVoiceModel]()
  151. var videoStickers = [PQEditVisionTrackMaterialsModel]()
  152. var titleStickers = [PQEditSubTitleModel]()
  153. for itemModel in itemData {
  154. voiceList.append(contentsOf: itemModel.voiceStickers)
  155. videoStickers.append(contentsOf: itemModel.videoStickers)
  156. titleStickers.append(contentsOf: itemModel.titleStickers)
  157. }
  158. guard let voiceCount = data?.reduce(0, { partialResult, itemModell in
  159. itemModell.voiceStickers.count + partialResult
  160. }) else {
  161. BFLog(1, message: "voiceStickers count += nil")
  162. let error = NSError(domain: "err", code: ExportError.VoiceLost.rawValue, userInfo: ["msg": "voiceStickers count += nil"])
  163. exportCompletion?(error as Error, nil)
  164. return
  165. }
  166. guard let totalDuration = data?.reduce(0, { partialResult, itemModell in
  167. itemModell.materialDuraion + partialResult
  168. }) else {
  169. let error = NSError(domain: "err", code: ExportError.TotalDurError.rawValue, userInfo: ["msg": "时长计算出错"])
  170. exportCompletion?(error as Error, nil)
  171. return
  172. }
  173. // 有录音操作或者多个视频,就会进入合成步骤,否则就是一个没有处理的素材,直接导出就行了
  174. if voiceCount > 0 || videoStickers.count > 1 {
  175. let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList, synthesisAll: synthesisAll)
  176. var filters:[PQBaseFilter] = Array.init()
  177. for sticker in videoStickers {
  178. filters.append(PQMovieFilter(movieSticker: sticker))
  179. }
  180. let outputSize:CGSize = CGSize(width: 1080, height: Int(1080 * CGFloat(UIScreen.main.bounds.size.height / UIScreen.main.bounds.size.width)))
  181. BFLog(message: "输出视频大小:\(outputSize)")
  182. //add by ak 有字幕数据 & 显示字幕开关打开 添加字幕filter
  183. if(titleStickers.count > 0 && ( titleStickers.first?.setting.subtitleIsShow ?? true)){
  184. filters.append(PQSubTitleFilter.init(st: titleStickers, inputSize: outputSize))
  185. }
  186. exporter = PQCompositionExporter(asset: composition, videoComposition: nil, audioMix: audioMix, filters: filters, animationTool: nil, exportURL: outPutMP4URL)
  187. var orgeBitRate = Int(outputSize.width * outputSize.height * 3)
  188. for stick in videoStickers {
  189. if stick.type == StickerType.VIDEO.rawValue, stick.locationPath.count > 0 {
  190. let asset = AVURLAsset(url: URL(fileURLWithPath: stick.locationPath), options: avAssertOptions)
  191. let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
  192. if Int(cbr ?? 0) > orgeBitRate {
  193. orgeBitRate = Int(cbr ?? 0)
  194. }
  195. }
  196. }
  197. BFLog(1, message: "导出设置的码率为:\(Double(orgeBitRate)/1024.0/1024.0)M")
  198. let preSize = Double(orgeBitRate) * totalDuration / (1024*1024)
  199. let freeSize = PQBridgeObject.getPhoneDiskFreeSize()
  200. if preSize + 100.0 > freeSize { // 存储完后磁盘剩余至少100M
  201. let error = NSError(domain: "err", code: ExportError.DiskNoSpace.rawValue, userInfo: ["msg":"磁盘空间不足"])
  202. self.exportCompletion?(error as Error, nil)
  203. return
  204. }
  205. let tempBeginExport = Date().timeIntervalSince1970
  206. if exporter!.prepare(videoSize: outputSize, videoAverageBitRate: orgeBitRate) {
  207. exporter!.start(playeTimeRange: CMTimeRange(start: CMTime.zero, end: synthesisAll ? CMTime(seconds: totalDuration, preferredTimescale: 100) : composition.duration))
  208. }
  209. exporter?.progressClosure = { [weak self] _, _, progress in
  210. // BFLog(message: "正片合成进度 \(progress * 100)%")
  211. let useProgress = progress > 1 ? 1 : progress
  212. if progress > 0 { // 更新进度
  213. self?.progress?(useProgress)
  214. }
  215. }
  216. exporter?.completion = { [weak self] url in
  217. // 输出视频时长
  218. if let url = url {
  219. let outSeconds = CMTimeGetSeconds(AVAsset(url: url).duration)
  220. let exportEndTime = Date().timeIntervalSince1970
  221. // BFLog(1, message: "视频导出完成: \(String(describing: url)) 生成视频时长为:\(outSeconds) 总用时:\(exportEndTime - tempBeginExport)")
  222. print("生成视频时长为:\(outSeconds) 总用时:\(exportEndTime - tempBeginExport)")
  223. cShowHUB(superView: nil, msg: (outSeconds == 0) ? "合成失败请重试。" : "合成成功")
  224. self?.exportCompletion?(nil, url)
  225. } else {
  226. let error = NSError(domain: "err", code: ExportError.ExportExcept.rawValue, userInfo: ["msg":"导出异常失败"])
  227. self?.exportCompletion?(error as Error, nil)
  228. cShowHUB(superView: nil, msg: "导出失败")
  229. }
  230. // 导出完成后取消导出
  231. self?.exporter?.cancel()
  232. }
  233. } else {
  234. // 没有处理,直接copy原文件
  235. if let localPath = data?.first?.localPath {
  236. exportCompletion?(nil, URL(fileURLWithPath: localPath))
  237. }
  238. }
  239. }
  240. func dealAsset() {
  241. // asset?.tracks.first(where: { track in
  242. // if track.mediaType == .audio{
  243. //
  244. // }
  245. // })
  246. }
  247. func getVideoSize(asset: AVURLAsset) -> CGSize {
  248. var size = CGSize.zero
  249. asset.tracks.forEach { track in
  250. if track.mediaType == .video {
  251. let realSize = __CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform)
  252. size = CGSize(width: ceil(abs(realSize.width)), height: ceil(abs(realSize.height)))
  253. }
  254. }
  255. return size
  256. }
  257. }
  258. extension BFRecordExport {
  259. func mergeAudio(videoStickers: [PQEditVisionTrackMaterialsModel], audios: [PQVoiceModel]?, synthesisAll: Bool) -> (AVMutableAudioMix, AVMutableComposition) {
  260. let composition = AVMutableComposition()
  261. let audioMix = AVMutableAudioMix()
  262. var tempParameters = [AVMutableAudioMixInputParameters]()
  263. var totalDuration: Float64 = 0
  264. for sticker in videoStickers {
  265. if sticker.volumeGain == 0 {
  266. // 如果添加了会有刺啦音
  267. BFLog(message: "音频音量 为0 不添加")
  268. continue
  269. }
  270. // sticker.volumeGain = 50
  271. totalDuration = max(totalDuration, sticker.duration)
  272. tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  273. }
  274. if let voices = audios {
  275. if synthesisAll {
  276. tempParameters += mergeRecordVoiceAll(voices: voices, composition)
  277. } else {
  278. tempParameters += mergeRecordVoiceOnly(voices: voices, composition)
  279. }
  280. }
  281. audioMix.inputParameters = tempParameters
  282. return (audioMix, composition)
  283. }
  284. func mergeRecordVoiceOnly(voices: [PQVoiceModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  285. var tempParameters = [AVMutableAudioMixInputParameters]()
  286. var totalDur: Double = 0.0
  287. for model in voices {
  288. if model.volume == 0 {
  289. // 如果添加了会有刺啦音
  290. BFLog(message: "音频音量 为0 不添加")
  291. continue
  292. }
  293. let duration = model.endTime - model.startTime
  294. let sticker = PQEditVisionTrackMaterialsModel()
  295. sticker.model_in = 0
  296. sticker.out = duration
  297. sticker.timelineIn = totalDur
  298. sticker.aptDuration = duration
  299. sticker.duration = duration
  300. sticker.locationPath = model.wavFilePath
  301. sticker.volumeGain = 100 // Float64(model.volume)
  302. tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  303. totalDur += duration
  304. }
  305. return tempParameters
  306. }
  307. func mergeRecordVoiceAll(voices: [PQVoiceModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  308. var tempParameters = [AVMutableAudioMixInputParameters]()
  309. for model in voices {
  310. if model.volume == 0 {
  311. // 如果添加了会有刺啦音
  312. BFLog(message: "音频音量 为0 不添加")
  313. continue
  314. }
  315. let sticker = PQEditVisionTrackMaterialsModel()
  316. sticker.model_in = 0
  317. sticker.timelineIn = model.startTime
  318. sticker.out = model.endTime
  319. sticker.aptDuration = model.endTime - model.startTime
  320. sticker.duration = sticker.aptDuration
  321. sticker.locationPath = model.wavFilePath
  322. sticker.volumeGain = 100 // Float64(model.volume)
  323. tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  324. }
  325. return tempParameters
  326. }
  327. }