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

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 (106) hide show
  1. package/README.md +14 -3
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +10 -10
  4. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +37 -37
  5. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +4 -4
  6. package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +3 -0
  7. package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +12 -10
  8. package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +2 -0
  9. package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +2 -0
  10. package/android/src/test/java/com/mentra/acsmeeting/source/{AcsInvestigationTest.kt → MediaDiagnosticsTest.kt} +10 -10
  11. package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +3 -0
  12. package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +2 -2
  13. package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +2 -0
  14. package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +2 -2
  15. package/build/AcsMeeting.types.d.ts +10 -1
  16. package/build/AcsMeeting.types.d.ts.map +1 -1
  17. package/build/AcsMeeting.types.js.map +1 -1
  18. package/build/AcsMeetingModule.d.ts +2 -2
  19. package/build/AcsMeetingModule.d.ts.map +1 -1
  20. package/build/AcsMeetingModule.js.map +1 -1
  21. package/ios/AcsMeeting.podspec +1 -1
  22. package/ios/AcsMeetingModule.swift +284 -148
  23. package/ios/PolicyKit/Package.swift +2 -1
  24. package/ios/PolicyKit/Sources/AcsAudioPolicy/CallbackOperation.swift +60 -0
  25. package/ios/PolicyKit/Sources/AcsAudioPolicy/CapturePolicy.swift +5 -0
  26. package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +1 -0
  27. package/ios/PolicyKit/Tests/AcsAudioPolicyTests/CallAgentRetirementTests.swift +113 -0
  28. package/package.json +3 -2
  29. package/src/AcsMeeting.types.ts +4 -1
  30. package/src/AcsMeetingModule.ts +2 -2
  31. package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +0 -291
  32. package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +0 -501
  33. package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +0 -51
  34. package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +0 -81
  35. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +0 -355
  36. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +0 -109
  37. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +0 -160
  38. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +0 -103
  39. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +0 -53
  40. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +0 -127
  41. package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +0 -573
  42. package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +0 -99
  43. package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +0 -487
  44. package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +0 -147
  45. package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +0 -45
  46. package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +0 -143
  47. package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +0 -772
  48. package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +0 -57
  49. package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +0 -17
  50. package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +0 -139
  51. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +0 -144
  52. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +0 -46
  53. package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +0 -160
  54. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +0 -205
  55. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +0 -214
  56. package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +0 -33
  57. package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +0 -110
  58. package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +0 -207
  59. package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +0 -356
  60. package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +0 -155
  61. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +0 -78
  62. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +0 -372
  63. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +0 -79
  64. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +0 -15
  65. package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +0 -48
  66. package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +0 -178
  67. package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +0 -145
  68. package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +0 -143
  69. package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +0 -22
  70. package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +0 -72
  71. package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +0 -102
  72. package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +0 -148
  73. package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +0 -59
  74. package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +0 -59
  75. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +0 -35
  76. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +0 -165
  77. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +0 -96
  78. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +0 -187
  79. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +0 -172
  80. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +0 -141
  81. package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +0 -69
  82. package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +0 -22
  83. package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +0 -41
  84. package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +0 -60
  85. package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +0 -151
  86. package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +0 -67
  87. package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +0 -165
  88. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +0 -148
  89. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +0 -76
  90. package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +0 -275
  91. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +0 -193
  92. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +0 -176
  93. package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +0 -45
  94. package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +0 -356
  95. package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +0 -411
  96. package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +0 -83
  97. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +0 -61
  98. package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +0 -322
  99. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +0 -18
  100. package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +0 -49
  101. package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +0 -149
  102. package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +0 -97
  103. package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +0 -33
  104. package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +0 -126
  105. package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +0 -68
  106. package/ios/WhepVideoSource.swift +0 -356
@@ -1,275 +0,0 @@
1
- package com.mentra.acsmeeting.source
2
-
3
- import com.mentra.acsmeeting.network.Ipv4Prefix
4
- import org.assertj.core.api.Assertions.assertThat
5
- import org.junit.Test
6
-
7
- /**
8
- * Pins the candidate classification the SoftAP path depends on.
9
- *
10
- * The cases here deliberately mirror
11
- * `com.mentra.asg_client.io.streaming.config.IcePostPolicyTest` on the glasses. The two
12
- * implementations cannot share code across the module boundary, and if they disagree the symptom is
13
- * a call that negotiates and then carries nothing: one side posts an offer it considers ready while
14
- * the other rejects it, or worse, both accept a candidate pair that cannot actually reach across
15
- * the hotspot.
16
- */
17
- class SoftApSdpGuardTest {
18
-
19
- private val hostCandidate =
20
- "a=candidate:1 1 udp 2122260223 192.168.43.20 51234 typ host generation 0"
21
- private val srflxCandidate =
22
- "a=candidate:2 1 udp 1686052607 203.0.113.7 51234 typ srflx raddr 192.168.43.20 rport 51234"
23
-
24
- private fun sdp(vararg lines: String) =
25
- (listOf("v=0", "m=video 9 UDP/TLS/RTP/SAVPF 96") + lines).joinToString("\r\n")
26
-
27
- // -----------------------------------------------------------------
28
- // Accepting a usable negotiation
29
- // -----------------------------------------------------------------
30
-
31
- @Test
32
- fun `an sdp with a hotspot host candidate is accepted`() {
33
- val verdict = SoftApSdpGuard.inspect(sdp(hostCandidate))
34
-
35
- assertThat(verdict).isInstanceOf(SoftApSdpGuard.Verdict.Ok::class.java)
36
- val ok = verdict as SoftApSdpGuard.Verdict.Ok
37
- assertThat(ok.hostCandidates).hasSize(1)
38
- assertThat(ok.routableCandidates).isEmpty()
39
- }
40
-
41
- /**
42
- * Not a rejection. A stale glasses build may still gather srflx candidates, and dropping the call
43
- * over it would be worse than a warning when a usable hotspot candidate is also present — but it
44
- * is reported, because ICE could otherwise select it.
45
- */
46
- @Test
47
- fun `a stray srflx candidate is reported but does not reject`() {
48
- val verdict = SoftApSdpGuard.inspect(sdp(hostCandidate, srflxCandidate))
49
-
50
- val ok = verdict as SoftApSdpGuard.Verdict.Ok
51
- assertThat(ok.hostCandidates).hasSize(1)
52
- assertThat(ok.routableCandidates).hasSize(1)
53
- assertThat(ok.routableCandidates.single()).contains("typ srflx")
54
- }
55
-
56
- @Test
57
- fun `any rfc1918 subnet counts, not just the default hotspot range`() {
58
- // A hotspot on a non-default subnet must not silently fail the gate.
59
- for (address in listOf("192.168.1.5", "10.0.0.9", "172.16.4.4", "172.31.255.254")) {
60
- val verdict = SoftApSdpGuard.inspect(
61
- sdp("a=candidate:1 1 udp 2122260223 $address 51234 typ host generation 0"),
62
- )
63
-
64
- assertThat(verdict)
65
- .describedAs(address)
66
- .isInstanceOf(SoftApSdpGuard.Verdict.Ok::class.java)
67
- }
68
- }
69
-
70
- // -----------------------------------------------------------------
71
- // Rejecting an unusable negotiation
72
- // -----------------------------------------------------------------
73
-
74
- @Test
75
- fun `an sdp with only a srflx candidate is rejected`() {
76
- val verdict = SoftApSdpGuard.inspect(sdp(srflxCandidate))
77
-
78
- val rejected = verdict as SoftApSdpGuard.Verdict.Rejected
79
- assertThat(rejected.code).isEqualTo(SoftApSdpGuard.REASON_ONLY_NON_HOTSPOT)
80
- assertThat(rejected.detail).contains("typ srflx")
81
- }
82
-
83
- @Test
84
- fun `an sdp with no candidates at all is rejected and says so`() {
85
- val verdict = SoftApSdpGuard.inspect(sdp())
86
-
87
- val rejected = verdict as SoftApSdpGuard.Verdict.Rejected
88
- assertThat(rejected.code).isEqualTo(SoftApSdpGuard.REASON_NO_CANDIDATES)
89
- assertThat(rejected.detail).isEqualTo("no candidates at all")
90
- }
91
-
92
- /** Public and loopback host candidates are unreachable from the glasses. */
93
- @Test
94
- fun `a host candidate on a public or loopback address is rejected`() {
95
- for (address in listOf("203.0.113.7", "127.0.0.1", "8.8.8.8", "172.32.0.1", "192.169.0.1")) {
96
- val verdict = SoftApSdpGuard.inspect(
97
- sdp("a=candidate:1 1 udp 2122260223 $address 51234 typ host generation 0"),
98
- )
99
-
100
- assertThat(verdict)
101
- .describedAs(address)
102
- .isInstanceOf(SoftApSdpGuard.Verdict.Rejected::class.java)
103
- }
104
- }
105
-
106
- /**
107
- * A link-local address means the interface never got a DHCP lease from the glasses. ICE would
108
- * gather it and connect to nothing.
109
- */
110
- @Test
111
- fun `a link local host candidate is rejected`() {
112
- val verdict = SoftApSdpGuard.inspect(
113
- sdp("a=candidate:1 1 udp 2122260223 169.254.10.20 51234 typ host generation 0"),
114
- )
115
-
116
- assertThat(verdict).isInstanceOf(SoftApSdpGuard.Verdict.Rejected::class.java)
117
- }
118
-
119
- /**
120
- * libwebrtc emits these to hide private addresses from remote peers. The glasses cannot resolve
121
- * them, so a negotiation carrying only mDNS candidates is broken however healthy it looks.
122
- */
123
- @Test
124
- fun `an mdns obfuscated candidate is rejected`() {
125
- val verdict = SoftApSdpGuard.inspect(
126
- sdp(
127
- "a=candidate:1 1 udp 2122260223 " +
128
- "b8e7c1f2-0000-4000-8000-000000000000.local 51234 typ host generation 0",
129
- ),
130
- )
131
-
132
- val rejected = verdict as SoftApSdpGuard.Verdict.Rejected
133
- assertThat(rejected.code).isEqualTo(SoftApSdpGuard.REASON_ONLY_NON_HOTSPOT)
134
- }
135
-
136
- @Test
137
- fun `an ipv6 host candidate does not satisfy the gate`() {
138
- // The SoftAP link is IPv4 only, so an IPv6 candidate cannot carry this media.
139
- val verdict = SoftApSdpGuard.inspect(
140
- sdp("a=candidate:1 1 udp 2122260223 fe80::1 51234 typ host generation 0"),
141
- )
142
-
143
- assertThat(verdict).isInstanceOf(SoftApSdpGuard.Verdict.Rejected::class.java)
144
- }
145
-
146
- @Test
147
- fun `empty and non sdp input is malformed rather than candidate-less`() {
148
- for (input in listOf(null, "", " ", "v=0\r\no=- 0 0 IN IP4 0.0.0.0")) {
149
- val verdict = SoftApSdpGuard.inspect(input)
150
-
151
- assertThat((verdict as SoftApSdpGuard.Verdict.Rejected).code)
152
- .describedAs(input.orEmpty())
153
- .isEqualTo(SoftApSdpGuard.REASON_MALFORMED)
154
- }
155
- }
156
-
157
- // -----------------------------------------------------------------
158
- // Parsing details
159
- // -----------------------------------------------------------------
160
-
161
- @Test
162
- fun `candidate extraction strips the attribute prefix and ignores other lines`() {
163
- val candidates = SoftApSdpGuard.candidateLines(
164
- sdp("a=mid:0", hostCandidate, "a=sendrecv", srflxCandidate),
165
- )
166
-
167
- assertThat(candidates).hasSize(2)
168
- assertThat(candidates).allSatisfy { assertThat(it).startsWith("candidate:") }
169
- }
170
-
171
- @Test
172
- fun `candidate extraction tolerates trailing whitespace and lone newlines`() {
173
- val candidates = SoftApSdpGuard.candidateLines("m=video 9\n$hostCandidate \n")
174
-
175
- assertThat(candidates).hasSize(1)
176
- }
177
-
178
- @Test
179
- fun `private ipv4 boundaries match rfc1918 exactly`() {
180
- assertThat(SoftApSdpGuard.isPrivateIpv4("10.0.0.0")).isTrue()
181
- assertThat(SoftApSdpGuard.isPrivateIpv4("10.255.255.255")).isTrue()
182
- assertThat(SoftApSdpGuard.isPrivateIpv4("172.15.0.1")).isFalse()
183
- assertThat(SoftApSdpGuard.isPrivateIpv4("172.16.0.0")).isTrue()
184
- assertThat(SoftApSdpGuard.isPrivateIpv4("172.31.0.0")).isTrue()
185
- assertThat(SoftApSdpGuard.isPrivateIpv4("172.32.0.0")).isFalse()
186
- assertThat(SoftApSdpGuard.isPrivateIpv4("192.168.0.0")).isTrue()
187
- assertThat(SoftApSdpGuard.isPrivateIpv4("192.167.0.0")).isFalse()
188
- }
189
-
190
- @Test
191
- fun `malformed addresses are not private`() {
192
- for (token in listOf("192.168.1", "192.168.1.1.1", "192.168.1.256", "a.b.c.d", "", "host")) {
193
- assertThat(SoftApSdpGuard.isPrivateIpv4(token)).describedAs(token).isFalse()
194
- }
195
- }
196
-
197
- @Test
198
- fun `a candidate must be typ host to count`() {
199
- assertThat(SoftApSdpGuard.isSoftApHostCandidate(hostCandidate.removePrefix("a="))).isTrue()
200
- assertThat(
201
- SoftApSdpGuard.isSoftApHostCandidate(
202
- "candidate:1 1 udp 2122260223 192.168.43.20 51234 typ relay",
203
- ),
204
- ).isFalse()
205
- }
206
-
207
- @Test
208
- fun `the candidate address is read positionally, with or without the a= prefix`() {
209
- assertThat(SoftApSdpGuard.candidateAddress(hostCandidate)).isEqualTo("192.168.43.20")
210
- assertThat(
211
- SoftApSdpGuard.candidateAddress(
212
- "candidate:3558496604 1 udp 2122260223 10.48.51.202 35664 typ host generation 0",
213
- ),
214
- ).isEqualTo("10.48.51.202")
215
- }
216
-
217
- /** `raddr` would win a scan for anything dotted-quad-shaped and blame the wrong interface. */
218
- @Test
219
- fun `a reflexive candidate reports its local address, not its raddr`() {
220
- assertThat(
221
- SoftApSdpGuard.candidateAddress(
222
- "candidate:2 1 udp 1686052607 203.0.113.9 44444 typ srflx raddr 192.168.43.20 rport 51234",
223
- ),
224
- ).isEqualTo("203.0.113.9")
225
- }
226
-
227
- @Test
228
- fun `a line without an address reports none rather than a stray token`() {
229
- assertThat(SoftApSdpGuard.candidateAddress("a=mid:0")).isNull()
230
- assertThat(SoftApSdpGuard.candidateAddress("")).isNull()
231
- }
232
-
233
- // -----------------------------------------------------------------
234
- // Pinning a cellular-substituted host address back onto the hotspot
235
- // -----------------------------------------------------------------
236
-
237
- @Test
238
- fun `a cellular host address is rewritten to the scoped SoftAP address`() {
239
- val prefix = Ipv4Prefix("192.168.43.79", 24)
240
- val original =
241
- sdp("a=candidate:1 1 udp 2122260223 10.48.51.202 35664 typ host generation 0")
242
-
243
- val pinned = SoftApSdpGuard.pinHostAddresses(original, "192.168.43.79", prefix)
244
-
245
- assertThat(pinned.rewritten).containsExactly("10.48.51.202")
246
- assertThat(pinned.sdp).contains("192.168.43.79 35664 typ host")
247
- assertThat(pinned.sdp).doesNotContain("10.48.51.202")
248
- assertThat(SoftApSdpGuard.inspect(pinned.sdp, prefix)).isInstanceOf(SoftApSdpGuard.Verdict.Ok::class.java)
249
- }
250
-
251
- @Test
252
- fun `a host address already on the hotspot is left alone`() {
253
- val prefix = Ipv4Prefix("192.168.43.79", 24)
254
- val original = sdp(hostCandidate)
255
-
256
- val pinned = SoftApSdpGuard.pinHostAddresses(original, "192.168.43.79", prefix)
257
-
258
- assertThat(pinned.rewritten).isEmpty()
259
- assertThat(pinned.sdp).isEqualTo(original)
260
- }
261
-
262
- @Test
263
- fun `a reflexive raddr is not treated as the connection address to pin`() {
264
- // Field 5 is 203.0.113.7; raddr is already the hotspot. Rewriting raddr would hide a srflx
265
- // that ICE could still select, and would not fix the address the glasses send to.
266
- val prefix = Ipv4Prefix("192.168.43.79", 24)
267
- val original = sdp(srflxCandidate)
268
-
269
- val pinned = SoftApSdpGuard.pinHostAddresses(original, "192.168.43.79", prefix)
270
-
271
- assertThat(pinned.rewritten).isEmpty()
272
- assertThat(pinned.sdp).contains("203.0.113.7")
273
- assertThat(pinned.sdp).contains("raddr 192.168.43.20")
274
- }
275
- }
@@ -1,193 +0,0 @@
1
- package com.mentra.acsmeeting.source
2
-
3
- import com.mentra.acsmeeting.telemetry.ChromaProbe
4
- import com.mentra.acsmeeting.video.I420Packer
5
- import org.assertj.core.api.Assertions.assertThat
6
- import org.junit.Test
7
- import java.nio.ByteBuffer
8
-
9
- class SyntheticFrameFactoryTest {
10
- @Test
11
- fun packedLengthMatchesI420() {
12
- val dest = buffer(WIDTH, HEIGHT)
13
- SyntheticFrameFactory().write(dest, WIDTH, HEIGHT, 0)
14
- assertThat(dest.remaining()).isEqualTo(I420Packer.packedSize(WIDTH, HEIGHT))
15
- }
16
-
17
- /**
18
- * Regression for the 1.4 fps wire collapse: a noisy CHEAP background is
19
- * incompressible and starves the ACS encoder. CHEAP must stay flat.
20
- */
21
- @Test
22
- fun cheapBackgroundStaysCompressible() {
23
- val dest = buffer(WIDTH, HEIGHT)
24
- SyntheticFrameFactory(SyntheticEntropy.CHEAP).write(dest, WIDTH, HEIGHT, 0)
25
- val y = yBytes(dest)
26
- var background = 0
27
- for (row in HEIGHT - 40 until HEIGHT) {
28
- val rowOff = row * WIDTH
29
- for (col in 0 until WIDTH) {
30
- if (y[rowOff + col] == SyntheticFrameFactory.BG_Y) background += 1
31
- }
32
- }
33
- assertThat(background).isGreaterThan(30 * WIDTH)
34
- }
35
-
36
- @Test
37
- fun consecutiveCheapFramesDifferInY() {
38
- val a = buffer(WIDTH, HEIGHT)
39
- val b = buffer(WIDTH, HEIGHT)
40
- val factory = SyntheticFrameFactory(SyntheticEntropy.CHEAP)
41
- factory.write(a, WIDTH, HEIGHT, 0)
42
- factory.write(b, WIDTH, HEIGHT, 1)
43
- assertThat(yBytes(a)).isNotEqualTo(yBytes(b))
44
- }
45
-
46
- @Test
47
- fun cheapIsDeterministicForAGivenIndex() {
48
- val a = buffer(WIDTH, HEIGHT)
49
- val b = buffer(WIDTH, HEIGHT)
50
- SyntheticFrameFactory(SyntheticEntropy.CHEAP).write(a, WIDTH, HEIGHT, 7)
51
- SyntheticFrameFactory(SyntheticEntropy.CHEAP).write(b, WIDTH, HEIGHT, 7)
52
- assertThat(copy(a)).isEqualTo(copy(b))
53
- }
54
-
55
- @Test
56
- fun incrementalCheapMatchesFreshWrite() {
57
- val stepped = buffer(WIDTH, HEIGHT)
58
- val fresh = buffer(WIDTH, HEIGHT)
59
- val factory = SyntheticFrameFactory(SyntheticEntropy.CHEAP)
60
- factory.write(stepped, WIDTH, HEIGHT, 0)
61
- factory.write(stepped, WIDTH, HEIGHT, 1)
62
- SyntheticFrameFactory(SyntheticEntropy.CHEAP).write(fresh, WIDTH, HEIGHT, 1)
63
- assertThat(copy(stepped)).isEqualTo(copy(fresh))
64
- }
65
-
66
- @Test
67
- fun chromaAveragesLandAt128() {
68
- for (entropy in SyntheticEntropy.entries) {
69
- val dest = buffer(WIDTH, HEIGHT)
70
- SyntheticFrameFactory(entropy).write(dest, WIDTH, HEIGHT, 3)
71
- val sample = ChromaProbe.samplePacked(dest, WIDTH, HEIGHT)
72
- assertThat(sample.u).isEqualTo(128)
73
- assertThat(sample.v).isEqualTo(128)
74
- }
75
- }
76
-
77
- @Test
78
- fun motionPansAlmostEveryPixel() {
79
- val a = buffer(WIDTH, HEIGHT)
80
- val b = buffer(WIDTH, HEIGHT)
81
- val factory = SyntheticFrameFactory(SyntheticEntropy.MOTION)
82
- factory.write(a, WIDTH, HEIGHT, 0)
83
- factory.write(b, WIDTH, HEIGHT, 1)
84
- val ya = yBytes(a)
85
- val yb = yBytes(b)
86
- var differ = 0
87
- for (i in ya.indices) if (ya[i] != yb[i]) differ += 1
88
- assertThat(differ).isGreaterThan(ya.size / 2)
89
- }
90
-
91
- /**
92
- * Glasses motion still has neighbors that look alike. A 32×32 patch in the
93
- * corner must stay a gradient, not snow — that is what lets an I-frame fit
94
- * the bitrate budget.
95
- */
96
- @Test
97
- fun motionKeepsSpatialStructure() {
98
- val dest = buffer(WIDTH, HEIGHT)
99
- SyntheticFrameFactory(SyntheticEntropy.MOTION).write(dest, WIDTH, HEIGHT, 0)
100
- val y = yBytes(dest)
101
- val unique = mutableSetOf<Byte>()
102
- for (row in HEIGHT - 80 until HEIGHT - 48) {
103
- val rowOff = row * WIDTH
104
- for (col in 0 until 32) unique.add(y[rowOff + col])
105
- }
106
- assertThat(unique.size).isLessThan(40)
107
- }
108
-
109
- @Test
110
- fun motionIsDeterministicForAGivenIndex() {
111
- val a = buffer(WIDTH, HEIGHT)
112
- val b = buffer(WIDTH, HEIGHT)
113
- SyntheticFrameFactory(SyntheticEntropy.MOTION).write(a, WIDTH, HEIGHT, 11)
114
- SyntheticFrameFactory(SyntheticEntropy.MOTION).write(b, WIDTH, HEIGHT, 11)
115
- assertThat(copy(a)).isEqualTo(copy(b))
116
- }
117
-
118
- @Test
119
- fun noiseRewritesTheFullYPlane() {
120
- val a = buffer(WIDTH, HEIGHT)
121
- val b = buffer(WIDTH, HEIGHT)
122
- val factory = SyntheticFrameFactory(SyntheticEntropy.NOISE)
123
- factory.write(a, WIDTH, HEIGHT, 0)
124
- factory.write(b, WIDTH, HEIGHT, 1)
125
- val ya = yBytes(a)
126
- val yb = yBytes(b)
127
- var differ = 0
128
- for (i in ya.indices) if (ya[i] != yb[i]) differ += 1
129
- assertThat(differ).isGreaterThan(ya.size / 2)
130
- }
131
-
132
- @Test
133
- fun stampPaintsFrameIdAndFpsOnADarkBox() {
134
- val dest = buffer(WIDTH, HEIGHT)
135
- SyntheticFrameFactory(SyntheticEntropy.CHEAP).write(dest, WIDTH, HEIGHT, 42, 15.0)
136
- val y = yBytes(dest)
137
- val boxX = WIDTH / 16
138
- val boxY = HEIGHT / 2 - 80
139
- var dark = 0
140
- var bright = 0
141
- for (row in boxY until boxY + 160) {
142
- val rowOff = row * WIDTH
143
- for (col in boxX until boxX + 400) {
144
- val value = y[rowOff + col].toInt() and 0xFF
145
- if (value <= 20) dark += 1
146
- if (value >= 220) bright += 1
147
- }
148
- }
149
- assertThat(dark).isGreaterThan(1_000)
150
- assertThat(bright).isGreaterThan(200)
151
- }
152
-
153
- @Test
154
- fun stampFrameIndexChangesTheOverlay() {
155
- val a = buffer(WIDTH, HEIGHT)
156
- val b = buffer(WIDTH, HEIGHT)
157
- SyntheticFrameFactory(SyntheticEntropy.CHEAP).write(a, WIDTH, HEIGHT, 1, 15.0)
158
- SyntheticFrameFactory(SyntheticEntropy.CHEAP).write(b, WIDTH, HEIGHT, 2, 15.0)
159
- assertThat(copy(a)).isNotEqualTo(copy(b))
160
- }
161
-
162
- @Test
163
- fun noiseIsDeterministicForAGivenIndex() {
164
- val a = buffer(WIDTH, HEIGHT)
165
- val b = buffer(WIDTH, HEIGHT)
166
- SyntheticFrameFactory(SyntheticEntropy.NOISE).write(a, WIDTH, HEIGHT, 11)
167
- SyntheticFrameFactory(SyntheticEntropy.NOISE).write(b, WIDTH, HEIGHT, 11)
168
- assertThat(copy(a)).isEqualTo(copy(b))
169
- }
170
-
171
- private fun buffer(width: Int, height: Int): ByteBuffer =
172
- ByteBuffer.allocate(I420Packer.packedSize(width, height))
173
-
174
- private fun yBytes(src: ByteBuffer): ByteArray {
175
- val view = src.duplicate()
176
- view.position(0)
177
- val y = ByteArray(WIDTH * HEIGHT)
178
- view.get(y)
179
- return y
180
- }
181
-
182
- private fun copy(src: ByteBuffer): ByteArray {
183
- val view = src.duplicate()
184
- val out = ByteArray(view.remaining())
185
- view.get(out)
186
- return out
187
- }
188
-
189
- companion object {
190
- private const val WIDTH = 1280
191
- private const val HEIGHT = 720
192
- }
193
- }
@@ -1,176 +0,0 @@
1
- package com.mentra.acsmeeting.source
2
-
3
- import com.mentra.acsmeeting.telemetry.PipelineStats
4
- import com.mentra.acsmeeting.video.I420Packer
5
- import org.assertj.core.api.Assertions.assertThat
6
- import org.junit.Test
7
- import java.util.concurrent.atomic.AtomicBoolean
8
-
9
- class SyntheticI420SourceTest {
10
- @Test
11
- fun withheldWhileNotReadyThenEmitsOnceReady() {
12
- val ready = AtomicBoolean(false)
13
- val frames = ArrayList<Triple<Int, Int, Int>>()
14
- val stats = PipelineStats()
15
- val source = source(stats, ready) { planes ->
16
- frames.add(Triple(I420Planes.remaining(planes.y) + I420Planes.remaining(planes.u) + I420Planes.remaining(planes.v), planes.width, planes.height))
17
- }
18
- source.becomeLive()
19
- source.setTargetSize(TargetSize(1280, 720))
20
-
21
- assertThat(source.emitOnce()).isFalse()
22
- assertThat(frames).isEmpty()
23
- assertThat(stats.sinkCount()).isEqualTo(0)
24
-
25
- ready.set(true)
26
- assertThat(source.emitOnce()).isTrue()
27
- assertThat(frames).hasSize(1)
28
- assertThat(frames[0].second).isEqualTo(1280)
29
- assertThat(frames[0].third).isEqualTo(720)
30
- assertThat(frames[0].first).isEqualTo(I420Packer.packedSize(1280, 720))
31
- assertThat(stats.sinkCount()).isEqualTo(1)
32
- assertThat(source.events.any { it.startsWith("P6 synthetic first-frame") }).isTrue()
33
- }
34
-
35
- @Test
36
- fun withheldWhileTargetSizeIsNull() {
37
- val frames = ArrayList<Int>()
38
- val stats = PipelineStats()
39
- val source = source(stats, AtomicBoolean(true)) { _ -> frames.add(1) }
40
- source.becomeLive()
41
-
42
- assertThat(source.emitOnce()).isFalse()
43
- assertThat(frames).isEmpty()
44
- assertThat(stats.sinkCount()).isEqualTo(0)
45
-
46
- source.setTargetSize(TargetSize(640, 360))
47
- assertThat(source.emitOnce()).isTrue()
48
- assertThat(frames).hasSize(1)
49
- }
50
-
51
- @Test
52
- fun nReadyCallsDeliverNFramesAndHonorResize() {
53
- val frames = ArrayList<Pair<Int, Int>>()
54
- val stats = PipelineStats()
55
- val source = source(stats, AtomicBoolean(true)) { planes -> frames.add(planes.width to planes.height) }
56
- source.becomeLive()
57
- source.setTargetSize(TargetSize(1280, 720))
58
- repeat(4) { source.emitOnce() }
59
- source.setTargetSize(TargetSize(640, 360))
60
- source.emitOnce()
61
-
62
- assertThat(frames).containsExactly(
63
- 1280 to 720,
64
- 1280 to 720,
65
- 1280 to 720,
66
- 1280 to 720,
67
- 640 to 360,
68
- )
69
- assertThat(stats.sinkCount()).isEqualTo(5)
70
- }
71
-
72
- @Test
73
- fun withheldTicksTouchNoCounters() {
74
- val stats = PipelineStats()
75
- val source = source(stats, AtomicBoolean(false)) { _ -> }
76
- source.becomeLive()
77
- source.setTargetSize(TargetSize(1280, 720))
78
- repeat(5) { source.emitOnce() }
79
- assertThat(stats.sinkCount()).isEqualTo(0)
80
- assertThat(stats.dropCount()).isEqualTo(0)
81
- assertThat(stats.subCount()).isEqualTo(0)
82
- assertThat(stats.pack.p95()).isEqualTo("na")
83
- }
84
-
85
- @Test
86
- fun readinessFlapProducesOnePauseAndOneResume() {
87
- val ready = AtomicBoolean(true)
88
- val source = source(PipelineStats(), ready) { _ -> }
89
- source.becomeLive()
90
- source.setTargetSize(TargetSize(1280, 720))
91
- source.emitOnce()
92
- ready.set(false)
93
- source.emitOnce()
94
- ready.set(true)
95
- source.emitOnce()
96
-
97
- val pauses = source.events.filter { it.contains("paused") }
98
- val resumes = source.events.filter { it.contains("resumed") }
99
- assertThat(pauses).hasSize(1)
100
- assertThat(resumes).hasSize(1)
101
- }
102
-
103
- @Test
104
- fun stopEmitsNothingAndRestartResetsIndex() {
105
- val indexes = ArrayList<Int>()
106
- val ready = AtomicBoolean(true)
107
- val source = source(PipelineStats(), ready) { planes ->
108
- indexes.add(yCorner(planes))
109
- }
110
- source.becomeLive()
111
- source.setTargetSize(TargetSize(64, 64))
112
- source.emitOnce()
113
- source.emitOnce()
114
- source.stop()
115
- assertThat(source.state).isEqualTo(SourceState.IDLE)
116
- assertThat(source.emitOnce()).isFalse()
117
-
118
- source.becomeLive()
119
- source.setTargetSize(TargetSize(64, 64))
120
- source.emitOnce()
121
- assertThat(indexes).hasSize(3)
122
- assertThat(indexes[0]).isEqualTo(indexes[2])
123
- }
124
-
125
- @Test
126
- fun restartLeavesSourceLive() {
127
- val source = source(PipelineStats(), AtomicBoolean(false)) { _ -> }
128
- source.start(SourceConfig("ignored", SourceKind.DIRECT))
129
- source.restart(SourceConfig("ignored-2", SourceKind.DIRECT))
130
- assertThat(source.state).isEqualTo(SourceState.LIVE)
131
- source.stop()
132
- assertThat(source.state).isEqualTo(SourceState.IDLE)
133
- }
134
-
135
- @Test
136
- fun pcmListenerIsNeverInvoked() {
137
- val source = SyntheticI420Source(
138
- video = { _ -> },
139
- stats = PipelineStats(),
140
- isReady = { true },
141
- )
142
- source.setPcmDeliveryEnabled(true)
143
- assertThat(source.pcmDeliveryEnabled()).isTrue()
144
- source.setPcmDeliveryEnabled(false)
145
- assertThat(source.pcmDeliveryEnabled()).isFalse()
146
- }
147
-
148
- private fun source(
149
- stats: PipelineStats,
150
- ready: AtomicBoolean,
151
- video: VideoFrameListener,
152
- ): SyntheticI420Source = SyntheticI420Source(
153
- video = video,
154
- stats = stats,
155
- isReady = { ready.get() },
156
- )
157
-
158
- @Test
159
- fun emitsTightStridePlanesAliasingThePackedBuffer() {
160
- val seen = ArrayList<I420Planes>()
161
- val source = source(PipelineStats(), AtomicBoolean(true)) { seen.add(it) }
162
- source.becomeLive()
163
- source.setTargetSize(TargetSize(64, 64))
164
- assertThat(source.emitOnce()).isTrue()
165
- val planes = seen.single()
166
- assertThat(planes.isTight()).isTrue()
167
- assertThat(planes.retain == null).isTrue()
168
- assertThat(planes.release == null).isTrue()
169
- assertThat(I420Planes.remaining(planes.y)).isEqualTo(64 * 64)
170
- assertThat(I420Planes.remaining(planes.u)).isEqualTo(32 * 32)
171
- }
172
-
173
- private fun yCorner(planes: I420Planes): Int {
174
- return planes.y.duplicate().get(0).toInt() and 0xFF
175
- }
176
- }
@@ -1,45 +0,0 @@
1
- package com.mentra.acsmeeting.source
2
-
3
- import org.assertj.core.api.Assertions.assertThat
4
- import org.junit.Test
5
-
6
- class TrackRegistryTest {
7
- @Test
8
- fun threeClaimsOfOneIdYieldTrueFalseFalse() {
9
- val registry = TrackRegistry()
10
- assertThat(registry.claim("track-a")).isTrue()
11
- assertThat(registry.claim("track-a")).isFalse()
12
- assertThat(registry.claim("track-a")).isFalse()
13
- assertThat(registry.size()).isEqualTo(1)
14
- assertThat(registry.skipped()).isEqualTo(2)
15
- }
16
-
17
- @Test
18
- fun distinctIdsEachClaimOnce() {
19
- val registry = TrackRegistry()
20
- assertThat(registry.claim("video")).isTrue()
21
- assertThat(registry.claim("audio")).isTrue()
22
- assertThat(registry.size()).isEqualTo(2)
23
- assertThat(registry.skipped()).isEqualTo(0)
24
- }
25
-
26
- @Test
27
- fun resetMakesTheSameIdClaimableAgain() {
28
- val registry = TrackRegistry()
29
- assertThat(registry.claim("track-a")).isTrue()
30
- registry.reset()
31
- assertThat(registry.size()).isEqualTo(0)
32
- assertThat(registry.skipped()).isEqualTo(0)
33
- assertThat(registry.claim("track-a")).isTrue()
34
- assertThat(registry.size()).isEqualTo(1)
35
- }
36
-
37
- @Test
38
- fun softapWhipReconnectCanReclaimVideo0() {
39
- val registry = TrackRegistry()
40
- assertThat(registry.claim("video0")).isTrue()
41
- assertThat(registry.claim("video0")).isFalse()
42
- registry.reset()
43
- assertThat(registry.claim("video0")).isTrue()
44
- }
45
- }