@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.
- package/README.md +14 -3
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +10 -10
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +37 -37
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +4 -4
- package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +3 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +12 -10
- package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +2 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +2 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/{AcsInvestigationTest.kt → MediaDiagnosticsTest.kt} +10 -10
- package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +3 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +2 -2
- package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +2 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +2 -2
- package/build/AcsMeeting.types.d.ts +10 -1
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +2 -2
- package/build/AcsMeetingModule.d.ts.map +1 -1
- package/build/AcsMeetingModule.js.map +1 -1
- package/ios/AcsMeeting.podspec +1 -1
- package/ios/AcsMeetingModule.swift +284 -148
- package/ios/PolicyKit/Package.swift +2 -1
- package/ios/PolicyKit/Sources/AcsAudioPolicy/CallbackOperation.swift +60 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/CapturePolicy.swift +5 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +1 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/CallAgentRetirementTests.swift +113 -0
- package/package.json +3 -2
- package/src/AcsMeeting.types.ts +4 -1
- package/src/AcsMeetingModule.ts +2 -2
- package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +0 -291
- package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +0 -501
- package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +0 -51
- package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +0 -81
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +0 -355
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +0 -109
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +0 -160
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +0 -103
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +0 -53
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +0 -127
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +0 -573
- package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +0 -99
- package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +0 -487
- package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +0 -147
- package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +0 -45
- package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +0 -143
- package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +0 -772
- package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +0 -57
- package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +0 -17
- package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +0 -139
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +0 -144
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +0 -46
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +0 -160
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +0 -205
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +0 -214
- package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +0 -33
- package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +0 -110
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +0 -207
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +0 -356
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +0 -155
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +0 -78
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +0 -372
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +0 -79
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +0 -15
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +0 -48
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +0 -178
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +0 -145
- package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +0 -143
- package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +0 -22
- package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +0 -72
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +0 -102
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +0 -148
- package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +0 -59
- package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +0 -59
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +0 -35
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +0 -165
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +0 -96
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +0 -187
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +0 -172
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +0 -141
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +0 -69
- package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +0 -22
- package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +0 -41
- package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +0 -60
- package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +0 -151
- package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +0 -67
- package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +0 -165
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +0 -148
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +0 -76
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +0 -275
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +0 -193
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +0 -176
- package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +0 -45
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +0 -356
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +0 -411
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +0 -83
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +0 -61
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +0 -322
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +0 -18
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +0 -49
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +0 -149
- package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +0 -97
- package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +0 -33
- package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +0 -126
- package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +0 -68
- 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
|
-
}
|