@mertushka/webrtc-node 0.1.0-alpha.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.
@@ -0,0 +1,38 @@
1
+ # Architecture
2
+
3
+ The package is intentionally split between a thin native layer and a
4
+ browser-compatible JavaScript facade.
5
+
6
+ ## Native Addon
7
+
8
+ `src/native/addon.cc` owns `libdatachannel` handles and exposes a small
9
+ Node-API surface to JavaScript. It must remain ABI-stable and must not use
10
+ direct V8 or NAN APIs.
11
+
12
+ Native callbacks never call JavaScript directly from `libdatachannel` callback
13
+ threads. They dispatch back to Node through a thread-safe function.
14
+
15
+ ## JavaScript Facade
16
+
17
+ `lib/index.js` implements the W3C-facing behavior:
18
+
19
+ - WebIDL-style conversions
20
+ - DOM-style `EventTarget` behavior
21
+ - promise timing and operations-chain behavior
22
+ - WebRTC state mapping
23
+ - `DOMException`-style errors
24
+ - data-channel message, open, close, and buffered amount semantics
25
+
26
+ Keep browser-compatible behavior in JavaScript unless native behavior is
27
+ required for correctness.
28
+
29
+ ## Type Declarations
30
+
31
+ `index.d.ts` describes the public API. Any runtime API change must update the
32
+ declarations and `scripts/check-api-surface.js` as needed.
33
+
34
+ ## Scope Boundary
35
+
36
+ The public scope is `RTCPeerConnection` plus `RTCDataChannel` for the WebRTC
37
+ data-channel profile. Media tracks, transceivers, RTP sender/receiver APIs,
38
+ stats, DTMF, and capture devices are not implemented.
@@ -0,0 +1,53 @@
1
+ # Conformance
2
+
3
+ The compatibility target is the selected WPT subset in `wpt-manifest.json`.
4
+ This project targets the WebRTC data-channel profile exposed through
5
+ `RTCPeerConnection` and `RTCDataChannel`.
6
+
7
+ ## Selected Scope
8
+
9
+ Expected-pass coverage currently includes:
10
+
11
+ - `RTCPeerConnection` construction, descriptions, signaling state, ICE state,
12
+ ICE candidates, and data-channel negotiation
13
+ - `RTCDataChannel` construction, id assignment, negotiated channels, ready
14
+ state, open/message/close/error behavior, send variants, binary type, and
15
+ buffered amount behavior
16
+ - WebRTC-shaped constructors and events such as `RTCSessionDescription`,
17
+ `RTCIceCandidate`, `RTCDataChannelEvent`, and ICE events
18
+
19
+ Out-of-scope WPT areas are grouped in the manifest as `notApplicable`,
20
+ `needsShim`, or `expectedFail`. Media and RTP APIs are outside this package's
21
+ public scope.
22
+
23
+ ## Running WPT
24
+
25
+ ```sh
26
+ npm run wpt:ensure
27
+ npm run wpt:selection:check
28
+ npm run wpt:test
29
+ npm run wpt:check:strict
30
+ npm run wpt:report -- --output wpt-report.md
31
+ ```
32
+
33
+ `wpt:test` writes `wpt-results.json`. `wpt:check:strict` requires every selected
34
+ subtest to pass and fails if a worker retry was needed.
35
+
36
+ ## CI Evidence
37
+
38
+ Each CI matrix job writes:
39
+
40
+ - `ci-evidence.json`
41
+ - `wpt-results.json`
42
+ - `wpt-report.md`
43
+ - `wpt-manifest.json`
44
+ - `wpt-manifest.txt`
45
+
46
+ After downloading CI artifacts into `ci-artifacts/`, validate the full matrix:
47
+
48
+ ```sh
49
+ npm run ci:evidence:check -- --artifacts ci-artifacts
50
+ ```
51
+
52
+ The verifier checks Linux, macOS, and Windows artifacts across Node 20, 22, and
53
+ 24. It rejects missing jobs, pin mismatches, WPT failures, and WPT retries.
@@ -0,0 +1,161 @@
1
+ # Development
2
+
3
+ This project builds a Node-API addon and a W3C-style JavaScript facade. Use the
4
+ lockfile and keep generated artifacts out of commits.
5
+
6
+ ## Setup
7
+
8
+ ```sh
9
+ npm ci
10
+ npm run native:check
11
+ npm run build
12
+ ```
13
+
14
+ `native:check` verifies the pinned `libdatachannel` commit, Node-API usage, and
15
+ thread-safe callback dispatch. The build uses `cmake-js` and links
16
+ `LibDataChannel::LibDataChannelStatic`.
17
+
18
+ If a local `libdatachannel/` checkout exists, CMake verifies it against the
19
+ pinned commit. Otherwise it fetches the pinned upstream commit with
20
+ `FetchContent`.
21
+
22
+ ## Local Checks
23
+
24
+ ```sh
25
+ npm run check
26
+ npm test
27
+ npm run api:check
28
+ npm run types:check
29
+ npm run pack:check
30
+ npm run wpt:selection:check
31
+ npm run wpt:smoke
32
+ npm run wpt:smoke:check
33
+ ```
34
+
35
+ Run the full selected WPT suite before claiming conformance changes:
36
+
37
+ ```sh
38
+ npm run wpt:ensure
39
+ npm run wpt:test
40
+ npm run wpt:check:strict
41
+ npm run wpt:report -- --output wpt-report.md
42
+ ```
43
+
44
+ The default `CI` workflow keeps pull requests fast by running the native
45
+ OS/Node matrix with unit tests plus a small WPT smoke subset on Ubuntu Node 24.
46
+ The full selected WPT matrix is in the `Conformance` workflow, which runs on
47
+ manual dispatch, weekly schedule, and version tags.
48
+
49
+ Only source-relevant changes run the native matrix, package-artifact source
50
+ build, and WPT smoke job. Source-relevant paths include `lib/`, `src/`,
51
+ `scripts/`, `test/`, `examples/`, `build-containers/`, `CMakeLists.txt`,
52
+ package files, TypeScript/API config, Biome config, and `wpt-manifest.json`.
53
+ Documentation, agent notes, and workflow-only maintenance changes run the
54
+ lightweight quality gate only.
55
+
56
+ By default, WPT is fetched into the ignored `wpt/` cache. Set `WPT_DIR` to use a
57
+ different pinned checkout.
58
+
59
+ For focused debugging:
60
+
61
+ ```sh
62
+ npm run wpt:test -- webrtc/RTCDataChannel-close.html
63
+ npm run wpt:test -- "webrtc/RTCDataChannel-send.html#Sending in ondatachannel should work"
64
+ ```
65
+
66
+ Bare file targets use manifest include/exclude rules. A `file#subtest` selector
67
+ runs one exact WPT subtest.
68
+
69
+ ## Formatting and Linting
70
+
71
+ Biome is used for JavaScript, TypeScript, and JSON formatting/linting:
72
+
73
+ ```sh
74
+ npm run check
75
+ npm run lint
76
+ npm run format:check
77
+ npm run format
78
+ ```
79
+
80
+ `npm run check` is the Biome gate used by GitHub Actions. The full Quality job
81
+ also runs `npm run types:check` and `npm run pack:check`. WPT selection checks
82
+ run after the native addon is built because the WPT harness loads the public
83
+ WebRTC facade. `pack:check` validates the npm source artifact contents. Use
84
+ `npm run format` before sending a pull request.
85
+
86
+ ## Package Artifact
87
+
88
+ CI has a Linux `Package artifact` job that packs the npm source package,
89
+ extracts it in a clean directory, installs dependencies, builds the native
90
+ addon, and requires the package. This guards against missing files in
91
+ `package.json#files` before npm publication.
92
+
93
+ ## Prebuilt Releases
94
+
95
+ The release workflow keeps `cmake-js` as the native build backend. Platform jobs
96
+ build `build/Release/webrtc_node.node`, then `npm run prebuild:package` creates
97
+ `prebuild-artifacts/webrtc-node-v<version>-napi-v8-<target>.tar.gz`. The npm
98
+ publish job downloads those artifacts, verifies the expected prebuild set,
99
+ uploads them to the GitHub Release, runs `pack:check`, and publishes the source
100
+ package. Prebuilds are not bundled inside the npm tarball.
101
+
102
+ Publishing uses npm trusted publishing with GitHub Actions OIDC, not an
103
+ `NPM_TOKEN` secret. Configure the npm package trusted publisher for repository
104
+ `mertushka/webrtc-node`, workflow filename `release.yml`, and environment `npm`
105
+ if the GitHub release environment is kept.
106
+
107
+ Manual `workflow_dispatch` releases expect a GitHub Release named
108
+ `v<package.json version>` to already exist, because prebuilt archives are
109
+ uploaded as release assets before `npm publish` runs.
110
+
111
+ Supported release targets are:
112
+
113
+ - `linux-x64-glibc`
114
+ - `linux-x64-musl`
115
+ - `darwin-x64`
116
+ - `darwin-arm64`
117
+ - `win32-x64`
118
+
119
+ Use `WEBRTC_NODE_NATIVE_PATH=/absolute/path/to/webrtc_node.node` to test a
120
+ specific local native binary. Use `npm install --build-from-source` to force the
121
+ install script to compile with `cmake-js`.
122
+
123
+ ## Docker Linux Slice
124
+
125
+ GitHub Actions is the authoritative conformance gate. The Docker helpers are
126
+ optional local reproducers for Linux CI behavior when a contributor has Docker
127
+ available.
128
+
129
+ On Linux or macOS:
130
+
131
+ ```sh
132
+ bash scripts/run-docker-linux-ci.sh --node-image node:20-bookworm --artifacts-dir ci-artifacts/docker-linux-node20
133
+ bash scripts/run-docker-linux-ci.sh --node-image node:22-bookworm --artifacts-dir ci-artifacts/docker-linux-node22
134
+ bash scripts/run-docker-linux-ci.sh --node-image node:24-bookworm --artifacts-dir ci-artifacts/docker-linux-node24
135
+ ```
136
+
137
+ On Windows with Docker Desktop:
138
+
139
+ ```powershell
140
+ ./scripts/run-docker-linux-ci.ps1 -NodeImage node:20-bookworm -ArtifactsDir ci-artifacts/docker-linux-node20
141
+ ./scripts/run-docker-linux-ci.ps1 -NodeImage node:22-bookworm -ArtifactsDir ci-artifacts/docker-linux-node22
142
+ ./scripts/run-docker-linux-ci.ps1 -NodeImage node:24-bookworm -ArtifactsDir ci-artifacts/docker-linux-node24
143
+ ```
144
+
145
+ Target a single WPT case:
146
+
147
+ ```sh
148
+ bash scripts/run-docker-linux-ci.sh \
149
+ --node-image node:24-bookworm \
150
+ --artifacts-dir ci-artifacts/docker-linux-node24-close \
151
+ --wpt-selector "webrtc/RTCDataChannel-close.html#Repeated open/send/echo/close datachannel works"
152
+ ```
153
+
154
+ ```powershell
155
+ ./scripts/run-docker-linux-ci.ps1 -NodeImage node:24-bookworm `
156
+ -ArtifactsDir ci-artifacts/docker-linux-node24-close `
157
+ -WptSelector "webrtc/RTCDataChannel-close.html#Repeated open/send/echo/close datachannel works"
158
+ ```
159
+
160
+ The helper writes logs and WPT artifacts under the selected `ci-artifacts/`
161
+ directory.
@@ -0,0 +1,230 @@
1
+ # Intentional W3C Divergences
2
+
3
+ This package targets W3C-style `RTCPeerConnection` and `RTCDataChannel`
4
+ behavior for Node.js. It intentionally does not expose browser media APIs. The
5
+ current implementation diverges from W3C WebRTC in the following places.
6
+
7
+ ## Local SDP application
8
+
9
+ `libdatachannel` generates local SDP inside `setLocalDescription()`. It does not
10
+ provide a public API that applies arbitrary caller-supplied local SDP. The JS
11
+ facade accepts `setLocalDescription(description)` for browser compatibility, but
12
+ currently uses the description type and lets libdatachannel generate the actual
13
+ local SDP.
14
+
15
+ Impact: WPT cases that mutate local SDP between `createOffer()` and
16
+ `setLocalDescription()` are expected to fail until the binding grows a validated
17
+ SDP-munging strategy or native support.
18
+
19
+ ## Media APIs
20
+
21
+ This package intentionally excludes media tracks, transceivers, RTP
22
+ senders/receivers, stats, DTMF, capture devices, and `ontrack`. libdatachannel
23
+ has RTP/SRTP track primitives, but they do not map directly to the exposed
24
+ `RTCPeerConnection` plus `RTCDataChannel` scope.
25
+
26
+ Impact: media-oriented WPT files are marked not applicable in
27
+ `wpt-manifest.json`.
28
+
29
+ The selected `historical.html` subset excludes only the legacy
30
+ `RTCRtpTransceiver` member check because `RTCRtpTransceiver` is not exposed.
31
+
32
+ ## RTCConfiguration
33
+
34
+ The JS facade validates and stores W3C-shaped `RTCConfiguration` dictionaries,
35
+ including `iceServers`, `iceTransportPolicy`, `bundlePolicy`, `rtcpMuxPolicy`,
36
+ `iceCandidatePoolSize`, and `certificates`. `getConfiguration()` returns a
37
+ clone of that JS-visible configuration and `setConfiguration()` applies W3C
38
+ validation and immutable-field checks.
39
+
40
+ libdatachannel consumes ICE servers and transport policy at peer-connection
41
+ construction time. The current binding does not reconfigure the native ICE
42
+ transport after construction, so `setConfiguration()` is a W3C-compatible facade
43
+ operation for validation and JS-visible state rather than a native ICE restart
44
+ or transport reconfiguration mechanism.
45
+
46
+ Impact: the selected WPT suite covers constructor, `getConfiguration()`, and
47
+ `setConfiguration()` validation for ICE servers, ICE candidate pool size,
48
+ certificates, bundle policy, RTCP mux policy, and ICE transport policy. Media
49
+ candidate-gathering and media SDP RTCP-mux validation cases remain outside the
50
+ expected-pass set.
51
+
52
+ ## Certificates
53
+
54
+ `RTCPeerConnection.generateCertificate()` and `RTCCertificate` are implemented
55
+ as native-backed WebRTC objects with generated PEM/key material, expiration,
56
+ and fingerprint accessors. `RTCPeerConnection` validates expired certificates,
57
+ rejects `setConfiguration()` calls that change the certificate set, and passes
58
+ the first configured certificate into libdatachannel's `rtc::Configuration` so
59
+ the generated DTLS fingerprint appears in local SDP.
60
+
61
+ libdatachannel accepts one certificate/key pair for a peer connection. The W3C
62
+ configuration dictionary allows a sequence of certificates, and WPT has a case
63
+ that expects all configured certificate fingerprints to appear in SDP. The
64
+ current binding preserves the JS-visible certificate set for W3C
65
+ `getConfiguration()`/`setConfiguration()` behavior but uses only the first
66
+ certificate for native DTLS identity.
67
+
68
+ Impact: the selected WPT suite covers certificate generation, unsupported
69
+ algorithm rejection, expiration validation, fingerprint object shape, and
70
+ certificate immutability in `setConfiguration()`, plus single-certificate SDP
71
+ fingerprint generation. The multi-certificate SDP fingerprint case remains
72
+ outside expected-pass until the binding has a defensible multi-certificate
73
+ strategy.
74
+
75
+ ## Transport object surface
76
+
77
+ `RTCPeerConnection.sctp` is implemented as a data-channel-backed
78
+ `RTCSctpTransport` facade, with a minimal `RTCDtlsTransport` object available at
79
+ `sctp.transport`. The facade is created only when SDP contains an
80
+ `m=application` data section. It tracks `connecting`, `connected`, and `closed`
81
+ from peer/data-channel state, exposes same-process remote certificates through
82
+ `getRemoteCertificates()` after connection, and exposes negotiated
83
+ `maxMessageSize` and post-connect `maxChannels` where the current native
84
+ binding can support them.
85
+ As in browsers, these transport facade objects are exposed from
86
+ `RTCPeerConnection.sctp` and related attributes rather than being publicly
87
+ constructible. The data-channel DTLS facade reports `"new"` until both local and
88
+ remote descriptions are present, then `"connecting"` until the data transport is
89
+ connected.
90
+
91
+ Impact: the selected WPT suite covers SCTP transport creation, state events,
92
+ `maxChannels`, and `maxMessageSize`. A minimal `RTCIceTransport` facade is
93
+ available at `sctp.transport.iceTransport`; it exposes ICE role, component,
94
+ state, gathering state, candidate accessors, selected candidate pair, and ICE
95
+ parameters from the data-channel peer-connection state.
96
+ `getSelectedCandidatePair()` returns a WebIDL-shaped `RTCIceCandidatePair`
97
+ object, not a plain object, when libdatachannel exposes a connected local/remote
98
+ candidate pair.
99
+
100
+ Impact: the selected WPT suite covers connected data-channel selected candidate
101
+ pairs, unconnected SCTP ICE transport behavior, data-channel disconnected
102
+ transport behavior after peer close, ICE restart transport-object identity for
103
+ data channels, and data-channel SCTP ICE transport gathering-state and
104
+ connected-state consistency. The pinned `RTCIceTransport.html?rest`
105
+ candidate-pair test has a runner shim for its `"completed"`/`"complete"`
106
+ gathering-state typo; the runtime keeps the WebRTC enum value `"complete"`.
107
+ Disconnected timing and media transport graph cases remain outside the current
108
+ expected-pass set.
109
+
110
+ ## Data channel closing
111
+
112
+ libdatachannel does not expose a native `closing` ready state. The JS facade
113
+ synthesizes `readyState === "closing"` and a `closing` event when
114
+ `RTCDataChannel.close()` is called. If application data was queued in the same
115
+ task, the facade marks the channel as closing immediately but briefly delays the
116
+ native SCTP close so the just-queued message can be delivered before reset.
117
+
118
+ Impact: local close behavior is browser-shaped in the JS layer. The current WPT
119
+ runner passes `RTCDataChannel-close.html`, including remote `closing`, repeated
120
+ open/send/close, and peer-close `RTCErrorEvent` cases.
121
+
122
+ For same-process peers that have exchanged SDP through this facade, the JS layer
123
+ also pairs peer connections and data channels by SDP/stream id. That pairing is
124
+ used only to synthesize remote data-channel close/error events when native close
125
+ callbacks are late or uneven under stress; send-drain closes still wait for
126
+ native delivery so queued messages are not truncated.
127
+
128
+ ## Incoming data channel event timing
129
+
130
+ libdatachannel can report an incoming data channel and its first channel events
131
+ from native callbacks before browser-style JS event handlers have been attached.
132
+ The JS facade queues incoming `datachannel` announcements and holds early
133
+ `open`/`message`/`close` events for that channel until after the
134
+ `datachannel` event has been dispatched.
135
+
136
+ Impact: this is the EventTarget timing layer required for WPT-compatible
137
+ ordering. The current runner passes full `RTCPeerConnection-ondatachannel.html`,
138
+ including the cases that close or send from inside the `datachannel` handler.
139
+
140
+ ## Worker-transferable data channels
141
+
142
+ Browser WebRTC exposes worker transfer behavior for `RTCDataChannel` through the
143
+ structured clone/transfer machinery. The current Node facade does not implement
144
+ transferable `RTCDataChannel` objects or a Worker-backed channel owner model.
145
+
146
+ Impact: worker and transferable data-channel WPT files remain outside
147
+ expected-pass, including `RTCDataChannel-worker*.js`,
148
+ `RTCDataChannel-worker-GC.html`, and `transfer-datachannel.html`.
149
+
150
+ ## bufferedAmount
151
+
152
+ libdatachannel reports bytes buffered in its SCTP transport queue. W3C
153
+ `bufferedAmount` increases synchronously for every `send()` call and decreases
154
+ later. The JS facade maintains its own W3C-style counter and does not surface
155
+ native buffered-amount-low callbacks directly because they are based on
156
+ libdatachannel's transport queue and can race the JS-visible counter.
157
+
158
+ Impact: this is intentionally shimmed in JS. The current WPT runner passes
159
+ `RTCDataChannel-bufferedAmount.html`.
160
+
161
+ ## End-of-candidates
162
+
163
+ libdatachannel candidate callbacks do not directly match browser
164
+ `icecandidate` event end-of-candidates semantics. The JS facade synthesizes a
165
+ final `icecandidate` event with `candidate === null` when gathering reaches
166
+ `complete`.
167
+
168
+ Impact: the selected WPT suite now covers candidate target validation, SDP
169
+ candidate insertion, and end-of-candidates mutation in
170
+ `RTCPeerConnection-addIceCandidate.html`. Operations-chain timing and
171
+ media-transceiver connection setup remain outside the selected scope.
172
+
173
+ ## ICE candidate errors
174
+
175
+ `RTCPeerConnectionIceErrorEvent` and the `onicecandidateerror` handler
176
+ attribute are exposed for WebRTC API shape compatibility. The current
177
+ libdatachannel binding does not surface STUN/TURN candidate-gathering failures
178
+ as browser `icecandidateerror` events.
179
+
180
+ Impact: constructor and handler-attribute behavior are covered locally, while
181
+ `RTCPeerConnection-onicecandidateerror.https.html` remains outside the selected
182
+ expected-pass WPT set until native ICE-server error events are available.
183
+
184
+ ## ICE restart
185
+
186
+ The default libdatachannel/libjuice path only accepts custom local ICE
187
+ credentials before candidate gathering starts. After gathering has started, the
188
+ facade treats `restartIce()` as a W3C-shaped renegotiation request and preserves
189
+ existing data channels through the offer/answer exchange, but it does not claim
190
+ fresh native ICE ufrag/password credentials.
191
+
192
+ Impact: the selected suite covers data-channel liveness with
193
+ `RTCDataChannel-iceRestart.html`, closed-state no-op behavior from
194
+ `RTCPeerConnection-restartIce.https.html`, and data-channel explicit rollback
195
+ gathering timing from
196
+ `RTCPeerConnection-explicit-rollback-iceGatheringState.html`. When native
197
+ gathering is already complete, the JS facade replays browser-shaped
198
+ `gathering`/`complete` events for the restart offer so WPT-visible task timing
199
+ matches the WebRTC API, but this does not imply fresh native ICE credentials.
200
+ WPT cases that assert fresh ICE credentials, media behavior, or detailed
201
+ restart signaling remain outside the current expected-pass set.
202
+
203
+ ## SCTP stream limit
204
+
205
+ The audited libdatachannel commit negotiates up to 1024 SCTP streams internally.
206
+ The JS facade exposes that connected-state `RTCSctpTransport.maxChannels` limit
207
+ even if the native limit read arrives slightly after the SCTP connected event.
208
+ Browser WebRTC/WPT includes cases around stream ids up to 65534.
209
+ For WebIDL construction compatibility, negotiated channels with ids above the
210
+ native limit can be constructed and keep their requested `id` while remaining in
211
+ the `"connecting"` state. They are not backed by a native SCTP stream and are
212
+ not expected to become usable until libdatachannel can negotiate that stream
213
+ range.
214
+
215
+ Impact: the selected WPT suite covers the construction-time negotiated id
216
+ `65534` case. Functional high-id transport cases remain outside expected-pass
217
+ unless libdatachannel is configured or changed to support the larger range.
218
+ Full `RTCDataChannel-id.html` coverage now passes; the JS facade assigns
219
+ browser-visible IDs when a remote answer determines the DTLS role before
220
+ libdatachannel exposes a native stream id.
221
+
222
+ ## Event and promise timing
223
+
224
+ Native libdatachannel callbacks run on internal worker threads. The addon uses a
225
+ Node-API thread-safe function and the JS facade dispatches DOM-style events on
226
+ the Node thread. Some exact browser task-source ordering is still being aligned
227
+ with WPT.
228
+
229
+ Impact: high-level negotiation and data-channel messaging work; precise timing
230
+ tests remain gated by the WPT harness.
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ const { RTCPeerConnection } = require("..");
4
+
5
+ async function main() {
6
+ const pc1 = new RTCPeerConnection();
7
+ const pc2 = new RTCPeerConnection();
8
+ let timeout;
9
+
10
+ const cleanup = () => {
11
+ if (timeout) clearTimeout(timeout);
12
+ pc1.close();
13
+ pc2.close();
14
+ };
15
+
16
+ pc1.addEventListener("icecandidate", ({ candidate }) => {
17
+ if (candidate) pc2.addIceCandidate(candidate).catch(console.error);
18
+ });
19
+ pc2.addEventListener("icecandidate", ({ candidate }) => {
20
+ if (candidate) pc1.addIceCandidate(candidate).catch(console.error);
21
+ });
22
+
23
+ pc2.addEventListener("datachannel", ({ channel }) => {
24
+ channel.addEventListener("message", ({ data }) => {
25
+ console.log(`pc2 received: ${data}`);
26
+ channel.send(`echo: ${data}`);
27
+ });
28
+ });
29
+
30
+ const channel = pc1.createDataChannel("chat");
31
+ channel.addEventListener("open", () => {
32
+ channel.send("hello from Node");
33
+ });
34
+ channel.addEventListener("message", ({ data }) => {
35
+ console.log(`pc1 received: ${data}`);
36
+ cleanup();
37
+ });
38
+
39
+ const offer = await pc1.createOffer();
40
+ await pc1.setLocalDescription(offer);
41
+ await pc2.setRemoteDescription(pc1.localDescription);
42
+
43
+ const answer = await pc2.createAnswer();
44
+ await pc2.setLocalDescription(answer);
45
+ await pc1.setRemoteDescription(pc2.localDescription);
46
+
47
+ timeout = setTimeout(() => {
48
+ cleanup();
49
+ process.exitCode = 1;
50
+ console.error("Timed out waiting for data-channel echo");
51
+ }, 10000).unref();
52
+ }
53
+
54
+ main().catch((error) => {
55
+ console.error(error);
56
+ process.exitCode = 1;
57
+ });