234 lines
9.4 KiB
Swift
234 lines
9.4 KiB
Swift
//
|
||
// ZLVideoManager.swift
|
||
// ZLPhotoBrowser
|
||
//
|
||
// Created by long on 2020/9/23.
|
||
//
|
||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||
//
|
||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
// of this software and associated documentation files (the "Software"), to deal
|
||
// in the Software without restriction, including without limitation the rights
|
||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
// copies of the Software, and to permit persons to whom the Software is
|
||
// furnished to do so, subject to the following conditions:
|
||
//
|
||
// The above copyright notice and this permission notice shall be included in
|
||
// all copies or substantial portions of the Software.
|
||
//
|
||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
// THE SOFTWARE.
|
||
|
||
import UIKit
|
||
import AVFoundation
|
||
import Photos
|
||
|
||
public class ZLVideoManager: NSObject {
|
||
class func getVideoExportFilePath(format: String? = nil) -> String {
|
||
let format = format ?? ZLPhotoConfiguration.default().cameraConfiguration.videoExportType.format
|
||
return NSTemporaryDirectory().appendingFormat("%@.%@", UUID().uuidString, format)
|
||
}
|
||
|
||
class func exportEditVideo(for asset: AVAsset, range: CMTimeRange, complete: @escaping ((URL?, Error?) -> Void)) {
|
||
let type: ZLVideoManager.ExportType = ZLPhotoConfiguration.default().cameraConfiguration.videoExportType == .mov ? .mov : .mp4
|
||
exportVideo(for: asset, range: range, exportType: type, presetName: AVAssetExportPresetPassthrough) { url, error in
|
||
if url != nil {
|
||
complete(url!, error)
|
||
} else {
|
||
complete(nil, error)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 没有针对不同分辨率视频做处理,仅用于处理相机拍照的视频
|
||
@objc public class func mergeVideos(fileUrls: [URL], completion: @escaping ((URL?, Error?) -> Void)) {
|
||
let composition = AVMutableComposition()
|
||
let assets = fileUrls.map { AVURLAsset(url: $0) }
|
||
|
||
var insertTime: CMTime = .zero
|
||
var assetVideoTracks: [AVAssetTrack] = []
|
||
|
||
let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID())!
|
||
let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: CMPersistentTrackID())!
|
||
|
||
for asset in assets {
|
||
do {
|
||
let timeRange = CMTimeRangeMake(start: .zero, duration: asset.duration)
|
||
if let videoTrack = asset.tracks(withMediaType: .video).first {
|
||
try compositionVideoTrack.insertTimeRange(
|
||
timeRange,
|
||
of: videoTrack,
|
||
at: insertTime
|
||
)
|
||
|
||
assetVideoTracks.append(videoTrack)
|
||
}
|
||
|
||
if let audioTrack = asset.tracks(withMediaType: .audio).first {
|
||
try compositionAudioTrack.insertTimeRange(
|
||
timeRange,
|
||
of: audioTrack,
|
||
at: insertTime
|
||
)
|
||
}
|
||
|
||
insertTime = CMTimeAdd(insertTime, asset.duration)
|
||
} catch {
|
||
completion(nil, NSError.videoMergeError)
|
||
return
|
||
}
|
||
}
|
||
|
||
guard assetVideoTracks.count == assets.count else {
|
||
completion(nil, NSError.videoMergeError)
|
||
return
|
||
}
|
||
|
||
let renderSize = getNaturalSize(videoTrack: assetVideoTracks[0])
|
||
|
||
let videoComposition = AVMutableVideoComposition()
|
||
videoComposition.instructions = getInstructions(compositionTrack: compositionVideoTrack, assetVideoTracks: assetVideoTracks, assets: assets)
|
||
videoComposition.frameDuration = assetVideoTracks[0].minFrameDuration
|
||
videoComposition.renderSize = renderSize
|
||
videoComposition.renderScale = 1
|
||
|
||
guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPreset1280x720) else {
|
||
completion(nil, NSError.videoMergeError)
|
||
return
|
||
}
|
||
|
||
let outputUrl = URL(fileURLWithPath: ZLVideoManager.getVideoExportFilePath())
|
||
exportSession.outputURL = outputUrl
|
||
exportSession.shouldOptimizeForNetworkUse = true
|
||
exportSession.outputFileType = ZLPhotoConfiguration.default().cameraConfiguration.videoExportType.avFileType
|
||
exportSession.videoComposition = videoComposition
|
||
exportSession.exportAsynchronously(completionHandler: {
|
||
let suc = exportSession.status == .completed
|
||
if exportSession.status == .failed {
|
||
zl_debugPrint("ZLPhotoBrowser: video merge failed: \(exportSession.error?.localizedDescription ?? "")")
|
||
}
|
||
ZLMainAsync {
|
||
completion(suc ? outputUrl : nil, exportSession.error)
|
||
}
|
||
})
|
||
}
|
||
|
||
private static func getNaturalSize(videoTrack: AVAssetTrack) -> CGSize {
|
||
var size = videoTrack.naturalSize
|
||
if isPortraitVideoTrack(videoTrack) {
|
||
swap(&size.width, &size.height)
|
||
}
|
||
return size
|
||
}
|
||
|
||
private static func getInstructions(
|
||
compositionTrack: AVMutableCompositionTrack,
|
||
assetVideoTracks: [AVAssetTrack],
|
||
assets: [AVURLAsset]
|
||
) -> [AVMutableVideoCompositionInstruction] {
|
||
var instructions: [AVMutableVideoCompositionInstruction] = []
|
||
|
||
var start: CMTime = .zero
|
||
for (index, videoTrack) in assetVideoTracks.enumerated() {
|
||
let asset = assets[index]
|
||
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionTrack)
|
||
layerInstruction.setTransform(videoTrack.preferredTransform, at: .zero)
|
||
|
||
let instruction = AVMutableVideoCompositionInstruction()
|
||
instruction.timeRange = CMTimeRangeMake(start: start, duration: asset.duration)
|
||
instruction.layerInstructions = [layerInstruction]
|
||
instructions.append(instruction)
|
||
|
||
start = CMTimeAdd(start, asset.duration)
|
||
}
|
||
|
||
return instructions
|
||
}
|
||
|
||
private static func isPortraitVideoTrack(_ track: AVAssetTrack) -> Bool {
|
||
let transform = track.preferredTransform
|
||
let tfA = transform.a
|
||
let tfB = transform.b
|
||
let tfC = transform.c
|
||
let tfD = transform.d
|
||
|
||
if (tfA == 0 && tfB == 1 && tfC == -1 && tfD == 0) ||
|
||
(tfA == 0 && tfB == 1 && tfC == 1 && tfD == 0) ||
|
||
(tfA == 0 && tfB == -1 && tfC == 1 && tfD == 0) {
|
||
return true
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: export methods
|
||
|
||
public extension ZLVideoManager {
|
||
@objc class func exportVideo(for asset: PHAsset, exportType: ZLVideoManager.ExportType = .mov, presetName: String = AVAssetExportPresetMediumQuality, complete: @escaping ((URL?, Error?) -> Void)) {
|
||
guard asset.mediaType == .video else {
|
||
complete(nil, NSError.videoExportTypeError)
|
||
return
|
||
}
|
||
|
||
_ = ZLPhotoManager.fetchAVAsset(forVideo: asset) { avAsset, _ in
|
||
if let set = avAsset {
|
||
self.exportVideo(for: set, exportType: exportType, presetName: presetName, complete: complete)
|
||
} else {
|
||
complete(nil, NSError.videoExportError)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc class func exportVideo(for asset: AVAsset, range: CMTimeRange = CMTimeRange(start: .zero, duration: .positiveInfinity), exportType: ZLVideoManager.ExportType = .mov, presetName: String = AVAssetExportPresetMediumQuality, complete: @escaping ((URL?, Error?) -> Void)) {
|
||
let outputUrl = URL(fileURLWithPath: getVideoExportFilePath(format: exportType.format))
|
||
guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else {
|
||
complete(nil, NSError.videoExportError)
|
||
return
|
||
}
|
||
exportSession.outputURL = outputUrl
|
||
exportSession.outputFileType = exportType.avFileType
|
||
exportSession.timeRange = range
|
||
|
||
exportSession.exportAsynchronously(completionHandler: {
|
||
let suc = exportSession.status == .completed
|
||
if exportSession.status == .failed {
|
||
zl_debugPrint("ZLPhotoBrowser: video export failed: \(exportSession.error?.localizedDescription ?? "")")
|
||
}
|
||
ZLMainAsync {
|
||
complete(suc ? outputUrl : nil, exportSession.error)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
public extension ZLVideoManager {
|
||
@objc enum ExportType: Int {
|
||
var format: String {
|
||
switch self {
|
||
case .mov:
|
||
return "mov"
|
||
case .mp4:
|
||
return "mp4"
|
||
}
|
||
}
|
||
|
||
var avFileType: AVFileType {
|
||
switch self {
|
||
case .mov:
|
||
return .mov
|
||
case .mp4:
|
||
return .mp4
|
||
}
|
||
}
|
||
|
||
case mov
|
||
case mp4
|
||
}
|
||
}
|