浏览代码

Merge branch 'master' of https://git.yishihui.com/iOS/BFFramework

* 'master' of https://git.yishihui.com/iOS/BFFramework:
  1,合成视频时音频拼接
huzhiqiang 3 年之前
父节点
当前提交
36372541fa

+ 1 - 1
BFFramework/Classes/Stuckpoint/Controller/PQStuckPointEditerController.swift

@@ -824,7 +824,7 @@ class PQStuckPointEditerController: PQBaseViewController {
         videoExporter.selectedTotalDuration = selectedTotalDuration
         videoExporter.selectedDataCount = selectedDataCount
         videoExporter.selectedImageDataCount = selectedImageDataCount
-        videoExporter.finallyUserAudioTime = finallyUserAudioTime
+        videoExporter.finallyUserAudioTime =  Float(finallyStuckPoints.last ?? 0) - Float(finallyStuckPoints.first ?? 0)
         videoExporter.clipAudioRange = getClipAudioRange()
         videoExporter.playeTimeRange = playeTimeRange
         // 使用深 copy

+ 151 - 7
BFFramework/Classes/Stuckpoint/Controller/PQStuckPointPublicController.swift

@@ -567,7 +567,7 @@ class PQStuckPointPublicController: PQBaseViewController {
             exportSession.cancelExport()
         }
         // 开始导出
-        beginExport()
+        appendAudio()
         /// 保存草稿
         saveDraftbox()
         // 曝光上报:窗口曝光
@@ -631,7 +631,150 @@ class PQStuckPointPublicController: PQBaseViewController {
 extension PQStuckPointPublicController {
     /// fp1 - 导出视频
     /// 开始导出视频
-    func beginExport() {
+    
+    /// 合并声音
+    /// - Parameter urls: 所有音频的URL  是全路径方便复用
+    /// - Parameter completeHander: 返回的 URL 全路径的 URL 如果要保存替换掉前缀
+    func mergeAudios(originAsset: AVURLAsset, mTotalDuration: Float,clipAudioRange: CMTimeRange = CMTimeRange.zero,mStartTime:CMTime = .zero , completeHander: @escaping (_ fileURL: URL?) -> Void) {
+        
+        
+        let timeInterval: TimeInterval = Date().timeIntervalSince1970
+        let composition = AVMutableComposition()
+ 
+        let originaDuration = CMTimeGetSeconds(clipAudioRange.duration)
+        BFLog(message: "处理主音频 原始时长startTime = \(originaDuration) 要显示时长totalDuration = \(mTotalDuration)")
+        
+        if(originaDuration <= CMTimeGetSeconds(clipAudioRange.duration)){
+            
+            BFLog(message: "不用拼接音频文件 \(originAsset.url) 时长is \(CMTimeGetSeconds(originAsset.duration))")
+            completeHander(originAsset.url)
+            return
+        }
+        
+        //整倍数
+        let count = Int(mTotalDuration) / Int(originaDuration)
+
+        //有余数多 clip 一整段
+        let row = mTotalDuration - Float(count) * Float(originaDuration)
+        //已经拼接的总时长
+        var totalDuration: CMTime = .zero
+        //第一段的时长
+        var duration: CMTime = .zero
+        //第一段的区间
+        var timeRange:CMTimeRange = CMTimeRange.zero
+        if count > 0 {
+            for index in 0 ... count {
+                
+                duration = CMTime(value: CMTimeValue((CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime)) * Double(playerTimescaleInt)), timescale: playerTimescaleInt)
+                BFLog(message: "每一个文件的 duration \(CMTimeGetSeconds(duration))")
+                var timeRange = CMTimeRangeMake(start: mStartTime, duration: duration)
+                
+                if(index != 0){
+                    //(CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime))为用户选择的第一段时长
+                    duration = CMTime(value: CMTimeValue((CMTimeGetSeconds( clipAudioRange.duration) * Double(index) + (CMTimeGetSeconds(clipAudioRange.end) - CMTimeGetSeconds(mStartTime))) * Float64(playerTimescaleInt)), timescale: playerTimescaleInt)
+                    BFLog(message: "每一个文件的 duration \(CMTimeGetSeconds(duration))")
+                    timeRange = clipAudioRange
+                    
+                }
+                
+                BFLog(message: "合并的文件地址: \(originAsset.url)")
+                let audioAsset = originAsset
+                let tracks = audioAsset.tracks(withMediaType: .audio)
+                if tracks.count == 0 {
+                    BFLog(message: "音频数据无效不进行合并,所有任务结束要确保输入的数据都正常! \(originAsset.url)")
+                    break
+                }
+                let assetTrack: AVAssetTrack = tracks[0]
+             
+                let compositionAudioTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID())!
+
+                do {
+                    //
+                    try compositionAudioTrack.insertTimeRange(timeRange, of: assetTrack, at: totalDuration)
+
+                } catch {
+                    BFLog(message: "error is \(error)")
+                }
+
+                totalDuration = CMTimeAdd(totalDuration, timeRange.duration)
+                
+            }
+        }
+        
+        if(row > 0){
+            duration = CMTime(value: CMTimeValue(Float(CMTimeGetSeconds(totalDuration)) * Float(playerTimescaleInt)), timescale: playerTimescaleInt)
+            timeRange = CMTimeRange(start: duration, end: CMTime(value: CMTimeValue((CMTimeGetSeconds(duration) + Double(row)) * Double(playerTimescaleInt)), timescale: playerTimescaleInt))
+            
+                BFLog(message: "合并的文件地址: \(originAsset.url)")
+                let audioAsset = originAsset
+                let tracks = audioAsset.tracks(withMediaType: .audio)
+                if tracks.count == 0 {
+                    BFLog(message: "音频数据无效不进行合并,所有任务结束要确保输入的数据都正常! \(originAsset.url)")
+
+                }
+                let assetTrack: AVAssetTrack = tracks[0]
+             
+                let compositionAudioTrack: AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID())!
+
+                do {
+                    //
+                    try compositionAudioTrack.insertTimeRange(timeRange, of: assetTrack, at: totalDuration)
+
+                } catch {
+                    BFLog(message: "error is \(error)")
+                }
+
+                totalDuration = CMTimeAdd(totalDuration, audioAsset.duration)
+                
+        }
+ 
+
+        let assetExport = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
+        BFLog(message: "assetExport.supportedFileTypes is \(String(describing: assetExport?.supportedFileTypes))")
+
+        assetExport?.outputFileType = .m4a
+        // XXXX 注意文件名的后缀要和outputFileType 一致 否则会导出失败
+        var audioFilePath = exportAudiosDirectory
+
+        if !directoryIsExists(dicPath: audioFilePath) {
+            BFLog(message: "文件夹不存在")
+            createDirectory(path: audioFilePath)
+        }
+        audioFilePath.append("merge_\(timeInterval).m4a")
+
+        let fileUrl = URL(fileURLWithPath: audioFilePath)
+
+        assetExport?.outputURL = fileUrl
+        assetExport?.exportAsynchronously {
+            if assetExport!.status == .completed {
+                // 85.819125
+                let audioAsset = AVURLAsset(url: fileUrl, options: avAssertOptions)
+
+                BFLog(message: "拼接声音文件 完成 \(fileUrl) 时长is \(CMTimeGetSeconds(audioAsset.duration))")
+                completeHander(fileUrl)
+
+            } else {
+                print("拼接出错 \(String(describing: assetExport?.error))")
+                completeHander(URL(string: ""))
+            }
+        }
+    }
+
+    func appendAudio() {
+ 
+        let inputAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + (audioMixModel?.localPath ?? "")), options: nil)
+        mergeAudios(originAsset: inputAsset, mTotalDuration: finallyUserAudioTime,clipAudioRange: clipAudioRange,mStartTime: CMTime(value: CMTimeValue((mStickers?.first?.timelineIn ?? 0.0) * Float64(playerTimescaleInt)), timescale: playerTimescaleInt)) {  [weak self] completURL in
+             
+            if(completURL != nil){
+                let asset = AVURLAsset(url: completURL!, options: nil)
+                FilterLog(message: "拼接后音频时长\(asset.duration.seconds)  url is \(String(describing: completURL))")
+                self?.beginExport(inputAsset: asset)
+            }
+          
+        }
+    }
+    
+    func beginExport(inputAsset:AVURLAsset!) {
         if !(editProjectModel?.sData?.sections != nil && (editProjectModel?.sData?.sections.count ?? 0) > 0) {
             BFLog(message: "项目段落错误❌")
             return
@@ -645,13 +788,14 @@ extension PQStuckPointPublicController {
         outPutMP4Path.append("video_\(String.qe.timestamp()).mp4")
         let outPutMP4URL = URL(fileURLWithPath: outPutMP4Path)
         BFLog(message: "导出视频地址 \(outPutMP4URL)")
-        let inputAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + (audioMixModel?.localPath ?? "")), options: nil)
+//        let inputAsset = AVURLAsset(url: URL(fileURLWithPath: documensDirectory + (audioMixModel?.localPath ?? "")), options: nil)
+ 
         // 每次初始化的时候设置初始值 为 nIl
         var audioMix: AVMutableAudioMix?
         var composition: AVMutableComposition?
-        if(finallyUserAudioTime != 0 && finallyUserAudioTime > Float(inputAsset.duration.seconds)){
-            (audioMix, composition) = PQPlayerViewModel.setupAudioMix(originAsset: inputAsset, bgmData:nil, videoStickers: nil,originMusicDuration:finallyUserAudioTime,clipAudioRange: clipAudioRange)
-        }
+//        if(finallyUserAudioTime != 0 && finallyUserAudioTime > Float(inputAsset.duration.seconds)){
+//            (audioMix, composition) = PQPlayerViewModel.setupAudioMix(originAsset: inputAsset, bgmData:nil, videoStickers: nil,originMusicDuration:finallyUserAudioTime,clipAudioRange: clipAudioRange,startTime:CMTime(value: CMTimeValue((mStickers?.first?.timelineIn ?? 0.0) * Float64(playerTimescaleInt)), timescale: playerTimescaleInt))
+//        }
         exporter = PQCompositionExporter(asset: inputAsset, videoComposition: nil, audioMix: audioMix, filters: nil, stickers: mStickers, animationTool: nil, exportURL: outPutMP4URL)
 
         var orgeBitRate = (editProjectModel?.sData?.videoMetaData?.videoWidth ?? 0) * (editProjectModel?.sData?.videoMetaData?.videoHeight ?? 0) * 3
@@ -1217,7 +1361,7 @@ extension PQStuckPointPublicController {
     @objc func willEnterForeground() {
         BFLog(message: "进入到前台")
         if !isExportSuccess {
-            beginExport()
+            appendAudio()
         }
         
        playBtn.isHidden = true