diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 43a54caba8..9133afcd0e 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -1225,6 +1225,7 @@ TESTPROGS = avpacket                                                    \
             utils                                                       \
 
 TESTPROGS-$(CONFIG_CABAC)                 += cabac
+TESTPROGS-$(CONFIG_CBS)                   += cbs_metadata
 TESTPROGS-$(CONFIG_DCT)                   += avfft
 TESTPROGS-$(CONFIG_FFT)                   += fft fft-fixed32
 TESTPROGS-$(CONFIG_GOLOMB)                += golomb
diff --git a/libavcodec/tests/cbs_metadata.c b/libavcodec/tests/cbs_metadata.c
new file mode 100644
index 0000000000..260a32ce7a
--- /dev/null
+++ b/libavcodec/tests/cbs_metadata.c
@@ -0,0 +1,390 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "config.h"
+
+#include "libavutil/mastering_display_metadata.h"
+#include "libavutil/stereo3d.h"
+
+#include "libavcodec/cbs.h"
+#include "libavcodec/cbs_metadata.h"
+
+typedef struct MetadataTestCodec {
+    enum AVCodecID codec_id;
+    enum CBSMetadataType *types;
+    const uint8_t *input;
+    size_t         input_size;
+} MetadataTestCodec;
+
+typedef struct MetadataTestType {
+    enum CBSMetadataType type;
+    const void *input;
+    int (*compare)(const void *input, const void *output);
+} MetadataTestType;
+
+// These test inputs are a single frame of 64x64 black.
+
+static uint8_t h264_test_stream[] = {
+    0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0a, 0xac, 0xd9, 0x44, 0x26,
+    0xc0, 0x44, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xc8,
+    0x3c, 0x48, 0x96, 0x58, 0x00, 0x00, 0x00, 0x01, 0x68, 0xeb, 0xe3, 0xcb,
+    0x22, 0xc0, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00, 0x2b, 0xff, 0xfe,
+    0xd8, 0xe7, 0xf3, 0x2c, 0xa7, 0xf4, 0xda, 0xbb, 0xf0, 0xac, 0xc4, 0x99,
+    0x8a, 0xa5, 0xc3, 0xab, 0x2f, 0x6b, 0xe0, 0x64, 0x26, 0xdd, 0x1d, 0x09,
+    0x60, 0xb8, 0x0d, 0x60, 0xf6, 0x89
+};
+
+static uint8_t h265_test_stream[] = {
+    0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,
+    0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00,
+    0x1e, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01,
+    0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,
+    0x00, 0x1e, 0xa0, 0x20, 0x81, 0x05, 0x96, 0x56, 0x69, 0x24, 0xca, 0xf0,
+    0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0c, 0x84,
+    0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x72, 0xb4, 0x22, 0x40, 0x00,
+    0x00, 0x01, 0x28, 0x01, 0xaf, 0x1d, 0x80, 0xf7, 0xcf, 0x80, 0xff, 0xf8,
+    0x90, 0xfa, 0x3b, 0x77, 0x87, 0x96, 0x96, 0xbc, 0x7c
+};
+
+static uint8_t av1_test_stream[] = {
+    0x12, 0x00, 0x0a, 0x0a,
+    0x00, 0x00, 0x00, 0x02, 0xaf, 0xff, 0x89, 0x5f, 0x20, 0x08, 0x32, 0x16,
+    0x10, 0x00, 0xa6, 0x80, 0x10, 0x40, 0x82, 0x08, 0x00, 0x00, 0x4b, 0xb7,
+    0x9a, 0x9b, 0x57, 0x72, 0xea, 0xe9, 0x28, 0xb8, 0x15, 0x30
+};
+
+static const MetadataTestCodec test_codecs[] = {
+#if CONFIG_CBS_H264
+    {
+        AV_CODEC_ID_H264,
+        (enum CBSMetadataType[]) {
+            CBS_METADATA_MASTERING_DISPLAY,
+            CBS_METADATA_CONTENT_LIGHT_LEVEL,
+            CBS_METADATA_DISPLAY_MATRIX,
+            CBS_METADATA_STEREO3D,
+            CBS_METADATA_NONE,
+        },
+        h264_test_stream,
+        sizeof(h264_test_stream),
+    },
+#endif
+#if CONFIG_CBS_H265
+    {
+        AV_CODEC_ID_H265,
+        (enum CBSMetadataType[]) {
+            CBS_METADATA_MASTERING_DISPLAY,
+            CBS_METADATA_CONTENT_LIGHT_LEVEL,
+            CBS_METADATA_NONE,
+        },
+        h265_test_stream,
+        sizeof(h265_test_stream),
+    },
+#endif
+#if CONFIG_CBS_AV1
+    {
+        AV_CODEC_ID_AV1,
+        (enum CBSMetadataType[]) {
+            CBS_METADATA_MASTERING_DISPLAY,
+            CBS_METADATA_CONTENT_LIGHT_LEVEL,
+            CBS_METADATA_NONE,
+        },
+        av1_test_stream,
+        sizeof(av1_test_stream),
+    },
+#endif
+};
+
+#define VALUE_RATIONAL(name) do { \
+        AVRational diff = av_sub_q(a->name, b->name); \
+        diff.num = FFABS(diff.num); \
+        if (av_cmp_q(diff, av_make_q(1, 25000)) >= 0) { \
+            av_log(NULL, AV_LOG_ERROR, \
+                   "%s differs by %d/%d after reading back.\n", \
+                   #name, diff.num, diff.den); \
+            return 1; \
+        } \
+    } while (0)
+
+#define VALUE_ENUM(name) do { \
+        if (a->name != b->name) { \
+            av_log(NULL, AV_LOG_ERROR, \
+                   "%s differs after reading back.\n", #name); \
+            return 1; \
+        } \
+    } while (0)
+
+#define VALUE_UINT(name) do { \
+        unsigned int diff; \
+        if (a->name > b->name) \
+            diff = a->name - b->name; \
+        else \
+            diff = b->name - a->name; \
+        if (diff > 1) { \
+            av_log(NULL, AV_LOG_ERROR, \
+                   "%s differs by %d after reading back.\n", \
+                   #name, diff); \
+            return 1; \
+        } \
+    } while (0)
+
+static int compare_mastering_display_metadata(const void *va,
+                                              const void *vb)
+{
+    const AVMasteringDisplayMetadata *a = va, *b = vb;
+    VALUE_RATIONAL(display_primaries[0][0]);
+    VALUE_RATIONAL(display_primaries[0][1]);
+    VALUE_RATIONAL(display_primaries[1][0]);
+    VALUE_RATIONAL(display_primaries[1][1]);
+    VALUE_RATIONAL(display_primaries[2][0]);
+    VALUE_RATIONAL(display_primaries[2][1]);
+    VALUE_RATIONAL(white_point[0]);
+    VALUE_RATIONAL(white_point[1]);
+    VALUE_RATIONAL(min_luminance);
+    VALUE_RATIONAL(max_luminance);
+    return 0;
+}
+
+static int compare_content_light_level_metadata(const void *va,
+                                                const void *vb)
+{
+    const AVContentLightMetadata *a = va, *b = vb;
+    VALUE_UINT(MaxCLL);
+    VALUE_UINT(MaxFALL);
+    return 0;
+}
+
+static int compare_display_matrix(const void *va,
+                                  const void *vb)
+{
+    const int32_t *a = va, *b = vb;
+    for (int i = 0; i < 9; i++) {
+        int diff = FFABS(a[i] - b[i]);
+        if (diff > 1) {
+            av_log(NULL, AV_LOG_ERROR,
+                   "Matrix %d differs by %d after reading back.\n",
+                   i, diff);
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int compare_stereo3d(const void *va,
+                            const void *vb)
+{
+    const AVStereo3D *a = va, *b = vb;
+    VALUE_ENUM(type);
+    VALUE_ENUM(view);
+    return 0;
+}
+
+static const MetadataTestType test_types[] = {
+    {
+        CBS_METADATA_MASTERING_DISPLAY,
+        &(AVMasteringDisplayMetadata) {
+            // This test colour space is BT.2020.
+            .display_primaries = {
+                { { 708, 1000 }, { 292, 1000 }, },
+                { { 170, 1000 }, { 797, 1000 }, },
+                { { 131, 1000 }, {  46, 1000 }, },
+            },
+            .white_point = {
+                { 3127, 10000 }, { 3290, 10000 },
+            },
+            // These are arbitrary numbers in a plausible range.
+            .min_luminance = {   56, 100 },
+            .max_luminance = { 1234, 1   },
+            .has_primaries = 1,
+            .has_luminance = 1,
+        },
+        &compare_mastering_display_metadata,
+    },
+    {
+        CBS_METADATA_CONTENT_LIGHT_LEVEL,
+        &(AVContentLightMetadata) {
+            .MaxCLL  = 1234,
+            .MaxFALL = 567,
+        },
+        &compare_content_light_level_metadata,
+    },
+    {
+        CBS_METADATA_DISPLAY_MATRIX,
+        &(int32_t[9]) {
+            // This is a quarter-turn clockwise.
+            0,              -1 * (1 << 16),  0,
+            1 * (1 << 16),   0,              0,
+            0,               0,              1 << 30,
+        },
+        &compare_display_matrix,
+    },
+    {
+        CBS_METADATA_STEREO3D,
+        &(AVStereo3D) {
+            .type  = AV_STEREO3D_SIDEBYSIDE,
+            .flags = 0,
+            .view  = AV_STEREO3D_VIEW_PACKED,
+        },
+        &compare_stereo3d,
+    },
+};
+
+#define CHECK(op) do { \
+        if ((op) < 0) { \
+            av_log(NULL, AV_LOG_ERROR, "%s failed.\n", #op); \
+            return 1; \
+        } \
+    } while (0)
+
+int main(void)
+{
+    // None of these tests care about any persistent header information,
+    // so we use the same context for all reading/writing.
+    CodedBitstreamContext *cbc;
+    CodedBitstreamFragment frag, frag2;
+    AVPacket *pkt;
+    int c, t, i;
+
+    memset(&frag,  0, sizeof(frag));
+    memset(&frag2, 0, sizeof(frag2));
+
+    pkt = av_packet_alloc();
+    if (!pkt)
+        return 1;
+
+    for (c = 0; c < FF_ARRAY_ELEMS(test_codecs); c++) {
+        const MetadataTestCodec *test_codec;
+        const AVCodecDescriptor *codec_desc;
+
+        test_codec = &test_codecs[c];
+        codec_desc = avcodec_descriptor_get(test_codec->codec_id);
+        if (!codec_desc) {
+            av_log(NULL, AV_LOG_ERROR, "Invalid codec %d used in test.\n",
+                   test_codec->codec_id);
+            return 1;
+        }
+
+        av_log(NULL, AV_LOG_INFO,
+               "Testing codec %d (%s):\n",
+               test_codec->codec_id, codec_desc->name);
+
+        CHECK(ff_cbs_init(&cbc, test_codec->codec_id, NULL));
+
+        // Having tracing enabled on the CBS read/write operations may be
+        // helpful when debug problems found during this test.
+        if (0) {
+            cbc->trace_enable = 1;
+            cbc->trace_level  = AV_LOG_INFO;
+        }
+
+        CHECK(ff_cbs_read(cbc, &frag,
+                          test_codec->input, test_codec->input_size));
+
+        // Verify no change.
+        CHECK(ff_cbs_write_packet(cbc, pkt, &frag));
+        if (memcmp(pkt->data, test_codec->input, test_codec->input_size)) {
+            av_log(NULL, AV_LOG_ERROR,
+                   "Passthrough output does not match.\n");
+            return 1;
+        }
+
+        for (t = 0; t < FF_ARRAY_ELEMS(test_types); t++) {
+            const MetadataTestType *test_type;
+            const CBSMetadataTypeDescriptor *type_desc;
+            void *output;
+
+            test_type = &test_types[t];
+            type_desc = ff_cbs_metadata_find_type(test_type->type);
+            if (!type_desc) {
+                av_log(NULL, AV_LOG_ERROR, "Invalid metadata type %d "
+                       "used in test.\n", test_type->type);
+                return 1;
+            }
+
+            for (i = 0; test_codec->types[i] != CBS_METADATA_NONE; i++) {
+                if (test_codec->types[i] == test_type->type)
+                    break;
+            }
+            if (test_codec->types[i] != test_type->type) {
+                // Type not supported by this codec.
+                continue;
+            }
+
+            av_log(NULL, AV_LOG_INFO,
+                   "Testing metadata type %d (%s) with codec %d (%s).\n",
+                   test_type->type, type_desc->name,
+                   test_codec->codec_id, codec_desc->name);
+
+            // Insert the metadata into the packet.
+            CHECK(ff_cbs_insert_metadata(cbc, &frag,
+                                         test_type->type,
+                                         test_type->input));
+
+            // Write packet containing the new metadata.
+            CHECK(ff_cbs_write_packet(cbc, pkt, &frag));
+
+            // Ensure that that actually did something.
+            if (pkt->size == test_codec->input_size &&
+                memcmp(pkt->data, test_codec->input,
+                       test_codec->input_size) == 0) {
+                av_log(NULL, AV_LOG_ERROR,
+                       "Output not changed by metadata addition.\n");
+                return 1;
+            }
+
+            // Read packet back into a different fragment.
+            CHECK(ff_cbs_read_packet(cbc, &frag2, pkt));
+
+            // Make a new instance of the same structure to extract to.
+            output = ff_cbs_alloc_metadata(test_type->type, NULL);
+            if (!output)
+                return 1;
+
+            // Extract the metadata from the new fragment.
+            CHECK(ff_cbs_extract_metadata(cbc, &frag2,
+                                          test_type->type, output));
+
+            // Make sure the result is sensible.
+            if (test_type->compare(test_type->input, output)) {
+                av_log(NULL, AV_LOG_ERROR,
+                       "Output metadata does not match input.\n");
+                return 1;
+            }
+
+            av_free(output);
+            ff_cbs_fragment_free(&frag2);
+
+            // Remove the metadata from the fragment again.
+            CHECK(ff_cbs_remove_metadata(cbc, &frag, test_type->type));
+
+            // Write it back to make sure it's been removed.
+            CHECK(ff_cbs_write_packet(cbc, pkt, &frag));
+            if (pkt->size != test_codec->input_size ||
+                memcmp(pkt->data, test_codec->input,
+                       test_codec->input_size)) {
+                av_log(NULL, AV_LOG_ERROR,
+                       "Output changed after adding/removing metadata.\n");
+                return 1;
+            }
+        }
+
+        ff_cbs_fragment_free(&frag);
+        ff_cbs_close(&cbc);
+    }
+
+    return 0;
+}
diff --git a/tests/fate/libavcodec.mak b/tests/fate/libavcodec.mak
index 747dae3704..e303e46b48 100644
--- a/tests/fate/libavcodec.mak
+++ b/tests/fate/libavcodec.mak
@@ -8,6 +8,11 @@ fate-cabac: libavcodec/tests/cabac$(EXESUF)
 fate-cabac: CMD = run libavcodec/tests/cabac$(EXESUF)
 fate-cabac: CMP = null
 
+FATE_LIBAVCODEC-$(CONFIG_CBS) += fate-cbs-metadata
+fate-cbs-metadata: libavcodec/tests/cbs_metadata$(EXESUF)
+fate-cbs-metadata: CMD = run libavcodec/tests/cbs_metadata$(EXESUF)
+fate-cbs-metadata: CMP = null
+
 FATE_LIBAVCODEC-yes += fate-celp_math
 fate-celp_math: libavcodec/tests/celp_math$(EXESUF)
 fate-celp_math: CMD = run libavcodec/tests/celp_math$(EXESUF)
