h265parser: Add a helper method to create SEI nal unit
authorSeungha Yang <seungha@centricular.com>
Thu, 19 Mar 2020 09:25:18 +0000 (18:25 +0900)
committerGStreamer Merge Bot <gitlab-merge-bot@gstreamer-foundation.org>
Thu, 2 Apr 2020 09:20:11 +0000 (09:20 +0000)
Add an API to create raw SEI nal unit. This would be useful in case
an user want to create SEI nal data and inject the SEI nal data
into bitstream.

gst-libs/gst/codecparsers/gsth265parser.c
gst-libs/gst/codecparsers/gsth265parser.h
tests/check/libs/h265parser.c

index 41d1d85..6217ca1 100644 (file)
@@ -3371,3 +3371,561 @@ gst_h265_profile_from_string (const gchar * string)
 
   return GST_H265_PROFILE_INVALID;
 }
+
+static gboolean
+gst_h265_write_sei_registered_user_data (NalWriter * nw,
+    GstH265RegisteredUserData * rud)
+{
+  WRITE_UINT8 (nw, rud->country_code, 8);
+  if (rud->country_code == 0xff)
+    WRITE_UINT8 (nw, rud->country_code_extension, 8);
+
+  WRITE_BYTES (nw, rud->data, rud->size);
+
+  return TRUE;
+
+error:
+  return FALSE;
+}
+
+static gboolean
+gst_h265_write_sei_time_code (NalWriter * nw, GstH265TimeCode * tc)
+{
+  gint i;
+
+  WRITE_UINT8 (nw, tc->num_clock_ts, 2);
+
+  for (i = 0; i < tc->num_clock_ts; i++) {
+    WRITE_UINT8 (nw, tc->clock_timestamp_flag[i], 1);
+    if (tc->clock_timestamp_flag[i]) {
+      WRITE_UINT8 (nw, tc->units_field_based_flag[i], 1);
+      WRITE_UINT8 (nw, tc->counting_type[i], 5);
+      WRITE_UINT8 (nw, tc->full_timestamp_flag[i], 1);
+      WRITE_UINT8 (nw, tc->discontinuity_flag[i], 1);
+      WRITE_UINT8 (nw, tc->cnt_dropped_flag[i], 1);
+      WRITE_UINT16 (nw, tc->n_frames[i], 9);
+
+      if (tc->full_timestamp_flag[i]) {
+        WRITE_UINT8 (nw, tc->seconds_value[i], 6);
+        WRITE_UINT8 (nw, tc->minutes_value[i], 6);
+        WRITE_UINT8 (nw, tc->hours_value[i], 5);
+      } else {
+        WRITE_UINT8 (nw, tc->seconds_flag[i], 1);
+        if (tc->seconds_flag[i]) {
+          WRITE_UINT8 (nw, tc->seconds_value[i], 6);
+          WRITE_UINT8 (nw, tc->minutes_flag[i], 1);
+          if (tc->minutes_flag[i]) {
+            WRITE_UINT8 (nw, tc->minutes_value[i], 6);
+            WRITE_UINT8 (nw, tc->hours_flag[i], 1);
+            if (tc->hours_flag[i]) {
+              WRITE_UINT8 (nw, tc->hours_value[i], 5);
+            }
+          }
+        }
+      }
+    }
+
+    WRITE_UINT8 (nw, tc->time_offset_length[i], 5);
+
+    if (tc->time_offset_length[i] > 0)
+      WRITE_UINT8 (nw, tc->time_offset_value[i], tc->time_offset_length[i]);
+  }
+
+  return TRUE;
+
+error:
+  return FALSE;
+}
+
+static gboolean
+gst_h265_write_sei_mastering_display_colour_volume (NalWriter * nw,
+    GstH265MasteringDisplayColourVolume * mdcv)
+{
+  gint i;
+
+  for (i = 0; i < 3; i++) {
+    WRITE_UINT16 (nw, mdcv->display_primaries_x[i], 16);
+    WRITE_UINT16 (nw, mdcv->display_primaries_y[i], 16);
+  }
+
+  WRITE_UINT16 (nw, mdcv->white_point_x, 16);
+  WRITE_UINT16 (nw, mdcv->white_point_y, 16);
+  WRITE_UINT32 (nw, mdcv->max_display_mastering_luminance, 32);
+  WRITE_UINT32 (nw, mdcv->min_display_mastering_luminance, 32);
+
+  return TRUE;
+
+error:
+  return FALSE;
+}
+
+static gboolean
+gst_h265_write_sei_content_light_level_info (NalWriter * nw,
+    GstH265ContentLightLevel * cll)
+{
+  WRITE_UINT16 (nw, cll->max_content_light_level, 16);
+  WRITE_UINT16 (nw, cll->max_pic_average_light_level, 16);
+
+  return TRUE;
+
+error:
+  return FALSE;
+}
+
+static GstMemory *
+gst_h265_create_sei_memory_internal (guint8 layer_id, guint8 temporal_id_plus1,
+    guint nal_prefix_size, gboolean packetized, GArray * messages)
+{
+  NalWriter nw;
+  gint i;
+  gboolean have_written_data = FALSE;
+
+  nal_writer_init (&nw, nal_prefix_size, packetized);
+
+  if (messages->len == 0)
+    goto error;
+
+  GST_DEBUG ("Create SEI nal from array, len: %d", messages->len);
+
+  /* nal header */
+  /* forbidden_zero_bit */
+  WRITE_UINT8 (&nw, 0, 1);
+  /* nal_unit_type */
+  WRITE_UINT8 (&nw, GST_H265_NAL_PREFIX_SEI, 6);
+  /* nuh_layer_id */
+  WRITE_UINT8 (&nw, layer_id, 6);
+  /* nuh_temporal_id_plus1 */
+  WRITE_UINT8 (&nw, temporal_id_plus1, 3);
+
+  for (i = 0; i < messages->len; i++) {
+    GstH265SEIMessage *msg = &g_array_index (messages, GstH265SEIMessage, i);
+    guint32 payload_size_data = 0;
+    guint32 payload_size_in_bits = 0;
+    guint32 payload_type_data = msg->payloadType;
+    gboolean need_align = FALSE;
+
+    switch (payload_type_data) {
+      case GST_H265_SEI_REGISTERED_USER_DATA:{
+        GstH265RegisteredUserData *rud = &msg->payload.registered_user_data;
+
+        /* itu_t_t35_country_code: 8 bits */
+        payload_size_data = 1;
+        if (rud->country_code == 0xff) {
+          /* itu_t_t35_country_code_extension_byte */
+          payload_size_data++;
+        }
+
+        payload_size_data += rud->size;
+        break;
+      }
+      case GST_H265_SEI_TIME_CODE:{
+        gint j;
+        GstH265TimeCode *tc = &msg->payload.time_code;
+        /* num_clock_ts: 2 bits */
+        payload_size_in_bits = 2;
+        for (j = 0; j < tc->num_clock_ts; j++) {
+          /* clock_timestamp_flag: 1 bit */
+          payload_size_in_bits += 1;
+
+          if (tc->clock_timestamp_flag[j]) {
+            /* units_field_based_flag: 1 bit
+             * counting_type: 5 bits
+             * full_timestamp_flag: 1 bit
+             * discontinuity_flag: 1 bit
+             * cnt_dropped_flag: 1 bit
+             * n_frames: 9 bit
+             */
+            payload_size_in_bits += 18;
+
+            if (tc->full_timestamp_flag[j]) {
+              /* seconds_value: 6 bits
+               * minutes_value: 6 bits
+               * hours_value: 5 bits
+               */
+              payload_size_in_bits += 17;
+            } else {
+              /* seconds_flag: 1 bit */
+              payload_size_in_bits += 1;
+
+              if (tc->seconds_flag[j]) {
+                /* seconds_value: 6 bits
+                 * minutes_flag: 1 bit
+                 */
+                payload_size_in_bits += 7;
+
+                if (tc->minutes_flag[j]) {
+                  /* minutes_value: 6 bits
+                   * hours_flag: 1 bit
+                   */
+                  payload_size_in_bits += 7;
+                  if (tc->hours_flag[j]) {
+                    /* hours_value: 5 bits */
+                    payload_size_in_bits += 5;
+                  }
+                }
+              }
+            }
+
+            /* time_offset_length: 5bits
+             * time_offset_value: time_offset_length bits
+             */
+            payload_size_in_bits += (5 + tc->time_offset_length[j]);
+          }
+        }
+
+        payload_size_data = payload_size_in_bits >> 3;
+
+        if ((payload_size_in_bits & 0x7) != 0) {
+          GST_INFO ("Bits for Time Code SEI is not byte aligned");
+          payload_size_data++;
+          need_align = TRUE;
+        }
+        break;
+      }
+      case GST_H265_SEI_MASTERING_DISPLAY_COLOUR_VOLUME:
+        /* x, y 16 bits per RGB channel
+         * x, y 16 bits white point
+         * max, min luminance 32 bits
+         *
+         * (2 * 2 * 3) + (2 * 2) + (4 * 2) = 24 bytes
+         */
+        payload_size_data = 24;
+        break;
+      case GST_H265_SEI_CONTENT_LIGHT_LEVEL:
+        /* maxCLL and maxFALL per 16 bits
+         *
+         * 2 * 2 = 4 bytes
+         */
+        payload_size_data = 4;
+        break;
+      default:
+        break;
+    }
+
+    if (payload_size_data == 0) {
+      GST_FIXME ("Unsupported SEI type %d", msg->payloadType);
+      continue;
+    }
+
+    /* write payload type bytes */
+    while (payload_type_data >= 0xff) {
+      WRITE_UINT8 (&nw, 0xff, 8);
+      payload_type_data -= -0xff;
+    }
+    WRITE_UINT8 (&nw, payload_type_data, 8);
+
+    /* write payload size bytes */
+    while (payload_size_data >= 0xff) {
+      WRITE_UINT8 (&nw, 0xff, 8);
+      payload_size_data -= -0xff;
+    }
+    WRITE_UINT8 (&nw, payload_size_data, 8);
+
+    switch (msg->payloadType) {
+      case GST_H265_SEI_REGISTERED_USER_DATA:
+        GST_DEBUG ("Writing \"Registered user data\" done");
+        if (!gst_h265_write_sei_registered_user_data (&nw,
+                &msg->payload.registered_user_data)) {
+          GST_WARNING ("Failed to write \"Registered user data\"");
+          goto error;
+        }
+        have_written_data = TRUE;
+        break;
+      case GST_H265_SEI_TIME_CODE:
+        GST_DEBUG ("Wrtiting \"Time code\"");
+        if (!gst_h265_write_sei_time_code (&nw, &msg->payload.time_code)) {
+          GST_WARNING ("Failed to write \"Time code\"");
+          goto error;
+        }
+        have_written_data = TRUE;
+        break;
+      case GST_H265_SEI_MASTERING_DISPLAY_COLOUR_VOLUME:
+        GST_DEBUG ("Wrtiting \"Mastering display colour volume\"");
+        if (!gst_h265_write_sei_mastering_display_colour_volume (&nw,
+                &msg->payload.mastering_display_colour_volume)) {
+          GST_WARNING ("Failed to write \"Mastering display colour volume\"");
+          goto error;
+        }
+        have_written_data = TRUE;
+        break;
+      case GST_H265_SEI_CONTENT_LIGHT_LEVEL:
+        GST_DEBUG ("Writing \"Content light level\" done");
+        if (!gst_h265_write_sei_content_light_level_info (&nw,
+                &msg->payload.content_light_level)) {
+          GST_WARNING ("Failed to write \"Content light level\"");
+          goto error;
+        }
+        have_written_data = TRUE;
+        break;
+      default:
+        break;
+    }
+
+    if (need_align && !nal_writer_do_rbsp_trailing_bits (&nw)) {
+      GST_WARNING ("Cannot insert traling bits");
+      goto error;
+    }
+  }
+
+  if (!have_written_data) {
+    GST_WARNING ("No written sei data");
+    goto error;
+  }
+
+  if (!nal_writer_do_rbsp_trailing_bits (&nw)) {
+    GST_WARNING ("Failed to insert rbsp trailing bits");
+    goto error;
+  }
+
+  return nal_writer_reset_and_get_memory (&nw);
+
+error:
+  nal_writer_reset (&nw);
+
+  return NULL;
+}
+
+/**
+ * gst_h265_create_sei_memory:
+ * @layer_id: a nal unit layer id
+ * @temporal_id_plus1: a nal unit temporal identifier
+ * @start_code_prefix_length: a length of start code prefix, must be 3 or 4
+ * @messages: (transfer none): a GArray of #GstH265SEIMessage
+ *
+ * Creates raw byte-stream format (a.k.a Annex B type) SEI nal unit data
+ * from @messages
+ *
+ * Returns: a #GstMemory containing a PREFIX SEI nal unit
+ *
+ * Since: 1.18
+ */
+GstMemory *
+gst_h265_create_sei_memory (guint8 layer_id, guint8 temporal_id_plus1,
+    guint8 start_code_prefix_length, GArray * messages)
+{
+  g_return_val_if_fail (start_code_prefix_length == 3
+      || start_code_prefix_length == 4, NULL);
+  g_return_val_if_fail (messages != NULL, NULL);
+  g_return_val_if_fail (messages->len > 0, NULL);
+
+  return gst_h265_create_sei_memory_internal (layer_id, temporal_id_plus1,
+      start_code_prefix_length, FALSE, messages);
+}
+
+/**
+ * gst_h265_create_sei_memory_hevc:
+ * @layer_id: a nal unit layer id
+ * @temporal_id_plus1: a nal unit temporal identifier
+ * @nal_length_size: a size of nal length field, allowed range is [1, 4]
+ * @messages: (transfer none): a GArray of #GstH265SEIMessage
+ *
+ * Creates raw packetized format SEI nal unit data from @messages
+ *
+ * Returns: a #GstMemory containing a PREFIX SEI nal unit
+ *
+ * Since: 1.18
+ */
+GstMemory *
+gst_h265_create_sei_memory_hevc (guint8 layer_id, guint8 temporal_id_plus1,
+    guint8 nal_length_size, GArray * messages)
+{
+  return gst_h265_create_sei_memory_internal (layer_id, temporal_id_plus1,
+      nal_length_size, TRUE, messages);
+}
+
+static GstBuffer *
+gst_h265_parser_insert_sei_internal (GstH265Parser * parser,
+    guint8 nal_prefix_size, gboolean packetized, GstBuffer * au,
+    GstMemory * sei)
+{
+  GstH265NalUnit nalu;
+  GstH265NalUnit sei_nalu;
+  GstMapInfo info;
+  GstMapInfo sei_info;
+  GstH265ParserResult pres;
+  guint offset = 0;
+  GstBuffer *new_buffer = NULL;
+  GstMemory *new_mem = NULL;
+
+  /* all SEI payload types supported by us need to have the identical
+   * temporal id to that of slice. Parse SEI first and we will
+   * update it if it's required */
+  if (!gst_memory_map (sei, &sei_info, GST_MAP_READ)) {
+    GST_ERROR ("Cannot map sei memory");
+    return NULL;
+  }
+
+  if (packetized) {
+    pres = gst_h265_parser_identify_nalu_hevc (parser,
+        sei_info.data, 0, sei_info.size, nal_prefix_size, &sei_nalu);
+  } else {
+    pres = gst_h265_parser_identify_nalu (parser,
+        sei_info.data, 0, sei_info.size, &sei_nalu);
+  }
+  gst_memory_unmap (sei, &sei_info);
+  if (pres != GST_H265_PARSER_OK && pres != GST_H265_PARSER_NO_NAL_END) {
+    GST_DEBUG ("Failed to identify sei nal unit, ret: %d", pres);
+    return NULL;
+  }
+
+  if (!gst_buffer_map (au, &info, GST_MAP_READ)) {
+    GST_ERROR ("Cannot map au buffer");
+    return NULL;
+  }
+
+  /* Find the offset of the first slice */
+  do {
+    if (packetized) {
+      pres = gst_h265_parser_identify_nalu_hevc (parser,
+          info.data, offset, info.size, nal_prefix_size, &nalu);
+    } else {
+      pres = gst_h265_parser_identify_nalu (parser,
+          info.data, offset, info.size, &nalu);
+    }
+
+    if (pres != GST_H265_PARSER_OK && pres != GST_H265_PARSER_NO_NAL_END) {
+      GST_DEBUG ("Failed to identify nal unit, ret: %d", pres);
+      gst_buffer_unmap (au, &info);
+
+      return NULL;
+    }
+
+    if ((nalu.type >= GST_H265_NAL_SLICE_TRAIL_N
+            && nalu.type <= GST_H265_NAL_SLICE_RASL_R)
+        || (nalu.type >= GST_H265_NAL_SLICE_BLA_W_LP
+            && nalu.type <= GST_H265_NAL_SLICE_CRA_NUT)) {
+      GST_DEBUG ("Found slice nal type %d at offset %d", nalu.type,
+          nalu.sc_offset);
+      break;
+    }
+
+    offset = nalu.offset + nalu.size;
+  } while (pres == GST_H265_PARSER_OK);
+  gst_buffer_unmap (au, &info);
+
+  /* found the best position now, create new buffer */
+  new_buffer = gst_buffer_new ();
+
+  /* copy all metadata */
+  if (!gst_buffer_copy_into (new_buffer, au, GST_BUFFER_COPY_METADATA, 0, -1)) {
+    GST_ERROR ("Failed to copy metadata into new buffer");
+    gst_clear_buffer (&new_buffer);
+    goto out;
+  }
+
+  /* copy non-slice nal */
+  if (nalu.sc_offset > 0) {
+    if (!gst_buffer_copy_into (new_buffer, au,
+            GST_BUFFER_COPY_MEMORY, 0, nalu.sc_offset)) {
+      GST_ERROR ("Failed to copy buffer");
+      gst_clear_buffer (&new_buffer);
+      goto out;
+    }
+  }
+
+  /* check whether we need to update temporal id and layer id.
+   * If it's not matched to slice nalu, update it.
+   */
+  if (sei_nalu.layer_id != nalu.layer_id || sei_nalu.temporal_id_plus1 !=
+      nalu.temporal_id_plus1) {
+    guint16 nalu_header;
+    guint16 layer_id_temporal_id = 0;
+    new_mem = gst_memory_copy (sei, 0, -1);
+
+    if (!gst_memory_map (new_mem, &sei_info, GST_MAP_READWRITE)) {
+      GST_ERROR ("Failed to map new sei memory");
+      gst_memory_unref (new_mem);
+      gst_clear_buffer (&new_buffer);
+      goto out;
+    }
+
+    nalu_header = GST_READ_UINT16_BE (sei_info.data + sei_nalu.offset);
+
+    /* clear bits 7 ~ 15
+     * NOTE:
+     * bit 0: forbidden_zero_bit
+     * bits 1 ~ 6: nalu type */
+    nalu_header &= 0xfe00;
+
+    layer_id_temporal_id = ((nalu.layer_id << 3) & 0x1f8);
+    layer_id_temporal_id |= (nalu.temporal_id_plus1 & 0x7);
+
+    nalu_header |= layer_id_temporal_id;
+    GST_WRITE_UINT16_BE (sei_info.data + sei_nalu.offset, nalu_header);
+    gst_memory_unmap (new_mem, &sei_info);
+  } else {
+    new_mem = gst_memory_ref (sei);
+  }
+
+  /* insert sei */
+  gst_buffer_append_memory (new_buffer, new_mem);
+
+  /* copy the rest */
+  if (!gst_buffer_copy_into (new_buffer, au,
+          GST_BUFFER_COPY_MEMORY, nalu.sc_offset, -1)) {
+    GST_ERROR ("Failed to copy buffer");
+    gst_clear_buffer (&new_buffer);
+    goto out;
+  }
+
+out:
+  return new_buffer;
+}
+
+/**
+ * gst_h265_parser_insert_sei:
+ * @parser: a #GstH265Parser
+ * @au: (transfer none): a #GstBuffer containing AU data
+ * @sei: (transfer none): a #GstMemory containing a SEI nal
+ *
+ * Copy @au into new #GstBuffer and insert @sei into the #GstBuffer.
+ * The validation for completeness of @au and @sei is caller's responsibility.
+ * Both @au and @sei must be byte-stream formatted
+ *
+ * Returns: (nullable): a SEI inserted #GstBuffer or %NULL
+ *   if cannot figure out proper position to insert a @sei
+ *
+ * Since: 1.18
+ */
+GstBuffer *
+gst_h265_parser_insert_sei (GstH265Parser * parser, GstBuffer * au,
+    GstMemory * sei)
+{
+  g_return_val_if_fail (parser != NULL, NULL);
+  g_return_val_if_fail (GST_IS_BUFFER (au), NULL);
+  g_return_val_if_fail (sei != NULL, NULL);
+
+  /* the size of start code prefix (3 or 4) is not matter since it will be
+   * scanned */
+  return gst_h265_parser_insert_sei_internal (parser, 4, FALSE, au, sei);
+}
+
+/**
+ * gst_h265_parser_insert_sei_hevc:
+ * @parser: a #GstH265Parser
+ * @nal_length_size: a size of nal length field, allowed range is [1, 4]
+ * @au: (transfer none): a #GstBuffer containing AU data
+ * @sei: (transfer none): a #GstMemory containing a SEI nal
+ *
+ * Copy @au into new #GstBuffer and insert @sei into the #GstBuffer.
+ * The validation for completeness of @au and @sei is caller's responsibility.
+ * Nal prefix type of both @au and @sei must be packetized, and
+ * also the size of nal length field must be identical to @nal_length_size
+ *
+ * Returns: (nullable): a SEI inserted #GstBuffer or %NULL
+ *   if cannot figure out proper position to insert a @sei
+ *
+ * Since: 1.18
+ */
+GstBuffer *
+gst_h265_parser_insert_sei_hevc (GstH265Parser * parser, guint8 nal_length_size,
+    GstBuffer * au, GstMemory * sei)
+{
+  g_return_val_if_fail (parser != NULL, NULL);
+  g_return_val_if_fail (nal_length_size > 0 && nal_length_size < 5, NULL);
+  g_return_val_if_fail (GST_IS_BUFFER (au), NULL);
+  g_return_val_if_fail (sei != NULL, NULL);
+
+  return gst_h265_parser_insert_sei_internal (parser, nal_length_size, TRUE,
+      au, sei);
+}
index 2efa959..5b6a9a2 100644 (file)
@@ -1669,5 +1669,28 @@ const gchar * gst_h265_profile_to_string (GstH265Profile profile);
 GST_CODEC_PARSERS_API
 GstH265Profile gst_h265_profile_from_string (const gchar * string);
 
+GST_CODEC_PARSERS_API
+GstMemory * gst_h265_create_sei_memory (guint8 layer_id,
+                                        guint8 temporal_id_plus1,
+                                        guint8 start_code_prefix_length,
+                                        GArray * messages);
+
+GST_CODEC_PARSERS_API
+GstMemory * gst_h265_create_sei_memory_hevc (guint8 layer_id,
+                                             guint8 temporal_id_plus1,
+                                             guint8 nal_length_size,
+                                             GArray * messages);
+
+GST_CODEC_PARSERS_API
+GstBuffer * gst_h265_parser_insert_sei (GstH265Parser * parser,
+                                        GstBuffer * au,
+                                        GstMemory * sei);
+
+GST_CODEC_PARSERS_API
+GstBuffer * gst_h265_parser_insert_sei_hevc (GstH265Parser * parser,
+                                             guint8 nal_length_size,
+                                             GstBuffer * au,
+                                             GstMemory * sei);
+
 G_END_DECLS
 #endif
index 61853cc..ca31eee 100644 (file)
@@ -112,6 +112,21 @@ static guint8 h265_sei_user_data_registered[] = {
   0xa6, 0xae, 0x5c, 0x83, 0x50, 0xdd, 0xf9, 0x8e, 0xc7, 0xbd, 0x00, 0x80
 };
 
+static guint8 h265_sei_time_code[] = {
+  0x00, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x88, 0x06, 0x60, 0x40, 0x00, 0x00, 0x03,
+  0x00, 0x10, 0x80
+};
+
+static guint8 h265_sei_mdcv[] = {
+  0x00, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x89, 0x18, 0x33, 0xc2, 0x86, 0xc4, 0x1d,
+  0x4c, 0x0b, 0xb8, 0x84, 0xd0, 0x3e, 0x80, 0x3d, 0x13, 0x40, 0x42, 0x00, 0x98,
+  0x96, 0x80, 0x00, 0x00, 0x03, 0x00, 0x01, 0x80
+};
+
+static guint8 h265_sei_cll[] = {
+  0x00, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x90, 0x04, 0x03, 0xe8, 0x01, 0x90, 0x80
+};
+
 GST_START_TEST (test_h265_parse_slice_eos_slice_eob)
 {
   GstH265ParserResult res;
@@ -656,6 +671,228 @@ GST_START_TEST (test_h265_sei_registered_user_data)
 
 GST_END_TEST;
 
+typedef gboolean (*SEICheckFunc) (gconstpointer a, gconstpointer b);
+
+static gboolean
+check_sei_user_data_registered (const GstH265RegisteredUserData * a,
+    const GstH265RegisteredUserData * b)
+{
+  if (a->country_code != b->country_code)
+    return FALSE;
+
+  if ((a->country_code == 0xff) &&
+      (a->country_code_extension != b->country_code_extension))
+    return FALSE;
+
+  if (a->size != b->size)
+    return FALSE;
+
+  return !memcmp (a->data, b->data, a->size);
+}
+
+static gboolean
+check_sei_time_code (const GstH265TimeCode * a, const GstH265TimeCode * b)
+{
+  gint i;
+
+  if (a->num_clock_ts != b->num_clock_ts)
+    return FALSE;
+
+  for (i = 0; i < a->num_clock_ts; i++) {
+    if (a->clock_timestamp_flag[i] != b->clock_timestamp_flag[i])
+      return FALSE;
+
+    if (a->clock_timestamp_flag[i]) {
+      if ((a->units_field_based_flag[i] != b->units_field_based_flag[i]) ||
+          (a->counting_type[i] != b->counting_type[i]) ||
+          (a->full_timestamp_flag[i] != b->full_timestamp_flag[i]) ||
+          (a->discontinuity_flag[i] != b->discontinuity_flag[i]) ||
+          (a->cnt_dropped_flag[i] != b->cnt_dropped_flag[i]) ||
+          (a->n_frames[i] != b->n_frames[i])) {
+        return FALSE;
+      }
+
+      if (a->full_timestamp_flag[i]) {
+        if ((a->seconds_value[i] != b->seconds_value[i]) ||
+            (a->minutes_value[i] != b->minutes_value[i]) ||
+            (a->hours_value[i] != b->hours_value[i])) {
+          return FALSE;
+        }
+      } else {
+        if (a->seconds_flag[i] != b->seconds_flag[i])
+          return FALSE;
+
+        if (a->seconds_flag[i]) {
+          if ((a->seconds_value[i] != b->seconds_value[i]) ||
+              (a->minutes_flag[i] != b->minutes_flag[i])) {
+            return FALSE;
+          }
+
+          if (a->minutes_flag[i]) {
+            if ((a->minutes_value[i] != b->minutes_value[i]) ||
+                (a->hours_flag[i] != b->hours_flag[i])) {
+              return FALSE;
+            }
+
+            if (a->hours_flag[i]) {
+              if (a->hours_value[i] != b->hours_value[i])
+                return FALSE;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return TRUE;
+}
+
+static gboolean
+check_sei_mdcv (const GstH265MasteringDisplayColourVolume * a,
+    const GstH265MasteringDisplayColourVolume * b)
+{
+  gint i;
+  for (i = 0; i < 3; i++) {
+    if (a->display_primaries_x[i] != b->display_primaries_x[i] ||
+        a->display_primaries_y[i] != b->display_primaries_y[i])
+      return FALSE;
+  }
+
+  return (a->white_point_x == b->white_point_x) &&
+      (a->white_point_y == b->white_point_y) &&
+      (a->max_display_mastering_luminance == b->max_display_mastering_luminance)
+      && (a->min_display_mastering_luminance ==
+      b->min_display_mastering_luminance);
+}
+
+static gboolean
+check_sei_cll (const GstH265ContentLightLevel * a,
+    const GstH265ContentLightLevel * b)
+{
+  return (a->max_content_light_level == b->max_content_light_level) &&
+      (a->max_pic_average_light_level == b->max_pic_average_light_level);
+}
+
+GST_START_TEST (test_h265_create_sei)
+{
+  GstH265Parser *parser;
+  GstH265ParserResult parse_ret;
+  GstH265NalUnit nalu;
+  GArray *msg_array = NULL;
+  GstMemory *mem;
+  gint i;
+  GstMapInfo info;
+  struct
+  {
+    guint8 *raw_data;
+    guint len;
+    GstH265SEIPayloadType type;
+    GstH265SEIMessage parsed_message;
+    SEICheckFunc check_func;
+  } test_list[] = {
+    /* *INDENT-OFF* */
+    {h265_sei_user_data_registered, G_N_ELEMENTS (h265_sei_user_data_registered),
+        GST_H265_SEI_REGISTERED_USER_DATA, {0,},
+        (SEICheckFunc) check_sei_user_data_registered},
+    {h265_sei_time_code, G_N_ELEMENTS (h265_sei_time_code),
+        GST_H265_SEI_TIME_CODE, {0,}, (
+        SEICheckFunc) check_sei_time_code},
+    {h265_sei_mdcv, G_N_ELEMENTS (h265_sei_mdcv),
+        GST_H265_SEI_MASTERING_DISPLAY_COLOUR_VOLUME, {0,},
+        (SEICheckFunc) check_sei_mdcv},
+    {h265_sei_cll, G_N_ELEMENTS (h265_sei_cll),
+        GST_H265_SEI_CONTENT_LIGHT_LEVEL, {0,},
+        (SEICheckFunc) check_sei_cll},
+    /* *INDENT-ON* */
+  };
+
+  parser = gst_h265_parser_new ();
+
+  /* test single sei message per sei nal unit */
+  for (i = 0; i < G_N_ELEMENTS (test_list); i++) {
+    gsize nal_size;
+
+    parse_ret = gst_h265_parser_identify_nalu_unchecked (parser,
+        test_list[i].raw_data, 0, test_list[i].len, &nalu);
+    assert_equals_int (parse_ret, GST_H265_PARSER_OK);
+    assert_equals_int (nalu.type, GST_H265_NAL_PREFIX_SEI);
+
+    parse_ret = gst_h265_parser_parse_sei (parser, &nalu, &msg_array);
+    assert_equals_int (parse_ret, GST_H265_PARSER_OK);
+    assert_equals_int (msg_array->len, 1);
+
+    /* test bytestream */
+    mem = gst_h265_create_sei_memory (nalu.layer_id,
+        nalu.temporal_id_plus1, 4, msg_array);
+    fail_unless (mem != NULL);
+    fail_unless (gst_memory_map (mem, &info, GST_MAP_READ));
+    GST_MEMDUMP ("created sei nal", info.data, info.size);
+    GST_MEMDUMP ("original sei nal", test_list[i].raw_data, test_list[i].len);
+    assert_equals_int (info.size, test_list[i].len);
+    fail_if (memcmp (info.data, test_list[i].raw_data, test_list[i].len));
+    gst_memory_unmap (mem, &info);
+    gst_memory_unref (mem);
+
+    /* test packetized */
+    mem = gst_h265_create_sei_memory_hevc (nalu.layer_id,
+        nalu.temporal_id_plus1, 4, msg_array);
+    fail_unless (mem != NULL);
+    fail_unless (gst_memory_map (mem, &info, GST_MAP_READ));
+    assert_equals_int (info.size, test_list[i].len);
+    fail_if (memcmp (info.data + 4, test_list[i].raw_data + 4,
+            test_list[i].len - 4));
+    nal_size = GST_READ_UINT32_BE (info.data);
+    assert_equals_int (nal_size, info.size - 4);
+    gst_memory_unmap (mem, &info);
+    gst_memory_unref (mem);
+
+    /* store parsed SEI for following tests */
+    fail_unless (gst_h265_sei_copy (&test_list[i].parsed_message,
+            &g_array_index (msg_array, GstH265SEIMessage, 0)));
+
+    g_array_unref (msg_array);
+  }
+
+  /* test multiple SEI messages in a nal unit */
+  msg_array = g_array_new (FALSE, FALSE, sizeof (GstH265SEIMessage));
+  for (i = 0; i < G_N_ELEMENTS (test_list); i++)
+    g_array_append_val (msg_array, test_list[i].parsed_message);
+
+  mem = gst_h265_create_sei_memory (nalu.layer_id,
+      nalu.temporal_id_plus1, 4, msg_array);
+  fail_unless (mem != NULL);
+  g_array_unref (msg_array);
+
+  /* parse sei message from buffer */
+  fail_unless (gst_memory_map (mem, &info, GST_MAP_READ));
+  parse_ret = gst_h265_parser_identify_nalu_unchecked (parser,
+      info.data, 0, info.size, &nalu);
+  assert_equals_int (parse_ret, GST_H265_PARSER_OK);
+  assert_equals_int (nalu.type, GST_H265_NAL_PREFIX_SEI);
+  parse_ret = gst_h265_parser_parse_sei (parser, &nalu, &msg_array);
+  gst_memory_unmap (mem, &info);
+  gst_memory_unref (mem);
+
+  assert_equals_int (parse_ret, GST_H265_PARSER_OK);
+  assert_equals_int (msg_array->len, G_N_ELEMENTS (test_list));
+  for (i = 0; i < msg_array->len; i++) {
+    GstH265SEIMessage *msg = &g_array_index (msg_array, GstH265SEIMessage, i);
+
+    assert_equals_int (msg->payloadType, test_list[i].type);
+    fail_unless (test_list[i].check_func (&msg->payload,
+            &test_list[i].parsed_message.payload));
+  }
+
+  /* clean up */
+  for (i = 0; i < G_N_ELEMENTS (test_list); i++)
+    gst_h265_sei_free (&test_list[i].parsed_message);
+
+  g_array_unref (msg_array);
+  gst_h265_parser_free (parser);
+}
+
+GST_END_TEST;
+
 static Suite *
 h265parser_suite (void)
 {
@@ -675,6 +912,7 @@ h265parser_suite (void)
   tcase_add_test (tc_chain, test_h265_parse_pps);
   tcase_add_test (tc_chain, test_h265_nal_type_classification);
   tcase_add_test (tc_chain, test_h265_sei_registered_user_data);
+  tcase_add_test (tc_chain, test_h265_create_sei);
 
   return s;
 }