refactor: improve separation of concerns in call feature
1. Move UI toggle state out of CallState.Connected:
- Removed isAudioMuted/isVideoEnabled/isSpeakerOn from domain
CallState. These are UI concerns, not call state.
- Added StateFlows for toggles in CallController (isAudioMuted,
isVideoEnabled, isSpeakerOn) with proper toggle methods.
- CallScreen reads toggle state from CallController flows.
- Removed toggleAudioMute/toggleVideo/toggleSpeaker from
CallManager (no longer manages UI state).
2. Move ICE candidate parsing to quartz layer:
- Added candidateSdp(), sdpMid(), sdpMLineIndex() parsers and
serializeCandidate() to CallIceCandidateEvent in quartz.
- Removed companion object with parseIceCandidate/
serializeIceCandidate from CallController.
- CallController now uses quartz-layer parsing directly.
- Updated IceCandidateSerializationTest accordingly.
3. Add helper methods to CallManager:
- currentCallId() and currentPeerPubKey() extract from any
active state, reducing repeated when expressions.
4. Fix empty callId bug in ChatroomScreen:
- Navigation to ActiveCall now uses callManager.currentCallId()
instead of hardcoded empty string.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
@@ -74,6 +74,15 @@ class CallController(
|
|||||||
private val _errorMessage = MutableStateFlow<String?>(null)
|
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||||
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
|
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
|
||||||
|
|
||||||
|
private val _isAudioMuted = MutableStateFlow(false)
|
||||||
|
val isAudioMuted: StateFlow<Boolean> = _isAudioMuted.asStateFlow()
|
||||||
|
|
||||||
|
private val _isVideoEnabled = MutableStateFlow(true)
|
||||||
|
val isVideoEnabled: StateFlow<Boolean> = _isVideoEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _isSpeakerOn = MutableStateFlow(false)
|
||||||
|
val isSpeakerOn: StateFlow<Boolean> = _isSpeakerOn.asStateFlow()
|
||||||
|
|
||||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -185,9 +194,8 @@ class CallController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onIceCandidateReceived(event: CallIceCandidateEvent) {
|
fun onIceCandidateReceived(event: CallIceCandidateEvent) {
|
||||||
val json = event.candidateJson()
|
|
||||||
try {
|
try {
|
||||||
val candidate = parseIceCandidate(json)
|
val candidate = IceCandidate(event.sdpMid(), event.sdpMLineIndex(), event.candidateSdp())
|
||||||
if (webRtcSession != null && remoteDescriptionSet) {
|
if (webRtcSession != null && remoteDescriptionSet) {
|
||||||
Log.d(TAG) { "Adding ICE candidate directly: ${candidate.sdp.take(50)}" }
|
Log.d(TAG) { "Adding ICE candidate directly: ${candidate.sdp.take(50)}" }
|
||||||
webRtcSession?.addIceCandidate(candidate)
|
webRtcSession?.addIceCandidate(candidate)
|
||||||
@@ -196,7 +204,7 @@ class CallController(
|
|||||||
pendingIceCandidates.add(candidate)
|
pendingIceCandidates.add(candidate)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to parse ICE candidate: $json", e)
|
Log.e(TAG, "Failed to parse ICE candidate", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,15 +217,21 @@ class CallController(
|
|||||||
candidates.forEach { session.addIceCandidate(it) }
|
candidates.forEach { session.addIceCandidate(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAudioMuted(muted: Boolean) {
|
fun toggleAudioMute() {
|
||||||
|
val muted = !_isAudioMuted.value
|
||||||
|
_isAudioMuted.value = muted
|
||||||
webRtcSession?.setAudioEnabled(!muted)
|
webRtcSession?.setAudioEnabled(!muted)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVideoEnabled(enabled: Boolean) {
|
fun toggleVideo() {
|
||||||
|
val enabled = !_isVideoEnabled.value
|
||||||
|
_isVideoEnabled.value = enabled
|
||||||
webRtcSession?.setVideoEnabled(enabled)
|
webRtcSession?.setVideoEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSpeakerOn(on: Boolean) {
|
fun toggleSpeaker() {
|
||||||
|
val on = !_isSpeakerOn.value
|
||||||
|
_isSpeakerOn.value = on
|
||||||
val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
am.isSpeakerphoneOn = on
|
am.isSpeakerphoneOn = on
|
||||||
}
|
}
|
||||||
@@ -240,6 +254,9 @@ class CallController(
|
|||||||
NotificationUtils.cancelCallNotification(context)
|
NotificationUtils.cancelCallNotification(context)
|
||||||
_remoteVideoTrack.value = null
|
_remoteVideoTrack.value = null
|
||||||
_localVideoTrack.value = null
|
_localVideoTrack.value = null
|
||||||
|
_isAudioMuted.value = false
|
||||||
|
_isVideoEnabled.value = true
|
||||||
|
_isSpeakerOn.value = false
|
||||||
webRtcSession?.dispose()
|
webRtcSession?.dispose()
|
||||||
webRtcSession = null
|
webRtcSession = null
|
||||||
currentCallId = null
|
currentCallId = null
|
||||||
@@ -316,7 +333,7 @@ class CallController(
|
|||||||
Log.d(TAG) { "Local ICE candidate: ${candidate.sdp.take(50)}" }
|
Log.d(TAG) { "Local ICE candidate: ${candidate.sdp.take(50)}" }
|
||||||
val callId = currentCallId ?: return
|
val callId = currentCallId ?: return
|
||||||
val peerPubKey = currentPeerPubKey ?: return
|
val peerPubKey = currentPeerPubKey ?: return
|
||||||
val candidateJson = serializeIceCandidate(candidate)
|
val candidateJson = CallIceCandidateEvent.serializeCandidate(candidate.sdp, candidate.sdpMid, candidate.sdpMLineIndex)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val signer = signerProvider()
|
val signer = signerProvider()
|
||||||
@@ -344,25 +361,4 @@ class CallController(
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun serializeIceCandidate(candidate: IceCandidate): String = """{"candidate":"${candidate.sdp}","sdpMid":"${candidate.sdpMid}","sdpMLineIndex":${candidate.sdpMLineIndex}}"""
|
|
||||||
|
|
||||||
fun parseIceCandidate(json: String): IceCandidate {
|
|
||||||
val candidateRegex = """"candidate"\s*:\s*"([^"]*)"""".toRegex()
|
|
||||||
val sdpMidRegex = """"sdpMid"\s*:\s*"([^"]*)"""".toRegex()
|
|
||||||
val sdpMLineIndexRegex = """"sdpMLineIndex"\s*:\s*(\d+)""".toRegex()
|
|
||||||
|
|
||||||
val sdp = candidateRegex.find(json)?.groupValues?.get(1) ?: ""
|
|
||||||
val sdpMid = sdpMidRegex.find(json)?.groupValues?.get(1) ?: "0"
|
|
||||||
val sdpMLineIndex =
|
|
||||||
sdpMLineIndexRegex
|
|
||||||
.find(json)
|
|
||||||
?.groupValues
|
|
||||||
?.get(1)
|
|
||||||
?.toIntOrNull() ?: 0
|
|
||||||
|
|
||||||
return IceCandidate(sdpMid, sdpMLineIndex, sdp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,18 +142,9 @@ fun CallScreen(
|
|||||||
callController = callController,
|
callController = callController,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onHangup = { scope.launch { callManager.hangup() } },
|
onHangup = { scope.launch { callManager.hangup() } },
|
||||||
onToggleMute = {
|
onToggleMute = { callController?.toggleAudioMute() },
|
||||||
callManager.toggleAudioMute()
|
onToggleVideo = { callController?.toggleVideo() },
|
||||||
callController?.setAudioMuted(!state.isAudioMuted)
|
onToggleSpeaker = { callController?.toggleSpeaker() },
|
||||||
},
|
|
||||||
onToggleVideo = {
|
|
||||||
callManager.toggleVideo()
|
|
||||||
callController?.setVideoEnabled(!state.isVideoEnabled)
|
|
||||||
},
|
|
||||||
onToggleSpeaker = {
|
|
||||||
callManager.toggleSpeaker()
|
|
||||||
callController?.setSpeakerOn(!state.isSpeakerOn)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +329,9 @@ private fun ConnectedCallUI(
|
|||||||
|
|
||||||
val remoteVideoTrack by (callController?.remoteVideoTrack ?: kotlinx.coroutines.flow.MutableStateFlow(null)).collectAsState()
|
val remoteVideoTrack by (callController?.remoteVideoTrack ?: kotlinx.coroutines.flow.MutableStateFlow(null)).collectAsState()
|
||||||
val localVideoTrack by (callController?.localVideoTrack ?: kotlinx.coroutines.flow.MutableStateFlow(null)).collectAsState()
|
val localVideoTrack by (callController?.localVideoTrack ?: kotlinx.coroutines.flow.MutableStateFlow(null)).collectAsState()
|
||||||
|
val isAudioMuted by (callController?.isAudioMuted ?: kotlinx.coroutines.flow.MutableStateFlow(false)).collectAsState()
|
||||||
|
val isVideoEnabled by (callController?.isVideoEnabled ?: kotlinx.coroutines.flow.MutableStateFlow(true)).collectAsState()
|
||||||
|
val isSpeakerOn by (callController?.isSpeakerOn ?: kotlinx.coroutines.flow.MutableStateFlow(false)).collectAsState()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -429,9 +423,9 @@ private fun ConnectedCallUI(
|
|||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (state.isAudioMuted) Icons.Default.MicOff else Icons.Default.Mic,
|
imageVector = if (isAudioMuted) Icons.Default.MicOff else Icons.Default.Mic,
|
||||||
contentDescription = if (state.isAudioMuted) "Unmute" else "Mute",
|
contentDescription = if (isAudioMuted) "Unmute" else "Mute",
|
||||||
tint = if (state.isAudioMuted) Color.Red else Color.White,
|
tint = if (isAudioMuted) Color.Red else Color.White,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -440,9 +434,9 @@ private fun ConnectedCallUI(
|
|||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (state.isVideoEnabled) Icons.Default.Videocam else Icons.Default.VideocamOff,
|
imageVector = if (isVideoEnabled) Icons.Default.Videocam else Icons.Default.VideocamOff,
|
||||||
contentDescription = if (state.isVideoEnabled) "Camera off" else "Camera on",
|
contentDescription = if (isVideoEnabled) "Camera off" else "Camera on",
|
||||||
tint = if (!state.isVideoEnabled) Color.Red else Color.White,
|
tint = if (!isVideoEnabled) Color.Red else Color.White,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -451,9 +445,9 @@ private fun ConnectedCallUI(
|
|||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (state.isSpeakerOn) Icons.Default.VolumeUp else Icons.Default.VolumeOff,
|
imageVector = if (isSpeakerOn) Icons.Default.VolumeUp else Icons.Default.VolumeOff,
|
||||||
contentDescription = if (state.isSpeakerOn) "Earpiece" else "Speaker",
|
contentDescription = if (isSpeakerOn) "Earpiece" else "Speaker",
|
||||||
tint = if (state.isSpeakerOn) Color.Cyan else Color.White,
|
tint = if (isSpeakerOn) Color.Cyan else Color.White,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -52,13 +52,15 @@ fun ChatroomScreen(
|
|||||||
rememberCallWithPermission(context) {
|
rememberCallWithPermission(context) {
|
||||||
val peerPubKey = roomId.users.firstOrNull() ?: return@rememberCallWithPermission
|
val peerPubKey = roomId.users.firstOrNull() ?: return@rememberCallWithPermission
|
||||||
accountViewModel.callController?.initiateCall(peerPubKey, CallType.VOICE)
|
accountViewModel.callController?.initiateCall(peerPubKey, CallType.VOICE)
|
||||||
nav.nav(Route.ActiveCall(callId = "", peerPubKey = peerPubKey))
|
val callId = accountViewModel.callManager.currentCallId() ?: ""
|
||||||
|
nav.nav(Route.ActiveCall(callId = callId, peerPubKey = peerPubKey))
|
||||||
}
|
}
|
||||||
val startVideoCall =
|
val startVideoCall =
|
||||||
rememberCallWithPermission(context) {
|
rememberCallWithPermission(context) {
|
||||||
val peerPubKey = roomId.users.firstOrNull() ?: return@rememberCallWithPermission
|
val peerPubKey = roomId.users.firstOrNull() ?: return@rememberCallWithPermission
|
||||||
accountViewModel.callController?.initiateCall(peerPubKey, CallType.VIDEO)
|
accountViewModel.callController?.initiateCall(peerPubKey, CallType.VIDEO)
|
||||||
nav.nav(Route.ActiveCall(callId = "", peerPubKey = peerPubKey))
|
val callId = accountViewModel.callManager.currentCallId() ?: ""
|
||||||
|
nav.nav(Route.ActiveCall(callId = callId, peerPubKey = peerPubKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
DisappearingScaffold(
|
DisappearingScaffold(
|
||||||
|
|||||||
+40
-42
@@ -20,59 +20,57 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.call
|
package com.vitorpamplona.amethyst.service.call
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class IceCandidateSerializationTest {
|
class IceCandidateSerializationTest {
|
||||||
@Test
|
@Test
|
||||||
fun parseIceCandidateFromJson() {
|
fun serializeCandidateProducesValidJson() {
|
||||||
val json = """{"candidate":"candidate:842163049 1 udp 1677729535 203.0.113.1 44323 typ srflx","sdpMid":"0","sdpMLineIndex":0}"""
|
val json = CallIceCandidateEvent.serializeCandidate("candidate:123 1 udp 456", "audio", 1)
|
||||||
val candidate = CallController.parseIceCandidate(json)
|
|
||||||
|
|
||||||
assertEquals("candidate:842163049 1 udp 1677729535 203.0.113.1 44323 typ srflx", candidate.sdp)
|
|
||||||
assertEquals("0", candidate.sdpMid)
|
|
||||||
assertEquals(0, candidate.sdpMLineIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseIceCandidateWithDifferentIndex() {
|
|
||||||
val json = """{"candidate":"candidate:1 1 tcp 1234","sdpMid":"audio","sdpMLineIndex":1}"""
|
|
||||||
val candidate = CallController.parseIceCandidate(json)
|
|
||||||
|
|
||||||
assertEquals("candidate:1 1 tcp 1234", candidate.sdp)
|
|
||||||
assertEquals("audio", candidate.sdpMid)
|
|
||||||
assertEquals(1, candidate.sdpMLineIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseIceCandidateHandlesMissingFields() {
|
|
||||||
val json = """{"candidate":"test"}"""
|
|
||||||
val candidate = CallController.parseIceCandidate(json)
|
|
||||||
|
|
||||||
assertEquals("test", candidate.sdp)
|
|
||||||
assertEquals("0", candidate.sdpMid) // default
|
|
||||||
assertEquals(0, candidate.sdpMLineIndex) // default
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun serializeIceCandidateProducesValidJson() {
|
|
||||||
val candidate = org.webrtc.IceCandidate("audio", 1, "candidate:123 1 udp 456")
|
|
||||||
val json = CallController.serializeIceCandidate(candidate)
|
|
||||||
|
|
||||||
// Verify it contains the expected fields
|
|
||||||
assert(json.contains(""""candidate":"candidate:123 1 udp 456""""))
|
assert(json.contains(""""candidate":"candidate:123 1 udp 456""""))
|
||||||
assert(json.contains(""""sdpMid":"audio""""))
|
assert(json.contains(""""sdpMid":"audio""""))
|
||||||
assert(json.contains(""""sdpMLineIndex":1"""))
|
assert(json.contains(""""sdpMLineIndex":1"""))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun roundTripIceCandidate() {
|
fun serializeAndParseRoundTrip() {
|
||||||
val original = org.webrtc.IceCandidate("video", 2, "candidate:999 1 udp 789 10.0.0.1 5000 typ host")
|
val sdp = "candidate:999 1 udp 789 10.0.0.1 5000 typ host"
|
||||||
val json = CallController.serializeIceCandidate(original)
|
val sdpMid = "video"
|
||||||
val parsed = CallController.parseIceCandidate(json)
|
val sdpMLineIndex = 2
|
||||||
|
|
||||||
assertEquals(original.sdp, parsed.sdp)
|
val json = CallIceCandidateEvent.serializeCandidate(sdp, sdpMid, sdpMLineIndex)
|
||||||
assertEquals(original.sdpMid, parsed.sdpMid)
|
|
||||||
assertEquals(original.sdpMLineIndex, parsed.sdpMLineIndex)
|
// Create a minimal event to test parsing
|
||||||
|
val event =
|
||||||
|
CallIceCandidateEvent(
|
||||||
|
id = "test",
|
||||||
|
pubKey = "test",
|
||||||
|
createdAt = 0,
|
||||||
|
tags = emptyArray(),
|
||||||
|
content = json,
|
||||||
|
sig = "test",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(sdp, event.candidateSdp())
|
||||||
|
assertEquals(sdpMid, event.sdpMid())
|
||||||
|
assertEquals(sdpMLineIndex, event.sdpMLineIndex())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parseCandidateHandlesMissingFields() {
|
||||||
|
val event =
|
||||||
|
CallIceCandidateEvent(
|
||||||
|
id = "test",
|
||||||
|
pubKey = "test",
|
||||||
|
createdAt = 0,
|
||||||
|
tags = emptyArray(),
|
||||||
|
content = """{"candidate":"test"}""",
|
||||||
|
sig = "test",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("test", event.candidateSdp())
|
||||||
|
assertEquals("0", event.sdpMid()) // default
|
||||||
|
assertEquals(0, event.sdpMLineIndex()) // default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-17
@@ -216,26 +216,23 @@ class CallManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleAudioMute() {
|
fun currentCallId(): String? =
|
||||||
val current = _state.value
|
when (val s = _state.value) {
|
||||||
if (current is CallState.Connected) {
|
is CallState.Offering -> s.callId
|
||||||
_state.value = current.copy(isAudioMuted = !current.isAudioMuted)
|
is CallState.IncomingCall -> s.callId
|
||||||
|
is CallState.Connecting -> s.callId
|
||||||
|
is CallState.Connected -> s.callId
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleVideo() {
|
fun currentPeerPubKey(): HexKey? =
|
||||||
val current = _state.value
|
when (val s = _state.value) {
|
||||||
if (current is CallState.Connected) {
|
is CallState.Offering -> s.peerPubKey
|
||||||
_state.value = current.copy(isVideoEnabled = !current.isVideoEnabled)
|
is CallState.IncomingCall -> s.callerPubKey
|
||||||
|
is CallState.Connecting -> s.peerPubKey
|
||||||
|
is CallState.Connected -> s.peerPubKey
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleSpeaker() {
|
|
||||||
val current = _state.value
|
|
||||||
if (current is CallState.Connected) {
|
|
||||||
_state.value = current.copy(isSpeakerOn = !current.isSpeakerOn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
_state.value = CallState.Idle
|
_state.value = CallState.Idle
|
||||||
|
|||||||
@@ -52,9 +52,6 @@ sealed interface CallState {
|
|||||||
val peerPubKey: HexKey,
|
val peerPubKey: HexKey,
|
||||||
val callType: CallType,
|
val callType: CallType,
|
||||||
val startedAtEpoch: Long,
|
val startedAtEpoch: Long,
|
||||||
val isAudioMuted: Boolean = false,
|
|
||||||
val isVideoEnabled: Boolean = true,
|
|
||||||
val isSpeakerOn: Boolean = false,
|
|
||||||
) : CallState
|
) : CallState
|
||||||
|
|
||||||
data class Ended(
|
data class Ended(
|
||||||
|
|||||||
+21
@@ -45,11 +45,32 @@ class CallIceCandidateEvent(
|
|||||||
|
|
||||||
fun candidateJson() = content
|
fun candidateJson() = content
|
||||||
|
|
||||||
|
fun candidateSdp(): String = CANDIDATE_REGEX.find(content)?.groupValues?.get(1) ?: ""
|
||||||
|
|
||||||
|
fun sdpMid(): String = SDP_MID_REGEX.find(content)?.groupValues?.get(1) ?: "0"
|
||||||
|
|
||||||
|
fun sdpMLineIndex(): Int =
|
||||||
|
SDP_MLINE_INDEX_REGEX
|
||||||
|
.find(content)
|
||||||
|
?.groupValues
|
||||||
|
?.get(1)
|
||||||
|
?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 25052
|
const val KIND = 25052
|
||||||
const val ALT_DESCRIPTION = "WebRTC ICE candidate"
|
const val ALT_DESCRIPTION = "WebRTC ICE candidate"
|
||||||
const val EXPIRATION_SECONDS = 20L
|
const val EXPIRATION_SECONDS = 20L
|
||||||
|
|
||||||
|
private val CANDIDATE_REGEX = """"candidate"\s*:\s*"([^"]*)"""".toRegex()
|
||||||
|
private val SDP_MID_REGEX = """"sdpMid"\s*:\s*"([^"]*)"""".toRegex()
|
||||||
|
private val SDP_MLINE_INDEX_REGEX = """"sdpMLineIndex"\s*:\s*(\d+)""".toRegex()
|
||||||
|
|
||||||
|
fun serializeCandidate(
|
||||||
|
sdp: String,
|
||||||
|
sdpMid: String,
|
||||||
|
sdpMLineIndex: Int,
|
||||||
|
): String = """{"candidate":"$sdp","sdpMid":"$sdpMid","sdpMLineIndex":$sdpMLineIndex}"""
|
||||||
|
|
||||||
fun build(
|
fun build(
|
||||||
candidateJson: String,
|
candidateJson: String,
|
||||||
peerPubKey: HexKey,
|
peerPubKey: HexKey,
|
||||||
|
|||||||
Reference in New Issue
Block a user