|
@@ -26,24 +26,29 @@
|
|
|
|
|
|
|
|
#if !os(watchOS)
|
|
#if !os(watchOS)
|
|
|
@preconcurrency import Photos
|
|
@preconcurrency import Photos
|
|
|
|
|
+import CoreServices
|
|
|
|
|
+
|
|
|
|
|
+var audioReader: AVAssetReader?
|
|
|
|
|
+var videoReader: AVAssetReader?
|
|
|
|
|
+var assetWriter: AVAssetWriter?
|
|
|
|
|
|
|
|
/// A structure that contains information about the result of loading a live photo.
|
|
/// A structure that contains information about the result of loading a live photo.
|
|
|
public struct LivePhotoLoadingInfoResult: Sendable {
|
|
public struct LivePhotoLoadingInfoResult: Sendable {
|
|
|
|
|
|
|
|
/// Retrieves the live photo disk URLs from this result.
|
|
/// Retrieves the live photo disk URLs from this result.
|
|
|
public let fileURLs: [URL]
|
|
public let fileURLs: [URL]
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Retrieves the cache source of the image, indicating from which cache layer it was retrieved.
|
|
/// Retrieves the cache source of the image, indicating from which cache layer it was retrieved.
|
|
|
///
|
|
///
|
|
|
/// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned.
|
|
/// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned.
|
|
|
/// Otherwise, ``CacheType/disk`` will be returned for the live photo. ``CacheType/memory`` is not available for
|
|
/// Otherwise, ``CacheType/disk`` will be returned for the live photo. ``CacheType/memory`` is not available for
|
|
|
/// live photos since it may take too much memory. All cached live photos are loaded from disk only.
|
|
/// live photos since it may take too much memory. All cached live photos are loaded from disk only.
|
|
|
public let cacheType: CacheType
|
|
public let cacheType: CacheType
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// The ``LivePhotoSource`` to which this result is related. This indicates where the `livePhoto` referenced by
|
|
/// The ``LivePhotoSource`` to which this result is related. This indicates where the `livePhoto` referenced by
|
|
|
/// `self` is located.
|
|
/// `self` is located.
|
|
|
public let source: LivePhotoSource
|
|
public let source: LivePhotoSource
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// The original ``LivePhotoSource`` from which the retrieval task begins. It may differ from the ``source`` property.
|
|
/// The original ``LivePhotoSource`` from which the retrieval task begins. It may differ from the ``source`` property.
|
|
|
/// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the
|
|
/// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the
|
|
|
/// ``originalSource`` will retain the initial ``source`` that initiated the image loading process.
|
|
/// ``originalSource`` will retain the initial ``source`` that initiated the image loading process.
|
|
@@ -61,7 +66,7 @@ public struct LivePhotoLoadingInfoResult: Sendable {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
extension KingfisherManager {
|
|
extension KingfisherManager {
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Retrieves a live photo from the specified source.
|
|
/// Retrieves a live photo from the specified source.
|
|
|
///
|
|
///
|
|
|
/// This method asynchronously loads a live photo from the given source, applying the specified options and
|
|
/// This method asynchronously loads a live photo from the given source, applying the specified options and
|
|
@@ -81,7 +86,7 @@ extension KingfisherManager {
|
|
|
///
|
|
///
|
|
|
/// - Note: This method uses `LivePhotoImageProcessor` by default. Custom processors are not supported for live photos.
|
|
/// - Note: This method uses `LivePhotoImageProcessor` by default. Custom processors are not supported for live photos.
|
|
|
///
|
|
///
|
|
|
- /// - Warning: Not all options are working for this method. And currently the `progressBlock` is not working.
|
|
|
|
|
|
|
+ /// - Warning: Not all options are working for this method. And currently the `progressBlock` is not working.
|
|
|
/// It will be implemented in the future.
|
|
/// It will be implemented in the future.
|
|
|
public func retrieveLivePhoto(
|
|
public func retrieveLivePhoto(
|
|
|
with source: LivePhotoSource,
|
|
with source: LivePhotoSource,
|
|
@@ -114,13 +119,44 @@ extension KingfisherManager {
|
|
|
|
|
|
|
|
let targetCache = checkedOptions.targetCache ?? cache
|
|
let targetCache = checkedOptions.targetCache ?? cache
|
|
|
var fileURLs = [URL]()
|
|
var fileURLs = [URL]()
|
|
|
|
|
+
|
|
|
|
|
+ let uuid = UUID().uuidString
|
|
|
|
|
+
|
|
|
for resource in source.resources {
|
|
for resource in source.resources {
|
|
|
let url = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: checkedOptions)
|
|
let url = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: checkedOptions)
|
|
|
guard let url else {
|
|
guard let url else {
|
|
|
// This should not happen normally if the previous `downloadAndCache` done without issue, but in case.
|
|
// This should not happen normally if the previous `downloadAndCache` done without issue, but in case.
|
|
|
throw KingfisherError.cacheError(reason: .missingLivePhotoResourceOnDisk(resource))
|
|
throw KingfisherError.cacheError(reason: .missingLivePhotoResourceOnDisk(resource))
|
|
|
}
|
|
}
|
|
|
- fileURLs.append(url)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ await measure("Running \(resource.referenceFileType)") { f in
|
|
|
|
|
+ if resource.referenceFileType == .mov {
|
|
|
|
|
+
|
|
|
|
|
+ let desS = url.absoluteString.replacingOccurrences(of: ".mov", with: "-.mov")
|
|
|
|
|
+ let desURL = URL(string: desS)!
|
|
|
|
|
+
|
|
|
|
|
+ print("From url: \(url)")
|
|
|
|
|
+ print("To url: \(desURL)")
|
|
|
|
|
+
|
|
|
|
|
+ await withCheckedContinuation { con in
|
|
|
|
|
+ addAssetID(uuid, toVideo: url, saveTo: desURL, progress: {_ in}, completion: {
|
|
|
|
|
+ print("Writing OK: \($0)")
|
|
|
|
|
+ fileURLs.append(desURL)
|
|
|
|
|
+ con.resume()
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ f()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let desS = url.absoluteString.replacingOccurrences(of: ".png", with: "-.png")
|
|
|
|
|
+ let desURL = URL(string: desS)!
|
|
|
|
|
+
|
|
|
|
|
+ addAssetID(uuid, toImage: url, saveTo: desURL)
|
|
|
|
|
+ fileURLs.append(desURL)
|
|
|
|
|
+ f()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return LivePhotoLoadingInfoResult(
|
|
return LivePhotoLoadingInfoResult(
|
|
@@ -133,6 +169,17 @@ extension KingfisherManager {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ func measure(_ title: String, block: (@escaping () -> ()) async -> ()) async {
|
|
|
|
|
+
|
|
|
|
|
+ let startTime = CFAbsoluteTimeGetCurrent()
|
|
|
|
|
+
|
|
|
|
|
+ await block {
|
|
|
|
|
+ let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
|
|
|
|
|
+ print("\(title):: Time: \(timeElapsed)")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
// Returns the missing resources for the given source and options. If the resource is not in the cache, it will be
|
|
// Returns the missing resources for the given source and options. If the resource is not in the cache, it will be
|
|
|
// returned as a missing resource.
|
|
// returned as a missing resource.
|
|
|
func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [LivePhotoResource] {
|
|
func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [LivePhotoResource] {
|
|
@@ -155,7 +202,7 @@ extension KingfisherManager {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Download the resources and store them to the cache.
|
|
// Download the resources and store them to the cache.
|
|
|
- // If the resource does not specify a file extension (from either the URL extension or the explicit
|
|
|
|
|
|
|
+ // If the resource does not specify a file extension (from either the URL extension or the explicit
|
|
|
// `referenceFileType`), we infer it from the file signature.
|
|
// `referenceFileType`), we infer it from the file signature.
|
|
|
func downloadAndCache(
|
|
func downloadAndCache(
|
|
|
resources: [LivePhotoResource],
|
|
resources: [LivePhotoResource],
|
|
@@ -166,9 +213,9 @@ extension KingfisherManager {
|
|
|
}
|
|
}
|
|
|
let downloader = options.downloader ?? downloader
|
|
let downloader = options.downloader ?? downloader
|
|
|
let cache = options.targetCache ?? cache
|
|
let cache = options.targetCache ?? cache
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Download all resources concurrently.
|
|
// Download all resources concurrently.
|
|
|
- return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) {
|
|
|
|
|
|
|
+ return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) {
|
|
|
group in
|
|
group in
|
|
|
|
|
|
|
|
for resource in resources {
|
|
for resource in resources {
|
|
@@ -188,7 +235,7 @@ extension KingfisherManager {
|
|
|
url: provider.contentURL
|
|
url: provider.contentURL
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// We need to specify the extension so the file is saved correctly. Live photo loading requires
|
|
// We need to specify the extension so the file is saved correctly. Live photo loading requires
|
|
|
// the file extension to be correct. Otherwise, a 3302 error will be thrown.
|
|
// the file extension to be correct. Otherwise, a 3302 error will be thrown.
|
|
|
// https://developer.apple.com/documentation/photokit/phphotoserror/code/invalidresource
|
|
// https://developer.apple.com/documentation/photokit/phphotoserror/code/invalidresource
|
|
@@ -201,6 +248,7 @@ extension KingfisherManager {
|
|
|
forcedExtension: fileExtension,
|
|
forcedExtension: fileExtension,
|
|
|
expiration: options.diskCacheExpiration
|
|
expiration: options.diskCacheExpiration
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
return downloadedResource
|
|
return downloadedResource
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -212,6 +260,233 @@ extension KingfisherManager {
|
|
|
return result
|
|
return result
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private func metadataForAssetID(_ assetIdentifier: String) -> AVMetadataItem {
|
|
|
|
|
+ let item = AVMutableMetadataItem()
|
|
|
|
|
+ let keyContentIdentifier = "com.apple.quicktime.content.identifier"
|
|
|
|
|
+ let keySpaceQuickTimeMetadata = "mdta"
|
|
|
|
|
+ item.key = keyContentIdentifier as (NSCopying & NSObjectProtocol)?
|
|
|
|
|
+ item.keySpace = AVMetadataKeySpace(rawValue: keySpaceQuickTimeMetadata)
|
|
|
|
|
+ item.value = assetIdentifier as (NSCopying & NSObjectProtocol)?
|
|
|
|
|
+ item.dataType = "com.apple.metadata.datatype.UTF-8"
|
|
|
|
|
+ return item
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func createMetadataAdaptorForStillImageTime() -> AVAssetWriterInputMetadataAdaptor {
|
|
|
|
|
+ let keyStillImageTime = "com.apple.quicktime.still-image-time"
|
|
|
|
|
+ let keySpaceQuickTimeMetadata = "mdta"
|
|
|
|
|
+ let spec : NSDictionary = [
|
|
|
|
|
+ kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier as NSString:
|
|
|
|
|
+ "\(keySpaceQuickTimeMetadata)/\(keyStillImageTime)",
|
|
|
|
|
+ kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType as NSString:
|
|
|
|
|
+ "com.apple.metadata.datatype.int8" ]
|
|
|
|
|
+ var desc : CMFormatDescription? = nil
|
|
|
|
|
+ CMMetadataFormatDescriptionCreateWithMetadataSpecifications(
|
|
|
|
|
+ allocator: kCFAllocatorDefault,
|
|
|
|
|
+ metadataType: kCMMetadataFormatType_Boxed,
|
|
|
|
|
+ metadataSpecifications: [spec] as CFArray,
|
|
|
|
|
+ formatDescriptionOut: &desc
|
|
|
|
|
+ )
|
|
|
|
|
+ let input = AVAssetWriterInput(mediaType: .metadata,
|
|
|
|
|
+ outputSettings: nil, sourceFormatHint: desc)
|
|
|
|
|
+ return AVAssetWriterInputMetadataAdaptor(assetWriterInput: input)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func metadataItemForStillImageTime() -> AVMetadataItem {
|
|
|
|
|
+ let item = AVMutableMetadataItem()
|
|
|
|
|
+ let keyStillImageTime = "com.apple.quicktime.still-image-time"
|
|
|
|
|
+ let keySpaceQuickTimeMetadata = "mdta"
|
|
|
|
|
+ item.key = keyStillImageTime as (NSCopying & NSObjectProtocol)?
|
|
|
|
|
+ item.keySpace = AVMetadataKeySpace(rawValue: keySpaceQuickTimeMetadata)
|
|
|
|
|
+ item.value = 0 as (NSCopying & NSObjectProtocol)?
|
|
|
|
|
+ item.dataType = "com.apple.metadata.datatype.int8"
|
|
|
|
|
+ return item
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func addAssetID(_ assetIdentifier: String, toImage imageURL: URL, saveTo destinationURL: URL) -> URL? {
|
|
|
|
|
+ guard let imageDestination = CGImageDestinationCreateWithURL(destinationURL as CFURL, kUTTypeJPEG, 1, nil),
|
|
|
|
|
+ let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, nil),
|
|
|
|
|
+ let imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, nil),
|
|
|
|
|
+ var imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [AnyHashable : Any] else { return nil }
|
|
|
|
|
+ let assetIdentifierKey = "17"
|
|
|
|
|
+ let assetIdentifierInfo = [assetIdentifierKey : assetIdentifier]
|
|
|
|
|
+ imageProperties[kCGImagePropertyMakerAppleDictionary] = assetIdentifierInfo
|
|
|
|
|
+ CGImageDestinationAddImage(imageDestination, imageRef, imageProperties as CFDictionary)
|
|
|
|
|
+ CGImageDestinationFinalize(imageDestination)
|
|
|
|
|
+ return destinationURL
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func addAssetID(_ assetIdentifier: String, toVideo videoURL: URL, saveTo destinationURL: URL, progress: @escaping (CGFloat) -> Void, completion: @escaping (URL?) -> Void) {
|
|
|
|
|
+
|
|
|
|
|
+ var audioWriterInput: AVAssetWriterInput?
|
|
|
|
|
+ var audioReaderOutput: AVAssetReaderOutput?
|
|
|
|
|
+ let videoAsset = AVURLAsset(url: videoURL)
|
|
|
|
|
+ let frameCount = videoAsset.countFrames(exact: false)
|
|
|
|
|
+ guard let videoTrack = videoAsset.tracks(withMediaType: .video).first else {
|
|
|
|
|
+ completion(nil)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ do {
|
|
|
|
|
+ // Create the Asset Writer
|
|
|
|
|
+ assetWriter = try AVAssetWriter(outputURL: destinationURL, fileType: .mov)
|
|
|
|
|
+ // Create Video Reader Output
|
|
|
|
|
+ videoReader = try AVAssetReader(asset: videoAsset)
|
|
|
|
|
+ let videoReaderSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA as UInt32)]
|
|
|
|
|
+ let videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)
|
|
|
|
|
+ videoReader?.add(videoReaderOutput)
|
|
|
|
|
+ // Create Video Writer Input
|
|
|
|
|
+ let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: [AVVideoCodecKey : AVVideoCodecH264, AVVideoWidthKey : videoTrack.naturalSize.width, AVVideoHeightKey : videoTrack.naturalSize.height])
|
|
|
|
|
+ videoWriterInput.transform = videoTrack.preferredTransform
|
|
|
|
|
+ videoWriterInput.expectsMediaDataInRealTime = true
|
|
|
|
|
+ assetWriter?.add(videoWriterInput)
|
|
|
|
|
+ // Create Audio Reader Output & Writer Input
|
|
|
|
|
+ if let audioTrack = videoAsset.tracks(withMediaType: .audio).first {
|
|
|
|
|
+ do {
|
|
|
|
|
+ let _audioReader = try AVAssetReader(asset: videoAsset)
|
|
|
|
|
+ let _audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
|
|
|
|
|
+ _audioReader.add(_audioReaderOutput)
|
|
|
|
|
+ audioReader = _audioReader
|
|
|
|
|
+ audioReaderOutput = _audioReaderOutput
|
|
|
|
|
+ let _audioWriterInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
|
|
|
|
|
+ _audioWriterInput.expectsMediaDataInRealTime = false
|
|
|
|
|
+ assetWriter?.add(_audioWriterInput)
|
|
|
|
|
+ audioWriterInput = _audioWriterInput
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ print(error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Create necessary identifier metadata and still image time metadata
|
|
|
|
|
+ let assetIdentifierMetadata = metadataForAssetID(assetIdentifier)
|
|
|
|
|
+ let stillImageTimeMetadataAdapter = createMetadataAdaptorForStillImageTime()
|
|
|
|
|
+ assetWriter?.metadata = [assetIdentifierMetadata]
|
|
|
|
|
+ assetWriter?.add(stillImageTimeMetadataAdapter.assetWriterInput)
|
|
|
|
|
+ // Start the Asset Writer
|
|
|
|
|
+ assetWriter?.startWriting()
|
|
|
|
|
+ assetWriter?.startSession(atSourceTime: CMTime.zero)
|
|
|
|
|
+ // Add still image metadata
|
|
|
|
|
+ let _stillImagePercent: Float = 0.5
|
|
|
|
|
+ stillImageTimeMetadataAdapter.append(AVTimedMetadataGroup(items: [metadataItemForStillImageTime()],timeRange: videoAsset.makeStillImageTimeRange(percent: _stillImagePercent, inFrameCount: frameCount)))
|
|
|
|
|
+ // For end of writing / progress
|
|
|
|
|
+ var writingVideoFinished = false
|
|
|
|
|
+ var writingAudioFinished = false
|
|
|
|
|
+ var currentFrameCount = 0
|
|
|
|
|
+ func didCompleteWriting() {
|
|
|
|
|
+ guard writingAudioFinished && writingVideoFinished else { return }
|
|
|
|
|
+ assetWriter?.finishWriting {
|
|
|
|
|
+ if assetWriter?.status == .completed {
|
|
|
|
|
+ completion(destinationURL)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ completion(nil)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Start writing video
|
|
|
|
|
+ if videoReader?.startReading() ?? false {
|
|
|
|
|
+ videoWriterInput.requestMediaDataWhenReady(on: DispatchQueue(label: "videoWriterInputQueue")) {
|
|
|
|
|
+ while videoWriterInput.isReadyForMoreMediaData {
|
|
|
|
|
+ if let sampleBuffer = videoReaderOutput.copyNextSampleBuffer() {
|
|
|
|
|
+ currentFrameCount += 1
|
|
|
|
|
+ let percent:CGFloat = CGFloat(currentFrameCount)/CGFloat(frameCount)
|
|
|
|
|
+ progress(percent)
|
|
|
|
|
+ if !videoWriterInput.append(sampleBuffer) {
|
|
|
|
|
+ print("Cannot write: \(String(describing: assetWriter?.error?.localizedDescription))")
|
|
|
|
|
+ videoReader?.cancelReading()
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ videoWriterInput.markAsFinished()
|
|
|
|
|
+ writingVideoFinished = true
|
|
|
|
|
+ didCompleteWriting()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ writingVideoFinished = true
|
|
|
|
|
+ didCompleteWriting()
|
|
|
|
|
+ }
|
|
|
|
|
+ // Start writing audio
|
|
|
|
|
+ if audioReader?.startReading() ?? false {
|
|
|
|
|
+ audioWriterInput?.requestMediaDataWhenReady(on: DispatchQueue(label: "audioWriterInputQueue")) {
|
|
|
|
|
+ while audioWriterInput?.isReadyForMoreMediaData ?? false {
|
|
|
|
|
+ guard let sampleBuffer = audioReaderOutput?.copyNextSampleBuffer() else {
|
|
|
|
|
+ audioWriterInput?.markAsFinished()
|
|
|
|
|
+ writingAudioFinished = true
|
|
|
|
|
+ didCompleteWriting()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ audioWriterInput?.append(sampleBuffer)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ writingAudioFinished = true
|
|
|
|
|
+ didCompleteWriting()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ print(error)
|
|
|
|
|
+ completion(nil)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fileprivate extension AVAsset {
|
|
|
|
|
+ func countFrames(exact:Bool) -> Int {
|
|
|
|
|
+
|
|
|
|
|
+ var frameCount = 0
|
|
|
|
|
+
|
|
|
|
|
+ if let videoReader = try? AVAssetReader(asset: self) {
|
|
|
|
|
+
|
|
|
|
|
+ if let videoTrack = self.tracks(withMediaType: .video).first {
|
|
|
|
|
+
|
|
|
|
|
+ frameCount = Int(CMTimeGetSeconds(self.duration) * Float64(videoTrack.nominalFrameRate))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ if exact {
|
|
|
|
|
+
|
|
|
|
|
+ frameCount = 0
|
|
|
|
|
+
|
|
|
|
|
+ let videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: nil)
|
|
|
|
|
+ videoReader.add(videoReaderOutput)
|
|
|
|
|
+
|
|
|
|
|
+ videoReader.startReading()
|
|
|
|
|
+
|
|
|
|
|
+ // count frames
|
|
|
|
|
+ while true {
|
|
|
|
|
+ let sampleBuffer = videoReaderOutput.copyNextSampleBuffer()
|
|
|
|
|
+ if sampleBuffer == nil {
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ frameCount += 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ videoReader.cancelReading()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return frameCount
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func makeStillImageTimeRange(percent:Float, inFrameCount:Int = 0) -> CMTimeRange {
|
|
|
|
|
+
|
|
|
|
|
+ var time = self.duration
|
|
|
|
|
+
|
|
|
|
|
+ var frameCount = inFrameCount
|
|
|
|
|
+
|
|
|
|
|
+ if frameCount == 0 {
|
|
|
|
|
+ frameCount = self.countFrames(exact: true)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let frameDuration = Int64(Float(time.value) / Float(frameCount))
|
|
|
|
|
+
|
|
|
|
|
+ time.value = Int64(Float(time.value) * percent)
|
|
|
|
|
+
|
|
|
|
|
+ //print("stillImageTime = \(CMTimeGetSeconds(time))")
|
|
|
|
|
+
|
|
|
|
|
+ return CMTimeRangeMake(start: time, duration: CMTimeMake(value: frameDuration, timescale: time.timescale))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
extension ImageCache {
|
|
extension ImageCache {
|
|
@@ -230,8 +505,8 @@ extension ImageCache {
|
|
|
// Returns the possible cache file URL for the given key and processor identifier. If the file is on disk, it will
|
|
// Returns the possible cache file URL for the given key and processor identifier. If the file is on disk, it will
|
|
|
// return the URL. Otherwise, it will return `nil`.
|
|
// return the URL. Otherwise, it will return `nil`.
|
|
|
//
|
|
//
|
|
|
- // This method also tries to guess the file extension if it is not specified in the `referenceFileType`.
|
|
|
|
|
- // `PHLivePhoto`'s `request` method requires the file extension to be correct on the disk, and we also stored the
|
|
|
|
|
|
|
+ // This method also tries to guess the file extension if it is not specified in the `referenceFileType`.
|
|
|
|
|
+ // `PHLivePhoto`'s `request` method requires the file extension to be correct on the disk, and we also stored the
|
|
|
// downloaded data with the correct extension (if it is not specified in the `referenceFileType`, we infer it from
|
|
// downloaded data with the correct extension (if it is not specified in the `referenceFileType`, we infer it from
|
|
|
// the file signature. See `FileType.determinedFileExtension` for more).
|
|
// the file signature. See `FileType.determinedFileExtension` for more).
|
|
|
func possibleCacheFileURLIfOnDisk(
|
|
func possibleCacheFileURLIfOnDisk(
|