From 73ab62352f601f777928862749575d3e1624f447 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 28 Jan 2025 20:23:12 +0100 Subject: [PATCH 1/4] Add more precise comments --- .../trixnity/core/model/events/ClientEvent.kt | 20 ++++++++++++++++--- .../core/model/events/EventContent.kt | 11 ++++++++++ .../events/m/room/RoomMessageEventContent.kt | 3 +++ 3 files changed, 31 insertions(+), 3 deletions(-) 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 8ca29a743..ad51c5383 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 29d4fb1a4..69234568b 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/room/RoomMessageEventContent.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/room/RoomMessageEventContent.kt index 4a19f6005..2a8146409 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) -- GitLab From 2398039916ee6f5de523674cdbcb2908743d43cf Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 28 Jan 2025 20:27:31 +0100 Subject: [PATCH 2/4] Add event types for VOIP --- .../model/events/m/call/CallEventContent.kt | 117 +++++++++++++++++ .../DefaultEventContentSerializerMappings.kt | 5 + .../m/call/CallEventContentSerializerTest.kt | 124 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/CallEventContent.kt create mode 100644 trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/m/call/CallEventContentSerializerTest.kt 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 000000000..1c79caff1 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/CallEventContent.kt @@ -0,0 +1,117 @@ +package net.folivo.trixnity.core.model.events.m.call + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +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: Long + + /** + * Matrix call invite content + * + * @see matrix spec + */ + @Serializable + data class Invite( + @SerialName("version") override val version: Long, + @SerialName("call_id") override val callId: String, + @SerialName("lifetime") val lifetime: Long, + @SerialName("offer") val offer: Offer, + ) : 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( + @SerialName("version") override val version: Long, + @SerialName("call_id") override val callId: String, + @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, + @SerialName("sdpMid") val sdpMid: String, + ) + } + + /** + * Matrix call answer content + * + * @see matrix spec + */ + @Serializable + data class Answer( + @SerialName("version") override val version: Long, + @SerialName("call_id") override val callId: String, + @SerialName("answer") val answer: Answer, + ) : 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( + @SerialName("version") override val version: Long, + @SerialName("call_id") override val callId: String, + @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, + } + } +} 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 7d5baf81d..36905edbe 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,10 @@ 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") stateOf("m.room.avatar") stateOf("m.room.canonical_alias") 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 000000000..9233bddb3 --- /dev/null +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/m/call/CallEventContentSerializerTest.kt @@ -0,0 +1,124 @@ +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.serialization.createMatrixEventJson +import net.folivo.trixnity.core.serialization.trimToFlatJson +import kotlin.test.Test +import kotlin.test.assertEquals + +class CallEventContentSerializerTest { + + private val json = createMatrixEventJson() + + private val testInvite = CallEventContent.Invite( + callId = "0123", + offer = Offer(sdp = """ + v=0 + o=- 6584580628695956864 2 IN IP4 127.0.0.1 + """.trimIndent(), + type = OfferType.OFFER), + lifetime = 30000, + version = 0, + ) + + private val serializedInvite = + """{ + "version":0, + "call_id":"0123", + "lifetime":30000, + "offer":{"sdp":"v=0\no=- 6584580628695956864 2 IN IP4 127.0.0.1","type":"offer"} + }""".trimToFlatJson() + + private val testCandidates = CallEventContent.Candidates( + 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 + ), + ), + version = 0, + ) + + 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() + + private val testAnswer = CallEventContent.Answer( + answer = Answer(sdp = "v=0", type = AnswerType.ANSWER), + callId = "0123", + version = 0, + ) + private val serializedAnswer = """{ + "version":0, + "call_id":"0123", + "answer":{"sdp":"v=0","type":"answer"} + }""".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) + } + + @Test + fun shouldSerializeCallCandidates() { + val result = json.encodeToString(testCandidates) + assertEquals(serializedCandidates, result) + } + + @Test + fun shouldDeserializeCallCandidates() { + val result: CallEventContent.Candidates = json.decodeFromString(serializedCandidates) + assertEquals(testCandidates, result) + } + + @Test + fun shouldSerializeCallAnswer() { + val result = json.encodeToString(testAnswer) + assertEquals(serializedAnswer, result) + } + + @Test + fun shouldDeserializeCallAnswer() { + val result: CallEventContent.Answer = json.decodeFromString(serializedAnswer) + assertEquals(testAnswer, result) + } + + @Test + fun shouldSerializeCallHangup() { + val testHangup = CallEventContent.Hangup(version = 0, callId = "0123", reason = Reason.INVITE_TIMEOUT) + val result = json.encodeToString(testHangup) + assertEquals("""{"version":0,"call_id":"0123","reason":"invite_timeout"}""", result) + } + + @Test + fun shouldDeserializeCallHangup() { + val result: CallEventContent.Hangup = json.decodeFromString("""{"call_id":"0123","reason":"ice_failed","version":0}""") + assertEquals(CallEventContent.Hangup(version = 0, callId = "0123", reason = Reason.ICE_FAILED), result) + } +} -- GitLab From d4466f88871ea818e3d7bff2564884561694886d Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 28 Jan 2025 23:27:15 +0100 Subject: [PATCH 3/4] Add support for version 1 VOIP events --- .../model/events/m/call/CallEventContent.kt | 171 ++++++++++- .../model/events/m/call/StreamMetadata.kt | 21 ++ .../DefaultEventContentSerializerMappings.kt | 6 +- .../m/call/CallEventContentSerializerTest.kt | 267 +++++++++++++++--- 4 files changed, 410 insertions(+), 55 deletions(-) create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/m/call/StreamMetadata.kt 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 index 1c79caff1..67c789cba 100644 --- 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 @@ -1,7 +1,14 @@ 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 @@ -13,19 +20,26 @@ import net.folivo.trixnity.core.model.events.m.RelatesTo */ sealed interface CallEventContent : MessageEventContent { val callId: String - val version: Long + val version: String + val partyId: String? /** * Matrix call invite content * - * @see matrix spec + * @see matrix spec */ @Serializable data class Invite( - @SerialName("version") override val version: Long, + @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 @@ -46,12 +60,14 @@ sealed interface CallEventContent : MessageEventContent { /** * Matrix call candidates content * - * @see matrix spec + * @see matrix spec */ @Serializable data class Candidates( - @SerialName("version") override val version: Long, + @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 @@ -61,21 +77,27 @@ sealed interface CallEventContent : MessageEventContent { @Serializable data class Candidate( @SerialName("candidate") val candidate: String, - @SerialName("sdpMLineIndex") val sdpMLineIndex: Long, - @SerialName("sdpMid") val sdpMid: String, + @SerialName("sdpMLineIndex") val sdpMLineIndex: Long? = null, + @SerialName("sdpMid") val sdpMid: String? = null, ) } /** * Matrix call answer content * - * @see matrix spec + * @see matrix spec */ @Serializable data class Answer( - @SerialName("version") override val version: Long, + @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 @@ -96,12 +118,14 @@ sealed interface CallEventContent : MessageEventContent { /** * Matrix call hangup content * - * @see matrix spec + * @see matrix spec */ @Serializable data class Hangup( - @SerialName("version") override val version: Long, + @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 @@ -112,6 +136,131 @@ sealed interface CallEventContent : MessageEventContent { 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 000000000..b40561a16 --- /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/serialization/events/DefaultEventContentSerializerMappings.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DefaultEventContentSerializerMappings.kt index 36905edbe..18e3f6339 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 @@ -33,6 +33,10 @@ val DefaultEventContentSerializerMappings = createEventContentSerializerMappings 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") @@ -89,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 index 9233bddb3..e4cda559a 100644 --- 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 @@ -8,6 +8,10 @@ import net.folivo.trixnity.core.model.events.m.call.CallEventContent.Candidates. 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 @@ -17,7 +21,10 @@ class CallEventContentSerializerTest { private val json = createMatrixEventJson() + ////////////////// m.call.invite ////////////////// + private val testInvite = CallEventContent.Invite( + version = "1", callId = "0123", offer = Offer(sdp = """ v=0 @@ -25,18 +32,50 @@ class CallEventContentSerializerTest { """.trimIndent(), type = OfferType.OFFER), lifetime = 30000, - version = 0, + sdpStreamMetadata = mapOf( + "271828182845" to StreamMetadata(Purpose.SCREENSHARE), + "314159265358" to StreamMetadata(Purpose.USERMEDIA), + ) ) - private val serializedInvite = - """{ - "version":0, - "call_id":"0123", - "lifetime":30000, - "offer":{"sdp":"v=0\no=- 6584580628695956864 2 IN IP4 127.0.0.1","type":"offer"} - }""".trimToFlatJson() + 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( @@ -50,40 +89,16 @@ class CallEventContentSerializerTest { sdpMLineIndex = 1 ), ), - version = 0, ) 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() - - private val testAnswer = CallEventContent.Answer( - answer = Answer(sdp = "v=0", type = AnswerType.ANSWER), - callId = "0123", - version = 0, - ) - private val serializedAnswer = """{ - "version":0, - "call_id":"0123", - "answer":{"sdp":"v=0","type":"answer"} - }""".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) - } + "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() { @@ -97,6 +112,21 @@ class CallEventContentSerializerTest { 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) @@ -109,16 +139,167 @@ class CallEventContentSerializerTest { 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 testHangup = CallEventContent.Hangup(version = 0, callId = "0123", reason = Reason.INVITE_TIMEOUT) val result = json.encodeToString(testHangup) - assertEquals("""{"version":0,"call_id":"0123","reason":"invite_timeout"}""", result) + assertEquals(serializedHangup, result) } @Test fun shouldDeserializeCallHangup() { - val result: CallEventContent.Hangup = json.decodeFromString("""{"call_id":"0123","reason":"ice_failed","version":0}""") - assertEquals(CallEventContent.Hangup(version = 0, callId = "0123", reason = Reason.ICE_FAILED), result) + 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) } } -- GitLab From 701fbc76b0f95833058e2c38a5c295f75f625569 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Fri, 31 Jan 2025 11:42:14 +0100 Subject: [PATCH 4/4] Add entry to CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 580e915d3..280f099aa 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`. -- GitLab