Swift Rabbit
Swift Rabbit

Reputation: 1370

Request AVAsset using iCloud PHAsset returns an AVAsset with no VideoTracks

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:

Upvotes: 7

Views: 2936

Answers (1)

Jan Ehrhardt
Jan Ehrhardt

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:

  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.

Upvotes: 0

Related Questions