|
@@ -5,19 +5,19 @@
|
|
|
// Created by 胡志强 on 2021/11/25.
|
|
|
// 录屏视频导出
|
|
|
|
|
|
-import Foundation
|
|
|
import AVFoundation
|
|
|
-import Photos
|
|
|
-import GPUImage
|
|
|
import BFCommonKit
|
|
|
import BFMediaKit
|
|
|
+import Foundation
|
|
|
+import GPUImage
|
|
|
+import Photos
|
|
|
|
|
|
public class BFRecordExport {
|
|
|
- public var progress : ((Float)->Void)?
|
|
|
- public var exportCompletion : ((Error?, URL?)->Void)?
|
|
|
-
|
|
|
- public var data:[BFRecordItemModel]? {
|
|
|
- didSet{
|
|
|
+ public var progress: ((Float) -> Void)?
|
|
|
+ public var exportCompletion: ((Error?, URL?) -> Void)?
|
|
|
+
|
|
|
+ public var data: [BFRecordItemModel]? {
|
|
|
+ didSet {
|
|
|
if data?.count ?? 0 > 0 {
|
|
|
for item in data! {
|
|
|
item.generationTimeRanges()
|
|
@@ -27,30 +27,28 @@ public class BFRecordExport {
|
|
|
}
|
|
|
|
|
|
var count = 0
|
|
|
-
|
|
|
+
|
|
|
var stickerRanges = [CMTimeRange]()
|
|
|
-
|
|
|
- var exporter : PQCompositionExporter?
|
|
|
+
|
|
|
+ var exporter: PQCompositionExporter?
|
|
|
// var mStickers = [PQEditVisionTrackMaterialsModel]()
|
|
|
-
|
|
|
- deinit {
|
|
|
- }
|
|
|
- public init(){}
|
|
|
|
|
|
-
|
|
|
- //MARK: -
|
|
|
+ deinit {}
|
|
|
+
|
|
|
+ public init() {}
|
|
|
+
|
|
|
+ // MARK: -
|
|
|
|
|
|
/// synthesisAll: 合成所有还是只合成录音部分
|
|
|
- public func startExprot(synthesisAll:Bool){
|
|
|
+ public func startExprot(synthesisAll: Bool) {
|
|
|
// 1,背景视频素材
|
|
|
if let itemModels = data {
|
|
|
-
|
|
|
var totalDur = 0.0
|
|
|
-
|
|
|
+
|
|
|
// 切割视频素材
|
|
|
for (_, itemModel) in itemModels.enumerated() {
|
|
|
itemModel.videoStickers.removeAll()
|
|
|
-
|
|
|
+
|
|
|
if synthesisAll {
|
|
|
// 保留全部
|
|
|
// let bgMovieInfo = splitBaseMaterial(timelineIn: totalDur, model_in: 0, duration: dur)
|
|
@@ -61,7 +59,7 @@ public class BFRecordExport {
|
|
|
let drangs = itemModel.dealedDurationRanges
|
|
|
for srange in drangs {
|
|
|
let range = srange.range
|
|
|
- let sticker = splitBaseMaterial(timelineIn: (totalDur + subDur), model_in: range.start.seconds, duration: range.duration.seconds)
|
|
|
+ let sticker = splitBaseMaterial(timelineIn: totalDur + subDur, model_in: range.start.seconds, duration: range.duration.seconds)
|
|
|
sticker.volumeGain = srange.isRecord ? 0 : 100
|
|
|
itemModel.videoStickers.append(sticker)
|
|
|
subDur += range.duration.seconds
|
|
@@ -73,7 +71,7 @@ public class BFRecordExport {
|
|
|
var drangs = itemModel.dealedDurationRanges.filter { srange in
|
|
|
srange.isRecord == true
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 是否按录音顺序排列
|
|
|
let needSort = false
|
|
|
if needSort {
|
|
@@ -83,7 +81,7 @@ public class BFRecordExport {
|
|
|
}
|
|
|
for srange in drangs {
|
|
|
let range = srange.range
|
|
|
- let sticker = splitBaseMaterial(timelineIn: (totalDur + subDur), model_in: range.start.seconds, duration: range.duration.seconds)
|
|
|
+ let sticker = splitBaseMaterial(timelineIn: totalDur + subDur, model_in: range.start.seconds, duration: range.duration.seconds)
|
|
|
sticker.volumeGain = 0
|
|
|
itemModel.videoStickers.append(sticker)
|
|
|
subDur += range.duration.seconds
|
|
@@ -91,40 +89,38 @@ public class BFRecordExport {
|
|
|
totalDur += subDur
|
|
|
}
|
|
|
}
|
|
|
- beginExport(synthesisAll:synthesisAll)
|
|
|
+ beginExport(synthesisAll: synthesisAll)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- public func cancelExport(){
|
|
|
- self.exporter?.cancel()
|
|
|
+
|
|
|
+ public func cancelExport() {
|
|
|
+ exporter?.cancel()
|
|
|
}
|
|
|
-
|
|
|
- public func clearFileCache(){
|
|
|
- data?.forEach({ itemModel in
|
|
|
+
|
|
|
+ public func clearFileCache() {
|
|
|
+ data?.forEach { itemModel in
|
|
|
itemModel.voiceStickers.forEach { model in
|
|
|
- if let localPath = model.wavFilePath{
|
|
|
+ if let localPath = model.wavFilePath {
|
|
|
try? FileManager.default.removeItem(atPath: localPath)
|
|
|
}
|
|
|
}
|
|
|
- })
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
enum DispatchError: Error {
|
|
|
case timeout
|
|
|
}
|
|
|
-
|
|
|
- func getOutputFilePath() -> URL{
|
|
|
+
|
|
|
+ func getOutputFilePath() -> URL {
|
|
|
var outPutMP4Path = exportVideosDirectory
|
|
|
if !directoryIsExists(dicPath: outPutMP4Path) {
|
|
|
createDirectory(path: outPutMP4Path)
|
|
|
}
|
|
|
outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
|
|
|
return URL(fileURLWithPath: outPutMP4Path)
|
|
|
-
|
|
|
}
|
|
|
-
|
|
|
- func splitBaseMaterial(timelineIn:Double, model_in:Double, duration:Double) -> PQEditVisionTrackMaterialsModel{
|
|
|
+
|
|
|
+ func splitBaseMaterial(timelineIn: Double, model_in: Double, duration: Double) -> PQEditVisionTrackMaterialsModel {
|
|
|
let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
|
|
|
bgMovieInfo.type = StickerType.VIDEO.rawValue
|
|
|
bgMovieInfo.timelineIn = timelineIn
|
|
@@ -139,11 +135,11 @@ public class BFRecordExport {
|
|
|
bgMovieInfo.locationPath = localPath
|
|
|
}
|
|
|
BFLog(1, message: "hhh- timIn:\(timelineIn), modIn:\(model_in), dur:\(duration)")
|
|
|
-
|
|
|
+
|
|
|
return bgMovieInfo
|
|
|
}
|
|
|
-
|
|
|
- func beginExport(synthesisAll:Bool) {
|
|
|
+
|
|
|
+ func beginExport(synthesisAll: Bool) {
|
|
|
// 输出视频地址
|
|
|
// exprotVideo()
|
|
|
// return;
|
|
@@ -154,13 +150,13 @@ public class BFRecordExport {
|
|
|
outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
|
|
|
let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
|
|
|
BFLog(1, message: "导出视频地址 \(outPutMP4URL)")
|
|
|
-
|
|
|
+
|
|
|
guard let itemData = data else {
|
|
|
- let error = NSError(domain: "err", code: -1, userInfo: ["msg":"voiceStickers count += nil"])
|
|
|
- self.exportCompletion?(error as Error, nil)
|
|
|
+ let error = NSError(domain: "err", code: -1, userInfo: ["msg": "voiceStickers count += nil"])
|
|
|
+ exportCompletion?(error as Error, nil)
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 处理导出
|
|
|
var voiceList = [PQVoiceModel]()
|
|
|
var videoStickers = [PQEditVisionTrackMaterialsModel]()
|
|
@@ -170,54 +166,52 @@ public class BFRecordExport {
|
|
|
videoStickers.append(contentsOf: itemModel.videoStickers)
|
|
|
titleStickers.append(contentsOf: itemModel.titleStickers)
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
guard let voiceCount = data?.reduce(0, { partialResult, itemModell in
|
|
|
itemModell.voiceStickers.count + partialResult
|
|
|
}) else {
|
|
|
BFLog(1, message: "voiceStickers count += nil")
|
|
|
- let error = NSError(domain: "err", code: -1, userInfo: ["msg":"voiceStickers count += nil"])
|
|
|
- self.exportCompletion?(error as Error, nil)
|
|
|
+ let error = NSError(domain: "err", code: -1, userInfo: ["msg": "voiceStickers count += nil"])
|
|
|
+ exportCompletion?(error as Error, nil)
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
guard let totalDuration = data?.reduce(0, { partialResult, itemModell in
|
|
|
- (itemModell.materialDuraion ) + partialResult
|
|
|
+ itemModell.materialDuraion + partialResult
|
|
|
}) else {
|
|
|
- let error = NSError(domain: "err", code: -1, userInfo: ["msg":"时长计算出错"])
|
|
|
- self.exportCompletion?(error as Error, nil)
|
|
|
+ let error = NSError(domain: "err", code: -1, userInfo: ["msg": "时长计算出错"])
|
|
|
+ exportCompletion?(error as Error, nil)
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 有录音操作或者多个视频,就会进入合成步骤,否则就是一个没有处理的素材,直接导出就行了
|
|
|
if voiceCount > 0 || videoStickers.count > 1 {
|
|
|
+ let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList, synthesisAll: synthesisAll)
|
|
|
|
|
|
- let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList,synthesisAll:synthesisAll)
|
|
|
-
|
|
|
let filter = videoStickers.map { sticker in
|
|
|
PQMovieFilter(movieSticker: sticker)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
exporter = PQCompositionExporter(asset: composition, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL)
|
|
|
-
|
|
|
+
|
|
|
// let asset = data?.first?.baseMaterial // 可能为空
|
|
|
|
|
|
- let size = UIScreen.main.bounds.size //getVideoSize(asset: asset!)
|
|
|
+ let size = UIScreen.main.bounds.size // getVideoSize(asset: asset!)
|
|
|
var orgeBitRate = Int(size.width * size.height * 3)
|
|
|
-
|
|
|
+
|
|
|
for stick in videoStickers {
|
|
|
if stick.type == StickerType.VIDEO.rawValue {
|
|
|
let asset = AVURLAsset(url: URL(fileURLWithPath: stick.locationPath), options: avAssertOptions)
|
|
|
-
|
|
|
+
|
|
|
let cbr = asset.tracks(withMediaType: .video).first?.estimatedDataRate
|
|
|
if Int(cbr ?? 0) > orgeBitRate {
|
|
|
orgeBitRate = Int(cbr ?? 0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
BFLog(message: "导出设置的码率为:\(orgeBitRate)")
|
|
|
-
|
|
|
+
|
|
|
if exporter!.prepare(videoSize: size, videoAverageBitRate: orgeBitRate) {
|
|
|
exporter!.start(playeTimeRange: CMTimeRange(start: CMTime.zero, end: synthesisAll ? CMTime(seconds: totalDuration, preferredTimescale: 100) : composition.duration))
|
|
|
}
|
|
@@ -232,9 +226,9 @@ public class BFRecordExport {
|
|
|
// 输出视频时长
|
|
|
if let url = url {
|
|
|
let outSeconds = CMTimeGetSeconds(AVAsset(url: url).duration)
|
|
|
-
|
|
|
+
|
|
|
BFLog(1, message: "无水印的视频导出完成: \(String(describing: url)) 生成视频时长为:\(outSeconds)")
|
|
|
- cShowHUB(superView: nil, msg: ( outSeconds == 0) ? "合成失败请重试。" : "合成成功")
|
|
|
+ cShowHUB(superView: nil, msg: (outSeconds == 0) ? "合成失败请重试。" : "合成成功")
|
|
|
self?.exportCompletion?(nil, url)
|
|
|
|
|
|
} else {
|
|
@@ -242,47 +236,46 @@ public class BFRecordExport {
|
|
|
self?.exportCompletion?(error as Error, nil)
|
|
|
cShowHUB(superView: nil, msg: "导出失败")
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 导出完成后取消导出
|
|
|
self?.exporter?.cancel()
|
|
|
}
|
|
|
} else {
|
|
|
// 没有处理,直接copy原文件
|
|
|
- if let localPath = data?.first?.localPath{
|
|
|
- self.exportCompletion?(nil, URL(fileURLWithPath: localPath))
|
|
|
+ if let localPath = data?.first?.localPath {
|
|
|
+ exportCompletion?(nil, URL(fileURLWithPath: localPath))
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
-
|
|
|
- func dealAsset(){
|
|
|
+
|
|
|
+ func dealAsset() {
|
|
|
// asset?.tracks.first(where: { track in
|
|
|
// if track.mediaType == .audio{
|
|
|
//
|
|
|
// }
|
|
|
// })
|
|
|
}
|
|
|
-
|
|
|
- func getVideoSize(asset:AVURLAsset) -> CGSize{
|
|
|
+
|
|
|
+ func getVideoSize(asset: AVURLAsset) -> CGSize {
|
|
|
var size = CGSize.zero
|
|
|
- asset.tracks.forEach({ track in
|
|
|
- if track.mediaType == .video{
|
|
|
+ asset.tracks.forEach { track in
|
|
|
+ if track.mediaType == .video {
|
|
|
let realSize = __CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform)
|
|
|
size = CGSize(width: ceil(abs(realSize.width)), height: ceil(abs(realSize.height)))
|
|
|
}
|
|
|
- })
|
|
|
-
|
|
|
+ }
|
|
|
+
|
|
|
return size
|
|
|
}
|
|
|
}
|
|
|
|
|
|
extension BFRecordExport {
|
|
|
- func mergeAudio(videoStickers:[PQEditVisionTrackMaterialsModel], audios:[PQVoiceModel]?, synthesisAll:Bool) -> (AVMutableAudioMix, AVMutableComposition){
|
|
|
+ func mergeAudio(videoStickers: [PQEditVisionTrackMaterialsModel], audios: [PQVoiceModel]?, synthesisAll: Bool) -> (AVMutableAudioMix, AVMutableComposition) {
|
|
|
let composition = AVMutableComposition()
|
|
|
let audioMix = AVMutableAudioMix()
|
|
|
var tempParameters = [AVMutableAudioMixInputParameters]()
|
|
|
-
|
|
|
- var totalDuration : Float64 = 0
|
|
|
+
|
|
|
+ var totalDuration: Float64 = 0
|
|
|
for sticker in videoStickers {
|
|
|
if sticker.volumeGain == 0 {
|
|
|
// 如果添加了会有刺啦音
|
|
@@ -295,18 +288,18 @@ extension BFRecordExport {
|
|
|
}
|
|
|
if let voices = audios {
|
|
|
if synthesisAll {
|
|
|
- tempParameters += mergeRecordVoiceAll(voices:voices, composition)
|
|
|
- }else {
|
|
|
- tempParameters += mergeRecordVoiceOnly(voices:voices, composition)
|
|
|
+ tempParameters += mergeRecordVoiceAll(voices: voices, composition)
|
|
|
+ } else {
|
|
|
+ tempParameters += mergeRecordVoiceOnly(voices: voices, composition)
|
|
|
}
|
|
|
}
|
|
|
audioMix.inputParameters = tempParameters
|
|
|
return (audioMix, composition)
|
|
|
}
|
|
|
-
|
|
|
- func mergeRecordVoiceOnly(voices:[PQVoiceModel], _ composition:AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
|
|
|
+
|
|
|
+ func mergeRecordVoiceOnly(voices: [PQVoiceModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
|
|
|
var tempParameters = [AVMutableAudioMixInputParameters]()
|
|
|
- var totalDur:Double = 0.0
|
|
|
+ var totalDur: Double = 0.0
|
|
|
for model in voices {
|
|
|
if model.volume == 0 {
|
|
|
// 如果添加了会有刺啦音
|
|
@@ -322,15 +315,15 @@ extension BFRecordExport {
|
|
|
sticker.aptDuration = duration
|
|
|
sticker.duration = duration
|
|
|
sticker.locationPath = model.wavFilePath
|
|
|
- sticker.volumeGain = 100 //Float64(model.volume)
|
|
|
+ sticker.volumeGain = 100 // Float64(model.volume)
|
|
|
tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
|
|
|
totalDur += duration
|
|
|
}
|
|
|
|
|
|
return tempParameters
|
|
|
}
|
|
|
-
|
|
|
- func mergeRecordVoiceAll(voices:[PQVoiceModel], _ composition:AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
|
|
|
+
|
|
|
+ func mergeRecordVoiceAll(voices: [PQVoiceModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
|
|
|
var tempParameters = [AVMutableAudioMixInputParameters]()
|
|
|
for model in voices {
|
|
|
if model.volume == 0 {
|
|
@@ -345,11 +338,10 @@ extension BFRecordExport {
|
|
|
sticker.aptDuration = model.endTime - model.startTime
|
|
|
sticker.duration = sticker.aptDuration
|
|
|
sticker.locationPath = model.wavFilePath
|
|
|
- sticker.volumeGain = 100 //Float64(model.volume)
|
|
|
+ sticker.volumeGain = 100 // Float64(model.volume)
|
|
|
tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return tempParameters
|
|
|
}
|
|
|
-
|
|
|
}
|