@tribe-nest/media-client 0.1.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.
Files changed (65) hide show
  1. package/README.md +68 -0
  2. package/build/core/index.d.ts +17 -0
  3. package/build/core/index.d.ts.map +1 -0
  4. package/build/core/index.js +41 -0
  5. package/build/core/index.js.map +1 -0
  6. package/build/core/reconnect.d.ts +95 -0
  7. package/build/core/reconnect.d.ts.map +1 -0
  8. package/build/core/reconnect.js +160 -0
  9. package/build/core/reconnect.js.map +1 -0
  10. package/build/core/signal.d.ts +184 -0
  11. package/build/core/signal.d.ts.map +1 -0
  12. package/build/core/signal.js +416 -0
  13. package/build/core/signal.js.map +1 -0
  14. package/build/core/socket.d.ts +57 -0
  15. package/build/core/socket.d.ts.map +1 -0
  16. package/build/core/socket.js +37 -0
  17. package/build/core/socket.js.map +1 -0
  18. package/build/core/state.d.ts +67 -0
  19. package/build/core/state.d.ts.map +1 -0
  20. package/build/core/state.js +193 -0
  21. package/build/core/state.js.map +1 -0
  22. package/build/index.d.ts +29 -0
  23. package/build/index.d.ts.map +1 -0
  24. package/build/index.js +51 -0
  25. package/build/index.js.map +1 -0
  26. package/build/protocol.d.ts +10 -0
  27. package/build/protocol.d.ts.map +1 -0
  28. package/build/protocol.js +26 -0
  29. package/build/protocol.js.map +1 -0
  30. package/build/react/index.d.ts +147 -0
  31. package/build/react/index.d.ts.map +1 -0
  32. package/build/react/index.js +319 -0
  33. package/build/react/index.js.map +1 -0
  34. package/build/room/browserDevice.d.ts +3 -0
  35. package/build/room/browserDevice.d.ts.map +1 -0
  36. package/build/room/browserDevice.js +94 -0
  37. package/build/room/browserDevice.js.map +1 -0
  38. package/build/room/device.d.ts +114 -0
  39. package/build/room/device.d.ts.map +1 -0
  40. package/build/room/device.js +3 -0
  41. package/build/room/device.js.map +1 -0
  42. package/build/room/room.d.ts +219 -0
  43. package/build/room/room.d.ts.map +1 -0
  44. package/build/room/room.js +438 -0
  45. package/build/room/room.js.map +1 -0
  46. package/package.json +69 -0
  47. package/src/_tests/clientBoundary.spec.ts +110 -0
  48. package/src/core/_tests/coreBoundary.spec.ts +70 -0
  49. package/src/core/_tests/fakeSignalServer.ts +188 -0
  50. package/src/core/_tests/reconnect.spec.ts +180 -0
  51. package/src/core/_tests/signal.spec.ts +347 -0
  52. package/src/core/_tests/state.spec.ts +226 -0
  53. package/src/core/index.ts +63 -0
  54. package/src/core/reconnect.ts +233 -0
  55. package/src/core/signal.ts +527 -0
  56. package/src/core/socket.ts +58 -0
  57. package/src/core/state.ts +251 -0
  58. package/src/index.ts +54 -0
  59. package/src/protocol.ts +9 -0
  60. package/src/react/_tests/hooks.spec.tsx +509 -0
  61. package/src/react/index.tsx +439 -0
  62. package/src/room/_tests/room.spec.ts +595 -0
  63. package/src/room/browserDevice.ts +114 -0
  64. package/src/room/device.ts +119 -0
  65. package/src/room/room.ts +600 -0
@@ -0,0 +1,114 @@
1
+ import { Device } from "mediasoup-client";
2
+ import type { Transport } from "mediasoup-client/types";
3
+
4
+ import type {
5
+ IceServer,
6
+ MediaConsumerHandle,
7
+ MediaDevice,
8
+ MediaProducerHandle,
9
+ MediaTransport,
10
+ RtpCapabilities,
11
+ RtpParameters,
12
+ TransportDescription,
13
+ TransportHandlers,
14
+ } from "./device";
15
+
16
+ /**
17
+ * The only file in this package that imports `mediasoup-client`.
18
+ *
19
+ * Everything above it is written against `MediaDevice`, so the room logic can
20
+ * be tested without a browser. Keeping that true is what `clientBoundary.spec`
21
+ * checks, and the reason it matters is that the interesting bugs are in the
22
+ * ORDER of operations rather than in the library calls: consume before the
23
+ * device is loaded, resume before the track is attached, rebuild on a reconnect
24
+ * without tearing the old transport down.
25
+ *
26
+ * This file is deliberately dull. If it grows logic, that logic is in the wrong
27
+ * place.
28
+ */
29
+
30
+ const adapt = (transport: Transport): MediaTransport => ({
31
+ id: transport.id,
32
+
33
+ async produce({ track, appData, encodings }) {
34
+ const producer = await transport.produce({
35
+ track,
36
+ ...(encodings ? { encodings: encodings as never } : {}),
37
+ ...(appData ? { appData } : {}),
38
+ });
39
+ return producer as unknown as MediaProducerHandle;
40
+ },
41
+
42
+ async consume({ id, producerId, kind, rtpParameters }) {
43
+ const consumer = await transport.consume({ id, producerId, kind, rtpParameters });
44
+ return consumer as unknown as MediaConsumerHandle;
45
+ },
46
+
47
+ close() {
48
+ transport.close();
49
+ },
50
+ });
51
+
52
+ export function createBrowserDevice(): MediaDevice {
53
+ const device = new Device();
54
+
55
+ const common = (description: TransportDescription, iceServers: IceServer[]) => ({
56
+ // The node calls it `transportId`; mediasoup-client calls it `id`. The
57
+ // rename happens here and nowhere else.
58
+ id: description.transportId,
59
+ iceParameters: description.iceParameters as never,
60
+ iceCandidates: description.iceCandidates as never,
61
+ dtlsParameters: description.dtlsParameters as never,
62
+ ...(description.sctpParameters ? { sctpParameters: description.sctpParameters as never } : {}),
63
+ // Relay credentials, minted per join. Without these a participant behind a
64
+ // symmetric NAT never connects, and the symptom is "connecting..." for ever
65
+ // with nothing naming TURN.
66
+ ...(iceServers.length ? { iceServers: iceServers as never } : {}),
67
+ });
68
+
69
+ const wire = (transport: Transport, handlers: Partial<TransportHandlers>) => {
70
+ // `connect` fires once, when the DTLS handshake needs its parameters
71
+ // delivered to the node. `errback` is not optional: without it a failed
72
+ // connect leaves the transport waiting rather than failing.
73
+ transport.on("connect", ({ dtlsParameters }, callback, errback) => {
74
+ handlers.onConnect?.(dtlsParameters).then(callback, errback);
75
+ });
76
+
77
+ if (handlers.onProduce) {
78
+ transport.on("produce", ({ kind, rtpParameters, appData }, callback, errback) => {
79
+ handlers
80
+ .onProduce?.({ kind, rtpParameters: rtpParameters as RtpParameters, appData: appData as Record<string, unknown> })
81
+ .then((id) => callback({ id }), errback);
82
+ });
83
+ }
84
+
85
+ if (handlers.onConnectionStateChange) {
86
+ transport.on("connectionstatechange", (state) => handlers.onConnectionStateChange?.(state));
87
+ }
88
+ };
89
+
90
+ return {
91
+ async load(routerRtpCapabilities: RtpCapabilities) {
92
+ await device.load({ routerRtpCapabilities: routerRtpCapabilities as never });
93
+ },
94
+ get loaded() {
95
+ return device.loaded;
96
+ },
97
+ get rtpCapabilities() {
98
+ return device.rtpCapabilities as RtpCapabilities;
99
+ },
100
+ canProduce(kind) {
101
+ return device.canProduce(kind);
102
+ },
103
+ createSendTransport({ description, iceServers, handlers }) {
104
+ const transport = device.createSendTransport(common(description, iceServers));
105
+ wire(transport, handlers);
106
+ return adapt(transport);
107
+ },
108
+ createRecvTransport({ description, iceServers, handlers }) {
109
+ const transport = device.createRecvTransport(common(description, iceServers));
110
+ wire(transport, handlers);
111
+ return adapt(transport);
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,119 @@
1
+ import type { RtpCapabilities, RtpParameters } from "mediasoup-client/types";
2
+
3
+ /**
4
+ * The `mediasoup-client` seam.
5
+ *
6
+ * ## Why the room talks to an interface instead of to `mediasoup-client`
7
+ *
8
+ * Everything interesting about the room layer - when to create a transport,
9
+ * what to do when a producer appears, whether a consume is permitted, how a
10
+ * reconnect rebuilds - is logic, and none of it needs a `RTCPeerConnection`.
11
+ * `mediasoup-client` needs a browser, so a room that imported it directly could
12
+ * only ever be tested in one.
13
+ *
14
+ * That is not a hypothetical cost. It is the difference between a reducer bug
15
+ * being caught by `npm test` and being caught by a person on a call.
16
+ *
17
+ * So the browser half is this interface, `room.ts` is written against it, and
18
+ * `browserDevice.ts` is the ~60 lines that actually import the library. The
19
+ * specs supply a fake; production supplies the real one.
20
+ *
21
+ * ## What deliberately is NOT abstracted
22
+ *
23
+ * The RTP parameter shapes. Those are `mediasoup-client`'s types re-exported,
24
+ * because inventing a parallel vocabulary for them would mean translating in
25
+ * both directions and the translation is where the bugs would live.
26
+ */
27
+
28
+ export type { RtpCapabilities, RtpParameters };
29
+
30
+ /** What the node said when it created a WebRTC transport for us. */
31
+ export type TransportDescription = {
32
+ transportId: string;
33
+ iceParameters: unknown;
34
+ iceCandidates: unknown;
35
+ dtlsParameters: unknown;
36
+ sctpParameters?: unknown;
37
+ };
38
+
39
+ export type IceServer = {
40
+ urls: string | string[];
41
+ username?: string;
42
+ credential?: string;
43
+ };
44
+
45
+ /**
46
+ * One direction of media. `send` publishes, `recv` subscribes.
47
+ *
48
+ * `onConnect` and `onProduce` are how mediasoup-client asks the application to
49
+ * talk to the server: the DTLS parameters and the RTP parameters are produced
50
+ * by the library and have to reach the node over OUR signalling channel.
51
+ */
52
+ export type TransportHandlers = {
53
+ onConnect(dtlsParameters: unknown): Promise<void>;
54
+ /** Must resolve to the node's producer id. */
55
+ onProduce(input: { kind: "audio" | "video"; rtpParameters: RtpParameters; appData?: Record<string, unknown> }): Promise<string>;
56
+ onConnectionStateChange?(state: string): void;
57
+ };
58
+
59
+ export interface MediaTransport {
60
+ readonly id: string;
61
+ produce(input: {
62
+ track: MediaStreamTrack;
63
+ appData?: Record<string, unknown>;
64
+ encodings?: unknown[];
65
+ }): Promise<MediaProducerHandle>;
66
+ consume(input: {
67
+ id: string;
68
+ producerId: string;
69
+ kind: "audio" | "video";
70
+ rtpParameters: RtpParameters;
71
+ }): Promise<MediaConsumerHandle>;
72
+ close(): void;
73
+ }
74
+
75
+ export type MediaProducerHandle = {
76
+ readonly id: string;
77
+ readonly kind: "audio" | "video";
78
+ pause(): void;
79
+ resume(): void;
80
+ close(): void;
81
+ readonly closed: boolean;
82
+ };
83
+
84
+ export type MediaConsumerHandle = {
85
+ readonly id: string;
86
+ readonly producerId: string;
87
+ readonly kind: "audio" | "video";
88
+ readonly track: MediaStreamTrack;
89
+ pause(): void;
90
+ resume(): void;
91
+ close(): void;
92
+ };
93
+
94
+ /**
95
+ * The device: capability negotiation plus transport construction.
96
+ *
97
+ * `rtpCapabilities` is what the node needs in order to answer `consume`, and it
98
+ * is only meaningful AFTER `load`, which is why `load` is separate from
99
+ * construction rather than folded into it.
100
+ */
101
+ export interface MediaDevice {
102
+ load(routerRtpCapabilities: RtpCapabilities): Promise<void>;
103
+ readonly loaded: boolean;
104
+ readonly rtpCapabilities: RtpCapabilities;
105
+ /** False when the browser cannot receive this kind at all. */
106
+ canProduce(kind: "audio" | "video"): boolean;
107
+ createSendTransport(input: {
108
+ description: TransportDescription;
109
+ iceServers: IceServer[];
110
+ handlers: TransportHandlers;
111
+ }): MediaTransport;
112
+ createRecvTransport(input: {
113
+ description: TransportDescription;
114
+ iceServers: IceServer[];
115
+ handlers: Omit<TransportHandlers, "onProduce">;
116
+ }): MediaTransport;
117
+ }
118
+
119
+ export type MediaDeviceFactory = () => MediaDevice;