AVAssetExportPresetHEVCHighestQualityWithAlpha decreases quality a lot when exporting video

Problem

I need to import a video, process and then export the video with alpha. I noticed the video gets a lot grayer/loses quality compared to the original. I don't need any compression.

Sidenote: I need to export video's with transparency enabled, that's why I use AVAssetExportPresetHEVCHighestQualityWithAlpha. It seems that that is causing the problem, since AVAssetExportPresetHighestQuality is looking good.

This are side-by-side frames of the original and a video that's processed. The left is the original frame, the right is a processed video: https://i.stack.imgur.com/ORqfz.png

This is another example where the bottom is exported and the above is the original. You can see at the bar where the YouTube NL is displayed, that the above one is almost fully black, while the below one (exported) is really gray: https://i.stack.imgur.com/s8lCn.png

As far as I know, I don't do anything special, I just load the video and directly export it. It still loses quality. How can I prevent this?

Reproduction path

You can either clone the repository, or see the code below.

The repository is available here: https://github.com/Jasperav/VideoCompression/tree/main/VideoCompressionTests. After you cloned it, run the only unit-test and check the logging of where the output of the video is stored. You can then observe that temp.mov is a lot grayer than the original video.

The code of importing and exporting the video is here. As far as I can see, I just import and directly export the movie without modifying it. What's the problem?

import AppKit
import AVFoundation
import Foundation
import Photos
import QuartzCore
import OSLog

let logger = Logger()

class VideoEditor {
    func export(
        url: URL,
        outputDir: URL
    ) async {
        let asset = AVURLAsset(url: url)
        let extract = try! await extractData(videoAsset: asset)

        try! await exportVideo(outputPath: outputDir, asset: asset, videoComposition: extract)
    }

    private func exportVideo(outputPath: URL, asset: AVAsset, videoComposition: AVMutableVideoComposition) async throws {
        let fileExists = FileManager.default.fileExists(atPath: outputPath.path())

        logger.debug("Output dir: \(outputPath), exists: \(fileExists), render size: \(String(describing: videoComposition.renderSize))")

        if fileExists {
            do {
                try FileManager.default.removeItem(atPath: outputPath.path())
            } catch {
                logger.error("remove file failed")
            }
        }

        let dir = outputPath.deletingLastPathComponent().path()

        logger.debug("Will try to create dir: \(dir)")

        try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)

        var isDirectory = ObjCBool(false)

        guard FileManager.default.fileExists(atPath: dir, isDirectory: &isDirectory), isDirectory.boolValue else {
            logger.error("Could not create dir, or dir is a file")

            fatalError()
        }

        guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHEVCHighestQualityWithAlpha) else {
            logger.error("generate export failed")

            fatalError()
        }

        exporter.outputURL = outputPath
        exporter.outputFileType = .mov
        exporter.shouldOptimizeForNetworkUse = false
        exporter.videoComposition = videoComposition

        await exporter.export()

        logger.debug("Status: \(String(describing: exporter.status)), error: \(exporter.error)")

        if exporter.status != .completed {
            fatalError()
        }
    }

    private func extractData(videoAsset: AVURLAsset) async throws -> AVMutableVideoComposition {
        guard let videoTrack = try await videoAsset.loadTracks(withMediaType: .video).first else {
            fatalError()
        }

        let composition = AVMutableComposition(urlAssetInitializationOptions: nil)

        guard let compositionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: videoTrack.trackID) else {
            fatalError()
        }

        let duration = try await videoAsset.load(.duration)

        try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: duration), of: videoTrack, at: CMTime.zero)

        let naturalSize = try await videoTrack.load(.naturalSize)
        let preferredTransform = try await videoTrack.load(.preferredTransform)
        let mainInstruction = AVMutableVideoCompositionInstruction()

        mainInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: duration)

        let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
        let videoComposition = AVMutableVideoComposition()
        let frameRate = try await videoTrack.load(.nominalFrameRate)

        videoComposition.frameDuration = CMTimeMake(value: 1, timescale: Int32(frameRate))

        mainInstruction.layerInstructions = [layerInstruction]
        videoComposition.instructions = [mainInstruction]

        videoComposition.renderSize = naturalSize

        return videoComposition
    }

}