From a599ce09088c76edd716b20b9bf1f44db638443a Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 8 Sep 2025 11:47:48 +0200 Subject: [PATCH] Video: Add mastering display metadata to HDR videos For most players this appears to made no difference. But one case it fixes is that with this metadata, on Youtube with SDR, 10bit PQ looks the same as 12bit PQ, where it was previously too desaturated. 10bit and 12bit HLG was already consistent without this metadata and there appears to be no impact, though it remains inconsistent with PQ. It seems good practice to be explicit about the mastering display regardless. The way we get the max luminance is not ideal though. We get it from the view transform name containing "HDR XXXX nits" as used in the Blender and ACES configs. There is currently no way to query this information from OpenColorIO. In the future we can add a user control for this. For now if we can not determine the max luminance this way, we don't write mastering display metadata. The video player will then make some default assumption for tone mapping, often 1000 nits. Pull Request: https://projects.blender.org/blender/blender/pulls/144378 --- .../blender/imbuf/movie/intern/movie_write.cc | 72 +++++++++++++++++++ .../blender/imbuf/movie/intern/movie_write.hh | 1 + 2 files changed, 73 insertions(+) diff --git a/source/blender/imbuf/movie/intern/movie_write.cc b/source/blender/imbuf/movie/intern/movie_write.cc index 98646453889..b04012114e5 100644 --- a/source/blender/imbuf/movie/intern/movie_write.cc +++ b/source/blender/imbuf/movie/intern/movie_write.cc @@ -144,6 +144,76 @@ static const char **get_file_extensions(int format) } } +static void add_hdr_mastering_display_metadata(AVCodecParameters *codecpar, + AVCodecContext *c, + const ImageFormatData *imf) +{ + if (c->color_primaries != AVCOL_PRI_BT2020) { + return; + } + + int max_luminance = 0; + if (c->color_trc == AVCOL_TRC_ARIB_STD_B67) { + /* HLG is always 1000 nits. */ + max_luminance = 1000; + } + else if (c->color_trc == AVCOL_TRC_SMPTEST2084) { + /* PQ uses heuristic based on view transform name. In the future this could become + * a user control, but this solves the common cases. */ + blender::StringRefNull view_name = imf->view_settings.view_transform; + if (view_name.find("HDR 500 nits")) { + max_luminance = 500; + } + else if (view_name.find("HDR 1000 nits")) { + max_luminance = 1000; + } + else if (view_name.find("HDR 2000 nits")) { + max_luminance = 2000; + } + else if (view_name.find("HDR 4000 nits")) { + max_luminance = 4000; + } + else if (view_name.find("HDR 10000 nits")) { + max_luminance = 10000; + } + } + + /* If we don't know anything, don't write metadata. The video player will make some + * default assumption, often 1000 nits. */ + if (max_luminance == 0) { + return; + } + + AVPacketSideData *side_data = av_packet_side_data_new(&codecpar->coded_side_data, + &codecpar->nb_coded_side_data, + AV_PKT_DATA_MASTERING_DISPLAY_METADATA, + sizeof(AVMasteringDisplayMetadata), + 0); + if (side_data == nullptr) { + CLOG_ERROR(&LOG, "Failed to attached mastering display metadata to stream"); + return; + } + + AVMasteringDisplayMetadata *mastering_metadata = reinterpret_cast( + side_data->data); + + /* Rec.2020 primaries and D65 white point. */ + mastering_metadata->has_primaries = 1; + mastering_metadata->display_primaries[0][0] = av_make_q(34000, 50000); + mastering_metadata->display_primaries[0][1] = av_make_q(16000, 50000); + mastering_metadata->display_primaries[1][0] = av_make_q(13250, 50000); + mastering_metadata->display_primaries[1][1] = av_make_q(34500, 50000); + mastering_metadata->display_primaries[2][0] = av_make_q(7500, 50000); + mastering_metadata->display_primaries[2][1] = av_make_q(3000, 50000); + + mastering_metadata->white_point[0] = av_make_q(15635, 50000); + mastering_metadata->white_point[1] = av_make_q(16450, 50000); + + mastering_metadata->has_luminance = 1; + mastering_metadata->min_luminance = av_make_q(1, 10000); + mastering_metadata->max_luminance = av_make_q(max_luminance, 1); +} + /* Write a frame to the output file */ static bool write_video_frame(MovieWriter *context, AVFrame *frame, ReportList *reports) { @@ -1141,6 +1211,8 @@ static AVStream *alloc_video_stream(MovieWriter *context, avcodec_parameters_from_context(st->codecpar, c); + add_hdr_mastering_display_metadata(st->codecpar, c, imf); + context->video_time = 0.0f; return st; diff --git a/source/blender/imbuf/movie/intern/movie_write.hh b/source/blender/imbuf/movie/intern/movie_write.hh index 3d5d673a995..dff2abcced6 100644 --- a/source/blender/imbuf/movie/intern/movie_write.hh +++ b/source/blender/imbuf/movie/intern/movie_write.hh @@ -25,6 +25,7 @@ extern "C" { # include # include # include +# include # include # include # include