// // BFRecordExport.swift // BFRecordScreenKit // // Created by 胡志强 on 2021/11/25. // 录屏视频导出 import Foundation import AVFoundation import BFFramework import BFVideoEditKit import Photos import GPUImage public class BFRecordExport { public var progress : ((Float)->Void)? public var exportCompletion : ((Error?, URL?)->Void)? // 视频素材 public var asset:AVURLAsset? // 录音段 public var voiceList:[PQVoiceModel]? { didSet { // audioAssets = voiceList?.map({ model in // AVURLAsset(url: URL(fileURLWithPath: model.wavFilePath)) // }) } } var count = 0 var audioAssets : [AVURLAsset]? var exporter : PQCompositionExporter? var mStickers = [PQEditVisionTrackMaterialsModel]() deinit { } public init(){} //MARK: - public func startExprot(){ // 1,背景视频素材 let bgMovieInfo: PQEditVisionTrackMaterialsModel = PQEditVisionTrackMaterialsModel() bgMovieInfo.type = StickerType.VIDEO.rawValue bgMovieInfo.locationPath = ((asset?.url.absoluteString)?.removingPercentEncoding ?? "").replacingOccurrences(of: "file://", with: "") bgMovieInfo.timelineIn = 0 bgMovieInfo.timelineOut = CMTimeGetSeconds(asset?.duration ?? CMTime.zero) bgMovieInfo.model_in = bgMovieInfo.timelineIn bgMovieInfo.out = bgMovieInfo.timelineOut bgMovieInfo.canvasFillType = stickerContentMode.aspectFitStr.rawValue bgMovieInfo.volumeGain = 30 bgMovieInfo.aptDuration = bgMovieInfo.timelineOut bgMovieInfo.duration = bgMovieInfo.timelineOut mStickers.append(bgMovieInfo) beginExport(videoStickers: mStickers) } public func cancelExport(){ self.exporter?.cancel() } 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 exprotVideo(){ // //重新创建GPUImageMovie用于保存 // saveMovie = [[GPUImageMovie alloc] initWithURL:self.pathURL]; let saveFilter = GPUImageFilter() let saveMovie = GPUImageMovie(url: asset?.url) saveMovie?.shouldRepeat = false saveMovie?.addTarget(saveFilter) let filePath = getOutputFilePath() let savewrite = GPUImageMovieWriter(movieURL: getOutputFilePath(), size: getVideoSize()) savewrite?.shouldPassthroughAudio = true savewrite?.encodingLiveVideo = true saveFilter.addTarget(savewrite) saveMovie?.enableSynchronizedEncoding(using: savewrite) // saveMovie?.audioEncodingTarget = savewrite savewrite?.startRecording() saveMovie?.startProcessing() savewrite?.completionBlock = { DispatchQueue.main.async { [weak self, weak savewrite] in saveFilter.removeTarget(savewrite) savewrite?.finishRecording() saveMovie?.cancelProcessing() saveMovie?.removeTarget(saveFilter) cShowHUB(superView: nil, msg: "合成成功") self?.exportCompletion?(nil, filePath) self?.saveVideoToPhoto(url: filePath) } } } func beginExport(videoStickers:[PQEditVisionTrackMaterialsModel]) { // 输出视频地址 // 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)") // 处理导出 if (voiceList?.count ?? 0 ) > 0 || videoStickers.count > 1 { // var audioUrl:URL? // if audioAsset?.count ?? 0 > 0 { // // 多音频合成 // if let list = voiceList?.map({ model in // URL(fileURLWithPath: model.wavFilePath) // }){ // if list.count == 1 { // audioUrl = list.first // }else { // let semaphore = DispatchSemaphore(value: 0) // PQPlayerViewModel.mergeAudios(urls: list) { completURL in // audioUrl = completURL // semaphore.signal() // } // _ = semaphore.wait(timeout: .now() + 5) // } // } // } let (audioMix, composition) = mergeAudio(videoStickers: videoStickers, audios: voiceList) let filter = mStickers.map { sticker in PQMovieFilter(movieSticker: sticker) } // 有 // if let completURL = audioUrl { // let inputAsset = AVURLAsset(url: completURL, options: avAssertOptions) //// (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: videoStickers) // //使用原视频无音版 // (audioMix, composition) = PQVideoEditViewModel.setupAudioMix(originAsset: inputAsset, bgmData: nil, videoStickers: nil) // // if composition != nil { // exporter = PQCompositionExporter(asset: composition!, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL) // }else { // exporter = PQCompositionExporter(asset: inputAsset, videoComposition: nil, audioMix: nil, filters: filter, animationTool: nil, exportURL: outPutMP4URL) // } // } exporter = PQCompositionExporter(asset: composition, videoComposition: nil, audioMix: audioMix, filters: filter, animationTool: nil, exportURL: outPutMP4URL) let size = getVideoSize() var orgeBitRate = Int(size.width * size.height * 3) for stick in mStickers { 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: asset?.duration ?? CMTime.zero)) } 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) BFLog(1, message: "无水印的视频导出完成: \(String(describing: url)) 生成视频时长为:\(outSeconds)") cShowHUB(superView: nil, msg: ( outSeconds == 0) ? "合成失败请重试。" : "合成成功") self?.saveVideoToPhoto(url: url) self?.exportCompletion?(nil, url) }else{ let error = NSError(domain: "err", code: -1, userInfo: nil) self?.exportCompletion?(error as Error, nil) cShowHUB(superView: nil, msg: "导出失败") } // 导出完成后取消导出 self?.exporter?.cancel() } } else { // 没有处理,直接copy原文件 self.exportCompletion?(nil, self.asset?.url) } } func saveVideoToPhoto(url:URL){ PHPhotoLibrary.shared().performChanges { PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url) } completionHandler: { isFinished, _ in if isFinished { DispatchQueue.main.async { cShowHUB(superView: nil, msg: "保存成功") } } } } func dealAsset(){ // asset?.tracks.first(where: { track in // if track.mediaType == .audio{ // // } // }) } func getVideoSize() -> CGSize{ var size = CGSize.zero self.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]?) -> (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 = 2 totalDuration = max(totalDuration, sticker.duration) tempParameters += PQVideoEditViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition) } if let voices = audios { for model in voices { if model.volume == 0 { // 如果添加了会有刺啦音 BFLog(message: "音频音量 为0 不添加") continue } let sticker = PQEditVisionTrackMaterialsModel() sticker.model_in = 0 sticker.timelineIn = model.startTime sticker.out = model.endTime sticker.aptDuration = model.endTime - model.startTime sticker.duration = sticker.aptDuration sticker.locationPath = model.wavFilePath sticker.volumeGain = 100 //Float64(model.volume) tempParameters += PQVideoEditViewModel.dealWithMaterialTrack(stickerModel: sticker, composition: composition) } } audioMix.inputParameters = tempParameters return (audioMix, composition) } }