diff --git a/libervia/backend/plugins/plugin_comp_pubsub/__init__.py b/libervia/backend/plugins/plugin_comp_pubsub/__init__.py
index 199b19ab..ed6818d6 100644
--- a/libervia/backend/plugins/plugin_comp_pubsub/__init__.py
+++ b/libervia/backend/plugins/plugin_comp_pubsub/__init__.py
@@ -45,7 +45,7 @@ from libervia.backend.memory.sqla_mapping import (
SubscriptionState,
)
from libervia.backend.plugins.plugin_xep_0060 import NodeMetadata, XEP_0060
-from libervia.backend.plugins.plugin_xep_0167 import NS_AV_CONFERENCES
+from libervia.backend.plugins.plugin_misc_av_conferences import NS_AV_CONFERENCES
from libervia.backend.plugins.plugin_xep_0355 import XEP_0355, DelegationData
from libervia.backend.plugins.plugin_xep_0356 import XEP_0356
from libervia.backend.plugins.plugin_xep_0496 import LINK_VAR, PARENT_VAR, XEP_0496
diff --git a/libervia/backend/plugins/plugin_misc_av_conferences.py b/libervia/backend/plugins/plugin_misc_av_conferences.py
new file mode 100644
index 00000000..4feaf024
--- /dev/null
+++ b/libervia/backend/plugins/plugin_misc_av_conferences.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+
+# Libervia plugin to handle Audio/Video conferences
+# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program 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 Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+from twisted.words.xish import domish
+from tx_xmpp import disco, itx_xmpp
+from zope.interface import implementer
+
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core.core_types import SatXMPPEntity
+from libervia.backend.core.i18n import _
+from libervia.backend.core.log import getLogger
+from libervia.backend.plugins.plugin_xep_0166 import XEP_0166
+
+log = getLogger(__name__)
+
+
+PLUGIN_INFO = {
+ C.PI_NAME: "Jingle A/V Conferences",
+ C.PI_IMPORT_NAME: "AV_CONFERENCES",
+ C.PI_TYPE: "MISC",
+ C.PI_MODES: C.PLUG_MODE_BOTH,
+ C.PI_PROTOCOLS: [],
+ C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0167"],
+ C.PI_MAIN: "AVConferences",
+ C.PI_HANDLER: "yes",
+ C.PI_DESCRIPTION: _(
+ "Handle Audio/Video conferences (A/V calls with a large number of participants).\n"
+ "This plugin implements the Jingle Audio/Video conferences for calls with a "
+ "large number of participants."
+ ),
+}
+
+NS_AV_CONFERENCES = "urn:xmpp:jingle:av-conferences:0"
+
+
+class AVConferences:
+ namespace = NS_AV_CONFERENCES
+
+ def __init__(self, host):
+ log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
+ self.host = host
+ self._j: XEP_0166 = host.plugins["XEP-0166"]
+ host.trigger.add("XEP-0167_jingle_handler", self._jingle_handler_trigger)
+
+ def get_handler(self, client):
+ return AVConferencesHandler(self)
+
+ def _jingle_handler_trigger(
+ self,
+ client: SatXMPPEntity,
+ action: str,
+ session: dict,
+ content_name: str,
+ desc_elt: domish.Element,
+ ) -> None:
+ """Store the origin of the contents.
+
+ The origin is kept in the content's application data, so that it can be exposed to
+ the frontend.
+ """
+ if action != self._j.A_CONTENT_ADD:
+ return
+ # get from the element tree, not the session state, which may be updated
+ # by a concurrent content-add before this trigger runs
+ try:
+ jingle_elt = desc_elt.parent.parent
+ except AttributeError:
+ jingle_elt = None
+ if jingle_elt is None:
+ jingle_elt = session.get("jingle_elt")
+ if jingle_elt is None:
+ return
+ assert jingle_elt.name == "jingle"
+ origin = self.parse_origin(jingle_elt)
+ if origin is None:
+ return
+ log.debug(
+ f"Content {content_name!r} of session {session['id']!r} "
+ f"({client.jid.full()}) originates from {origin!r}."
+ )
+ content_data = session["contents"].get(content_name)
+ if content_data is not None:
+ content_data["application_data"]["origin"] = origin
+
+ @staticmethod
+ def build_origin_elt(origin: str) -> domish.Element:
+ """Create an element to identify the stream origin
+
+ @param origin: entity URI of the stream origin (typically an "xmpp:" URL)
+ @return: element
+ """
+ origin_elt = domish.Element((NS_AV_CONFERENCES, "origin"))
+ origin_elt["entity"] = origin
+ return origin_elt
+
+ @staticmethod
+ def parse_origin(jingle_elt: domish.Element) -> str | None:
+ """Get the origin of the stream from a element
+
+ @return: entity URI of the stream origin, or None
+ """
+ try:
+ origin_elt = next(jingle_elt.elements(NS_AV_CONFERENCES, "origin"))
+ except StopIteration:
+ return None
+ return origin_elt.getAttribute("entity")
+
+
+@implementer(itx_xmpp.IDisco)
+class AVConferencesHandler(XMPPHandler):
+ def __init__(self, plugin_parent):
+ self.plugin_parent = plugin_parent
+
+ def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
+ return [
+ disco.DiscoFeature(NS_AV_CONFERENCES),
+ ]
+
+ def getDiscoItems(self, requestor, target, nodeIdentifier=""):
+ return []
diff --git a/libervia/backend/plugins/plugin_xep_0167/constants.py b/libervia/backend/plugins/plugin_xep_0167/constants.py
index b8ac4426..ffcfaa1d 100644
--- a/libervia/backend/plugins/plugin_xep_0167/constants.py
+++ b/libervia/backend/plugins/plugin_xep_0167/constants.py
@@ -24,4 +24,3 @@ NS_JINGLE_RTP_AUDIO: Final = f"{NS_JINGLE_RTP_BASE}:audio"
NS_JINGLE_RTP_VIDEO: Final = f"{NS_JINGLE_RTP_BASE}:video"
NS_JINGLE_RTP_ERRORS: Final = f"{NS_JINGLE_RTP_BASE}:errors:1"
NS_JINGLE_RTP_INFO: Final = f"{NS_JINGLE_RTP_BASE}:info:1"
-NS_AV_CONFERENCES: Final = "urn:xmpp:jingle:av-conferences:0"