三.音视频编辑-媒体组合-导出

引言

一个可以播放的媒体组合我们已经创建好了,不过如果不能把这个组合的结果导出那么组合的价值就不那么突出,所以我们还要学习如何将组合的媒体进行导出。

时间处理

有了上篇博客做基础,导出功能就显得容易许多,所以我们在这篇博客中稍微介绍一下在视频编辑以及视频播放中时间和时间范围相关的知识。

CMTime

通常来讲我们在日常开发中都是通过浮点数来表示时间,或者使用NSTimerInterval。实际上,AV Foundation在AVAudioPlayer和AVAudioRecorder中处理时间问题时本身也会使用这个类型。虽然很多通用的开发环境使用双精度表示时间已经足够满足要求了,不过其天然的不精确性导致双精度类型无法应用于更多的高级时基媒体的开发中。比如,一个单一舍入错误就会导致丢帧或音频丢失。相反,苹果公司使用Core Media框架定义的CMTime数据类型作为时间格式。结构如下:

Swift 复制代码
/**
	@typedef	CMTime
	@abstract	Rational time value represented as int64/int32.
*/
@available(iOS 4.0, *)
public struct CMTime {
    public init()
    public init(value: CMTimeValue, timescale: CMTimeScale, flags: CMTimeFlags, epoch: CMTimeEpoch)
    /**< The value of the CMTime. value/timescale = seconds */
    public var value: CMTimeValue
    /**< The timescale of the CMTime. value/timescale = seconds. */
    public var timescale: CMTimeScale
    /**< The flags, eg. kCMTimeFlags_Valid, kCMTimeFlags_PositiveInfinity, etc. */
    public var flags: CMTimeFlags
    /**< Differentiates between equal timestamps that are actually different because
    								of looping, multi-item sequencing, etc.
    								Will be used during comparison: greater epochs happen after lesser ones.
    								Additions/subtraction is only possible within a single epoch,
    								however, since epoch length may be unknown/variable */
    public var epoch: CMTimeEpoch
}

上面结构中与时间最相关的三个属性分别是:value、timescale、flags。

value-CMTimeValue:64位有符号整型变量。

timescale-CMTimeScale:32位有符号整型变量。

分别是CMTime元素分数形式的分子和分母。

flags-CMTimeFlags:是一个位掩码,用于表示时间的指定状态,比如判断数据是否有效、不确定或是否出现舍入值等。

CMTime实例可标记特定的时间点或用于表示持续时间。

创建CMTime

有多种方法可以创建CMTime实例,不过最常见的方法是使用CMTimeMake函数,指定一个64位的value参数和一个32位的timescale参数。

下面的三种创建方式都表示一个3秒的CMTime。

Swift 复制代码
            let time1 = CMTimeMake(value: 3, timescale: 1)
            let time2 = CMTimeMake(value: 6, timescale: 2)
            let time3 = CMTime(value: 9, timescale: 3)

显示CMTime

使用CMTimeShow函数来显示这些时间。

Swift 复制代码
            CMTimeShow(time1)
            CMTimeShow(time2)
            CMTimeShow(time3)

CMTimeRange

Core Media框架还为时间范围提供了一个数据类型,称为CMTimeRange,它在有关资源编辑的API中扮演着重要角色。CMTimeRange由两个CMTime值组成,第一个值定义时间范围的起点,第二个值定义时间范围的持续时间。

Swift 复制代码
public struct CMTimeRange {
    public init()
    public init(start: CMTime, duration: CMTime)
    /**< The start time of the time range. */
    public var start: CMTime
    /**< The duration of the time range. */
    public var duration: CMTime
}

创建CMTimeRange的一个方法是使用CMTimeRangeMake函数,它的第一个参数是定义时间范围起点的CMTime值,第二个参数是表示范围持续时长的CMTime值。比如,如果我们要创建一个时间范围,从时间轴5秒位置开始,持续时长5秒,则创建方法如下所示:

Swift 复制代码
        let fiveSecondsTime = CMTimeMake(value: 5, timescale: 1)
        let timeRange = CMTimeRange(start: fiveSecondsTime, duration: fiveSecondsTime)
        CMTimeRangeShow(timeRange)

另一种创建时间范围的方法是使用CMTimeRangeFromTimetoTime函数。该函数通过表示范围起点和终点的CMTime值来创建一个CMTimeRange。比如,上面的示例还可以这样实现:

Swift 复制代码
        let fiveSecondsTime = CMTimeMake(value: 5, timescale: 1)
        let tenSecondsTime = CMTimeMake(value: 10, timescale: 1)
        let timeRange = CMTimeRangeFromTimeToTime(start: fiveSecondsTime, end: tenSecondsTime)
        CMTimeRangeShow(timeRange)

CMTimeRange还提供了大量的函数来处理时间范围相关的运算和比较。

如果希望创建一个两个时间交叉的时间范围,或者希望得到两个时间范围的总和,可以尝试下面给出的实现方法。

Swift 复制代码
        let range1 = CMTimeRange(start: CMTime.zero, duration: CMTimeMake(value: 5, timescale: 1))
        let range2 = CMTimeRange(start: CMTimeMake(value: 2, timescale: 1), duration: CMTimeMake(value: 5, timescale: 1))
        let intersectionRange = CMTimeRangeGetIntersection(range1, otherRange: range2)
        CMTimeRangeShow(intersectionRange)
        
        let unionRange = CMTimeRangeGetUnion(range1, otherRange: range2)
        CMTimeRangeShow(unionRange)

创建AVAssetExportSession

PHComposition协议中声明了两个协议方法,之前播放组合我们在PHBaseComposition中实现了其中一个生成视频的可播放版本,接下来我们来实现另外一个方法生成媒体资源的可导出版本。

Swift 复制代码
    //MARK: PHComposition - 生成 AVAssetExportSession
    func makeAssetExportSession() -> AVAssetExportSession? {
        var assetExportSession:AVAssetExportSession? = nil
        if let compostion = compostion {
            let prset = AVAssetExportPresetHighestQuality
            assetExportSession = AVAssetExportSession(asset: compostion.copy() as! AVAsset, presetName: prset)
        }
        return assetExportSession
    }

使用AVMutableComposition的副本,创建一个新的AVAssetExportSession实例。

开始导出

创建一个PHCompositionExporter类专门负责媒体资源的导出。

Swift 复制代码
    private var composition:PHComposition!
    /// 视频导出类
    private var exportSession:AVAssetExportSession?
    /// 是否正在导出
    private var exporting:Bool = false
    /// 延迟任务
    private var afterTask:PHAfterTask?
    /// 进度
    private var progress:Double = 0.0

    init(composition: PHComposition!) {
        self.composition = composition
    }
  1. composition:负责创建视频的可导出版本。
  2. exporsession:负责视频的导出。
  3. exporting:标记是否正在导出,用于监听导出过程。
  4. afterTask:延迟任务类,在监听媒体导出过程的时候我们会运用到延迟执行方法,这个类在之前的博客可中有过相关介绍,目的是为了解决延迟方法导致对self的延迟释放问题。
  5. progress:媒体资源的导出进度。
  6. 一个自定义的初始化,传入PHComposition类的实例对象。

另外声明一个开始导出的方法

Swift 复制代码
    /// 开始导出
    public func beginExport() {
        self.exportSession = self.composition.makeAssetExportSession()
        guard let exportSession = exportSession else { return }
        exportSession.outputURL = exportURL()
        exportSession.outputFileType = .mp4
        exportSession.exportAsynchronously {[weak self] in
            guard let self = self  else { return }
            guard let exportSession = self.exportSession else { return  }
            let status = exportSession.status
            if status == .completed {
                //成功导出,存储到相册
                writeExportedVideoToPhotoLibrary()
                
            }
        }
        exporting = true
        monitorExportProgress()
    }

    /// 获取视频导出地址
    /// - Returns: 视频导出地址
    private func exportURL() -> URL {
        var filePath:String? = nil
        let count:UInt = 0
        repeat {
            let temp = NSTemporaryDirectory()
            let number = String(format: "-%li", count)
            let fileName = String(format: "Masterpice-%@mp4", number)
            filePath = String(format: "%@/%@", temp,fileName)
        } while (FileManager.default.fileExists(atPath: filePath!))
        return URL(filePath: filePath!)
    }
  1. 首先获取一个组合的可导出版本,这会返回一个AVAssetExportSession实例,这个实例有基础AVComposition实例,且具有相应的话预设值。我们为这个会话动态生成一个唯一的输出URL并设置输出文件类型。
  2. 调用exportAsynchronouslyWithCompletionHandler:方法开始导出过程。
  3. 设置exporting属性为YES。PHMainViewController会监听该属性的变化,并呈现用户界面的进展。
  4. 最后,调用monitorExportProgress方法,轮询导出会话的状态来确定当前进度。继续看一下monitorExportProgress方法的实现。

监听导出进度

monitorExportProgress方法实现

Swift 复制代码
    /// 监听视频导出进度
    @objc private func monitorExportProgress() {
        guard let exportSession = exportSession else { return  }
        let status = exportSession.status
        if status == .exporting {
            self.progress = Double(exportSession.progress)
            self.afterTask = PHAfterTask(after: 0.1, target: self, selector: #selector(monitorExportProgress))
        } else {
            exporting = false
        }
    }
  1. 通过检查导出绘画的status属性来确定当前的状态,如果状态返回.exporting,则使用当前的导出绘画的进度值来在更新progres熟悉,并递归调用monitorExportProgress方法。否则则设置exporting为false,导出过程结束不再进行递归。

保存到相册

保存到相册需要使用到Photos框架中的知识,将视频保存到相册需要提前申请相册的权限,接下来我们看一下保存到相册的代码实现。

Swift 复制代码
    /// 保存到相册
    private func writeExportedVideoToPhotoLibrary() {
        let url = self.exportSession?.outputURL
        let photoLibrary = PHPhotoLibrary.shared()
        DispatchQueue.main.async {[weak self] in
            guard let self = self else { return }
            var assetId = ""
            var assetCollectionId = ""
            do {
                try photoLibrary.performChangesAndWait {
                    assetId = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url!)?.placeholderForCreatedAsset!.localIdentifier ?? ""
                }
                var createAssetCollection = self.fetchAssetCollection(title: "Masterpice")
                if createAssetCollection == nil {
                    try photoLibrary.performChangesAndWait {
                        assetCollectionId = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "Masterpice").placeholderForCreatedAssetCollection.localIdentifier 
                    }
                    //获取刚刚创建的相册
                    createAssetCollection = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [assetCollectionId], options: nil).firstObject
                }
                //保存
                try photoLibrary.performChangesAndWait {
                    let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject
                    let collectionChangeRequest = PHAssetCollectionChangeRequest(for: createAssetCollection!)
                    collectionChangeRequest?.addAssets([asset!] as NSFastEnumeration)
                }
            } catch {
                print("writeExportedVideoToPhotoLibrary error")
            }
        }
    }
    
    /// 获取相册
    /// - Parameter title: 相册名
    /// - Returns: 相册
    private func fetchAssetCollection(title:String) -> PHAssetCollection? {
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", title)
        let fetchResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
        if fetchResult.count > 0 {
            return fetchResult.firstObject
        }
        return nil
    }

以上代码将导出的视频保存到了一个自定义名为Masterpice的相册中,具体细节在这里就不过多介绍。

结语

学习AV Foundation的编辑功能,其中最重要的一点就是熟练掌握CMTime和CMTimeRange的知识,需要对相关知识多加练习。现在我们应用已经具备了创建组合和导出组合的功能,下面的博客我们将继续讨论如何先剪切编辑,处理音频等诸多高级功能。

相关推荐
AI脑极体1 天前
解密与推广IAB/MRC零售媒体测量指南
媒体·零售
是店小二呀1 天前
从数据仓库到数据中台再到数据飞轮:社交媒体的数据技术进化史
大数据·数据仓库·媒体
树莓集团2 天前
从AI到大数据,数字技术服务平台全栈赋能企业升级
大数据·人工智能·科技·物联网·媒体
MediaTea2 天前
Pr 入门系列之三:挑选与添加媒体到序列(上)
媒体
铁松溜达py2 天前
“MIME 媒体类型“用来标识网络传输内容的格式标准
开发语言·网络·python·媒体
阿里云视频云2 天前
信通院发布首个《大模型媒体生产与处理》标准,阿里云智能媒体服务作为业界首家“卓越级”通过
阿里云·云计算·媒体
ehviwer233 天前
MathType7.9绿色和谐版激活补丁包下载
android·macos·ios·cocoa·媒体
PlumCarefree4 天前
USB摄像头视频流转RTSP流
图像处理·ffmpeg·音视频·媒体·视频编解码
kkai人工智能6 天前
掌握ChatGPT:高效利用AI助手
人工智能·学习·ai·chatgpt·媒体
华媒舍传媒6 天前
区块链媒体:区块链媒体套餐倾心推广解析!
区块链·媒体