123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 |
- //
- // 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 = .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.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)")
- }
- // NSError *error;
- // AVURLAsset *avurlasset = (AVURLAsset*)asset;
- // NSURL *fileURL = [NSURL fileURLWithPath:savePath];
- //
- // if ([[NSFileManager defaultManager] copyItemAtURL:avurlasset.URL toURL:fileURL error:&error]) {
- // CBLog(@"保存成功");
- // dispatch_async(dispatch_get_main_queue(), ^{
- // if (result) {
- // result(savePath,[savePath lastPathComponent]);
- // }
- // });
- // }else{
- // CBLog(@"error=%@",error);
- // [[NSFileManager defaultManager]removeItemAtPath:savePath error:nil];
- // dispatch_async(dispatch_get_main_queue(), ^{
- // failure(error.description);
- // });
- // }
- // 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) ? AVAssetExportPreset1920x1080 :(deliveryMode == .highQualityFormat ? AVAssetExportPresetHighestQuality : AVAssetExportPresetLowQuality) , resultHandler: { avAssetExportSession, _ in
- // BFLog(message: "导出相册视频-请求到导出 avAssetExportSession = \(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?.shouldOptimizeForNetworkUse = true
- // 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)
- }
- }
- }
- }
- /// 导出相册视频
- /// - 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, "导出失败")
- }
- 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 ..< frameCount {
- // 取出索引对应的图片
- guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else {
- BFLog(message: "取出对应的图片失败")
- return
- }
- if frameCount == 1 {
- // 单帧
- gifDuration = .infinity
- } else {
- // 1.获取gif没帧的时间间隔
- // 获取到该帧图片的属性字典
- guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as? [String: Any] else {
- BFLog(message: "取出对应的图片属性失败")
- return
- }
- // 获取该帧图片中的GIF相关的属性字典
- guard let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] else {
- BFLog(message: "取出对应的图片属性失败")
- return
- }
- let defaultFrameDuration = 0.1
- // 获取该帧图片的播放时间
- let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
- // 如果通过kCGImagePropertyGIFUnclampedDelayTime没有获取到播放时长,就通过kCGImagePropertyGIFDelayTime来获取,两者的含义是相同的;
- let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
- let duration = unclampedDelayTime ?? delayTime
- guard let frameDuration = duration else {
- BFLog(message: "获取帧时间间隔失败")
- return
- }
- // 对于播放时间低于0.011s的,重新指定时长为0.100s;
- let gifFrameDuration = frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
- // 计算总时间
- gifDuration += gifFrameDuration
- // 2.图片
- var frameImage: UIImage? = UIImage(cgImage: imageRef, scale: 1.0, orientation: .up)
- if isRenderingColor != nil {
- frameImage = frameImage?.tintImage(color: isRenderingColor!, blendMode: .destinationIn)
- }
- if frameImage != nil {
- images.append(frameImage!)
- }
- }
- }
- resultHandler(data, images, gifDuration)
- }
- }
|