7

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:

  1. Using the new picker view controller to import video assets
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = PHPickerFilter.videos
let videoPickerController = PHPickerViewController(configuration: configuration)
videoPickerController.delegate = self
present(videoPickerController, animated: true, completion: nil)
  1. I extract the PHAsset from the [PHPickerResult] (delegate method)
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")
  }
}
  1. Then, I request an AVAsset for the matching PHAsset
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:

  • I have experienced the same issue with PHCachingImageManager and PHImageManager
  • Some popular apps seem to have the same issue for some assets (e.g. Instagram, TikTok...)
  • PHCachingImageManager.default().requestPlayerItem does not work either. It returns an AVPlayerItem that does not contain any track.
  • iPhone XS, corporate managed device
Swift Rabbit
  • 1,370
  • 3
  • 14
  • 29
  • Have you tried a KVO observation on the `tracks` property? I'd imagine you're attempting to access the tracks before they're loaded. – Claus Jørgensen Sep 27 '20 at 23:34
  • Hey @ClausJørgensen. I also tried that by putting the code avAsset.loadValuesAsynchronously(forKeys: ["playable", "tracks"]) { [weak self] in before trying to get the videoTracks from the avAsset. The result is the same unfortunately Is that what you had in mind? – Swift Rabbit Sep 28 '20 at 00:32
  • 1
    I was thinking `avAsset.observe(\.tracks, ...`. Or you could try add a 5 second delay before attempting to read the tracks, see if that changes the outcome (if it doesn't, it's not a race condition, and the problem is elsewhere) – Claus Jørgensen Sep 28 '20 at 02:26
  • So I tried to add a delay of 10 seconds (dispatch async). There still is no video track after the delay. I tried observing using the keypath, there is just no update on that variable after the callback so it never gets called, even for assets that work. – Swift Rabbit Sep 28 '20 at 03:58
  • Would this also make the duration 0? I think I am getting this error as well for the AVAssets. – agibson007 Oct 02 '20 at 18:32
  • @agibson007 Yes, the duration of the avAsset is 0. I'm assuming it's because it does not contain any tracks. – Swift Rabbit Oct 05 '20 at 15:03
  • I have not been able to reproduce myself. We are getting crash reports every now of then that I can only believe that matches this because the time range construction is getting 0 for duration – agibson007 Oct 05 '20 at 15:45
  • I am able to get constant repros on my side but some of my colleagues are not. "Glad" to know other people are experiencing this. I think this is issue is regarding iCloud but it seems there are other factors. Device model, corporate managed device... ? – Swift Rabbit Oct 05 '20 at 16:24
  • 1
    Yes we are seeing it but it is not on all devices. XR, 8s, 11,XS. That’s basically it. XR is most common. – agibson007 Oct 07 '20 at 00:46
  • Ok so going to the Photos album and then viewing the asset and returning to the app let's it play. I still have not reproduced it but teammates have. Just giving this other info – agibson007 Oct 13 '20 at 16:38
  • Yeah, that's how we discovered the bug is related to iCloud. You can also send the video via imessage and it will fetch the video on the device too. You can also fetch using high quality format (see my sample app) to make it work on automatic format for following attempts. – Swift Rabbit Oct 13 '20 at 18:38
  • We noticed the issue is happening more often on iOS 14 but is affecting all kinds of OS. – Swift Rabbit Oct 19 '20 at 15:20
  • Having the exact same issues, have you made any new discoveries around this? – Greenwell Nov 30 '20 at 14:09
  • 1
    I am seeing the same behavior, but using .requesPlayerItem and adding the observer on avPlayerItem.status it shows that it failed, with avPlayerItem.error = 257, which means no permission to view. I go to Photos, open the asset there and come back to my app and suddenly it is .readyToPlay. – Rodrigo Fava Dec 01 '20 at 18:09
  • Did anyone experience the same issue while using UIImagePickerController in stead of PHPickerViewController? My guess is that it should not happen with the older picker. – Jan Ehrhardt Dec 23 '20 at 14:11
  • @JanEhrhardt Happens with both – Swift Rabbit Dec 28 '20 at 17:49
  • @SwiftRabbit Did you manage to find a workaround or have opened a radar I could dupe? – Dory Mar 15 '21 at 12:09
  • 1
    @Dory "Workaround" I put in place is to refetch the asset in full quality. Now one could check if connected to WIFI vs 3G to do it, but we noticed positive impact when just refetching. Did create a radar named: "requestAVAssetForVideo returns an Asset with a videoTrack of natural size of 0;0 when video stored on iCloud". Can't share with you the number. – Swift Rabbit Mar 15 '21 at 17:04

1 Answers1

0

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:

  1. The phAsset.duration returns the correct value
  2. The phAsset.creationDate returns the correct value
  3. The avAsset URL returns (null)
  4. The duration of the avAsset is 0.0 seconds
  5. The number of tracks of the avAsset is 0

By using result.itemProvider loadFileRepresentationForTypeIdentifier you can force downloading of the video from iCloud to the device. Then the results are as follows:

  1. The phAsset.duration returns the correct value
  2. The phAsset.creationDate returns the correct value
  3. The avAsset URL returns the correct path
  4. The duration of the avAsset is the correct value
  5. The number of tracks of the avAsset is 1

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.

Jan Ehrhardt
  • 395
  • 1
  • 8
  • 20
  • “ By using result.itemProvider loadFileRepresentationForTypeIdentifier you can force downloading of the video from iCloud to the device” But I would just point out that that is how you would get the video even if it were NOT in the cloud. – matt Dec 24 '20 at 20:52
  • True. If the video is only in the cloud, you are in fact doing two things: (1) download the file to the device and (2) make a copy for whatever the PHPicker was intended to do with the file. In both cases you know for sure that avAsset will return a correct value for duration and tracks. – Jan Ehrhardt Dec 24 '20 at 23:30
  • But I think what I'm saying is rather more profound than that. You can say `loadFileRepresentation` anyway. If you're going to do that, what is the `assetIdentifier` for? The whole idea here is that we have direct photo library access. The OP's question is why _that_ access seems not to be working. – matt Dec 25 '20 at 00:00
  • I am getting this error without using PHPicker with networkAccess on for my call manager.requestAVAsset(forVideo:). I have even put loadValuesAsynchronously(forKeys: ["playable","tracks"]) at the end of the call after I have the asset but on occasion the app is crashing. I am also not using PHAssets duration for this but rather the completed AVAsset. So in other words my app thinks it has downloaded the asset but creating a time range is not possible – agibson007 Feb 12 '21 at 13:41