sdpmessage: Try to re-create profile-level-id

Some WebRTC implementations such as Pion are unhappy if the
profile-level-id isn't returned with a compatible profile as the
RFC requires. Let's try to reform it

In practice, the correct way to do this would be to not use caps
intersection, but to instead implement the correct RFC compliant
SDP O/A negotiation of formats.

Include a unit test written by Philippe Normand

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/9031>
This commit is contained in:
Olivier Crête 2025-05-19 23:17:46 -04:00 committed by GStreamer Marge Bot
parent 7fb6965ded
commit 5d9abea64c
2 changed files with 152 additions and 5 deletions

View File

@ -3836,6 +3836,87 @@ _sdp_media_has_extmap (const GstSDPMedia * media, guint id)
}
/* This function tries to recreate the profile_idc and constraints, but
* it's not foolproof, as there could be many variants. To be perfectly
* compliant with the RFC, we'd need to store and return the orignal
* profile-level-id
*/
static gboolean
h264_get_profile_idc (const gchar * profile, guint8 * profile_idc,
guint8 * constraint_flags)
{
const guint8 CSF0 = 0x80; /* Baseline compatible */
const guint8 CSF1 = 0x40; /* Main compatible */
// const guint8 CSF2 = 0x20; /* Extended compatible */
const guint8 CSF3 = 0x10;
const guint8 CSF4 = 0x08;
const guint8 CSF5 = 0x04;
g_return_val_if_fail (profile, FALSE);
*constraint_flags = 0;
if (!strcmp (profile, "constrained-baseline")) {
*profile_idc = 66;
*constraint_flags = CSF0 | CSF1;
} else if (!strcmp (profile, "baseline")) {
*profile_idc = 66;
*constraint_flags = CSF0;
} else if (!strcmp (profile, "main")) {
*profile_idc = 77;
*constraint_flags = CSF1;
} else if (!strcmp (profile, "extended")) {
*profile_idc = 88;
*constraint_flags = CSF3;
} else if (!strcmp (profile, "constrained-high")) {
*profile_idc = 100;
*constraint_flags = CSF4 | CSF5;
} else if (!strcmp (profile, "progressive-high")) {
*profile_idc = 100;
*constraint_flags = CSF4;
} else if (!strcmp (profile, "high-10")) {
*profile_idc = 110;
} else if (!strcmp (profile, "high-10-intra")) {
*profile_idc = 110;
*constraint_flags = CSF3;
} else if (!strcmp (profile, "progressive-high-10")) {
*profile_idc = 110;
*constraint_flags = CSF4;
} else if (!strcmp (profile, "high-4:2:2")) {
*profile_idc = 122;
} else if (!strcmp (profile, "high-4:2:2-intra")) {
*profile_idc = 122;
*constraint_flags = CSF3;
} else if (!strcmp (profile, "high-4:4:4")) {
*profile_idc = 244;
} else if (!strcmp (profile, "high-4:4:4-intra")) {
*profile_idc = 244;
*constraint_flags = CSF3;
} else if (!strcmp (profile, "cavlc-4:4:4-intra")) {
*profile_idc = 44;
} else if (!strcmp (profile, "multiview-high")) {
*profile_idc = 118;
} else if (!strcmp (profile, "stereo-high")) {
*profile_idc = 128;
} else if (!strcmp (profile, "scalable-baseline")) {
*profile_idc = 83;
} else if (!strcmp (profile, "scalable-constrained-baseline")) {
*profile_idc = 83;
*constraint_flags = CSF5;
} else if (!strcmp (profile, "scalable-high")) {
*profile_idc = 86;
} else if (!strcmp (profile, "scalable-constrained-high")) {
*profile_idc = 86;
*constraint_flags = CSF5;
} else if (!strcmp (profile, "scalable-high-intra")) {
*profile_idc = 86;
*constraint_flags = CSF3;
} else {
return FALSE;
}
return TRUE;
}
static GstSDPResult
_add_media_format_from_structure (const GstStructure * s, GstSDPMedia * media)
{
@ -4091,11 +4172,19 @@ _add_media_format_from_structure (const GstStructure * s, GstSDPMedia * media)
* representation */
if (!g_strcmp0 (gst_structure_get_string (s, "encoding-name"), "H264")
&& !g_strcmp0 (fname, "profile")) {
fname = "level-asymmetry-allowed";
fval = "1";
}
guint8 profile_idc, constraint_flags;
g_string_append_printf (fmtp, "%s%s=%s", first ? "" : ";", fname, fval);
g_string_append_printf (fmtp, "%slevel-asymmetry-allowed=1",
first ? "" : ";");
if (h264_get_profile_idc (fval, &profile_idc, &constraint_flags))
g_string_append_printf (fmtp, ";profile-level-id=%02x%02x1f",
profile_idc, constraint_flags);
else
GST_FIXME ("Can't convert profile %s back into profile-level-id",
fval);
} else {
g_string_append_printf (fmtp, "%s%s=%s", first ? "" : ";", fname, fval);
}
first = FALSE;
}
}

View File

@ -27,6 +27,7 @@
#include <gst/check/gstcheck.h>
#include <gst/sdp/gstsdpmessage.h>
#include <gst/pbutils/pbutils.h>
/* *INDENT-OFF* */
static const gchar *sdp = "v=0\r\n"
@ -774,11 +775,15 @@ GST_START_TEST (media_from_caps_h264_with_profile_asymmetry_allowed)
const GstSDPMedia *result_video;
GstStructure *s_video;
GstCaps *caps_video;
GstSDPMedia media;
GstSDPResult ret = GST_SDP_OK;
const gchar *fmtp;
gboolean profile_level_id_found = FALSE;
gchar **pairs;
gst_sdp_message_new (&message);
gst_sdp_message_parse_buffer ((guint8 *) h264_sdp, length, message);
result_video = gst_sdp_message_get_media (message, 0);
fail_unless (result_video != NULL);
caps_video = gst_sdp_media_get_caps_from_media (result_video, 96);
@ -789,6 +794,59 @@ GST_START_TEST (media_from_caps_h264_with_profile_asymmetry_allowed)
fail_unless_equals_string (gst_structure_get_string (s_video, "profile"),
"constrained-baseline");
/* Check that the conversion from caps to SDP preserved the profile and level
* caps fields, through the profile-level-id SDP sub-attribute of the fmtp
* attribute. */
memset (&media, 0, sizeof (media));
fail_unless_equals_int (GST_SDP_OK, gst_sdp_media_init (&media));
ret = gst_sdp_media_set_media_from_caps (caps_video, &media);
fail_unless (ret == GST_SDP_OK);
fmtp = gst_sdp_media_get_attribute_val (&media, "fmtp");
fail_unless (fmtp != NULL);
pairs = g_strsplit (fmtp, ";", 0);
for (int i = 0; pairs[i]; i++) {
gchar *valpos;
const gchar *val, *key;
valpos = strstr (pairs[i], "=");
if (valpos) {
/* we have a '=' and thus a value, remove the '=' with \0 */
*valpos = '\0';
/* value is everything between '=' and ';'. We split the pairs at ;
* boundaries so we can take the remainder of the value. Some servers
* put spaces around the value which we strip off here. Alternatively
* we could strip those spaces in the depayloaders should these spaces
* actually carry any meaning in the future. */
val = g_strstrip (valpos + 1);
} else {
/* simple <param>;.. is translated into <param>=1;... */
val = "1";
}
/* strip the key of spaces, convert key to lowercase but not the value. */
key = g_strstrip (pairs[i]);
if (!strcmp (key, "profile-level-id")) {
gint64 spsint;
guint8 sps[3];
spsint = g_ascii_strtoll (val, NULL, 16);
sps[0] = spsint >> 16;
sps[1] = (spsint >> 8) & 0xff;
sps[2] = spsint & 0xff;
g_print ("%s\n", val);
fail_unless_equals_string (gst_codec_utils_h264_get_profile (sps, 3),
"constrained-baseline");
fail_unless_equals_string (gst_codec_utils_h264_get_level (sps, 3),
"3.1");
profile_level_id_found = TRUE;
break;
}
}
g_strfreev (pairs);
fail_unless (profile_level_id_found);
gst_sdp_media_uninit (&media);
gst_caps_unref (caps_video);
gst_sdp_message_free (message);
}