changeset 26997:72bcdcb0629f

Add Gmail video support. Thanks to Eion for all his testing help.
author Mike Ruprecht <maiku@soc.pidgin.im>
date Tue, 02 Jun 2009 05:00:20 +0000
parents 4605fe3c43bb
children f0621e47ccf3
files libpurple/media.c libpurple/protocols/jabber/disco.c libpurple/protocols/jabber/google.c libpurple/protocols/jabber/google.h libpurple/protocols/jabber/jabber.c libpurple/protocols/jabber/jabber.h libpurple/protocols/jabber/presence.c
diffstat 7 files changed, 249 insertions(+), 49 deletions(-) [+]
line wrap: on
line diff
--- a/libpurple/media.c	Mon Jun 01 10:33:38 2009 +0000
+++ b/libpurple/media.c	Tue Jun 02 05:00:20 2009 +0000
@@ -43,6 +43,7 @@
 #ifdef USE_VV
 
 #include <gst/farsight/fs-conference-iface.h>
+#include <gst/farsight/fs-element-added-notifier.h>
 
 /** @copydoc _PurpleMediaSession */
 typedef struct _PurpleMediaSession PurpleMediaSession;
@@ -2380,6 +2381,18 @@
 	stream->connected_cb_id = purple_timeout_add(0,
 			(GSourceFunc)purple_media_connected_cb, stream);
 }
+
+static void
+purple_media_element_added_cb(FsElementAddedNotifier *self,
+		GstBin *bin, GstElement *element, gpointer user_data)
+{
+	/*
+	 * Hack to make H264 work with Gmail video.
+	 */
+	if (!strncmp(GST_ELEMENT_NAME(element), "x264", 4)) {
+		g_object_set(GST_OBJECT(element), "cabac", FALSE, NULL);
+	}
+}
 #endif  /* USE_VV */
 
 gboolean
@@ -2456,6 +2469,19 @@
 			g_object_set(G_OBJECT(session->session),
 					"no-rtcp-timeout", 0, NULL);
 
+		/*
+		 * Hack to make x264 work with Gmail video.
+		 */
+		if (is_nice && !strcmp(sess_id, "google-video")) {
+			FsElementAddedNotifier *notifier =
+					fs_element_added_notifier_new();
+			g_signal_connect(G_OBJECT(notifier), "element-added",
+					G_CALLBACK(purple_media_element_added_cb),
+					stream);
+			fs_element_added_notifier_add(notifier,
+					GST_BIN(media->priv->conference));
+		}
+
 		fs_codec_list_destroy(codec_conf);
 
 		session->id = g_strdup(sess_id);
@@ -2670,7 +2696,8 @@
 	PurpleMediaStream *stream;
 	g_return_val_if_fail(PURPLE_IS_MEDIA(media), NULL);
 	stream = purple_media_get_stream(media, sess_id, participant);
-	return purple_media_candidate_list_from_fs(stream->local_candidates);
+	return stream ? purple_media_candidate_list_from_fs(
+			stream->local_candidates) : NULL;
 #else
 	return NULL;
 #endif
--- a/libpurple/protocols/jabber/disco.c	Mon Jun 01 10:33:38 2009 +0000
+++ b/libpurple/protocols/jabber/disco.c	Tue Jun 02 05:00:20 2009 +0000
@@ -148,6 +148,18 @@
 			 */
 			xmlnode *feature = xmlnode_new_child(query, "feature");
 			xmlnode_set_attrib(feature, "var", "http://www.google.com/xmpp/protocol/voice/v1");
+		} else if (g_str_equal(node, CAPS0115_NODE "#" "video-v1")) {
+			/*
+			 * HUGE HACK! We advertise this ext (see jabber_presence_create_js
+			 * where we add <c/> to the <presence/>) for the Google Talk
+			 * clients that don't actually check disco#info features.
+			 *
+			 * This specific feature is redundant but is what
+			 * node='http://mail.google.com/xmpp/client/caps', ver='1.1'
+			 * advertises as 'video-v1'.
+			 */
+			xmlnode *feature = xmlnode_new_child(query, "feature");
+			xmlnode_set_attrib(feature, "var", "http://www.google.com/xmpp/protocol/video/v1");
 #endif
 		} else {
 			xmlnode *error, *inf;
--- a/libpurple/protocols/jabber/google.c	Mon Jun 01 10:33:38 2009 +0000
+++ b/libpurple/protocols/jabber/google.c	Tue Jun 02 05:00:20 2009 +0000
@@ -36,6 +36,9 @@
 
 #ifdef USE_VV
 
+#define NS_GOOGLE_VIDEO "http://www.google.com/session/video"
+#define NS_GOOGLE_PHONE "http://www.google.com/session/phone"
+
 typedef struct {
 	char *id;
 	char *initiator;
@@ -55,6 +58,7 @@
 	PurpleMedia *media;
 	JabberStream *js;
 	char *remote_jid;
+	gboolean video;
 } GoogleSession;
 
 static gboolean
@@ -104,9 +108,13 @@
 google_session_send_candidates(PurpleMedia *media, gchar *session_id,
 		gchar *participant, GoogleSession *session)
 {
-	GList *candidates = purple_media_get_local_candidates(session->media, "google-voice",
-							      session->remote_jid);
+	GList *candidates = purple_media_get_local_candidates(
+			session->media, session_id, session->remote_jid);
 	PurpleMediaCandidate *transport;
+	gboolean video = FALSE;
+
+	if (!strcmp(session_id, "google-video"))
+		video = TRUE;
 
 	for (;candidates;candidates = candidates->next) {
 		JabberIq *iq;
@@ -114,11 +122,10 @@
 		PurpleMediaCandidateType type;
 		xmlnode *sess;
 		xmlnode *candidate;
+		guint component_id;
 		transport = (PurpleMediaCandidate*)(candidates->data);
-
-		if (purple_media_candidate_get_component_id(transport)
-				!= PURPLE_MEDIA_COMPONENT_RTP)
-			continue;
+		component_id = purple_media_candidate_get_component_id(
+				transport);
 
 		iq = jabber_iq_new(session->js, JABBER_IQ_SET);
 		sess = google_session_create_xmlnode(session, "candidates");
@@ -139,7 +146,11 @@
 
 		xmlnode_set_attrib(candidate, "address", ip);
 		xmlnode_set_attrib(candidate, "port", port);
-		xmlnode_set_attrib(candidate, "name", "rtp");
+		xmlnode_set_attrib(candidate, "name",
+				component_id == PURPLE_MEDIA_COMPONENT_RTP ?
+				video ? "video_rtp" : "rtp" :
+				component_id == PURPLE_MEDIA_COMPONENT_RTCP ?
+				video ? "video_rtcp" : "rtcp" : "none");
 		xmlnode_set_attrib(candidate, "username", username);
 		/*
 		 * As of this writing, Farsight 2 in Google compatibility
@@ -205,13 +216,38 @@
 			google_session_send_candidates(session->media,
 					"google-voice", session->remote_jid,
 					session);
+			google_session_send_candidates(session->media,
+					"google-video", session->remote_jid,
+					session);
 			xmlnode_set_attrib(iq->node, "to", session->remote_jid);
 			xmlnode_set_attrib(iq->node, "from", me);
 			sess = google_session_create_xmlnode(session, "accept");
 		}
 		xmlnode_insert_child(iq->node, sess);
 		desc = xmlnode_new_child(sess, "description");
-		xmlnode_set_namespace(desc, "http://www.google.com/session/phone");
+		if (session->video)
+			xmlnode_set_namespace(desc, NS_GOOGLE_VIDEO);
+		else
+			xmlnode_set_namespace(desc, NS_GOOGLE_PHONE);
+
+		codecs = purple_media_get_codecs(media, "google-video");
+
+		for (iter = codecs; iter; iter = g_list_next(iter)) {
+			PurpleMediaCodec *codec = (PurpleMediaCodec*)iter->data;
+			gchar *id = g_strdup_printf("%d",
+					purple_media_codec_get_id(codec));
+			gchar *encoding_name =
+					purple_media_codec_get_encoding_name(codec);
+			payload = xmlnode_new_child(desc, "payload-type");
+			xmlnode_set_attrib(payload, "id", id);
+			xmlnode_set_attrib(payload, "name", encoding_name);
+			xmlnode_set_attrib(payload, "width", "320");
+			xmlnode_set_attrib(payload, "height", "200");
+			xmlnode_set_attrib(payload, "framerate", "30");
+			g_free(encoding_name);
+			g_free(id);
+		}
+		purple_media_codec_list_free(codecs);
 
 		codecs = purple_media_get_codecs(media, "google-voice");
 
@@ -224,6 +260,8 @@
 			gchar *clock_rate = g_strdup_printf("%d",
 					purple_media_codec_get_clock_rate(codec));
 			payload = xmlnode_new_child(desc, "payload-type");
+			if (session->video)
+				xmlnode_set_namespace(payload, NS_GOOGLE_PHONE);
 			xmlnode_set_attrib(payload, "id", id);
 			xmlnode_set_attrib(payload, "name", encoding_name);
 			xmlnode_set_attrib(payload, "clockrate", clock_rate);
@@ -235,10 +273,14 @@
 
 		jabber_iq_send(iq);
 
-		if (is_initiator)
+		if (is_initiator) {
 			google_session_send_candidates(session->media,
 					"google-voice", session->remote_jid,
 					session);
+			google_session_send_candidates(session->media,
+					"google-video", session->remote_jid,
+					session);
+		}
 
 		g_signal_handlers_disconnect_by_func(G_OBJECT(session->media),
 				G_CALLBACK(google_session_ready), session);
@@ -339,6 +381,9 @@
 	session->js = js;
 	session->remote_jid = jid;
 
+	if (type & PURPLE_MEDIA_VIDEO)
+		session->video = TRUE;
+
 	session->media = purple_media_manager_create_media(
 			purple_media_manager_get(),
 			purple_connection_get_account(js->gc),
@@ -349,8 +394,12 @@
 	params = jabber_google_session_get_params(js, &num_params);
 
 	if (purple_media_add_stream(session->media, "google-voice",
-				session->remote_jid, PURPLE_MEDIA_AUDIO,
-				TRUE, "nice", num_params, params) == FALSE) {
+			session->remote_jid, PURPLE_MEDIA_AUDIO,
+			TRUE, "nice", num_params, params) == FALSE ||
+			(session->video && purple_media_add_stream(
+			session->media, "google-video",
+			session->remote_jid, PURPLE_MEDIA_VIDEO,
+			TRUE, "nice", num_params, params) == FALSE)) {
 		purple_media_error(session->media, "Error adding stream.");
 		purple_media_stream_info(session->media,
 				PURPLE_MEDIA_INFO_HANGUP, NULL, NULL, TRUE);
@@ -378,10 +427,10 @@
 google_session_handle_initiate(JabberStream *js, GoogleSession *session, xmlnode *sess, const char *iq_id)
 {
 	JabberIq *result;
-	GList *codecs = NULL;
+	GList *codecs = NULL, *video_codecs = NULL;
 	xmlnode *desc_element, *codec_element;
 	PurpleMediaCodec *codec;
-	const char *id, *encoding_name,  *clock_rate;
+	const char *xmlns;
 	GParameter *params;
 	guint num_params;
 
@@ -390,6 +439,19 @@
 		return;
 	}
 
+	desc_element = xmlnode_get_child(sess, "description");
+	xmlns = xmlnode_get_namespace(desc_element);
+
+	if (purple_strequal(xmlns, NS_GOOGLE_PHONE))
+		session->video = FALSE;
+	else if (purple_strequal(xmlns, NS_GOOGLE_VIDEO))
+		session->video = TRUE;
+	else {
+		purple_debug_error("jabber", "Received initiate with "
+				"invalid namespace %s.\n", xmlns);
+		return;
+	}
+
 	session->media = purple_media_manager_create_media(
 			purple_media_manager_get(),
 			purple_connection_get_account(js->gc),
@@ -401,7 +463,11 @@
 
 	if (purple_media_add_stream(session->media, "google-voice",
 			session->remote_jid, PURPLE_MEDIA_AUDIO, FALSE,
-			"nice", num_params, params) == FALSE) {
+			"nice", num_params, params) == FALSE ||
+			(session->video && purple_media_add_stream(
+			session->media, "google-video",
+			session->remote_jid, PURPLE_MEDIA_VIDEO,
+			FALSE, "nice", num_params, params) == FALSE)) {
 		purple_media_error(session->media, "Error adding stream.");
 		purple_media_stream_info(session->media,
 				PURPLE_MEDIA_INFO_HANGUP, NULL, NULL, TRUE);
@@ -412,23 +478,55 @@
 
 	g_free(params);
 
-	desc_element = xmlnode_get_child(sess, "description");
+	for (codec_element = xmlnode_get_child(desc_element, "payload-type");
+	     codec_element; codec_element = codec_element->next) {
+		const char *id, *encoding_name,  *clock_rate,
+				*width, *height, *framerate;
+		gboolean video;
+		if (codec_element->name &&
+				strcmp(codec_element->name, "payload-type"))
+			continue;
 
-	for (codec_element = xmlnode_get_child(desc_element, "payload-type");
-	     codec_element;
-	     codec_element = xmlnode_get_next_twin(codec_element)) {
+		xmlns = xmlnode_get_namespace(codec_element);
 		encoding_name = xmlnode_get_attrib(codec_element, "name");
 		id = xmlnode_get_attrib(codec_element, "id");
-		clock_rate = xmlnode_get_attrib(codec_element, "clockrate");
+
+		if (!session->video ||
+				(xmlns && !strcmp(xmlns, NS_GOOGLE_PHONE))) {
+			clock_rate = xmlnode_get_attrib(
+					codec_element, "clockrate");
+			video = FALSE;
+		} else {
+			width = xmlnode_get_attrib(codec_element, "width");
+			height = xmlnode_get_attrib(codec_element, "height");
+			framerate = xmlnode_get_attrib(
+					codec_element, "framerate");
+			clock_rate = "90000";
+			video = TRUE;
+		}
 
 		if (id) {
-			codec = purple_media_codec_new(atoi(id), encoding_name, PURPLE_MEDIA_AUDIO,
-					     clock_rate ? atoi(clock_rate) : 0);
-			codecs = g_list_append(codecs, codec);
+			codec = purple_media_codec_new(atoi(id), encoding_name,
+					video ?	PURPLE_MEDIA_VIDEO :
+					PURPLE_MEDIA_AUDIO,
+					clock_rate ? atoi(clock_rate) : 0);
+			if (video)
+				video_codecs = g_list_append(
+						video_codecs, codec);
+			else
+				codecs = g_list_append(codecs, codec);
 		}
 	}
 
-	purple_media_set_remote_codecs(session->media, "google-voice", session->remote_jid, codecs);
+	if (codecs)
+		purple_media_set_remote_codecs(session->media, "google-voice",
+				session->remote_jid, codecs);
+	if (video_codecs)
+		purple_media_set_remote_codecs(session->media, "google-video",
+				session->remote_jid, video_codecs);
+
+	purple_media_codec_list_free(codecs);
+	purple_media_codec_list_free(video_codecs);
 
 	g_signal_connect_swapped(G_OBJECT(session->media), "accepted",
 			G_CALLBACK(google_session_ready), session);
@@ -442,8 +540,6 @@
 	g_signal_connect(G_OBJECT(session->media), "stream-info",
 			G_CALLBACK(google_session_stream_info_cb), session);
 
-	purple_media_codec_list_free(codecs);
-
 	result = jabber_iq_new(js, JABBER_IQ_RESULT);
 	jabber_iq_set_id(result, iq_id);
 	xmlnode_set_attrib(result->node, "to", session->remote_jid);
@@ -454,19 +550,22 @@
 google_session_handle_candidates(JabberStream  *js, GoogleSession *session, xmlnode *sess, const char *iq_id)
 {
 	JabberIq *result;
-	GList *list = NULL;
+	GList *list = NULL, *video_list = NULL;
 	xmlnode *cand;
 	static int name = 0;
 	char n[4];
 
-	for (cand = xmlnode_get_child(sess, "candidate"); cand; cand = xmlnode_get_next_twin(cand)) {
+	for (cand = xmlnode_get_child(sess, "candidate"); cand;
+			cand = xmlnode_get_next_twin(cand)) {
 		PurpleMediaCandidate *info;
+		const gchar *cname = xmlnode_get_attrib(cand, "name");
 		const gchar *type = xmlnode_get_attrib(cand, "type");
 		const gchar *protocol = xmlnode_get_attrib(cand, "protocol");
 		const gchar *address = xmlnode_get_attrib(cand, "address");
 		const gchar *port = xmlnode_get_attrib(cand, "port");
+		guint component_id;
 
-		if (type && address && port) {
+		if (cname && type && address && port) {
 			PurpleMediaCandidateType candidate_type;
 
 			g_snprintf(n, sizeof(n), "S%d", name++);
@@ -480,7 +579,13 @@
 			else
 				candidate_type = PURPLE_MEDIA_CANDIDATE_TYPE_HOST;
 
-			info = purple_media_candidate_new(n, PURPLE_MEDIA_COMPONENT_RTP,
+			if (purple_strequal(cname, "rtcp") ||
+					purple_strequal(cname, "video_rtcp"))
+				component_id = PURPLE_MEDIA_COMPONENT_RTCP;
+			else
+				component_id = PURPLE_MEDIA_COMPONENT_RTP;
+
+			info = purple_media_candidate_new(n, component_id,
 					candidate_type,
 					purple_strequal(protocol, "udp") ?
 							PURPLE_MEDIA_NETWORK_PROTOCOL_UDP :
@@ -489,12 +594,23 @@
 					atoi(port));
 			g_object_set(info, "username", xmlnode_get_attrib(cand, "username"),
 					"password", xmlnode_get_attrib(cand, "password"), NULL);
-			list = g_list_append(list, info);
+			if (!strncmp(cname, "video_", 6))
+				video_list = g_list_append(video_list, info);
+			else
+				list = g_list_append(list, info);
 		}
 	}
 
-	purple_media_add_remote_candidates(session->media, "google-voice", session->remote_jid, list);
+	if (list)
+		purple_media_add_remote_candidates(
+				session->media, "google-voice",
+				session->remote_jid, list);
+	if (video_list)
+		purple_media_add_remote_candidates(
+				session->media, "google-video",
+				session->remote_jid, video_list);
 	purple_media_candidate_list_free(list);
+	purple_media_candidate_list_free(video_list);
 
 	result = jabber_iq_new(js, JABBER_IQ_RESULT);
 	jabber_iq_set_id(result, iq_id);
@@ -506,28 +622,57 @@
 google_session_handle_accept(JabberStream *js, GoogleSession *session, xmlnode *sess, const char *iq_id)
 {
 	xmlnode *desc_element = xmlnode_get_child(sess, "description");
-	xmlnode *codec_element = xmlnode_get_child(desc_element, "payload-type");
-	GList *codecs = NULL;
+	xmlnode *codec_element = xmlnode_get_child(
+			desc_element, "payload-type");
+	GList *codecs = NULL, *video_codecs = NULL;
 	JabberIq *result = NULL;
+	const gchar *xmlns = xmlnode_get_namespace(desc_element);
+	gboolean video = (xmlns && !strcmp(xmlns, NS_GOOGLE_VIDEO));
+
+	for (; codec_element; codec_element = codec_element->next) {
+		const gchar *xmlns, *encoding_name, *id,
+				*clock_rate, *width, *height, *framerate;
+		gboolean video_codec = FALSE;
+
+		if (!purple_strequal(codec_element->name, "payload-type"))
+			continue;
 
-	for (; codec_element; codec_element =
-			xmlnode_get_next_twin(codec_element)) {
-		const gchar *encoding_name =
-				xmlnode_get_attrib(codec_element, "name");
-		const gchar *id = xmlnode_get_attrib(codec_element, "id");
-		const gchar *clock_rate =
-				xmlnode_get_attrib(codec_element, "clockrate");
+		xmlns = xmlnode_get_namespace(codec_element);
+		encoding_name =	xmlnode_get_attrib(codec_element, "name");
+		id = xmlnode_get_attrib(codec_element, "id");
+
+		if (!video || purple_strequal(xmlns, NS_GOOGLE_PHONE))
+			clock_rate = xmlnode_get_attrib(
+					codec_element, "clockrate");
+		else {
+			clock_rate = "90000";
+			width = xmlnode_get_attrib(codec_element, "width");
+			height = xmlnode_get_attrib(codec_element, "height");
+			framerate = xmlnode_get_attrib(
+					codec_element, "framerate");
+			video_codec = TRUE;
+		}
 
 		if (id && encoding_name) {
-			PurpleMediaCodec *codec = purple_media_codec_new(atoi(id),
-					encoding_name, PURPLE_MEDIA_AUDIO,
+			PurpleMediaCodec *codec = purple_media_codec_new(
+					atoi(id), encoding_name,
+					video_codec ? PURPLE_MEDIA_VIDEO :
+					PURPLE_MEDIA_AUDIO,
 					clock_rate ? atoi(clock_rate) : 0);
-			codecs = g_list_append(codecs, codec);
+			if (video_codec)
+				video_codecs = g_list_append(
+						video_codecs, codec);
+			else
+				codecs = g_list_append(codecs, codec);
 		}
 	}
 
-	purple_media_set_remote_codecs(session->media, "google-voice",
-			session->remote_jid, codecs);
+	if (codecs)
+		purple_media_set_remote_codecs(session->media, "google-voice",
+				session->remote_jid, codecs);
+	if (video_codecs)
+		purple_media_set_remote_codecs(session->media, "google-video",
+				session->remote_jid, video_codecs);
 
 	purple_media_stream_info(session->media, PURPLE_MEDIA_INFO_ACCEPT,
 			NULL, NULL, FALSE);
--- a/libpurple/protocols/jabber/google.h	Mon Jun 01 10:33:38 2009 +0000
+++ b/libpurple/protocols/jabber/google.h	Tue Jun 02 05:00:20 2009 +0000
@@ -28,6 +28,7 @@
 #include "media.h"
 
 #define GOOGLE_VOICE_CAP "http://www.google.com/xmpp/protocol/voice/v1"
+#define GOOGLE_VIDEO_CAP "http://www.google.com/xmpp/protocol/video/v1"
 #define GOOGLE_JINGLE_INFO_NAMESPACE "google:jingleinfo"
 
 void jabber_gmail_init(JabberStream *js);
--- a/libpurple/protocols/jabber/jabber.c	Mon Jun 01 10:33:38 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Tue Jun 02 05:00:20 2009 +0000
@@ -2949,7 +2949,7 @@
 	return (caps & (PURPLE_MEDIA_CAPS_AUDIO | PURPLE_MEDIA_CAPS_AUDIO_SINGLE_DIRECTION));
 }
 
-static gboolean
+gboolean
 jabber_video_enabled(JabberStream *js, const char *namespace)
 {
 	PurpleMediaManager *manager = purple_media_manager_get();
@@ -3189,8 +3189,12 @@
 				caps |= PURPLE_MEDIA_CAPS_MODIFY_SESSION |
 						PURPLE_MEDIA_CAPS_CHANGE_DIRECTION;
 		}
-		if (jabber_resource_has_capability(jbr, GOOGLE_VOICE_CAP))
+		if (jabber_resource_has_capability(jbr, GOOGLE_VOICE_CAP)) {
 			caps |= PURPLE_MEDIA_CAPS_AUDIO;
+			if (jabber_resource_has_capability(jbr,
+					GOOGLE_VIDEO_CAP))
+				caps |= PURPLE_MEDIA_CAPS_AUDIO_VIDEO;
+		}
 		return caps;
 	}
 
--- a/libpurple/protocols/jabber/jabber.h	Mon Jun 01 10:33:38 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.h	Tue Jun 02 05:00:20 2009 +0000
@@ -366,6 +366,7 @@
 GList *jabber_actions(PurplePlugin *plugin, gpointer context);
 
 gboolean jabber_audio_enabled(JabberStream *js, const char *unused);
+gboolean jabber_video_enabled(JabberStream *js, const char *unused);
 gboolean jabber_initiate_media(PurpleAccount *account, const char *who,
 		PurpleMediaSessionType type);
 PurpleMediaCaps jabber_get_media_caps(PurpleAccount *account, const char *who);
--- a/libpurple/protocols/jabber/presence.c	Mon Jun 01 10:33:38 2009 +0000
+++ b/libpurple/protocols/jabber/presence.c	Tue Jun 02 05:00:20 2009 +0000
@@ -245,6 +245,7 @@
 {
 	xmlnode *show, *status, *presence, *pri, *c;
 	const char *show_string = NULL;
+	gboolean audio_enabled, video_enabled;
 
 	presence = xmlnode_new("presence");
 
@@ -300,9 +301,18 @@
 	 * just assume that if we specify a 'voice-v1' ext (ignoring that
 	 * these are to be assigned no semantic value), we support receiving voice
 	 * calls.
+	 *
+	 * Ditto for 'video-v1'.
 	 */
-	if (jabber_audio_enabled(js, NULL /* unused */))
+	audio_enabled = jabber_audio_enabled(js, NULL /* unused */);
+	video_enabled = jabber_video_enabled(js, NULL /* unused */);
+
+	if (audio_enabled && video_enabled)
+		xmlnode_set_attrib(c, "ext", "voice-v1 video-v1");
+	else if (audio_enabled)
 		xmlnode_set_attrib(c, "ext", "voice-v1");
+	else if (video_enabled)
+		xmlnode_set_attrib(c, "ext", "video-v1");
 #endif
 
 	return presence;