123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- //
- // BFRecordExport.swift
- // BFRecordScreenKit
- //
- // Created by 胡志强 on 2021/11/25.
- // 录屏视频导出
- import AVFoundation
- import BFCommonKit
- import BFMediaKit
- import Foundation
- import GPUImage
- import Photos
- import UIKit
- public enum ExportError: Int {
- case FileNotExist = -31001
- case DataLost = -31002
- case VoiceLost = -31003
- case TotalDurError = -31004
- case ExportExcept = -31005
- case DiskNoSpace = -31006
- }
- public class BFRecordExport {
- public var progress: ((Float) -> Void)?
- public var exportCompletion: ((Error?, URL?) -> Void)?
- public var originSoundVolumn: Float = 1.0 // 无录音时原声大小
- public var originSoundInRecordVolumn: Float = 0.0 // 录音时原声大小
- public var data: [BFRecordItemModel]? {
- didSet {
- if data?.count ?? 0 > 0 {
- for item in data! {
- item.generationTimeRanges()
- }
- }
- }
- }
- var count = 0
- var stickerRanges = [CMTimeRange]()
- var exporter: PQCompositionExporter?
- // var mStickers = [PQEditVisionTrackMaterialsModel]()
- deinit {}
- public init() {}
- // MARK: -
- /// synthesisAll: 合成所有还是只合成录音部分
- public func startExprot(synthesisAll: Bool) {
- // 1,背景视频素材
- if let itemModels = data {
- var totalDur = 0.0
- var titleStickers = [PQEditSubTitleModel]()
- var voiceList = [PQEditVisionTrackMaterialsModel]()
- var videoStickers = [PQEditVisionTrackMaterialsModel]()
- // 切割视频素材
- for (_, itemModel) in itemModels.enumerated() {
- // 保留录音部分时,是否按录音顺序合成最终视频;
- // 如果需要排序,则排视频的顺序;否则排音频的顺序
- let needSort = false
- if needSort {
- } else {
- // 音频排序
- itemModel.voiceStickers.sort { m1, m2 in
- m1.startCMTime.seconds < m2.startCMTime.seconds
- }
- // 字幕排序
- itemModel.titleStickers.sort { model1, model2 in
- model1.timelineIn < model2.timelineIn
- }
- }
- if itemModel.mediaType == .IMAGE {
- // 图片素材
- if !synthesisAll, itemModel.voiceStickers.count == 0 {
- // 图片无录音在保留模式里不合成
- continue
- }
- var duration = itemModel.materialDuraion.seconds
- if itemModel.voiceStickers.count == 0 {
- // 图片无录音保持2s
- duration = 2
- let voice = PQEditVisionTrackMaterialsModel()
- voice.model_in = 0.0
- voice.out = 2.0
- voice.aptDuration = 2.0
- // voice.voiceType = VOICETYPT.None.rawValue
- voice.volumeGain = 100
- voiceList.append(voice)
- } else {
- //
- for mod in itemModel.voiceStickers {
- let sticker = PQEditVisionTrackMaterialsModel()
- sticker.model_in = 0
- sticker.out = mod.endCMTime.seconds - mod.startCMTime.seconds
- sticker.timelineIn = totalDur + mod.startCMTime.seconds
- sticker.timelineOut = totalDur + mod.endCMTime.seconds
- sticker.aptDuration = sticker.out
- sticker.duration = sticker.out
- sticker.locationPath = mod.wavFilePath
- sticker.volumeGain = 100 // Float64(model.volume)
- voiceList.append(sticker)
- }
- }
- let sticker = splitBaseMaterial(timelineIn: totalDur, model_in: 0, duration: duration)
- sticker.originalData = itemModel.coverImg?.pngData()
- sticker.volumeGain = 0
- sticker.type = StickerType.IMAGE.rawValue
- videoStickers.append(sticker)
- BFLog(1, message: "image sticker - timIn:\(sticker.timelineIn), modIn:\(sticker.model_in), dur:\(duration)")
- for titleS in itemModel.titleStickers {
- // let leng = titleS.timelineOut - titleS.timelineIn
- let newTitleSticker = PQEditSubTitleModel()
- titleStickers.append(newTitleSticker)
- newTitleSticker.text = titleS.text
- newTitleSticker.setting = titleS.setting
- newTitleSticker.timelineIn = titleS.timelineIn + CMTime(seconds: totalDur, preferredTimescale: 1000)
- newTitleSticker.timelineOut = titleS.timelineOut + CMTime(seconds: totalDur, preferredTimescale: 1000)
- }
- totalDur += duration
- continue
- }
- // 视频处理
- if let localPath = itemModel.localPath {
- if !FileManager.default.fileExists(atPath: localPath) {
- let error = NSError(domain: "err", code: ExportError.FileNotExist.rawValue, userInfo: ["msg": "file not exist"])
- exportCompletion?(error as Error, nil)
- return
- }
- // voiceList.append(contentsOf: itemModel.voiceStickers)
- if synthesisAll {
- var subDur = 0.0
- 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)
- sticker.locationPath = localPath
- sticker.volumeGain = Float64(srange.isRecord ? originSoundInRecordVolumn * 100 : originSoundVolumn * 100)
- videoStickers.append(sticker)
- subDur += range.duration.seconds
- if srange.isRecord {
- // 处理voice
- if let mod = itemModel.voiceStickers.first(where: { m in
- m.startCMTime.seconds == range.start.seconds
- }) {
- let sticker = PQEditVisionTrackMaterialsModel()
- sticker.model_in = 0
- sticker.out = mod.endCMTime.seconds - mod.startCMTime.seconds
- sticker.timelineIn = totalDur + mod.startCMTime.seconds
- sticker.timelineOut = totalDur + mod.endCMTime.seconds
- sticker.aptDuration = sticker.out
- sticker.duration = sticker.out
- sticker.locationPath = mod.wavFilePath
- sticker.volumeGain = 100 // Float64(model.volume)
- voiceList.append(sticker)
- }
- }
- }
- for titleS in itemModel.titleStickers {
- let newTitleSticker = PQEditSubTitleModel()
- titleStickers.append(newTitleSticker)
- newTitleSticker.text = titleS.text
- newTitleSticker.setting = titleS.setting
- newTitleSticker.timelineIn = titleS.timelineIn + CMTime(seconds: totalDur, preferredTimescale: 1000)
- newTitleSticker.timelineOut = titleS.timelineOut + CMTime(seconds: totalDur, preferredTimescale: 1000)
- }
- totalDur += subDur
- } else {
- // 只保留录音部分
- var subDur = 0.0
- var drangs = itemModel.dealedDurationRanges.filter { srange in
- srange.isRecord == true
- }
- if needSort {
- drangs.sort { range1, range2 in
- range1.index < range2.index
- }
- }
- for (index, srange) in drangs.enumerated() {
- let range = srange.range
- let sticker = splitBaseMaterial(timelineIn: totalDur + subDur, model_in: range.start.seconds, duration: range.duration.seconds)
- sticker.locationPath = localPath
- sticker.volumeGain = Float64(srange.isRecord ? originSoundInRecordVolumn * 100 : originSoundVolumn * 100)
- videoStickers.append(sticker)
- let voiceSticker = itemModel.voiceStickers[index]
- let voice = PQEditVisionTrackMaterialsModel()
- voice.model_in = 0
- voice.out = voiceSticker.endCMTime.seconds - voiceSticker.startCMTime.seconds
- voice.timelineIn = totalDur + subDur
- voice.timelineOut = totalDur + subDur + voice.out
- voice.aptDuration = voice.out
- voice.duration = voice.out
- voice.locationPath = voiceSticker.wavFilePath
- voice.volumeGain = 100 // Float64(model.volume)
- voiceList.append(voice)
- let titleModels = itemModel.titleStickers.filter { mod in
- mod.recordId == voiceSticker.recordId
- }
-
- // 字幕的时间点是以相关录音的原点为坐标计算的。
- for titleS in titleModels {
- let newTitleSticker = PQEditSubTitleModel()
- titleStickers.append(newTitleSticker)
- newTitleSticker.text = titleS.text
- newTitleSticker.setting = titleS.setting
- newTitleSticker.timelineIn = titleS.timelineIn + CMTime(seconds: totalDur + subDur , preferredTimescale: 1000) - voiceSticker.startCMTime
- newTitleSticker.timelineOut = titleS.timelineOut + CMTime(seconds: totalDur + subDur, preferredTimescale: 1000) - voiceSticker.startCMTime
- BFLog(1, message: "timein - \(newTitleSticker.timelineIn), out - \(newTitleSticker.timelineOut)")
- }
- subDur += range.duration.seconds
- }
-
- totalDur += subDur
- }
- }
- }
- beginExport(synthesisAll: synthesisAll, videoStickers: videoStickers, voiceList: voiceList, titleStickers: titleStickers)
- }
- }
- public func cancelExport() {
- exporter?.cancel()
-
- }
- public func clearFileCache() {
- data?.forEach { itemModel in
- itemModel.voiceStickers.forEach { model in
- if let localPath = model.wavFilePath {
- try? FileManager.default.removeItem(atPath: localPath)
- }
- }
- }
- }
- enum DispatchError: Error {
- case timeout
- }
- 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 {
- let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel()
- bgMovieInfo.type = StickerType.VIDEO.rawValue
- bgMovieInfo.timelineIn = timelineIn
- bgMovieInfo.timelineOut = timelineIn + duration
- bgMovieInfo.model_in = model_in
- bgMovieInfo.out = model_in + duration
- bgMovieInfo.canvasFillType = stickerContentMode.aspectFitStr.rawValue
- bgMovieInfo.volumeGain = 1
- bgMovieInfo.aptDuration = bgMovieInfo.timelineOut
- bgMovieInfo.duration = bgMovieInfo.timelineOut
- BFLog(1, message: "hhh- timIn:\(timelineIn), modIn:\(model_in), dur:\(duration)")
- return bgMovieInfo
- }
- // 因为titleStickers 是传递过来的,会修改timelinein,需要重新生成,以免影响原来的数据
- // voiceList是考虑到图片有时候没有录音,在保留全部时,需要添加一个2秒的空sticker
- func beginExport(synthesisAll: Bool, videoStickers: [PQEditVisionTrackMaterialsModel], voiceList: [PQEditVisionTrackMaterialsModel], titleStickers: [PQEditSubTitleModel]) {
- // 输出视频地址
- // exprotVideo()
- // return;
- var outPutMP4Path = exportVideosDirectory
- if !directoryIsExists(dicPath: outPutMP4Path) {
- createDirectory(path: outPutMP4Path)
- }
- 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: ExportError.DataLost.rawValue, userInfo: ["msg": "compose_fail_lost".BFLocale])
- exportCompletion?(error as Error, nil)
- return
- }
- // 处理导出
- 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: ExportError.VoiceLost.rawValue, userInfo: ["msg": "voiceStickers count += nil"])
- exportCompletion?(error as Error, nil)
- return
- }
- guard let totalDuration = data?.reduce(0.0, { partialResult, itemModell in
- var modelDuraion = 0.0
- if itemModell.mediaType == .IMAGE {
- if itemModell.voiceStickers.count == 0, synthesisAll {
- modelDuraion += 2
- } else {
- modelDuraion = itemModell.materialDuraion.seconds
- }
- } else if itemModell.mediaType == .VIDEO {
- modelDuraion = itemModell.dealedDurationRanges.reduce(0.0) { partialResult, srange in
- // partialResult + (!synthesisAll && srange.isRecord) ?
- if synthesisAll {
- return partialResult + srange.range.duration.seconds
- } else {
- return partialResult + (srange.isRecord ? srange.range.duration.seconds : 0)
- }
- }
- }
- return (partialResult ?? 0) + modelDuraion
- }) else {
- let error = NSError(domain: "err", code: ExportError.TotalDurError.rawValue, userInfo: ["msg": "时长计算出错"])
- exportCompletion?(error as Error, nil)
- return
- }
- // MARK: - 声音合成
- // 有录音操作或者多个视频,就会进入合成步骤,否则就是一个没有处理的素材,直接导出就行了
- if voiceCount > 0 || videoStickers.count > 1 {
- let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList, synthesisAll: synthesisAll)
- let outputSize: CGSize = CGSize(width: 1080, height: Int(1080 * CGFloat(UIScreen.main.bounds.size.height / UIScreen.main.bounds.size.width)))
- BFLog(message: "输出视频大小:\(outputSize)")
-
- // add by ak 有字幕数据 & 显示字幕开关打开 添加字幕filter
- var subtitleFilter:PQSubTitleFilter?
- if titleStickers.count > 0,titleStickers.first?.setting.subtitleIsShow ?? true {
- subtitleFilter = PQSubTitleFilter(st: titleStickers, inputSize: outputSize)
- }
- exporter = PQCompositionExporter(asset: composition, videoComposition: nil, audioMix: audioMix, filters:nil,stickers: videoStickers, animationTool: nil, exportURL: outPutMP4URL,subTitleFilter:subtitleFilter)
- var orgeBitRate = Int(outputSize.width * outputSize.height * 3)
- for stick in videoStickers {
- if stick.type == StickerType.VIDEO.rawValue, stick.locationPath.count > 0 {
- 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(1, message: String(format: "导出设置的码率为:%.3f MB", Double(orgeBitRate) / 1024.0 / 1024.0 / 8.0))
- let preSize = Double(orgeBitRate) * totalDuration / (1024 * 1024 * 8)
- let freeSize = PQBridgeObject.getPhoneDiskFreeSize()
- if preSize + 100.0 > freeSize { // 存储完后磁盘剩余至少100M
- let error = NSError(domain: "err", code: ExportError.DiskNoSpace.rawValue, userInfo: ["msg": "需要\(Int(preSize))MB,可用\(Int(freeSize))MB"])
- exportCompletion?(error as Error, nil)
- return
- }
- let tempBeginExport = Date().timeIntervalSince1970
- if exporter!.prepare(videoSize: outputSize, videoAverageBitRate: orgeBitRate) {
- exporter!.start(playeTimeRange: CMTimeRange(start: CMTime.zero, end: CMTime(seconds: totalDuration, preferredTimescale: 1000)))
- }
- exporter?.progressClosure = { [weak self] _, _, progress in
- // BFLog(message: "正片合成进度 \(progress * 100)%")
- let useProgress = progress > 1 ? 1 : progress
- if progress > 0 { // 更新进度
- self?.progress?(useProgress)
- }
- }
- exporter?.completion = { [weak self] url in
- // 输出视频时长
- if let url = url {
- let outSeconds = CMTimeGetSeconds(AVAsset(url: url).duration)
- let exportEndTime = Date().timeIntervalSince1970
- BFLog(1, message: "生成视频时长为:\(outSeconds) 总用时:\(exportEndTime - tempBeginExport)")
- if(PQENVUtil.shared.channel == "Development"){
- cShowHUB(superView: nil, msg: (outSeconds == 0) ? "compose_retry3".BFLocale : String(format: "总用时: %.2f", exportEndTime - tempBeginExport))
- }
- self?.exportCompletion?(nil, url)
- } else {
- let error = NSError(domain: "err", code: ExportError.ExportExcept.rawValue, userInfo: ["msg": "compose_fail_export".BFLocale])
- self?.exportCompletion?(error as Error, nil)
- cShowHUB(superView: nil, msg: "compose_fail_export".BFLocale)
- }
- // 导出完成后取消导出
- self?.exporter?.cancel()
- }
- } else {
- // 没有处理,直接copy原文件
- if let localPath = data?.first?.localPath {
- exportCompletion?(nil, URL(fileURLWithPath: localPath))
- }
- }
- }
- func dealAsset() {
- // asset?.tracks.first(where: { track in
- // if track.mediaType == .audio{
- //
- // }
- // })
- }
- func getVideoSize(asset: AVURLAsset) -> CGSize {
- var size = CGSize.zero
- 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: [PQEditVisionTrackMaterialsModel]?, synthesisAll: Bool) -> (AVMutableAudioMix, AVMutableComposition) {
- let composition = AVMutableComposition()
- let audioMix = AVMutableAudioMix()
- var tempParameters = [AVMutableAudioMixInputParameters]()
- var totalDuration: Float64 = 0
- for sticker in videoStickers {
- if sticker.volumeGain == 0 {
- // 如果添加了会有刺啦音
- BFLog(message: "音频音量 为0 不添加")
- continue
- }
- // sticker.volumeGain = 50
- totalDuration = max(totalDuration, sticker.duration)
- tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition)
- }
- if let voices = audios {
- if synthesisAll {
- tempParameters += mergeRecordVoiceAll(voices: voices, composition)
- } else {
- tempParameters += mergeRecordVoiceOnly(voices: voices, composition)
- }
- }
- audioMix.inputParameters = tempParameters
- return (audioMix, composition)
- }
- func mergeRecordVoiceOnly(voices: [PQEditVisionTrackMaterialsModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
- var tempParameters = [AVMutableAudioMixInputParameters]()
- var totalDur: Double = 0.0
- for model in voices {
- if model.volumeGain == 0 {
- // 如果添加了会有刺啦音
- BFLog(message: "音频音量 为0 不添加")
- continue
- }
- tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: model, composition: composition)
- totalDur += model.aptDuration
- }
- return tempParameters
- }
- func mergeRecordVoiceAll(voices: [PQEditVisionTrackMaterialsModel], _ composition: AVMutableComposition) -> [AVMutableAudioMixInputParameters] {
- var tempParameters = [AVMutableAudioMixInputParameters]()
- var totalDur = 0.0
- for model in voices {
- if model.volumeGain == 0 {
- // 如果添加了会有刺啦音
- BFLog(message: "音频音量 为0 不添加")
- continue
- }
- tempParameters += PQPlayerViewModel.dealWithMaterialTrack(stickerModel: model, composition: composition)
- }
- return tempParameters
- }
- }
|