// // PQPHAssetVideoParaseUtil.swift // PQSpeed // // Created by SanW on 2020/8/3. // Copyright © 2020 BytesFlow. All rights reserved. // import CoreServices import Photos import UIKit var currentExportSession: AVAssetExportSession? open class PQPHAssetVideoParaseUtil: NSObject { static var imagesOptions: PHImageRequestOptions = { let imagesOptions = PHImageRequestOptions() imagesOptions.isSynchronous = false imagesOptions.deliveryMode = .fastFormat imagesOptions.resizeMode = .fast imagesOptions.version = .current return imagesOptions }() static var singleImageOptions: PHImageRequestOptions = { let singleImageOptions = PHImageRequestOptions() singleImageOptions.isSynchronous = true singleImageOptions.isNetworkAccessAllowed = true singleImageOptions.deliveryMode = .highQualityFormat singleImageOptions.resizeMode = .none singleImageOptions.version = .current return singleImageOptions }() static var videoRequestOptions: PHVideoRequestOptions = { let videoRequestOptions = PHVideoRequestOptions() // 解决慢动作视频返回AVComposition而不是AVURLAsset // videoRequestOptions.version = .original videoRequestOptions.version = .current // 下载iCloud视频 videoRequestOptions.isNetworkAccessAllowed = true videoRequestOptions.deliveryMode = .mediumQualityFormat return videoRequestOptions }() /// PHAsset解析为AVPlayerItem /// - Parameters: /// - asset: <#asset description#> /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func parasToAVPlayerItem(phAsset: PHAsset, isHighQuality: Bool = false, resultHandler: @escaping (AVPlayerItem?, Float64, [AnyHashable: Any]?) -> Void) { PHImageManager().requestPlayerItem(forVideo: phAsset, options: videoRequestOptions) { playerItem, info in if isHighQuality, (playerItem?.asset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false { let tempVideoOptions = PHVideoRequestOptions() tempVideoOptions.version = .original // 下载iCloud视频 tempVideoOptions.isNetworkAccessAllowed = true tempVideoOptions.deliveryMode = .highQualityFormat tempVideoOptions.progressHandler = { progress, error, pointer, info in BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))") } PHImageManager().requestPlayerItem(forVideo: phAsset, options: tempVideoOptions) { playerItem, info in let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey]) BFLog(message: "size = \(String(describing: size))") resultHandler(playerItem, Float64(size?.fileSize ?? 0), info) } } else { let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey]) BFLog(message: "size = \(String(describing: size))") resultHandler(playerItem, Float64(size?.fileSize ?? 0), info) } } } /// PHAsset解析为AVAsset /// - Parameters: /// - asset: <#asset description#> /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func parasToAVAsset(phAsset: PHAsset, isHighQuality: Bool = true, resultHandler: @escaping (AVAsset?, Int, AVAudioMix?, [AnyHashable: Any]?) -> Void) { PHImageManager.default().requestAVAsset(forVideo: phAsset, options: videoRequestOptions) { avAsset, audioMix, info in if isHighQuality, (avAsset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false { let tempVideoOptions = PHVideoRequestOptions() tempVideoOptions.version = .current // 下载iCloud视频 tempVideoOptions.isNetworkAccessAllowed = true tempVideoOptions.deliveryMode = .highQualityFormat tempVideoOptions.progressHandler = { progress, error, pointer, info in BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))") } PHImageManager.default().requestAVAsset(forVideo: phAsset, options: tempVideoOptions) { tempAvAsset, tempAudioMix, tempInfo in let size = try! (tempAvAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey]) BFLog(message: "size = \(String(describing: size))") resultHandler(tempAvAsset, size?.fileSize ?? 0, tempAudioMix, tempInfo) } } else { let size = try! (avAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey]) resultHandler(avAsset, size?.fileSize ?? 0, audioMix, info) BFLog(message: "size = \(String(describing: size))") } } } /// PHAsset 转码为.mp4保存本地 /// - Parameters: /// - phAsset: <#phAsset description#> /// - isAdjustRotationAngle: 是否调整旋转角度 /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func exportPHAssetToMP4(phAsset: PHAsset, isAdjustRotationAngle: Bool = true, isCancelCurrentExport: Bool = false, deliveryMode: PHVideoRequestOptionsDeliveryMode? = .automatic, resultHandler: @escaping (_ phAsset: PHAsset, _ aVAsset: AVAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) { BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)") if isCancelCurrentExport { currentExportSession?.cancelExport() } PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in if avAsset is AVURLAsset { // 创建目录 createDirectory(path: photoLibraryDirectory) let fileName = (avAsset as! AVURLAsset).url.absoluteString let filePath = photoLibraryDirectory + fileName.md5.md5 + ".mp4" let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath)) if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 { BFLog(message: "导出相册视频-已经导出完成:\(filePath)") DispatchQueue.main.async { resultHandler(phAsset, avAsset, filePath, nil) } } else { // let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset] let tempExportSession: AVAssetExportSession? = nil if tempExportSession != nil { BFLog(message: "导出相册视频-正在导出") return } BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)") // 删除以创建地址 if FileManager.default.fileExists(atPath: filePath) { do { try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath)) } catch { BFLog(message: "导出相册视频-error == \(error)") } } let requestOptions = PHVideoRequestOptions() // 解决慢动作视频返回AVComposition而不是AVURLAsset // videoRequestOptions.version = .original requestOptions.version = .current // 下载iCloud视频 requestOptions.isNetworkAccessAllowed = false requestOptions.progressHandler = { progress, error, pointer, info in BFLog(message: "导出相册视频-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))") } requestOptions.deliveryMode = deliveryMode ?? .automatic PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: (deliveryMode == .automatic || deliveryMode == .mediumQualityFormat) ? AVAssetExportPresetMediumQuality : (deliveryMode == .highQualityFormat ? AVAssetExportPresetHighestQuality : AVAssetExportPresetLowQuality), resultHandler: { avAssetExportSession, _ in BFLog(message: "导出相册视频-请求到导出 avAssetExportSession = \(String(describing: avAssetExportSession))") currentExportSession = avAssetExportSession if avAssetExportSession != nil { // PQSingletoMemoryUtil.shared.allExportSession[phAsset] = avAssetExportSession! } avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL avAssetExportSession?.shouldOptimizeForNetworkUse = true avAssetExportSession?.outputFileType = .mp4 if isAdjustRotationAngle { let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: avAsset!) // mdf by ak 统一导出的视频为30FPS var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0) var mixedTransform: CGAffineTransform = CGAffineTransform() let videoComposition = AVMutableVideoComposition() videoComposition.frameDuration = CMTime(value: 1, timescale: 30) let tracks = avAsset?.tracks(withMediaType: .video) let firstTrack = tracks?.first videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0) mixedTransform = centerTranslate.rotated(by: 0) if rotationAngle == 90 { centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0) mixedTransform = centerTranslate.rotated(by: .pi / 2) videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0) } else if rotationAngle == 180 { centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0) mixedTransform = centerTranslate.rotated(by: .pi) videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0) } else if rotationAngle == 270 { centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0) mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3) videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0) } let roateInstruction = AVMutableVideoCompositionInstruction() roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: avAsset?.duration ?? CMTime.zero) if firstTrack != nil { let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!) layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero) roateInstruction.layerInstructions = [layRoateInstruction] videoComposition.instructions = [roateInstruction] avAssetExportSession?.videoComposition = videoComposition } else { BFLog(message: "firstTrack is error !!!") } } avAssetExportSession?.exportAsynchronously(completionHandler: { BFLog(message: "导出相册视频-progress = \(avAssetExportSession?.progress ?? 0),status = \(String(describing: avAssetExportSession?.status))") switch avAssetExportSession?.status { case .unknown: DispatchQueue.main.async { resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription) } avAssetExportSession?.cancelExport() // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset) BFLog(message: "导出相册视频-发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")") case .waiting: BFLog(message: "导出相册视频-等待导出mp4:\(filePath)") case .exporting: BFLog(message: "导出相册视频-导出相册视频中...:\(filePath)") case .completed: DispatchQueue.main.async { resultHandler(phAsset, avAsset, filePath, nil) } avAssetExportSession?.cancelExport() // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset) BFLog(message: "导出相册视频-导出完成:\(filePath)") case .failed: DispatchQueue.main.async { resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription) } avAssetExportSession?.cancelExport() // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset) BFLog(message: "导出相册视频-导出失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")") case .cancelled: DispatchQueue.main.async { resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription) } avAssetExportSession?.cancelExport() // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset) BFLog(message: "导出相册视频-取消导出:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")") default: break } }) }) } } else if avAsset is AVComposition { BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))") let assetResources = PHAssetResource.assetResources(for: phAsset) var resource: PHAssetResource? for assetRes in assetResources { if assetRes.type == .video || assetRes.type == .pairedVideo { resource = assetRes } } if phAsset.mediaType == .video, resource != nil { let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "") let filePath = photoLibraryDirectory + fileName.md5 + ".mp4" let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath)) if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 { DispatchQueue.main.async { resultHandler(phAsset, avAsset, filePath, nil) } } else { PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in DispatchQueue.main.async { resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil) } } } } else { DispatchQueue.main.async { resultHandler(phAsset, avAsset, nil, nil) } } } else { DispatchQueue.main.async { resultHandler(phAsset, avAsset, nil, nil) } } } } /// PHAsset 转码为.mp4保存本地 /// - Parameters: /// - phAsset: <#phAsset description#> /// - isAdjustRotationAngle: 是否调整旋转角度 /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func writePHAssetDataToMP4(phAsset: PHAsset, isAdjustRotationAngle _: Bool = true, isCancelCurrentExport: Bool = false, deliveryMode _: PHVideoRequestOptionsDeliveryMode? = .automatic, resultHandler: @escaping (_ phAsset: PHAsset, _ aVAsset: AVAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) { BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)") if isCancelCurrentExport { currentExportSession?.cancelExport() } PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in if avAsset is AVURLAsset { // 创建目录 createDirectory(path: photoLibraryDirectory) let fileName = (avAsset as! AVURLAsset).url.absoluteString let filePath = photoLibraryDirectory + fileName.md5 + ".mp4" let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath)) if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 { BFLog(message: "导出相册视频-已经导出完成:\(filePath)") DispatchQueue.main.async { resultHandler(phAsset, avAsset, filePath, nil) } } else { // let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset] let tempExportSession: AVAssetExportSession? = nil if tempExportSession != nil { BFLog(message: "导出相册视频-正在导出") return } BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)") // 删除以创建地址 if FileManager.default.fileExists(atPath: filePath) { do { try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath)) } catch { BFLog(message: "导出相册视频-error == \(error)") } } do { try FileManager.default.copyItem(at: (avAsset as! AVURLAsset).url, to: URL(fileURLWithPath: filePath)) } catch { BFLog(message: "导出相册视频-error == \(error)") } } } else if avAsset is AVComposition { BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))") let assetResources = PHAssetResource.assetResources(for: phAsset) var resource: PHAssetResource? for assetRes in assetResources { if assetRes.type == .video || assetRes.type == .pairedVideo { resource = assetRes } } if phAsset.mediaType == .video, resource != nil { let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "") let filePath = photoLibraryDirectory + fileName.md5 + ".mp4" let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath)) if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 { DispatchQueue.main.async { resultHandler(phAsset, avAsset, filePath, nil) } } else { PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in DispatchQueue.main.async { resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil) } } } } else { DispatchQueue.main.async { resultHandler(phAsset, avAsset, nil, nil) } } } else { DispatchQueue.main.async { resultHandler(phAsset, avAsset, nil, nil) } } } } /// 导出相册视频 /// - Parameters: /// - aVAsset: <#aVAsset description#> /// - isAdjustRotationAngle: <#isAdjustRotationAngle description#> /// - resultHandler: <#resultHandler description#> public class func exportAVAssetToMP4(aVAsset: AVURLAsset, isAdjustRotationAngle: Bool = true, resultHandler: @escaping (_ aVAsset: AVURLAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) { currentExportSession?.cancelExport() BFLog(message: "开始导出相册视频:url = \(aVAsset.url.absoluteString)") // 创建目录 createDirectory(path: photoLibraryDirectory) let fileName = aVAsset.url.absoluteString let filePath = photoLibraryDirectory + fileName.md5 + ".mp4" let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath)) let fileSize = try! aVAsset.url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 { DispatchQueue.main.async { resultHandler(aVAsset, filePath, nil) } } else { BFLog(message: "未导出视频过,开始导出:aVAsset = \(aVAsset)") // 删除以创建地址 try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath)) let avAssetExportSession = AVAssetExportSession(asset: aVAsset, presetName: AVAssetExportPreset1280x720) currentExportSession = avAssetExportSession avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL avAssetExportSession?.shouldOptimizeForNetworkUse = false avAssetExportSession?.outputFileType = .mp4 if isAdjustRotationAngle { let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: aVAsset) // mdf by ak 统一导出的视频为30FPS var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0) var mixedTransform: CGAffineTransform = CGAffineTransform() let videoComposition = AVMutableVideoComposition() videoComposition.frameDuration = CMTime(value: 1, timescale: 30) let tracks = aVAsset.tracks(withMediaType: .video) let firstTrack = tracks.first videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0) mixedTransform = centerTranslate.rotated(by: 0) if rotationAngle == 90 { centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0) mixedTransform = centerTranslate.rotated(by: .pi / 2) videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0) } else if rotationAngle == 180 { centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0) mixedTransform = centerTranslate.rotated(by: .pi) videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0) } else if rotationAngle == 270 { centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0) mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3) videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0) } let roateInstruction = AVMutableVideoCompositionInstruction() roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: aVAsset.duration) let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!) layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero) roateInstruction.layerInstructions = [layRoateInstruction] videoComposition.instructions = [roateInstruction] avAssetExportSession?.videoComposition = videoComposition } avAssetExportSession?.shouldOptimizeForNetworkUse = true avAssetExportSession?.exportAsynchronously(completionHandler: { BFLog(message: "导出相册视频progress = \(avAssetExportSession?.progress ?? 0)") switch avAssetExportSession?.status { case .unknown: DispatchQueue.main.async { resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription) } BFLog(message: "导出相册视频发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")") case .waiting: BFLog(message: "等待导出mp4:\(filePath)") case .exporting: BFLog(message: "导出相册视频中...:\(filePath)") case .completed: DispatchQueue.main.async { resultHandler(aVAsset, filePath, nil) } BFLog(message: "导出相册视频完成:\(filePath)") case .failed: DispatchQueue.main.async { resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription) } BFLog(message: "导出相册视频失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")") case .cancelled: DispatchQueue.main.async { resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription) } BFLog(message: "取消导出相册视频:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")") default: break } }) } } /// 获取视频资源的旋转角度 /// - Parameter assert: <#assert description#> /// - Returns: <#description#> public class func videoRotationAngle(assert: AVAsset) -> Int { var rotationAngle: Int = 0 let tracks = assert.tracks(withMediaType: .video) if tracks.count > 0 { let firstTrack = tracks.first let transform = firstTrack?.preferredTransform if transform?.a == 0, transform?.b == 1.0, transform?.c == -1.0, transform?.d == 0 { rotationAngle = 90 } else if transform?.a == -1.0, transform?.b == 0, transform?.c == 0, transform?.d == -1.0 { rotationAngle = 180 } else if transform?.a == 0, transform?.b == -1.0, transform?.c == 1.0, transform?.d == 0 { rotationAngle = 270 } else if transform?.a == 1.0, transform?.b == 0, transform?.c == 0, transform?.d == 1.0 { rotationAngle = 0 } } return rotationAngle } /// 裁剪背景音乐并导出 /// - Parameters: /// - url: 原始地址 /// - startTime: 开始时间 /// - endTime: 结束时间 /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func cutAudioToLocal(url: String, startTime: Float, endTime: Float, resultHandler: @escaping (_ url: String, _ filePath: String?, _ startTime: Float, _ endTime: Float, _ errorMsg: String?) -> Void) { // 创建目录 createDirectory(path: bgMusicDirectory) let filePath = bgMusicDirectory + url.md5 + ".mp3" let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath)) if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > 0 { DispatchQueue.main.async { resultHandler(url, filePath, startTime, endTime, nil) } } else { // 删除以创建地址 try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath)) let audioAsset = AVURLAsset(url: URL(string: url)!) audioAsset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) { let status = audioAsset.statusOfValue(forKey: "tracks", error: nil) switch status { case .loaded: // 加载完成 // AVAssetExportPresetPassthrough /AVAssetExportPresetAppleM4A let exportSession = AVAssetExportSession(asset: audioAsset, presetName: AVAssetExportPresetHighestQuality) exportSession?.outputURL = URL(fileURLWithPath: filePath) exportSession?.outputFileType = .mp3 exportSession?.timeRange = CMTimeRange(start: CMTime(seconds: Double(startTime), preferredTimescale: 1000), end: CMTime(seconds: Double(endTime), preferredTimescale: 1000)) exportSession?.exportAsynchronously(completionHandler: { switch exportSession?.status { case .waiting: BFLog(message: "等待导出mp3:\(filePath)") case .exporting: BFLog(message: "导出中...:\(filePath)") case .completed: DispatchQueue.main.async { resultHandler(url, filePath, startTime, endTime, nil) } BFLog(message: "导出完成:\(filePath)") case .cancelled, .failed, .unknown: DispatchQueue.main.async { resultHandler(url, nil, startTime, endTime, exportSession?.error?.localizedDescription) } BFLog(message: "导出失败:\(filePath),\(exportSession?.error?.localizedDescription ?? "")") default: break } }) case .loading: BFLog(message: "加载中...:\(url)") case .failed, .cancelled, .unknown: DispatchQueue.main.async { resultHandler(url, nil, startTime, endTime, "compose_fail_export".BFLocale) } default: break } } } } /// 创建本地保存地址 /// - Parameters: /// - sourceFilePath: <#sourceFilePath description#> /// - completeHandle: <#completeHandle description#> /// - Returns: <#description#> public class func createLocalFile(sourceFilePath: String, completeHandle: (_ isFileExists: Bool, _ isCreateSuccess: Bool, _ filePath: String) -> Void) { let cLocalPath = NSString(string: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("\(sourceFilePath.md5).mp4") if FileManager.default.fileExists(atPath: cLocalPath) { BFLog(message: "文件已经存在:\(cLocalPath)") completeHandle(true, false, cLocalPath) } else { let result = FileManager.default.createFile(atPath: cLocalPath, contents: nil, attributes: nil) BFLog(message: "文件创建:\(cLocalPath),\(result)") completeHandle(false, result, cLocalPath) } } /// 获取图库图片 /// - Parameters: /// - asset: <#asset description#> /// - itemSize: <#itemSize description#> /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func requestAssetImage(asset: PHAsset, itemSize: CGSize, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) { PHCachingImageManager().requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: imagesOptions, resultHandler: { image, info in BFLog(message: "info = \(info ?? [:])") if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0" { resultHandler(image, info) } }) } /// 获取图库原图 /// - Parameters: /// - asset: <#asset description#> /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func requestAssetOringinImage(asset: PHAsset, resultHandler: @escaping (_ isGIF: Bool, _ data: Data?, UIImage?, [AnyHashable: Any]?) -> Void) { PHCachingImageManager().requestImageData(for: asset, options: singleImageOptions) { data, _, _, info in var image: UIImage? if data != nil { image = UIImage(data: data!) } if info?.keys.contains("PHImageFileUTIKey") ?? false, "\(info?["PHImageFileUTIKey"] ?? "")" == "com.compuserve.gif" { resultHandler(true, data, image, info) } else { resultHandler(false, data, image, info) } } } /// 获取gif帧跟时长 /// - Parameters: /// - data: <#data description#> /// - isRenderingTemplate /// - resultHandler: <#resultHandler description#> /// - Returns: <#description#> public class func parasGIFImage(data: Data, isRenderingColor: UIColor? = nil, resultHandler: @escaping (_ data: Data, _ images: [UIImage]?, _ duration: Double?) -> Void) { let info: [String: Any] = [ kCGImageSourceShouldCache as String: true, kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF, ] guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else { resultHandler(data, nil, nil) BFLog(message: "获取gifimageSource 失败") return } // 获取帧数 let frameCount = CGImageSourceGetCount(imageSource) var gifDuration = 0.0 var images = [UIImage]() for i in 0.. 0.011 ? frameDuration.doubleValue : defaultFrameDuration // 计算总时间 gifDuration += gifFrameDuration // 2.图片 var frameImage: UIImage? = UIImage(cgImage: imageRef, scale: 1.0, orientation: .up) if isRenderingColor != nil, frameImage != nil { frameImage = tintImage(image: frameImage!, color: isRenderingColor!, blendMode: .destinationIn) } if frameImage != nil { images.append(frameImage!) } } } resultHandler(data, images, gifDuration) } /// 改变图片主题颜色 /// - Parameters: /// - color: <#color description#> /// - blendMode: <#blendMode description#> /// - Returns: <#description#> public class func tintImage(image: UIImage, color: UIColor, blendMode: CGBlendMode) -> UIImage? { let rect = CGRect(origin: CGPoint.zero, size: image.size) UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) color.setFill() UIRectFill(rect) image.draw(in: rect, blendMode: blendMode, alpha: 1.0) let tintedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return tintedImage } }