123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- //
- // 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)
- }
-
- }
|