Search code examples
iosswiftavassetexportsessionavassetphotokit

Modifying Metadata from PHAsset with mediaType Video fails


I try adding/modifying the Metadata from an PHAsset with mediaType == .video I found some Questions refering to a similar problem:

How to change video metadata using AVAssetWriter?

Add custom metadata to video using AVFoundation

Regarding to the Answers in these Questions I build the following snippet which is a extension of a PHAsset:

let options = PHVideoRequestOptions()
options.version = .original

PHImageManager.default().requestAVAsset(forVideo: self, options: options, resultHandler: {
    asset, audioMix, info in

    if asset != nil && asset!.isKind(of: AVURLAsset.self) {
        let urlAsset = asset as! AVURLAsset

        let start = CMTimeMakeWithSeconds(0.0, 1)
        let duration = asset!.duration                    


        var exportSession = AVAssetExportSession(asset: asset!, presetName: AVAssetExportPresetPassthrough)
        exportSession!.outputURL = urlAsset.url
        exportSession!.outputFileType = AVFileTypeAppleM4V
        exportSession!.timeRange = CMTimeRange(start: start, duration: duration)

        var modifiedMetadata = asset!.metadata

        let metadataItem = AVMutableMetadataItem()
        metadataItem.keySpace = AVMetadataKeySpaceQuickTimeUserData
        metadataItem.key = AVMetadataQuickTimeMetadataKeyRatingUser as NSString
        metadataItem.value = NSNumber(floatLiteral: Double(rating))

        modifiedMetadata.append(metadataItem)

        exportSession!.metadata = modifiedMetadata

        LogInfo("\(modifiedMetadata)")


        exportSession!.exportAsynchronously(completionHandler: {
            let status = exportSession?.status
            let success = status == AVAssetExportSessionStatus.completed
            if success {
                completion(true)
            } else {
                LogError("\(exportSession!.error!)")
                completion(false)
            }
        })
    }
})

When I execute this snippet, the exportSession failed an has the following error:

Error Domain=NSURLErrorDomain 
Code=-3000 "Cannot create file" 
UserInfo={NSLocalizedDescription=Cannot create file, 
NSUnderlyingError=0x1702439f0 
{Error Domain=NSOSStatusErrorDomain Code=-12124 "(null)"}}

Solution

  • I found my mistake. To modify the metadata of an PHAsset with the MediaType MediaType.video you can use the following snippet, where self is the PHAsset:

    First you need to create an PHContentEditingOutput you can do that with requesting an PHContentEditingInput from the PHAsset you want to modify. When changing an PHAsset you also have to set the .adjustmentData Value of the PHContentEditingOutput or else the .performChanges() Block will fail.

       self.requestContentEditingInput(with: options, completionHandler: {
            (contentEditingInput, _) -> Void in
    
            if contentEditingInput != nil {
    
                let adjustmentData = PHAdjustmentData(formatIdentifier: starRatingIdentifier, formatVersion: formatVersion, data: NSKeyedArchiver.archivedData(withRootObject: rating))
    
                let contentEditingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput!)
                contentEditingOutput.adjustmentData = adjustmentData
                self.applyRatingToVideo(rating, contentEditingInput, contentEditingOutput, completion: {
                    output in
                    if output != nil {
                        PHPhotoLibrary.shared().performChanged({
                            let request = PHAssetChangeRequest(for: self)
                            request.contentEditingOutput = output
                        }, completionHandler: {
                            success, error in
                            if !success {
                                print("can't edit asset: \(String(describing: error))")
                            }
                        })
                    }
                })
            }
        })
    

    With the snippet above, you change the PHAsset after modifying the PHContentEditingOutput in the following snippet you will see, how to set the Metadata for an User Rating:

    private func applyRatingToVideo(_ rating: Int, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping (PHContentEditingOutput?) -> Void) {
        guard let avAsset = input.audiovisualAsset else { return }
    
        guard let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetPassthrough) else { return }
    
        var mutableMetadata = exportSession.asset.metadata
        let metadataCopy = mutableMetadata
    
        for item in metadataCopy {
            if item.identifier == AVMetadataIdentifierQuickTimeMetadataRatingUser {
                mutableMetadata.remove(object: item)
            }
        }
    
        let metadataItem = AVMutableMetadataItem()
        metadataItem.identifier = AVMetadataIdentifierQuickTimeMetadataRatingUser
        metadataItem.keySpace = AVMetadataKeySpaceQuickTimeMetadata
        metadataItem.key = AVMetadataQuickTimeMetadataKeyRatingUser as NSString
        metadataItem.value = NSNumber(floatLiteral: Double(rating))
    
        exportSession.outputURL = output.renderedContentURL
        mutableMetadata.append(metadataItem)
        exportSession.metadata = mutableMetadata
        exportSession.outputFileType = AVFileTypeQuickTimeMovie
        exportSession.shouldOptimizeForNetworkUse = true
        exportSession.exportAsynchronously(completionHandler: {
            if exportSession.status == .completed {
                completion(output)
            } else if exportSession.error != nil {
                completion(nil)
            }
        })
    }
    

    Consider, that if you do not remove the AVMetadataItem with the same Identifier as the one you want to add, the AVAssetExportSession will set multiple Items with the same Identifier for the AVAsset.

    NOTE:

    When you now access the Video through the PHImageManager-method .requestAVAsset(forVideo:,options:,resultHandler:) you have to pass an PHVideoRequestOptions-object with the .version variable set to .current. It is set as default value of the variable but if you change it to .original you will get the unmodified Video from that method.