@statewalker/webrun-streams-livekit 0.1.1 → 0.2.0
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 +139 -6
- package/dist/connect-serve.d.ts +9 -1
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/index.js +26 -4
- package/package.json +10 -4
- package/src/connect-serve.ts +45 -5
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,16 +1,149 @@
|
|
|
1
1
|
# @statewalker/webrun-streams-livekit
|
|
2
2
|
|
|
3
|
-
LiveKit-backed `Connect` / `Serve` adapter
|
|
3
|
+
LiveKit-backed `Connect` / `Serve` adapter in the `webrun-streams-*` family.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
A binding between a connected LiveKit [`Room`](https://docs.livekit.io/client-sdk-js/classes/Room.html)
|
|
8
|
+
and the [`webrun-streams`](../webrun-streams) `Duplex` seam. A room plus a
|
|
9
|
+
remote participant identity becomes one `ByteChannel`; `emulateMux` layers many
|
|
10
|
+
concurrent logical calls on top.
|
|
11
|
+
|
|
12
|
+
Your handler is an ordinary `Duplex` —
|
|
13
|
+
`(input: AsyncIterable<Uint8Array>) => AsyncGenerator<Uint8Array>` — the same
|
|
14
|
+
function you would run over a WebSocket or WebRTC.
|
|
15
|
+
|
|
16
|
+
## Why it exists
|
|
17
|
+
|
|
18
|
+
Peer-to-peer links are excellent until they aren't: symmetric NATs, corporate
|
|
19
|
+
firewalls, mobile networks and multi-party sessions all argue for a managed
|
|
20
|
+
SFU. LiveKit provides one, with authentication, room membership and presence
|
|
21
|
+
already solved.
|
|
22
|
+
|
|
23
|
+
What LiveKit's data channel does *not* provide is request semantics. This
|
|
24
|
+
adapter adds them, so an application written against the `Duplex` seam can move
|
|
25
|
+
from a direct WebRTC link to a LiveKit room by changing one import — the
|
|
26
|
+
handlers, the HTTP layer above them, and the tests all stay put.
|
|
27
|
+
|
|
28
|
+
Publishes are forced to LiveKit's **`RELIABLE`** mode (ordered, retransmitted),
|
|
29
|
+
because the framing above assumes byte-stream ordering.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install @statewalker/webrun-streams-livekit livekit-client
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`livekit-client` is a **peer dependency** (`^2.18.3`) — you own the `Room`, its
|
|
38
|
+
token, and its lifecycle.
|
|
39
|
+
|
|
40
|
+
## Getting started
|
|
41
|
+
|
|
42
|
+
Both sides take the same parameters: a connected `Room` and the identity of the
|
|
43
|
+
participant at the other end.
|
|
44
|
+
|
|
45
|
+
Responder:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Room } from "livekit-client";
|
|
49
|
+
import { serve } from "@statewalker/webrun-streams-livekit";
|
|
50
|
+
|
|
51
|
+
const room = new Room();
|
|
52
|
+
await room.connect(url, token); // identity: "agent-7"
|
|
53
|
+
|
|
54
|
+
const stop = await serve({ room, peerIdentity: "client-3" }, async function* echo(input) {
|
|
55
|
+
for await (const chunk of input) yield chunk;
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Caller:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { connect } from "@statewalker/webrun-streams-livekit";
|
|
63
|
+
|
|
64
|
+
const { call, close } = await connect({ room, peerIdentity: "agent-7" });
|
|
65
|
+
|
|
66
|
+
for await (const chunk of call([new TextEncoder().encode("ping")])) {
|
|
67
|
+
console.log(new TextDecoder().decode(chunk)); // "ping"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await close();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Carrying HTTP over it
|
|
4
74
|
|
|
5
75
|
```ts
|
|
6
|
-
import {
|
|
76
|
+
import { fetchOverDuplex } from "@statewalker/webrun-http-streams";
|
|
77
|
+
|
|
78
|
+
const response = await fetchOverDuplex(call, new Request("http://peer/api/events"));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
[`apps/livekit-demo`](../../apps/livekit-demo) runs this end to end — a dev
|
|
82
|
+
LiveKit server, a token service, and two browser pages exchanging HTTP and SSE
|
|
83
|
+
through a room.
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
### `connect(params): Promise<{ call, close }>`
|
|
88
|
+
|
|
89
|
+
Type: `Connect<LiveKitParams>`. Each `call(input)` opens a new logical stream
|
|
90
|
+
addressed to `peerIdentity`; `close()` tears them down. It does not disconnect
|
|
91
|
+
the `Room`, which you own.
|
|
92
|
+
|
|
93
|
+
### `serve(params, handler): Promise<() => Promise<void>>`
|
|
94
|
+
|
|
95
|
+
Type: `Serve<LiveKitParams>`. Runs `handler` for inbound streams from
|
|
96
|
+
`peerIdentity`. Returns an idempotent teardown.
|
|
97
|
+
|
|
98
|
+
### `LiveKitParams`
|
|
7
99
|
|
|
8
|
-
|
|
9
|
-
|
|
100
|
+
| Field | Type | Meaning |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `room` | `Room` | An already-connected LiveKit room. |
|
|
103
|
+
| `peerIdentity` | `string` | Identity of the remote participant this side talks to. |
|
|
104
|
+
| `mux` | `EmulateMuxOptions` | Flow-control tuning (`mtu`, `maxStreamBuffer`) forwarded to `emulateMux`. `side` is set by which function you call (`"initiator"` for `connect`, `"responder"` for `serve`) regardless of `mux.side`. **`mtu` defaults to 12 KiB here**, not `emulateMux`'s 64 KiB — see below. |
|
|
105
|
+
|
|
106
|
+
### `byteChannelFromLiveKit(room, peerIdentity): ByteChannel`
|
|
107
|
+
|
|
108
|
+
Wraps a room + peer identity as a `ByteChannel` (`send` / `recv` / `closed` /
|
|
109
|
+
`close`) for driving `emulateMux` yourself.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
### Why the MTU default is lower here
|
|
113
|
+
|
|
114
|
+
A LiveKit reliable data packet is capped at roughly 15 KiB, and a payload over
|
|
115
|
+
that is dropped rather than fragmented. `emulateMux`'s own 64 KiB default
|
|
116
|
+
therefore does not survive this transport, and it fails silently: a 1 MiB body
|
|
117
|
+
arrives as zero bytes and a 10 MiB body never completes, with no error on
|
|
118
|
+
either side. So `connect` and `serve` default `mtu` to 12 KiB — under the cap
|
|
119
|
+
with room for the frame header (`[varint streamId][1-byte type]`).
|
|
120
|
+
|
|
121
|
+
An explicit `mux.mtu` still wins, so a deployment that permits larger packets
|
|
122
|
+
can raise it.
|
|
123
|
+
|
|
124
|
+
## Conformance
|
|
125
|
+
|
|
126
|
+
Conformance is **browser-gated** — it needs a real LiveKit server rather than a
|
|
127
|
+
Node shim:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
pnpm --filter @statewalker/webrun-streams-livekit test:browser
|
|
10
131
|
```
|
|
11
132
|
|
|
12
|
-
|
|
133
|
+
with the `WEBRUN_STREAMS_LIVEKIT_*` environment variables pointing at a running
|
|
134
|
+
server. The package's dev dependencies include `testcontainers` and
|
|
135
|
+
`livekit-server-sdk` for standing one up.
|
|
136
|
+
|
|
137
|
+
## Dependencies
|
|
138
|
+
|
|
139
|
+
| Dependency | Kind | Why |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| [`@statewalker/webrun-streams`](../webrun-streams) | runtime | The `Duplex` / `ByteChannel` seam and `emulateMux`. |
|
|
142
|
+
| `livekit-client` | **peer** (`^2.18.3`) | You supply and own the `Room`. |
|
|
143
|
+
| `livekit-server-sdk`, `testcontainers` | dev | Token minting and a containerised server for conformance. |
|
|
144
|
+
|
|
145
|
+
ESM only (`"type": "module"`).
|
|
13
146
|
|
|
14
147
|
## License
|
|
15
148
|
|
|
16
|
-
MIT
|
|
149
|
+
MIT © statewalker — see [LICENSE](../../LICENSE).
|
package/dist/connect-serve.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { type Connect, type Serve } from "@statewalker/webrun-streams";
|
|
1
|
+
import { type Connect, type EmulateMuxOptions, type Serve } from "@statewalker/webrun-streams";
|
|
2
2
|
import type { Room } from "livekit-client";
|
|
3
3
|
export interface LiveKitParams {
|
|
4
4
|
/** Already-connected LiveKit `Room`. */
|
|
5
5
|
room: Room;
|
|
6
6
|
/** Identity of the remote participant the call addresses. */
|
|
7
7
|
peerIdentity: string;
|
|
8
|
+
/**
|
|
9
|
+
* Flow-control tuning forwarded to `emulateMux` — `mtu` and
|
|
10
|
+
* `maxStreamBuffer`, which is the credit this side advertises. `side` here
|
|
11
|
+
* wins over `mux.side`. Defaults are `emulateMux`'s own; the conformance
|
|
12
|
+
* suite's L6 uses this to run at a window small enough that a sender
|
|
13
|
+
* genuinely stalls.
|
|
14
|
+
*/
|
|
15
|
+
mux?: EmulateMuxOptions;
|
|
8
16
|
}
|
|
9
17
|
export declare const connect: Connect<LiveKitParams>;
|
|
10
18
|
export declare const serve: Serve<LiveKitParams>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAEZ,KAAK,iBAAiB,EAEtB,KAAK,KAAK,EACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAkB3C,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,IAAI,EAAE,IAAI,CAAC;IACX,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,iBAAiB,CAAC;CACzB;AAED,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,aAAa,CAa1C,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,aAAa,CAmBtC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -87,9 +87,27 @@ function byteChannelFromLiveKit(room, peerIdentity) {
|
|
|
87
87
|
}
|
|
88
88
|
//#endregion
|
|
89
89
|
//#region src/connect-serve.ts
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Default `mtu` for this transport.
|
|
92
|
+
*
|
|
93
|
+
* `emulateMux` defaults to 64 KiB, which LiveKit cannot carry: a reliable data
|
|
94
|
+
* packet is capped around 15 KiB, and anything larger is dropped rather than
|
|
95
|
+
* fragmented. The symptom is not an error — a 1 MiB body simply arrives as
|
|
96
|
+
* zero bytes and a 10 MiB body hangs — so the ceiling has to be applied here,
|
|
97
|
+
* where the transport is known, rather than left to every caller.
|
|
98
|
+
*
|
|
99
|
+
* 12 KiB leaves room for the mux frame header (`[varint streamId][1-byte
|
|
100
|
+
* type]`) inside that budget. An explicit `mux.mtu` still wins, so a caller who
|
|
101
|
+
* knows their deployment allows more can raise it.
|
|
102
|
+
*/
|
|
103
|
+
const LIVEKIT_SAFE_MTU = 12288;
|
|
104
|
+
const connect = async ({ room, peerIdentity, mux: muxOpts }) => {
|
|
91
105
|
const channel = byteChannelFromLiveKit(room, peerIdentity);
|
|
92
|
-
const mux = emulateMux(channel, {
|
|
106
|
+
const mux = emulateMux(channel, {
|
|
107
|
+
mtu: LIVEKIT_SAFE_MTU,
|
|
108
|
+
...muxOpts,
|
|
109
|
+
side: "initiator"
|
|
110
|
+
});
|
|
93
111
|
return {
|
|
94
112
|
call: mux.call,
|
|
95
113
|
async close() {
|
|
@@ -97,9 +115,13 @@ const connect = async ({ room, peerIdentity }) => {
|
|
|
97
115
|
}
|
|
98
116
|
};
|
|
99
117
|
};
|
|
100
|
-
const serve = async ({ room, peerIdentity }, handler) => {
|
|
118
|
+
const serve = async ({ room, peerIdentity, mux: muxOpts }, handler) => {
|
|
101
119
|
const channel = byteChannelFromLiveKit(room, peerIdentity);
|
|
102
|
-
const mux = emulateMux(channel, {
|
|
120
|
+
const mux = emulateMux(channel, {
|
|
121
|
+
mtu: LIVEKIT_SAFE_MTU,
|
|
122
|
+
...muxOpts,
|
|
123
|
+
side: "responder"
|
|
124
|
+
});
|
|
103
125
|
const off = mux.serve(handler);
|
|
104
126
|
channel.closed.then(() => mux.close());
|
|
105
127
|
let torn = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-streams-livekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "LiveKit-backed Connect/Serve adapter in the webrun-streams-* family",
|
|
@@ -15,33 +15,39 @@
|
|
|
15
15
|
"url": "git@github.com:statewalker/webrun-wire.git"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
|
-
".":
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
19
22
|
},
|
|
20
23
|
"files": [
|
|
21
24
|
"dist",
|
|
22
25
|
"src"
|
|
23
26
|
],
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"@statewalker/webrun-streams": "0.
|
|
28
|
+
"@statewalker/webrun-streams": "0.2.0"
|
|
26
29
|
},
|
|
27
30
|
"peerDependencies": {
|
|
28
31
|
"livekit-client": "^2.18.3"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@types/node": "^26.2.0",
|
|
35
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
32
36
|
"livekit-client": "^2.18.3",
|
|
33
37
|
"livekit-server-sdk": "^2.10.0",
|
|
38
|
+
"playwright": "^1.62.1",
|
|
34
39
|
"rimraf": "^6.1.3",
|
|
35
40
|
"rolldown": "^1.2.4",
|
|
36
41
|
"testcontainers": "^11.0.0",
|
|
37
42
|
"typescript": "^7.0.2",
|
|
38
43
|
"vitest": "^4.1.10",
|
|
39
|
-
"@statewalker/webrun-streams-conformance": "0.
|
|
44
|
+
"@statewalker/webrun-streams-conformance": "0.2.0"
|
|
40
45
|
},
|
|
41
46
|
"sideEffects": false,
|
|
42
47
|
"publishConfig": {
|
|
43
48
|
"access": "public"
|
|
44
49
|
},
|
|
50
|
+
"types": "./dist/index.d.ts",
|
|
45
51
|
"scripts": {
|
|
46
52
|
"build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
|
|
47
53
|
"test": "vitest run",
|
package/src/connect-serve.ts
CHANGED
|
@@ -1,17 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Connect,
|
|
3
|
+
type Duplex,
|
|
4
|
+
type EmulateMuxOptions,
|
|
5
|
+
emulateMux,
|
|
6
|
+
type Serve,
|
|
7
|
+
} from "@statewalker/webrun-streams";
|
|
2
8
|
import type { Room } from "livekit-client";
|
|
3
9
|
import { byteChannelFromLiveKit } from "./byte-channel.js";
|
|
4
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Default `mtu` for this transport.
|
|
13
|
+
*
|
|
14
|
+
* `emulateMux` defaults to 64 KiB, which LiveKit cannot carry: a reliable data
|
|
15
|
+
* packet is capped around 15 KiB, and anything larger is dropped rather than
|
|
16
|
+
* fragmented. The symptom is not an error — a 1 MiB body simply arrives as
|
|
17
|
+
* zero bytes and a 10 MiB body hangs — so the ceiling has to be applied here,
|
|
18
|
+
* where the transport is known, rather than left to every caller.
|
|
19
|
+
*
|
|
20
|
+
* 12 KiB leaves room for the mux frame header (`[varint streamId][1-byte
|
|
21
|
+
* type]`) inside that budget. An explicit `mux.mtu` still wins, so a caller who
|
|
22
|
+
* knows their deployment allows more can raise it.
|
|
23
|
+
*/
|
|
24
|
+
const LIVEKIT_SAFE_MTU = 12 * 1024;
|
|
25
|
+
|
|
5
26
|
export interface LiveKitParams {
|
|
6
27
|
/** Already-connected LiveKit `Room`. */
|
|
7
28
|
room: Room;
|
|
8
29
|
/** Identity of the remote participant the call addresses. */
|
|
9
30
|
peerIdentity: string;
|
|
31
|
+
/**
|
|
32
|
+
* Flow-control tuning forwarded to `emulateMux` — `mtu` and
|
|
33
|
+
* `maxStreamBuffer`, which is the credit this side advertises. `side` here
|
|
34
|
+
* wins over `mux.side`. Defaults are `emulateMux`'s own; the conformance
|
|
35
|
+
* suite's L6 uses this to run at a window small enough that a sender
|
|
36
|
+
* genuinely stalls.
|
|
37
|
+
*/
|
|
38
|
+
mux?: EmulateMuxOptions;
|
|
10
39
|
}
|
|
11
40
|
|
|
12
|
-
export const connect: Connect<LiveKitParams> = async ({ room, peerIdentity }) => {
|
|
41
|
+
export const connect: Connect<LiveKitParams> = async ({ room, peerIdentity, mux: muxOpts }) => {
|
|
13
42
|
const channel = byteChannelFromLiveKit(room, peerIdentity);
|
|
14
|
-
const mux = emulateMux(channel, {
|
|
43
|
+
const mux = emulateMux(channel, {
|
|
44
|
+
mtu: LIVEKIT_SAFE_MTU,
|
|
45
|
+
...muxOpts,
|
|
46
|
+
side: "initiator",
|
|
47
|
+
});
|
|
15
48
|
return {
|
|
16
49
|
call: mux.call,
|
|
17
50
|
async close() {
|
|
@@ -20,9 +53,16 @@ export const connect: Connect<LiveKitParams> = async ({ room, peerIdentity }) =>
|
|
|
20
53
|
};
|
|
21
54
|
};
|
|
22
55
|
|
|
23
|
-
export const serve: Serve<LiveKitParams> = async (
|
|
56
|
+
export const serve: Serve<LiveKitParams> = async (
|
|
57
|
+
{ room, peerIdentity, mux: muxOpts },
|
|
58
|
+
handler: Duplex,
|
|
59
|
+
) => {
|
|
24
60
|
const channel = byteChannelFromLiveKit(room, peerIdentity);
|
|
25
|
-
const mux = emulateMux(channel, {
|
|
61
|
+
const mux = emulateMux(channel, {
|
|
62
|
+
mtu: LIVEKIT_SAFE_MTU,
|
|
63
|
+
...muxOpts,
|
|
64
|
+
side: "responder",
|
|
65
|
+
});
|
|
26
66
|
const off = mux.serve(handler);
|
|
27
67
|
void channel.closed.then(() => mux.close());
|
|
28
68
|
let torn = false;
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2022-2026 statewalker
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|