Skip to content
Merged
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
32 changes: 32 additions & 0 deletions src/main/java/org/jitsi/xmpp/extensions/colibri2/MediaSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public class MediaSource
*/
public static final String TYPE_ATTR_NAME = "type";

/**
* The name of the <tt>synthetic</tt> attribute.
*/
public static final String SYNTHETIC_ATTR_NAME = "synthetic";

/**
* Construct a MediaSource. Needs to be public for DefaultPacketExtensionProvider to work.
*/
Expand Down Expand Up @@ -86,6 +91,11 @@ private MediaSource(Builder b)
}
setAttribute(ID_NAME, b.id);

if (b.synthetic)
{
setAttribute(SYNTHETIC_ATTR_NAME, true);
}

for (SourcePacketExtension s: b.sources)
{
addChildExtension(s);
Expand Down Expand Up @@ -113,6 +123,14 @@ private MediaSource(Builder b)
return MediaType.parseString(getAttributeAsString(TYPE_ATTR_NAME));
}

/**
* Whether this is a synthetic source (i.e. one generated by the bridge rather than sent by an endpoint).
*/
public boolean isSynthetic()
{
return Boolean.parseBoolean(getAttributeAsString(SYNTHETIC_ATTR_NAME));
}

/**
* Get the sources of this media source.
*/
Expand Down Expand Up @@ -153,6 +171,11 @@ public static final class Builder
*/
String id = null;

/**
* Whether the media source being built is synthetic.
*/
boolean synthetic = false;

/**
* The <tt>source</tt> elements defined by XEP-0339: Source-Specific
* Media Attributes associated with this <tt>media-source</tt>.
Expand Down Expand Up @@ -183,6 +206,15 @@ public Builder setId(@NotNull String id)
return this;
}

/**
* Sets whether the media source being built is synthetic.
*/
public Builder setSynthetic(boolean synthetic)
{
this.synthetic = synthetic;
return this;
}

/**
* Adds a payload type to the media being built.
*/
Expand Down
125 changes: 124 additions & 1 deletion src/main/kotlin/org/jitsi/xmpp/extensions/colibri2/Connect.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,30 @@ class Connect(
}
fun removePing() = getPing()?.let { removeChildExtension(it) }

fun getExports(): List<String> = getChildExtension(Exports::class.java)?.getNames() ?: emptyList()
fun addExport(name: String) {
val exports = getChildExtension(Exports::class.java) ?: Exports().also { addChildExtension(it) }
exports.addExport(name)
}
fun setExports(names: List<String>) {
getChildExtension(Exports::class.java)?.let { removeChildExtension(it) }
if (names.isNotEmpty()) {
addChildExtension(Exports(names))
}
}

fun getRequests(): List<String> = getChildExtension(Requests::class.java)?.getNames() ?: emptyList()
fun addRequest(name: String) {
val requests = getChildExtension(Requests::class.java) ?: Requests().also { addChildExtension(it) }
requests.addRequest(name)
}
fun setRequests(names: List<String>) {
getChildExtension(Requests::class.java)?.let { removeChildExtension(it) }
if (names.isNotEmpty()) {
addChildExtension(Requests(names))
}
}

class HttpHeader(val name: String, val value: String) : AbstractPacketExtension(NAMESPACE, ELEMENT) {
init {
setAttribute(NAME_ATTR_NAME, name)
Expand All @@ -91,13 +115,72 @@ class Connect(
}
}

/** A container for the source names this connection should export (send out). */
class Exports() : AbstractPacketExtension(NAMESPACE, ELEMENT) {
constructor(names: List<String>) : this() {
names.forEach { addChildExtension(Export(it)) }
}

fun getNames(): List<String> = getChildExtensionsOfType(Export::class.java).map { it.name }
fun addExport(name: String) = addChildExtension(Export(name))

companion object {
const val ELEMENT = "exports"
}
}

/** A single exported source, identified by its source name. */
class Export(name: String) : AbstractPacketExtension(NAMESPACE, ELEMENT) {
init {
setAttribute(NAME_ATTR_NAME, name)
}

val name: String
get() = getAttributeAsString(NAME_ATTR_NAME)

companion object {
const val ELEMENT = "export"
const val NAME_ATTR_NAME = "name"
}
}

/** A container for the source names this connection requests (wants to receive). */
class Requests() : AbstractPacketExtension(NAMESPACE, ELEMENT) {
constructor(names: List<String>) : this() {
names.forEach { addChildExtension(Request(it)) }
}

fun getNames(): List<String> = getChildExtensionsOfType(Request::class.java).map { it.name }
fun addRequest(name: String) = addChildExtension(Request(name))

companion object {
const val ELEMENT = "requests"
}
}

/** A single requested source, identified by its source name. */
class Request(name: String) : AbstractPacketExtension(NAMESPACE, ELEMENT) {
init {
setAttribute(NAME_ATTR_NAME, name)
}

val name: String
get() = getAttributeAsString(NAME_ATTR_NAME)

companion object {
const val ELEMENT = "request"
const val NAME_ATTR_NAME = "name"
}
}

enum class Protocols(val value: String) {
MEDIAJSON("mediajson")
}

enum class Types(val value: String) {
RECORDER("recorder"),
TRANSCRIBER("transcriber")
TRANSCRIBER("transcriber"),
TRANSLATOR("translator")
}

companion object {
Expand Down Expand Up @@ -186,6 +269,16 @@ class ConnectProvider : DefaultPacketExtensionProvider<Connect>(Connect::class.j
}
connect.setPing(interval, timeout)
}
Connect.Exports.ELEMENT -> {
connect.setExports(
parseSourceNames(parser, Connect.Exports.ELEMENT, Connect.Export.ELEMENT)
)
}
Connect.Requests.ELEMENT -> {
connect.setRequests(
parseSourceNames(parser, Connect.Requests.ELEMENT, Connect.Request.ELEMENT)
)
}
}
}
XmlPullParser.Event.END_ELEMENT -> {
Expand All @@ -199,4 +292,34 @@ class ConnectProvider : DefaultPacketExtensionProvider<Connect>(Connect::class.j

return connect
}

/**
* Parses a container element (e.g. <exports>) holding a list of source-name items (e.g. <export name='...'/>),
* returning the list of source names. The parser is left positioned on the container's end element.
*/
@Throws(XmlPullParserException::class, IOException::class, SmackParsingException::class)
private fun parseSourceNames(parser: XmlPullParser, containerElement: String, itemElement: String): List<String> {
val names = mutableListOf<String>()
var done = false
while (!done) {
when (parser.next()) {
XmlPullParser.Event.START_ELEMENT -> {
if (parser.name == itemElement) {
val name = parser.getAttributeValue("", Connect.Export.NAME_ATTR_NAME)
?: throw SmackParsingException.RequiredAttributeMissingException(
"Missing 'name' attribute in $itemElement element"
)
names.add(name)
}
}
XmlPullParser.Event.END_ELEMENT -> {
if (parser.name == containerElement) {
done = true
}
}
else -> { /* ignore */ }
}
}
return names
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ object Colibri2JSONDeserializer {
setId(it.asText())
}

mediaSource[MediaSource.SYNTHETIC_ATTR_NAME]?.let {
require(it.isBoolean) { "Expected boolean for ${MediaSource.SYNTHETIC_ATTR_NAME}, got ${it.nodeType}" }
setSynthetic(it.asBoolean())
}

mediaSource[Colibri2JSONSerializer.SOURCES]?.let { sources ->
require(sources is ArrayNode) { "Expected array for sources, got ${sources.nodeType}" }
sources.forEach { addSource(JSONDeserializer.deserializeSource(it)) }
Expand Down Expand Up @@ -396,6 +401,32 @@ object Colibri2JSONDeserializer {
}
}

// Deserialize exports
connect[Connect.Exports.ELEMENT]?.let { exports ->
require(exports is ArrayNode) {
"Expected array for ${Connect.Exports.ELEMENT}, got ${exports.nodeType}"
}
exports.forEach { export ->
require(export.isTextual) {
"Expected string for ${Connect.Export.ELEMENT}, got ${export.nodeType}"
}
connectObj.addExport(export.asText())
}
}

// Deserialize requests
connect[Connect.Requests.ELEMENT]?.let { requests ->
require(requests is ArrayNode) {
"Expected array for ${Connect.Requests.ELEMENT}, got ${requests.nodeType}"
}
requests.forEach { request ->
require(request.isTextual) {
"Expected string for ${Connect.Request.ELEMENT}, got ${request.nodeType}"
}
connectObj.addRequest(request.asText())
}
}

addConnect(connectObj)
added = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ object Colibri2JSONSerializer {
return JsonNodeFactory.instance.objectNode().apply {
put(MediaSource.TYPE_ATTR_NAME, source.type.toString())
put(MediaSource.ID_NAME, source.id)
if (source.isSynthetic) put(MediaSource.SYNTHETIC_ATTR_NAME, true)
if (source.sources.isNotEmpty()) {
set<ObjectNode>(SOURCES, JSONSerializer.serializeSources(source.sources))
}
Expand Down Expand Up @@ -258,6 +259,22 @@ object Colibri2JSONSerializer {
pingObj.put(Connect.Ping.TIMEOUT_ATTR_NAME, ping.timeout)
set<ObjectNode>("ping", pingObj)
}

// Serialize exports
val exports = connect.getExports()
if (exports.isNotEmpty()) {
val exportsArray = JsonNodeFactory.instance.arrayNode()
exports.forEach { exportsArray.add(it) }
set<ArrayNode>(Connect.Exports.ELEMENT, exportsArray)
}

// Serialize requests
val requests = connect.getRequests()
if (requests.isNotEmpty()) {
val requestsArray = JsonNodeFactory.instance.arrayNode()
requests.forEach { requestsArray.add(it) }
set<ArrayNode>(Connect.Requests.ELEMENT, requestsArray)
}
}

private fun serializeConnects(connects: Connects) = JsonNodeFactory.instance.arrayNode().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/
package org.jitsi.xmpp.extensions.colibri2;

import org.jitsi.utils.*;
import org.jivesoftware.smack.util.*;
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class MediaSourceTest
{
Expand Down Expand Up @@ -50,4 +53,28 @@ public void parsingTest()
"An invalid type must result in an exception"
);
}

@Test
public void syntheticTest()
throws Exception
{
MediaSource notSynthetic =
provider.parse(PacketParserUtils.getParserFor("<media-source type='audio' id='id'/>"));
assertFalse(notSynthetic.isSynthetic(), "Synthetic must default to false when the attribute is absent");

MediaSource synthetic =
provider.parse(PacketParserUtils.getParserFor("<media-source type='audio' id='id' synthetic='true'/>"));
assertTrue(synthetic.isSynthetic());

MediaSource built = MediaSource.getBuilder()
.setType(MediaType.AUDIO)
.setId("id")
.setSynthetic(true)
.build();
assertTrue(built.isSynthetic());

// The attribute is only emitted when true, so it round-trips through XML.
MediaSource reparsed = provider.parse(PacketParserUtils.getParserFor(built.toXML().toString()));
assertTrue(reparsed.isSynthetic());
}
}
Loading
Loading