# HDR Support on tvOS with mpv - Research Document ## Problem Statement HDR content appears washed out on Apple TV when using the mpv-based player. The TV doesn't show an HDR indicator and colors look flat compared to other apps like Infuse. **Key Discovery:** HDR works correctly on iPhone but not on tvOS, despite using the same mpv player. --- ## Why HDR Works on iPhone In `MpvPlayerView.swift`: ```swift #if !os(tvOS) if #available(iOS 17.0, *) { displayLayer.wantsExtendedDynamicRangeContent = true } #endif ``` On iOS 17+, setting `wantsExtendedDynamicRangeContent = true` on `AVSampleBufferDisplayLayer` enables Extended Dynamic Range (EDR). This tells the display layer to preserve HDR metadata and render in high dynamic range. **This API does not exist on tvOS.** Attempting to use it results in: > 'wantsExtendedDynamicRangeContent' is unavailable in tvOS tvOS uses a different HDR architecture designed for external displays via HDMI. --- ## tvOS HDR Architecture Unlike iPhone (integrated display), Apple TV connects to external TVs. Apple expects apps to: 1. **Use `AVDisplayCriteria`** to request display mode changes 2. **Attach proper colorspace metadata** to pixel buffers 3. **Let the TV handle HDR rendering** via HDMI passthrough This is how Netflix, Infuse, and the TV app work - they signal "I'm playing HDR10 at 24fps" and tvOS switches the TV to that mode. --- ## MPVKit vo_avfoundation Analysis **Location:** `/MPVKit/Sources/BuildScripts/patch/libmpv/0004-avfoundation-video-output.patch` ### Existing HDR Infrastructure The driver has comprehensive HDR support already built in: #### 1. HDR Metadata Copy Function (lines 253-270) ```c static void copy_hdr_metadata(CVPixelBufferRef src, CVPixelBufferRef dst) { const CFStringRef keys[] = { kCVImageBufferTransferFunctionKey, // PQ for HDR10, HLG for HLG kCVImageBufferColorPrimariesKey, // BT.2020 for HDR kCVImageBufferYCbCrMatrixKey, kCVImageBufferMasteringDisplayColorVolumeKey, // HDR10 static metadata kCVImageBufferContentLightLevelInfoKey, // MaxCLL, MaxFALL }; for (size_t i = 0; i < MP_ARRAY_SIZE(keys); i++) { CFTypeRef value = CVBufferGetAttachment(src, keys[i], NULL); if (value) { CVBufferSetAttachment(dst, keys[i], value, kCVAttachmentMode_ShouldPropagate); } } } ``` #### 2. 10-bit HDR Format Support (lines 232-247) ```c // For 10-bit HDR content (P010), use RGBA half-float to preserve HDR precision if (format == kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange || format == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange) { outputFormat = kCVPixelFormatType_64RGBAHalf; } ``` #### 3. HDR-Safe GPU Compositing (lines 694-695) ```c CGColorSpaceRef workingColorSpace = CGColorSpaceCreateWithName( kCGColorSpaceExtendedLinearDisplayP3); ``` ### The Problem: Metadata Not Attached in Main Code Path **Critical Finding:** `copy_hdr_metadata()` is only called during OSD compositing (line 609-610): ```c // In composite mode, render OSD and composite onto frame if (p->composite_osd) { render_osd(vo, pts); CVPixelBufferRef composited = composite_frame(vo, pixbuf); // copy_hdr_metadata() called inside composite_frame() } ``` If `composite_osd` is false (default), **HDR metadata is never attached**. ### Frame Flow Analysis ``` draw_frame() called │ ├─► Hardware decoded (IMGFMT_VIDEOTOOLBOX) │ └─► pixbuf = mpi->planes[3] // Direct from VideoToolbox │ └─► Metadata SHOULD be attached by decoder, but not verified │ └─► Software decoded (NV12, 420P, P010) └─► upload_software_frame() └─► Creates new CVPixelBuffer └─► Copies pixel data only └─► ❌ NO colorspace metadata attached! ▼ CMVideoFormatDescriptionCreateForImageBuffer(finalBuffer) └─► Format description created FROM pixel buffer └─► If buffer lacks HDR metadata, format won't have it ▼ [displayLayer enqueueSampleBuffer:buf] └─► Sent to display layer without HDR signal ``` --- ## Root Cause Summary | Issue | Impact | |-------|--------| | `wantsExtendedDynamicRangeContent` unavailable on tvOS | Can't use iOS EDR approach | | `copy_hdr_metadata()` only runs during OSD compositing | Main playback path skips HDR metadata | | Software decoded frames get no colorspace attachments | mpv knows colorspace but doesn't pass it to pixel buffer | | VideoToolbox metadata not verified | May or may not have HDR attachments | --- ## mp_image Colorspace Structures mpv uses libplacebo's colorspace structures. Here's how colorspace info flows: ### Structure Hierarchy ``` mp_image (video/mp_image.h) └─► params: mp_image_params └─► color: pl_color_space │ ├─► primaries: pl_color_primaries (BT.2020, etc.) │ ├─► transfer: pl_color_transfer (PQ, HLG, etc.) │ └─► hdr: pl_hdr_metadata (MaxCLL, MaxFALL, etc.) └─► repr: pl_color_repr ├─► sys: pl_color_system (BT.2100_PQ, etc.) └─► levels: pl_color_levels (TV/Full range) ``` ### Key Enums #### Color Primaries (`enum pl_color_primaries`) ```c PL_COLOR_PRIM_UNKNOWN = 0, PL_COLOR_PRIM_BT_709, // HD/SDR standard PL_COLOR_PRIM_BT_2020, // UHD/HDR wide gamut ← HDR PL_COLOR_PRIM_DCI_P3, // DCI P3 (cinema) PL_COLOR_PRIM_DISPLAY_P3, // Display P3 (Apple) // ... more ``` #### Transfer Functions (`enum pl_color_transfer`) ```c PL_COLOR_TRC_UNKNOWN = 0, PL_COLOR_TRC_BT_1886, // SDR gamma PL_COLOR_TRC_SRGB, // sRGB PL_COLOR_TRC_PQ, // SMPTE 2084 PQ (HDR10/DolbyVision) ← HDR PL_COLOR_TRC_HLG, // ITU-R BT.2100 HLG ← HDR // ... more ``` #### Color Systems (`enum pl_color_system`) ```c PL_COLOR_SYSTEM_BT_709, // HD/SDR PL_COLOR_SYSTEM_BT_2020_NC, // UHD (non-constant luminance) PL_COLOR_SYSTEM_BT_2100_PQ, // HDR10 ← HDR PL_COLOR_SYSTEM_BT_2100_HLG, // HLG ← HDR PL_COLOR_SYSTEM_DOLBYVISION, // Dolby Vision ← HDR // ... more ``` ### HDR Metadata Structure (`struct pl_hdr_metadata`) ```c struct pl_hdr_metadata { struct pl_raw_primaries prim; // CIE xy primaries float min_luma, max_luma; // Luminance range (cd/m²) float max_cll; // Maximum Content Light Level float max_fall; // Maximum Frame-Average Light Level // ... more }; ``` ### Accessing Colorspace in vo_avfoundation ```c // In draw_frame(): struct mp_image *mpi = frame->current; // Color primaries enum pl_color_primaries prim = mpi->params.color.primaries; // Transfer function enum pl_color_transfer trc = mpi->params.color.transfer; // HDR metadata struct pl_hdr_metadata hdr = mpi->params.color.hdr; // HDR detection bool is_hdr = (trc == PL_COLOR_TRC_PQ || trc == PL_COLOR_TRC_HLG); bool is_wide_gamut = (prim == PL_COLOR_PRIM_BT_2020); ``` --- ## The Fix ### Required Changes in vo_avfoundation **File:** `MPVKit/Sources/BuildScripts/patch/libmpv/0004-avfoundation-video-output.patch` **Location:** After line 821 in `draw_frame()`, before the sample buffer is created. #### Add Required Include ```c #include "video/csputils.h" // For pl_color_* enums (if not already included) ``` #### Add HDR Metadata Attachment Function ```c // Add after copy_hdr_metadata() function (around line 270) static void attach_hdr_metadata(struct vo *vo, CVPixelBufferRef pixbuf, struct mp_image *mpi) { enum pl_color_primaries prim = mpi->params.color.primaries; enum pl_color_transfer trc = mpi->params.color.transfer; // Attach BT.2020 color primaries (HDR wide color gamut) if (prim == PL_COLOR_PRIM_BT_2020) { CVBufferSetAttachment(pixbuf, kCVImageBufferColorPrimariesKey, kCVImageBufferColorPrimaries_ITU_R_2020, kCVAttachmentMode_ShouldPropagate); CVBufferSetAttachment(pixbuf, kCVImageBufferYCbCrMatrixKey, kCVImageBufferYCbCrMatrix_ITU_R_2020, kCVAttachmentMode_ShouldPropagate); MP_VERBOSE(vo, "HDR: Attached BT.2020 color primaries\n"); } // Attach PQ transfer function (HDR10/Dolby Vision) if (trc == PL_COLOR_TRC_PQ) { CVBufferSetAttachment(pixbuf, kCVImageBufferTransferFunctionKey, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ, kCVAttachmentMode_ShouldPropagate); MP_VERBOSE(vo, "HDR: Attached PQ transfer function (HDR10)\n"); } // Attach HLG transfer function else if (trc == PL_COLOR_TRC_HLG) { CVBufferSetAttachment(pixbuf, kCVImageBufferTransferFunctionKey, kCVImageBufferTransferFunction_ITU_R_2100_HLG, kCVAttachmentMode_ShouldPropagate); MP_VERBOSE(vo, "HDR: Attached HLG transfer function\n"); } // Attach HDR static metadata if available struct pl_hdr_metadata hdr = mpi->params.color.hdr; if (hdr.max_cll > 0 || hdr.max_fall > 0) { // ContentLightLevelInfo is a 4-byte structure: // - 2 bytes: MaxCLL (max content light level) // - 2 bytes: MaxFALL (max frame-average light level) uint16_t cll_data[2] = { (uint16_t)fminf(hdr.max_cll, 65535.0f), (uint16_t)fminf(hdr.max_fall, 65535.0f) }; CFDataRef cllInfo = CFDataCreate(NULL, (const UInt8 *)cll_data, sizeof(cll_data)); if (cllInfo) { CVBufferSetAttachment(pixbuf, kCVImageBufferContentLightLevelInfoKey, cllInfo, kCVAttachmentMode_ShouldPropagate); CFRelease(cllInfo); MP_VERBOSE(vo, "HDR: Attached CLL metadata (MaxCLL=%d, MaxFALL=%d)\n", cll_data[0], cll_data[1]); } } } ``` #### Call the Function in draw_frame() ```c // In draw_frame(), after line 821 (after getting pixbuf), add: // Attach HDR colorspace metadata to pixel buffer // This ensures the display layer receives proper HDR signaling attach_hdr_metadata(vo, pixbuf, mpi); ``` ### Complete draw_frame() Modification The modified section should look like: ```c CVPixelBufferRef pixbuf = NULL; bool pixbufNeedsRelease = false; // Handle different input formats if (mpi->imgfmt == IMGFMT_VIDEOTOOLBOX) { // Hardware decoded: zero-copy passthrough pixbuf = (CVPixelBufferRef)mpi->planes[3]; } else { // Software decoded: upload to CVPixelBuffer pixbuf = upload_software_frame(vo, mpi); if (!pixbuf) { MP_ERR(vo, "Failed to upload software frame\n"); mp_image_unrefp(&mpi); return false; } pixbufNeedsRelease = true; } // >>> NEW: Attach HDR colorspace metadata <<< attach_hdr_metadata(vo, pixbuf, mpi); CVPixelBufferRef finalBuffer = pixbuf; bool needsRelease = false; // ... rest of the function ``` --- ## Alternative Solutions ### Option A: Enable composite_osd Mode (Quick Test) Since `copy_hdr_metadata()` works in composite mode, try enabling it: ``` --avfoundation-composite-osd=yes ``` This would trigger the existing HDR metadata path. Downside: OSD compositing has performance overhead. ### Option B: Full vo_avfoundation Fix (Recommended) Modify the driver to always attach colorspace metadata based on `mp_image` params. This is the implementation described above. ### Option C: Dual Player Approach Use AVPlayer for HDR content, mpv for everything else. This is what Swiftfin does. --- ## Implementation Checklist - [ ] Clone MPVKit fork - [ ] Modify `0004-avfoundation-video-output.patch`: - [ ] Add `attach_hdr_metadata()` function - [ ] Call it in `draw_frame()` after getting pixbuf - [ ] Add necessary includes if needed - [ ] Rebuild MPVKit - [ ] Test with HDR10 content on tvOS - [ ] Verify TV shows HDR indicator - [ ] Test with HLG content - [ ] Test with Dolby Vision content (may need additional work) --- ## Current Implementation Status **What's implemented in Streamyfin:** 1. **HDR Detection** (`MPVLayerRenderer.swift`) - Reads `video-params/primaries` and `video-params/gamma` from mpv - Detects HDR10 (bt.2020 + pq), HLG, Dolby Vision 2. **AVDisplayCriteria** (`MpvPlayerView.swift`) - Sets `preferredDisplayCriteria` on tvOS 17.0+ when HDR detected - Creates CMFormatDescription with HDR color extensions 3. **target-colorspace-hint** (`MPVLayerRenderer.swift`) - Added `target-colorspace-hint=yes` for tvOS **What's NOT working:** - TV doesn't show HDR indicator - Colors appear washed out - The pixel buffers lack HDR metadata attachments ← **This is what the fix addresses** --- ## Industry Context | Project | Player | HDR Status | |---------|--------|------------| | [Swiftfin](https://github.com/jellyfin/Swiftfin/issues/811) | VLCKit | Washed out, uses AVPlayer for HDR | | [Plex](https://freetime.mikeconnelly.com/archives/8360) | mpv | No HDR support | | Infuse | Custom Metal engine | Works correctly | **Key insight:** No mpv-based player has solved HDR on tvOS. This fix could be a first. --- ## Technical References ### Apple Documentation - [AVDisplayManager](https://developer.apple.com/documentation/avkit/avdisplaymanager) - [AVDisplayCriteria](https://developer.apple.com/documentation/avkit/avdisplaycriteria) - [WWDC22: Display HDR video in EDR](https://developer.apple.com/videos/play/wwdc2022/110565/) ### CVImageBuffer Keys - `kCVImageBufferColorPrimariesKey` - Color gamut (BT.709, BT.2020, P3) - `kCVImageBufferTransferFunctionKey` - Transfer function (sRGB, PQ, HLG) - `kCVImageBufferYCbCrMatrixKey` - YCbCr conversion matrix - `kCVImageBufferMasteringDisplayColorVolumeKey` - Mastering display metadata - `kCVImageBufferContentLightLevelInfoKey` - MaxCLL/MaxFALL ### mpv/libplacebo Source - mp_image struct: `video/mp_image.h` - Colorspace enums: libplacebo `pl_color.h` - vo_avfoundation: `MPVKit/Sources/BuildScripts/patch/libmpv/0004-avfoundation-video-output.patch` ### Key Functions in vo_avfoundation | Function | Line | Purpose | |----------|------|---------| | `draw_frame()` | 781 | Main frame rendering | | `copy_hdr_metadata()` | 253 | Copy HDR metadata between buffers | | `upload_software_frame()` | 295 | Upload SW frames to CVPixelBuffer | | `composite_frame()` | 582 | OSD compositing with HDR support |