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 mediajson (json definitions). #205

Merged
merged 5 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions jicoco/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>${jackson.version}</version>
</dependency>

<!-- jersey relies on this version of javassist or we get hk2
errors, but other libs (Powermock, at this time) also rely on
it but an older version, and that version is getting selected
Expand Down
106 changes: 106 additions & 0 deletions jicoco/src/main/kotlin/org/jitsi/mediajson/MediaJson.kt
Original file line number Diff line number Diff line change
@@ -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 toXml(): String = objectMapper.writeValueAsString(this)
companion object {
fun parse(s: String): Event = objectMapper.readValue(s, Event::class.java)
fun parse(s: List<String>): List<Event> = 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<Int>() {
override fun serialize(value: Int, gen: JsonGenerator, p: SerializerProvider) {
gen.writeString(value.toString())
}
}
class String2IntDeserializer : JsonDeserializer<Int>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Int {
return p.readValueAs(Int::class.java).toInt()
}
}
class Long2StringSerializer : JsonSerializer<Long>() {
override fun serialize(value: Long, gen: JsonGenerator, p: SerializerProvider) {
gen.writeString(value.toString())
}
}
class String2LongDeserializer : JsonDeserializer<Long>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long {
return p.readValueAs(Long::class.java).toLong()
}
}
253 changes: 253 additions & 0 deletions jicoco/src/test/kotlin/org/jitsi/mediajson/MediaJsonTest.kt
Original file line number Diff line number Diff line change
@@ -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.toXml())

parsed.shouldBeInstanceOf<JSONObject>()
parsed["event"] shouldBe "start"
// intentionally encoded as a string
parsed["sequenceNumber"] shouldBe seq.toString()
val start = parsed["start"]
start.shouldBeInstanceOf<JSONObject>()
start["tag"] shouldBe tag
val mediaFormat = start["mediaFormat"]
mediaFormat.shouldBeInstanceOf<JSONObject>()
mediaFormat["encoding"] shouldBe enc
mediaFormat["sampleRate"] shouldBe sampleRate
mediaFormat["channels"] shouldBe channels
}
context("Parsing") {
val parsed = Event.parse(event.toXml())
(parsed == event) shouldBe true
(parsed === event) shouldBe false

val parsedList = Event.parse(listOf(event.toXml(), event.toXml()))
parsedList.shouldBeInstanceOf<List<Event>>()
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.toXml())
parsed.shouldBeInstanceOf<JSONObject>()
parsed["event"] shouldBe "media"
// intentionally encoded as a string
parsed["sequenceNumber"] shouldBe seq.toString()
val media = parsed["media"]
media.shouldBeInstanceOf<JSONObject>()
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.toXml())
(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<StartEvent>()
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<StartEvent>()
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<MediaEvent>()
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<MediaEvent>()
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<InvalidFormatException> {
Event.parse(
"""
{
"event": "media",
"sequenceNumber": "not a number",
"media": {
"tag": "incoming",
"chunk": "1",
"timestamp": "5",
"payload": "no+JhoaJjpzSHxAKBgYJ...=="
}
}
""".trimIndent()
)
}
}
context("Invalid chunk") {
shouldThrow<InvalidFormatException> {
Event.parse(
"""
{
"event": "media",
"sequenceNumber": "1",
"media": {
"tag": "incoming",
"chunk": "not a number",
"timestamp": "5",
"payload": "no+JhoaJjpzSHxAKBgYJ...=="
}
}
""".trimIndent()
)
}
}
context("Invalid timestamp") {
shouldThrow<InvalidFormatException> {
Event.parse(
"""
{
"event": "media",
"sequenceNumber": "1",
"media": {
"tag": "incoming",
"chunk": "1",
"timestamp": "not a number",
"payload": "no+JhoaJjpzSHxAKBgYJ...=="
}
}
""".trimIndent()
)
}
}
}
}
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.0</junit.version>
<kotlin.version>1.9.10</kotlin.version>
<jackson.version>2.17.1</jackson.version>
<kotest.version>5.7.2</kotest.version>
<mockk.version>1.13.8</mockk.version>
<prometheus.version>0.16.0</prometheus.version>
Expand Down
Loading