diff mbox series

[FFmpeg-devel,2/2] avutil: add HDR10+ dynamic metadata serialization function

Message ID 725ad573-5e22-84a3-b12f-e68baa2b3980@vimeo.com
State New
Headers show
Series [FFmpeg-devel,1/2] avcodec/avutil: move dynamic HDR metadata parsing to libavutil | expand

Checks

Context Check Description
yinshiyou/make_loongarch64 success Make finished
yinshiyou/make_fate_loongarch64 success Make fate finished
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Raphaël Zumer Feb. 27, 2023, 5:34 p.m. UTC
Resent due to my mail client incorrectly re-wrapping lines in the version I sent earlier.
See the previous patch for context.

Signed-off-by: Raphaël Zumer <rzumer@tebako.net>
---
 libavutil/hdr_dynamic_metadata.c | 147 +++++++++++++++++++++++++++++++
 libavutil/hdr_dynamic_metadata.h |  10 +++
 libavutil/version.h              |   2 +-
 3 files changed, 158 insertions(+), 1 deletion(-)

Comments

quietvoid March 2, 2023, 6:33 p.m. UTC | #1
On 27/02/2023 12.34, Raphaël Zumer wrote:

> Resent due to my mail client incorrectly re-wrapping lines in the version I sent earlier.
> See the previous patch for context.
>
> Signed-off-by: Raphaël Zumer<rzumer@tebako.net>
> ---
>   libavutil/hdr_dynamic_metadata.c | 147 +++++++++++++++++++++++++++++++
>   libavutil/hdr_dynamic_metadata.h |  10 +++
>   libavutil/version.h              |   2 +-
>   3 files changed, 158 insertions(+), 1 deletion(-)
>
> diff --git a/libavutil/hdr_dynamic_metadata.c b/libavutil/hdr_dynamic_metadata.c
> index 98f399b032..72d310e633 100644
> --- a/libavutil/hdr_dynamic_metadata.c
> +++ b/libavutil/hdr_dynamic_metadata.c
> @@ -225,3 +225,150 @@ int av_dynamic_hdr_plus_from_t35(AVDynamicHDRPlus *s, const uint8_t *data,
>   
>       return 0;
>   }
> +
> +AVBufferRef *av_dynamic_hdr_plus_to_t35(AVDynamicHDRPlus *s)
> +{
> +    AVBufferRef *buf;
> +    size_t size_bits, size_bytes;
> +    PutBitContext pbc, *pb = &pbc;
> +
> +    if (!s)
> +        return NULL;
> +
> +    // Buffer size per CTA-861-H p.253-254:
> +    size_bits =
> +    // 56 bits for the header
> +    58 +
> +    // 2 bits for num_windows
> +    2 +
> +    // 937 bits for window geometry for each window above 1
> +    FFMAX((s->num_windows - 1), 0) * 937 +
> +    // 27 bits for targeted_system_display_maximum_luminance
> +    27 +
> +    // 1-3855 bits for targeted system display peak luminance information
> +    1 + (s->targeted_system_display_actual_peak_luminance_flag ? 10 +
> +        s->num_rows_targeted_system_display_actual_peak_luminance *
> +        s->num_cols_targeted_system_display_actual_peak_luminance * 4 : 0) +
> +    // 0-442 bits for intra-window pixel distribution information
> +    s->num_windows * 82;
> +    for (int w = 0; w < s->num_windows; w++) {
> +        size_bits += s->params[w].num_distribution_maxrgb_percentiles * 24;
> +    }
> +    // 1-3855 bits for mastering display peak luminance information
> +    size_bits += 1 + (s->mastering_display_actual_peak_luminance_flag ? 10 +
> +        s->num_rows_mastering_display_actual_peak_luminance *
> +        s->num_cols_mastering_display_actual_peak_luminance * 4 : 0) +
> +    // 0-537 bits for per-window tonemapping information
> +    s->num_windows * 1;
> +    for (int w = 0; w < s->num_windows; w++) {
> +        if (s->params[w].tone_mapping_flag) {
> +            size_bits += 28 + s->params[w].num_bezier_curve_anchors * 10;
> +        }
> +    }
> +    // 0-21 bits for per-window color saturation mapping information
> +    size_bits += s->num_windows * 1;
> +    for (int w = 0; w < s->num_windows; w++) {
> +        if (s->params[w].color_saturation_mapping_flag) {
> +            size_bits += 6;
> +        }
> +    }
> +
> +    size_bytes = (size_bits + 7) / 8;
> +
> +    buf = av_buffer_alloc(size_bytes);
> +    if (!buf) {
> +        return NULL;
> +    }
> +
> +    init_put_bits(pb, buf->data, size_bytes);
> +
> +    // itu_t_t35_country_code shall be 0xB5 (USA)
> +    put_bits(pb, 8, 0xB5);

This patch series mentions that it would be used for AV1, however the AV1
specification is clear that the payload does not contain the country code.
https://aomediacodec.github.io/av1-hdr10plus/#hdr10plus-metadata, Figure 1.

So far all the AV1 HDR10+ conformance samples also respect this.
There would probably be a need to specify which behaviour is wanted.
The SEI for HEVC does keep the country code in the payload, but not AV1.

> +    // itu_t_t35_terminal_provider_code shall be 0x003C
> +    put_bits(pb, 16, 0x003C);
> +    // itu_t_t35_terminal_provider_oriented_code is set to ST 2094-40
> +    put_bits(pb, 16, 0x0001);
> +    // application_identifier shall be set to 4
> +    put_bits(pb, 8, 4);
> +    // application_mode is set to Application Version 1
> +    put_bits(pb, 8, 1);
> +
> +    // Payload as per CTA-861-H p.253-254
> +    put_bits(pb, 2, s->num_windows);
> +
> +    for (int w = 1; w < s->num_windows; w++) {
> +        put_bits(pb, 16, s->params[w].window_upper_left_corner_x.num / s->params[w].window_upper_left_corner_x.den);
> +        put_bits(pb, 16, s->params[w].window_upper_left_corner_y.num / s->params[w].window_upper_left_corner_y.den);
> +        put_bits(pb, 16, s->params[w].window_lower_right_corner_x.num / s->params[w].window_lower_right_corner_x.den);
> +        put_bits(pb, 16, s->params[w].window_lower_right_corner_y.num / s->params[w].window_lower_right_corner_y.den);
> +        put_bits(pb, 16, s->params[w].center_of_ellipse_x);
> +        put_bits(pb, 16, s->params[w].center_of_ellipse_y);
> +        put_bits(pb, 8, s->params[w].rotation_angle);
> +        put_bits(pb, 16, s->params[w].semimajor_axis_internal_ellipse);
> +        put_bits(pb, 16, s->params[w].semimajor_axis_external_ellipse);
> +        put_bits(pb, 16, s->params[w].semiminor_axis_external_ellipse);
> +        put_bits(pb, 1, s->params[w].overlap_process_option);
> +    }
> +
> +    put_bits(pb, 27, s->targeted_system_display_maximum_luminance.num * luminance_den /
> +        s->targeted_system_display_maximum_luminance.den);
> +    put_bits(pb, 1, s->targeted_system_display_actual_peak_luminance_flag);
> +    if (s->targeted_system_display_actual_peak_luminance_flag) {
> +        put_bits(pb, 5, s->num_rows_targeted_system_display_actual_peak_luminance);
> +        put_bits(pb, 5, s->num_cols_targeted_system_display_actual_peak_luminance);
> +        for (int i = 0; i < s->num_rows_targeted_system_display_actual_peak_luminance; i++) {
> +            for (int j = 0; j < s->num_cols_targeted_system_display_actual_peak_luminance; j++) {
> +                put_bits(pb, 4, s->targeted_system_display_actual_peak_luminance[i][j].num * peak_luminance_den /
> +                    s->targeted_system_display_actual_peak_luminance[i][j].den);
> +            }
> +        }
> +    }
> +
> +    for (int w = 0; w < s->num_windows; w++) {
> +        for (int i = 0; i < 3; i++) {
> +            put_bits(pb, 17, s->params[w].maxscl[i].num * rgb_den / s->params[w].maxscl[i].den);
> +        }
> +        put_bits(pb, 17, s->params[w].average_maxrgb.num * rgb_den / s->params[w].average_maxrgb.den);
> +        put_bits(pb, 4, s->params[w].num_distribution_maxrgb_percentiles);
> +        for (int i = 0; i < s->params[w].num_distribution_maxrgb_percentiles; i++) {
> +            put_bits(pb, 7, s->params[w].distribution_maxrgb[i].percentage);
> +            put_bits(pb, 17, s->params[w].distribution_maxrgb[i].percentile.num * rgb_den /
> +                s->params[w].distribution_maxrgb[i].percentile.den);
> +        }
> +        put_bits(pb, 10, s->params[w].fraction_bright_pixels.num * fraction_pixel_den /
> +            s->params[w].fraction_bright_pixels.den);
> +    }
> +
> +    put_bits(pb, 1, s->mastering_display_actual_peak_luminance_flag);
> +    if (s->mastering_display_actual_peak_luminance_flag) {
> +        put_bits(pb, 5, s->num_rows_mastering_display_actual_peak_luminance);
> +        put_bits(pb, 5, s->num_cols_mastering_display_actual_peak_luminance);
> +        for (int i = 0; i < s->num_rows_mastering_display_actual_peak_luminance; i++) {
> +            for (int j = 0; j < s->num_cols_mastering_display_actual_peak_luminance; j++) {
> +                put_bits(pb, 4, s->mastering_display_actual_peak_luminance[i][j].num * peak_luminance_den /
> +                    s->mastering_display_actual_peak_luminance[i][j].den);
> +            }
> +        }
> +    }
> +
> +    for (int w = 0; w < s->num_windows; w++) {
> +        put_bits(pb, 1, s->params[w].tone_mapping_flag);
> +        if (s->params[w].tone_mapping_flag) {
> +            put_bits(pb, 12, s->params[w].knee_point_x.num * knee_point_den / s->params[w].knee_point_x.den);
> +            put_bits(pb, 12, s->params[w].knee_point_y.num * knee_point_den / s->params[w].knee_point_y.den);
> +            put_bits(pb, 4, s->params[w].num_bezier_curve_anchors);
> +            for (int i = 0; i < s->params[w].num_bezier_curve_anchors; i++) {
> +                put_bits(pb, 10, s->params[w].bezier_curve_anchors[i].num * bezier_anchor_den /
> +                    s->params[w].bezier_curve_anchors[i].den);
> +            }
> +            put_bits(pb, 1, s->params[w].color_saturation_mapping_flag);
> +            if (s->params[w].color_saturation_mapping_flag) {
> +                put_bits(pb, 6, s->params[w].color_saturation_weight.num * saturation_weight_den /
> +                    s->params[w].color_saturation_weight.den);
> +            }
> +        }
> +    }
> +
> +    flush_put_bits(pb);
> +    return buf;
> +}
> diff --git a/libavutil/hdr_dynamic_metadata.h b/libavutil/hdr_dynamic_metadata.h
> index 1f953ef1f5..f75d3e0739 100644
> --- a/libavutil/hdr_dynamic_metadata.h
> +++ b/libavutil/hdr_dynamic_metadata.h
> @@ -21,6 +21,7 @@
>   #ifndef AVUTIL_HDR_DYNAMIC_METADATA_H
>   #define AVUTIL_HDR_DYNAMIC_METADATA_H
>   
> +#include "buffer.h"
>   #include "frame.h"
>   #include "rational.h"
>   
> @@ -351,4 +352,13 @@ AVDynamicHDRPlus *av_dynamic_hdr_plus_create_side_data(AVFrame *frame);
>   int av_dynamic_hdr_plus_from_t35(AVDynamicHDRPlus *s, const uint8_t *data,
>                                    int size);
>   
> +/**
> + * Serialize dynamic HDR10+ metadata to a user data registered ITU-T T.35 buffer.
> + * @param s A pointer containing the decoded AVDynamicHDRPlus structure.
> + *
> + * @return Pointer to an AVBufferRef containing the raw ITU-T T.35 representation
> + *         of the HDR10+ metadata if succeed, or NULL if buffer allocation fails.
> + */
> +AVBufferRef *av_dynamic_hdr_plus_to_t35(AVDynamicHDRPlus *s);
> +
>   #endif /* AVUTIL_HDR_DYNAMIC_METADATA_H */
> diff --git a/libavutil/version.h b/libavutil/version.h
> index 900b798971..7635672985 100644
> --- a/libavutil/version.h
> +++ b/libavutil/version.h
> @@ -79,7 +79,7 @@
>    */
>   
>   #define LIBAVUTIL_VERSION_MAJOR  58
> -#define LIBAVUTIL_VERSION_MINOR   3
> +#define LIBAVUTIL_VERSION_MINOR   4
>   #define LIBAVUTIL_VERSION_MICRO 100
>   
>   #define LIBAVUTIL_VERSION_INT   AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
Raphaël Zumer March 2, 2023, 6:57 p.m. UTC | #2
On 3/2/23 13:33, quietvoid wrote:
> This patch series mentions that it would be used for AV1, however the AV1
> specification is clear that the payload does not contain the country code.
> https://aomediacodec.github.io/av1-hdr10plus/#hdr10plus-metadata, Figure 1.
>
> So far all the AV1 HDR10+ conformance samples also respect this.
> There would probably be a need to specify which behaviour is wanted.
> The SEI for HEVC does keep the country code in the payload, but not AV1.

As you pointed out, HEVC does include the country code in the payload, so either approach can be seen as reasonable. There are a few reasons why I don't think it makes sense to reproduce the AV1 specification:

- it does not make sense to exclude the country code from the payload while including other static headers like the terminal provider codes and the application identifier, so there is no particular advantage or logic that I can see in splitting the payload in that way.

- pragmatically, it is much less inconvenient to deal with a 1-byte offset in the payload for AV1 than it is to insert that byte into the payload for HEVC and other applications that expect the country code to be included in the header. For the same reason, I think that an option to include or exclude the country code byte would be more trouble than it's worth.

- perhaps most importantly, the payload syntax is standardized in ANSI/CTA-861-H, which does not make any distinction between the header (country code, terminal provider code, etc.) and the remainder of the payload, so the AV1 specification does not conform either to other implementations nor to the standard. Following the most authoritative document is the safest route in my opinion.

Raphaël Zumer
James Almer March 2, 2023, 6:57 p.m. UTC | #3
On 3/2/2023 3:33 PM, quietvoid wrote:> On 27/02/2023 12.34, Raphaël 
Zumer wrote:
 >
 >> Resent due to my mail client incorrectly re-wrapping lines in the
 >> version I sent earlier.
 >> See the previous patch for context.
 >>
 >> Signed-off-by: Raphaël Zumer<rzumer@tebako.net>
 >> ---
 >>   libavutil/hdr_dynamic_metadata.c | 147 +++++++++++++++++++++++++++++++
 >>   libavutil/hdr_dynamic_metadata.h |  10 +++
 >>   libavutil/version.h              |   2 +-
 >>   3 files changed, 158 insertions(+), 1 deletion(-)
 >>
 >> diff --git a/libavutil/hdr_dynamic_metadata.c
 >> b/libavutil/hdr_dynamic_metadata.c
 >> index 98f399b032..72d310e633 100644
 >> --- a/libavutil/hdr_dynamic_metadata.c
 >> +++ b/libavutil/hdr_dynamic_metadata.c
 >> @@ -225,3 +225,150 @@ int
 >> av_dynamic_hdr_plus_from_t35(AVDynamicHDRPlus *s, const uint8_t *data,
 >>       return 0;
 >>   }
 >> +
 >> +AVBufferRef *av_dynamic_hdr_plus_to_t35(AVDynamicHDRPlus *s)
 >> +{
 >> +    AVBufferRef *buf;
 >> +    size_t size_bits, size_bytes;
 >> +    PutBitContext pbc, *pb = &pbc;
 >> +
 >> +    if (!s)
 >> +        return NULL;
 >> +
 >> +    // Buffer size per CTA-861-H p.253-254:
 >> +    size_bits =
 >> +    // 56 bits for the header
 >> +    58 +
 >> +    // 2 bits for num_windows
 >> +    2 +
 >> +    // 937 bits for window geometry for each window above 1
 >> +    FFMAX((s->num_windows - 1), 0) * 937 +
 >> +    // 27 bits for targeted_system_display_maximum_luminance
 >> +    27 +
 >> +    // 1-3855 bits for targeted system display peak luminance
 >> information
 >> +    1 + (s->targeted_system_display_actual_peak_luminance_flag ? 10 +
 >> +        s->num_rows_targeted_system_display_actual_peak_luminance *
 >> +        s->num_cols_targeted_system_display_actual_peak_luminance * 4
 >> : 0) +
 >> +    // 0-442 bits for intra-window pixel distribution information
 >> +    s->num_windows * 82;
 >> +    for (int w = 0; w < s->num_windows; w++) {
 >> +        size_bits += s->params[w].num_distribution_maxrgb_percentiles
 >> * 24;
 >> +    }
 >> +    // 1-3855 bits for mastering display peak luminance information
 >> +    size_bits += 1 + (s->mastering_display_actual_peak_luminance_flag
 >> ? 10 +
 >> +        s->num_rows_mastering_display_actual_peak_luminance *
 >> +        s->num_cols_mastering_display_actual_peak_luminance * 4 : 0) +
 >> +    // 0-537 bits for per-window tonemapping information
 >> +    s->num_windows * 1;
 >> +    for (int w = 0; w < s->num_windows; w++) {
 >> +        if (s->params[w].tone_mapping_flag) {
 >> +            size_bits += 28 + s->params[w].num_bezier_curve_anchors *
 >> 10;
 >> +        }
 >> +    }
 >> +    // 0-21 bits for per-window color saturation mapping information
 >> +    size_bits += s->num_windows * 1;
 >> +    for (int w = 0; w < s->num_windows; w++) {
 >> +        if (s->params[w].color_saturation_mapping_flag) {
 >> +            size_bits += 6;
 >> +        }
 >> +    }
 >> +
 >> +    size_bytes = (size_bits + 7) / 8;
 >> +
 >> +    buf = av_buffer_alloc(size_bytes);
 >> +    if (!buf) {
 >> +        return NULL;
 >> +    }
 >> +
 >> +    init_put_bits(pb, buf->data, size_bytes);
 >> +
 >> +    // itu_t_t35_country_code shall be 0xB5 (USA)
 >> +    put_bits(pb, 8, 0xB5);
 >
 > This patch series mentions that it would be used for AV1, however the AV1
 > specification is clear that the payload does not contain the country 
code.
 > https://aomediacodec.github.io/av1-hdr10plus/#hdr10plus-metadata, 
Figure 1.
 >
 > So far all the AV1 HDR10+ conformance samples also respect this.
 > There would probably be a need to specify which behaviour is wanted.
 > The SEI for HEVC does keep the country code in the payload, but not AV1.
Are you sure about HEVC? I just checked a sample and trace_headers 
reported this:

[trace_headers] Prefix Supplemental Enhancement Information
[trace_headers] 0  forbidden_zero_bit               0 = 0
[trace_headers] 1  nal_unit_type               100111 = 39
[trace_headers] 7  nuh_layer_id                000000 = 0
[trace_headers] 13 nuh_temporal_id_plus1          001 = 1
[trace_headers] 16 last_payload_type_byte    00000100 = 4
[trace_headers] 24 last_payload_size_byte    01000000 = 64
[trace_headers] User Data Registered ITU-T T.35
[trace_headers] 32 itu_t_t35_country_code    10110101 = 181
[trace_headers] 40 itu_t_t35_payload_byte[1] 00000000 = 0
[trace_headers] 48 itu_t_t35_payload_byte[2] 00111100 = 60
[trace_headers] 56 itu_t_t35_payload_byte[3] 00000000 = 0
[trace_headers] 64 itu_t_t35_payload_byte[4] 00000001 = 1
[trace_headers] 72 itu_t_t35_payload_byte[5] 00000100 = 4
[trace_headers] 80 itu_t_t35_payload_byte[6] 00000001 = 1

Which is in line with section 8.3.1 of ITU-T Rec. H.274 as well as 
section D.2.6 of ITU-T Rec. H.265.

So i think this function should not set the country code at all as part 
of the payload, and start with itu_t_t35_terminal_provider_code.
Raphaël Zumer March 2, 2023, 7:14 p.m. UTC | #4
On 3/2/23 13:57, James Almer wrote:
>  > The SEI for HEVC does keep the country code in the payload, but not AV1.
> Are you sure about HEVC? I just checked a sample and trace_headers 
> reported this:
>
> [trace_headers] Prefix Supplemental Enhancement Information
> [trace_headers] 0  forbidden_zero_bit               0 = 0
> [trace_headers] 1  nal_unit_type               100111 = 39
> [trace_headers] 7  nuh_layer_id                000000 = 0
> [trace_headers] 13 nuh_temporal_id_plus1          001 = 1
> [trace_headers] 16 last_payload_type_byte    00000100 = 4
> [trace_headers] 24 last_payload_size_byte    01000000 = 64
> [trace_headers] User Data Registered ITU-T T.35
> [trace_headers] 32 itu_t_t35_country_code    10110101 = 181
> [trace_headers] 40 itu_t_t35_payload_byte[1] 00000000 = 0
> [trace_headers] 48 itu_t_t35_payload_byte[2] 00111100 = 60
> [trace_headers] 56 itu_t_t35_payload_byte[3] 00000000 = 0
> [trace_headers] 64 itu_t_t35_payload_byte[4] 00000001 = 1
> [trace_headers] 72 itu_t_t35_payload_byte[5] 00000100 = 4
> [trace_headers] 80 itu_t_t35_payload_byte[6] 00000001 = 1
>
> Which is in line with section 8.3.1 of ITU-T Rec. H.274 as well as 
> section D.2.6 of ITU-T Rec. H.265.
>
> So i think this function should not set the country code at all as part 
> of the payload, and start with itu_t_t35_terminal_provider_code.

OK, I will make that change since both HEVC and AV1 implementations seem to match.

Thanks
Raphaël Zumer
diff mbox series

Patch

diff --git a/libavutil/hdr_dynamic_metadata.c b/libavutil/hdr_dynamic_metadata.c
index 98f399b032..72d310e633 100644
--- a/libavutil/hdr_dynamic_metadata.c
+++ b/libavutil/hdr_dynamic_metadata.c
@@ -225,3 +225,150 @@  int av_dynamic_hdr_plus_from_t35(AVDynamicHDRPlus *s, const uint8_t *data,
 
     return 0;
 }
+
+AVBufferRef *av_dynamic_hdr_plus_to_t35(AVDynamicHDRPlus *s)
+{
+    AVBufferRef *buf;
+    size_t size_bits, size_bytes;
+    PutBitContext pbc, *pb = &pbc;
+
+    if (!s)
+        return NULL;
+
+    // Buffer size per CTA-861-H p.253-254:
+    size_bits =
+    // 56 bits for the header
+    58 +
+    // 2 bits for num_windows
+    2 +
+    // 937 bits for window geometry for each window above 1
+    FFMAX((s->num_windows - 1), 0) * 937 +
+    // 27 bits for targeted_system_display_maximum_luminance
+    27 +
+    // 1-3855 bits for targeted system display peak luminance information
+    1 + (s->targeted_system_display_actual_peak_luminance_flag ? 10 +
+        s->num_rows_targeted_system_display_actual_peak_luminance *
+        s->num_cols_targeted_system_display_actual_peak_luminance * 4 : 0) +
+    // 0-442 bits for intra-window pixel distribution information
+    s->num_windows * 82;
+    for (int w = 0; w < s->num_windows; w++) {
+        size_bits += s->params[w].num_distribution_maxrgb_percentiles * 24;
+    }
+    // 1-3855 bits for mastering display peak luminance information
+    size_bits += 1 + (s->mastering_display_actual_peak_luminance_flag ? 10 +
+        s->num_rows_mastering_display_actual_peak_luminance *
+        s->num_cols_mastering_display_actual_peak_luminance * 4 : 0) +
+    // 0-537 bits for per-window tonemapping information
+    s->num_windows * 1;
+    for (int w = 0; w < s->num_windows; w++) {
+        if (s->params[w].tone_mapping_flag) {
+            size_bits += 28 + s->params[w].num_bezier_curve_anchors * 10;
+        }
+    }
+    // 0-21 bits for per-window color saturation mapping information
+    size_bits += s->num_windows * 1;
+    for (int w = 0; w < s->num_windows; w++) {
+        if (s->params[w].color_saturation_mapping_flag) {
+            size_bits += 6;
+        }
+    }
+
+    size_bytes = (size_bits + 7) / 8;
+
+    buf = av_buffer_alloc(size_bytes);
+    if (!buf) {
+        return NULL;
+    }
+
+    init_put_bits(pb, buf->data, size_bytes);
+
+    // itu_t_t35_country_code shall be 0xB5 (USA)
+    put_bits(pb, 8, 0xB5);
+    // itu_t_t35_terminal_provider_code shall be 0x003C
+    put_bits(pb, 16, 0x003C);
+    // itu_t_t35_terminal_provider_oriented_code is set to ST 2094-40
+    put_bits(pb, 16, 0x0001);
+    // application_identifier shall be set to 4
+    put_bits(pb, 8, 4);
+    // application_mode is set to Application Version 1
+    put_bits(pb, 8, 1);
+
+    // Payload as per CTA-861-H p.253-254
+    put_bits(pb, 2, s->num_windows);
+
+    for (int w = 1; w < s->num_windows; w++) {
+        put_bits(pb, 16, s->params[w].window_upper_left_corner_x.num / s->params[w].window_upper_left_corner_x.den);
+        put_bits(pb, 16, s->params[w].window_upper_left_corner_y.num / s->params[w].window_upper_left_corner_y.den);
+        put_bits(pb, 16, s->params[w].window_lower_right_corner_x.num / s->params[w].window_lower_right_corner_x.den);
+        put_bits(pb, 16, s->params[w].window_lower_right_corner_y.num / s->params[w].window_lower_right_corner_y.den);
+        put_bits(pb, 16, s->params[w].center_of_ellipse_x);
+        put_bits(pb, 16, s->params[w].center_of_ellipse_y);
+        put_bits(pb, 8, s->params[w].rotation_angle);
+        put_bits(pb, 16, s->params[w].semimajor_axis_internal_ellipse);
+        put_bits(pb, 16, s->params[w].semimajor_axis_external_ellipse);
+        put_bits(pb, 16, s->params[w].semiminor_axis_external_ellipse);
+        put_bits(pb, 1, s->params[w].overlap_process_option);
+    }
+
+    put_bits(pb, 27, s->targeted_system_display_maximum_luminance.num * luminance_den /
+        s->targeted_system_display_maximum_luminance.den);
+    put_bits(pb, 1, s->targeted_system_display_actual_peak_luminance_flag);
+    if (s->targeted_system_display_actual_peak_luminance_flag) {
+        put_bits(pb, 5, s->num_rows_targeted_system_display_actual_peak_luminance);
+        put_bits(pb, 5, s->num_cols_targeted_system_display_actual_peak_luminance);
+        for (int i = 0; i < s->num_rows_targeted_system_display_actual_peak_luminance; i++) {
+            for (int j = 0; j < s->num_cols_targeted_system_display_actual_peak_luminance; j++) {
+                put_bits(pb, 4, s->targeted_system_display_actual_peak_luminance[i][j].num * peak_luminance_den /
+                    s->targeted_system_display_actual_peak_luminance[i][j].den);
+            }
+        }
+    }
+
+    for (int w = 0; w < s->num_windows; w++) {
+        for (int i = 0; i < 3; i++) {
+            put_bits(pb, 17, s->params[w].maxscl[i].num * rgb_den / s->params[w].maxscl[i].den);
+        }
+        put_bits(pb, 17, s->params[w].average_maxrgb.num * rgb_den / s->params[w].average_maxrgb.den);
+        put_bits(pb, 4, s->params[w].num_distribution_maxrgb_percentiles);
+        for (int i = 0; i < s->params[w].num_distribution_maxrgb_percentiles; i++) {
+            put_bits(pb, 7, s->params[w].distribution_maxrgb[i].percentage);
+            put_bits(pb, 17, s->params[w].distribution_maxrgb[i].percentile.num * rgb_den /
+                s->params[w].distribution_maxrgb[i].percentile.den);
+        }
+        put_bits(pb, 10, s->params[w].fraction_bright_pixels.num * fraction_pixel_den /
+            s->params[w].fraction_bright_pixels.den);
+    }
+
+    put_bits(pb, 1, s->mastering_display_actual_peak_luminance_flag);
+    if (s->mastering_display_actual_peak_luminance_flag) {
+        put_bits(pb, 5, s->num_rows_mastering_display_actual_peak_luminance);
+        put_bits(pb, 5, s->num_cols_mastering_display_actual_peak_luminance);
+        for (int i = 0; i < s->num_rows_mastering_display_actual_peak_luminance; i++) {
+            for (int j = 0; j < s->num_cols_mastering_display_actual_peak_luminance; j++) {
+                put_bits(pb, 4, s->mastering_display_actual_peak_luminance[i][j].num * peak_luminance_den /
+                    s->mastering_display_actual_peak_luminance[i][j].den);
+            }
+        }
+    }
+
+    for (int w = 0; w < s->num_windows; w++) {
+        put_bits(pb, 1, s->params[w].tone_mapping_flag);
+        if (s->params[w].tone_mapping_flag) {
+            put_bits(pb, 12, s->params[w].knee_point_x.num * knee_point_den / s->params[w].knee_point_x.den);
+            put_bits(pb, 12, s->params[w].knee_point_y.num * knee_point_den / s->params[w].knee_point_y.den);
+            put_bits(pb, 4, s->params[w].num_bezier_curve_anchors);
+            for (int i = 0; i < s->params[w].num_bezier_curve_anchors; i++) {
+                put_bits(pb, 10, s->params[w].bezier_curve_anchors[i].num * bezier_anchor_den /
+                    s->params[w].bezier_curve_anchors[i].den);
+            }
+            put_bits(pb, 1, s->params[w].color_saturation_mapping_flag);
+            if (s->params[w].color_saturation_mapping_flag) {
+                put_bits(pb, 6, s->params[w].color_saturation_weight.num * saturation_weight_den /
+                    s->params[w].color_saturation_weight.den);
+            }
+        }
+    }
+
+    flush_put_bits(pb);
+    return buf;
+}
diff --git a/libavutil/hdr_dynamic_metadata.h b/libavutil/hdr_dynamic_metadata.h
index 1f953ef1f5..f75d3e0739 100644
--- a/libavutil/hdr_dynamic_metadata.h
+++ b/libavutil/hdr_dynamic_metadata.h
@@ -21,6 +21,7 @@ 
 #ifndef AVUTIL_HDR_DYNAMIC_METADATA_H
 #define AVUTIL_HDR_DYNAMIC_METADATA_H
 
+#include "buffer.h"
 #include "frame.h"
 #include "rational.h"
 
@@ -351,4 +352,13 @@  AVDynamicHDRPlus *av_dynamic_hdr_plus_create_side_data(AVFrame *frame);
 int av_dynamic_hdr_plus_from_t35(AVDynamicHDRPlus *s, const uint8_t *data,
                                  int size);
 
+/**
+ * Serialize dynamic HDR10+ metadata to a user data registered ITU-T T.35 buffer.
+ * @param s A pointer containing the decoded AVDynamicHDRPlus structure.
+ *
+ * @return Pointer to an AVBufferRef containing the raw ITU-T T.35 representation
+ *         of the HDR10+ metadata if succeed, or NULL if buffer allocation fails.
+ */
+AVBufferRef *av_dynamic_hdr_plus_to_t35(AVDynamicHDRPlus *s);
+
 #endif /* AVUTIL_HDR_DYNAMIC_METADATA_H */
diff --git a/libavutil/version.h b/libavutil/version.h
index 900b798971..7635672985 100644
--- a/libavutil/version.h
+++ b/libavutil/version.h
@@ -79,7 +79,7 @@ 
  */
 
 #define LIBAVUTIL_VERSION_MAJOR  58
-#define LIBAVUTIL_VERSION_MINOR   3
+#define LIBAVUTIL_VERSION_MINOR   4
 #define LIBAVUTIL_VERSION_MICRO 100
 
 #define LIBAVUTIL_VERSION_INT   AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \