milen.me
milen.me

Live Photo API on iOS

Permalink | RSS

What are Live Photos? From a marketing perspective, Live Photos record the moments just before and after you take a photo. Under the hood, a Live Photo is just a JPEG image together with a QuickTime file contaning an H.264 track.

Storing Live Photo Data

The first step in using Live Photos is to retrieve them and then access the underlying data. The easiest way to get hold of a Live Photo is to ask the user to select one from their camera roll. For this, we can use UIImagePickerController — the crucial part is including both kUTTypeLivePhoto and kUTTypeImage in the media types array.

@IBAction func showImagePicker(sender: UIButton) {
  let picker = UIImagePickerController()
  picker.delegate = self;
  picker.allowsEditing = false;
  picker.sourceType = .PhotoLibrary;
  picker.mediaTypes = [kUTTypeLivePhoto as String, kUTTypeImage as String];
  
  presentViewController(picker, animated: true, completion: nil);
}

Once the user has selected a photo, the delegate method will be called. From there we can check whether they have actually selected a Live Photo. If that's the case, we will generate a random directory where we can store all the associated resources. The PHAssetResourceManager gives us chunks of data which we concatenate. If we wanted to be more efficient, we could have used an NSOutputStream or NSFileHandle.

func imagePickerController(
  picker: UIImagePickerController,
  didFinishPickingMediaWithInfo info: [String : AnyObject]
) {
  guard
    let livePhoto = info[UIImagePickerControllerLivePhoto] as? PHLivePhoto,
    let photoDir = generateFolderForLivePhotoResources()
  else {
    return;
  }
  
  let assetResources = PHAssetResource.assetResourcesForLivePhoto(livePhoto)
  for resource in assetResources {
    let buffer = NSMutableData()
    PHAssetResourceManager.defaultManager().requestDataForAssetResource(
      resource,
      options: nil,
      dataReceivedHandler: { (chunk: NSData) -> Void in
        buffer.appendData(chunk)
      },
      completionHandler:saveAssetResource(resource, inDirectory: photoDir, buffer: buffer)
    )
  }
}

Once we finished collecting the data for a resource, we're ready to write it to a file. We get the preferred file extension and save the file to disk.

func saveAssetResource(
  resource: PHAssetResource,
  inDirectory: NSURL,
  buffer: NSMutableData)(maybeError: NSError?
) -> Void {
  guard maybeError == nil else {
    print("Could not request data for resource: \(resource), error: \(maybeError)")
    return
  }
  
  let maybeExt = UTTypeCopyPreferredTagWithClass(
    resource.uniformTypeIdentifier,
    kUTTagClassFilenameExtension
  )?.takeRetainedValue()
  
  guard let ext = maybeExt else {
    return
  }
  
  var fileUrl = inDirectory.URLByAppendingPathComponent(NSUUID().UUIDString)
  fileUrl = fileUrl.URLByAppendingPathExtension(ext as String)
  
  if(!buffer.writeToURL(fileUrl, atomically: true)) {
    print("Could not save resource \(resource) to filepath \(fileUrl)")
  }
}

Below, you can see how we generate a random directory. In the example code, it will be a subdirectory of the temporary directory, which gets cleaned up by the OS at unspecified intervals.

func generateFolderForLivePhotoResources() -> NSURL? {
  let photoDir = NSURL(
    // NB: Files in NSTemporaryDirectory() are automatically cleaned up by the OS
    fileURLWithPath: NSTemporaryDirectory(),
    isDirectory: true
  ).URLByAppendingPathComponent(NSUUID().UUIDString)
  
  let fileManager = NSFileManager()
  // we need to specify type as ()? as otherwise the compiler generates a warning
  let success : ()? = try? fileManager.createDirectoryAtURL(
    photoDir,
    withIntermediateDirectories: true,
    attributes: nil
  )
  
  return success != nil ? photoDir : nil
}

After you have saved all the resources to disk, you can transfer them to your servers or any other destination.

Displaying Live Photos

To go in the other direction, all we need is a list of file NSURLs representing a single Live Photo. Those would have been downloaded by your app from your servers. Note that the block callback will be executed multiple times, initially providing a PHLivePhoto of degraded quality.

Note that there's currently a crasher bug in PHLivePhotoView. It manifests itself if you try to request a Live Photo from non-existing files. The workaround is to check for a non-nil photo with zero size.
func showLivePhoto(photoFiles: [NSURL]) {
  let requestID = PHLivePhoto.requestLivePhotoWithResourceFileURLs(
    photoFiles,
    placeholderImage: nil,
    targetSize: CGSizeZero,
    contentMode: .AspectFit
  ) { (livePhoto: PHLivePhoto?, info: [NSObject : AnyObject]) -> Void in
      let error = info[PHLivePhotoInfoErrorKey] as? NSError
      let degraded = info[PHLivePhotoInfoIsDegradedKey] as? NSNumber
      let cancelled = info[PHLivePhotoInfoCancelledKey] as? NSNumber
      let finished = (
        (error != nil) ||
        (cancelled != nil && cancelled!.boolValue == true) ||
        (degraded != nil && degraded!.boolValue == false)
      )
      
      if finished {
        // If you have saved `requestID`, you can now set it to PHLivePhotoRequestIDInvalid
        print("Live photo request finished")
      }
      
      if let photo = livePhoto where photo.size == CGSizeZero {
        // Workaround for crasher rdar://24021574 (https://openradar.appspot.com/24021574)
        return;
      }
      
      self.photoView.livePhoto = livePhoto
  }
}

← Back to Writings

Any opinions and viewpoints expressed, explicitly or implicitly, are not endorsed by and do not represent any of my previous, current or future employers.