Reputation: 1370
Recently, started noticing some assets were not being displayed when importing them from the photo library. The assets in questions are stored in iCloud and are not cached on the device. I believe it is an iOS 14 issue, since I have never experienced this issue on iOS 13. (I am not 100% sure since I am not able to roll out my personal device to iOS 13).
Here is what I am doing in iOS 14:
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = PHPickerFilter.videos
let videoPickerController = PHPickerViewController(configuration: configuration)
videoPickerController.delegate = self
present(videoPickerController, animated: true, completion: nil)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true, completion: nil)
guard results.count > 0 else {
return
}
guard let firstPHAssetIdentifier = results.first?.assetIdentifier else {
fatalError("No asset identifier")
}
let fetchOptions = PHFetchOptions()
guard let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [firstPHAssetIdentifier], options: fetchOptions).firstObject else {
fatalError("No matching PHAsset")
}
guard phAsset.mediaType == .video else {
fatalError("Asset not of the video type")
}
}
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
options.progressHandler = { progress, _, _, _ in
print("Progress: \(progress)")
}
PHCachingImageManager.default().requestAVAsset(forVideo: phAsset, options: options) { [weak self] avAsset, _, info in
guard info?[PHImageCancelledKey] == nil && info?[PHImageErrorKey] == nil else {
print("Error or cancelled. Info: \(String(describing: info))")
return
}
guard let avAsset = avAsset else {
print("Asset is nil. Info: \(String(describing: info))")
return
}
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
print("Cound not extract video track from AvAsset") // <- My issue
return
}
}
Problem: Often, I won't have a videoTrack when the asset is coming from iCloud. The avAsset.duration will also be 0.0.
I will see the download progress but I will fall in that last guard statement. Sometimes, once the asset has been downloaded and could not load videoTrack, retrying will just instantly fail (it will not try to load the asset again, seems like it's corrupted). It will fall into that last guard statement.
I noticed using deliveryMode = .highQualityFormat on the PHVideoRequestOptions makes it work but I would rather download only a 720p video, not a high quality video.
I suppose I am doing something wrong here. Should I get the phAsset from the PHPickerResult this way? Any pointer / help would be greatly appreciated. I created this repo to repro https://github.com/SwiftRabbit/RequestAVAssetIssues/tree/main/RequestAVAssetIssues. Doesn't repro 100% and has to be iCloud videos that are not on device anymore.
Additional notes / attempts:
Upvotes: 7
Views: 2936
Reputation: 405
I was able to reproduce the issue by recording a video on my iPad, wait until it was synced to iCloud and then requesting the AVAsset on my iPhone SE 2016. I have created a repo on github to illustrate this. It is a Objective-C project, but the conclusions should be the same for Swift. The essential part of the repo is on https://github.com/Jan-E/ikyle.me-code-examples/blob/master/Photo%20Picker%20ObjC/Photo%20Picker%20ObjC/PHPickerController.m
This is the code I am using:
for (PHAsset *phAsset in assetResults) {
float recordingDuration = phAsset.duration;
NSLog(@"recordingDuration %f", recordingDuration);
NSDate *PHAssetCreationDate = phAsset.creationDate;
NSLog(@"PHAssetCreationDate %@", PHAssetCreationDate);
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
options.networkAccessAllowed = NO;
[[PHImageManager defaultManager] requestAVAssetForVideo:phAsset
options:options
resultHandler:^(AVAsset *avAsset, AVAudioMix *audioMix, NSDictionary *info) {
NSURL *videoURL = (NSURL *)[[(AVURLAsset *)avAsset URL] fileReferenceURL];
NSLog(@"videoURL absoluteString = %@", [videoURL absoluteString]);
NSLog(@"videoURL relativePath = %@", [videoURL relativePath]);
AVURLAsset *avUrl = [AVURLAsset assetWithURL:videoURL];
CMTime time = [avUrl duration];
float recordingDuration;
recordingDuration = time.value/time.timescale;
NSArray *tracks = [avUrl tracksWithMediaType:AVMediaTypeVideo];
NSLog(@"duration %f, tracks %lu", recordingDuration, (unsigned long)tracks.count);
}];
}
When the video is still only on iCloud these are the results, returned by NSLog:
By using result.itemProvider loadFileRepresentationForTypeIdentifier
you can force downloading of the video from iCloud to the device. Then the results are as follows:
The complete NSLog output is in the 2 comments below the last of the 2 commits that did the trick: https://github.com/Jan-E/ikyle.me-code-examples/commit/2d0174c2766abf742b6d76d053abc0a46199358f
Edit: note that in the example that started without the video on the device it took 4:23 minutes to download the 1.2GB video from iCloud (2020-12-24 15:45:13.669505+0100 - 2020-12-24 15:49:36.224240+0100). Downloading might not be necessary in all cases. The information that phAsset.duration has a value of 680.044983 seconds may be all the info you need in your app, as it implies that the video has at least 1 track.
Edit 2: By using options:nil
I was using an implicit networkAccessAllowed = NO
. With the edit I made it explicit. It might also give an explanation for the duration 0.0 and tracks 0. What if network access is allowed, but a network connection fails? Then you would still get a meaningful value for phAsset.duration (from earlier iCloud syncs), but no live value for the avAsset.
Upvotes: 0