@mentra/acs-meeting 3.2.0-dev.235 → 3.2.0-dev.245

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.
@@ -0,0 +1,330 @@
1
+ package com.mentra.acsmeeting.telemetry
2
+
3
+ import com.mentra.glassesmedia.trace.SoftApTrace
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertTrue
6
+ import org.junit.Test
7
+
8
+ /**
9
+ * These exist because the analyzer parses these exact field names. A rename here and no rename
10
+ * there produces a comparison that silently reports nothing rather than failing, which is the one
11
+ * outcome that wastes a device session.
12
+ */
13
+ class CallDiagnosticsTest {
14
+
15
+ private fun sample(
16
+ state: String = "connected",
17
+ sinceConnectedMs: Long = 4_000,
18
+ wireBitrateBps: Long? = 1_800_000,
19
+ ) = CallDiagnostics.BweSample(
20
+ state = state,
21
+ sinceJoinMs = 12_000,
22
+ sinceConnectedMs = sinceConnectedMs,
23
+ lobbyDwellMs = 7_500,
24
+ sendQuality = "GOOD",
25
+ wireBitrateBps = wireBitrateBps,
26
+ wireWidth = 960,
27
+ wireHeight = 540,
28
+ sentFps = 14.6,
29
+ wireFps = 14.2,
30
+ inboundBitrateBps = 2_100_000,
31
+ inboundFps = 15.0,
32
+ decodedFps = 15.0,
33
+ framesGated = 3,
34
+ pacerDrops = 1,
35
+ budgetBps = 2_500_000,
36
+ rateArm = "advertise_requested",
37
+ mediaStatsReports = 4,
38
+ mediaStatsAttached = true,
39
+ videoOut = "started",
40
+ packetsPerSecond = 42.5,
41
+ subCount = 180,
42
+ sinkCount = 182,
43
+ )
44
+
45
+ private fun render(fields: Array<Pair<String, Any?>>): String =
46
+ SoftApTrace.format("abc123", "acs_bwe_sample", 1_000, *fields)
47
+
48
+ @Test
49
+ fun `a sample carries the origin it was taken under`() {
50
+ val line = render(CallDiagnostics.bweFields("joined", "call-1", sample()))
51
+
52
+ assertTrue(line, line.contains(" origin=joined"))
53
+ assertTrue(line, line.contains(" callId=call-1"))
54
+ }
55
+
56
+ /** An unstamped call still has to group somewhere, and "unknown" is a third bucket, not a guess. */
57
+ @Test
58
+ fun `a caller that sent no origin is recorded as unknown rather than either path`() {
59
+ val line = render(CallDiagnostics.bweFields("", "", sample()))
60
+
61
+ assertTrue(line, line.contains(" origin=unknown"))
62
+ assertTrue(line, line.contains(" callId=none"))
63
+ }
64
+
65
+ @Test
66
+ fun `the sample names both hops and what was asked for`() {
67
+ val line = render(CallDiagnostics.bweFields("created", "call-2", sample()))
68
+
69
+ for (key in listOf(
70
+ "state=connected",
71
+ "sinceJoinMs=12000",
72
+ "sinceConnectedMs=4000",
73
+ "lobbyDwellMs=7500",
74
+ "sendQuality=GOOD",
75
+ "wireBitrateBps=1800000",
76
+ "sentFps=14.6",
77
+ // Both hops on one line is what makes the comparison exact instead of aligned by timestamp.
78
+ "inboundBitrateBps=2100000",
79
+ "inboundFps=15.0",
80
+ "framesGated=3",
81
+ "pacerDrops=1",
82
+ "budgetBps=2500000",
83
+ "p7RateBound=advertise_requested",
84
+ "mediaStatsReports=4",
85
+ "mediaStatsAttached=true",
86
+ "videoOut=started",
87
+ "packetsPerSecond=42.5",
88
+ "subCount=180",
89
+ "sinkCount=182",
90
+ )) {
91
+ assertTrue("$key missing from $line", line.contains(" $key"))
92
+ }
93
+ }
94
+
95
+ @Test
96
+ fun `media-stats attach and report names stay stable for the analyzer`() {
97
+ val attach = render(CallDiagnostics.mediaStatsAttachFields("created", "call-4", ok = true))
98
+ assertTrue(attach, attach.contains(" origin=created"))
99
+ assertTrue(attach, attach.contains(" ok=true"))
100
+ assertTrue(attach, attach.contains(" error=none"))
101
+
102
+ val report = render(
103
+ CallDiagnostics.mediaStatsReportFields(
104
+ origin = "joined",
105
+ callId = "call-5",
106
+ n = 3,
107
+ videos = 1,
108
+ audios = 1,
109
+ wireBitrateBps = 1_320_000,
110
+ width = 960,
111
+ height = 540,
112
+ fps = 15.0,
113
+ codec = "H264 HW",
114
+ packetCount = 800,
115
+ ),
116
+ )
117
+ for (key in listOf(
118
+ "origin=joined",
119
+ "n=3",
120
+ "videos=1",
121
+ "wireBitrateBps=1320000",
122
+ "width=960",
123
+ "height=540",
124
+ "fps=15.0",
125
+ "codec=H264_HW",
126
+ "packetCount=800",
127
+ )) {
128
+ assertTrue("$key missing from $report", report.contains(" $key"))
129
+ }
130
+ }
131
+
132
+ /** Zero would read as "sending nothing", which is the opposite of "nobody has told us yet". */
133
+ @Test
134
+ fun `an unreported bitrate is negative rather than zero`() {
135
+ val line = render(CallDiagnostics.bweFields("created", "call-3", sample(wireBitrateBps = null)))
136
+
137
+ assertTrue(line, line.contains(" wireBitrateBps=-1"))
138
+ }
139
+
140
+ @Test
141
+ fun `sampling stays dense through the settling window and relaxes after it`() {
142
+ // Before connected: still settling, so still dense.
143
+ assertEquals(CallDiagnostics.DENSE_INTERVAL_MS, CallDiagnostics.sampleIntervalMs(-1))
144
+ assertEquals(CallDiagnostics.DENSE_INTERVAL_MS, CallDiagnostics.sampleIntervalMs(0))
145
+ assertEquals(CallDiagnostics.DENSE_INTERVAL_MS, CallDiagnostics.sampleIntervalMs(CallDiagnostics.DENSE_WINDOW_MS))
146
+ assertEquals(
147
+ CallDiagnostics.SPARSE_INTERVAL_MS,
148
+ CallDiagnostics.sampleIntervalMs(CallDiagnostics.DENSE_WINDOW_MS + 1),
149
+ )
150
+ }
151
+
152
+ private fun health(
153
+ sinceConnectedMs: Long,
154
+ wireBitrateBps: Long?,
155
+ msSinceLow: Long = -1,
156
+ msSinceAdaptation: Long = -1,
157
+ ) = CallDiagnostics.wireHealth(sinceConnectedMs, wireBitrateBps, msSinceLow, msSinceAdaptation)
158
+
159
+ @Test
160
+ fun `an established call at full rate is the only thing sampled sparsely`() {
161
+ val settled = health(sinceConnectedMs = 200_000, wireBitrateBps = 1_300_000)
162
+
163
+ assertEquals(CallDiagnostics.WireHealth.HEALTHY, settled)
164
+ assertEquals(CallDiagnostics.SPARSE_INTERVAL_MS, CallDiagnostics.sampleIntervalMs(settled))
165
+ }
166
+
167
+ /**
168
+ * The bug this replaced: the old cadence went sparse at 90 s and the collapse began at 150 s, so
169
+ * the event the whole investigation was about was sampled every 10 seconds.
170
+ */
171
+ @Test
172
+ fun `a collapse long after the settling window pulls the cadence back to dense`() {
173
+ val low = health(sinceConnectedMs = 150_000, wireBitrateBps = 33_000)
174
+
175
+ assertEquals(CallDiagnostics.WireHealth.LOW, low)
176
+ assertEquals(CallDiagnostics.DENSE_INTERVAL_MS, CallDiagnostics.sampleIntervalMs(low))
177
+ }
178
+
179
+ /** The climb back was the longer half of the outage, so it is sampled at the same rate. */
180
+ @Test
181
+ fun `the recovery ramp stays dense while it is still degraded`() {
182
+ val climbing = health(sinceConnectedMs = 250_000, wireBitrateBps = 700_000)
183
+
184
+ assertEquals(CallDiagnostics.WireHealth.RECOVERING, climbing)
185
+ assertEquals(CallDiagnostics.DENSE_INTERVAL_MS, CallDiagnostics.sampleIntervalMs(climbing))
186
+ }
187
+
188
+ @Test
189
+ fun `a call that just came back is watched a while before being trusted`() {
190
+ val justBack = health(sinceConnectedMs = 250_000, wireBitrateBps = 1_300_000, msSinceLow = 5_000)
191
+ assertEquals(CallDiagnostics.WireHealth.RECOVERING, justBack)
192
+
193
+ val longBack =
194
+ health(
195
+ sinceConnectedMs = 400_000,
196
+ wireBitrateBps = 1_300_000,
197
+ msSinceLow = CallDiagnostics.RECOVERY_WATCH_MS + 1,
198
+ )
199
+ assertEquals(CallDiagnostics.WireHealth.HEALTHY, longBack)
200
+ }
201
+
202
+ @Test
203
+ fun `a recent downscale is watched even at a healthy rate`() {
204
+ val adapted = health(sinceConnectedMs = 300_000, wireBitrateBps = 1_300_000, msSinceAdaptation = 4_000)
205
+
206
+ assertEquals(CallDiagnostics.WireHealth.RECOVERING, adapted)
207
+ }
208
+
209
+ /**
210
+ * ACS publishing an empty report is a fact about ACS, not a bitrate of zero, so it is its own
211
+ * state — and a dense one, because the other columns (sent fps, subscriber count, glasses hop)
212
+ * are what characterise the blindness.
213
+ */
214
+ @Test
215
+ fun `a silent wire is distinguished from a low one and sampled densely`() {
216
+ assertEquals(CallDiagnostics.WireHealth.SILENT, health(sinceConnectedMs = 300_000, wireBitrateBps = null))
217
+ assertEquals(CallDiagnostics.WireHealth.SILENT, health(sinceConnectedMs = 300_000, wireBitrateBps = -1))
218
+ assertEquals(
219
+ CallDiagnostics.DENSE_INTERVAL_MS,
220
+ CallDiagnostics.sampleIntervalMs(CallDiagnostics.WireHealth.SILENT),
221
+ )
222
+ }
223
+
224
+ @Test
225
+ fun `everything inside the settling window is settling whatever the rate`() {
226
+ assertEquals(CallDiagnostics.WireHealth.SETTLING, health(sinceConnectedMs = -1, wireBitrateBps = null))
227
+ assertEquals(CallDiagnostics.WireHealth.SETTLING, health(sinceConnectedMs = 10_000, wireBitrateBps = 33_000))
228
+ }
229
+
230
+ @Test
231
+ fun `a downscale names what was asked for as well as what arrived`() {
232
+ val line = SoftApTrace.format(
233
+ "abc123",
234
+ "acs_wire_adaptation",
235
+ 2_000,
236
+ *CallDiagnostics.wireAdaptationFields(
237
+ origin = "joined",
238
+ callId = "call-6",
239
+ width = 320,
240
+ height = 180,
241
+ direction = "down",
242
+ sinceConnectedMs = 150_000,
243
+ askedWidth = 960,
244
+ askedHeight = 540,
245
+ wireBitrateBps = 33_000,
246
+ ceilingBps = 3_000_000,
247
+ fps = 14.4,
248
+ ),
249
+ )
250
+
251
+ for (key in listOf(
252
+ "origin=joined",
253
+ "width=320",
254
+ "height=180",
255
+ "direction=down",
256
+ "sinceConnectedMs=150000",
257
+ "asked=960x540",
258
+ // 11% of what was negotiated. The percentage is the part that reads as a fault rather than
259
+ // as a number, which is why it is computed here instead of left to the reader.
260
+ "percentOfAsked=11",
261
+ "wireBitrateBps=33000",
262
+ "ceilingBps=3000000",
263
+ )) {
264
+ assertTrue("$key missing from $line", line.contains(" $key"))
265
+ }
266
+ }
267
+
268
+ @Test
269
+ fun `an episode carries its floor, its cost and the hop that stayed healthy`() {
270
+ val line = SoftApTrace.format(
271
+ "abc123",
272
+ "acs_wire_episode",
273
+ 3_000,
274
+ *CallDiagnostics.episodeFields(
275
+ origin = "joined",
276
+ callId = "call-7",
277
+ episode = "recovered",
278
+ startedAtMs = 150_000,
279
+ durationMs = 90_000,
280
+ minBitrateBps = 33_000,
281
+ minResolution = "320x180",
282
+ recoveryMs = 170_000,
283
+ inboundBitrateBps = 2_100_000.0,
284
+ sentFps = 14.5,
285
+ ceilingBps = 3_000_000,
286
+ ),
287
+ )
288
+
289
+ for (key in listOf(
290
+ "episode=recovered",
291
+ "startedAtMs=150000",
292
+ "durationMs=90000",
293
+ "minBitrateBps=33000",
294
+ "minResolution=320x180",
295
+ "recoveryMs=170000",
296
+ "inboundBitrateBps=2100000.0",
297
+ "sentFps=14.5",
298
+ "ceilingBps=3000000",
299
+ )) {
300
+ assertTrue("$key missing from $line", line.contains(" $key"))
301
+ }
302
+ }
303
+
304
+ /** "Not back yet" and "back instantly" differ by 170 seconds on the capture that started this. */
305
+ @Test
306
+ fun `an unrecovered episode reports negative rather than zero recovery`() {
307
+ val line = SoftApTrace.format(
308
+ "abc123",
309
+ "acs_wire_episode",
310
+ 3_000,
311
+ *CallDiagnostics.episodeFields(
312
+ origin = "created",
313
+ callId = "call-8",
314
+ episode = "begin",
315
+ startedAtMs = 150_000,
316
+ durationMs = 0,
317
+ minBitrateBps = 33_000,
318
+ minResolution = "",
319
+ recoveryMs = -1,
320
+ inboundBitrateBps = null,
321
+ sentFps = 15.0,
322
+ ceilingBps = 1_500_000,
323
+ ),
324
+ )
325
+
326
+ assertTrue(line, line.contains(" recoveryMs=-1"))
327
+ assertTrue(line, line.contains(" inboundBitrateBps=na"))
328
+ assertTrue(line, line.contains(" minResolution=na"))
329
+ }
330
+ }
@@ -0,0 +1,180 @@
1
+ package com.mentra.acsmeeting.telemetry
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertNotNull
6
+ import org.junit.Assert.assertNull
7
+ import org.junit.Assert.assertTrue
8
+ import org.junit.Test
9
+
10
+ /**
11
+ * The state machine that decides an episode happened. Tested hard because it is the thing that
12
+ * turns a device session into a verdict, and its two failure modes are both silent: inventing
13
+ * episodes out of ACS's empty reports, or missing a real one and pronouncing a bad call healthy.
14
+ */
15
+ class WireEpisodeTrackerTest {
16
+
17
+ private fun tracker() = WireEpisodeTracker()
18
+
19
+ private fun WireEpisodeTracker.feed(
20
+ atMs: Long,
21
+ bitrateBps: Long?,
22
+ width: Int? = 960,
23
+ height: Int? = 540,
24
+ inbound: Double? = 2_000_000.0,
25
+ sentFps: Double = 15.0,
26
+ ) = observe(atMs, bitrateBps, width, height, inbound, sentFps)
27
+
28
+ @Test
29
+ fun `a healthy wire produces no events at all`() {
30
+ val tracker = tracker()
31
+
32
+ for (t in 0..10) {
33
+ assertNull(tracker.feed(t * 2_000L, 1_300_000))
34
+ }
35
+ assertFalse(tracker.isInEpisode())
36
+ assertFalse(tracker.isAwaitingRecovery())
37
+ }
38
+
39
+ /**
40
+ * The capture that motivated the class: a dip that ends long before the picture comes back.
41
+ * Reporting only the dip would understate what the wearer sat through by more than half.
42
+ */
43
+ @Test
44
+ fun `a dip and its climb are reported as separate costs`() {
45
+ val tracker = tracker()
46
+
47
+ assertNull(tracker.feed(0, 1_300_000))
48
+ val begin = tracker.feed(150_000, 33_000, width = 320, height = 180)
49
+ assertNotNull(begin)
50
+ assertEquals(WireEpisodeTracker.Phase.BEGIN, begin!!.phase)
51
+ assertEquals(150_000L, begin.startedAtMs)
52
+ assertEquals(0L, begin.durationMs)
53
+ // Not yet recovered, and -1 says so rather than claiming an instant recovery.
54
+ assertEquals(-1L, begin.recoveryMs)
55
+ assertTrue(tracker.isInEpisode())
56
+
57
+ assertNull(tracker.feed(200_000, 40_000, width = 320, height = 180))
58
+
59
+ val end = tracker.feed(240_000, 700_000, width = 640, height = 360)
60
+ assertNotNull(end)
61
+ assertEquals(WireEpisodeTracker.Phase.END, end!!.phase)
62
+ assertEquals(90_000L, end.durationMs)
63
+ assertEquals(33_000L, end.minBitrateBps)
64
+ assertEquals("320x180", end.minResolution)
65
+ assertEquals(-1L, end.recoveryMs)
66
+ assertFalse(tracker.isInEpisode())
67
+ assertTrue(tracker.isAwaitingRecovery())
68
+
69
+ // Still under 1 Mbps: degraded, so not recovered.
70
+ assertNull(tracker.feed(300_000, 900_000))
71
+
72
+ val recovered = tracker.feed(410_000, 1_300_000)
73
+ assertNotNull(recovered)
74
+ assertEquals(WireEpisodeTracker.Phase.RECOVERED, recovered!!.phase)
75
+ assertEquals(90_000L, recovered.durationMs)
76
+ assertEquals(170_000L, recovered.recoveryMs)
77
+ assertFalse(tracker.isAwaitingRecovery())
78
+ }
79
+
80
+ /**
81
+ * The rule that keeps the whole thing honest. ACS has been observed publishing 32 empty
82
+ * MEDIA_STATISTICS reports against 12 filled ones on one seven-minute call; if an unset field
83
+ * counted as zero, that call would report 32 episodes and the real ones would be unfindable.
84
+ */
85
+ @Test
86
+ fun `an unreported rate is not an episode`() {
87
+ val tracker = tracker()
88
+
89
+ assertNull(tracker.feed(0, 1_300_000))
90
+ assertNull(tracker.feed(2_000, null))
91
+ assertNull(tracker.feed(4_000, -1))
92
+ assertNull(tracker.feed(6_000, 0))
93
+
94
+ assertFalse(tracker.isInEpisode())
95
+ // And the healthy state survived the silence rather than being reset by it.
96
+ assertNull(tracker.feed(8_000, 1_300_000))
97
+ }
98
+
99
+ /** Silence in the middle of an episode must not end it either. */
100
+ @Test
101
+ fun `silence during an episode leaves it open`() {
102
+ val tracker = tracker()
103
+
104
+ assertNotNull(tracker.feed(0, 40_000))
105
+ assertNull(tracker.feed(2_000, null))
106
+ assertTrue(tracker.isInEpisode())
107
+
108
+ val end = tracker.feed(10_000, 1_300_000)
109
+ assertEquals(WireEpisodeTracker.Phase.END, end!!.phase)
110
+ assertEquals(10_000L, end.durationMs)
111
+ }
112
+
113
+ /**
114
+ * Two dips with a brief good patch between them are two episodes. Merging them would hide the
115
+ * flapping, which is a different fault from one long collapse and points somewhere else.
116
+ */
117
+ @Test
118
+ fun `a second dip before recovery starts a new episode`() {
119
+ val tracker = tracker()
120
+
121
+ assertNotNull(tracker.feed(0, 40_000))
122
+ val firstEnd = tracker.feed(10_000, 600_000)
123
+ assertEquals(WireEpisodeTracker.Phase.END, firstEnd!!.phase)
124
+
125
+ val secondBegin = tracker.feed(20_000, 35_000)
126
+ assertNotNull(secondBegin)
127
+ assertEquals(WireEpisodeTracker.Phase.BEGIN, secondBegin!!.phase)
128
+ assertEquals(20_000L, secondBegin.startedAtMs)
129
+ // The new episode starts clean rather than inheriting the first one's floor.
130
+ assertEquals(35_000L, secondBegin.minBitrateBps)
131
+ }
132
+
133
+ @Test
134
+ fun `the glasses hop during the episode is averaged and carried`() {
135
+ val tracker = tracker()
136
+
137
+ tracker.feed(0, 40_000, inbound = 2_000_000.0)
138
+ tracker.feed(2_000, 35_000, inbound = 2_200_000.0)
139
+ val end = tracker.feed(4_000, 1_300_000, inbound = 2_100_000.0)
140
+
141
+ // ~2.1 Mbps from the source while the wire sat at 35 kbps. This is the number that says the
142
+ // collapse was ACS's choice and not a starved pipeline.
143
+ assertEquals(2_100_000.0, end!!.inboundBitrateBps!!, 1.0)
144
+ }
145
+
146
+ @Test
147
+ fun `an episode with no glasses reading reports none rather than zero`() {
148
+ val tracker = tracker()
149
+
150
+ tracker.feed(0, 40_000, inbound = null)
151
+ val end = tracker.feed(2_000, 1_300_000, inbound = null)
152
+
153
+ assertNull(end!!.inboundBitrateBps)
154
+ }
155
+
156
+ @Test
157
+ fun `an episode with no size reading still reports its floor`() {
158
+ val tracker = tracker()
159
+
160
+ tracker.feed(0, 40_000, width = null, height = null)
161
+ val end = tracker.feed(2_000, 1_300_000)
162
+
163
+ assertEquals(40_000L, end!!.minBitrateBps)
164
+ assertEquals("", end.minResolution)
165
+ }
166
+
167
+ /** A jump straight from the floor past the recovery line reports one event, then the other. */
168
+ @Test
169
+ fun `a straight jump to full rate reports end then recovered`() {
170
+ val tracker = tracker()
171
+
172
+ tracker.feed(0, 40_000)
173
+ val end = tracker.feed(2_000, 1_300_000)
174
+ assertEquals(WireEpisodeTracker.Phase.END, end!!.phase)
175
+
176
+ val recovered = tracker.feed(4_000, 1_300_000)
177
+ assertEquals(WireEpisodeTracker.Phase.RECOVERED, recovered!!.phase)
178
+ assertEquals(2_000L, recovered.recoveryMs)
179
+ }
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/acs-meeting",
3
- "version": "3.2.0-dev.235",
3
+ "version": "3.2.0-dev.245",
4
4
  "description": "MentraOS native ACS Teams meeting module (WHEP decode → ACS raw media)",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@expo/config-plugins": "~55.0.10",
35
- "@mentra/glasses-media": "3.2.0-dev.235"
35
+ "@mentra/glasses-media": "3.2.0-dev.245"
36
36
  },
37
37
  "devDependencies": {
38
38
  "expo-module-scripts": "^55.0.2",