PQPHAssetVideoParaseUtil.swift 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  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 = .current
  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. }
  312. } else if avAsset is AVComposition {
  313. BFLog(message: "导出相册视频-是AVComposition = \(String(describing: avAsset))")
  314. let assetResources = PHAssetResource.assetResources(for: phAsset)
  315. var resource: PHAssetResource?
  316. for assetRes in assetResources {
  317. if assetRes.type == .video || assetRes.type == .pairedVideo {
  318. resource = assetRes
  319. }
  320. }
  321. if phAsset.mediaType == .video, resource != nil {
  322. let fileName = (resource?.originalFilename ?? "") + (resource?.assetLocalIdentifier ?? "") + (resource?.uniformTypeIdentifier ?? "")
  323. let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
  324. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  325. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
  326. DispatchQueue.main.async {
  327. resultHandler(phAsset, avAsset, filePath, nil)
  328. }
  329. } else {
  330. PHAssetResourceManager.default().writeData(for: resource!, toFile: URL(fileURLWithPath: filePath), options: nil) { error in
  331. DispatchQueue.main.async {
  332. resultHandler(phAsset, avAsset, error == nil ? filePath : nil, nil)
  333. }
  334. }
  335. }
  336. } else {
  337. DispatchQueue.main.async {
  338. resultHandler(phAsset, avAsset, nil, nil)
  339. }
  340. }
  341. } else {
  342. DispatchQueue.main.async {
  343. resultHandler(phAsset, avAsset, nil, nil)
  344. }
  345. }
  346. }
  347. }
  348. /// 导出相册视频
  349. /// - Parameters:
  350. /// - aVAsset: <#aVAsset description#>
  351. /// - isAdjustRotationAngle: <#isAdjustRotationAngle description#>
  352. /// - resultHandler: <#resultHandler description#>
  353. public class func exportAVAssetToMP4(aVAsset: AVURLAsset, isAdjustRotationAngle: Bool = true, resultHandler: @escaping (_ aVAsset: AVURLAsset?, _ filePath: String?, _ errorMsg: String?) -> Void) {
  354. currentExportSession?.cancelExport()
  355. BFLog(message: "开始导出相册视频:url = \(aVAsset.url.absoluteString)")
  356. // 创建目录
  357. createDirectory(path: photoLibraryDirectory)
  358. let fileName = aVAsset.url.absoluteString
  359. let filePath = photoLibraryDirectory + fileName.md5 + ".mp4"
  360. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  361. let fileSize = try! aVAsset.url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
  362. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > fileSize / 40 {
  363. DispatchQueue.main.async {
  364. resultHandler(aVAsset, filePath, nil)
  365. }
  366. } else {
  367. BFLog(message: "未导出视频过,开始导出:aVAsset = \(aVAsset)")
  368. // 删除以创建地址
  369. try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
  370. let avAssetExportSession = AVAssetExportSession(asset: aVAsset, presetName: AVAssetExportPreset1280x720)
  371. currentExportSession = avAssetExportSession
  372. avAssetExportSession?.outputURL = NSURL(fileURLWithPath: filePath) as URL
  373. avAssetExportSession?.shouldOptimizeForNetworkUse = false
  374. avAssetExportSession?.outputFileType = .mp4
  375. if isAdjustRotationAngle {
  376. let rotationAngle = PQPHAssetVideoParaseUtil.videoRotationAngle(assert: aVAsset)
  377. // mdf by ak 统一导出的视频为30FPS
  378. var centerTranslate: CGAffineTransform = CGAffineTransform(translationX: 0, y: 0)
  379. var mixedTransform: CGAffineTransform = CGAffineTransform()
  380. let videoComposition = AVMutableVideoComposition()
  381. videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
  382. let tracks = aVAsset.tracks(withMediaType: .video)
  383. let firstTrack = tracks.first
  384. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  385. mixedTransform = centerTranslate.rotated(by: 0)
  386. if rotationAngle == 90 {
  387. centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.height ?? 0, y: 0)
  388. mixedTransform = centerTranslate.rotated(by: .pi / 2)
  389. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  390. } else if rotationAngle == 180 {
  391. centerTranslate = CGAffineTransform(translationX: firstTrack?.naturalSize.width ?? 0, y: firstTrack?.naturalSize.height ?? 0)
  392. mixedTransform = centerTranslate.rotated(by: .pi)
  393. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.width ?? 0, height: firstTrack?.naturalSize.height ?? 0)
  394. } else if rotationAngle == 270 {
  395. centerTranslate = CGAffineTransform(translationX: 0, y: firstTrack?.naturalSize.width ?? 0)
  396. mixedTransform = centerTranslate.rotated(by: .pi / 2 * 3)
  397. videoComposition.renderSize = CGSize(width: firstTrack?.naturalSize.height ?? 0, height: firstTrack?.naturalSize.width ?? 0)
  398. }
  399. let roateInstruction = AVMutableVideoCompositionInstruction()
  400. roateInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: aVAsset.duration)
  401. let layRoateInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
  402. layRoateInstruction.setTransform(mixedTransform, at: CMTime.zero)
  403. roateInstruction.layerInstructions = [layRoateInstruction]
  404. videoComposition.instructions = [roateInstruction]
  405. avAssetExportSession?.videoComposition = videoComposition
  406. }
  407. avAssetExportSession?.shouldOptimizeForNetworkUse = true
  408. avAssetExportSession?.exportAsynchronously(completionHandler: {
  409. BFLog(message: "导出相册视频progress = \(avAssetExportSession?.progress ?? 0)")
  410. switch avAssetExportSession?.status {
  411. case .unknown:
  412. DispatchQueue.main.async {
  413. resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
  414. }
  415. BFLog(message: "导出相册视频发生未知错误:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  416. case .waiting:
  417. BFLog(message: "等待导出mp4:\(filePath)")
  418. case .exporting:
  419. BFLog(message: "导出相册视频中...:\(filePath)")
  420. case .completed:
  421. DispatchQueue.main.async {
  422. resultHandler(aVAsset, filePath, nil)
  423. }
  424. BFLog(message: "导出相册视频完成:\(filePath)")
  425. case .failed:
  426. DispatchQueue.main.async {
  427. resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
  428. }
  429. BFLog(message: "导出相册视频失败:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  430. case .cancelled:
  431. DispatchQueue.main.async {
  432. resultHandler(aVAsset, nil, avAssetExportSession?.error?.localizedDescription)
  433. }
  434. BFLog(message: "取消导出相册视频:\(filePath),\(avAssetExportSession?.error?.localizedDescription ?? "")")
  435. default:
  436. break
  437. }
  438. })
  439. }
  440. }
  441. /// 获取视频资源的旋转角度
  442. /// - Parameter assert: <#assert description#>
  443. /// - Returns: <#description#>
  444. public class func videoRotationAngle(assert: AVAsset) -> Int {
  445. var rotationAngle: Int = 0
  446. let tracks = assert.tracks(withMediaType: .video)
  447. if tracks.count > 0 {
  448. let firstTrack = tracks.first
  449. let transform = firstTrack?.preferredTransform
  450. if transform?.a == 0, transform?.b == 1.0, transform?.c == -1.0, transform?.d == 0 {
  451. rotationAngle = 90
  452. } else if transform?.a == -1.0, transform?.b == 0, transform?.c == 0, transform?.d == -1.0 {
  453. rotationAngle = 180
  454. } else if transform?.a == 0, transform?.b == -1.0, transform?.c == 1.0, transform?.d == 0 {
  455. rotationAngle = 270
  456. } else if transform?.a == 1.0, transform?.b == 0, transform?.c == 0, transform?.d == 1.0 {
  457. rotationAngle = 0
  458. }
  459. }
  460. return rotationAngle
  461. }
  462. /// 裁剪背景音乐并导出
  463. /// - Parameters:
  464. /// - url: 原始地址
  465. /// - startTime: 开始时间
  466. /// - endTime: 结束时间
  467. /// - resultHandler: <#resultHandler description#>
  468. /// - Returns: <#description#>
  469. public class func cutAudioToLocal(url: String, startTime: Float, endTime: Float, resultHandler: @escaping (_ url: String, _ filePath: String?, _ startTime: Float, _ endTime: Float, _ errorMsg: String?) -> Void) {
  470. // 创建目录
  471. createDirectory(path: bgMusicDirectory)
  472. let filePath = bgMusicDirectory + url.md5 + ".mp3"
  473. let data = try? Data(contentsOf: NSURL.fileURL(withPath: filePath))
  474. if FileManager.default.fileExists(atPath: filePath) && (data?.count ?? 0) > 0 {
  475. DispatchQueue.main.async {
  476. resultHandler(url, filePath, startTime, endTime, nil)
  477. }
  478. } else {
  479. // 删除以创建地址
  480. try? FileManager.default.removeItem(at: NSURL.fileURL(withPath: filePath))
  481. let audioAsset = AVURLAsset(url: URL(string: url)!)
  482. audioAsset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
  483. let status = audioAsset.statusOfValue(forKey: "tracks", error: nil)
  484. switch status {
  485. case .loaded: // 加载完成
  486. // AVAssetExportPresetPassthrough /AVAssetExportPresetAppleM4A
  487. let exportSession = AVAssetExportSession(asset: audioAsset, presetName: AVAssetExportPresetHighestQuality)
  488. exportSession?.outputURL = URL(fileURLWithPath: filePath)
  489. exportSession?.outputFileType = .mp3
  490. exportSession?.timeRange = CMTimeRange(start: CMTime(seconds: Double(startTime), preferredTimescale: 1000), end: CMTime(seconds: Double(endTime), preferredTimescale: 1000))
  491. exportSession?.exportAsynchronously(completionHandler: {
  492. switch exportSession?.status {
  493. case .waiting:
  494. BFLog(message: "等待导出mp3:\(filePath)")
  495. case .exporting:
  496. BFLog(message: "导出中...:\(filePath)")
  497. case .completed:
  498. DispatchQueue.main.async {
  499. resultHandler(url, filePath, startTime, endTime, nil)
  500. }
  501. BFLog(message: "导出完成:\(filePath)")
  502. case .cancelled, .failed, .unknown:
  503. DispatchQueue.main.async {
  504. resultHandler(url, nil, startTime, endTime, exportSession?.error?.localizedDescription)
  505. }
  506. BFLog(message: "导出失败:\(filePath),\(exportSession?.error?.localizedDescription ?? "")")
  507. default:
  508. break
  509. }
  510. })
  511. case .loading:
  512. BFLog(message: "加载中...:\(url)")
  513. case .failed, .cancelled, .unknown:
  514. DispatchQueue.main.async {
  515. resultHandler(url, nil, startTime, endTime, "导出失败")
  516. }
  517. default:
  518. break
  519. }
  520. }
  521. }
  522. }
  523. /// 创建本地保存地址
  524. /// - Parameters:
  525. /// - sourceFilePath: <#sourceFilePath description#>
  526. /// - completeHandle: <#completeHandle description#>
  527. /// - Returns: <#description#>
  528. public class func createLocalFile(sourceFilePath: String, completeHandle: (_ isFileExists: Bool, _ isCreateSuccess: Bool, _ filePath: String) -> Void) {
  529. let cLocalPath = NSString(string: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("\(sourceFilePath.md5).mp4")
  530. if FileManager.default.fileExists(atPath: cLocalPath) {
  531. BFLog(message: "文件已经存在:\(cLocalPath)")
  532. completeHandle(true, false, cLocalPath)
  533. } else {
  534. let result = FileManager.default.createFile(atPath: cLocalPath, contents: nil, attributes: nil)
  535. BFLog(message: "文件创建:\(cLocalPath),\(result)")
  536. completeHandle(false, result, cLocalPath)
  537. }
  538. }
  539. /// 获取图库图片
  540. /// - Parameters:
  541. /// - asset: <#asset description#>
  542. /// - itemSize: <#itemSize description#>
  543. /// - resultHandler: <#resultHandler description#>
  544. /// - Returns: <#description#>
  545. public class func requestAssetImage(asset: PHAsset, itemSize: CGSize, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
  546. PHCachingImageManager().requestImage(for: asset, targetSize: itemSize, contentMode: .aspectFill, options: imagesOptions, resultHandler: { image, info in
  547. BFLog(message: "info = \(info ?? [:])")
  548. if info?.keys.contains("PHImageResultIsDegradedKey") ?? false, "\(info?["PHImageResultIsDegradedKey"] ?? "0")" == "0" {
  549. resultHandler(image, info)
  550. }
  551. })
  552. }
  553. /// 获取图库原图
  554. /// - Parameters:
  555. /// - asset: <#asset description#>
  556. /// - resultHandler: <#resultHandler description#>
  557. /// - Returns: <#description#>
  558. public class func requestAssetOringinImage(asset: PHAsset, resultHandler: @escaping (_ isGIF: Bool, _ data: Data?, UIImage?, [AnyHashable: Any]?) -> Void) {
  559. PHCachingImageManager().requestImageData(for: asset, options: singleImageOptions) { data, _, _, info in
  560. var image: UIImage?
  561. if data != nil {
  562. image = UIImage(data: data!)
  563. }
  564. if info?.keys.contains("PHImageFileUTIKey") ?? false, "\(info?["PHImageFileUTIKey"] ?? "")" == "com.compuserve.gif" {
  565. resultHandler(true, data, image, info)
  566. } else {
  567. resultHandler(false, data, image, info)
  568. }
  569. }
  570. }
  571. /// 获取gif帧跟时长
  572. /// - Parameters:
  573. /// - data: <#data description#>
  574. /// - isRenderingTemplate
  575. /// - resultHandler: <#resultHandler description#>
  576. /// - Returns: <#description#>
  577. public class func parasGIFImage(data: Data, isRenderingColor: UIColor? = nil, resultHandler: @escaping (_ data: Data, _ images: [UIImage]?, _ duration: Double?) -> Void) {
  578. let info: [String: Any] = [
  579. kCGImageSourceShouldCache as String: true,
  580. kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF,
  581. ]
  582. guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
  583. resultHandler(data, nil, nil)
  584. BFLog(message: "获取gifimageSource 失败")
  585. return
  586. }
  587. // 获取帧数
  588. let frameCount = CGImageSourceGetCount(imageSource)
  589. var gifDuration = 0.0
  590. var images = [UIImage]()
  591. for i in 0..<frameCount {
  592. // 取出索引对应的图片
  593. guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else {
  594. BFLog(message: "取出对应的图片失败")
  595. return
  596. }
  597. if frameCount == 1 {
  598. // 单帧
  599. gifDuration = .infinity
  600. } else {
  601. // 1.获取gif没帧的时间间隔
  602. // 获取到该帧图片的属性字典
  603. guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as? [String: Any] else {
  604. BFLog(message: "取出对应的图片属性失败")
  605. return
  606. }
  607. // 获取该帧图片中的GIF相关的属性字典
  608. guard let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] else {
  609. BFLog(message: "取出对应的图片属性失败")
  610. return
  611. }
  612. let defaultFrameDuration = 0.1
  613. // 获取该帧图片的播放时间
  614. let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
  615. // 如果通过kCGImagePropertyGIFUnclampedDelayTime没有获取到播放时长,就通过kCGImagePropertyGIFDelayTime来获取,两者的含义是相同的;
  616. let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
  617. let duration = unclampedDelayTime ?? delayTime
  618. guard let frameDuration = duration else {
  619. BFLog(message: "获取帧时间间隔失败")
  620. return
  621. }
  622. // 对于播放时间低于0.011s的,重新指定时长为0.100s;
  623. let gifFrameDuration = frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
  624. // 计算总时间
  625. gifDuration += gifFrameDuration
  626. // 2.图片
  627. var frameImage: UIImage? = UIImage(cgImage: imageRef, scale: 1.0, orientation: .up)
  628. if isRenderingColor != nil, frameImage != nil {
  629. frameImage = tintImage(image: frameImage!, color: isRenderingColor!, blendMode: .destinationIn)
  630. }
  631. if frameImage != nil {
  632. images.append(frameImage!)
  633. }
  634. }
  635. }
  636. resultHandler(data, images, gifDuration)
  637. }
  638. /// 改变图片主题颜色
  639. /// - Parameters:
  640. /// - color: <#color description#>
  641. /// - blendMode: <#blendMode description#>
  642. /// - Returns: <#description#>
  643. public class func tintImage(image: UIImage, color: UIColor, blendMode: CGBlendMode) -> UIImage? {
  644. let rect = CGRect(origin: CGPoint.zero, size: image.size)
  645. UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
  646. color.setFill()
  647. UIRectFill(rect)
  648. image.draw(in: rect, blendMode: blendMode, alpha: 1.0)
  649. let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
  650. UIGraphicsEndImageContext()
  651. return tintedImage
  652. }
  653. }