BFRecordExport.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. //
  2. // BFRecordExport.swift
  3. // BFRecordScreenKit
  4. //
  5. // Created by 胡志强 on 2021/11/25.
  6. // 录屏视频导出
  7. import Foundation
  8. import AVFoundation
  9. import BFFramework
  10. import BFVideoEditKit
  11. import Photos
  12. import GPUImage
  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. }
  31. public init(){}
  32. //MARK: -
  33. /// synthesisAll: 合成所有还是只合成录音部分
  34. public func startExprot(synthesisAll:Bool){
  35. // 1,背景视频素材
  36. if let itemModels = data {
  37. var totalDur = 0.0
  38. for (index, itemModel) in itemModels.enumerated() {
  39. itemModel.videoStickers.removeAll()
  40. let asset = itemModel.baseMaterial
  41. if let dur = asset?.duration.seconds {
  42. if synthesisAll {
  43. let bgMovieInfo = splitBaseMaterial(timelineIn: totalDur, model_in: 0, duration: dur)
  44. bgMovieInfo.volumeGain = 0
  45. itemModel.videoStickers.append(bgMovieInfo)
  46. totalDur += dur
  47. } else {
  48. var subDur = 0.0
  49. let drangs = itemModel.dealedDurationRanges.filter { ranges in
  50. ranges.isRecord == true
  51. }
  52. for srange in drangs {
  53. let range = srange.range
  54. let sticker = splitBaseMaterial(timelineIn: (totalDur + subDur), model_in: range.start.seconds, duration: range.duration.seconds)
  55. sticker.volumeGain = 0
  56. itemModel.videoStickers.append(sticker)
  57. subDur += range.duration.seconds
  58. }
  59. totalDur += subDur
  60. }
  61. }
  62. }
  63. beginExport(synthesisAll:synthesisAll)
  64. }
  65. }
  66. public func cancelExport(){
  67. self.exporter?.cancel()
  68. }
  69. enum DispatchError: Error {
  70. case timeout
  71. }
  72. func getOutputFilePath() -> URL{
  73. var outPutMP4Path = exportVideosDirectory
  74. if !directoryIsExists(dicPath: outPutMP4Path) {
  75. createDirectory(path: outPutMP4Path)
  76. }
  77. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  78. return URL(fileURLWithPath: outPutMP4Path)
  79. }
  80. func splitBaseMaterial(timelineIn:Double, model_in:Double, duration:Double) -> PQEditVisionTrackMaterialsModel{
  81. let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
  82. if let asset = data?.first?.baseMaterial {
  83. bgMovieInfo.type = StickerType.VIDEO.rawValue
  84. bgMovieInfo.locationPath = ((asset.url.absoluteString).removingPercentEncoding ?? "").replacingOccurrences(of: "file://", with: "")
  85. bgMovieInfo.timelineIn = timelineIn
  86. bgMovieInfo.timelineOut = timelineIn + duration
  87. bgMovieInfo.model_in = model_in
  88. bgMovieInfo.out = model_in + duration
  89. bgMovieInfo.canvasFillType = stickerContentMode.aspectFitStr.rawValue
  90. bgMovieInfo.volumeGain = 1
  91. bgMovieInfo.aptDuration = bgMovieInfo.timelineOut
  92. bgMovieInfo.duration = bgMovieInfo.timelineOut
  93. BFLog(1, message: "hhh- timIn:\(timelineIn), modIn:\(model_in), dur:\(duration)")
  94. }
  95. return bgMovieInfo
  96. }
  97. func beginExport(synthesisAll:Bool) {
  98. // 输出视频地址
  99. // exprotVideo()
  100. // return;
  101. var outPutMP4Path = exportVideosDirectory
  102. if !directoryIsExists(dicPath: outPutMP4Path) {
  103. createDirectory(path: outPutMP4Path)
  104. }
  105. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  106. let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
  107. BFLog(1, message: "导出视频地址 \(outPutMP4URL)")
  108. guard let itemModel = data?.first else {
  109. return
  110. }
  111. // 处理导出
  112. let voiceList = itemModel.voiceStickers
  113. let videoStickers = itemModel.videoStickers
  114. if voiceList.count > 0 || videoStickers.count > 1 {
  115. let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList,synthesisAll:synthesisAll)
  116. let filter = videoStickers.map { sticker in
  117. PQMovieFilter(movieSticker: sticker)
  118. }
  119. // 有
  120. // if let completURL = audioUrl {
  121. // let inputAsset = AVURLAsset(url: completURL, options: avAssertOptions)
  122. //// (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: videoStickers)
  123. // //使用原视频无音版
  124. // (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: nil)
  125. //
  126. // if composition != nil {
  127. // exporter = PQCompositionExporter(asset: composition!, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  128. // }else {
  129. // exporter = PQCompositionExporter(asset: inputAsset, videoComposition: nil, audioMix: nil, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  130. // }
  131. // }
  132. exporter = PQCompositionExporter(asset: composition, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  133. let asset = data?.first?.baseMaterial
  134. let size = getVideoSize(asset: asset!)
  135. var orgeBitRate = Int(size.width * size.height * 3)
  136. for stick in videoStickers {
  137. if stick.type == StickerType.VIDEO.rawValue {
  138. let asset = AVURLAsset(url: URL(fileURLWithPath: stick.locationPath), options: avAssertOptions)
  139. let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
  140. if Int(cbr ?? 0) > orgeBitRate {
  141. orgeBitRate = Int(cbr ?? 0)
  142. }
  143. }
  144. }
  145. BFLog(message: "导出设置的码率为:\(orgeBitRate)")
  146. if exporter!.prepare(videoSize: size, videoAverageBitRate: orgeBitRate) {
  147. exporter!.start(playeTimeRange: CMTimeRange(start: CMTime.zero, end: synthesisAll ? asset?.duration as! CMTime : composition.duration))
  148. }
  149. exporter?.progressClosure = { [weak self] _, _, progress in
  150. // BFLog(message: "正片合成进度 \(progress * 100)%")
  151. let useProgress = progress > 1 ? 1 : progress
  152. if progress > 0 { // 更新进度
  153. self?.progress?(useProgress)
  154. }
  155. }
  156. exporter?.completion = { [weak self] url in
  157. // 输出视频时长
  158. if let url = url {
  159. let outSeconds = CMTimeGetSeconds(AVAsset(url: url).duration)
  160. BFLog(1, message: "无水印的视频导出完成: \(String(describing: url)) 生成视频时长为:\(outSeconds)")
  161. cShowHUB(superView: nil, msg: ( outSeconds == 0) ? "合成失败请重试。" : "合成成功")
  162. self?.exportCompletion?(nil, url)
  163. }else{
  164. let error = NSError(domain: "err", code: -1, userInfo: nil)
  165. self?.exportCompletion?(error as Error, nil)
  166. cShowHUB(superView: nil, msg: "导出失败")
  167. }
  168. // 导出完成后取消导出
  169. self?.exporter?.cancel()
  170. }
  171. } else {
  172. // 没有处理,直接copy原文件
  173. if let ass = data?.first?.baseMaterial{
  174. self.exportCompletion?(nil, ass.url)
  175. }
  176. }
  177. }
  178. func dealAsset(){
  179. // asset?.tracks.first(where: { track in
  180. // if track.mediaType == .audio{
  181. //
  182. // }
  183. // })
  184. }
  185. func getVideoSize(asset:AVURLAsset) -> CGSize{
  186. var size = CGSize.zero
  187. asset.tracks.forEach({ track in
  188. if track.mediaType == .video{
  189. let realSize = __CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform)
  190. size = CGSize(width: ceil(abs(realSize.width)), height: ceil(abs(realSize.height)))
  191. }
  192. })
  193. return size
  194. }
  195. }
  196. extension BFRecordExport {
  197. func mergeAudio(videoStickers:[PQEditVisionTrackMaterialsModel], audios:[PQVoiceModel]?, synthesisAll:Bool) -> (AVMutableAudioMix, AVMutableComposition){
  198. let composition = AVMutableComposition()
  199. let audioMix = AVMutableAudioMix()
  200. var tempParameters = [AVMutableAudioMixInputParameters]()
  201. var totalDuration : Float64 = 0
  202. for sticker in videoStickers {
  203. if sticker.volumeGain == 0 {
  204. // 如果添加了会有刺啦音
  205. BFLog(message: "音频音量 为0 不添加")
  206. continue
  207. }
  208. sticker.volumeGain = 50
  209. totalDuration = max(totalDuration, sticker.duration)
  210. tempParameters += PQVideoEditViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  211. }
  212. if let voices = audios {
  213. if synthesisAll {
  214. tempParameters += mergeRecordVoiceAll(voices:voices, composition)
  215. }else {
  216. tempParameters += mergeRecordVoiceOnly(voices:voices, composition)
  217. }
  218. }
  219. audioMix.inputParameters = tempParameters
  220. return (audioMix, composition)
  221. }
  222. func mergeRecordVoiceOnly(voices:[PQVoiceModel], _ composition:AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  223. var tempParameters = [AVMutableAudioMixInputParameters]()
  224. var totalDur:Double = 0.0
  225. for model in voices {
  226. if model.volume == 0 {
  227. // 如果添加了会有刺啦音
  228. BFLog(message: "音频音量 为0 不添加")
  229. continue
  230. }
  231. let duration = model.endTime - model.startTime
  232. let sticker = PQEditVisionTrackMaterialsModel()
  233. sticker.model_in = 0
  234. sticker.out = duration
  235. sticker.timelineIn = totalDur
  236. sticker.aptDuration = duration
  237. sticker.duration = duration
  238. sticker.locationPath = model.wavFilePath
  239. sticker.volumeGain = 100 //Float64(model.volume)
  240. tempParameters += PQVideoEditViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  241. totalDur += duration
  242. }
  243. return tempParameters
  244. }
  245. func mergeRecordVoiceAll(voices:[PQVoiceModel], _ composition:AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
  246. var tempParameters = [AVMutableAudioMixInputParameters]()
  247. for model in voices {
  248. if model.volume == 0 {
  249. // 如果添加了会有刺啦音
  250. BFLog(message: "音频音量 为0 不添加")
  251. continue
  252. }
  253. let sticker = PQEditVisionTrackMaterialsModel()
  254. sticker.model_in = 0
  255. sticker.timelineIn = model.startTime
  256. sticker.out = model.endTime
  257. sticker.aptDuration = model.endTime - model.startTime
  258. sticker.duration = sticker.aptDuration
  259. sticker.locationPath = model.wavFilePath
  260. sticker.volumeGain = 100 //Float64(model.volume)
  261. tempParameters += PQVideoEditViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  262. }
  263. return tempParameters
  264. }
  265. }