BFRecordExport.swift 14 KB

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