@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,356 +0,0 @@
1
- package com.mentra.acsmeeting.source
2
-
3
- import com.mentra.acsmeeting.source.WhipIngestProtocol.Action
4
- import com.mentra.acsmeeting.source.WhipIngestProtocol.Endpoint
5
- import com.mentra.acsmeeting.source.WhipIngestProtocol.Request
6
- import com.mentra.acsmeeting.source.WhipIngestProtocol.State
7
- import org.assertj.core.api.Assertions.assertThat
8
- import org.junit.Test
9
-
10
- /**
11
- * The endpoint's decision table.
12
- *
13
- * These are the cases that are painful to provoke against a live device and cheap to state here: a
14
- * duplicate POST during a gather, a DELETE against an id that has already been replaced, a POST
15
- * that lands after teardown. Each one has a specific status the glasses' WHIP client reacts to
16
- * differently, so the statuses are pinned rather than merely "not 200".
17
- */
18
- class WhipIngestProtocolTest {
19
-
20
- private val endpoint = Endpoint("192.168.43.20", 8790)
21
- private val free = State()
22
- private val offer = "v=0\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\n"
23
-
24
- private fun post(
25
- body: String = offer,
26
- contentType: String? = "application/sdp",
27
- length: Long? = body.length.toLong(),
28
- target: String = "/whip",
29
- ) = Request("POST", target, contentType, length, body)
30
-
31
- // -----------------------------------------------------------------
32
- // The happy path
33
- // -----------------------------------------------------------------
34
-
35
- @Test
36
- fun `a valid offer on a free endpoint negotiates`() {
37
- val action = WhipIngestProtocol.decide(post(), free, "sess-1")
38
-
39
- assertThat(action).isInstanceOf(Action.Negotiate::class.java)
40
- val negotiate = action as Action.Negotiate
41
- assertThat(negotiate.sessionId).isEqualTo("sess-1")
42
- assertThat(negotiate.offer).isEqualTo(offer)
43
- }
44
-
45
- @Test
46
- fun `a lowercase method still negotiates`() {
47
- // HTTP methods are case-sensitive in the spec, but accepting the variant costs nothing and
48
- // failing it would present as an unexplained 405 on device.
49
- val action = WhipIngestProtocol.decide(post().copy(method = "post"), free, "sess-1")
50
-
51
- assertThat(action).isInstanceOf(Action.Negotiate::class.java)
52
- }
53
-
54
- @Test
55
- fun `a query string does not hide the base path`() {
56
- val action = WhipIngestProtocol.decide(post(target = "/whip?x=1"), free, "sess-1")
57
-
58
- assertThat(action).isInstanceOf(Action.Negotiate::class.java)
59
- }
60
-
61
- @Test
62
- fun `a trailing slash is the same resource`() {
63
- val action = WhipIngestProtocol.decide(post(target = "/whip/"), free, "sess-1")
64
-
65
- assertThat(action).isInstanceOf(Action.Negotiate::class.java)
66
- }
67
-
68
- @Test
69
- fun `a content type with parameters is still sdp`() {
70
- val action =
71
- WhipIngestProtocol.decide(post(contentType = "application/SDP; charset=utf-8"), free, "s")
72
-
73
- assertThat(action).isInstanceOf(Action.Negotiate::class.java)
74
- }
75
-
76
- // -----------------------------------------------------------------
77
- // 201 carries the absolute Location the client will DELETE
78
- // -----------------------------------------------------------------
79
-
80
- /**
81
- * Absolute, not `/whip/<id>`. WHIP terminations go to the URL in `Location`, and the glasses'
82
- * client has not been verified to resolve a relative one against the request URL.
83
- */
84
- @Test
85
- fun `201 returns an absolute location and the answer`() {
86
- val response = WhipIngestProtocol.respondNegotiated(endpoint, "sess-1", "v=0 answer")
87
-
88
- assertThat(response.status).isEqualTo(201)
89
- assertThat(response.headers["Location"]).isEqualTo("http://192.168.43.20:8790/whip/sess-1")
90
- assertThat(response.headers["Content-Type"]).isEqualTo("application/sdp")
91
- assertThat(response.body).isEqualTo("v=0 answer")
92
- }
93
-
94
- @Test
95
- fun `the location url is what a delete is accepted on`() {
96
- val sessionUrl = endpoint.sessionUrl("sess-1")
97
- val path = sessionUrl.removePrefix("http://192.168.43.20:8790")
98
-
99
- val action = WhipIngestProtocol.decide(
100
- Request("DELETE", path),
101
- State(activeSessionId = "sess-1"),
102
- "unused",
103
- )
104
-
105
- assertThat(action).isEqualTo(Action.Terminate("sess-1"))
106
- }
107
-
108
- // -----------------------------------------------------------------
109
- // One publisher
110
- // -----------------------------------------------------------------
111
-
112
- /**
113
- * A second POST usually means the glasses retried a request whose reply they never saw. Replacing
114
- * a live peer with a speculative one would drop a working call, so the live session wins.
115
- */
116
- @Test
117
- fun `a second offer while publishing conflicts and keeps the live session`() {
118
- val action = WhipIngestProtocol.decide(post(), State(activeSessionId = "sess-1"), "sess-2")
119
-
120
- val response = (action as Action.Reply).response
121
- assertThat(response.status).isEqualTo(409)
122
- assertThat(response.headers["Location"]).isEqualTo("sess-1")
123
- assertThat(response.body).contains("sess-1")
124
- }
125
-
126
- // -----------------------------------------------------------------
127
- // Termination
128
- // -----------------------------------------------------------------
129
-
130
- @Test
131
- fun `deleting the active session terminates it`() {
132
- val action =
133
- WhipIngestProtocol.decide(Request("DELETE", "/whip/sess-1"), State("sess-1"), "unused")
134
-
135
- assertThat(action).isEqualTo(Action.Terminate("sess-1"))
136
- }
137
-
138
- /**
139
- * A stale DELETE must not tear down whoever holds the slot now. This is the rejoin race: the
140
- * glasses DELETE an old session while a new one is already publishing.
141
- */
142
- @Test
143
- fun `deleting a stale session is a 404 and spares the current publisher`() {
144
- val action =
145
- WhipIngestProtocol.decide(Request("DELETE", "/whip/old"), State("sess-2"), "unused")
146
-
147
- assertThat((action as Action.Reply).response.status).isEqualTo(404)
148
- }
149
-
150
- @Test
151
- fun `deleting when nothing is active is a 404`() {
152
- val action = WhipIngestProtocol.decide(Request("DELETE", "/whip/sess-1"), free, "unused")
153
-
154
- assertThat((action as Action.Reply).response.status).isEqualTo(404)
155
- }
156
-
157
- @Test
158
- fun `termination returns no content`() {
159
- assertThat(WhipIngestProtocol.terminated().status).isEqualTo(204)
160
- assertThat(WhipIngestProtocol.terminated().body).isEmpty()
161
- }
162
-
163
- // -----------------------------------------------------------------
164
- // The tombstone
165
- // -----------------------------------------------------------------
166
-
167
- /**
168
- * After stop, a well-formed POST is still refused. Granting it a session would attach a publisher
169
- * to an endpoint whose peer and scoped network are already gone.
170
- */
171
- @Test
172
- fun `a well formed offer after stop is gone, not created`() {
173
- val action = WhipIngestProtocol.decide(post(), State(stopped = true), "sess-1")
174
-
175
- assertThat((action as Action.Reply).response.status).isEqualTo(410)
176
- }
177
-
178
- @Test
179
- fun `a delete after stop is gone rather than 404`() {
180
- val action = WhipIngestProtocol.decide(
181
- Request("DELETE", "/whip/sess-1"),
182
- State(activeSessionId = "sess-1", stopped = true),
183
- "unused",
184
- )
185
-
186
- assertThat((action as Action.Reply).response.status).isEqualTo(410)
187
- }
188
-
189
- // -----------------------------------------------------------------
190
- // Malformed requests
191
- // -----------------------------------------------------------------
192
-
193
- @Test
194
- fun `a non post on the base path is 405 and advertises post`() {
195
- val action = WhipIngestProtocol.decide(Request("GET", "/whip"), free, "sess-1")
196
-
197
- val response = (action as Action.Reply).response
198
- assertThat(response.status).isEqualTo(405)
199
- assertThat(response.headers["Allow"]).isEqualTo("POST")
200
- }
201
-
202
- @Test
203
- fun `a non delete on the session path is 405 and advertises delete`() {
204
- val action = WhipIngestProtocol.decide(Request("GET", "/whip/sess-1"), State("sess-1"), "x")
205
-
206
- val response = (action as Action.Reply).response
207
- assertThat(response.status).isEqualTo(405)
208
- assertThat(response.headers["Allow"]).isEqualTo("DELETE")
209
- }
210
-
211
- @Test
212
- fun `a non sdp content type is 415`() {
213
- val action = WhipIngestProtocol.decide(post(contentType = "application/json"), free, "s")
214
-
215
- assertThat((action as Action.Reply).response.status).isEqualTo(415)
216
- }
217
-
218
- @Test
219
- fun `a missing content type is 415`() {
220
- val action = WhipIngestProtocol.decide(post(contentType = null), free, "s")
221
-
222
- assertThat((action as Action.Reply).response.status).isEqualTo(415)
223
- }
224
-
225
- /** Length is checked before type, because an oversized body is never read and so never typed. */
226
- @Test
227
- fun `an oversized offer is 413 even before the type is considered`() {
228
- val action = WhipIngestProtocol.decide(
229
- post(contentType = "application/json", length = WhipIngestProtocol.MAX_BODY_BYTES + 1),
230
- free,
231
- "s",
232
- )
233
-
234
- assertThat((action as Action.Reply).response.status).isEqualTo(413)
235
- }
236
-
237
- @Test
238
- fun `an offer exactly at the cap is accepted`() {
239
- val action = WhipIngestProtocol.decide(
240
- post(length = WhipIngestProtocol.MAX_BODY_BYTES),
241
- free,
242
- "s",
243
- )
244
-
245
- assertThat(action).isInstanceOf(Action.Negotiate::class.java)
246
- }
247
-
248
- @Test
249
- fun `an empty offer body is 400`() {
250
- val action = WhipIngestProtocol.decide(post(body = " ", length = 3), free, "s")
251
-
252
- assertThat((action as Action.Reply).response.status).isEqualTo(400)
253
- }
254
-
255
- @Test
256
- fun `an unrelated path is 404`() {
257
- val action = WhipIngestProtocol.decide(post(target = "/health"), free, "s")
258
-
259
- assertThat((action as Action.Reply).response.status).isEqualTo(404)
260
- }
261
-
262
- @Test
263
- fun `a nested session path is not a session id`() {
264
- val action =
265
- WhipIngestProtocol.decide(Request("DELETE", "/whip/a/b"), State("a"), "unused")
266
-
267
- assertThat((action as Action.Reply).response.status).isEqualTo(404)
268
- }
269
-
270
- @Test
271
- fun `negotiation failure is a 500, not a client error`() {
272
- // The publisher did nothing wrong and a retry may succeed, so this must not read as 4xx.
273
- val response = WhipIngestProtocol.respondNegotiationFailed("gather_timeout")
274
-
275
- assertThat(response.status).isEqualTo(500)
276
- assertThat(response.body).isEqualTo("gather_timeout")
277
- }
278
-
279
- // -----------------------------------------------------------------
280
- // Wire rendering
281
- // -----------------------------------------------------------------
282
-
283
- /**
284
- * A missing `Content-Length` turns a clean refusal into a hang: the client waits for a body that
285
- * never ends, then retries on timeout, which is exactly what the tombstone is meant to prevent.
286
- */
287
- @Test
288
- fun `every response carries a content length and closes the connection`() {
289
- val rendered = WhipIngestProtocol.render(WhipIngestProtocol.terminated())
290
-
291
- assertThat(rendered).startsWith("HTTP/1.1 204 No Content\r\n")
292
- assertThat(rendered).contains("Content-Length: 0\r\n")
293
- assertThat(rendered).contains("Connection: close\r\n")
294
- assertThat(rendered).endsWith("\r\n\r\n")
295
- }
296
-
297
- @Test
298
- fun `the rendered length counts utf8 bytes rather than characters`() {
299
- val rendered = WhipIngestProtocol.render(
300
- WhipIngestProtocol.Response(500, "Internal Server Error", body = "café"),
301
- )
302
-
303
- assertThat(rendered).contains("Content-Length: 5\r\n")
304
- }
305
-
306
- @Test
307
- fun `an sdp answer keeps its own content type`() {
308
- val rendered =
309
- WhipIngestProtocol.render(WhipIngestProtocol.respondNegotiated(endpoint, "s", "v=0"))
310
-
311
- assertThat(rendered).contains("Content-Type: application/sdp\r\n")
312
- assertThat(rendered).doesNotContain("text/plain")
313
- assertThat(rendered).endsWith("\r\n\r\nv=0")
314
- }
315
-
316
- @Test
317
- fun `a plain text body gets a default content type`() {
318
- val rendered =
319
- WhipIngestProtocol.render(WhipIngestProtocol.Response(410, "Gone", body = "stopped"))
320
-
321
- assertThat(rendered).contains("Content-Type: text/plain; charset=utf-8\r\n")
322
- }
323
-
324
- // -----------------------------------------------------------------
325
- // Request line and header parsing
326
- // -----------------------------------------------------------------
327
-
328
- @Test
329
- fun `a request line parses into method and target`() {
330
- assertThat(WhipIngestProtocol.parseRequestLine("POST /whip HTTP/1.1"))
331
- .isEqualTo("POST" to "/whip")
332
- }
333
-
334
- @Test
335
- fun `a request line with extra spacing still parses`() {
336
- assertThat(WhipIngestProtocol.parseRequestLine(" DELETE /whip/s1 HTTP/1.1 "))
337
- .isEqualTo("DELETE" to "/whip/s1")
338
- }
339
-
340
- @Test
341
- fun `an absolute or garbage request target is rejected`() {
342
- // Only origin-form targets are accepted; anything else would bypass the path routing above.
343
- assertThat(WhipIngestProtocol.parseRequestLine("POST http://host/whip HTTP/1.1")).isNull()
344
- assertThat(WhipIngestProtocol.parseRequestLine("garbage")).isNull()
345
- assertThat(WhipIngestProtocol.parseRequestLine("")).isNull()
346
- }
347
-
348
- @Test
349
- fun `header lookup ignores case as clients assume`() {
350
- val headers = mapOf("content-TYPE" to "application/sdp", "Content-Length" to "12")
351
-
352
- assertThat(WhipIngestProtocol.headerValue(headers, "Content-Type")).isEqualTo("application/sdp")
353
- assertThat(WhipIngestProtocol.headerValue(headers, "CONTENT-LENGTH")).isEqualTo("12")
354
- assertThat(WhipIngestProtocol.headerValue(headers, "Missing")).isNull()
355
- }
356
- }