BFRecordExport.swift.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. //
  2. // BFRecordExport.swift.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. mStickers.append(bgMovieInfo)
  47. beginExport(videoStickers: mStickers, audioAsset: self.audioAssets)
  48. }
  49. public func cancelExport(){
  50. self.exporter?.cancel()
  51. }
  52. enum DispatchError: Error {
  53. case timeout
  54. }
  55. func beginExport(videoStickers:[PQEditVisionTrackMaterialsModel], audioAsset: [AVURLAsset]?) {
  56. // 输出视频地址
  57. var outPutMP4Path = exportVideosDirectory
  58. if !directoryIsExists(dicPath: outPutMP4Path) {
  59. createDirectory(path: outPutMP4Path)
  60. }
  61. outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
  62. let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
  63. BFLog(1, message: "导出视频地址 \(outPutMP4URL)")
  64. // 处理导出
  65. if (audioAsset?.count ?? 0 ) > 0 || videoStickers.count > 1 {
  66. var audioUrl:URL?
  67. if audioAsset?.count ?? 0 > 0 {
  68. // 多音频合成
  69. if let list = voiceList?.map({ model in
  70. URL(fileURLWithPath: model.wavFilePath)
  71. }){
  72. if list.count == 1 {
  73. audioUrl = list.first
  74. }else {
  75. let semaphore = DispatchSemaphore(value: 0)
  76. PQPlayerViewModel.mergeAudios(urls: list) { completURL in
  77. audioUrl = completURL
  78. BFLog(1, message: "异步做同步")
  79. semaphore.signal()
  80. }
  81. _ = semaphore.wait(timeout: .now() + 5)
  82. }
  83. }
  84. }
  85. // 每次初始化的时候设置初始值 为 nIl
  86. var audioMix: AVMutableAudioMix?
  87. var composition: AVMutableComposition?
  88. let filter = mStickers.map { sticker in
  89. PQMovieFilter(movieSticker: sticker)
  90. }
  91. // 有
  92. if let completURL = audioUrl {
  93. let inputAsset = AVURLAsset(url: completURL, options: avAssertOptions)
  94. // (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: videoStickers)
  95. //使用原视频无音版
  96. (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: nil)
  97. if composition != nil {
  98. exporter = PQCompositionExporter(asset: composition!, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  99. }else {
  100. exporter = PQCompositionExporter(asset: inputAsset, videoComposition: nil, audioMix: nil, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
  101. }
  102. }
  103. let size = getVideoSize()
  104. var orgeBitRate = Int(size.width * size.height * 3)
  105. for stick in mStickers {
  106. if stick.type == StickerType.VIDEO.rawValue {
  107. let asset = AVURLAsset(url: URL(fileURLWithPath: stick.locationPath), options: avAssertOptions)
  108. let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
  109. if Int(cbr ?? 0) > orgeBitRate {
  110. orgeBitRate = Int(cbr ?? 0)
  111. }
  112. }
  113. }
  114. BFLog(message: "导出设置的码率为:\(orgeBitRate)")
  115. if exporter!.prepare(videoSize: size, videoAverageBitRate: orgeBitRate) {
  116. exporter!.start(playeTimeRange: CMTimeRange(start: CMTime.zero, end: asset?.duration ?? CMTime.zero))
  117. }
  118. exporter?.progressClosure = { [weak self] _, _, progress in
  119. // BFLog(message: "正片合成进度 \(progress * 100)%")
  120. let useProgress = progress > 1 ? 1 : progress
  121. if progress > 0 { // 更新进度
  122. self?.progress?(useProgress)
  123. }
  124. }
  125. exporter?.completion = { [weak self] url in
  126. // 输出视频时长
  127. if let url = url {
  128. let outSeconds = CMTimeGetSeconds(AVAsset(url: url).duration)
  129. BFLog(1, message: "无水印的视频导出完成: \(String(describing: url)) 生成视频时长为:\(outSeconds)")
  130. cShowHUB(superView: nil, msg: ( outSeconds == 0) ? "合成失败请重试。" : "合成成功")
  131. self?.saveVideoToPhoto(url: url)
  132. self?.exportCompletion?(nil, url)
  133. }else{
  134. let error = NSError(domain: "err", code: -1, userInfo: nil)
  135. self?.exportCompletion?(error as Error, nil)
  136. }
  137. // 导出完成后取消导出
  138. self?.exporter?.cancel()
  139. }
  140. } else {
  141. // 没有处理,直接copy原文件
  142. self.exportCompletion?(nil, self.asset?.url)
  143. }
  144. }
  145. func saveVideoToPhoto(url:URL){
  146. PHPhotoLibrary.shared().performChanges {
  147. PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
  148. } completionHandler: { isFinished, _ in
  149. DispatchQueue.main.async {
  150. cShowHUB(superView: nil, msg: "保存成功")
  151. }
  152. }
  153. }
  154. func dealAsset(){
  155. // asset?.tracks.first(where: { track in
  156. // if track.mediaType == .audio{
  157. //
  158. // }
  159. // })
  160. }
  161. func getVideoSize() -> CGSize{
  162. var size = CGSize.zero
  163. self.asset?.tracks.forEach({ track in
  164. if track.mediaType == .video{
  165. let realSize = __CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform)
  166. size = CGSize(width: abs(realSize.width), height: abs(realSize.height))
  167. }
  168. })
  169. return size
  170. }
  171. }