@mentra/acs-meeting 3.2.0-dev.120

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 (102) hide show
  1. package/NOTICE +20 -0
  2. package/README.md +412 -0
  3. package/THIRD_PARTY +7 -0
  4. package/android/build.gradle +64 -0
  5. package/android/src/main/AndroidManifest.xml +5 -0
  6. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +80 -0
  7. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +846 -0
  8. package/android/src/main/java/com/mentra/acsmeeting/RemoteRoster.kt +123 -0
  9. package/android/src/main/java/com/mentra/acsmeeting/audio/AcsAudioPolicy.kt +90 -0
  10. package/android/src/main/java/com/mentra/acsmeeting/audio/AudioPolicyApplier.kt +176 -0
  11. package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingAudioPump.kt +112 -0
  12. package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingRateProbe.kt +65 -0
  13. package/android/src/main/java/com/mentra/acsmeeting/audio/PcmBridge.kt +108 -0
  14. package/android/src/main/java/com/mentra/acsmeeting/audio/PcmResampler.kt +159 -0
  15. package/android/src/main/java/com/mentra/acsmeeting/audio/PhoneMicCapturer.kt +115 -0
  16. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkPacer.kt +302 -0
  17. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +230 -0
  18. package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +99 -0
  19. package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +578 -0
  20. package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +45 -0
  21. package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +101 -0
  22. package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +57 -0
  23. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +205 -0
  24. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +214 -0
  25. package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +31 -0
  26. package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +41 -0
  27. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +78 -0
  28. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +281 -0
  29. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +51 -0
  30. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +15 -0
  31. package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +33 -0
  32. package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +475 -0
  33. package/android/src/main/java/com/mentra/acsmeeting/video/AcsTimestamp.kt +26 -0
  34. package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +22 -0
  35. package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +90 -0
  36. package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +72 -0
  37. package/android/src/main/java/com/mentra/acsmeeting/video/Nv12FormatSpec.kt +36 -0
  38. package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +51 -0
  39. package/android/src/main/java/com/mentra/acsmeeting/video/SendGate.kt +27 -0
  40. package/android/src/main/java/com/mentra/acsmeeting/video/VideoProfile.kt +57 -0
  41. package/android/src/test/java/com/mentra/acsmeeting/RemoteRosterTest.kt +40 -0
  42. package/android/src/test/java/com/mentra/acsmeeting/audio/AcsAudioPolicyTest.kt +204 -0
  43. package/android/src/test/java/com/mentra/acsmeeting/audio/AudioPolicyApplierTest.kt +223 -0
  44. package/android/src/test/java/com/mentra/acsmeeting/audio/CapturePolicyTest.kt +12 -0
  45. package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingAudioPumpTest.kt +97 -0
  46. package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingRateProbeTest.kt +78 -0
  47. package/android/src/test/java/com/mentra/acsmeeting/audio/PcmResamplerTest.kt +125 -0
  48. package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkPacerTest.kt +201 -0
  49. package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkSenderTest.kt +166 -0
  50. package/android/src/test/java/com/mentra/acsmeeting/source/AcsInvestigationTest.kt +26 -0
  51. package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +22 -0
  52. package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +60 -0
  53. package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +133 -0
  54. package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +67 -0
  55. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +193 -0
  56. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +176 -0
  57. package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +36 -0
  58. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +61 -0
  59. package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +256 -0
  60. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +18 -0
  61. package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +49 -0
  62. package/android/src/test/java/com/mentra/acsmeeting/video/AcsTimestampTest.kt +43 -0
  63. package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +33 -0
  64. package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +77 -0
  65. package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +126 -0
  66. package/android/src/test/java/com/mentra/acsmeeting/video/Nv12FormatSpecTest.kt +36 -0
  67. package/android/src/test/java/com/mentra/acsmeeting/video/Nv12PackerTest.kt +58 -0
  68. package/android/src/test/java/com/mentra/acsmeeting/video/SendGateTest.kt +79 -0
  69. package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +68 -0
  70. package/build/AcsMeeting.types.d.ts +59 -0
  71. package/build/AcsMeeting.types.d.ts.map +1 -0
  72. package/build/AcsMeeting.types.js +2 -0
  73. package/build/AcsMeeting.types.js.map +1 -0
  74. package/build/AcsMeetingModule.d.ts +15 -0
  75. package/build/AcsMeetingModule.d.ts.map +1 -0
  76. package/build/AcsMeetingModule.js +3 -0
  77. package/build/AcsMeetingModule.js.map +1 -0
  78. package/build/AcsMeetingModule.web.d.ts +16 -0
  79. package/build/AcsMeetingModule.web.d.ts.map +1 -0
  80. package/build/AcsMeetingModule.web.js +31 -0
  81. package/build/AcsMeetingModule.web.js.map +1 -0
  82. package/build/index.d.ts +3 -0
  83. package/build/index.d.ts.map +1 -0
  84. package/build/index.js +3 -0
  85. package/build/index.js.map +1 -0
  86. package/expo-module.config.json +9 -0
  87. package/ios/AcsFrameSender.swift +47 -0
  88. package/ios/AcsMeeting.podspec +27 -0
  89. package/ios/AcsMeetingModule.swift +558 -0
  90. package/ios/PcmBridge.swift +117 -0
  91. package/ios/PhoneMicCapturer.swift +64 -0
  92. package/ios/PolicyKit/Package.swift +17 -0
  93. package/ios/PolicyKit/Sources/AcsAudioPolicy/AcsAudioPolicy.swift +102 -0
  94. package/ios/PolicyKit/Sources/AcsAudioPolicy/AudioPolicyApplier.swift +162 -0
  95. package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +68 -0
  96. package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +273 -0
  97. package/ios/WhepVideoSource.swift +356 -0
  98. package/package.json +56 -0
  99. package/src/AcsMeeting.types.ts +66 -0
  100. package/src/AcsMeetingModule.ts +16 -0
  101. package/src/AcsMeetingModule.web.ts +33 -0
  102. package/src/index.ts +2 -0
@@ -0,0 +1,61 @@
1
+ package com.mentra.acsmeeting.telemetry
2
+
3
+ import com.mentra.acsmeeting.video.I420Packer
4
+ import org.assertj.core.api.Assertions.assertThat
5
+ import org.junit.Test
6
+ import java.nio.ByteBuffer
7
+
8
+ class ChromaProbeTest {
9
+ @Test
10
+ fun packedNeutralAveragesNearMidChroma() {
11
+ val width = 8
12
+ val height = 8
13
+ val packed = ByteBuffer.allocate(I420Packer.packedSize(width, height))
14
+ val ySize = width * height
15
+ val uvSize = I420Packer.chromaStride(width) * I420Packer.chromaStride(height)
16
+ repeat(ySize) { packed.put(100.toByte()) }
17
+ repeat(uvSize) { packed.put(128.toByte()) }
18
+ repeat(uvSize) { packed.put(128.toByte()) }
19
+ packed.flip()
20
+ val sample = ChromaProbe.samplePacked(packed, width, height)
21
+ assertThat(sample.y).isEqualTo(100)
22
+ assertThat(sample.u).isEqualTo(128)
23
+ assertThat(sample.v).isEqualTo(128)
24
+ }
25
+
26
+ @Test
27
+ fun samplePlanesAgreesWithSamplePacked() {
28
+ val width = 8
29
+ val height = 8
30
+ val packed = ByteBuffer.allocate(I420Packer.packedSize(width, height))
31
+ val ySize = width * height
32
+ val uvSize = I420Packer.chromaStride(width) * I420Packer.chromaStride(height)
33
+ repeat(ySize) { packed.put(90.toByte()) }
34
+ repeat(uvSize) { packed.put(130.toByte()) }
35
+ repeat(uvSize) { packed.put(126.toByte()) }
36
+ packed.flip()
37
+ val packedSample = ChromaProbe.samplePacked(packed, width, height)
38
+ val y = packed.duplicate().apply { position(0); limit(ySize) }.slice()
39
+ val u = packed.duplicate().apply { position(ySize); limit(ySize + uvSize) }.slice()
40
+ val v = packed.duplicate().apply { position(ySize + uvSize); limit(ySize + 2 * uvSize) }.slice()
41
+ val planeSample = ChromaProbe.samplePlanes(y, u, v)
42
+ assertThat(planeSample).isEqualTo(packedSample)
43
+ }
44
+
45
+ @Test
46
+ fun sampleNv12SeparatesInterleavedChroma() {
47
+ val y = ByteBuffer.allocate(8)
48
+ repeat(8) { y.put(100.toByte()) }
49
+ y.flip()
50
+ val uv = ByteBuffer.allocate(8)
51
+ for (i in 0 until 4) {
52
+ uv.put(130.toByte())
53
+ uv.put(126.toByte())
54
+ }
55
+ uv.flip()
56
+ val sample = ChromaProbe.sampleNv12(y, uv)
57
+ assertThat(sample.y).isEqualTo(100)
58
+ assertThat(sample.u).isEqualTo(130)
59
+ assertThat(sample.v).isEqualTo(126)
60
+ }
61
+ }
@@ -0,0 +1,256 @@
1
+ package com.mentra.acsmeeting.telemetry
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+
6
+ class PipelineStatsTest {
7
+ @Test
8
+ fun conservationHoldsAcrossEveryDisposition() {
9
+ val stats = PipelineStats()
10
+ repeat(4) { stats.onSink() }
11
+ stats.onQueued(); stats.onSub()
12
+ stats.onDropSize()
13
+ stats.onDropBusy()
14
+ stats.onDropNullI420()
15
+ assertThat(stats.conserved()).isTrue()
16
+ assertThat(stats.sinkCount()).isEqualTo(4)
17
+ assertThat(stats.subCount()).isEqualTo(1)
18
+ assertThat(stats.dropCount()).isEqualTo(3)
19
+ assertThat(stats.inFlightCount()).isEqualTo(0)
20
+ }
21
+
22
+ @Test
23
+ fun latencyStraddleIsOneHundredPercentOverTenTicks() {
24
+ var clock = 0L
25
+ val stats = PipelineStats { clock }
26
+ val ticks = ArrayList<String>()
27
+ stats.onSink()
28
+ stats.onQueued()
29
+ clock += 1000
30
+ ticks.add(stats.tick())
31
+ stats.onSub()
32
+ repeat(9) {
33
+ clock += 1000
34
+ ticks.add(stats.tick())
35
+ }
36
+ assertThat(stats.subCount()).isEqualTo(stats.sinkCount())
37
+ assertThat(stats.conserved()).isTrue()
38
+ val last = ticks.last()
39
+ assertThat(last).contains("cum{sink=1 sub=1 drop=0 inFlight=0}")
40
+ assertThat(last).doesNotContain("CONSERVE_FAIL")
41
+ }
42
+
43
+ @Test
44
+ fun cumulativeCountersNeverDecrease() {
45
+ var clock = 0L
46
+ val stats = PipelineStats { clock }
47
+ stats.onSink(); stats.onQueued(); stats.onSub()
48
+ clock += 1000
49
+ val first = stats.tick()
50
+ clock += 1000
51
+ val idle = stats.tick()
52
+ assertThat(extractCum(first)).isEqualTo(extractCum(idle))
53
+ assertThat(stats.sinkCount()).isEqualTo(1)
54
+ assertThat(stats.subCount()).isEqualTo(1)
55
+ }
56
+
57
+ @Test
58
+ fun idleTickStillEmitsSinkZero() {
59
+ var clock = 0L
60
+ val stats = PipelineStats { clock }
61
+ clock += 1000
62
+ val line = stats.tick()
63
+ assertThat(line).startsWith("P6 ladder")
64
+ assertThat(line).contains("sink=0.0")
65
+ assertThat(line).contains("dec=na")
66
+ assertThat(line).contains("wire=0x0@na kbps=na codec=na")
67
+ assertThat(line).contains("drop{")
68
+ assertThat(line).contains("ms{")
69
+ assertThat(line).contains("chroma{")
70
+ assertThat(line).contains("cum{sink=0 sub=0 drop=0 inFlight=0}")
71
+ }
72
+
73
+ @Test
74
+ fun queuedFrameCountsAsInFlightUntilSettled() {
75
+ val stats = PipelineStats()
76
+ stats.onSink()
77
+ stats.onQueued()
78
+ assertThat(stats.conserved()).isTrue()
79
+ assertThat(stats.inFlightCount()).isEqualTo(1)
80
+ stats.onSub()
81
+ assertThat(stats.inFlightCount()).isEqualTo(0)
82
+ assertThat(stats.conserved()).isTrue()
83
+ }
84
+
85
+ /**
86
+ * The device logged 14 CONSERVE_FAIL lines on a pipeline with zero drops.
87
+ * Cause was a tracked inFlight counter read mid-settle, not lost frames.
88
+ */
89
+ @Test
90
+ fun concurrentSendsNeverReportConserveFail() {
91
+ val stats = PipelineStats { 0L }
92
+ val sender = Thread {
93
+ repeat(20_000) {
94
+ stats.onSink()
95
+ stats.onQueued()
96
+ stats.onSub()
97
+ }
98
+ }
99
+ val failures = ArrayList<String>()
100
+ val reader = Thread {
101
+ repeat(20_000) {
102
+ if (!stats.conserved()) failures.add("inFlight=${stats.inFlightCount()}")
103
+ }
104
+ }
105
+ sender.start(); reader.start()
106
+ sender.join(); reader.join()
107
+ assertThat(failures).isEmpty()
108
+ assertThat(stats.inFlightCount()).isEqualTo(0)
109
+ assertThat(stats.tick()).doesNotContain("CONSERVE_FAIL")
110
+ }
111
+
112
+ @Test
113
+ fun recvHealthReportsDeltasNotAbsolutes() {
114
+ val stats = PipelineStats { 0L }
115
+ assertThat(stats.recvLabel()).isEqualTo("recv{na}")
116
+ stats.setRecvHealth(
117
+ PipelineStats.RecvHealth(assembled = 100, dropped = 5, packetsLost = 40, nack = 7, pli = 2, freezes = 1),
118
+ )
119
+ assertThat(stats.recvLabel()).contains("drop=5", "lost=40", "nack=7", "pli=2", "freeze=1")
120
+ stats.setRecvHealth(
121
+ PipelineStats.RecvHealth(assembled = 115, dropped = 5, packetsLost = 52, nack = 9, pli = 2, freezes = 3),
122
+ )
123
+ assertThat(stats.recvLabel()).contains("drop=0", "lost=12", "nack=2", "pli=0", "freeze=2")
124
+ }
125
+
126
+ @Test
127
+ fun recvHealthAveragesPerFrameTimings() {
128
+ val stats = PipelineStats { 0L }
129
+ stats.setRecvHealth(
130
+ PipelineStats.RecvHealth(
131
+ assembled = 200,
132
+ decodeSec = 0.8,
133
+ jitterBufferSec = 12.0,
134
+ jitterBufferEmits = 200,
135
+ jitter = 0.015,
136
+ ),
137
+ )
138
+ val label = stats.recvLabel()
139
+ assertThat(label).contains("decMs=4.0")
140
+ assertThat(label).contains("jbMs=60.0")
141
+ assertThat(label).contains("jit=15.0")
142
+ }
143
+
144
+ @Test
145
+ fun recvHealthToleratesMissingMembers() {
146
+ val stats = PipelineStats { 0L }
147
+ stats.setRecvHealth(PipelineStats.RecvHealth())
148
+ val label = stats.recvLabel()
149
+ assertThat(label).contains("drop=na", "lost=na", "decMs=na", "jbMs=na", "decImpl=na")
150
+ assertThat(stats.tick()).contains("recv{")
151
+ }
152
+
153
+ @Test
154
+ fun recvHealthPrintsDecoderImplementation() {
155
+ val stats = PipelineStats { 0L }
156
+ stats.setRecvHealth(PipelineStats.RecvHealth(decImpl = "OMX.qcom.video.decoder.avc"))
157
+ assertThat(stats.recvLabel()).contains("decImpl=OMX.qcom.video.decoder.avc")
158
+ }
159
+
160
+ @Test
161
+ fun ladderIncludesArmLabel() {
162
+ var clock = 0L
163
+ val stats = PipelineStats { clock }
164
+ clock += 1000
165
+ assertThat(stats.tick()).contains("arm=whep")
166
+ stats.arm = "synthetic"
167
+ clock += 1000
168
+ assertThat(stats.tick()).contains("arm=synthetic")
169
+ }
170
+
171
+ @Test
172
+ fun ladderExposesCpuAttributionFields() {
173
+ var clock = 0L
174
+ val stats = PipelineStats { clock }
175
+ stats.toI420.record(4_000_000)
176
+ stats.sinkCb.record(8_000_000)
177
+ stats.split.record(1_000_000)
178
+ stats.onDestAlloc()
179
+ stats.onPlaneAlloc()
180
+ stats.onPlaneAlloc()
181
+ clock += 1000
182
+ val line = stats.tick()
183
+ assertThat(line).contains("i420P95=4.0")
184
+ assertThat(line).contains("sinkCbP95=8.0")
185
+ assertThat(line).contains("splitP95=1.0")
186
+ assertThat(line).contains("alloc{dest=1 plane=2}")
187
+ }
188
+
189
+ @Test
190
+ fun ladderCarriesPathBufStrideZcAndCopyP95Na() {
191
+ var clock = 0L
192
+ val stats = PipelineStats { clock }
193
+ clock += 1000
194
+ val idle = stats.tick()
195
+ assertThat(idle).contains("path{mode=texture copy=packsplit pix=i420}")
196
+ assertThat(idle).contains("wire=0x0@na kbps=na codec=na")
197
+ assertThat(idle).contains("buf{tex=0 i420=0 other=0}")
198
+ assertThat(idle).contains("stride{y=0 u=0 v=0 tight=0 padded=0}")
199
+ assertThat(idle).contains("zc{on=0 used=0 fell=0 padded=0 heldMax=0 timeout=0}")
200
+ assertThat(idle).contains("copyP95=na")
201
+
202
+ stats.pathMode = "bytebuf"
203
+ stats.pathCopy = "nv12"
204
+ stats.pix = "nv12"
205
+ stats.codecName = "h264 sw"
206
+ stats.onFrameBuffer("tex")
207
+ stats.onFrameBuffer("i420")
208
+ stats.onFrameBuffer("i420")
209
+ stats.onFrameBuffer("nv12")
210
+ stats.onStrides(1280, 640, 640, 1280)
211
+ stats.onStrides(1280, 768, 768, 1280)
212
+ stats.zcOn = 1
213
+ stats.onZcUsed(1)
214
+ stats.onZcUsed(2)
215
+ stats.onZcFell()
216
+ stats.onZcPadded()
217
+ stats.onZcTimeout()
218
+ stats.copy.record(3_000_000)
219
+ clock += 1000
220
+ val line = stats.tick()
221
+ assertThat(line).contains("path{mode=bytebuf copy=nv12 pix=nv12}")
222
+ assertThat(line).contains("codec=h264_sw")
223
+ assertThat(line).contains("buf{tex=1 i420=2 other=1}")
224
+ assertThat(line).contains("stride{y=1280 u=768 v=768 tight=1 padded=1}")
225
+ assertThat(line).contains("zc{on=1 used=2 fell=1 padded=1 heldMax=2 timeout=1}")
226
+ assertThat(line).contains("copyP95=3.0")
227
+ }
228
+
229
+ @Test
230
+ fun ladderPrintsAcsWireGeometryAndBitrate() {
231
+ var clock = 0L
232
+ val stats = PipelineStats { clock }
233
+ stats.wireWidth = 960
234
+ stats.wireHeight = 540
235
+ stats.wireFps = 29.8
236
+ stats.wireBitrateBps = 1_480_000
237
+ clock += 1000
238
+ stats.codecName = "H264SkypeEncoder_HW"
239
+ assertThat(stats.tick()).contains("wire=960x540@29.8 kbps=1480 codec=H264SkypeEncoder_HW")
240
+ }
241
+
242
+ @Test
243
+ fun ladderExposesRecvRateSeparateFromDecode() {
244
+ var clock = 0L
245
+ val stats = PipelineStats { clock }
246
+ stats.recvFps = 14.8
247
+ stats.decodedFps = 8.1
248
+ clock += 1000
249
+ val line = stats.tick()
250
+ assertThat(line).contains("recv=14.8")
251
+ assertThat(line).contains("dec=8.1")
252
+ }
253
+
254
+ private fun extractCum(line: String): String =
255
+ Regex("""cum\{[^}]+\}""").find(line)?.value ?: line
256
+ }
@@ -0,0 +1,18 @@
1
+ package com.mentra.acsmeeting.telemetry
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+
6
+ class ProcessCpuTest {
7
+ @Test
8
+ fun percentMatchesTopStyleOverOneSecond() {
9
+ assertThat(ProcessCpu.percent(cpuDeltaMs = 1120, wallDeltaMs = 1000)).isEqualTo(112.0)
10
+ assertThat(ProcessCpu.percent(cpuDeltaMs = 200, wallDeltaMs = 1000)).isEqualTo(20.0)
11
+ }
12
+
13
+ @Test
14
+ fun labelFormatsNaUntilFirstSample() {
15
+ assertThat(ProcessCpu.label(null, 8)).isEqualTo("cpu{proc=na cores=8}")
16
+ assertThat(ProcessCpu.label(112.4, 8)).isEqualTo("cpu{proc=112.4 cores=8}")
17
+ }
18
+ }
@@ -0,0 +1,49 @@
1
+ package com.mentra.acsmeeting.telemetry
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+ import java.util.concurrent.CountDownLatch
6
+ import java.util.concurrent.Executors
7
+ import java.util.concurrent.TimeUnit
8
+
9
+ class RingPercentileTest {
10
+ @Test
11
+ fun emptyIsNa() {
12
+ assertThat(RingPercentile().p50()).isEqualTo("na")
13
+ assertThat(RingPercentile().p95()).isEqualTo("na")
14
+ }
15
+
16
+ @Test
17
+ fun knownInputs() {
18
+ val ring = RingPercentile(8)
19
+ ring.record(10_000_000)
20
+ ring.record(20_000_000)
21
+ ring.record(40_000_000)
22
+ assertThat(ring.p50()).isEqualTo("20.0")
23
+ }
24
+
25
+ @Test
26
+ fun wraparoundKeepsNewest() {
27
+ val ring = RingPercentile(3)
28
+ repeat(6) { index -> ring.record((index + 1) * 1_000_000L) }
29
+ assertThat(ring.size()).isEqualTo(3)
30
+ assertThat(ring.p50()).isEqualTo("5.0")
31
+ }
32
+
33
+ @Test
34
+ fun concurrentRecordDoesNotCorruptCount() {
35
+ val ring = RingPercentile(32)
36
+ val pool = Executors.newFixedThreadPool(4)
37
+ val latch = CountDownLatch(4)
38
+ repeat(4) {
39
+ pool.execute {
40
+ repeat(50) { ring.record(1_000_000) }
41
+ latch.countDown()
42
+ }
43
+ }
44
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue()
45
+ pool.shutdown()
46
+ assertThat(ring.size()).isEqualTo(32)
47
+ assertThat(ring.p50()).isNotEqualTo("na")
48
+ }
49
+ }
@@ -0,0 +1,43 @@
1
+ package com.mentra.acsmeeting.video
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+
6
+ class AcsTimestampTest {
7
+ @Test
8
+ fun streamClockWinsWhenItIsAdvancing() {
9
+ val ticks = AcsTimestamp.resolve(streamTicks = 5_000, captureNs = 1_000_000, lastTicks = 100)
10
+ assertThat(ticks).isEqualTo(5_000)
11
+ }
12
+
13
+ @Test
14
+ fun captureNsConvertsToHundredNanosecondTicks() {
15
+ val ticks = AcsTimestamp.resolve(streamTicks = 0, captureNs = 66_700_000, lastTicks = 0)
16
+ assertThat(ticks).isEqualTo(667_000)
17
+ }
18
+
19
+ /**
20
+ * A run of timestamp-0 frames is what Teams renders as a freeze: the jitter
21
+ * buffer holds the last picture because presentation time never moves.
22
+ */
23
+ @Test
24
+ fun zeroInputsStillAdvance() {
25
+ val first = AcsTimestamp.resolve(streamTicks = 0, captureNs = 0, lastTicks = 0)
26
+ val second = AcsTimestamp.resolve(streamTicks = 0, captureNs = 0, lastTicks = first)
27
+ assertThat(first).isEqualTo(1)
28
+ assertThat(second).isGreaterThan(first)
29
+ }
30
+
31
+ @Test
32
+ fun neverReusesTheLastTick() {
33
+ val stuckStream = 40L
34
+ val ticks = AcsTimestamp.resolve(streamTicks = stuckStream, captureNs = 1_000, lastTicks = 40)
35
+ assertThat(ticks).isEqualTo(41)
36
+ }
37
+
38
+ @Test
39
+ fun captureBehindLastTickStillAdvances() {
40
+ val ticks = AcsTimestamp.resolve(streamTicks = 0, captureNs = 100, lastTicks = 9_000)
41
+ assertThat(ticks).isEqualTo(9_001)
42
+ }
43
+ }
@@ -0,0 +1,33 @@
1
+ package com.mentra.acsmeeting.video
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+
6
+ class FrameGeometryTest {
7
+ @Test
8
+ fun rotationZeroUsesBufferSize() {
9
+ val size = FrameGeometry.packSize(1280, 720, 0)
10
+ assertThat(size.width).isEqualTo(1280)
11
+ assertThat(size.height).isEqualTo(720)
12
+ assertThat(size.rotationNonZero).isFalse()
13
+ }
14
+
15
+ @Test
16
+ fun rotation90KeepsBufferCoordinatesNotRotatedDisplaySize() {
17
+ val bufferW = 1280
18
+ val bufferH = 720
19
+ val rotatedWidth = bufferH
20
+ val rotatedHeight = bufferW
21
+ val size = FrameGeometry.packSize(bufferW, bufferH, 90)
22
+ assertThat(size.width).isEqualTo(bufferW)
23
+ assertThat(size.height).isEqualTo(bufferH)
24
+ assertThat(size.rotationNonZero).isTrue()
25
+ assertThat(size.width).isNotEqualTo(rotatedWidth)
26
+ assertThat(size.height).isNotEqualTo(rotatedHeight)
27
+ val overrun = I420Packer.packedSize(rotatedWidth, rotatedHeight)
28
+ val actual = I420Packer.packedSize(size.width, size.height)
29
+ assertThat(overrun).isEqualTo(actual)
30
+ val planeIfRotated = rotatedHeight * 1280
31
+ assertThat(planeIfRotated).isGreaterThan(bufferW * bufferH)
32
+ }
33
+ }
@@ -0,0 +1,77 @@
1
+ package com.mentra.acsmeeting.video
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+
6
+ /**
7
+ * Asserts the advertised format arithmetic via [I420FormatSpec]. The ACS
8
+ * VideoStreamFormat object itself is not constructed here: it is native-backed
9
+ * and throws ExceptionInInitializerError outside an Android runtime.
10
+ */
11
+ class AcsFrameSenderFormatTest {
12
+ @Test
13
+ fun i420FormatOffersTight720p20() {
14
+ val spec = I420FormatSpec.of()
15
+ assertThat(spec.width).isEqualTo(1280)
16
+ assertThat(spec.height).isEqualTo(720)
17
+ assertThat(spec.fps).isEqualTo(20f)
18
+ assertThat(spec.strideY).isEqualTo(1280)
19
+ assertThat(spec.strideU).isEqualTo(640)
20
+ assertThat(spec.strideV).isEqualTo(640)
21
+ }
22
+
23
+ @Test
24
+ fun i420FormatRoundsOddChromaStrides() {
25
+ val spec = I420FormatSpec.of(width = 3, height = 3, fps = 15f)
26
+ assertThat(spec.strideY).isEqualTo(3)
27
+ assertThat(spec.strideU).isEqualTo(2)
28
+ assertThat(spec.strideV).isEqualTo(2)
29
+ }
30
+
31
+ @Test
32
+ fun advertisedStridesMatchPacker() {
33
+ assertThat(I420Packer.chromaStride(1280)).isEqualTo(640)
34
+ assertThat(I420Packer.packedSize(1280, 720)).isEqualTo(1_382_400)
35
+ }
36
+
37
+ /** A disagreement here means ACS reads chroma at the wrong offset: green frames. */
38
+ @Test
39
+ fun advertisedSizeMatchesWhatThePackerWrites() {
40
+ for ((w, h) in listOf(1280 to 720, 960 to 540, 640 to 360, 3 to 3, 641 to 361)) {
41
+ val spec = I420FormatSpec.of(width = w, height = h)
42
+ assertThat(spec.packedSize())
43
+ .describedAs("%dx%d", w, h)
44
+ .isEqualTo(I420Packer.packedSize(w, h))
45
+ }
46
+ }
47
+
48
+ @Test
49
+ fun i420FormatStaysInsideAcsBounds() {
50
+ assertThat(I420FormatSpec.of().withinAcsBounds()).isTrue()
51
+ }
52
+
53
+ @Test
54
+ fun parseOrNullAllowsDocumentedVirtualCameraSizesOnly() {
55
+ assertThat(I420FormatSpec.parseOrNull(1280, 720, 15f)).isNotNull()
56
+ assertThat(I420FormatSpec.parseOrNull(960, 540, 30f)).isNotNull()
57
+ assertThat(I420FormatSpec.parseOrNull(540, 960, 30f)).isNull()
58
+ assertThat(I420FormatSpec.parseOrNull(854, 480, 15f)).isNull()
59
+ }
60
+
61
+ @Test
62
+ fun namedResolutionMapsAcsEnumsAndRejectsNearMatches() {
63
+ assertThat(I420FormatSpec.of(1280, 720).namedResolution()).isEqualTo(AcsNamedResolution.P720)
64
+ assertThat(I420FormatSpec.of(960, 540).namedResolution()).isEqualTo(AcsNamedResolution.P540)
65
+ assertThat(I420FormatSpec.of(858, 480).namedResolution()).isEqualTo(AcsNamedResolution.P480)
66
+ assertThat(I420FormatSpec.of(640, 360).namedResolution()).isEqualTo(AcsNamedResolution.P360)
67
+ assertThat(I420FormatSpec.of(854, 480).namedResolution()).isNull()
68
+ assertThat(I420FormatSpec.of(540, 960).namedResolution()).isNull()
69
+ }
70
+
71
+ @Test
72
+ fun outOfRangeGeometryIsReportedNotSilentlyClamped() {
73
+ assertThat(I420FormatSpec.of(width = 3, height = 3).withinAcsBounds()).isFalse()
74
+ assertThat(I420FormatSpec.of(width = 4096, height = 2160).withinAcsBounds()).isFalse()
75
+ assertThat(I420FormatSpec.of(fps = 60f).withinAcsBounds()).isFalse()
76
+ }
77
+ }
@@ -0,0 +1,126 @@
1
+ package com.mentra.acsmeeting.video
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+ import java.nio.ByteBuffer
6
+
7
+ class I420PackerTest {
8
+ @Test
9
+ fun percentileMsHandlesEmptyAndOrder() {
10
+ assertThat(I420Packer.percentileMs(LongArray(0), 0.50)).isEqualTo("na")
11
+ assertThat(I420Packer.percentileMs(longArrayOf(10_000_000, 20_000_000, 40_000_000), 0.50)).isEqualTo("20.0")
12
+ }
13
+
14
+ @Test
15
+ fun packedSizeUsesChromaRounding() {
16
+ assertThat(I420Packer.packedSize(4, 4)).isEqualTo(24)
17
+ assertThat(I420Packer.packedSize(3, 3)).isEqualTo(3 * 3 + 2 * 2 * 2)
18
+ assertThat(I420Packer.packedSize(1280, 720)).isEqualTo(1280 * 720 * 3 / 2)
19
+ }
20
+
21
+ @Test
22
+ fun planeMinBytesIsLastRowPlusFullStrides() {
23
+ assertThat(I420Packer.planeMinBytes(8, 8, 4)).isEqualTo(32)
24
+ assertThat(I420Packer.planeMinBytes(16, 8, 4)).isEqualTo(16 * 3 + 8)
25
+ assertThat(I420Packer.planeMinBytes(4, 8, 4)).isEqualTo(0)
26
+ }
27
+
28
+ @Test
29
+ fun copyPlaneFromPaddedStrideWritesTightDest() {
30
+ val width = 4
31
+ val height = 4
32
+ val padded = buildPlanes(width, height, strideY = 8, strideUv = 4, seed = 7)
33
+ val dest = ByteBuffer.allocate(width * height)
34
+ I420Packer.copyPlane(padded.y, 8, width, height, dest)
35
+ dest.flip()
36
+ val out = ByteArray(dest.remaining())
37
+ dest.get(out)
38
+ assertThat(out).containsExactly(
39
+ 7, 8, 9, 10,
40
+ 11, 12, 13, 14,
41
+ 15, 16, 17, 18,
42
+ 19, 20, 21, 22,
43
+ )
44
+ }
45
+
46
+ @Test
47
+ fun chromaStrideMatchesAcsI420Offer() {
48
+ assertThat(I420Packer.chromaStride(1280)).isEqualTo(640)
49
+ assertThat(I420Packer.chromaStride(3)).isEqualTo(2)
50
+ }
51
+
52
+ @Test
53
+ fun paddedStridesPackIdenticallyToTightStrides() {
54
+ val width = 4
55
+ val height = 4
56
+ val tight = buildPlanes(width, height, strideY = 4, strideUv = 2, seed = 7)
57
+ val padded = buildPlanes(width, height, strideY = 8, strideUv = 4, seed = 7)
58
+ val tightOut = ByteBuffer.allocate(I420Packer.packedSize(width, height))
59
+ val paddedOut = ByteBuffer.allocate(I420Packer.packedSize(width, height))
60
+ I420Packer.pack(
61
+ tight.y, 4, tight.u, 2, tight.v, 2, width, height, tightOut,
62
+ )
63
+ I420Packer.pack(
64
+ padded.y, 8, padded.u, 4, padded.v, 4, width, height, paddedOut,
65
+ )
66
+ assertThat(bytes(paddedOut)).isEqualTo(bytes(tightOut))
67
+ assertThat(tightOut.remaining()).isEqualTo(I420Packer.packedSize(width, height))
68
+ }
69
+
70
+ @Test
71
+ fun oddDimensionsPackExactBytes() {
72
+ val width = 3
73
+ val height = 3
74
+ val planes = buildPlanes(width, height, strideY = 5, strideUv = 3, seed = 11)
75
+ val dest = ByteBuffer.allocate(I420Packer.packedSize(width, height))
76
+ I420Packer.pack(planes.y, 5, planes.u, 3, planes.v, 3, width, height, dest)
77
+ val packed = bytes(dest)
78
+ assertThat(packed).hasSize(3 * 3 + 2 * 2 * 2)
79
+ // Visible Y is seed + row*width + col; chroma is 2x2 after (3+1)/2 rounding.
80
+ assertThat(packed.copyOfRange(0, 9)).containsExactly(
81
+ 11, 12, 13,
82
+ 14, 15, 16,
83
+ 17, 18, 19,
84
+ )
85
+ assertThat(packed.copyOfRange(9, 13)).containsExactly(31, 32, 33, 34)
86
+ assertThat(packed.copyOfRange(13, 17)).containsExactly(41, 42, 43, 44)
87
+ }
88
+
89
+ private fun buildPlanes(
90
+ width: Int,
91
+ height: Int,
92
+ strideY: Int,
93
+ strideUv: Int,
94
+ seed: Int,
95
+ ): Planes {
96
+ val chromaW = (width + 1) / 2
97
+ val chromaH = (height + 1) / 2
98
+ val y = ByteBuffer.allocate(strideY * height)
99
+ val u = ByteBuffer.allocate(strideUv * chromaH)
100
+ val v = ByteBuffer.allocate(strideUv * chromaH)
101
+ for (row in 0 until height) {
102
+ for (col in 0 until width) {
103
+ y.put(row * strideY + col, (seed + row * width + col).toByte())
104
+ }
105
+ }
106
+ for (row in 0 until chromaH) {
107
+ for (col in 0 until chromaW) {
108
+ u.put(row * strideUv + col, (seed + 20 + row * chromaW + col).toByte())
109
+ v.put(row * strideUv + col, (seed + 30 + row * chromaW + col).toByte())
110
+ }
111
+ }
112
+ y.rewind()
113
+ u.rewind()
114
+ v.rewind()
115
+ return Planes(y, u, v)
116
+ }
117
+
118
+ private fun bytes(buffer: ByteBuffer): ByteArray {
119
+ val copy = buffer.duplicate()
120
+ val out = ByteArray(copy.remaining())
121
+ copy.get(out)
122
+ return out
123
+ }
124
+
125
+ private data class Planes(val y: ByteBuffer, val u: ByteBuffer, val v: ByteBuffer)
126
+ }