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} + +