@mentra/acs-meeting 3.2.0-dev.221 → 3.2.0-dev.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +4 -2
  2. package/android/build.gradle +23 -0
  3. package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +291 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/java/com/mentra/acsmeeting/AbandonedCallAgent.kt +36 -0
  6. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +392 -22
  7. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +909 -56
  8. package/android/src/main/java/com/mentra/acsmeeting/CallCapabilities.kt +33 -0
  9. package/android/src/main/java/com/mentra/acsmeeting/audio/AcsAudioPolicy.kt +21 -0
  10. package/android/src/main/java/com/mentra/acsmeeting/audio/AudioUplinkChain.kt +126 -0
  11. package/android/src/main/java/com/mentra/acsmeeting/audio/PcmBridge.kt +27 -4
  12. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkPacer.kt +45 -10
  13. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +110 -5
  14. package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +501 -0
  15. package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +51 -0
  16. package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +81 -0
  17. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +355 -0
  18. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +109 -0
  19. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +160 -0
  20. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +103 -0
  21. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +53 -0
  22. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +127 -0
  23. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +573 -0
  24. package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +10 -101
  25. package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +147 -0
  26. package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +47 -5
  27. package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +772 -0
  28. package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +73 -0
  29. package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +17 -0
  30. package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +139 -0
  31. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +144 -0
  32. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +46 -0
  33. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +160 -0
  34. package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +3 -1
  35. package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +69 -0
  36. package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +207 -0
  37. package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +356 -0
  38. package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +155 -0
  39. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +94 -3
  40. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +33 -5
  41. package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +15 -0
  42. package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +178 -0
  43. package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +145 -0
  44. package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +143 -0
  45. package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +23 -4
  46. package/android/src/main/java/com/mentra/acsmeeting/video/FramePacer.kt +70 -0
  47. package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +1 -1
  48. package/android/src/main/java/com/mentra/acsmeeting/video/VideoProfile.kt +36 -3
  49. package/android/src/test/java/com/mentra/acsmeeting/AbandonedCallAgentTest.kt +74 -0
  50. package/android/src/test/java/com/mentra/acsmeeting/CallCapabilitiesTest.kt +47 -0
  51. package/android/src/test/java/com/mentra/acsmeeting/audio/AudioUplinkChainTest.kt +250 -0
  52. package/android/src/test/java/com/mentra/acsmeeting/audio/CapturePolicyTest.kt +44 -0
  53. package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkPacerTest.kt +34 -4
  54. package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkSenderTest.kt +96 -0
  55. package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +102 -0
  56. package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +148 -0
  57. package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +59 -0
  58. package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +59 -0
  59. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +35 -0
  60. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +165 -0
  61. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +96 -0
  62. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +187 -0
  63. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +172 -0
  64. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +141 -0
  65. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +69 -0
  66. package/android/src/test/java/com/mentra/acsmeeting/source/AcsInvestigationTest.kt +40 -0
  67. package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +41 -0
  68. package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +26 -8
  69. package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +109 -0
  70. package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +165 -0
  71. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +148 -0
  72. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +76 -0
  73. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +275 -0
  74. package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +9 -0
  75. package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +356 -0
  76. package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +411 -0
  77. package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +83 -0
  78. package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +69 -3
  79. package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +182 -0
  80. package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +149 -0
  81. package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +97 -0
  82. package/android/src/test/java/com/mentra/acsmeeting/video/AcsTimestampTest.kt +34 -0
  83. package/android/src/test/java/com/mentra/acsmeeting/video/FramePacerTest.kt +105 -0
  84. package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +5 -0
  85. package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +38 -3
  86. package/build/AcsMeeting.types.d.ts +5 -0
  87. package/build/AcsMeeting.types.d.ts.map +1 -1
  88. package/build/AcsMeeting.types.js.map +1 -1
  89. package/build/AcsMeetingModule.d.ts +23 -0
  90. package/build/AcsMeetingModule.d.ts.map +1 -1
  91. package/build/AcsMeetingModule.js.map +1 -1
  92. package/build/AcsMeetingModule.web.d.ts +15 -0
  93. package/build/AcsMeetingModule.web.d.ts.map +1 -1
  94. package/build/AcsMeetingModule.web.js +13 -0
  95. package/build/AcsMeetingModule.web.js.map +1 -1
  96. package/ios/AcsMeetingModule.swift +213 -2
  97. package/package.json +1 -1
  98. package/src/AcsMeeting.types.ts +5 -0
  99. package/src/AcsMeetingModule.ts +13 -0
  100. package/src/AcsMeetingModule.web.ts +13 -0
@@ -0,0 +1,103 @@
1
+ package com.mentra.acsmeeting.network
2
+
3
+ /**
4
+ * Decides when a joined scoped network is actually ready for ICE to gather on.
5
+ *
6
+ * `onAvailable` is not that moment. Android's own guidance is to wait for `onLinkPropertiesChanged`
7
+ * and `onCapabilitiesChanged` rather than reading properties synchronously inside `onAvailable`, and
8
+ * on device the consequence was concrete: the hotspot address can land in the kernel's interface
9
+ * table *after* `onAvailable` returns, and libwebrtc gathers from that table. Starting ingest on
10
+ * `onAvailable` therefore raced the address into existence, and losing that race is indistinguishable
11
+ * from the wiring bugs it was mistaken for.
12
+ *
13
+ * Readiness needs agreement from three places, so all three are recorded separately and the missing
14
+ * one is named. The kernel table is the odd one out: it has no callback, so the shell polls it and
15
+ * feeds the answer in through [onInterfaceTable].
16
+ *
17
+ * Pure and mutable-but-self-contained, so the awkward orderings are unit testable without a device.
18
+ */
19
+ class ScopedNetworkReadiness {
20
+
21
+ /** What each source has reported so far. Null means "not yet", not "absent". */
22
+ data class Observations(
23
+ val available: Boolean = false,
24
+ val capabilities: Boolean = false,
25
+ val linkInterface: String? = null,
26
+ val linkAddress: String? = null,
27
+ val tableOwner: String? = null,
28
+ )
29
+
30
+ sealed interface Verdict {
31
+ /**
32
+ * [owner] is the interface the kernel says holds [address]. It can differ from
33
+ * [interfaceName], which is what `LinkProperties` claimed — a disagreement worth logging,
34
+ * not worth blocking on, since ICE only ever uses the address.
35
+ */
36
+ data class Ready(
37
+ val address: String,
38
+ val interfaceName: String?,
39
+ val owner: String,
40
+ ) : Verdict
41
+
42
+ /** [missing] names the one source still to report, for the timeout message. */
43
+ data class Waiting(val missing: String) : Verdict
44
+ }
45
+
46
+ @Volatile
47
+ var observations = Observations()
48
+ private set
49
+
50
+ private val lock = Any()
51
+
52
+ fun onAvailable() = update { it.copy(available = true) }
53
+
54
+ fun onCapabilities() = update { it.copy(capabilities = true) }
55
+
56
+ /**
57
+ * Taken verbatim: `LinkProperties` carries the network's whole current state, so a later
58
+ * callback without an address means the address is gone, not that the old one still holds.
59
+ */
60
+ fun onLinkProperties(interfaceName: String?, address: String?) =
61
+ update { it.copy(linkInterface = interfaceName, linkAddress = address) }
62
+
63
+ /** Seed from the synchronous read in `onAvailable`; never clears what a callback reported. */
64
+ fun seedLinkProperties(interfaceName: String?, address: String?) =
65
+ update {
66
+ if (address == null) it else it.copy(linkInterface = interfaceName, linkAddress = address)
67
+ }
68
+
69
+ fun onInterfaceTable(owner: String?) = update { it.copy(tableOwner = owner) }
70
+
71
+ fun reset() = update { Observations() }
72
+
73
+ fun verdict(): Verdict = verdictOf(observations)
74
+
75
+ /** True once everything with a callback has reported, so only the table poll is left. */
76
+ fun callbacksSatisfied(): Boolean = callbacksSatisfiedBy(observations)
77
+
78
+ private inline fun update(transform: (Observations) -> Observations) {
79
+ synchronized(lock) { observations = transform(observations) }
80
+ }
81
+
82
+ companion object {
83
+ fun callbacksSatisfiedBy(observations: Observations): Boolean =
84
+ observations.available && observations.capabilities && observations.linkAddress != null
85
+
86
+ /** Ordered so the named blocker is the earliest unmet one, which is the useful one. */
87
+ fun verdictOf(observations: Observations): Verdict =
88
+ when {
89
+ !observations.available -> Verdict.Waiting("onAvailable")
90
+ !observations.capabilities -> Verdict.Waiting("onCapabilitiesChanged")
91
+ observations.linkAddress == null ->
92
+ Verdict.Waiting("an IPv4 address from onLinkPropertiesChanged")
93
+ observations.tableOwner == null ->
94
+ Verdict.Waiting("the kernel interface table to carry ${observations.linkAddress}")
95
+ else ->
96
+ Verdict.Ready(
97
+ observations.linkAddress,
98
+ observations.linkInterface,
99
+ observations.tableOwner,
100
+ )
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,53 @@
1
+ package com.mentra.acsmeeting.network
2
+
3
+ import android.net.NetworkCapabilities
4
+
5
+ /**
6
+ * Declarative description of the SoftAP network request.
7
+ *
8
+ * `NetworkRequest.Builder` cannot be constructed in a plain JVM test, so the shape of the request
9
+ * lives here as data and [ScopedSoftApNetwork] translates it into the real builder. That makes the
10
+ * one property this architecture depends on — that the request does **not** ask for
11
+ * `NET_CAPABILITY_INTERNET` — assertable without a device.
12
+ *
13
+ * Dropping the internet capability is what keeps the phone dual-homed: Android will not promote the
14
+ * glasses hotspot to the default network, so ACS traffic stays on cellular while media crosses the
15
+ * local link. The other half of that guarantee is the MTK firmware patch that stops the AP
16
+ * advertising a default route.
17
+ */
18
+ data class ScopedNetworkRequestSpec(
19
+ val ssid: String,
20
+ val passphrase: String,
21
+ val transportTypes: Set<Int>,
22
+ val removedCapabilities: Set<Int>,
23
+ val timeoutMs: Int,
24
+ ) {
25
+ /** True when the request cannot be satisfied by, or promoted to, an internet-bearing network. */
26
+ val avoidsInternetCapability: Boolean
27
+ get() = removedCapabilities.contains(NetworkCapabilities.NET_CAPABILITY_INTERNET)
28
+
29
+ val hasPassphrase: Boolean
30
+ get() = passphrase.isNotEmpty()
31
+
32
+ companion object {
33
+ /**
34
+ * Android caps `requestNetwork` timeouts well above this; 30 s matches the value the
35
+ * Bluetooth SDK already uses for the same hotspot and is long enough for a cold AP start.
36
+ */
37
+ const val DEFAULT_TIMEOUT_MS = 30_000
38
+
39
+ /** The canonical SoftAP request. Every caller should go through this. */
40
+ fun forSoftAp(
41
+ ssid: String,
42
+ passphrase: String,
43
+ timeoutMs: Int = DEFAULT_TIMEOUT_MS,
44
+ ): ScopedNetworkRequestSpec =
45
+ ScopedNetworkRequestSpec(
46
+ ssid = ssid,
47
+ passphrase = passphrase,
48
+ transportTypes = setOf(NetworkCapabilities.TRANSPORT_WIFI),
49
+ removedCapabilities = setOf(NetworkCapabilities.NET_CAPABILITY_INTERNET),
50
+ timeoutMs = timeoutMs,
51
+ )
52
+ }
53
+ }
@@ -0,0 +1,127 @@
1
+ package com.mentra.acsmeeting.network
2
+
3
+ /**
4
+ * Pure lifecycle state machine for a scoped SoftAP network join.
5
+ *
6
+ * Split from the Android shell so the awkward parts — duplicate callbacks, callbacks arriving after
7
+ * a cancel, a timeout racing a late `onAvailable` — are unit testable without a device. The shell
8
+ * ([ScopedSoftApNetwork]) owns `ConnectivityManager` and forwards raw callbacks in here.
9
+ *
10
+ * `ConnectivityManager` gives no ordering or at-most-once guarantee: `onAvailable` can fire more
11
+ * than once, `onCapabilitiesChanged` fires repeatedly, and callbacks from a request that was already
12
+ * unregistered can still land. Every transition below is therefore idempotent and generation-guarded.
13
+ */
14
+ class ScopedNetworkState {
15
+
16
+ enum class Phase {
17
+ /** No request outstanding. */
18
+ IDLE,
19
+
20
+ /** `requestNetwork` issued, waiting for a usable network. */
21
+ REQUESTING,
22
+
23
+ /** Network joined and usable. */
24
+ AVAILABLE,
25
+
26
+ /** Terminal for this generation: released, lost, timed out, or failed. */
27
+ LOST,
28
+ }
29
+
30
+ /** Why a join ended without producing a usable network. */
31
+ enum class Failure {
32
+ NONE,
33
+ TIMEOUT,
34
+ UNAVAILABLE,
35
+ LOST,
36
+ PERMISSION_DENIED,
37
+ REQUEST_FAILED,
38
+ }
39
+
40
+ var phase: Phase = Phase.IDLE
41
+ private set
42
+
43
+ var failure: Failure = Failure.NONE
44
+ private set
45
+
46
+ /** Incremented on every new request so late callbacks from an old one can be rejected. */
47
+ var generation: Int = 0
48
+ private set
49
+
50
+ val isActive: Boolean
51
+ get() = phase == Phase.REQUESTING || phase == Phase.AVAILABLE
52
+
53
+ /**
54
+ * Begin a new join. Any in-flight generation is abandoned, so its callbacks become stale.
55
+ *
56
+ * @return the generation for this request; pass it back to every callback
57
+ */
58
+ fun startRequest(): Int {
59
+ generation += 1
60
+ phase = Phase.REQUESTING
61
+ failure = Failure.NONE
62
+ return generation
63
+ }
64
+
65
+ /** True when a callback belongs to the current, still-active generation. */
66
+ fun accepts(callbackGeneration: Int): Boolean = callbackGeneration == generation && isActive
67
+
68
+ /**
69
+ * A usable network arrived.
70
+ *
71
+ * @return true if this transitioned the machine; false when stale or already available
72
+ */
73
+ fun onAvailable(callbackGeneration: Int): Boolean {
74
+ if (!accepts(callbackGeneration)) return false
75
+ if (phase == Phase.AVAILABLE) return false // duplicate onAvailable
76
+ phase = Phase.AVAILABLE
77
+ return true
78
+ }
79
+
80
+ /** The request timed out before a usable network arrived. Ignored once available. */
81
+ fun onTimeout(callbackGeneration: Int): Boolean {
82
+ if (callbackGeneration != generation) return false
83
+ if (phase != Phase.REQUESTING) return false
84
+ phase = Phase.LOST
85
+ failure = Failure.TIMEOUT
86
+ return true
87
+ }
88
+
89
+ /** The framework reported the network cannot be provided. */
90
+ fun onUnavailable(callbackGeneration: Int): Boolean {
91
+ if (!accepts(callbackGeneration)) return false
92
+ phase = Phase.LOST
93
+ failure = Failure.UNAVAILABLE
94
+ return true
95
+ }
96
+
97
+ /** The joined network went away. This is the one the call path must react to. */
98
+ fun onLost(callbackGeneration: Int): Boolean {
99
+ if (!accepts(callbackGeneration)) return false
100
+ phase = Phase.LOST
101
+ failure = Failure.LOST
102
+ return true
103
+ }
104
+
105
+ /** `requestNetwork` threw, or the local-network permission was denied. */
106
+ fun onRequestFailed(callbackGeneration: Int, permissionDenied: Boolean): Boolean {
107
+ if (callbackGeneration != generation) return false
108
+ if (!isActive) return false
109
+ phase = Phase.LOST
110
+ failure = if (permissionDenied) Failure.PERMISSION_DENIED else Failure.REQUEST_FAILED
111
+ return true
112
+ }
113
+
114
+ /** Caller-initiated teardown. Not a failure. */
115
+ fun release(): Boolean {
116
+ if (!isActive) return false
117
+ phase = Phase.LOST
118
+ failure = Failure.NONE
119
+ return true
120
+ }
121
+
122
+ /** Return to IDLE so the same instance can be reused for the next call. */
123
+ fun reset() {
124
+ phase = Phase.IDLE
125
+ failure = Failure.NONE
126
+ }
127
+ }