PQPHAssetVideoParaseUtil.swift 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. //
  2. // PQPHAssetVideoParaseUtil.swift
  3. // PQSpeed
  4. //
  5. // Created by SanW on 2020/8/3.
  6. // Copyright © 2020 BytesFlow. All rights reserved.
  7. //
  8. import CoreServices
  9. import Photos
  10. import UIKit
  11. var currentExportSession: AVAssetExportSession?
  12. open class PQPHAssetVideoParaseUtil: NSObject {
  13. static var imagesOptions: PHImageRequestOptions = {
  14. let imagesOptions = PHImageRequestOptions()
  15. imagesOptions.isSynchronous = false
  16. imagesOptions.deliveryMode = .fastFormat
  17. imagesOptions.resizeMode = .fast
  18. imagesOptions.version = .current
  19. return imagesOptions
  20. }()
  21. static var singleImageOptions: PHImageRequestOptions = {
  22. let singleImageOptions = PHImageRequestOptions()
  23. singleImageOptions.isSynchronous = true
  24. singleImageOptions.isNetworkAccessAllowed = true
  25. singleImageOptions.deliveryMode = .highQualityFormat
  26. singleImageOptions.resizeMode = .none
  27. singleImageOptions.version = .current
  28. return singleImageOptions
  29. }()
  30. static var videoRequestOptions: PHVideoRequestOptions = {
  31. let videoRequestOptions = PHVideoRequestOptions()
  32. // 解决慢动作视频返回AVComposition而不是AVURLAsset
  33. // videoRequestOptions.version = .original
  34. videoRequestOptions.version = .current
  35. // 下载iCloud视频
  36. videoRequestOptions.isNetworkAccessAllowed = true
  37. videoRequestOptions.deliveryMode = .mediumQualityFormat
  38. return videoRequestOptions
  39. }()
  40. /// PHAsset解析为AVPlayerItem
  41. /// - Parameters:
  42. /// - asset: <#asset description#>
  43. /// - resultHandler: <#resultHandler description#>
  44. /// - Returns: <#description#>
  45. public class func parasToAVPlayerItem(phAsset: PHAsset, isHighQuality: Bool = false, resultHandler: @escaping (AVPlayerItem?, Float64, [AnyHashable: Any]?) -> Void) {
  46. PHImageManager().requestPlayerItem(forVideo: phAsset, options: videoRequestOptions) { playerItem, info in
  47. if isHighQuality, (playerItem?.asset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false {
  48. let tempVideoOptions = PHVideoRequestOptions()
  49. tempVideoOptions.version = .original
  50. // 下载iCloud视频
  51. tempVideoOptions.isNetworkAccessAllowed = true
  52. tempVideoOptions.deliveryMode = .highQualityFormat
  53. tempVideoOptions.progressHandler = { progress, error, pointer, info in
  54. BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
  55. }
  56. PHImageManager().requestPlayerItem(forVideo: phAsset, options: tempVideoOptions) { playerItem, info in
  57. let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
  58. BFLog(message: "size = \(String(describing: size))")
  59. resultHandler(playerItem, Float64(size?.fileSize ?? 0), info)
  60. }
  61. } else {
  62. let size = try! (playerItem?.asset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
  63. BFLog(message: "size = \(String(describing: size))")
  64. resultHandler(playerItem, Float64(size?.fileSize ?? 0), info)
  65. }
  66. }
  67. }
  68. /// PHAsset解析为AVAsset
  69. /// - Parameters:
  70. /// - asset: <#asset description#>
  71. /// - resultHandler: <#resultHandler description#>
  72. /// - Returns: <#description#>
  73. public class func parasToAVAsset(phAsset: PHAsset, isHighQuality: Bool = true, resultHandler: @escaping (AVAsset?, Int, AVAudioMix?, [AnyHashable: Any]?) -> Void) {
  74. PHImageManager.default().requestAVAsset(forVideo: phAsset, options: videoRequestOptions) { avAsset, audioMix, info in
  75. if isHighQuality, (avAsset as? AVURLAsset)?.url.absoluteString.components(separatedBy: "/").last?.contains(".medium.") ?? false {
  76. let tempVideoOptions = PHVideoRequestOptions()
  77. tempVideoOptions.version = .original
  78. // 下载iCloud视频
  79. tempVideoOptions.isNetworkAccessAllowed = true
  80. tempVideoOptions.deliveryMode = .highQualityFormat
  81. tempVideoOptions.progressHandler = { progress, error, pointer, info in
  82. BFLog(message: "导出playerItem-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
  83. }
  84. PHImageManager.default().requestAVAsset(forVideo: phAsset, options: tempVideoOptions) { tempAvAsset, tempAudioMix, tempInfo in
  85. let size = try! (tempAvAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
  86. BFLog(message: "size = \(String(describing: size))")
  87. resultHandler(tempAvAsset, size?.fileSize ?? 0, tempAudioMix, tempInfo)
  88. }
  89. } else {
  90. let size = try! (avAsset as? AVURLAsset)?.url.resourceValues(forKeys: [.fileSizeKey])
  91. resultHandler(avAsset, size?.fileSize ?? 0, audioMix, info)
  92. BFLog(message: "size = \(String(describing: size))")
  93. }
  94. }
  95. }
  96. /// PHAsset 转码为.mp4保存本地
  97. /// - Parameters:
  98. /// - phAsset: <#phAsset description#>
  99. /// - isAdjustRotationAngle: 是否调整旋转角度
  100. /// - resultHandler: <#resultHandler description#>
  101. /// - Returns: <#description#>
  102. 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) {
  103. BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)")
  104. if isCancelCurrentExport {
  105. currentExportSession?.cancelExport()
  106. }
  107. PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in
  108. if avAsset is AVURLAsset {
  109. // 创建目录
  110. createDirectory(path: photoLibraryDirectory)
  111. let fileName = (avAsset as! AVURLAsset).url.absoluteString
  112. let filePath = photoLibraryDirectory + fileName.md5.md5 + ".mp4"
  113. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  114. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
  115. BFLog(message: "导出相册视频-已经导出完成:\(filePath)")
  116. DispatchQueue.main.async {
  117. resultHandler(phAsset, avAsset, filePath, nil)
  118. }
  119. } else {
  120. // let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset]
  121. let tempExportSession : AVAssetExportSession? = nil
  122. if tempExportSession != nil {
  123. BFLog(message: "导出相册视频-正在导出")
  124. return
  125. }
  126. BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)")
  127. // 删除以创建地址
  128. if FileManager.default.fileExists(atPath: filePath) {
  129. do {
  130. try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
  131. } catch {
  132. BFLog(message: "导出相册视频-error == \(error)")
  133. }
  134. }
  135. let requestOptions = PHVideoRequestOptions()
  136. // 解决慢动作视频返回AVComposition而不是AVURLAsset
  137. // videoRequestOptions.version = .original
  138. requestOptions.version = .current
  139. // 下载iCloud视频
  140. requestOptions.isNetworkAccessAllowed = false
  141. requestOptions.progressHandler = { progress, error, pointer, info in
  142. BFLog(message: "导出相册视频-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
  143. }
  144. requestOptions.deliveryMode = deliveryMode ?? .automatic
  145. PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: (deliveryMode == .automatic || deliveryMode == .mediumQualityFormat) ? AVAssetExportPresetMediumQuality : (deliveryMode == .highQualityFormat ? AVAssetExportPresetHighestQuality : AVAssetExportPresetLowQuality), resultHandler: { avAssetExportSession, _ in
  146. BFLog(message: "导出相册视频-请求到导出 avAssetExportSession = \(String(describing: avAssetExportSession))")
  147. currentExportSession = avAssetExportSession
  148. if avAssetExportSession != nil {
  149. // PQSingletoMemoryUtil.shared.allExportSession[phAsset] = avAssetExportSession!
  150. }
  151. avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
  152. avAssetExportSession?.shouldOptimizeForNetworkUse = true
  153. avAssetExportSession?.outputFileType = .mp4
  154. if isAdjustRotationAngle {
  155. let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: avAsset!)
  156. // mdf by ak 统一导出的视频为30FPS
  157. var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
  158. var mixedTransform: CGAffineTransform = CGAffineTransform()
  159. let videoComposition = AVMutableVideoComposition()
  160. videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
  161. let tracks = avAsset?.tracks(withMediaType: .video)
  162. let firstTrack = tracks?.first
  163. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  164. mixedTransform = centerTranslate.rotated(by: 0)
  165. if rotationAngle == 90 {
  166. centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
  167. mixedTransform = centerTranslate.rotated(by: .pi / 2)
  168. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  169. } else if rotationAngle == 180 {
  170. centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
  171. mixedTransform = centerTranslate.rotated(by: .pi)
  172. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  173. } else if rotationAngle == 270 {
  174. centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
  175. mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
  176. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  177. }
  178. let roateInstruction = AVMutableVideoCompositionInstruction()
  179. roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: avAsset?.duration ?? CMTime.zero)
  180. if firstTrack != nil {
  181. let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
  182. layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
  183. roateInstruction.layerInstructions = [layRoateInstruction]
  184. videoComposition.instructions = [roateInstruction]
  185. avAssetExportSession?.videoComposition = videoComposition
  186. } else {
  187. BFLog(message: "firstTrack is error !!!")
  188. }
  189. }
  190. avAssetExportSession?.exportAsynchronously(completionHandler: {
  191. BFLog(message: "导出相册视频-progress = \(avAssetExportSession?.progress ?? 0),status = \(String(describing: avAssetExportSession?.status))")
  192. switch avAssetExportSession?.status {
  193. case .unknown:
  194. DispatchQueue.main.async {
  195. resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
  196. }
  197. avAssetExportSession?.cancelExport()
  198. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  199. BFLog(message: "导出相册视频-发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  200. case .waiting:
  201. BFLog(message: "导出相册视频-等待导出mp4:\(filePath)")
  202. case .exporting:
  203. BFLog(message: "导出相册视频-导出相册视频中...:\(filePath)")
  204. case .completed:
  205. DispatchQueue.main.async {
  206. resultHandler(phAsset, avAsset, filePath, nil)
  207. }
  208. avAssetExportSession?.cancelExport()
  209. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  210. BFLog(message: "导出相册视频-导出完成:\(filePath)")
  211. case .failed:
  212. DispatchQueue.main.async {
  213. resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
  214. }
  215. avAssetExportSession?.cancelExport()
  216. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  217. BFLog(message: "导出相册视频-导出失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  218. case .cancelled:
  219. DispatchQueue.main.async {
  220. resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
  221. }
  222. avAssetExportSession?.cancelExport()
  223. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  224. BFLog(message: "导出相册视频-取消导出:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  225. default:
  226. break
  227. }
  228. })
  229. })
  230. }
  231. } else if avAsset is AVComposition {
  232. BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))")
  233. let assetResources = PHAssetResource.assetResources(for: phAsset)
  234. var resource: PHAssetResource?
  235. for assetRes in assetResources {
  236. if assetRes.type == .video || assetRes.type == .pairedVideo {
  237. resource = assetRes
  238. }
  239. }
  240. if phAsset.mediaType == .video, resource != nil {
  241. let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "")
  242. let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
  243. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  244. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
  245. DispatchQueue.main.async {
  246. resultHandler(phAsset, avAsset, filePath, nil)
  247. }
  248. } else {
  249. PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in
  250. DispatchQueue.main.async {
  251. resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil)
  252. }
  253. }
  254. }
  255. } else {
  256. DispatchQueue.main.async {
  257. resultHandler(phAsset, avAsset, nil, nil)
  258. }
  259. }
  260. } else {
  261. DispatchQueue.main.async {
  262. resultHandler(phAsset, avAsset, nil, nil)
  263. }
  264. }
  265. }
  266. }
  267. /// PHAsset 转码为.mp4保存本地
  268. /// - Parameters:
  269. /// - phAsset: <#phAsset description#>
  270. /// - isAdjustRotationAngle: 是否调整旋转角度
  271. /// - resultHandler: <#resultHandler description#>
  272. /// - Returns: <#description#>
  273. 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) {
  274. BFLog(message: "导出相册视频-开始导出:phAsset = \(phAsset)")
  275. if isCancelCurrentExport {
  276. currentExportSession?.cancelExport()
  277. }
  278. PQPHAssetVideoParaseUtil.parasToAVAsset(phAsset: phAsset) { avAsset, fileSize, _, _ in
  279. if avAsset is AVURLAsset {
  280. // 创建目录
  281. createDirectory(path: photoLibraryDirectory)
  282. let fileName = (avAsset as! AVURLAsset).url.absoluteString
  283. let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
  284. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  285. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
  286. BFLog(message: "导出相册视频-已经导出完成:\(filePath)")
  287. DispatchQueue.main.async {
  288. resultHandler(phAsset, avAsset, filePath, nil)
  289. }
  290. } else {
  291. // let tempExportSession = PQSingletoMemoryUtil.shared.allExportSession[phAsset]
  292. let tempExportSession : AVAssetExportSession? = nil
  293. if tempExportSession != nil {
  294. BFLog(message: "导出相册视频-正在导出")
  295. return
  296. }
  297. BFLog(message: "导出相册视频-未导出视频过,开始导出:phAsset = \(phAsset)")
  298. // 删除以创建地址
  299. if FileManager.default.fileExists(atPath: filePath) {
  300. do {
  301. try FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
  302. } catch {
  303. BFLog(message: "导出相册视频-error == \(error)")
  304. }
  305. }
  306. do {
  307. try FileManager.default.copyItem(at: (avAsset as! AVURLAsset).url, to: URL(fileURLWithPath: filePath))
  308. } catch {
  309. BFLog(message: "导出相册视频-error == \(error)")
  310. }
  311. // NSError *error;
  312. // AVURLAsset *avurlasset = (AVURLAsset*)asset;
  313. // NSURL *fileURL = [NSURL fileURLWithPath:savePath];
  314. //
  315. // if ([[NSFileManager defaultManager] copyItemAtURL:avurlasset.URL toURL:fileURL error:&error]) {
  316. // CBLog(@"保存成功");
  317. // dispatch_async(dispatch_get_main_queue(), ^{
  318. // if (result) {
  319. // result(savePath,[savePath lastPathComponent]);
  320. // }
  321. // });
  322. // }else{
  323. // CBLog(@"error=%@",error);
  324. // [[NSFileManager defaultManager]removeItemAtPath:savePath error:nil];
  325. // dispatch_async(dispatch_get_main_queue(), ^{
  326. // failure(error.description);
  327. // });
  328. // }
  329. // let requestOptions = PHVideoRequestOptions()
  330. // // 解决慢动作视频返回AVComposition而不是AVURLAsset
  331. // // videoRequestOptions.version = .original
  332. // requestOptions.version = .current
  333. // // 下载iCloud视频
  334. // requestOptions.isNetworkAccessAllowed = false
  335. // requestOptions.progressHandler = { progress, error, pointer, info in
  336. // BFLog(message: "导出相册视频-progress = \(progress),error = \(String(describing: error)),pointer = \(pointer),info = \(String(describing: info))")
  337. // }
  338. // requestOptions.deliveryMode = deliveryMode ?? .automatic
  339. // PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: (deliveryMode == .automatic || deliveryMode == .mediumQualityFormat) ? AVAssetExportPreset1920x1080 :(deliveryMode == .highQualityFormat ? AVAssetExportPresetHighestQuality : AVAssetExportPresetLowQuality) , resultHandler: { avAssetExportSession, _ in
  340. // BFLog(message: "导出相册视频-请求到导出 avAssetExportSession = \(avAssetExportSession)")
  341. // currentExportSession = avAssetExportSession
  342. // if avAssetExportSession != nil {
  343. // PQSingletoMemoryUtil.shared.allExportSession[phAsset] = avAssetExportSession!
  344. // }
  345. // avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
  346. // avAssetExportSession?.shouldOptimizeForNetworkUse = true
  347. // avAssetExportSession?.outputFileType = .mp4
  348. // if isAdjustRotationAngle {
  349. // let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: avAsset!)
  350. // // mdf by ak 统一导出的视频为30FPS
  351. // var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
  352. // var mixedTransform: CGAffineTransform = CGAffineTransform()
  353. // let videoComposition = AVMutableVideoComposition()
  354. // videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
  355. // let tracks = avAsset?.tracks(withMediaType: .video)
  356. // let firstTrack = tracks?.first
  357. //
  358. // videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  359. //
  360. // mixedTransform = centerTranslate.rotated(by: 0)
  361. //
  362. // if rotationAngle == 90 {
  363. // centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
  364. // mixedTransform = centerTranslate.rotated(by: .pi / 2)
  365. // videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  366. // } else if rotationAngle == 180 {
  367. // centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
  368. // mixedTransform = centerTranslate.rotated(by: .pi)
  369. // videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  370. // } else if rotationAngle == 270 {
  371. // centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
  372. // mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
  373. // videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  374. // }
  375. // let roateInstruction = AVMutableVideoCompositionInstruction()
  376. // roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: avAsset?.duration ?? CMTime.zero)
  377. // if firstTrack != nil {
  378. // let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
  379. // layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
  380. // roateInstruction.layerInstructions = [layRoateInstruction]
  381. // videoComposition.instructions = [roateInstruction]
  382. // avAssetExportSession?.videoComposition = videoComposition
  383. // } else {
  384. // BFLog(message: "firstTrack is error !!!")
  385. // }
  386. // }
  387. // avAssetExportSession?.shouldOptimizeForNetworkUse = true
  388. // avAssetExportSession?.exportAsynchronously(completionHandler: {
  389. // BFLog(message: "导出相册视频-progress = \(avAssetExportSession?.progress ?? 0),status = \(String(describing: avAssetExportSession?.status))")
  390. // switch avAssetExportSession?.status {
  391. // case .unknown:
  392. // DispatchQueue.main.async {
  393. // resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
  394. // }
  395. // avAssetExportSession?.cancelExport()
  396. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  397. // BFLog(message: "导出相册视频-发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  398. // case .waiting:
  399. // BFLog(message: "导出相册视频-等待导出mp4:\(filePath)")
  400. // case .exporting:
  401. // BFLog(message: "导出相册视频-导出相册视频中...:\(filePath)")
  402. // case .completed:
  403. // DispatchQueue.main.async {
  404. // resultHandler(phAsset, avAsset, filePath, nil)
  405. // }
  406. // avAssetExportSession?.cancelExport()
  407. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  408. // BFLog(message: "导出相册视频-导出完成:\(filePath)")
  409. // case .failed:
  410. // DispatchQueue.main.async {
  411. // resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
  412. // }
  413. // avAssetExportSession?.cancelExport()
  414. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  415. // BFLog(message: "导出相册视频-导出失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  416. // case .cancelled:
  417. // DispatchQueue.main.async {
  418. // resultHandler(phAsset, avAsset, nil, avAssetExportSession?.error?.localizedDescription)
  419. // }
  420. // avAssetExportSession?.cancelExport()
  421. // PQSingletoMemoryUtil.shared.allExportSession.removeValue(forKey: phAsset)
  422. // BFLog(message: "导出相册视频-取消导出:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  423. // default:
  424. // break
  425. // }
  426. // })
  427. // })
  428. }
  429. } else if avAsset is AVComposition {
  430. BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))")
  431. let assetResources = PHAssetResource.assetResources(for: phAsset)
  432. var resource: PHAssetResource?
  433. for assetRes in assetResources {
  434. if assetRes.type == .video || assetRes.type == .pairedVideo {
  435. resource = assetRes
  436. }
  437. }
  438. if phAsset.mediaType == .video, resource != nil {
  439. let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "")
  440. let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
  441. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  442. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
  443. DispatchQueue.main.async {
  444. resultHandler(phAsset, avAsset, filePath, nil)
  445. }
  446. } else {
  447. PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in
  448. DispatchQueue.main.async {
  449. resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil)
  450. }
  451. }
  452. }
  453. } else {
  454. DispatchQueue.main.async {
  455. resultHandler(phAsset, avAsset, nil, nil)
  456. }
  457. }
  458. } else {
  459. DispatchQueue.main.async {
  460. resultHandler(phAsset, avAsset, nil, nil)
  461. }
  462. }
  463. }
  464. }
  465. /// 导出相册视频
  466. /// - Parameters:
  467. /// - aVAsset: <#aVAsset description#>
  468. /// - isAdjustRotationAngle: <#isAdjustRotationAngle description#>
  469. /// - resultHandler: <#resultHandler description#>
  470. public class func exportAVAssetToMP4(aVAsset: AVURLAsset, isAdjustRotationAngle: Bool = true, resultHandler: @escaping (_ aVAsset: AVURLAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
  471. currentExportSession?.cancelExport()
  472. BFLog(message: "开始导出相册视频:url = \(aVAsset.url.absoluteString)")
  473. // 创建目录
  474. createDirectory(path: photoLibraryDirectory)
  475. let fileName = aVAsset.url.absoluteString
  476. let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
  477. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  478. let fileSize = try! aVAsset.url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
  479. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
  480. DispatchQueue.main.async {
  481. resultHandler(aVAsset, filePath, nil)
  482. }
  483. } else {
  484. BFLog(message: "未导出视频过,开始导出:aVAsset = \(aVAsset)")
  485. // 删除以创建地址
  486. try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
  487. let avAssetExportSession = AVAssetExportSession(asset: aVAsset, presetName: AVAssetExportPreset1280x720)
  488. currentExportSession = avAssetExportSession
  489. avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
  490. avAssetExportSession?.shouldOptimizeForNetworkUse = false
  491. avAssetExportSession?.outputFileType = .mp4
  492. if isAdjustRotationAngle {
  493. let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: aVAsset)
  494. // mdf by ak 统一导出的视频为30FPS
  495. var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
  496. var mixedTransform: CGAffineTransform = CGAffineTransform()
  497. let videoComposition = AVMutableVideoComposition()
  498. videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
  499. let tracks = aVAsset.tracks(withMediaType: .video)
  500. let firstTrack = tracks.first
  501. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  502. mixedTransform = centerTranslate.rotated(by: 0)
  503. if rotationAngle == 90 {
  504. centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
  505. mixedTransform = centerTranslate.rotated(by: .pi / 2)
  506. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  507. } else if rotationAngle == 180 {
  508. centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
  509. mixedTransform = centerTranslate.rotated(by: .pi)
  510. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  511. } else if rotationAngle == 270 {
  512. centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
  513. mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
  514. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  515. }
  516. let roateInstruction = AVMutableVideoCompositionInstruction()
  517. roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: aVAsset.duration)
  518. let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
  519. layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
  520. roateInstruction.layerInstructions = [layRoateInstruction]
  521. videoComposition.instructions = [roateInstruction]
  522. avAssetExportSession?.videoComposition = videoComposition
  523. }
  524. avAssetExportSession?.shouldOptimizeForNetworkUse = true
  525. avAssetExportSession?.exportAsynchronously(completionHandler: {
  526. BFLog(message: "导出相册视频progress = \(avAssetExportSession?.progress ?? 0)")
  527. switch avAssetExportSession?.status {
  528. case .unknown:
  529. DispatchQueue.main.async {
  530. resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
  531. }
  532. BFLog(message: "导出相册视频发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  533. case .waiting:
  534. BFLog(message: "等待导出mp4:\(filePath)")
  535. case .exporting:
  536. BFLog(message: "导出相册视频中...:\(filePath)")
  537. case .completed:
  538. DispatchQueue.main.async {
  539. resultHandler(aVAsset, filePath, nil)
  540. }
  541. BFLog(message: "导出相册视频完成:\(filePath)")
  542. case .failed:
  543. DispatchQueue.main.async {
  544. resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
  545. }
  546. BFLog(message: "导出相册视频失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  547. case .cancelled:
  548. DispatchQueue.main.async {
  549. resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
  550. }
  551. BFLog(message: "取消导出相册视频:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  552. default:
  553. break
  554. }
  555. })
  556. }
  557. }
  558. /// 获取视频资源的旋转角度
  559. /// - Parameter assert: <#assert description#>
  560. /// - Returns: <#description#>
  561. public class func videoRotationAngle(assert: AVAsset) -> Int {
  562. var rotationAngle: Int = 0
  563. let tracks = assert.tracks(withMediaType: .video)
  564. if tracks.count > 0 {
  565. let firstTrack = tracks.first
  566. let transform = firstTrack?.preferredTransform
  567. if transform?.a == 0, transform?.b == 1.0, transform?.c == -1.0, transform?.d == 0 {
  568. rotationAngle = 90
  569. } else if transform?.a == -1.0, transform?.b == 0, transform?.c == 0, transform?.d == -1.0 {
  570. rotationAngle = 180
  571. } else if transform?.a == 0, transform?.b == -1.0, transform?.c == 1.0, transform?.d == 0 {
  572. rotationAngle = 270
  573. } else if transform?.a == 1.0, transform?.b == 0, transform?.c == 0, transform?.d == 1.0 {
  574. rotationAngle = 0
  575. }
  576. }
  577. return rotationAngle
  578. }
  579. /// 裁剪背景音乐并导出
  580. /// - Parameters:
  581. /// - url: 原始地址
  582. /// - startTime: 开始时间
  583. /// - endTime: 结束时间
  584. /// - resultHandler: <#resultHandler description#>
  585. /// - Returns: <#description#>
  586. public class func cutAudioToLocal(url: String, startTime: Float, endTime: Float, resultHandler: @escaping (_ url: String, _ filePath: String?, _ startTime: Float, _ endTime: Float, _ errorMsg: String?) -> Void) {
  587. // 创建目录
  588. createDirectory(path: bgMusicDirectory)
  589. let filePath = bgMusicDirectory + url.md5 + ".mp3"
  590. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  591. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > 0 {
  592. DispatchQueue.main.async {
  593. resultHandler(url, filePath, startTime, endTime, nil)
  594. }
  595. } else {
  596. // 删除以创建地址
  597. try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
  598. let audioAsset = AVURLAsset(url: URL(string: url)!)
  599. audioAsset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
  600. let status = audioAsset.statusOfValue(forKey: "tracks", error: nil)
  601. switch status {
  602. case .loaded: // 加载完成
  603. // AVAssetExportPresetPassthrough /AVAssetExportPresetAppleM4A
  604. let exportSession = AVAssetExportSession(asset: audioAsset, presetName: AVAssetExportPresetHighestQuality)
  605. exportSession?.outputURL = URL(fileURLWithPath: filePath)
  606. exportSession?.outputFileType = .mp3
  607. exportSession?.timeRange = CMTimeRange(start: CMTime(seconds: Double(startTime), preferredTimescale: 1000), end: CMTime(seconds: Double(endTime), preferredTimescale: 1000))
  608. exportSession?.exportAsynchronously(completionHandler: {
  609. switch exportSession?.status {
  610. case .waiting:
  611. BFLog(message: "等待导出mp3:\(filePath)")
  612. case .exporting:
  613. BFLog(message: "导出中...:\(filePath)")
  614. case .completed:
  615. DispatchQueue.main.async {
  616. resultHandler(url, filePath, startTime, endTime, nil)
  617. }
  618. BFLog(message: "导出完成:\(filePath)")
  619. case .cancelled, .failed, .unknown:
  620. DispatchQueue.main.async {
  621. resultHandler(url, nil, startTime, endTime, exportSession?.error?.localizedDescription)
  622. }
  623. BFLog(message: "导出失败:\(filePath),\(exportSession?.error?.localizedDescription ?? "")")
  624. default:
  625. break
  626. }
  627. })
  628. case .loading:
  629. BFLog(message: "加载中...:\(url)")
  630. case .failed, .cancelled, .unknown:
  631. DispatchQueue.main.async {
  632. resultHandler(url, nil, startTime, endTime, "导出失败")
  633. }
  634. default:
  635. break
  636. }
  637. }
  638. }
  639. }
  640. /// 创建本地保存地址
  641. /// - Parameters:
  642. /// - sourceFilePath: <#sourceFilePath description#>
  643. /// - completeHandle: <#completeHandle description#>
  644. /// - Returns: <#description#>
  645. public class func createLocalFile(sourceFilePath: String, completeHandle: (_ isFileExists: Bool, _ isCreateSuccess: Bool, _ filePath: String) -> Void) {
  646. let cLocalPath = NSString(string: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("\(sourceFilePath.md5).mp4")
  647. if FileManager.default.fileExists(atPath: cLocalPath) {
  648. BFLog(message: "文件已经存在:\(cLocalPath)")
  649. completeHandle(true, false, cLocalPath)
  650. } else {
  651. let result = FileManager.default.createFile(atPath: cLocalPath, contents: nil, attributes: nil)
  652. BFLog(message: "文件创建:\(cLocalPath),\(result)")
  653. completeHandle(false, result, cLocalPath)
  654. }
  655. }
  656. /// 获取图库图片
  657. /// - Parameters:
  658. /// - asset: <#asset description#>
  659. /// - itemSize: <#itemSize description#>
  660. /// - resultHandler: <#resultHandler description#>
  661. /// - Returns: <#description#>
  662. public class func requestAssetImage(asset: PHAsset, itemSize: CGSize, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
  663. PHCachingImageManager().requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: imagesOptions, resultHandler: { image, info in
  664. BFLog(message: "info = \(info ?? [:])")
  665. if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0" {
  666. resultHandler(image, info)
  667. }
  668. })
  669. }
  670. /// 获取图库原图
  671. /// - Parameters:
  672. /// - asset: <#asset description#>
  673. /// - resultHandler: <#resultHandler description#>
  674. /// - Returns: <#description#>
  675. public class func requestAssetOringinImage(asset: PHAsset, resultHandler: @escaping (_ isGIF: Bool, _ data: Data?, UIImage?, [AnyHashable: Any]?) -> Void) {
  676. PHCachingImageManager().requestImageData(for: asset, options: singleImageOptions) { data, _, _, info in
  677. var image: UIImage?
  678. if data != nil {
  679. image = UIImage(data: data!)
  680. }
  681. if info?.keys.contains("PHImageFileUTIKey") ?? false, "\(info?["PHImageFileUTIKey"] ?? "")" == "com.compuserve.gif" {
  682. resultHandler(true, data, image, info)
  683. } else {
  684. resultHandler(false, data, image, info)
  685. }
  686. }
  687. }
  688. /// 获取gif帧跟时长
  689. /// - Parameters:
  690. /// - data: <#data description#>
  691. /// - isRenderingTemplate
  692. /// - resultHandler: <#resultHandler description#>
  693. /// - Returns: <#description#>
  694. public class func parasGIFImage(data: Data, isRenderingColor: UIColor? = nil, resultHandler: @escaping (_ data: Data, _ images: [UIImage]?, _ duration: Double?) -> Void) {
  695. let info: [String: Any] = [
  696. kCGImageSourceShouldCache as String: true,
  697. kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF,
  698. ]
  699. guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
  700. resultHandler(data, nil, nil)
  701. BFLog(message: "获取gifimageSource 失败")
  702. return
  703. }
  704. // 获取帧数
  705. let frameCount = CGImageSourceGetCount(imageSource)
  706. var gifDuration = 0.0
  707. var images = [UIImage]()
  708. for i in 0 ..< frameCount {
  709. // 取出索引对应的图片
  710. guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else {
  711. BFLog(message: "取出对应的图片失败")
  712. return
  713. }
  714. if frameCount == 1 {
  715. // 单帧
  716. gifDuration = .infinity
  717. } else {
  718. // 1.获取gif没帧的时间间隔
  719. // 获取到该帧图片的属性字典
  720. guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as? [String: Any] else {
  721. BFLog(message: "取出对应的图片属性失败")
  722. return
  723. }
  724. // 获取该帧图片中的GIF相关的属性字典
  725. guard let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] else {
  726. BFLog(message: "取出对应的图片属性失败")
  727. return
  728. }
  729. let defaultFrameDuration = 0.1
  730. // 获取该帧图片的播放时间
  731. let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
  732. // 如果通过kCGImagePropertyGIFUnclampedDelayTime没有获取到播放时长,就通过kCGImagePropertyGIFDelayTime来获取,两者的含义是相同的;
  733. let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
  734. let duration = unclampedDelayTime ?? delayTime
  735. guard let frameDuration = duration else {
  736. BFLog(message: "获取帧时间间隔失败")
  737. return
  738. }
  739. // 对于播放时间低于0.011s的,重新指定时长为0.100s;
  740. let gifFrameDuration = frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
  741. // 计算总时间
  742. gifDuration += gifFrameDuration
  743. // 2.图片
  744. var frameImage: UIImage? = UIImage(cgImage: imageRef, scale: 1.0, orientation: .up)
  745. if isRenderingColor != nil {
  746. frameImage = frameImage?.tintImage(color: isRenderingColor!, blendMode: .destinationIn)
  747. }
  748. if frameImage != nil {
  749. images.append(frameImage!)
  750. }
  751. }
  752. }
  753. resultHandler(data, images, gifDuration)
  754. }
  755. }