2323
2424import Foundation
2525import AVFoundation
26+ import VideoToolbox
2627
2728// MARK: - types
2829
30+ /// HDR transfer function for video export.
31+ public enum HDRTransferFunction : String , Sendable {
32+ case hlg = " ITU_R_2100_HLG "
33+ case hdr10 = " SMPTE_ST_2084_PQ "
34+
35+ var avTransferFunction : String {
36+ switch self {
37+ case . hlg:
38+ return AVVideoTransferFunction_ITU_R_2100_HLG
39+ case . hdr10:
40+ return AVVideoTransferFunction_SMPTE_ST_2084_PQ
41+ }
42+ }
43+ }
44+
2945/// Session export errors.
3046public enum NextLevelSessionExporterError : LocalizedError , Sendable {
3147 case setupFailure( String )
@@ -111,6 +127,12 @@ open class NextLevelSessionExporter: NSObject, @unchecked Sendable {
111127 /// Audio output configuration dictionary, using keys defined in `<AVFoundation/AVAudioSettings.h>`
112128 public var audioOutputConfiguration : [ String : Any ] ?
113129
130+ /// Automatically detect and preserve HDR content from source video.
131+ /// When enabled, the exporter will detect HDR color space and transfer function
132+ /// from the source asset and apply appropriate 10-bit HEVC encoding settings.
133+ /// Set to `false` to force SDR output. Default is `true`.
134+ public var preserveHDR : Bool = true
135+
114136 /// Export session status state.
115137 public var status : AVAssetExportSession . Status {
116138 get {
@@ -170,6 +192,11 @@ open class NextLevelSessionExporter: NSObject, @unchecked Sendable {
170192 fileprivate var _videoSetupFailed : Bool = false
171193 fileprivate var _videoSetupError : Error ?
172194
195+ fileprivate var _detectedHDR : Bool = false
196+ fileprivate var _hdrTransferFunction : String ?
197+ fileprivate var _hdrColorPrimaries : String ?
198+ fileprivate var _hdrYCbCrMatrix : String ?
199+
173200 // MARK: - object lifecycle
174201
175202 /// Initializes a session with an asset to export.
@@ -258,6 +285,10 @@ extension NextLevelSessionExporter {
258285 self . _progress = 0
259286 self . _videoSetupFailed = false
260287 self . _videoSetupError = nil
288+ self . _detectedHDR = false
289+ self . _hdrTransferFunction = nil
290+ self . _hdrColorPrimaries = nil
291+ self . _hdrYCbCrMatrix = nil
261292
262293 do {
263294 self . _reader = try AVAssetReader ( asset: asset)
@@ -379,6 +410,123 @@ extension NextLevelSessionExporter {
379410
380411}
381412
413+ // MARK: - HDR support
414+
415+ extension NextLevelSessionExporter {
416+
417+ /// Detects HDR color properties from the source video track.
418+ ///
419+ /// - Parameter videoTrack: The video track to inspect for HDR properties
420+ /// - Returns: True if HDR content is detected
421+ private func detectHDR( from videoTrack: AVAssetTrack ) -> Bool {
422+ guard self . preserveHDR else {
423+ return false
424+ }
425+
426+ guard let formatDescriptions = videoTrack. formatDescriptions as? [ CMFormatDescription ] ,
427+ let formatDescription = formatDescriptions. first else {
428+ return false
429+ }
430+
431+ guard let extensions = CMFormatDescriptionGetExtensions ( formatDescription) as? [ String : Any ] else {
432+ return false
433+ }
434+
435+ // Check for HDR transfer function (HLG or PQ)
436+ if let transferFunction = extensions [ kCVImageBufferTransferFunctionKey as String ] as? String {
437+ if transferFunction == kCVImageBufferTransferFunction_ITU_R_2100_HLG as String ||
438+ transferFunction == kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ as String {
439+ self . _hdrTransferFunction = transferFunction
440+ }
441+ }
442+
443+ // Check for wide color gamut primaries
444+ if let colorPrimaries = extensions [ kCVImageBufferColorPrimariesKey as String ] as? String {
445+ if colorPrimaries == kCVImageBufferColorPrimaries_ITU_R_2020 as String {
446+ self . _hdrColorPrimaries = colorPrimaries
447+ }
448+ }
449+
450+ // Check for YCbCr matrix
451+ if let yCbCrMatrix = extensions [ kCVImageBufferYCbCrMatrixKey as String ] as? String {
452+ if yCbCrMatrix == kCVImageBufferYCbCrMatrix_ITU_R_2020 as String {
453+ self . _hdrYCbCrMatrix = yCbCrMatrix
454+ }
455+ }
456+
457+ // HDR is detected if we have transfer function and color primaries
458+ let isHDR = self . _hdrTransferFunction != nil && self . _hdrColorPrimaries != nil
459+ self . _detectedHDR = isHDR
460+
461+ return isHDR
462+ }
463+
464+ /// Configures the exporter for explicit HDR output with the specified transfer function.
465+ ///
466+ /// Call this method to force HDR encoding even if the source is SDR, or to override
467+ /// the detected HDR transfer function.
468+ ///
469+ /// - Parameter transferFunction: The HDR transfer function to use (.hlg or .hdr10)
470+ ///
471+ /// ## Example
472+ /// ```swift
473+ /// let exporter = NextLevelSessionExporter(withAsset: asset)
474+ /// exporter.configureForHDR(transferFunction: .hlg)
475+ /// ```
476+ ///
477+ /// - Note: This will be applied when you call `export()`. You must also ensure your
478+ /// `videoOutputConfiguration` uses HEVC codec and appropriate dimensions.
479+ public func configureForHDR( transferFunction: HDRTransferFunction = . hlg) {
480+ self . _detectedHDR = true
481+ self . _hdrTransferFunction = transferFunction. avTransferFunction
482+ self . _hdrColorPrimaries = kCVImageBufferColorPrimaries_ITU_R_2020 as String
483+ self . _hdrYCbCrMatrix = kCVImageBufferYCbCrMatrix_ITU_R_2020 as String
484+ }
485+
486+ /// Applies HDR color properties to the video output configuration.
487+ ///
488+ /// - Parameter videoConfig: The video output configuration dictionary to modify
489+ /// - Returns: Modified configuration with HDR properties added
490+ private func applyHDRProperties( to videoConfig: [ String : Any ] ) -> [ String : Any ] {
491+ guard self . _detectedHDR,
492+ let transferFunction = self . _hdrTransferFunction,
493+ let colorPrimaries = self . _hdrColorPrimaries else {
494+ return videoConfig
495+ }
496+
497+ var config = videoConfig
498+
499+ // Ensure HEVC codec for HDR
500+ if config [ AVVideoCodecKey] == nil {
501+ config [ AVVideoCodecKey] = AVVideoCodecType . hevc
502+ }
503+
504+ // Add color properties for HDR
505+ var colorProperties : [ String : Any ] = [ : ]
506+ colorProperties [ AVVideoTransferFunctionKey] = transferFunction
507+ colorProperties [ AVVideoColorPrimariesKey] = colorPrimaries
508+
509+ if let yCbCrMatrix = self . _hdrYCbCrMatrix {
510+ colorProperties [ AVVideoYCbCrMatrixKey] = yCbCrMatrix
511+ }
512+
513+ config [ AVVideoColorPropertiesKey] = colorProperties
514+
515+ // Add compression properties for 10-bit HEVC
516+ var compressionProperties = config [ AVVideoCompressionPropertiesKey] as? [ String : Any ] ?? [ : ]
517+ compressionProperties [ AVVideoProfileLevelKey] = kVTProfileLevel_HEVC_Main10_AutoLevel
518+
519+ // Add HDR metadata insertion mode (iOS 14+ / macOS 11+)
520+ if #available( iOS 14 . 0 , macOS 11 . 0 , * ) {
521+ compressionProperties [ kVTCompressionPropertyKey_HDRMetadataInsertionMode as String ] = kVTHDRMetadataInsertionMode_Auto
522+ }
523+
524+ config [ AVVideoCompressionPropertiesKey] = compressionProperties
525+
526+ return config
527+ }
528+ }
529+
382530// MARK: - setup funcs
383531
384532extension NextLevelSessionExporter {
@@ -390,7 +538,26 @@ extension NextLevelSessionExporter {
390538 return
391539 }
392540
393- self . _videoOutput = AVAssetReaderVideoCompositionOutput ( videoTracks: videoTracks, videoSettings: self . videoInputConfiguration)
541+ // Detect HDR from the first video track
542+ if let firstVideoTrack = videoTracks. first {
543+ let isHDR = self . detectHDR ( from: firstVideoTrack)
544+ if isHDR {
545+ print ( " NextLevelSessionExporter, HDR content detected - will preserve HDR properties " )
546+ }
547+ }
548+
549+ // Configure video input settings for HDR if detected
550+ var videoInputSettings = self . videoInputConfiguration
551+ if self . _detectedHDR {
552+ // For HDR, use 10-bit pixel format
553+ if videoInputSettings == nil {
554+ videoInputSettings = [ : ]
555+ }
556+ // Use 420YpCbCr10BiPlanarVideoRange for 10-bit HDR processing
557+ videoInputSettings ? [ kCVPixelBufferPixelFormatTypeKey as String ] = NSNumber ( integerLiteral: Int ( kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange) )
558+ }
559+
560+ self . _videoOutput = AVAssetReaderVideoCompositionOutput ( videoTracks: videoTracks, videoSettings: videoInputSettings)
394561 self . _videoOutput? . alwaysCopiesSampleData = false
395562
396563 if let videoComposition = self . videoComposition {
@@ -407,8 +574,14 @@ extension NextLevelSessionExporter {
407574 }
408575
409576 // video input
410- if self . _writer? . canApply ( outputSettings: self . videoOutputConfiguration, forMediaType: AVMediaType . video) == true {
411- self . _videoInput = AVAssetWriterInput ( mediaType: AVMediaType . video, outputSettings: self . videoOutputConfiguration)
577+ // Apply HDR properties to video output configuration if HDR was detected
578+ var finalVideoOutputConfiguration = self . videoOutputConfiguration
579+ if self . _detectedHDR, let videoConfig = finalVideoOutputConfiguration {
580+ finalVideoOutputConfiguration = self . applyHDRProperties ( to: videoConfig)
581+ }
582+
583+ if self . _writer? . canApply ( outputSettings: finalVideoOutputConfiguration, forMediaType: AVMediaType . video) == true {
584+ self . _videoInput = AVAssetWriterInput ( mediaType: AVMediaType . video, outputSettings: finalVideoOutputConfiguration)
412585 self . _videoInput? . expectsMediaDataInRealTime = self . expectsMediaDataInRealTime
413586 } else {
414587 // Mark video setup as failed - this will cause the export to fail with a clear error
@@ -427,7 +600,11 @@ extension NextLevelSessionExporter {
427600 // setup pixelbuffer adaptor
428601
429602 var pixelBufferAttrib : [ String : Any ] = [ : ]
430- pixelBufferAttrib [ kCVPixelBufferPixelFormatTypeKey as String ] = NSNumber ( integerLiteral: Int ( kCVPixelFormatType_32RGBA) )
603+
604+ // Use 10-bit pixel format for HDR, otherwise 8-bit RGBA for SDR
605+ let pixelFormat = self . _detectedHDR ? kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange : kCVPixelFormatType_32RGBA
606+ pixelBufferAttrib [ kCVPixelBufferPixelFormatTypeKey as String ] = NSNumber ( integerLiteral: Int ( pixelFormat) )
607+
431608 if let videoComposition = self . _videoOutput? . videoComposition {
432609 pixelBufferAttrib [ kCVPixelBufferWidthKey as String ] = NSNumber ( integerLiteral: Int ( videoComposition. renderSize. width) )
433610 pixelBufferAttrib [ kCVPixelBufferHeightKey as String ] = NSNumber ( integerLiteral: Int ( videoComposition. renderSize. height) )
@@ -762,6 +939,11 @@ extension NextLevelSessionExporter {
762939
763940 self . _videoSetupFailed = false
764941 self . _videoSetupError = nil
942+
943+ self . _detectedHDR = false
944+ self . _hdrTransferFunction = nil
945+ self . _hdrColorPrimaries = nil
946+ self . _hdrYCbCrMatrix = nil
765947 }
766948
767949}
0 commit comments