Skip to content

Commit 25ae678

Browse files
committed
add HDR support
Closes #40
1 parent bf06df2 commit 25ae678

2 files changed

Lines changed: 249 additions & 4 deletions

File tree

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ The library provides customizable audio and video encoding options unlike `AVAss
1010
### ✨ What's New in Swift 6
1111

1212
- **🚀 Modern Async/Await API** - Native Swift concurrency support with `async/await` and `AsyncSequence`
13+
- **🌈 HDR Video Support** - Automatic detection and preservation of HLG and HDR10 content with 10-bit HEVC
1314
- **⚡ Better Performance** - Proper memory management with autoreleasepool in encoding loop
1415
- **🎯 QoS Configuration** - Control export priority to prevent thread priority inversion (PR #44)
1516
- **🔒 Swift 6 Strict Concurrency** - Full `Sendable` conformance and thread-safety
1617
- **📝 Enhanced Error Messages** - Contextual error descriptions with recovery suggestions
1718
- **♻️ Task Cancellation** - Proper cancellation support for modern Swift concurrency
19+
- **🛡️ Better Error Handling** - Fixed silent failures causing audio-only exports (#38)
1820
- **🔙 Backwards Compatible** - Legacy completion handler API still works for iOS 13+
1921

2022
### Requirements
@@ -283,6 +285,67 @@ exporter.export { renderFrame, presentationTime, resultBuffer in
283285
}
284286
```
285287

288+
### HDR Video Support
289+
290+
NextLevelSessionExporter automatically detects and preserves HDR content (HLG and HDR10) from source videos:
291+
292+
```swift
293+
// Automatic HDR preservation (default behavior)
294+
let exporter = NextLevelSessionExporter(withAsset: hdrAsset)
295+
exporter.outputURL = outputURL
296+
exporter.videoOutputConfiguration = [
297+
AVVideoWidthKey: 1920,
298+
AVVideoHeightKey: 1080
299+
]
300+
// HDR properties automatically detected and preserved ✨
301+
302+
let result = try await exporter.export()
303+
// Output maintains HDR color space, transfer function, and 10-bit encoding
304+
```
305+
306+
**Features:**
307+
- **Automatic Detection**: Detects HLG (Hybrid Log-Gamma) and HDR10 (PQ) transfer functions
308+
- **10-bit HEVC**: Automatically configures Main10 profile for 10-bit encoding
309+
- **Color Properties**: Preserves ITU-R BT.2020 color primaries and YCbCr matrix
310+
- **HDR Metadata**: Automatically inserts and preserves HDR metadata (iOS 14+)
311+
312+
#### Force SDR Output
313+
314+
To convert HDR to SDR, disable HDR preservation:
315+
316+
```swift
317+
exporter.preserveHDR = false
318+
// Output will be 8-bit SDR
319+
```
320+
321+
#### Explicit HDR Configuration
322+
323+
Force HDR encoding even for SDR source, or override detected transfer function:
324+
325+
```swift
326+
// Configure for HLG HDR
327+
exporter.configureForHDR(transferFunction: .hlg)
328+
329+
// Or configure for HDR10 (PQ)
330+
exporter.configureForHDR(transferFunction: .hdr10)
331+
332+
// Note: HEVC codec and appropriate dimensions required
333+
exporter.videoOutputConfiguration = [
334+
AVVideoCodecKey: AVVideoCodecType.hevc,
335+
AVVideoWidthKey: 1920,
336+
AVVideoHeightKey: 1080
337+
]
338+
```
339+
340+
**Requirements:**
341+
- iOS 15.0+ or macOS 12.0+
342+
- HEVC (H.265) codec required for HDR
343+
- Device must support 10-bit HEVC encoding
344+
345+
**Supported HDR Formats:**
346+
- HLG (Hybrid Log-Gamma) - Broadcast standard, better for wide compatibility
347+
- HDR10 (PQ/SMPTE ST 2084) - Consumer HDR standard with static metadata
348+
286349
### Time Range Trimming
287350

288351
Export only a portion of the video:

Sources/NextLevelSessionExporter.swift

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,25 @@
2323

2424
import Foundation
2525
import 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.
3046
public 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

384532
extension 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

Comments
 (0)