This commit is contained in:
DDIsFriend
2023-08-18 17:28:57 +08:00
commit f0e8a1709d
4282 changed files with 192396 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
//
// 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
}
}