BFRecordExport.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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. // 视频素材
  17. public var asset:AVURLAsset?
  18. // 录音段
  19. public var voiceList:[PQVoiceModel]? {
  20. didSet {
  21. // audioAssets = voiceList?.map({ model in
  22. // AVURLAsset(url: URL(fileURLWithPath: model.wavFilePath))
  23. // })
  24. }
  25. }
  26. var count = 0
  27. var audioAssets : [AVURLAsset]?
  28. var exporter : PQCompositionExporter?
  29. var mStickers = [PQEditVisionTrackMaterialsModel]()
  30. deinit {
  31. }
  32. public init(){}
  33. //MARK: -
  34. public func startExprot(){
  35. // 1,背景视频素材
  36. let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
  37. bgMovieInfo.type = StickerType.VIDEO.rawValue
  38. bgMovieInfo.locationPath = ((asset?.url.absoluteString)?.removingPercentEncoding ?? "").replacingOccurrences(of: "file://", with: "")
  39. bgMovieInfo.timelineIn = 0
  40. bgMovieInfo.timelineOut = CMTimeGetSeconds(asset?.duration ?? CMTime.zero)
  41. bgMovieInfo.model_in = bgMovieInfo.timelineIn
  42. bgMovieInfo.out = bgMovieInfo.timelineOut
  43. bgMovieInfo.canvasFillType = stickerContentMode.aspectFitStr.rawValue
  44. bgMovieInfo.volumeGain = 30
  45. bgMovieInfo.aptDuration = bgMovieInfo.timelineOut
  46. bgMovieInfo.duration = bgMovieInfo.timelineOut
  47. mStickers.append(bgMovieInfo)
  48. beginExport(videoStickers: mStickers)
  49. }
  50. public func cancelExport(){
  51. self.exporter?.cancel()
  52. }
  53. enum DispatchError: Error {
  54. case timeout
  55. }
  56. func getOutputFilePath() -> URL{
  57. var outPutMP4Path = exportVideosDirectory
  58. if !directoryIsExists(dicPath: outPutMP4Path) {
  59. createDirectory(path: outPutMP4Path)
  60. }
  61. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  62. return URL(fileURLWithPath: outPutMP4Path)
  63. }
  64. func exprotVideo(){
  65. // //重新创建GPUImageMovie用于保存
  66. // saveMovie = [[GPUImageMovie alloc] initWithURL:self.pathURL];
  67. let saveFilter = GPUImageFilter()
  68. let saveMovie = GPUImageMovie(url: asset?.url)
  69. saveMovie?.shouldRepeat = false
  70. saveMovie?.addTarget(saveFilter)
  71. let filePath = getOutputFilePath()
  72. let savewrite = GPUImageMovieWriter(movieURL: getOutputFilePath(), size: getVideoSize())
  73. savewrite?.shouldPassthroughAudio = true
  74. savewrite?.encodingLiveVideo = true
  75. saveFilter.addTarget(savewrite)
  76. saveMovie?.enableSynchronizedEncoding(using: savewrite)
  77. // saveMovie?.audioEncodingTarget = savewrite
  78. savewrite?.startRecording()
  79. saveMovie?.startProcessing()
  80. savewrite?.completionBlock = {
  81. DispatchQueue.main.async { [weak self, weak savewrite] in
  82. saveFilter.removeTarget(savewrite)
  83. savewrite?.finishRecording()
  84. saveMovie?.cancelProcessing()
  85. saveMovie?.removeTarget(saveFilter)
  86. cShowHUB(superView: nil, msg: "合成成功")
  87. self?.exportCompletion?(nil, filePath)
  88. self?.saveVideoToPhoto(url: filePath)
  89. }
  90. }
  91. }
  92. func beginExport(videoStickers:[PQEditVisionTrackMaterialsModel]) {
  93. // 输出视频地址
  94. // exprotVideo()
  95. // return;
  96. var outPutMP4Path = exportVideosDirectory
  97. if !directoryIsExists(dicPath: outPutMP4Path) {
  98. createDirectory(path: outPutMP4Path)
  99. }
  100. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  101. let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
  102. BFLog(1, message: "导出视频地址 \(outPutMP4URL)")
  103. // 处理导出
  104. if (voiceList?.count ?? 0 ) > 0 || videoStickers.count > 1 {
  105. // var audioUrl:URL?
  106. // if audioAsset?.count ?? 0 > 0 {
  107. // // 多音频合成
  108. // if let list = voiceList?.map({ model in
  109. // URL(fileURLWithPath: model.wavFilePath)
  110. // }){
  111. // if list.count == 1 {
  112. // audioUrl = list.first
  113. // }else {
  114. // let semaphore = DispatchSemaphore(value: 0)
  115. // PQPlayerViewModel.mergeAudios(urls: list) { completURL in
  116. // audioUrl = completURL
  117. // semaphore.signal()
  118. // }
  119. // _ = semaphore.wait(timeout: .now() + 5)
  120. // }
  121. // }
  122. // }
  123. let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList)
  124. let filter = mStickers.map { sticker in
  125. PQMovieFilter(movieSticker: sticker)
  126. }
  127. // 有
  128. // if let completURL = audioUrl {
  129. // let inputAsset = AVURLAsset(url: completURL, options: avAssertOptions)
  130. //// (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: videoStickers)
  131. // //使用原视频无音版
  132. // (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: nil)
  133. //
  134. // if composition != nil {
  135. // exporter = PQCompositionExporter(asset: composition!, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  136. // }else {
  137. // exporter = PQCompositionExporter(asset: inputAsset, videoComposition: nil, audioMix: nil, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  138. // }
  139. // }
  140. exporter = PQCompositionExporter(asset: composition, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  141. let size = getVideoSize()
  142. var orgeBitRate = Int(size.width * size.height * 3)
  143. for stick in mStickers {
  144. if stick.type == StickerType.VIDEO.rawValue {
  145. let asset = AVURLAsset(url: URL(fileURLWithPath: stick.locationPath), options: avAssertOptions)
  146. let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
  147. if Int(cbr ?? 0) > orgeBitRate {
  148. orgeBitRate = Int(cbr ?? 0)
  149. }
  150. }
  151. }
  152. BFLog(message: "导出设置的码率为:\(orgeBitRate)")
  153. if exporter!.prepare(videoSize: size, videoAverageBitRate: orgeBitRate) {
  154. exporter!.start(playeTimeRange: CMTimeRange(start: CMTime.zero, end: asset?.duration ?? CMTime.zero))
  155. }
  156. exporter?.progressClosure = { [weak self] _, _, progress in
  157. // BFLog(message: "正片合成进度 \(progress * 100)%")
  158. let useProgress = progress > 1 ? 1 : progress
  159. if progress > 0 { // 更新进度
  160. self?.progress?(useProgress)
  161. }
  162. }
  163. exporter?.completion = { [weak self] url in
  164. // 输出视频时长
  165. if let url = url {
  166. let outSeconds = CMTimeGetSeconds(AVAsset(url: url).duration)
  167. BFLog(1, message: "无水印的视频导出完成: \(String(describing: url)) 生成视频时长为:\(outSeconds)")
  168. cShowHUB(superView: nil, msg: ( outSeconds == 0) ? "合成失败请重试。" : "合成成功")
  169. self?.saveVideoToPhoto(url: url)
  170. self?.exportCompletion?(nil, url)
  171. }else{
  172. let error = NSError(domain: "err", code: -1, userInfo: nil)
  173. self?.exportCompletion?(error as Error, nil)
  174. cShowHUB(superView: nil, msg: "导出失败")
  175. }
  176. // 导出完成后取消导出
  177. self?.exporter?.cancel()
  178. }
  179. } else {
  180. // 没有处理,直接copy原文件
  181. self.exportCompletion?(nil, self.asset?.url)
  182. }
  183. }
  184. func saveVideoToPhoto(url:URL){
  185. PHPhotoLibrary.shared().performChanges {
  186. PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
  187. } completionHandler: { isFinished, _ in
  188. if isFinished {
  189. DispatchQueue.main.async {
  190. cShowHUB(superView: nil, msg: "保存成功")
  191. }
  192. }
  193. }
  194. }
  195. func dealAsset(){
  196. // asset?.tracks.first(where: { track in
  197. // if track.mediaType == .audio{
  198. //
  199. // }
  200. // })
  201. }
  202. func getVideoSize() -> CGSize{
  203. var size = CGSize.zero
  204. self.asset?.tracks.forEach({ track in
  205. if track.mediaType == .video{
  206. let realSize = __CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform)
  207. size = CGSize(width: ceil(abs(realSize.width)), height: ceil(abs(realSize.height)))
  208. }
  209. })
  210. return size
  211. }
  212. }
  213. extension BFRecordExport {
  214. func mergeAudio(videoStickers:[PQEditVisionTrackMaterialsModel], audios:[PQVoiceModel]?) -> (AVMutableAudioMix, AVMutableComposition){
  215. let composition = AVMutableComposition()
  216. let audioMix = AVMutableAudioMix()
  217. var tempParameters = [AVMutableAudioMixInputParameters]()
  218. var totalDuration : Float64 = 0
  219. for sticker in videoStickers {
  220. if sticker.volumeGain == 0 {
  221. // 如果添加了会有刺啦音
  222. BFLog(message: "音频音量 为0 不添加")
  223. continue
  224. }
  225. sticker.volumeGain = 2
  226. totalDuration = max(totalDuration, sticker.duration)
  227. tempParameters += PQVideoEditViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  228. }
  229. if let voices = audios {
  230. for model in voices {
  231. if model.volume == 0 {
  232. // 如果添加了会有刺啦音
  233. BFLog(message: "音频音量 为0 不添加")
  234. continue
  235. }
  236. let sticker = PQEditVisionTrackMaterialsModel()
  237. sticker.model_in = 0
  238. sticker.timelineIn = model.startTime
  239. sticker.out = model.endTime
  240. sticker.aptDuration = model.endTime - model.startTime
  241. sticker.duration = sticker.aptDuration
  242. sticker.locationPath = model.wavFilePath
  243. sticker.volumeGain = 100 //Float64(model.volume)
  244. tempParameters += PQVideoEditViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
  245. }
  246. }
  247. audioMix.inputParameters = tempParameters
  248. return (audioMix, composition)
  249. }
  250. }