|
@@ -0,0 +1,322 @@
|
|
|
+//
|
|
|
+// NXLAssetsCollection.swift
|
|
|
+// RainbowPlanet
|
|
|
+//
|
|
|
+// Created by 南鑫林 on 2019/11/19.
|
|
|
+// Copyright © 2019 RainbowPlanet. All rights reserved.
|
|
|
+//
|
|
|
+
|
|
|
+import UIKit
|
|
|
+import Photos
|
|
|
+import PhotosUI
|
|
|
+import MobileCoreServices
|
|
|
+
|
|
|
+public struct NXLPHAsset {
|
|
|
+
|
|
|
+ /// 云端下载状态
|
|
|
+ enum CloudDownloadState {
|
|
|
+ case ready, progress, complete, failed
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 资源类型
|
|
|
+ public enum AssetType {
|
|
|
+ case photo, video, livePhoto
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 图片后缀
|
|
|
+ public enum ImageExtType: String {
|
|
|
+ case png, jpg, gif, heic
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /// 云端下载状态
|
|
|
+ var state = CloudDownloadState.ready
|
|
|
+
|
|
|
+ /// 资源
|
|
|
+ var phAsset: PHAsset? = nil
|
|
|
+
|
|
|
+ /// 是否选中相机
|
|
|
+ var isSelectedFromCamera = false
|
|
|
+
|
|
|
+ /// 选中的订单
|
|
|
+ var selectedOrder: Int = 0
|
|
|
+
|
|
|
+ /// 资源类型
|
|
|
+ var type: AssetType {
|
|
|
+ get {
|
|
|
+ guard let phAsset = self.phAsset else { return .photo }
|
|
|
+ if phAsset.mediaSubtypes.contains(.photoLive) {
|
|
|
+ return .livePhoto
|
|
|
+ }else if phAsset.mediaType == .video {
|
|
|
+ return .video
|
|
|
+ }else {
|
|
|
+ return .photo
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ init(asset: PHAsset?) {
|
|
|
+ phAsset = asset
|
|
|
+ }
|
|
|
+
|
|
|
+ static func asset(with localIdentifier: String) -> NXLPHAsset? {
|
|
|
+ let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
|
|
|
+ return NXLPHAsset(asset: fetchResult.firstObject)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 全分辨率图像
|
|
|
+ var fullResolutionImage: UIImage? {
|
|
|
+ get {
|
|
|
+ guard let phAsset = self.phAsset else { return nil }
|
|
|
+ return NXLPhotoLibrary.fullResolutionImageData(asset: phAsset)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 原始文件名
|
|
|
+ var originalFileName: String? {
|
|
|
+ get {
|
|
|
+ guard let phAsset = self.phAsset,let resource = PHAssetResource.assetResources(for: phAsset).first else { return nil }
|
|
|
+ return resource.originalFilename
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 图片后缀类型
|
|
|
+ func extType() -> ImageExtType {
|
|
|
+ var ext = ImageExtType.png
|
|
|
+ if let fileName = self.originalFileName, let extention = URL(string: fileName)?.pathExtension.lowercased() {
|
|
|
+ ext = ImageExtType(rawValue: extention) ?? .png
|
|
|
+ }
|
|
|
+ return ext
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 图片size
|
|
|
+ /// - Parameters:
|
|
|
+ /// - options: 图像请求选项
|
|
|
+ /// - completion: 完成回调
|
|
|
+ /// - livePhotoVideoSize: 是否livePhotoView
|
|
|
+ func photoSize(options: PHImageRequestOptions? = nil ,completion: @escaping ((Int)->Void), livePhotoVideoSize: Bool = false) {
|
|
|
+ guard let phAsset = self.phAsset, self.type == .photo || self.type == .livePhoto else { completion(-1); return }
|
|
|
+ var resource: PHAssetResource? = nil
|
|
|
+ if phAsset.mediaSubtypes.contains(.photoLive) == true, livePhotoVideoSize {
|
|
|
+ resource = PHAssetResource.assetResources(for: phAsset).filter { $0.type == .pairedVideo }.first
|
|
|
+ }else {
|
|
|
+ resource = PHAssetResource.assetResources(for: phAsset).filter { $0.type == .photo }.first
|
|
|
+ }
|
|
|
+ if let fileSize = resource?.value(forKey: "fileSize") as? Int {
|
|
|
+ completion(fileSize)
|
|
|
+ }else {
|
|
|
+ PHImageManager.default().requestImageData(for: phAsset, options: nil) { (data, uti, orientation, info) in
|
|
|
+ var fileSize = -1
|
|
|
+ if let data = data {
|
|
|
+ let bcf = ByteCountFormatter()
|
|
|
+ bcf.countStyle = .file
|
|
|
+ fileSize = data.count
|
|
|
+ }
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion(fileSize)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /// 视频size
|
|
|
+ /// - Parameters:
|
|
|
+ /// - options: 视频请求选项
|
|
|
+ /// - completion: 完成回调
|
|
|
+ public func videoSize(options: PHVideoRequestOptions? = nil, completion: @escaping ((Int)->Void)) {
|
|
|
+ guard let phAsset = self.phAsset, self.type == .video else { completion(-1); return }
|
|
|
+ let resource = PHAssetResource.assetResources(for: phAsset).filter { $0.type == .video }.first
|
|
|
+ if let fileSize = resource?.value(forKey: "fileSize") as? Int {
|
|
|
+ completion(fileSize)
|
|
|
+ }else {
|
|
|
+ PHImageManager.default().requestAVAsset(forVideo: phAsset, options: options) { (avasset, audioMix, info) in
|
|
|
+ func fileSize(_ url: URL?) -> Int? {
|
|
|
+ do {
|
|
|
+ guard let fileSize = try url?.resourceValues(forKeys: [.fileSizeKey]).fileSize else { return nil }
|
|
|
+ return fileSize
|
|
|
+ }catch { return nil }
|
|
|
+ }
|
|
|
+ var url: URL? = nil
|
|
|
+ if let urlAsset = avasset as? AVURLAsset {
|
|
|
+ url = urlAsset.url
|
|
|
+ }else if let sandboxKeys = info?["PHImageFileSandboxExtensionTokenKey"] as? String, let path = sandboxKeys.components(separatedBy: ";").last {
|
|
|
+ url = URL(fileURLWithPath: path)
|
|
|
+ }
|
|
|
+ let size = fileSize(url) ?? -1
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion(size)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// MIMEType
|
|
|
+ /// - Parameter url: URL
|
|
|
+ func MIMEType(_ url: URL?) -> String? {
|
|
|
+ guard let ext = url?.pathExtension else { return nil }
|
|
|
+ if !ext.isEmpty {
|
|
|
+ let UTIRef = UTTypeCreatePreferredIdentifierForTag("public.filename-extension" as CFString, ext as CFString, nil)
|
|
|
+ let UTI = UTIRef?.takeUnretainedValue()
|
|
|
+ UTIRef?.release()
|
|
|
+ if let UTI = UTI {
|
|
|
+ guard let MIMETypeRef = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType) else { return nil }
|
|
|
+ let MIMEType = MIMETypeRef.takeUnretainedValue()
|
|
|
+ MIMETypeRef.release()
|
|
|
+ return MIMEType as String
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /// 临时复制媒体文件
|
|
|
+ /// - Parameters:
|
|
|
+ /// - videoRequestOptions: 视频请求选项
|
|
|
+ /// - imageRequestOptions: 图片请求选项
|
|
|
+ /// - exportPreset: 出口预设
|
|
|
+ /// - convertLivePhotosToJPG:将实时照片转换为JPG /false : If you want mov file at live photos / true : If you want png file at live photos ( HEIC )
|
|
|
+ /// - progressBlock: 进度回调
|
|
|
+ /// - completionBlock: 完成回调
|
|
|
+ @discardableResult
|
|
|
+ public func tempCopyMediaFile(videoRequestOptions: PHVideoRequestOptions? = nil, imageRequestOptions: PHImageRequestOptions? = nil, exportPreset: String = AVAssetExportPresetHighestQuality, convertLivePhotosToJPG: Bool = false, progressBlock:((Double) -> Void)? = nil, completionBlock:@escaping ((URL,String) -> Void)) -> PHImageRequestID? {
|
|
|
+ guard let phAsset = self.phAsset else { return nil }
|
|
|
+ var type: PHAssetResourceType? = nil
|
|
|
+ if phAsset.mediaSubtypes.contains(.photoLive) == true, convertLivePhotosToJPG == false {
|
|
|
+ type = .pairedVideo
|
|
|
+ }else {
|
|
|
+ type = phAsset.mediaType == .video ? .video : .photo
|
|
|
+ }
|
|
|
+ guard let resource = (PHAssetResource.assetResources(for: phAsset).filter{ $0.type == type }).first else { return nil }
|
|
|
+ let fileName = resource.originalFilename
|
|
|
+ var writeURL: URL? = nil
|
|
|
+ if #available(iOS 10.0, *) {
|
|
|
+ writeURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(fileName)")
|
|
|
+ } else {
|
|
|
+ writeURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("\(fileName)")
|
|
|
+ }
|
|
|
+ if (writeURL?.pathExtension.uppercased() == "HEIC" || writeURL?.pathExtension.uppercased() == "HEIF") && convertLivePhotosToJPG {
|
|
|
+ if let fileName2 = writeURL?.deletingPathExtension().lastPathComponent {
|
|
|
+ writeURL?.deleteLastPathComponent()
|
|
|
+ writeURL?.appendPathComponent("\(fileName2).jpg")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ guard let localURL = writeURL,let mimetype = MIMEType(writeURL) else { return nil }
|
|
|
+ switch phAsset.mediaType {
|
|
|
+ case .video:
|
|
|
+ var requestOptions = PHVideoRequestOptions()
|
|
|
+ if let options = videoRequestOptions {
|
|
|
+ requestOptions = options
|
|
|
+ }else {
|
|
|
+ requestOptions.isNetworkAccessAllowed = true
|
|
|
+ }
|
|
|
+ //iCloud download progress
|
|
|
+ requestOptions.progressHandler = { (progress, error, stop, info) in
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ progressBlock?(progress)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: exportPreset) { (session, infoDict) in
|
|
|
+ session?.outputURL = localURL
|
|
|
+ session?.outputFileType = AVFileType.mov
|
|
|
+ session?.exportAsynchronously(completionHandler: {
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completionBlock(localURL, mimetype)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ case .image:
|
|
|
+ var requestOptions = PHImageRequestOptions()
|
|
|
+ if let options = imageRequestOptions {
|
|
|
+ requestOptions = options
|
|
|
+ }else {
|
|
|
+ requestOptions.isNetworkAccessAllowed = true
|
|
|
+ }
|
|
|
+ //iCloud download progress
|
|
|
+ requestOptions.progressHandler = { (progress, error, stop, info) in
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ progressBlock?(progress)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return PHImageManager.default().requestImageData(for: phAsset, options: requestOptions, resultHandler: { (data, uti, orientation, info) in
|
|
|
+ do {
|
|
|
+ var data = data
|
|
|
+ let needConvertLivePhotoToJPG = phAsset.mediaSubtypes.contains(.photoLive) == true && convertLivePhotosToJPG == true
|
|
|
+ if needConvertLivePhotoToJPG, let imgData = data, let rawImage = UIImage(data: imgData)?.upOrientationImage() {
|
|
|
+ data = rawImage.jpegData(compressionQuality: 1)
|
|
|
+ }
|
|
|
+ try data?.write(to: localURL)
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completionBlock(localURL, mimetype)
|
|
|
+ }
|
|
|
+ }catch { }
|
|
|
+ })
|
|
|
+ default:
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 视频文件名
|
|
|
+ /// - Parameter phAsset: 资源
|
|
|
+ func videoFilename(phAsset: PHAsset) -> URL? {
|
|
|
+ guard let resource = (PHAssetResource.assetResources(for: phAsset).filter{ $0.type == .video }).first else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ var writeURL: URL?
|
|
|
+ let fileName = resource.originalFilename
|
|
|
+ if #available(iOS 10.0, *) {
|
|
|
+ writeURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(fileName)")
|
|
|
+ } else {
|
|
|
+ writeURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("\(fileName)")
|
|
|
+ }
|
|
|
+ return writeURL
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 导出视频文件
|
|
|
+ /// - Parameters:
|
|
|
+ /// - options: 视频请求选项
|
|
|
+ /// - outputURL: 输出网址
|
|
|
+ /// - outputFileType: 输出文件类型
|
|
|
+ /// - progressBlock: 进度回调
|
|
|
+ /// - completionBlock: 完成回调
|
|
|
+ func exportVideoFile(options: PHVideoRequestOptions? = nil, outputURL: URL? = nil, outputFileType: AVFileType = .mov, progressBlock:((Double) -> Void)? = nil, completionBlock:@escaping ((URL,String) -> Void)) {
|
|
|
+ guard
|
|
|
+ let phAsset = self.phAsset,
|
|
|
+ phAsset.mediaType == .video,
|
|
|
+ let writeURL = outputURL ?? videoFilename(phAsset: phAsset),
|
|
|
+ let mimetype = MIMEType(writeURL)
|
|
|
+ else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var requestOptions = PHVideoRequestOptions()
|
|
|
+ if let options = options {
|
|
|
+ requestOptions = options
|
|
|
+ }else {
|
|
|
+ requestOptions.isNetworkAccessAllowed = true
|
|
|
+ }
|
|
|
+ requestOptions.progressHandler = { (progress, error, stop, info) in
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ progressBlock?(progress)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ PHImageManager.default().requestAVAsset(forVideo: phAsset, options: requestOptions) { (avasset, avaudioMix, infoDict) in
|
|
|
+ guard let avasset = avasset else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let exportSession = AVAssetExportSession.init(asset: avasset, presetName: AVAssetExportPresetHighestQuality)
|
|
|
+ exportSession?.outputURL = writeURL
|
|
|
+ exportSession?.outputFileType = outputFileType
|
|
|
+ exportSession?.exportAsynchronously(completionHandler: {
|
|
|
+ completionBlock(writeURL, mimetype)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+extension NXLPHAsset: Equatable {
|
|
|
+ public static func ==(lhs: NXLPHAsset, rhs: NXLPHAsset) -> Bool {
|
|
|
+ guard let lphAsset = lhs.phAsset, let rphAsset = rhs.phAsset else { return false }
|
|
|
+ return lphAsset.localIdentifier == rphAsset.localIdentifier
|
|
|
+ }
|
|
|
+}
|