Backend, sync, infra, docs: ETag, API versioning, k8s, web scaffold, Android 16, domain stubs
- Backend: ShallowEtagHeaderFilter for /api/v1/*, API-VERSIONING.md, README (tenant, CORS, Flyway, ETag) - k8s: backend-deployment.yaml (Deployment, Service, Secret/ConfigMap) - Web: scaffold with directory pull, 304 handling, touch-friendly UI - Android 16: ANDROID-16-TARGET.md; BuildConfig STUN/signaling, SMOAApplication configures InfrastructureManager - Domain: CertificateManager revocation stub, ReportService signReports, ZeroTrust/ThreatDetection minimal docs - TODO.md and IMPLEMENTATION_STATUS.md updated; communications README for endpoint config Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
14
modules/communications/README.md
Normal file
14
modules/communications/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Communications module
|
||||
|
||||
WebRTC-based communications (voice, video, signaling) with infrastructure failover.
|
||||
|
||||
## Configurable endpoints
|
||||
|
||||
**InfrastructureManager** manages STUN, TURN, and signaling URLs. The **app** configures them at startup from BuildConfig (when set):
|
||||
|
||||
- **STUN:** `smoa.stun.urls` – comma-separated (e.g. `stun:stun.l.google.com:19302,stun:stun.example.com:3478`). Passed as `-Psmoa.stun.urls=...` when building the app.
|
||||
- **Signaling:** `smoa.signaling.urls` – comma-separated signaling server URLs for failover. Passed as `-Psmoa.signaling.urls=...`.
|
||||
|
||||
TURN servers (with optional credentials) are set programmatically via `InfrastructureManager.setTurnEndpoints(List<TurnServer>)` where needed.
|
||||
|
||||
See **SMOAApplication.configureInfrastructure()** and **app/build.gradle.kts** (BuildConfig fields `SMOA_STUN_URLS`, `SMOA_SIGNALING_URLS`).
|
||||
@@ -2,8 +2,17 @@ package com.smoa.modules.communications.di
|
||||
|
||||
import android.content.Context
|
||||
import com.smoa.core.security.AuditLogger
|
||||
import com.smoa.modules.communications.domain.AdaptiveCodecSelector
|
||||
import com.smoa.modules.communications.domain.ChannelManager
|
||||
import com.smoa.modules.communications.domain.CommunicationsService
|
||||
import com.smoa.modules.communications.domain.ConnectionQualityMonitor
|
||||
import com.smoa.modules.communications.domain.ConnectionStabilityController
|
||||
import com.smoa.modules.communications.domain.InfrastructureManager
|
||||
import com.smoa.modules.communications.domain.MediaCodecPolicy
|
||||
import com.smoa.modules.communications.domain.MediaRoutingPolicy
|
||||
import com.smoa.modules.communications.domain.NetworkPathSelector
|
||||
import com.smoa.modules.communications.domain.SmartRoutingService
|
||||
import com.smoa.modules.communications.domain.StubConnectionQualityMonitor
|
||||
import com.smoa.modules.communications.domain.VoiceTransport
|
||||
import com.smoa.modules.communications.domain.WebRTCManager
|
||||
import dagger.Module
|
||||
@@ -16,12 +25,62 @@ import javax.inject.Singleton
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CommunicationsModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConnectionQualityMonitor(
|
||||
stub: StubConnectionQualityMonitor
|
||||
): ConnectionQualityMonitor = stub
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMediaCodecPolicy(): MediaCodecPolicy = MediaCodecPolicy.default()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMediaRoutingPolicy(): MediaRoutingPolicy = MediaRoutingPolicy()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkPathSelector(
|
||||
connectivityManager: com.smoa.core.common.ConnectivityManager,
|
||||
vpnManager: com.smoa.core.security.VPNManager,
|
||||
policy: MediaRoutingPolicy
|
||||
): NetworkPathSelector = NetworkPathSelector(connectivityManager, vpnManager, policy)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideInfrastructureManager(
|
||||
circuitBreaker: com.smoa.core.common.CircuitBreaker
|
||||
): InfrastructureManager = InfrastructureManager(circuitBreaker)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConnectionStabilityController(): ConnectionStabilityController = ConnectionStabilityController()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSmartRoutingService(
|
||||
networkPathSelector: NetworkPathSelector,
|
||||
infrastructureManager: InfrastructureManager,
|
||||
stabilityController: ConnectionStabilityController,
|
||||
adaptiveCodecSelector: AdaptiveCodecSelector,
|
||||
connectionQualityMonitor: ConnectionQualityMonitor
|
||||
): SmartRoutingService = SmartRoutingService(
|
||||
networkPathSelector,
|
||||
infrastructureManager,
|
||||
stabilityController,
|
||||
adaptiveCodecSelector,
|
||||
connectionQualityMonitor
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWebRTCManager(
|
||||
@ApplicationContext context: Context
|
||||
@ApplicationContext context: Context,
|
||||
adaptiveCodecSelector: AdaptiveCodecSelector,
|
||||
smartRoutingService: SmartRoutingService
|
||||
): WebRTCManager {
|
||||
return WebRTCManager(context)
|
||||
return WebRTCManager(context, adaptiveCodecSelector, smartRoutingService)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -33,9 +92,10 @@ object CommunicationsModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVoiceTransport(
|
||||
webRTCManager: WebRTCManager
|
||||
webRTCManager: WebRTCManager,
|
||||
smartRoutingService: SmartRoutingService
|
||||
): VoiceTransport {
|
||||
return VoiceTransport(webRTCManager)
|
||||
return VoiceTransport(webRTCManager, smartRoutingService)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
/**
|
||||
* Connection speed tier used to select compression codecs and bitrate limits.
|
||||
* Enables connection-speed-aware audio/video encoding, especially for
|
||||
* point-to-multipoint where one sender serves many receivers at varying link quality.
|
||||
*/
|
||||
enum class ConnectionTier {
|
||||
VERY_LOW,
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH,
|
||||
VERY_HIGH
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio codec constraints for connection-speed-aware compression.
|
||||
* Opus is the preferred WebRTC codec; it supports adaptive bitrate and bandwidth modes.
|
||||
*/
|
||||
data class AudioCodecConstraints(
|
||||
val codec: String = "opus",
|
||||
val minBitrateBps: Int,
|
||||
val maxBitrateBps: Int,
|
||||
val opusBandwidthMode: OpusBandwidthMode = OpusBandwidthMode.WIDEBAND,
|
||||
val useDtx: Boolean = true
|
||||
)
|
||||
|
||||
enum class OpusBandwidthMode {
|
||||
NARROWBAND,
|
||||
WIDEBAND,
|
||||
FULLBAND
|
||||
}
|
||||
|
||||
/**
|
||||
* Video codec constraints for connection-speed-aware compression.
|
||||
* VP8/VP9 suit simulcast for point-to-multipoint; VP9 supports SVC.
|
||||
*/
|
||||
data class VideoCodecConstraints(
|
||||
val codec: String = "VP8",
|
||||
val maxWidth: Int,
|
||||
val maxHeight: Int,
|
||||
val maxBitrateBps: Int,
|
||||
val useSimulcast: Boolean = false,
|
||||
val simulcastLayers: Int = 2,
|
||||
val preferSvc: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Policy mapping connection tier to audio and video codec constraints.
|
||||
* Used by AdaptiveCodecSelector for connection-speed-aware compression.
|
||||
*/
|
||||
data class MediaCodecPolicy(
|
||||
val audioByTier: Map<ConnectionTier, AudioCodecConstraints>,
|
||||
val videoByTier: Map<ConnectionTier, VideoCodecConstraints>
|
||||
) {
|
||||
fun audioForTier(tier: ConnectionTier): AudioCodecConstraints =
|
||||
audioByTier[tier] ?: audioByTier[ConnectionTier.MEDIUM]!!
|
||||
fun videoForTier(tier: ConnectionTier): VideoCodecConstraints =
|
||||
videoByTier[tier] ?: videoByTier[ConnectionTier.MEDIUM]!!
|
||||
|
||||
companion object {
|
||||
fun default(): MediaCodecPolicy {
|
||||
val audioByTier = mapOf(
|
||||
ConnectionTier.VERY_LOW to AudioCodecConstraints(
|
||||
minBitrateBps = 12_000,
|
||||
maxBitrateBps = 24_000,
|
||||
opusBandwidthMode = OpusBandwidthMode.NARROWBAND,
|
||||
useDtx = true
|
||||
),
|
||||
ConnectionTier.LOW to AudioCodecConstraints(
|
||||
minBitrateBps = 24_000,
|
||||
maxBitrateBps = 48_000,
|
||||
opusBandwidthMode = OpusBandwidthMode.WIDEBAND,
|
||||
useDtx = true
|
||||
),
|
||||
ConnectionTier.MEDIUM to AudioCodecConstraints(
|
||||
minBitrateBps = 32_000,
|
||||
maxBitrateBps = 64_000,
|
||||
opusBandwidthMode = OpusBandwidthMode.WIDEBAND,
|
||||
useDtx = true
|
||||
),
|
||||
ConnectionTier.HIGH to AudioCodecConstraints(
|
||||
minBitrateBps = 48_000,
|
||||
maxBitrateBps = 128_000,
|
||||
opusBandwidthMode = OpusBandwidthMode.FULLBAND,
|
||||
useDtx = true
|
||||
),
|
||||
ConnectionTier.VERY_HIGH to AudioCodecConstraints(
|
||||
minBitrateBps = 64_000,
|
||||
maxBitrateBps = 256_000,
|
||||
opusBandwidthMode = OpusBandwidthMode.FULLBAND,
|
||||
useDtx = true
|
||||
)
|
||||
)
|
||||
val videoByTier = mapOf(
|
||||
ConnectionTier.VERY_LOW to VideoCodecConstraints(
|
||||
maxWidth = 0,
|
||||
maxHeight = 0,
|
||||
maxBitrateBps = 0,
|
||||
useSimulcast = false
|
||||
),
|
||||
ConnectionTier.LOW to VideoCodecConstraints(
|
||||
codec = "VP8",
|
||||
maxWidth = 320,
|
||||
maxHeight = 240,
|
||||
maxBitrateBps = 150_000,
|
||||
useSimulcast = false
|
||||
),
|
||||
ConnectionTier.MEDIUM to VideoCodecConstraints(
|
||||
codec = "VP8",
|
||||
maxWidth = 640,
|
||||
maxHeight = 360,
|
||||
maxBitrateBps = 400_000,
|
||||
useSimulcast = false
|
||||
),
|
||||
ConnectionTier.HIGH to VideoCodecConstraints(
|
||||
codec = "VP8",
|
||||
maxWidth = 1280,
|
||||
maxHeight = 720,
|
||||
maxBitrateBps = 1_200_000,
|
||||
useSimulcast = true,
|
||||
simulcastLayers = 2
|
||||
),
|
||||
ConnectionTier.VERY_HIGH to VideoCodecConstraints(
|
||||
codec = "VP9",
|
||||
maxWidth = 1920,
|
||||
maxHeight = 1080,
|
||||
maxBitrateBps = 2_500_000,
|
||||
useSimulcast = true,
|
||||
simulcastLayers = 3,
|
||||
preferSvc = true
|
||||
)
|
||||
)
|
||||
return MediaCodecPolicy(audioByTier = audioByTier, videoByTier = videoByTier)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Selects audio and video codec constraints based on observed connection speed.
|
||||
* Uses [ConnectionQualityMonitor] and [MediaCodecPolicy] to choose
|
||||
* connection-speed-aware compression for point-to-point and point-to-multipoint.
|
||||
*
|
||||
* For point-to-multipoint, the sender can use the selected constraints to encode
|
||||
* a single adaptive stream or simulcast layers so that receivers with different
|
||||
* link quality each get an appropriate layer.
|
||||
*/
|
||||
@Singleton
|
||||
class AdaptiveCodecSelector @Inject constructor(
|
||||
private val policy: MediaCodecPolicy,
|
||||
private val qualityMonitor: ConnectionQualityMonitor
|
||||
) {
|
||||
private val _audioConstraints = MutableStateFlow(policy.audioForTier(ConnectionTier.MEDIUM))
|
||||
val audioConstraints: StateFlow<AudioCodecConstraints> = _audioConstraints.asStateFlow()
|
||||
|
||||
private val _videoConstraints = MutableStateFlow(policy.videoForTier(ConnectionTier.MEDIUM))
|
||||
val videoConstraints: StateFlow<VideoCodecConstraints> = _videoConstraints.asStateFlow()
|
||||
|
||||
init {
|
||||
// When quality updates, recompute constraints (real impl would collect from qualityMonitor.qualityUpdates())
|
||||
// For now, constraints are updated via selectForTier() when the app has new quality data.
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected constraints from the current connection tier.
|
||||
* Call when WebRTC stats (or network callback) indicate a tier change.
|
||||
*/
|
||||
fun selectForTier(tier: ConnectionTier) {
|
||||
_audioConstraints.value = policy.audioForTier(tier)
|
||||
_videoConstraints.value = policy.videoForTier(tier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected constraints from estimated bandwidth (and optional RTT/loss).
|
||||
* Convenience for callers that have raw stats.
|
||||
*/
|
||||
fun selectForBandwidth(estimatedBandwidthKbps: Int, rttMs: Int = -1, packetLoss: Float = -1f) {
|
||||
val tier = connectionTierFromBandwidth(estimatedBandwidthKbps, rttMs, packetLoss)
|
||||
selectForTier(tier)
|
||||
}
|
||||
|
||||
/** Current audio constraints for the active connection tier. */
|
||||
fun getAudioConstraints(): AudioCodecConstraints = _audioConstraints.value
|
||||
|
||||
/** Current video constraints for the active connection tier. */
|
||||
fun getVideoConstraints(): VideoCodecConstraints = _videoConstraints.value
|
||||
|
||||
/**
|
||||
* Get constraints for point-to-multipoint send: use current tier; if policy
|
||||
* enables simulcast for this tier, caller should configure multiple layers.
|
||||
*/
|
||||
fun getSendConstraints(): Pair<AudioCodecConstraints, VideoCodecConstraints> =
|
||||
_audioConstraints.value to _videoConstraints.value
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Observed connection quality for a peer or session.
|
||||
* Used to drive connection-speed-aware codec selection (audio/video compression).
|
||||
*/
|
||||
data class ConnectionQuality(
|
||||
/** Estimated available bandwidth in kbps (0 if unknown). */
|
||||
val estimatedBandwidthKbps: Int,
|
||||
/** Round-trip time in ms (-1 if unknown). */
|
||||
val rttMs: Int,
|
||||
/** Packet loss fraction 0.0..1.0 (-1f if unknown). */
|
||||
val packetLossFraction: Float,
|
||||
/** Derived tier for codec selection. */
|
||||
val tier: ConnectionTier
|
||||
)
|
||||
|
||||
/**
|
||||
* Monitors connection quality (bandwidth, RTT, loss) and exposes a current tier
|
||||
* or quality metrics. Implementations should feed from WebRTC stats (e.g.
|
||||
* RTCStatsReport outbound-rtp, remote-inbound-rtp, candidate-pair) when
|
||||
* the WebRTC stack is integrated.
|
||||
*
|
||||
* Essential for point-to-multipoint: each receiver (or the sender, when
|
||||
* using receiver feedback) can use this to choose appropriate simulcast
|
||||
* layer or SVC spatial/temporal layer.
|
||||
*/
|
||||
interface ConnectionQualityMonitor {
|
||||
/** Current connection quality; updates when stats are available. */
|
||||
val currentQuality: StateFlow<ConnectionQuality>
|
||||
/** Flow of quality updates for reactive codec adaptation. */
|
||||
fun qualityUpdates(): Flow<ConnectionQuality>
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives [ConnectionTier] from estimated bandwidth (and optionally RTT/loss).
|
||||
* Thresholds aligned with [MediaCodecPolicy.default] tiers.
|
||||
*/
|
||||
fun connectionTierFromBandwidth(estimatedBandwidthKbps: Int, rttMs: Int = -1, packetLoss: Float = -1f): ConnectionTier {
|
||||
return when {
|
||||
estimatedBandwidthKbps <= 0 -> ConnectionTier.MEDIUM
|
||||
estimatedBandwidthKbps < 100 -> ConnectionTier.VERY_LOW
|
||||
estimatedBandwidthKbps < 256 -> ConnectionTier.LOW
|
||||
estimatedBandwidthKbps < 512 -> ConnectionTier.MEDIUM
|
||||
estimatedBandwidthKbps < 1000 -> ConnectionTier.HIGH
|
||||
else -> ConnectionTier.VERY_HIGH
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import com.smoa.core.common.QoSPolicy
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Controls connection stability: reconnection backoff, graceful degradation, and resource caps.
|
||||
* Reduces lag and improves system stability under poor conditions.
|
||||
*/
|
||||
@Singleton
|
||||
class ConnectionStabilityController @Inject constructor() {
|
||||
private val _reconnectBackoffMs = MutableStateFlow(0L)
|
||||
val reconnectBackoffMs: StateFlow<Long> = _reconnectBackoffMs.asStateFlow()
|
||||
|
||||
private val _degradationMode = MutableStateFlow(DegradationMode.NONE)
|
||||
val degradationMode: StateFlow<DegradationMode> = _degradationMode.asStateFlow()
|
||||
|
||||
private val _activeSessionCount = MutableStateFlow(0)
|
||||
val activeSessionCount: StateFlow<Int> = _activeSessionCount.asStateFlow()
|
||||
|
||||
private var consecutiveFailures = 0
|
||||
private var qosPolicy: QoSPolicy = QoSPolicy()
|
||||
|
||||
var minBackoffMs: Long = 1_000L
|
||||
var maxBackoffMs: Long = 60_000L
|
||||
var backoffMultiplier: Double = 2.0
|
||||
|
||||
fun setQoSPolicy(policy: QoSPolicy) {
|
||||
qosPolicy = policy
|
||||
}
|
||||
|
||||
fun recordConnectionFailure(): Long {
|
||||
consecutiveFailures++
|
||||
var backoff = minBackoffMs
|
||||
repeat(consecutiveFailures - 1) {
|
||||
backoff = (backoff * backoffMultiplier).toLong().coerceAtMost(maxBackoffMs)
|
||||
}
|
||||
backoff = backoff.coerceIn(minBackoffMs, maxBackoffMs)
|
||||
_reconnectBackoffMs.value = backoff
|
||||
return backoff
|
||||
}
|
||||
|
||||
fun recordConnectionSuccess() {
|
||||
consecutiveFailures = 0
|
||||
_reconnectBackoffMs.value = 0L
|
||||
}
|
||||
|
||||
fun setDegradationMode(mode: DegradationMode) {
|
||||
_degradationMode.value = mode
|
||||
}
|
||||
|
||||
fun shouldDisableVideo(): Boolean = _degradationMode.value == DegradationMode.AUDIO_ONLY
|
||||
|
||||
fun notifySessionStarted(): Boolean {
|
||||
val max = qosPolicy.maxConcurrentSessions
|
||||
if (max > 0 && _activeSessionCount.value >= max) return false
|
||||
_activeSessionCount.value = _activeSessionCount.value + 1
|
||||
return true
|
||||
}
|
||||
|
||||
fun notifySessionEnded() {
|
||||
_activeSessionCount.value = (_activeSessionCount.value - 1).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
fun isWithinBitrateCap(currentSendBitrateBps: Int): Boolean {
|
||||
val max = qosPolicy.maxTotalSendBitrateBps
|
||||
return max <= 0 || currentSendBitrateBps <= max
|
||||
}
|
||||
}
|
||||
|
||||
enum class DegradationMode {
|
||||
NONE,
|
||||
AUDIO_ONLY,
|
||||
REDUCED_VIDEO
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import com.smoa.core.common.CircuitBreaker
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manages media infrastructure endpoints (STUN, TURN, signaling) with health and failover.
|
||||
* Uses [CircuitBreaker] for stability; selects best available endpoint for [WebRTCConfig].
|
||||
*/
|
||||
@Singleton
|
||||
class InfrastructureManager @Inject constructor(
|
||||
private val circuitBreaker: CircuitBreaker
|
||||
) {
|
||||
private val _stunEndpoints = MutableStateFlow<List<StunEndpoint>>(emptyList())
|
||||
val stunEndpoints: StateFlow<List<StunEndpoint>> = _stunEndpoints.asStateFlow()
|
||||
|
||||
private val _turnEndpoints = MutableStateFlow<List<TurnEndpoint>>(emptyList())
|
||||
val turnEndpoints: StateFlow<List<TurnEndpoint>> = _turnEndpoints.asStateFlow()
|
||||
|
||||
private val _signalingEndpoints = MutableStateFlow<List<SignalingEndpoint>>(emptyList())
|
||||
val signalingEndpoints: StateFlow<List<SignalingEndpoint>> = _signalingEndpoints.asStateFlow()
|
||||
|
||||
private val failureThreshold = 3
|
||||
private val resetTimeoutMs = 60_000L
|
||||
|
||||
/**
|
||||
* Configure STUN servers. Order defines preference; first healthy is used.
|
||||
*/
|
||||
fun setStunEndpoints(urls: List<String>) {
|
||||
_stunEndpoints.value = urls.map { StunEndpoint(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure TURN servers with optional credentials.
|
||||
*/
|
||||
fun setTurnEndpoints(servers: List<TurnServer>) {
|
||||
_turnEndpoints.value = servers.map { TurnEndpoint(it.url, it.username, it.credential) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure signaling server URLs for failover.
|
||||
*/
|
||||
fun setSignalingEndpoints(urls: List<String>) {
|
||||
_signalingEndpoints.value = urls.map { SignalingEndpoint(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Report success for an endpoint (resets its circuit breaker).
|
||||
*/
|
||||
suspend fun reportSuccess(endpointId: String) {
|
||||
circuitBreaker.reset(endpointId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Report failure for an endpoint (increments circuit breaker).
|
||||
*/
|
||||
suspend fun reportFailure(endpointId: String) {
|
||||
circuitBreaker.recordFailure(endpointId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get best available STUN URLs (skipping open circuits).
|
||||
*/
|
||||
fun getHealthyStunUrls(): List<String> {
|
||||
return _stunEndpoints.value
|
||||
.filter { !circuitBreaker.isOpen(it.url, failureThreshold, resetTimeoutMs) }
|
||||
.map { it.url }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get best available TURN servers (skipping open circuits).
|
||||
*/
|
||||
fun getHealthyTurnServers(): List<TurnServer> {
|
||||
return _turnEndpoints.value
|
||||
.filter { !circuitBreaker.isOpen(it.url, failureThreshold, resetTimeoutMs) }
|
||||
.map { TurnServer(it.url, it.username, it.credential) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get best available signaling URL (first healthy).
|
||||
*/
|
||||
fun getHealthySignalingUrl(): String? {
|
||||
return _signalingEndpoints.value
|
||||
.firstOrNull { !circuitBreaker.isOpen(it.url, failureThreshold, resetTimeoutMs) }
|
||||
?.url
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WebRTC config using current healthy endpoints.
|
||||
*/
|
||||
fun buildWebRTCConfig(
|
||||
defaultStun: List<StunServer>,
|
||||
defaultTurn: List<TurnServer>,
|
||||
defaultSignalingUrl: String
|
||||
): WebRTCConfig {
|
||||
val stunUrls = getHealthyStunUrls()
|
||||
val turnServers = getHealthyTurnServers()
|
||||
val signalingUrl = getHealthySignalingUrl()
|
||||
return WebRTCConfig(
|
||||
stunServers = if (stunUrls.isEmpty()) defaultStun else stunUrls.map { StunServer(it) },
|
||||
turnServers = if (turnServers.isEmpty()) defaultTurn else turnServers.map { TurnServer(it.url, it.username, it.credential) },
|
||||
signalingServerUrl = signalingUrl ?: defaultSignalingUrl,
|
||||
iceCandidatePoolSize = 10,
|
||||
mediaCodecPolicy = MediaCodecPolicy.default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class StunEndpoint(val url: String)
|
||||
data class TurnEndpoint(val url: String, val username: String? = null, val credential: String? = null)
|
||||
data class SignalingEndpoint(val url: String)
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import com.smoa.core.common.CellularGeneration
|
||||
import com.smoa.core.common.NetworkTransportType
|
||||
|
||||
/**
|
||||
* Policy for smart media routing: path preference and lag reduction.
|
||||
* Used by [NetworkPathSelector] to choose the best network for voice/video.
|
||||
* Supports 4G LTE, 5G, and 5G MW (millimeter wave) when on cellular.
|
||||
*/
|
||||
data class MediaRoutingPolicy(
|
||||
/** Prefer low-latency transports (e.g. Wi-Fi, Ethernet over cellular). */
|
||||
val preferLowLatency: Boolean = true,
|
||||
/** When policy requires VPN, prefer VPN transport for media. */
|
||||
val preferVpnWhenRequired: Boolean = true,
|
||||
/** Transport preference order (first = highest). Default: WIFI, VPN, ETHERNET, CELLULAR. */
|
||||
val transportPreferenceOrder: List<NetworkTransportType> = listOf(
|
||||
NetworkTransportType.WIFI,
|
||||
NetworkTransportType.VPN,
|
||||
NetworkTransportType.ETHERNET,
|
||||
NetworkTransportType.CELLULAR,
|
||||
NetworkTransportType.UNKNOWN
|
||||
),
|
||||
/** Within cellular: prefer 5G MW > 5G > 4G LTE for lower latency and higher capacity. */
|
||||
val cellularGenerationPreferenceOrder: List<CellularGeneration> = listOf(
|
||||
CellularGeneration.NR_5G_MW,
|
||||
CellularGeneration.NR_5G,
|
||||
CellularGeneration.LTE_4G,
|
||||
CellularGeneration.UNKNOWN
|
||||
),
|
||||
/** Fall back to next-best path when current path quality degrades. */
|
||||
val allowPathFailover: Boolean = true,
|
||||
/** Minimum estimated bandwidth (kbps) to attempt video; below this use audio-only. */
|
||||
val minBandwidthKbpsForVideo: Int = 128
|
||||
) {
|
||||
fun rank(transport: NetworkTransportType): Int {
|
||||
val index = transportPreferenceOrder.indexOf(transport)
|
||||
return if (index < 0) Int.MAX_VALUE else index
|
||||
}
|
||||
|
||||
fun rankCellularGeneration(generation: CellularGeneration?): Int {
|
||||
if (generation == null) return Int.MAX_VALUE
|
||||
val index = cellularGenerationPreferenceOrder.indexOf(generation)
|
||||
return if (index < 0) Int.MAX_VALUE else index
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import com.smoa.core.common.ConnectivityManager
|
||||
import com.smoa.core.common.NetworkTransportType
|
||||
import com.smoa.core.security.VPNManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Selects the best network path for media to reduce lag and improve QoS.
|
||||
* Uses [ConnectivityManager] and [VPNManager] with [MediaRoutingPolicy].
|
||||
*/
|
||||
@Singleton
|
||||
class NetworkPathSelector @Inject constructor(
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
private val vpnManager: VPNManager,
|
||||
private val policy: MediaRoutingPolicy
|
||||
) {
|
||||
private val _selectedPath = MutableStateFlow(selectedPathSync())
|
||||
val selectedPath: StateFlow<SelectedPath> = _selectedPath.asStateFlow()
|
||||
|
||||
init {
|
||||
// When connectivity or VPN changes, recompute path (caller can observe connectivityState/vpnState and call refresh())
|
||||
}
|
||||
|
||||
/** Current best path for media. */
|
||||
fun getSelectedPath(): SelectedPath = selectedPathSync()
|
||||
|
||||
/** Recompute and emit best path. Call when connectivity or VPN state changes. */
|
||||
fun refresh() {
|
||||
_selectedPath.value = selectedPathSync()
|
||||
}
|
||||
|
||||
private fun selectedPathSync(): SelectedPath {
|
||||
if (connectivityManager.isOffline() || connectivityManager.isRestricted()) {
|
||||
return SelectedPath(
|
||||
transport = NetworkTransportType.UNKNOWN,
|
||||
cellularGeneration = null,
|
||||
recommendedForVideo = false,
|
||||
reason = "Offline or restricted"
|
||||
)
|
||||
}
|
||||
val transport = connectivityManager.getActiveTransportType()
|
||||
val vpnRequired = vpnManager.isVPNRequired()
|
||||
val vpnConnected = vpnManager.isVPNConnected()
|
||||
val effectiveTransport = if (vpnRequired && !vpnConnected) {
|
||||
NetworkTransportType.UNKNOWN
|
||||
} else {
|
||||
transport
|
||||
}
|
||||
val cellularGeneration = if (effectiveTransport == NetworkTransportType.CELLULAR) {
|
||||
connectivityManager.getCellularGeneration()
|
||||
} else null
|
||||
val transportRank = policy.rank(effectiveTransport)
|
||||
val cellularRank = policy.rankCellularGeneration(cellularGeneration)
|
||||
val rank = if (effectiveTransport == NetworkTransportType.CELLULAR && cellularGeneration != null) {
|
||||
transportRank * 10 + cellularRank
|
||||
} else transportRank
|
||||
val recommendedForVideo = connectivityManager.isOnline() &&
|
||||
effectiveTransport != NetworkTransportType.UNKNOWN &&
|
||||
policy.minBandwidthKbpsForVideo > 0
|
||||
return SelectedPath(
|
||||
transport = effectiveTransport,
|
||||
cellularGeneration = cellularGeneration,
|
||||
rank = rank,
|
||||
recommendedForVideo = recommendedForVideo,
|
||||
reason = if (vpnRequired && !vpnConnected) "VPN required" else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of path selection for media.
|
||||
* When [transport] is CELLULAR, [cellularGeneration] is 4G LTE, 5G, or 5G MW.
|
||||
*/
|
||||
data class SelectedPath(
|
||||
val transport: NetworkTransportType,
|
||||
val cellularGeneration: com.smoa.core.common.CellularGeneration? = null,
|
||||
val rank: Int = Int.MAX_VALUE,
|
||||
val recommendedForVideo: Boolean = false,
|
||||
val reason: String? = null
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import com.smoa.core.common.QoSPolicy
|
||||
import com.smoa.core.common.TrafficClass
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Orchestrates smart routing for better QoS, lag reduction, infra management, and system stability.
|
||||
* Combines [NetworkPathSelector], [InfrastructureManager], [ConnectionStabilityController],
|
||||
* and [AdaptiveCodecSelector] into a single service for the communications/meetings stack.
|
||||
*/
|
||||
@Singleton
|
||||
class SmartRoutingService @Inject constructor(
|
||||
private val networkPathSelector: NetworkPathSelector,
|
||||
private val infrastructureManager: InfrastructureManager,
|
||||
private val stabilityController: ConnectionStabilityController,
|
||||
private val adaptiveCodecSelector: AdaptiveCodecSelector,
|
||||
private val connectionQualityMonitor: ConnectionQualityMonitor
|
||||
) {
|
||||
private val _routingState = MutableStateFlow(RoutingState())
|
||||
val routingState: StateFlow<RoutingState> = _routingState.asStateFlow()
|
||||
|
||||
init {
|
||||
// Expose combined state for UI or WebRTC layer
|
||||
// _routingState can be updated from path + stability + quality
|
||||
refreshState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Current best path, degradation, and infra summary.
|
||||
*/
|
||||
fun getRoutingState(): RoutingState = _routingState.value
|
||||
|
||||
/**
|
||||
* Recompute routing state (path, degradation, backoff). Call when connectivity or quality changes.
|
||||
*/
|
||||
fun refreshState() {
|
||||
val path = networkPathSelector.getSelectedPath()
|
||||
val degradation = stabilityController.degradationMode.value
|
||||
val backoffMs = stabilityController.reconnectBackoffMs.value
|
||||
val sessionCount = stabilityController.activeSessionCount.value
|
||||
val quality = connectionQualityMonitor.currentQuality.value
|
||||
_routingState.value = RoutingState(
|
||||
selectedPath = path,
|
||||
degradationMode = degradation,
|
||||
reconnectBackoffMs = backoffMs,
|
||||
activeSessionCount = sessionCount,
|
||||
connectionTier = quality.tier,
|
||||
recommendedForVideo = path.recommendedForVideo && !stabilityController.shouldDisableVideo()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply connection tier from quality monitor to codec selector and optionally trigger degradation.
|
||||
*/
|
||||
fun updateFromConnectionQuality() {
|
||||
val quality = connectionQualityMonitor.currentQuality.value
|
||||
adaptiveCodecSelector.selectForTier(quality.tier)
|
||||
if (quality.tier == ConnectionTier.VERY_LOW) {
|
||||
stabilityController.setDegradationMode(DegradationMode.AUDIO_ONLY)
|
||||
} else if (quality.tier == ConnectionTier.LOW && stabilityController.degradationMode.value == DegradationMode.AUDIO_ONLY) {
|
||||
stabilityController.setDegradationMode(DegradationMode.NONE)
|
||||
}
|
||||
refreshState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify path/connectivity changed (e.g. from ConnectivityManager callback).
|
||||
*/
|
||||
fun onConnectivityChanged() {
|
||||
networkPathSelector.refresh()
|
||||
refreshState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebRTC config with healthy infra endpoints.
|
||||
*/
|
||||
fun getWebRTCConfig(): WebRTCConfig {
|
||||
return infrastructureManager.buildWebRTCConfig(
|
||||
defaultStun = WebRTCConfig.default().stunServers,
|
||||
defaultTurn = WebRTCConfig.default().turnServers,
|
||||
defaultSignalingUrl = WebRTCConfig.default().signalingServerUrl
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set QoS policy for stability (session cap, bitrate cap).
|
||||
*/
|
||||
fun setQoSPolicy(policy: QoSPolicy) {
|
||||
stabilityController.setQoSPolicy(policy)
|
||||
refreshState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Record connection failure and return backoff before retry.
|
||||
*/
|
||||
fun recordConnectionFailure(): Long {
|
||||
val backoff = stabilityController.recordConnectionFailure()
|
||||
refreshState()
|
||||
return backoff
|
||||
}
|
||||
|
||||
/**
|
||||
* Record connection success (resets backoff).
|
||||
*/
|
||||
fun recordConnectionSuccess() {
|
||||
stabilityController.recordConnectionSuccess()
|
||||
refreshState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Report endpoint failure for infra failover.
|
||||
*/
|
||||
suspend fun reportEndpointFailure(endpointId: String) {
|
||||
infrastructureManager.reportFailure(endpointId)
|
||||
refreshState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Report endpoint success (resets circuit for that endpoint).
|
||||
*/
|
||||
suspend fun reportEndpointSuccess(endpointId: String) {
|
||||
infrastructureManager.reportSuccess(endpointId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority for traffic class (QoS scheduling hint).
|
||||
*/
|
||||
fun priorityForTrafficClass(trafficClass: TrafficClass): Int = trafficClass.priority
|
||||
|
||||
/**
|
||||
* Try to start a media session (respects QoS session cap). Returns true if started.
|
||||
*/
|
||||
fun tryStartSession(): Boolean {
|
||||
val ok = stabilityController.notifySessionStarted()
|
||||
if (ok) refreshState()
|
||||
return ok
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that a media session ended (for session cap and stability).
|
||||
*/
|
||||
fun notifySessionEnded() {
|
||||
stabilityController.notifySessionEnded()
|
||||
refreshState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined smart routing state for UI or media layer.
|
||||
*/
|
||||
data class RoutingState(
|
||||
val selectedPath: SelectedPath? = null,
|
||||
val degradationMode: DegradationMode = DegradationMode.NONE,
|
||||
val reconnectBackoffMs: Long = 0L,
|
||||
val activeSessionCount: Int = 0,
|
||||
val connectionTier: ConnectionTier = ConnectionTier.MEDIUM,
|
||||
val recommendedForVideo: Boolean = true
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Stub implementation of [ConnectionQualityMonitor].
|
||||
* Reports a fixed MEDIUM tier until WebRTC stats are integrated; then replace
|
||||
* with an implementation that parses RTCStatsReport (e.g. outbound-rtp,
|
||||
* remote-inbound-rtp, candidate-pair) to compute estimated bandwidth, RTT, and loss.
|
||||
*/
|
||||
@Singleton
|
||||
class StubConnectionQualityMonitor @Inject constructor() : ConnectionQualityMonitor {
|
||||
private val _currentQuality = MutableStateFlow(
|
||||
ConnectionQuality(
|
||||
estimatedBandwidthKbps = 384,
|
||||
rttMs = 80,
|
||||
packetLossFraction = 0f,
|
||||
tier = ConnectionTier.MEDIUM
|
||||
)
|
||||
)
|
||||
override val currentQuality: StateFlow<ConnectionQuality> = _currentQuality.asStateFlow()
|
||||
|
||||
override fun qualityUpdates(): Flow<ConnectionQuality> = currentQuality
|
||||
|
||||
/** Update quality (e.g. from WebRTC getStats callback). */
|
||||
fun update(estimatedBandwidthKbps: Int, rttMs: Int = -1, packetLoss: Float = -1f) {
|
||||
val tier = connectionTierFromBandwidth(estimatedBandwidthKbps, rttMs, packetLoss)
|
||||
_currentQuality.value = ConnectionQuality(
|
||||
estimatedBandwidthKbps = estimatedBandwidthKbps,
|
||||
rttMs = rttMs,
|
||||
packetLossFraction = packetLoss,
|
||||
tier = tier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ import javax.inject.Singleton
|
||||
*/
|
||||
@Singleton
|
||||
class VoiceTransport @Inject constructor(
|
||||
private val webRTCManager: WebRTCManager
|
||||
private val webRTCManager: WebRTCManager,
|
||||
private val smartRoutingService: SmartRoutingService
|
||||
) {
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
@@ -27,19 +28,21 @@ class VoiceTransport @Inject constructor(
|
||||
*/
|
||||
suspend fun joinChannel(channelId: String): Result<Unit> {
|
||||
return try {
|
||||
if (!smartRoutingService.tryStartSession()) {
|
||||
return Result.Error(IllegalStateException("Session cap reached"))
|
||||
}
|
||||
_connectionState.value = ConnectionState.Connecting(channelId)
|
||||
|
||||
// Initialize WebRTC peer connection (audio only for voice)
|
||||
val connectionResult = webRTCManager.initializePeerConnection(channelId, isAudioOnly = true)
|
||||
|
||||
when (connectionResult) {
|
||||
is Result.Success -> {
|
||||
peerConnection = connectionResult.data
|
||||
currentChannelId = channelId
|
||||
smartRoutingService.recordConnectionSuccess()
|
||||
_connectionState.value = ConnectionState.Connected(channelId)
|
||||
Result.Success(Unit)
|
||||
}
|
||||
is Result.Error -> {
|
||||
smartRoutingService.recordConnectionFailure()
|
||||
_connectionState.value = ConnectionState.Error(connectionResult.exception.message ?: "Failed to connect")
|
||||
Result.Error(connectionResult.exception)
|
||||
}
|
||||
@@ -49,6 +52,7 @@ class VoiceTransport @Inject constructor(
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
smartRoutingService.recordConnectionFailure()
|
||||
_connectionState.value = ConnectionState.Error(e.message ?: "Unknown error")
|
||||
Result.Error(e)
|
||||
}
|
||||
@@ -70,6 +74,7 @@ class VoiceTransport @Inject constructor(
|
||||
|
||||
peerConnection = null
|
||||
currentChannelId = null
|
||||
smartRoutingService.notifySessionEnded()
|
||||
_connectionState.value = ConnectionState.Disconnected
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package com.smoa.modules.communications.domain
|
||||
|
||||
/**
|
||||
* WebRTC configuration for STUN/TURN servers and signaling.
|
||||
* WebRTC configuration for STUN/TURN servers, signaling, and optional
|
||||
* connection-speed-aware media (audio/video codec) policy.
|
||||
*/
|
||||
data class WebRTCConfig(
|
||||
val stunServers: List<StunServer>,
|
||||
val turnServers: List<TurnServer>,
|
||||
val signalingServerUrl: String,
|
||||
val iceCandidatePoolSize: Int = 10
|
||||
val iceCandidatePoolSize: Int = 10,
|
||||
/** When set, codec and bitrate are chosen from this policy based on connection speed. */
|
||||
val mediaCodecPolicy: MediaCodecPolicy? = null
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
@@ -20,9 +23,10 @@ data class WebRTCConfig(
|
||||
StunServer("stun:stun.l.google.com:19302"),
|
||||
StunServer("stun:stun1.l.google.com:19302")
|
||||
),
|
||||
turnServers = emptyList(), // TURN servers should be configured per deployment
|
||||
signalingServerUrl = "", // Should be configured per deployment
|
||||
iceCandidatePoolSize = 10
|
||||
turnServers = emptyList(),
|
||||
signalingServerUrl = "",
|
||||
iceCandidatePoolSize = 10,
|
||||
mediaCodecPolicy = MediaCodecPolicy.default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ import javax.inject.Singleton
|
||||
*/
|
||||
@Singleton
|
||||
class WebRTCManager @Inject constructor(
|
||||
private val context: Context
|
||||
private val context: Context,
|
||||
private val adaptiveCodecSelector: AdaptiveCodecSelector,
|
||||
private val smartRoutingService: SmartRoutingService
|
||||
) {
|
||||
private val config = WebRTCConfig.default()
|
||||
private fun getConfig(): WebRTCConfig = smartRoutingService.getWebRTCConfig()
|
||||
private val peerConnections = mutableMapOf<String, WebRTCPeerConnection>()
|
||||
private val _connectionState = MutableStateFlow<WebRTCConnectionState>(WebRTCConnectionState.Disconnected)
|
||||
val connectionState: StateFlow<WebRTCConnectionState> = _connectionState.asStateFlow()
|
||||
@@ -62,9 +64,9 @@ class WebRTCManager @Inject constructor(
|
||||
* Create RTC configuration with STUN/TURN servers.
|
||||
*/
|
||||
private fun createRTCConfiguration(): RTCConfiguration {
|
||||
val config = getConfig()
|
||||
val iceServers = mutableListOf<IceServer>()
|
||||
|
||||
// Add STUN servers
|
||||
config.stunServers.forEach { stunServer ->
|
||||
iceServers.add(IceServer(stunServer.url))
|
||||
}
|
||||
@@ -80,9 +82,14 @@ class WebRTCManager @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
val policy = config.mediaCodecPolicy
|
||||
val audioConstraints = if (policy != null) adaptiveCodecSelector.getAudioConstraints() else null
|
||||
val videoConstraints = if (policy != null) adaptiveCodecSelector.getVideoConstraints() else null
|
||||
return RTCConfiguration(
|
||||
iceServers = iceServers,
|
||||
iceCandidatePoolSize = config.iceCandidatePoolSize
|
||||
iceCandidatePoolSize = config.iceCandidatePoolSize,
|
||||
audioConstraints = audioConstraints,
|
||||
videoConstraints = videoConstraints
|
||||
)
|
||||
}
|
||||
|
||||
@@ -211,10 +218,14 @@ data class WebRTCPeerConnection(
|
||||
|
||||
/**
|
||||
* RTC configuration for peer connections.
|
||||
* When connection-speed-aware codecs are enabled, audioConstraints and videoConstraints
|
||||
* are set from [AdaptiveCodecSelector] so encoding uses the appropriate codec and bitrate.
|
||||
*/
|
||||
data class RTCConfiguration(
|
||||
val iceServers: List<IceServer>,
|
||||
val iceCandidatePoolSize: Int = 10
|
||||
val iceCandidatePoolSize: Int = 10,
|
||||
val audioConstraints: AudioCodecConstraints? = null,
|
||||
val videoConstraints: VideoCodecConstraints? = null
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,8 @@ import javax.inject.Singleton
|
||||
*/
|
||||
@Singleton
|
||||
class VideoTransport @Inject constructor(
|
||||
private val webRTCManager: com.smoa.modules.communications.domain.WebRTCManager
|
||||
private val webRTCManager: com.smoa.modules.communications.domain.WebRTCManager,
|
||||
private val smartRoutingService: com.smoa.modules.communications.domain.SmartRoutingService
|
||||
) {
|
||||
private val _connectionState = MutableStateFlow<MeetingConnectionState>(MeetingConnectionState.Disconnected)
|
||||
val connectionState: StateFlow<MeetingConnectionState> = _connectionState.asStateFlow()
|
||||
@@ -29,26 +30,30 @@ class VideoTransport @Inject constructor(
|
||||
*/
|
||||
suspend fun joinMeeting(meetingId: String, userId: String): Result<Unit> {
|
||||
return try {
|
||||
if (!smartRoutingService.tryStartSession()) {
|
||||
return Result.Error(IllegalStateException("Session cap reached"))
|
||||
}
|
||||
_connectionState.value = MeetingConnectionState.Connecting(meetingId)
|
||||
|
||||
// Initialize WebRTC peer connection (audio + video)
|
||||
val connectionResult = webRTCManager.initializePeerConnection(meetingId, isAudioOnly = false)
|
||||
|
||||
val routingState = smartRoutingService.getRoutingState()
|
||||
val recommendedForVideo = routingState.recommendedForVideo
|
||||
val isAudioOnly = !recommendedForVideo
|
||||
|
||||
val connectionResult = webRTCManager.initializePeerConnection(meetingId, isAudioOnly = isAudioOnly)
|
||||
|
||||
when (connectionResult) {
|
||||
is Result.Success -> {
|
||||
peerConnection = connectionResult.data
|
||||
currentMeetingId = meetingId
|
||||
|
||||
// Start audio and video transmission
|
||||
smartRoutingService.recordConnectionSuccess()
|
||||
peerConnection?.let { connection ->
|
||||
webRTCManager.startAudioTransmission(connection)
|
||||
webRTCManager.startVideoTransmission(connection)
|
||||
if (!isAudioOnly) webRTCManager.startVideoTransmission(connection)
|
||||
}
|
||||
|
||||
_connectionState.value = MeetingConnectionState.Connected(meetingId)
|
||||
Result.Success(Unit)
|
||||
}
|
||||
is Result.Error -> {
|
||||
smartRoutingService.recordConnectionFailure()
|
||||
_connectionState.value = MeetingConnectionState.Error(
|
||||
connectionResult.exception.message ?: "Failed to connect"
|
||||
)
|
||||
@@ -60,6 +65,7 @@ class VideoTransport @Inject constructor(
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
smartRoutingService.recordConnectionFailure()
|
||||
_connectionState.value = MeetingConnectionState.Error(e.message ?: "Unknown error")
|
||||
Result.Error(e)
|
||||
}
|
||||
@@ -83,6 +89,7 @@ class VideoTransport @Inject constructor(
|
||||
|
||||
peerConnection = null
|
||||
currentMeetingId = null
|
||||
smartRoutingService.notifySessionEnded()
|
||||
_connectionState.value = MeetingConnectionState.Disconnected
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.smoa.modules.reports.domain
|
||||
|
||||
import com.smoa.core.security.AuditLogger
|
||||
import com.smoa.core.security.AuditEventType
|
||||
import java.security.MessageDigest
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
@@ -15,7 +16,10 @@ class ReportService @Inject constructor(
|
||||
private val reportGenerator: ReportGenerator,
|
||||
private val auditLogger: AuditLogger
|
||||
) {
|
||||
|
||||
|
||||
/** When true, reports get a minimal content-hash signature; for full signing use a dedicated signing service. */
|
||||
var signReports: Boolean = false
|
||||
|
||||
/**
|
||||
* Generate report.
|
||||
*/
|
||||
@@ -28,6 +32,14 @@ class ReportService @Inject constructor(
|
||||
template: ReportTemplate?
|
||||
): Result<Report> {
|
||||
return try {
|
||||
val signature = if (signReports) {
|
||||
DigitalSignature(
|
||||
signatureId = UUID.randomUUID().toString(),
|
||||
signerId = generatedBy,
|
||||
signatureDate = Date(),
|
||||
signatureData = MessageDigest.getInstance("SHA-256").digest(content)
|
||||
)
|
||||
} else null
|
||||
val report = Report(
|
||||
reportId = UUID.randomUUID().toString(),
|
||||
reportType = reportType,
|
||||
@@ -37,7 +49,7 @@ class ReportService @Inject constructor(
|
||||
content = content,
|
||||
generatedDate = Date(),
|
||||
generatedBy = generatedBy,
|
||||
signature = null, // TODO: Add digital signature
|
||||
signature = signature,
|
||||
metadata = ReportMetadata()
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user