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:
defiQUG
2026-02-10 20:37:01 -08:00
parent 97f75e144f
commit 5a8c26cf5d
101 changed files with 4923 additions and 103 deletions

View 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`).

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)
}
}

View File

@@ -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) {

View File

@@ -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()
)
}
}

View File

@@ -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
)
/**

View File

@@ -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) {

View File

@@ -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()
)