diff --git a/jicoco-mediajson/pom.xml b/jicoco-mediajson/pom.xml
new file mode 100644
index 00000000..bb7083e4
--- /dev/null
+++ b/jicoco-mediajson/pom.xml
@@ -0,0 +1,142 @@
+
+
+
+ 4.0.0
+
+ org.jitsi
+ jicoco-parent
+ 1.1-SNAPSHOT
+
+ jicoco-mediajson
+ 1.1-SNAPSHOT
+ jicoco-mediajson
+ Jitsi Common Components (Media JSON)
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+ ${jackson.version}
+
+
+
+ io.kotest
+ kotest-runner-junit5-jvm
+ ${kotest.version}
+ test
+
+
+ io.kotest
+ kotest-assertions-core-jvm
+ ${kotest.version}
+ test
+
+
+ com.googlecode.json-simple
+ json-simple
+ ${json.simple.version}
+
+
+
+ junit
+ junit
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ -opt-in=kotlin.ExperimentalStdlibApi
+
+
+ ${project.basedir}/src/main/kotlin
+
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+ -opt-in=kotlin.ExperimentalStdlibApi
+
+
+ ${project.basedir}/src/test/kotlin
+
+
+
+
+
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.10.1
+
+
+
+ default-compile
+ none
+
+
+
+ default-testCompile
+ none
+
+
+ java-compile
+ compile
+
+ compile
+
+
+
+ java-test-compile
+ test-compile
+
+ testCompile
+
+
+
+
+ 11
+
+ -Xlint:all
+
+
+
+
+
+
diff --git a/jicoco-mediajson/src/main/kotlin/org/jitsi/mediajson/MediaJson.kt b/jicoco-mediajson/src/main/kotlin/org/jitsi/mediajson/MediaJson.kt
new file mode 100644
index 00000000..c937df8f
--- /dev/null
+++ b/jicoco-mediajson/src/main/kotlin/org/jitsi/mediajson/MediaJson.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.mediajson
+
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.JsonDeserializer
+import com.fasterxml.jackson.databind.JsonSerializer
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+
+private val objectMapper = jacksonObjectMapper().apply {
+ configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+}
+
+/**
+ * This is based on the format used by VoxImplant here, hence the encoding of certain numeric fields as strings:
+ * https://voximplant.com/docs/guides/voxengine/websocket
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "event")
+@JsonSubTypes(
+ JsonSubTypes.Type(value = MediaEvent::class, name = "media"),
+ JsonSubTypes.Type(value = StartEvent::class, name = "start"),
+)
+sealed class Event(val event: String) {
+ fun toJson(): String = objectMapper.writeValueAsString(this)
+ companion object {
+ fun parse(s: String): Event = objectMapper.readValue(s, Event::class.java)
+ fun parse(s: List): List = s.map { objectMapper.readValue(it, Event::class.java) }
+ }
+}
+
+data class MediaEvent(
+ @JsonSerialize(using = Int2StringSerializer::class)
+ @JsonDeserialize(using = String2IntDeserializer::class)
+ val sequenceNumber: Int,
+ val media: Media
+) : Event("media")
+
+data class StartEvent(
+ @JsonSerialize(using = Int2StringSerializer::class)
+ @JsonDeserialize(using = String2IntDeserializer::class)
+ val sequenceNumber: Int,
+ val start: Start
+) : Event("start")
+
+data class MediaFormat(
+ val encoding: String,
+ val sampleRate: Int,
+ val channels: Int
+)
+data class Start(
+ val tag: String,
+ val mediaFormat: MediaFormat
+)
+
+data class Media(
+ val tag: String,
+ @JsonSerialize(using = Int2StringSerializer::class)
+ @JsonDeserialize(using = String2IntDeserializer::class)
+ val chunk: Int,
+ @JsonSerialize(using = Long2StringSerializer::class)
+ @JsonDeserialize(using = String2LongDeserializer::class)
+ val timestamp: Long,
+ val payload: String
+)
+
+class Int2StringSerializer : JsonSerializer() {
+ override fun serialize(value: Int, gen: JsonGenerator, p: SerializerProvider) {
+ gen.writeString(value.toString())
+ }
+}
+class String2IntDeserializer : JsonDeserializer() {
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Int {
+ return p.readValueAs(Int::class.java).toInt()
+ }
+}
+class Long2StringSerializer : JsonSerializer() {
+ override fun serialize(value: Long, gen: JsonGenerator, p: SerializerProvider) {
+ gen.writeString(value.toString())
+ }
+}
+class String2LongDeserializer : JsonDeserializer() {
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long {
+ return p.readValueAs(Long::class.java).toLong()
+ }
+}
diff --git a/jicoco-mediajson/src/test/kotlin/org/jitsi/mediajson/MediaJsonTest.kt b/jicoco-mediajson/src/test/kotlin/org/jitsi/mediajson/MediaJsonTest.kt
new file mode 100644
index 00000000..92623d34
--- /dev/null
+++ b/jicoco-mediajson/src/test/kotlin/org/jitsi/mediajson/MediaJsonTest.kt
@@ -0,0 +1,253 @@
+/*
+ * 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.mediajson
+
+import com.fasterxml.jackson.databind.exc.InvalidFormatException
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.spec.style.ShouldSpec
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.types.shouldBeInstanceOf
+import org.json.simple.JSONObject
+import org.json.simple.parser.JSONParser
+
+class MediaJsonTest : ShouldSpec() {
+ val parser = JSONParser()
+
+ init {
+ val seq = 123
+ val tag = "t"
+ context("StartEvent") {
+ val enc = "opus"
+ val sampleRate = 48000
+ val channels = 2
+ val event = StartEvent(seq, Start(tag, MediaFormat(enc, sampleRate, channels)))
+
+ context("Serializing") {
+ val parsed = parser.parse(event.toJson())
+
+ parsed.shouldBeInstanceOf()
+ parsed["event"] shouldBe "start"
+ // intentionally encoded as a string
+ parsed["sequenceNumber"] shouldBe seq.toString()
+ val start = parsed["start"]
+ start.shouldBeInstanceOf()
+ start["tag"] shouldBe tag
+ val mediaFormat = start["mediaFormat"]
+ mediaFormat.shouldBeInstanceOf()
+ mediaFormat["encoding"] shouldBe enc
+ mediaFormat["sampleRate"] shouldBe sampleRate
+ mediaFormat["channels"] shouldBe channels
+ }
+ context("Parsing") {
+ val parsed = Event.parse(event.toJson())
+ (parsed == event) shouldBe true
+ (parsed === event) shouldBe false
+
+ val parsedList = Event.parse(listOf(event.toJson(), event.toJson()))
+ parsedList.shouldBeInstanceOf>()
+ parsedList.size shouldBe 2
+ parsedList[0] shouldBe event
+ parsedList[1] shouldBe event
+ }
+ }
+ context("MediaEvent") {
+ val chunk = 213
+ val timestamp = 0x1_0000_ffff
+ val payload = "p"
+ val event = MediaEvent(seq, Media(tag, chunk, timestamp, payload))
+
+ context("Serializing") {
+ val parsed = parser.parse(event.toJson())
+ parsed.shouldBeInstanceOf()
+ parsed["event"] shouldBe "media"
+ // intentionally encoded as a string
+ parsed["sequenceNumber"] shouldBe seq.toString()
+ val media = parsed["media"]
+ media.shouldBeInstanceOf()
+ media["tag"] shouldBe tag
+ // intentionally encoded as a string
+ media["chunk"] shouldBe chunk.toString()
+ // intentionally encoded as a string
+ media["timestamp"] shouldBe timestamp.toString()
+ media["payload"] shouldBe payload
+ }
+ context("Parsing") {
+ val parsed = Event.parse(event.toJson())
+ (parsed == event) shouldBe true
+ (parsed === event) shouldBe false
+ }
+ }
+ context("Parsing valid samples") {
+ context("Start") {
+ val parsed = Event.parse(
+ """
+ {
+ "event": "start",
+ "sequenceNumber": "0",
+ "start": {
+ "tag": "incoming",
+ "mediaFormat": {
+ "encoding": "audio/x-mulaw",
+ "sampleRate": 8000,
+ "channels": 1
+ },
+ "customParameters": {
+ "text1":"12312"
+ }
+ }
+ }
+ """.trimIndent()
+ )
+
+ parsed.shouldBeInstanceOf()
+ parsed.event shouldBe "start"
+ parsed.sequenceNumber shouldBe 0
+ parsed.start.tag shouldBe "incoming"
+ parsed.start.mediaFormat.encoding shouldBe "audio/x-mulaw"
+ parsed.start.mediaFormat.sampleRate shouldBe 8000
+ parsed.start.mediaFormat.channels shouldBe 1
+ }
+ context("Start with sequence number as int") {
+ val parsed = Event.parse(
+ """
+ {
+ "event": "start",
+ "sequenceNumber": 0,
+ "start": {
+ "tag": "incoming",
+ "mediaFormat": {
+ "encoding": "audio/x-mulaw",
+ "sampleRate": 8000,
+ "channels": 1
+ },
+ "customParameters": {
+ "text1":"12312"
+ }
+ }
+ }
+ """.trimIndent()
+ )
+
+ parsed.shouldBeInstanceOf()
+ parsed.sequenceNumber shouldBe 0
+ }
+ context("Media") {
+ val parsed = Event.parse(
+ """
+ {
+ "event": "media",
+ "sequenceNumber": "2",
+ "media": {
+ "tag": "incoming",
+ "chunk": "1",
+ "timestamp": "5",
+ "payload": "no+JhoaJjpzSHxAKBgYJ...=="
+ }
+ }
+ """.trimIndent()
+ )
+
+ parsed.shouldBeInstanceOf()
+ parsed.event shouldBe "media"
+ parsed.sequenceNumber shouldBe 2
+ parsed.media.tag shouldBe "incoming"
+ parsed.media.chunk shouldBe 1
+ parsed.media.timestamp shouldBe 5
+ parsed.media.payload shouldBe "no+JhoaJjpzSHxAKBgYJ...=="
+ }
+ context("Media with seq/chunk/timestamp as numbers") {
+ val parsed = Event.parse(
+ """
+ {
+ "event": "media",
+ "sequenceNumber": 2,
+ "media": {
+ "tag": "incoming",
+ "chunk": 1,
+ "timestamp": 5,
+ "payload": "no+JhoaJjpzSHxAKBgYJ...=="
+ }
+ }
+ """.trimIndent()
+ )
+
+ parsed.shouldBeInstanceOf()
+ parsed.event shouldBe "media"
+ parsed.sequenceNumber shouldBe 2
+ parsed.media.tag shouldBe "incoming"
+ parsed.media.chunk shouldBe 1
+ parsed.media.timestamp shouldBe 5
+ parsed.media.payload shouldBe "no+JhoaJjpzSHxAKBgYJ...=="
+ }
+ }
+ context("Parsing invalid samples") {
+ context("Invalid sequence number") {
+ shouldThrow {
+ Event.parse(
+ """
+ {
+ "event": "media",
+ "sequenceNumber": "not a number",
+ "media": {
+ "tag": "incoming",
+ "chunk": "1",
+ "timestamp": "5",
+ "payload": "no+JhoaJjpzSHxAKBgYJ...=="
+ }
+ }
+ """.trimIndent()
+ )
+ }
+ }
+ context("Invalid chunk") {
+ shouldThrow {
+ Event.parse(
+ """
+ {
+ "event": "media",
+ "sequenceNumber": "1",
+ "media": {
+ "tag": "incoming",
+ "chunk": "not a number",
+ "timestamp": "5",
+ "payload": "no+JhoaJjpzSHxAKBgYJ...=="
+ }
+ }
+ """.trimIndent()
+ )
+ }
+ }
+ context("Invalid timestamp") {
+ shouldThrow {
+ Event.parse(
+ """
+ {
+ "event": "media",
+ "sequenceNumber": "1",
+ "media": {
+ "tag": "incoming",
+ "chunk": "1",
+ "timestamp": "not a number",
+ "payload": "no+JhoaJjpzSHxAKBgYJ...=="
+ }
+ }
+ """.trimIndent()
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/jicoco-metrics-jetty/pom.xml b/jicoco-metrics-jetty/pom.xml
new file mode 100644
index 00000000..f45ed952
--- /dev/null
+++ b/jicoco-metrics-jetty/pom.xml
@@ -0,0 +1,217 @@
+
+
+
+
+ 4.0.0
+
+
+ org.jitsi
+ jicoco-parent
+ 1.1-SNAPSHOT
+
+
+ jicoco-metrics-jetty
+ 1.1-SNAPSHOT
+ jicoco-metrics-jetty
+ Jitsi Common Components (Metrics Jetty)
+
+
+
+ ${project.groupId}
+ jicoco-metrics
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ ${jetty.version}
+
+
+ org.eclipse.jetty
+ jetty-servlets
+ ${jetty.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty.version}
+
+
+ org.glassfish.jersey.containers
+ jersey-container-jetty-http
+ ${jersey.version}
+
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet
+ ${jersey.version}
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ ${jersey.version}
+
+
+
+
+ org.junit.platform
+ junit-platform-launcher
+ 1.10.0
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+ io.kotest
+ kotest-runner-junit5-jvm
+ ${kotest.version}
+ test
+
+
+ io.kotest
+ kotest-assertions-core-jvm
+ ${kotest.version}
+ test
+
+
+ org.glassfish.jersey.test-framework
+ jersey-test-framework-core
+ ${jersey.version}
+ test
+
+
+ junit
+ junit
+
+
+
+
+ org.glassfish.jersey.test-framework.providers
+ jersey-test-framework-provider-jetty
+ ${jersey.version}
+ test
+
+
+ junit
+ junit
+
+
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ -opt-in=kotlin.ExperimentalStdlibApi
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/main/java
+
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+ -opt-in=kotlin.ExperimentalStdlibApi
+
+
+ ${project.basedir}/src/test/kotlin
+ ${project.basedir}/src/test/java
+
+
+
+
+
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.10.1
+
+
+
+ default-compile
+ none
+
+
+
+ default-testCompile
+ none
+
+
+ java-compile
+ compile
+
+ compile
+
+
+
+ java-test-compile
+ test-compile
+
+ testCompile
+
+
+
+
+ 11
+
+ -Xlint:all
+
+
+
+
+
+
diff --git a/jicoco-metrics/src/main/java/org/jitsi/rest/prometheus/Prometheus.java b/jicoco-metrics-jetty/src/main/java/org/jitsi/rest/prometheus/Prometheus.java
similarity index 100%
rename from jicoco-metrics/src/main/java/org/jitsi/rest/prometheus/Prometheus.java
rename to jicoco-metrics-jetty/src/main/java/org/jitsi/rest/prometheus/Prometheus.java
diff --git a/jicoco-metrics/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt b/jicoco-metrics-jetty/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt
similarity index 100%
rename from jicoco-metrics/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt
rename to jicoco-metrics-jetty/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt
diff --git a/jicoco-metrics/pom.xml b/jicoco-metrics/pom.xml
index 312e354e..dd8356ae 100644
--- a/jicoco-metrics/pom.xml
+++ b/jicoco-metrics/pom.xml
@@ -50,37 +50,6 @@
simpleclient_common
${prometheus.version}
-
- org.eclipse.jetty
- jetty-servlet
- ${jetty.version}
-
-
- org.eclipse.jetty
- jetty-servlets
- ${jetty.version}
-
-
- org.eclipse.jetty
- jetty-util
- ${jetty.version}
-
-
- org.glassfish.jersey.containers
- jersey-container-jetty-http
- ${jersey.version}
-
-
- org.glassfish.jersey.containers
- jersey-container-servlet
- ${jersey.version}
-
-
- org.glassfish.jersey.inject
- jersey-hk2
- ${jersey.version}
-
-
org.junit.platform
@@ -124,18 +93,6 @@
-
- org.glassfish.jersey.test-framework.providers
- jersey-test-framework-provider-jetty
- ${jersey.version}
- test
-
-
- junit
- junit
-
-
-
org.junit.vintage
junit-vintage-engine
@@ -163,7 +120,6 @@
${project.basedir}/src/main/kotlin
- ${project.basedir}/src/main/java
diff --git a/jicoco/pom.xml b/jicoco/pom.xml
index 224fadc3..2365e371 100644
--- a/jicoco/pom.xml
+++ b/jicoco/pom.xml
@@ -106,6 +106,12 @@
jersey-media-json-jackson
${jersey.version}
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+ ${jackson.version}
+
+