在上一篇博客中,我们已经介绍了创建视频过渡的实现方案,步骤非常繁琐,在生成AVMutableVideoCompositionInstruction和AVMutableVideoCompositionLayerInstruction的计算也十分复杂,但其实还有一个创建视频组合的捷径。不过我们还是需要理解上一篇博客中我们所讨论的步骤,只有理解了那些步骤,才能发现学习这些对应的使用变得容易许多。
AVVideoComposition定义了一个十分便捷的初始化方法init(propertiesOf asset: AVAsset),我们可以将AVCompostion作为参数来创建一个AVVideoComposition。该方法会为我们创建一个带有如下配置的AVVideoComposition对象:
下面我们开始着手创建视频过渡,和之前的实现方案一样,先创建一个遵循PHComposition协议名为PHTransitionComposition的类,以及遵循PHCompositionBuilder协议名为PHTransitionCompositionBuilder的类。
PHTransitionComposition负责构建视频的可播放和可导出版本。
PHTransitionCompositionBuilder负责构建PHTransitionComposition,里面会创建用于视频编辑的AVMutableComposition,AVMutableAudioMix以及AVMutableVideoComposition。
代码实现如下:
- import UIKit
- import AVFoundation
-
- class PHTransitionComposition: NSObject,PHComposition {
- /// 组合轨道
- var composition:AVMutableComposition!
- /// 视频轨道
- var videoComposition:AVMutableVideoComposition?
- /// 音频混合
- var audioMix:AVMutableAudioMix?
-
- init(composition: AVMutableComposition!, videoComposition: AVMutableVideoComposition?, audioMix: AVMutableAudioMix?) {
- self.composition = composition
- self.videoComposition = videoComposition
- self.audioMix = audioMix
- }
-
- func makePlayerItem() -> AVPlayerItem? {
- let playerItem = AVPlayerItem(asset: composition.copy() as! AVAsset)
- playerItem.videoComposition = videoComposition
- playerItem.audioMix = audioMix
- return playerItem
- }
-
- func makeAssetExportSession() -> AVAssetExportSession? {
- let exportSession = AVAssetExportSession(asset: composition.copy() as! AVAsset, presetName: AVAssetExportPresetHighestQuality)
- exportSession?.videoComposition = videoComposition
- exportSession?.audioMix = audioMix
- return exportSession!
- }
-
- }
代码较以前的类相比多了一个AVMutableVideoComposition属性,用来实现视频的过渡效果。
该类中的代码和以往一样主要目的是构建PHComposition,分成三个部分,添加视频到组合轨道,添加音频到组合轨道,添加背景音乐到组合轨道。
- let defaultTransitionDuration = CMTime(value: 2, timescale: 1)
-
- class PHTransitionCompositionBuilder: NSObject,PHCompositionBuilder {
-
- /// 资源模型
- var timeLine:PHTimeLine!
- /// 组合轨道
- let composition = AVMutableComposition()
-
- init(timeLine: PHTimeLine!) {
- self.timeLine = timeLine
- }
-
- func buildComposition() -> PHComposition? {
- // 添加视频到组合轨道
- addVideoCompositionTrack()
- // 创建AVVideoComposition
- let videoComposition = buildVideoComposition()
- // 添加音频到组合轨道
- addAudioCompositionTrack()
- // 添加背景音乐到组合轨道
- let audioMix = addMusicCompositionTrack()
- return PHTransitionComposition(composition: composition, videoComposition: videoComposition, audioMix: audioMix)
- }
-
- /// 添加视频到组合轨道
- func addVideoCompositionTrack() {
- ....
- }
-
- /// 创建AVVideoComposition
- func buildVideoComposition() -> AVMutableVideoComposition? {
- ....
- }
-
-
-
-
- /// 添加音频到组合轨道
- func addAudioCompositionTrack() {
- let _ = addCompositionTrack(mediaType: .audio, mediaItems: timeLine.audioItems)
- }
-
- /// 添加背景音乐到组合轨道
- func addMusicCompositionTrack() -> AVMutableAudioMix?{
- // 添加背景音乐
- var audioMix:AVMutableAudioMix? = nil
- if timeLine.musicItem != nil {
- let musicCompositionTrack = addCompositionTrack(mediaType: .audio, mediaItems: [timeLine.musicItem!])
- let musicAudioMix = buildAudioMixWithTrack(track: musicCompositionTrack)
- audioMix = musicAudioMix
- }
- return audioMix
- }
-
-
- /// 私有方法-添加媒体资源轨道
- /// - Parameters:
- /// - mediaType: 媒体类型
- /// - mediaItems: 媒体媒体资源数组
- /// - Returns: 返回一个AVCompositionTrack
- private func addCompositionTrack(mediaType:AVMediaType,mediaItems:[PHMediaItem]?) -> AVMutableCompositionTrack? {
- if PHIsEmpty(array: mediaItems) {
- return nil
- }
- let trackID = kCMPersistentTrackID_Invalid
- guard let compositionTrack = composition.addMutableTrack(withMediaType: mediaType, preferredTrackID: trackID) else { return nil }
- //设置起始时间
- var cursorTime = CMTime.zero
- guard let mediaItems = mediaItems else { return nil }
- for item in mediaItems {
- //这里默认时间都是从0开始
- guard let asset = item.asset else { continue }
- guard let assetTrack = asset.tracks(withMediaType: mediaType).first else { continue }
- do {
- try compositionTrack.insertTimeRange(item.timeRange, of: assetTrack, at: cursorTime)
- } catch {
- print("addCompositionTrack error")
- }
- cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration)
- }
- return compositionTrack
- }
-
- /// 创建音频混合器
- /// - Parameters:
- /// - musicTrack: 音乐轨道
- func buildAudioMixWithTrack(track:AVMutableCompositionTrack?) -> AVMutableAudioMix? {
- guard let track = track else { return nil }
- guard let musicItem = timeLine.musicItem else { return nil }
- let audioMix = AVMutableAudioMix()
- let audioMixParam = AVMutableAudioMixInputParameters(track: track)
- for volumeAutomaition in musicItem.volumeAutomations {
- audioMixParam.setVolumeRamp(fromStartVolume: volumeAutomaition.startVolume, toEndVolume: volumeAutomaition.endVolume, timeRange: volumeAutomaition.timeRange)
- }
- audioMix.inputParameters = [audioMixParam]
- return audioMix
- }
-
-
-
- }
关于添加音频和背景音乐以及创建AVAudioMix的部分,我们在前面的博客中已经进行过介绍,在这里就不再重复解释了,把重点放到添加视频到组合轨道以及创建AVVideoComposition上。
下面看一下添加视频到组合轨道的实现:
- /// 添加视频到组合轨道
- func addVideoCompositionTrack() {
- let compositionTrackA = self.composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
- let compositionTrackB = self.composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
- let videoTracks = [compositionTrackA,compositionTrackB]
-
- let transitionDuration = defaultTransitionDuration
- // 初始时间
- var cursorTime = CMTime.zero
- let videoItems = timeLine.videoItmes
- for (index,item) in videoItems.enumerated() {
- let trackIndex = index % 2
- let currentTrack = videoTracks[trackIndex]
- guard let asset = item.asset else { continue }
- guard let assetTrack = asset.tracks(withMediaType: .video).first else { continue }
- do {
- try currentTrack?.insertTimeRange(item.timeRange, of: assetTrack, at: cursorTime)
- } catch {
- print("insertTimeRange error")
- }
- // 更新cursorTime
- cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration)
- cursorTime = CMTimeSubtract(cursorTime, transitionDuration)
- }
- }
创建AVVideoComposition的实现如下:
- /// 创建AVVideoComposition
- func buildVideoComposition() -> AVMutableVideoComposition? {
- let videoComposition = AVMutableVideoComposition(propertiesOf: self.composition)
- videoComposition.renderSize = CGSize(width: 1920, height: 1080)
- /// 获取instructions
- let instructions = videoComposition.instructions
- var layerInstructionIndex = 1
- for instruction in instructions {
- guard let videoCompositionInstruction = instruction as? AVMutableVideoCompositionInstruction else { continue }
- let layerInstructions = videoCompositionInstruction.layerInstructions
- // 判断是否有两个layerInstructions
- guard layerInstructions.count == 2 else { continue }
- // 创建过渡效果
- let fromeLayerInstruction = layerInstructions[1 - layerInstructionIndex] as! AVMutableVideoCompositionLayerInstruction
- let toLayerInstruction = layerInstructions[layerInstructionIndex] as! AVMutableVideoCompositionLayerInstruction
- // 设置动画
- fromeLayerInstruction.setOpacityRamp(fromStartOpacity: 1.0, toEndOpacity: 0.0, timeRange: videoCompositionInstruction.timeRange)
- layerInstructionIndex = layerInstructionIndex == 1 ? 0 : 1
- }
- return videoComposition
- }
除了上面列出的最简单的渐隐过渡方式,还支持很多其它的过渡方式。
推入过渡效果:
- // 推入过渡效果
- let identityTransform = CGAffineTransform.identity
- let videoWidth = videoComposition.renderSize.width
- let fromeTransform = CGAffineTransform(translationX: -videoWidth, y: 0)
- let toTransform = CGAffineTransform(translationX: videoWidth, y: 0)
- fromeLayerInstruction.setTransformRamp(fromStart: identityTransform, toEnd: fromeTransform, timeRange: videoCompositionInstruction.timeRange)
- toLayerInstruction.setTransformRamp(fromStart: toTransform, toEnd: identityTransform, timeRange: videoCompositionInstruction.timeRange)
擦除过渡效果:
- // 擦除过渡效果
- let width = videoComposition.renderSize.width
- let height = videoComposition.renderSize.height
- let startRect = CGRect(x: 0, y: 0, width: width, height: height)
- let endRect = CGRect(x: width, y: height, width: width, height: 0.0)
- fromeLayerInstruction.setCropRectangleRamp(fromStartCropRectangle: startRect, toEndCropRectangle: endRect, timeRange: videoCompositionInstruction.timeRange)
创建PHTransitionCompositionBuilder实例,构建视频的可播放版本,进行播放。
- func player() {
- guard let delegate = self.delegate else { return }
- let compositionBuilder = PHTransitionCompositionBuilder(timeLine: timeLine)
- let composition = compositionBuilder.buildComposition()
- let playerItem = composition?.makePlayerItem()
- delegate.replaceCurrentItem(playerItem: playerItem)
- }
代码中我们把工作的重点集中到了A-B轨道的方式交替添加视频资源,以及构建AVMutableVideoComposition并创建过渡效果上面。但事实上我们还需要注意到播放进度的显示,创建过渡后两个视频之间会有重叠部分,在呈现的时候需要减去重叠时间。
需要注意其它轨道的长度需要小于视频组合轨道的轨道长度。
另外最需要注意的是AVMutableVideoComposition的renderSize熟悉,一定要使用能够满足组合视频轨道中最大视频维度的尺寸值。