Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a "export" extension to colibri2. #108

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.packet.*;

import java.util.*;

public class ConferenceModifyIQ
extends AbstractConferenceModificationIQ<ConferenceModifyIQ>
{
Expand Down Expand Up @@ -95,6 +97,10 @@ private ConferenceModifyIQ(Builder b)
rtcstatsEnabled = b.rtcstatsEnabled;
create = b.create;
expire = b.expire;
if (b.exports != null)
{
addExtension(b.exports);
}

if (b.meetingId == null)
{
Expand Down Expand Up @@ -170,6 +176,12 @@ public boolean getExpire()
return expire;
}

@Nullable
public Exports getExports()
{
return getExtension(Exports.class);
}

@Contract("_ -> new")
public static @NotNull Builder builder(XMPPConnection connection)
{
Expand All @@ -196,6 +208,7 @@ public static final class Builder
private boolean expire = EXPIRE_DEFAULT;
private String conferenceName;
private String meetingId;
private Exports exports = null;

private Builder(IqData iqCommon)
{
Expand All @@ -218,6 +231,22 @@ public Builder setRtcstatsEnabled(boolean rtcstatsEnabled)
return this;
}

public Builder setEmptyExports()
{
exports = new Exports();
return this;
}

public Builder addExport(@NotNull Export export)
{
if (exports == null)
{
exports = new Exports();
}
exports.addExport(export);
return this;
}

public Builder setConferenceName(String name)
{
conferenceName = name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ private static void doRegisterProviders()
/* Original colibri does something weird with these elements' namespaces, so register them here. */
ProviderManager.addExtensionProvider(ForceMute.ELEMENT, ForceMute.NAMESPACE, new ForceMute.Provider());
ProviderManager.addExtensionProvider(InitialLastN.ELEMENT, InitialLastN.NAMESPACE, new InitialLastNProvider());
ProviderManager.addExtensionProvider(Export.ELEMENT, Export.NAMESPACE, new ExportProvider());
ProviderManager.addExtensionProvider(Exports.ELEMENT, Exports.NAMESPACE, new ExportsProvider());
ProviderManager.addExtensionProvider(Capability.ELEMENT, Capability.NAMESPACE, new Capability.Provider());
ProviderManager.addExtensionProvider(Sctp.ELEMENT, Sctp.NAMESPACE, new Sctp.Provider());
ProviderManager.addExtensionProvider(Colibri2Error.ELEMENT,
Expand Down
71 changes: 71 additions & 0 deletions src/main/kotlin/org/jitsi/xmpp/extensions/colibri2/Export.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright @ 2024 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.xmpp.extensions.colibri2

import org.jitsi.xmpp.extensions.AbstractPacketExtension
import org.jitsi.xmpp.extensions.DefaultPacketExtensionProvider
import org.jivesoftware.smack.packet.XmlEnvironment
import org.jivesoftware.smack.parsing.SmackParsingException
import org.jivesoftware.smack.xml.XmlPullParser
import org.jivesoftware.smack.xml.XmlPullParserException
import java.io.IOException
import java.net.URI
import java.net.URISyntaxException

class Export(
val url: URI,
audio: Boolean = false,
video: Boolean = false
) : AbstractPacketExtension(NAMESPACE, ELEMENT) {
init {
setAttribute(URL_ATTR_NAME, url)
if (audio) {
setAttribute(AUDIO_ATTR_NAME, true)
}
if (video) {
setAttribute(VIDEO_ATTR_NAME, true)
}
}

val audio: Boolean
get() = getAttributeAsString(AUDIO_ATTR_NAME)?.toBoolean() ?: false
val video: Boolean
get() = getAttributeAsString(VIDEO_ATTR_NAME)?.toBoolean() ?: false

companion object {
const val ELEMENT = "export"
const val NAMESPACE = ConferenceModifyIQ.NAMESPACE
const val URL_ATTR_NAME = "url"
const val AUDIO_ATTR_NAME = "audio"
const val VIDEO_ATTR_NAME = "video"
}
}

class ExportProvider : DefaultPacketExtensionProvider<Export>(Export::class.java) {
@Throws(XmlPullParserException::class, IOException::class, SmackParsingException::class)
override fun parse(parser: XmlPullParser, depth: Int, xml: XmlEnvironment?): Export {
val url = parser.getAttributeValue("", Export.URL_ATTR_NAME)
?: throw SmackParsingException.RequiredAttributeMissingException("Missing 'url' attribute")
val audio = parser.getAttributeValue("", Export.AUDIO_ATTR_NAME)?.toBoolean() ?: false
val video = parser.getAttributeValue("", Export.VIDEO_ATTR_NAME)?.toBoolean() ?: false

try {
return Export(URI(url), audio, video)
} catch (e: URISyntaxException) {
throw SmackParsingException("Invalid 'url': ${e.message}")
}
}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/org/jitsi/xmpp/extensions/colibri2/Exports.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright @ 2024 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.xmpp.extensions.colibri2

import org.jitsi.xmpp.extensions.AbstractPacketExtension
import org.jitsi.xmpp.extensions.DefaultPacketExtensionProvider

class Exports : AbstractPacketExtension(NAMESPACE, ELEMENT) {

fun getExports(): List<Export> = getChildExtensionsOfType(Export::class.java)
fun addExport(export: Export) = addChildExtension(export)

companion object {
const val ELEMENT = "exports"
const val NAMESPACE = ConferenceModifyIQ.NAMESPACE
}
}

class ExportsProvider : DefaultPacketExtensionProvider<Exports>(Exports::class.java)
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.jitsi.xmpp.extensions.colibri2.Colibri2Relay
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifiedIQ
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifyIQ
import org.jitsi.xmpp.extensions.colibri2.Endpoints
import org.jitsi.xmpp.extensions.colibri2.Export
import org.jitsi.xmpp.extensions.colibri2.ForceMute
import org.jitsi.xmpp.extensions.colibri2.InitialLastN
import org.jitsi.xmpp.extensions.colibri2.Media
Expand All @@ -37,6 +38,7 @@ import org.jivesoftware.smackx.muc.MUCRole
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import java.lang.IllegalArgumentException
import java.net.URI

object Colibri2JSONDeserializer {
private fun deserializeMedia(media: JSONObject): Media {
Expand Down Expand Up @@ -356,6 +358,26 @@ object Colibri2JSONDeserializer {
setRtcstatsEnabled(it)
}
}

conferenceModify["exports"]?.let {
if (it is JSONArray) {
var added = false
it.forEach { export ->
if (export is JSONObject) {
addExport(
Export(
URI(export[Export.URL_ATTR_NAME] as String),
audio = export[Export.AUDIO_ATTR_NAME]?.toString()?.toBoolean() ?: false,
video = export[Export.VIDEO_ATTR_NAME]?.toString()?.toBoolean() ?: false
)
)
added = true
}
}
// An empty array is distinct from no value specified.
if (!added) setEmptyExports()
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import org.jitsi.xmpp.extensions.colibri2.Colibri2Endpoint
import org.jitsi.xmpp.extensions.colibri2.Colibri2Relay
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifiedIQ
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifyIQ
import org.jitsi.xmpp.extensions.colibri2.Export
import org.jitsi.xmpp.extensions.colibri2.Exports
import org.jitsi.xmpp.extensions.colibri2.ForceMute
import org.jitsi.xmpp.extensions.colibri2.InitialLastN
import org.jitsi.xmpp.extensions.colibri2.Media
Expand Down Expand Up @@ -231,6 +233,16 @@ object Colibri2JSONSerializer {
}
}

private fun serializeExport(export: Export) = JSONObject().apply {
put("url", export.url.toString())
if (export.audio) put("audio", true)
if (export.video) put("video", true)
}

private fun serializeExports(exports: Exports) = JSONArray().apply {
exports.getExports().forEach { add(serializeExport(it)) }
}

@JvmStatic
fun serializeConferenceModify(iq: ConferenceModifyIQ): JSONObject {
return serializeAbstractConferenceModificationIQ(iq).apply {
Expand All @@ -246,6 +258,10 @@ object Colibri2JSONSerializer {
put(ConferenceModifyIQ.RTCSTATS_ENABLED_ATTR_NAME, iq.isRtcstatsEnabled)
}

iq.exports?.let {
put("exports", serializeExports(it))
}

put(ConferenceModifyIQ.MEETING_ID_ATTR_NAME, iq.meetingId)

iq.conferenceName?.let { put(ConferenceModifyIQ.NAME_ATTR_NAME, it) }
Expand Down
64 changes: 64 additions & 0 deletions src/test/kotlin/org/jitsi/xmpp/extensions/colibri2/ExportTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright @ 2024 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.xmpp.extensions.colibri2

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import org.jivesoftware.smack.parsing.SmackParsingException
import org.jivesoftware.smack.util.PacketParserUtils
import java.net.URI

class ExportTest : ShouldSpec() {
init {
IqProviderUtils.registerProviders()
val provider = ExportProvider()
val url = "ws://example.com"

context("Parsing a valid extension") {
context("Without audio/video") {
val export = provider.parse(PacketParserUtils.getParserFor("<export url='$url'/>"))
export.url shouldBe URI(url)
export.audio shouldBe false
export.video shouldBe false
}
context("With audio") {
val export = provider.parse(PacketParserUtils.getParserFor("<export url='$url' audio='true'/>"))
export.url shouldBe URI(url)
export.audio shouldBe true
export.video shouldBe false
}
context("With video") {
val export = provider.parse(
PacketParserUtils.getParserFor("<export url='$url' audio='false' video='true'/>")
)
export.url shouldBe URI(url)
export.audio shouldBe false
export.video shouldBe true
}
}
context("Parsing with missing url") {
shouldThrow<SmackParsingException> {
provider.parse(PacketParserUtils.getParserFor("<export/>"))
}
}
context("Parsing with invalid url") {
shouldThrow<SmackParsingException> {
provider.parse(PacketParserUtils.getParserFor("<export url='in val id'/>"))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ private val expectedMappings = listOf(
<transport ice-controlling="true"/>
<capability name="source-names"/>
</endpoint>
<exports>
<export url='wss://example.com/audio' audio='true'/>
<export url='wss://example.com/video' video='true'/>
</exports>
</conference-modify>
</iq>
""",
Expand Down Expand Up @@ -278,6 +282,10 @@ private val expectedMappings = listOf(
"transport": { "ice-controlling": true },
"capabilities": [ "source-names" ]
}
],
"exports": [
{ "url": "wss://example.com/audio", "audio": true },
{ "url": "wss://example.com/video", "video": true }
]
}
""",
Expand Down
Loading