diff --git a/CHANGELOG.md b/CHANGELOG.md index 580e915d3cdbf56ae3e1051e6386f17e55f93520..280f099aa11e683b963fe4074217f8263a08b583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Event content types for VOIP. + ### Changed - `AccessTokenAuthenticationFunction` now allows servers to set `soft_logout`. diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/ClientEvent.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/ClientEvent.kt index 8ca29a7432ab6103bbce85ecbb56162540151bfa..ad51c5383d4254a789dbb87c1ece89e162f446bb 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/ClientEvent.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/ClientEvent.kt @@ -7,11 +7,16 @@ import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId /** - * @see matrix spec + * A client event with a specific type given by the generic parameter C. + * + * @see Matrix events */ sealed interface ClientEvent : Event { /** - * @see matrix spec + * Matrix room event. Either a message event or a state event. + * + * @see Types of matrix room events + * @see Matrix room event format */ sealed interface RoomEvent : ClientEvent { val id: EventId @@ -20,6 +25,12 @@ sealed interface ClientEvent : Event { val originTimestamp: Long val unsigned: UnsignedRoomEventData? + /** + * Matrix message event + * + * @see Types of matrix room events + * @see Matrix room event format + */ @Serializable data class MessageEvent( @SerialName("content") override val content: C, @@ -31,7 +42,10 @@ sealed interface ClientEvent : Event { ) : RoomEvent /** - * @see matrix spec + * Matrix state event + * + * @see Types of matrix room events + * @see Matrix room event format */ @Serializable data class StateEvent( diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/EventContent.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/EventContent.kt index 29d4fb1a47ae823c6d3c5b995d54de04885194bb..69234568bacc22331917fa4d11e79d110001b6ba 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/EventContent.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/EventContent.kt @@ -7,6 +7,11 @@ import net.folivo.trixnity.core.model.events.m.RelatesTo sealed interface EventContent +/** + * Content of a matrix room event + * + * @see Types of matrix room events + */ sealed interface RoomEventContent : EventContent { /** * @see matrix spec @@ -14,11 +19,17 @@ sealed interface RoomEventContent : EventContent { val externalUrl: String? } +/** + * Content of a matrix message event + */ interface MessageEventContent : RoomEventContent { val relatesTo: RelatesTo? val mentions: Mentions? } +/** + * Content of a matrix state event + */ interface StateEventContent : RoomEventContent interface ToDeviceEventContent : EventContent diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/CallEventContent.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/CallEventContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..67c789cbaec9faac38a72972a9dcf556412187a3 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/CallEventContent.kt @@ -0,0 +1,266 @@ +package net.folivo.trixnity.core.model.events.m.call + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.jsonPrimitive +import net.folivo.trixnity.core.model.events.MessageEventContent +import net.folivo.trixnity.core.model.events.m.Mentions +import net.folivo.trixnity.core.model.events.m.RelatesTo + +/** + * Matrix call event content + * + * @see matrix spec + */ +sealed interface CallEventContent : MessageEventContent { + val callId: String + val version: String + val partyId: String? + + /** + * Matrix call invite content + * + * @see matrix spec + */ + @Serializable + data class Invite( + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String, + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String? = null, + @SerialName("invitee") val invitee: String? = null, + @SerialName("lifetime") val lifetime: Long, + @SerialName("offer") val offer: Offer, + + // Added in v1.10: + @SerialName("sdp_stream_metadata") val sdpStreamMetadata: Map?, + ) : CallEventContent { + override val externalUrl: String? = null + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + + @Serializable + enum class OfferType { + @SerialName("offer") OFFER, + } + + @Serializable + data class Offer( + @SerialName("sdp") val sdp: String, + @SerialName("type") val type: OfferType, + ) + } + + /** + * Matrix call candidates content + * + * @see matrix spec + */ + @Serializable + data class Candidates( + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String, + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String? = null, + @SerialName("candidates") val candidates: List, + ) : CallEventContent { + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + override val externalUrl: String? = null + + @Serializable + data class Candidate( + @SerialName("candidate") val candidate: String, + @SerialName("sdpMLineIndex") val sdpMLineIndex: Long? = null, + @SerialName("sdpMid") val sdpMid: String? = null, + ) + } + + /** + * Matrix call answer content + * + * @see matrix spec + */ + @Serializable + data class Answer( + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String, + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String? = null, + @SerialName("answer") val answer: Answer, + + // Added in v1.10: + + @SerialName("sdp_stream_metadata") val sdpStreamMetadata: Map? = null, + ) : CallEventContent { + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + override val externalUrl: String? = null + + @Serializable + enum class AnswerType { + @SerialName("answer") ANSWER, + } + + @Serializable + data class Answer( + @SerialName("sdp") val sdp: String, + @SerialName("type") val type: AnswerType, + ) + } + + /** + * Matrix call hangup content + * + * @see matrix spec + */ + @Serializable + data class Hangup( + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String, + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String? = null, + @SerialName("reason") val reason: Reason? = null, + ) : CallEventContent { + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + override val externalUrl: String? = null + + @Serializable + enum class Reason { + @SerialName("ice_failed") ICE_FAILED, + @SerialName("invite_timeout") INVITE_TIMEOUT, + // Added in v1.7: + @SerialName("ice_timeout") ICE_TIMEOUT, + @SerialName("user_hangup") USER_HANGUP, + @SerialName("user_media_failed") USER_MEDIA_FAILED, + @SerialName("user_busy") USER_BUSY, + @SerialName("unknown_error") UNKNOWN_ERROR, + } + } + + // Added in v1.7: + + /** + * Matrix call negotiate content + * + * @see matrix spec + */ + @Serializable + data class Negotiate( + @EncodeDefault + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String = "1", + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String, + @SerialName("description") val description: Description, + @SerialName("lifetime") val lifetime: Long, + + // Added in v1.10: + + @SerialName("sdp_stream_metadata") val sdpStreamMetadata: Map? = null + ) : CallEventContent { + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + override val externalUrl: String? = null + + @Serializable + enum class DescriptionType { + @SerialName("offer") OFFER, + @SerialName("answer") ANSWER + } + + @Serializable + data class Description( + @SerialName("sdp") val sdp: String, + @SerialName("type") val type: DescriptionType, + ) + } + + /** + * Matrix call reject content + * + * @see matrix spec + */ + @Serializable + data class Reject( + @EncodeDefault + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String = "1", + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String, + ) : CallEventContent { + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + override val externalUrl: String? = null + } + + /** + * Matrix call select answer content + * + * @see matrix spec + */ + @Serializable + data class SelectAnswer( + @EncodeDefault + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String = "1", + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String, + @SerialName("selected_party_id") val selectedPartyId: String, + ) : CallEventContent { + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + override val externalUrl: String? = null + } + + // Added in V1.11: + + /** + * Matrix call SDP stream metadata changed event content + * + * @see matrix spec + */ + @Serializable + data class SdpStreamMetadataChanged( + @EncodeDefault + @Serializable(with = VersionSerializer::class) + @SerialName("version") override val version: String = "1", + @SerialName("call_id") override val callId: String, + @SerialName("party_id") override val partyId: String, + @SerialName("sdp_stream_metadata") val sdpStreamMetadata: Map, + ) : CallEventContent { + override val relatesTo: RelatesTo? = null + override val mentions: Mentions? = null + override val externalUrl: String? = null + } +} + +internal object VersionSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("net.folivo.trixnity.core.model.call.Version") + + override fun deserialize(decoder: Decoder): String { + val jsonDecoder = decoder as? JsonDecoder ?: throw IllegalStateException("Expected JsonDecoder") + val element = jsonDecoder.decodeJsonElement().jsonPrimitive + + // All VoIP events have a version field. This is used to determine whether devices support this new version of + // the protocol. For example, clients can use this field to know whether to expect an m.call.select_answer event + // from their opponent. If clients see events with version other than 0 or "1" (including, for example, the + // numeric value 1), they should treat these the same as if they had version == "1". + // @see matrix spec + return element.content + } + + override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: String) { + when (value) { + "0" -> encoder.encodeLong(0) + else -> encoder.encodeString(value) + } + } +} diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/StreamMetadata.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/StreamMetadata.kt new file mode 100644 index 0000000000000000000000000000000000000000..b40561a16251243edd5ed55d59bc5f1a82ed9863 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/StreamMetadata.kt @@ -0,0 +1,21 @@ +package net.folivo.trixnity.core.model.events.m.call + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class Purpose { + @SerialName("m.usermedia") USERMEDIA, + @SerialName("m.screenshare") SCREENSHARE, +} + +@Serializable +data class StreamMetadata( + @SerialName("purpose") val purpose: Purpose, + + // Added in v1.11: + + @SerialName("audio_muted") val audioMuted: Boolean? = false, + @SerialName("video_muted") val videoMuted: Boolean? = false, +) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/room/RoomMessageEventContent.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/room/RoomMessageEventContent.kt index 4a19f600579e10d619ea878ddd5a7d5edbf9ae89..2a81464093d95727887a96aeb248cb4c25dbf87a 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/room/RoomMessageEventContent.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/room/RoomMessageEventContent.kt @@ -18,6 +18,9 @@ import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent.* import net.folivo.trixnity.core.model.events.m.key.verification.VerificationRequest as IVerificationRequest /** + * Matrix room message event content + * + * Room messages have "type": "m.room.message". * @see matrix spec */ @Serializable(with = RoomMessageEventContentSerializer::class) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DefaultEventContentSerializerMappings.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DefaultEventContentSerializerMappings.kt index 7d5baf81d58f76a12d158c69a202028080b587c4..18e3f6339c27fcb51a6deff02f9662806c584b1e 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DefaultEventContentSerializerMappings.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DefaultEventContentSerializerMappings.kt @@ -1,6 +1,7 @@ package net.folivo.trixnity.core.serialization.events import net.folivo.trixnity.core.model.events.m.* +import net.folivo.trixnity.core.model.events.m.call.CallEventContent import net.folivo.trixnity.core.model.events.m.crosssigning.MasterKeyEventContent import net.folivo.trixnity.core.model.events.m.crosssigning.SelfSigningKeyEventContent import net.folivo.trixnity.core.model.events.m.crosssigning.UserSigningKeyEventContent @@ -28,6 +29,14 @@ val DefaultEventContentSerializerMappings = createEventContentSerializerMappings messageOf("m.key.verification.accept") messageOf("m.key.verification.key") messageOf("m.key.verification.mac") + messageOf("m.call.invite") + messageOf("m.call.candidates") + messageOf("m.call.answer") + messageOf("m.call.hangup") + messageOf("m.call.negotiate") + messageOf("m.call.reject") + messageOf("m.call.select_answer") + messageOf("m.call.sdp_stream_metadata_changed") stateOf("m.room.avatar") stateOf("m.room.canonical_alias") @@ -84,4 +93,4 @@ val DefaultEventContentSerializerMappings = createEventContentSerializerMappings roomAccountDataOf("m.fully_read") roomAccountDataOf("m.marked_unread") roomAccountDataOf("m.tag") -} \ No newline at end of file +} diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/m/call/CallEventContentSerializerTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/m/call/CallEventContentSerializerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e4cda559aa8222918dadec7dab71270f457a1086 --- /dev/null +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/m/call/CallEventContentSerializerTest.kt @@ -0,0 +1,305 @@ +package net.folivo.trixnity.core.serialization.m.call + +import kotlinx.serialization.encodeToString +import net.folivo.trixnity.core.model.events.m.call.CallEventContent +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Answer.Answer +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Answer.AnswerType +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Candidates.Candidate +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Invite.Offer +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Invite.OfferType +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Hangup.Reason +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Negotiate.Description +import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Negotiate.DescriptionType +import net.folivo.trixnity.core.model.events.m.call.Purpose +import net.folivo.trixnity.core.model.events.m.call.StreamMetadata +import net.folivo.trixnity.core.serialization.createMatrixEventJson +import net.folivo.trixnity.core.serialization.trimToFlatJson +import kotlin.test.Test +import kotlin.test.assertEquals + +class CallEventContentSerializerTest { + + private val json = createMatrixEventJson() + + ////////////////// m.call.invite ////////////////// + + private val testInvite = CallEventContent.Invite( + version = "1", + callId = "0123", + offer = Offer(sdp = """ + v=0 + o=- 6584580628695956864 2 IN IP4 127.0.0.1 + """.trimIndent(), + type = OfferType.OFFER), + lifetime = 30000, + sdpStreamMetadata = mapOf( + "271828182845" to StreamMetadata(Purpose.SCREENSHARE), + "314159265358" to StreamMetadata(Purpose.USERMEDIA), + ) + ) + + private val serializedInvite = """{ + "version": "1", + "call_id": "0123", + "lifetime": 30000, + "offer": { + "sdp": "v=0\no=- 6584580628695956864 2 IN IP4 127.0.0.1", + "type": "offer" + }, + "sdp_stream_metadata": { + "271828182845": { + "purpose": "m.screenshare", + "audio_muted": false, + "video_muted": false + }, + "314159265358": { + "purpose": "m.usermedia", + "audio_muted": false, + "video_muted": false + } + } + }""".trimToFlatJson() + + @Test + fun shouldSerializeCallInvite() { + val result = json.encodeToString(testInvite) + assertEquals(serializedInvite, result) + } + + @Test + fun shouldDeserializeCallInvite() { + val result: CallEventContent.Invite = json.decodeFromString(serializedInvite) + assertEquals(testInvite, result) + } + + ////////////////// m.call.candidates ////////////////// + + private val testCandidates = CallEventContent.Candidates( + version = "0", + callId = "0123", + candidates = listOf( + Candidate( + candidate = "candidate:423458654 1 udp 2130706431 192.168.0.100 51008 typ host generation 0 ufrag 3f8a", + sdpMid = "audio", + sdpMLineIndex = 0 + ), + Candidate( + candidate = "candidate:423458654 2 udp 2130706431 192.168.0.100 51009 typ host generation 0 ufrag 4839", + sdpMid = "audio", + sdpMLineIndex = 1 + ), + ), + ) + + private val serializedCandidates = """{ + "version": 0, + "call_id": "0123", + "candidates": [ + {"candidate":"candidate:423458654 1 udp 2130706431 192.168.0.100 51008 typ host generation 0 ufrag 3f8a","sdpMLineIndex":0,"sdpMid":"audio"}, + {"candidate":"candidate:423458654 2 udp 2130706431 192.168.0.100 51009 typ host generation 0 ufrag 4839","sdpMLineIndex":1,"sdpMid":"audio"} + ] + }""".trimToFlatJson() + + @Test + fun shouldSerializeCallCandidates() { + val result = json.encodeToString(testCandidates) + assertEquals(serializedCandidates, result) + } + + @Test + fun shouldDeserializeCallCandidates() { + val result: CallEventContent.Candidates = json.decodeFromString(serializedCandidates) + assertEquals(testCandidates, result) + } + + ////////////////// m.call.answer ////////////////// + + private val testAnswer = CallEventContent.Answer( + version = "0", + callId = "0123", + partyId = null, + answer = Answer(sdp = "v=0", type = AnswerType.ANSWER), + sdpStreamMetadata = null, + ) + private val serializedAnswer = """{ + "version": 0, + "call_id": "0123", + "answer": {"sdp":"v=0","type":"answer"} + }""".trimToFlatJson() + + @Test + fun shouldSerializeCallAnswer() { + val result = json.encodeToString(testAnswer) + assertEquals(serializedAnswer, result) + } + + @Test + fun shouldDeserializeCallAnswer() { + val result: CallEventContent.Answer = json.decodeFromString(serializedAnswer) + assertEquals(testAnswer, result) + } + + ////////////////// m.call.hangup ////////////////// + + private val testHangup = CallEventContent.Hangup( + callId = "0123", + version = "0", + partyId = null, + reason = Reason.INVITE_TIMEOUT + ) + + private val serializedHangup = """{ + "version": 0, + "call_id": "0123", + "reason": "invite_timeout" + }""".trimToFlatJson() + + @Test + fun shouldSerializeCallHangup() { + val result = json.encodeToString(testHangup) + assertEquals(serializedHangup, result) + } + + @Test + fun shouldDeserializeCallHangup() { + val result: CallEventContent.Hangup = json.decodeFromString(serializedHangup) + assertEquals(testHangup, result) + } + + ////////////////// m.call.negotiate ////////////////// + + private val testNegotiate = CallEventContent.Negotiate( + callId = "0123", + partyId = "67890", + description = Description("v=0", DescriptionType.OFFER), + lifetime = 10000, + sdpStreamMetadata = mapOf( + "271828182845" to StreamMetadata(Purpose.SCREENSHARE), + "314159265358" to StreamMetadata(Purpose.USERMEDIA), + ), + ) + private val serializedNegotiate = """{ + "version": "1", + "call_id": "0123", + "party_id": "67890", + "description": { + "sdp": "v=0", + "type": "offer" + }, + "lifetime": 10000, + "sdp_stream_metadata": { + "271828182845": { + "purpose": "m.screenshare", + "audio_muted": false, + "video_muted": false + }, + "314159265358": { + "purpose": "m.usermedia", + "audio_muted": false, + "video_muted": false + } + } + }""".trimToFlatJson() + + @Test + fun shouldSerializeCallNegotiate() { + val result = json.encodeToString(testNegotiate) + assertEquals(serializedNegotiate, result) + } + + @Test + fun shouldDeserializeCallNegotiate() { + val result: CallEventContent.Negotiate = json.decodeFromString(serializedNegotiate) + assertEquals(testNegotiate, result) + } + + ////////////////// m.call.reject ////////////////// + + private val testReject = CallEventContent.Reject(callId = "0123", partyId = "23423") + + private val serializedReject = """{ + "version": "1", + "call_id": "0123", + "party_id": "23423" + }""".trimToFlatJson() + + @Test + fun shouldSerializeCallReject() { + val result = json.encodeToString(testReject) + assertEquals(serializedReject, result) + } + + @Test + fun shouldDeserializeCallReject() { + val result: CallEventContent.Reject = json.decodeFromString(serializedReject) + assertEquals(testReject, result) + } + + ////////////////// m.call.select_answer ////////////////// + + private val testSelectAnswer = CallEventContent.SelectAnswer( + version = "0", + callId = "0123", + partyId = "23423", + selectedPartyId = "67890" + ) + + private val serializedSelectAnswer = """{ + "version": 0, + "call_id": "0123", + "party_id": "23423", + "selected_party_id": "67890" + }""".trimToFlatJson() + + @Test + fun shouldSerializeCallSelectAnswer() { + val result = json.encodeToString(testSelectAnswer) + assertEquals(serializedSelectAnswer, result) + } + + @Test + fun shouldDeserializeCallSelectAnswer() { + val result: CallEventContent.SelectAnswer = json.decodeFromString(serializedSelectAnswer) + assertEquals(testSelectAnswer, result) + } + + ////////////////// m.call.sdp_stream_metadata_changed ////////////////// + + private val testSdpStreamMetadataChanged = CallEventContent.SdpStreamMetadataChanged( + callId = "0123", + partyId = "67890", + sdpStreamMetadata = mapOf( + "271828182845" to StreamMetadata(Purpose.SCREENSHARE, audioMuted = true), + "314159265358" to StreamMetadata(Purpose.USERMEDIA, videoMuted = true), + ), + ) + private val serializedSdpStreamMetadataChanged = """{ + "version": "1", + "call_id": "0123", + "party_id": "67890", + "sdp_stream_metadata": { + "271828182845": { + "purpose": "m.screenshare", + "audio_muted": true, + "video_muted": false + }, + "314159265358": { + "purpose": "m.usermedia", + "audio_muted": false, + "video_muted": true + } + } + }""".trimToFlatJson() + + @Test + fun shouldDeserializeSdpStreamMetadataChanged() { + val result: CallEventContent.SdpStreamMetadataChanged = json.decodeFromString(serializedSdpStreamMetadataChanged) + assertEquals(testSdpStreamMetadataChanged, result) + } + + @Test + fun shouldSerializeSdpStreamMetadataChanged() { + val result = json.encodeToString(testSdpStreamMetadataChanged) + assertEquals(serializedSdpStreamMetadataChanged, result) + } +}