openrtc 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 (122) hide show
  1. package/README.md +106 -0
  2. package/dist/OpenRTC.d.ts +71 -0
  3. package/dist/OpenRTC.js +130 -0
  4. package/dist/api/MediaTransport.d.ts +7 -0
  5. package/dist/api/MediaTransport.js +262 -0
  6. package/dist/api/PlutoPeerConnection.d.ts +50 -0
  7. package/dist/api/PlutoPeerConnection.js +434 -0
  8. package/dist/api/PlutoWebSocket.d.ts +24 -0
  9. package/dist/api/PlutoWebSocket.js +113 -0
  10. package/dist/api/PlutoWebTransport.d.ts +29 -0
  11. package/dist/api/PlutoWebTransport.js +105 -0
  12. package/dist/auth/host.d.ts +73 -0
  13. package/dist/auth/host.js +513 -0
  14. package/dist/auth/index.d.ts +1 -0
  15. package/dist/auth/index.js +1 -0
  16. package/dist/auth/internal.d.ts +1 -0
  17. package/dist/auth/internal.js +1 -0
  18. package/dist/compat/index.d.ts +20 -0
  19. package/dist/compat/index.js +20 -0
  20. package/dist/core/Client.d.ts +352 -0
  21. package/dist/core/Client.js +3689 -0
  22. package/dist/core/Connection.d.ts +67 -0
  23. package/dist/core/Connection.js +404 -0
  24. package/dist/core/IEngineBridge.d.ts +18 -0
  25. package/dist/core/IEngineBridge.js +1 -0
  26. package/dist/core/ISignalingTransport.d.ts +13 -0
  27. package/dist/core/ISignalingTransport.js +1 -0
  28. package/dist/core/PeerLifecycleManager.d.ts +43 -0
  29. package/dist/core/PeerLifecycleManager.js +330 -0
  30. package/dist/core/PeerProjectionStore.d.ts +43 -0
  31. package/dist/core/PeerProjectionStore.js +327 -0
  32. package/dist/core/PlutoRTC.d.ts +13 -0
  33. package/dist/core/PlutoRTC.js +26 -0
  34. package/dist/core/Room.d.ts +47 -0
  35. package/dist/core/Room.js +376 -0
  36. package/dist/core/Signaling.d.ts +39 -0
  37. package/dist/core/Signaling.js +147 -0
  38. package/dist/core/bridges/BridgeCodecs.d.ts +19 -0
  39. package/dist/core/bridges/BridgeCodecs.js +317 -0
  40. package/dist/core/bridges/BrowserFirestoreRooms.d.ts +36 -0
  41. package/dist/core/bridges/BrowserFirestoreRooms.js +169 -0
  42. package/dist/core/bridges/BrowserFirestoreSignaling.d.ts +111 -0
  43. package/dist/core/bridges/BrowserFirestoreSignaling.js +1010 -0
  44. package/dist/core/bridges/NativeIpcBridge.d.ts +156 -0
  45. package/dist/core/bridges/NativeIpcBridge.js +1472 -0
  46. package/dist/core/bridges/RtdbPresence.d.ts +122 -0
  47. package/dist/core/bridges/RtdbPresence.js +201 -0
  48. package/dist/core/bridges/TauriBridge.d.ts +8 -0
  49. package/dist/core/bridges/TauriBridge.js +7 -0
  50. package/dist/core/bridges/WasmBridge.d.ts +233 -0
  51. package/dist/core/bridges/WasmBridge.js +1811 -0
  52. package/dist/core/config.d.ts +31 -0
  53. package/dist/core/config.js +138 -0
  54. package/dist/core/transports/ITransport.d.ts +12 -0
  55. package/dist/core/transports/ITransport.js +1 -0
  56. package/dist/core/transports/LocalDaemonBridge.d.ts +41 -0
  57. package/dist/core/transports/LocalDaemonBridge.js +227 -0
  58. package/dist/core/transports/TauriSignaling.d.ts +28 -0
  59. package/dist/core/transports/TauriSignaling.js +373 -0
  60. package/dist/core/transports/TransportFactory.d.ts +9 -0
  61. package/dist/core/transports/TransportFactory.js +59 -0
  62. package/dist/core/transports/WasmSignaling.d.ts +78 -0
  63. package/dist/core/transports/WasmSignaling.js +522 -0
  64. package/dist/core/transports/WebRTCTransport.d.ts +23 -0
  65. package/dist/core/transports/WebRTCTransport.js +132 -0
  66. package/dist/core/trust.d.ts +23 -0
  67. package/dist/core/trust.js +99 -0
  68. package/dist/core/types.d.ts +559 -0
  69. package/dist/core/types.js +19 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +3 -0
  72. package/dist/moq/MoQMediaTransport.d.ts +6 -0
  73. package/dist/moq/MoQMediaTransport.js +106 -0
  74. package/dist/moq/MoQPublisher.d.ts +22 -0
  75. package/dist/moq/MoQPublisher.js +179 -0
  76. package/dist/moq/MoQSession.d.ts +35 -0
  77. package/dist/moq/MoQSession.js +166 -0
  78. package/dist/moq/MoQSubscriber.d.ts +19 -0
  79. package/dist/moq/MoQSubscriber.js +235 -0
  80. package/dist/moq/common.d.ts +47 -0
  81. package/dist/moq/common.js +304 -0
  82. package/dist/moq/messages.d.ts +105 -0
  83. package/dist/moq/messages.js +367 -0
  84. package/dist/presets/plutoHosted.d.ts +13 -0
  85. package/dist/presets/plutoHosted.js +18 -0
  86. package/dist/runtime/DelegatingRuntimeAdapter.d.ts +68 -0
  87. package/dist/runtime/DelegatingRuntimeAdapter.js +250 -0
  88. package/dist/runtime/IpcRuntimeAdapter.d.ts +9 -0
  89. package/dist/runtime/IpcRuntimeAdapter.js +13 -0
  90. package/dist/runtime/RuntimeClient.d.ts +257 -0
  91. package/dist/runtime/RuntimeClient.js +1484 -0
  92. package/dist/runtime/TauriRuntimeAdapter.d.ts +1 -0
  93. package/dist/runtime/TauriRuntimeAdapter.js +1 -0
  94. package/dist/runtime/WasmRuntimeAdapter.d.ts +6 -0
  95. package/dist/runtime/WasmRuntimeAdapter.js +11 -0
  96. package/dist/runtime/factories.d.ts +24 -0
  97. package/dist/runtime/factories.js +66 -0
  98. package/dist/runtime/index.d.ts +6 -0
  99. package/dist/runtime/index.js +5 -0
  100. package/dist/runtime/ipc.d.ts +8 -0
  101. package/dist/runtime/ipc.js +65 -0
  102. package/dist/runtime/ipcContract.d.ts +46 -0
  103. package/dist/runtime/ipcContract.js +1 -0
  104. package/dist/runtime/tauri.d.ts +16 -0
  105. package/dist/runtime/tauri.js +22 -0
  106. package/dist/runtime/types.d.ts +110 -0
  107. package/dist/runtime/types.js +1 -0
  108. package/dist/transports/TauriTransport.d.ts +17 -0
  109. package/dist/transports/TauriTransport.js +49 -0
  110. package/dist/transports/WebTransport.d.ts +23 -0
  111. package/dist/transports/WebTransport.js +71 -0
  112. package/dist/utils/debug.d.ts +4 -0
  113. package/dist/utils/debug.js +33 -0
  114. package/dist/utils/runtime.d.ts +1 -0
  115. package/dist/utils/runtime.js +10 -0
  116. package/package.json +97 -0
  117. package/wasm/README.md +64 -0
  118. package/wasm/openrtc.d.ts +284 -0
  119. package/wasm/openrtc.js +2132 -0
  120. package/wasm/openrtc_bg.wasm +0 -0
  121. package/wasm/openrtc_bg.wasm.d.ts +110 -0
  122. package/wasm/package.json +16 -0
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # OpenRTC TypeScript Package
2
+
3
+ The TypeScript package is the host-facing layer for `openrtc`.
4
+
5
+ It does not implement a second realtime engine. It exposes the Rust runtime to:
6
+ - browser apps through WASM plus the scoped browser Firebase signaling layer
7
+ - native apps through a generic IPC contract, with first-party Tauri wiring in `openrtc-tauri`
8
+
9
+ ## Recommended Usage
10
+
11
+ Use the OpenRTC client entry points for new integrations:
12
+
13
+ ```ts
14
+ import { OpenRTC } from 'openrtc';
15
+
16
+ const client = OpenRTC({
17
+ apiKey: 'pk_live_...',
18
+ authMode: 'external',
19
+ });
20
+
21
+ await client.connect();
22
+ ```
23
+
24
+ For normal app code, the default flow is:
25
+
26
+ 1. discover or select a `deviceId`
27
+ 2. call `connectPeer(...)` or `connectToDevice(...)`
28
+ 3. wait for `waitForConnectedPeer(deviceId)`
29
+ 4. open peer-targeted streams with `openPeerBi(...)` / `openPeerUni(...)`
30
+
31
+ Low-level peer handles returned by runtime methods, along with `openPeerBi(deviceId)` and `openPeerUni(deviceId)`, remain available for advanced runtime flows, but they are not the default integration model.
32
+
33
+ ## What This Package Owns
34
+ - runtime factories and adapters
35
+ - auth context injection
36
+ - Pluto hosted auth helpers
37
+ - browser Firebase signaling/rooms wiring
38
+ - host wiring for WASM and generic IPC
39
+
40
+ ## What This Package Does Not Own
41
+ - native Tauri API bindings
42
+ - peer discovery or lifecycle policy beyond the browser Firebase host layer
43
+ - iroh connection lifecycle
44
+
45
+ Those responsibilities belong to the Rust runtime and the host platform adapter.
46
+
47
+ ## Message Model
48
+ - `openrtc` is message-agnostic above the runtime layer.
49
+ - The runtime only defines internal control messages such as handshake and heartbeat.
50
+ - Application payload protocols, including file transfer, should be implemented on top of generic runtime connections and streams.
51
+ - If an app needs richer payload ergonomics, add generic stream/connection primitives to the runtime instead of adding app-specific message types to `pluto-rtc`.
52
+
53
+ ## Protocol Extension Model
54
+
55
+ The recommended TypeScript extension path is:
56
+
57
+ 1. wait for a Rust-backed connected peer session
58
+ 2. open a bidirectional or unidirectional stream
59
+ 3. implement your own framing and message semantics in app code
60
+
61
+ This keeps the runtime responsible for peer/session establishment while leaving
62
+ full control of channel/message specifics to the developer.
63
+
64
+ Advanced custom ALPN protocol plugins are a Rust-first/native-first feature.
65
+ Browser parity is guaranteed at the connected-peer stream layer, not at the
66
+ full ALPN plugin layer.
67
+
68
+ ## Main Surfaces
69
+
70
+ ### Root Package
71
+ - `OpenRTC()`
72
+ - `OpenRTC.native()`
73
+ - `Client` (re-export alias)
74
+ - `RuntimeClient`
75
+
76
+ ### Runtime Secondary Entrypoint
77
+ - `openrtc/runtime`
78
+ - `createWasmClient()` and `createIpcClient(..., bridge)` (legacy compatibility)
79
+ - `IpcRuntimeAdapter`
80
+ - `WasmRuntimeAdapter`
81
+ - `PlutoIpcBridge` types for advanced host integrations
82
+
83
+ ### Tauri Package
84
+ - `openrtc-tauri`
85
+ - `createTauriIpcBridge()`
86
+ - `TauriRuntimeAdapter`
87
+ - `openrtc/runtime/tauri` remains as a deprecated compatibility shim for one release cycle
88
+
89
+ ## Auth Model
90
+ - `anonymous`: no Pluto auth required
91
+ - `required`: Pluto auth required; the SDK may initiate Pluto SSO
92
+ - `external`: Pluto auth still required, but the host app acquires it and injects it into the runtime
93
+
94
+ ## Design Rule
95
+ If a feature requires discovery, signaling, presence, or lifecycle state, the TypeScript package should delegate to Rust rather than reimplement it locally, except for the scoped browser Firebase host layer where the official browser SDK is the source of truth.
96
+
97
+ ## Allowed vs. Not Allowed
98
+
99
+ - Allowed:
100
+ - host/runtime adapter selection
101
+ - auth injection and hosted Pluto auth helpers
102
+ - thin, typed wrappers over Rust-owned state
103
+ - Not allowed:
104
+ - TypeScript-owned discovery or lifecycle engines
105
+ - TypeScript-owned peer identity policy
106
+ - app-specific protocol messages embedded into the runtime
@@ -0,0 +1,71 @@
1
+ /**
2
+ * OpenRTC — simplified developer-facing entry point.
3
+ *
4
+ * Usage:
5
+ * import { OpenRTC } from 'openrtc';
6
+ * const client = OpenRTC({ apiKey: 'pk_live_...' });
7
+ * await client.connect();
8
+ *
9
+ * For native (Tauri) apps:
10
+ * const client = OpenRTC.native({ apiKey: 'pk_live_...' });
11
+ *
12
+ * The returned Client object combines RuntimeClient methods with
13
+ * auth helpers (setAuth, syncPlutoAuth, bindPlutoAuthHost, signIn, signOut).
14
+ *
15
+ * Backward compatibility:
16
+ * - createWasmClient() and createIpcClient() remain available from `openrtc/runtime`
17
+ * - PlutoRuntimeAssembly remains available from `openrtc/runtime`
18
+ * - RuntimeClient is still re-exported as Client
19
+ */
20
+ import type { AuthContext } from './core/IEngineBridge';
21
+ import type { ClientOptions } from './core/types';
22
+ import { RuntimeClient } from './runtime/RuntimeClient';
23
+ import { type PlutoRuntimeAssembly } from './runtime/factories';
24
+ import { type PlutoIpcBridge } from './runtime/ipc';
25
+ /**
26
+ * OpenRTCClient extends RuntimeClient with auth helpers that were
27
+ * previously only available on PlutoRuntimeAssembly.
28
+ *
29
+ * This is the unified type returned by OpenRTC() and OpenRTC.native().
30
+ */
31
+ export interface OpenRTCClient extends RuntimeClient {
32
+ /** @deprecated Use setAuth() instead */
33
+ setAuthContext(context: AuthContext | null): Promise<void>;
34
+ /** Set auth context for the runtime */
35
+ setAuth(context: AuthContext | null): Promise<void>;
36
+ /** Sync Pluto-hosted auth to the runtime */
37
+ syncPlutoAuth(options?: {
38
+ userId?: string | null;
39
+ forceRefresh?: boolean;
40
+ }): Promise<void>;
41
+ /** Bind Pluto auth host to auto-sync on token changes */
42
+ bindPlutoAuthHost(): () => void;
43
+ /** Sign in via Pluto SSO */
44
+ signIn(): Promise<void>;
45
+ /** Sign out and clear auth context */
46
+ signOut(): Promise<void>;
47
+ /**
48
+ * Access to the underlying PlutoRuntimeAssembly for backward compatibility.
49
+ * @deprecated Access methods directly on the client instead.
50
+ */
51
+ readonly runtime: RuntimeClient;
52
+ /**
53
+ * Access to the underlying PlutoRuntimeAssembly for backward compatibility.
54
+ * @internal
55
+ */
56
+ readonly _assembly: PlutoRuntimeAssembly;
57
+ }
58
+ /**
59
+ * Create an OpenRTC client for browser/WASM environments.
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * import { OpenRTC } from 'openrtc';
64
+ * const client = OpenRTC({ apiKey: 'pk_live_...' });
65
+ * await client.connect();
66
+ * ```
67
+ */
68
+ export declare function OpenRTC(options: ClientOptions): OpenRTCClient;
69
+ export declare namespace OpenRTC {
70
+ var native: (options: ClientOptions, bridge?: PlutoIpcBridge) => OpenRTCClient;
71
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * OpenRTC — simplified developer-facing entry point.
3
+ *
4
+ * Usage:
5
+ * import { OpenRTC } from 'openrtc';
6
+ * const client = OpenRTC({ apiKey: 'pk_live_...' });
7
+ * await client.connect();
8
+ *
9
+ * For native (Tauri) apps:
10
+ * const client = OpenRTC.native({ apiKey: 'pk_live_...' });
11
+ *
12
+ * The returned Client object combines RuntimeClient methods with
13
+ * auth helpers (setAuth, syncPlutoAuth, bindPlutoAuthHost, signIn, signOut).
14
+ *
15
+ * Backward compatibility:
16
+ * - createWasmClient() and createIpcClient() remain available from `openrtc/runtime`
17
+ * - PlutoRuntimeAssembly remains available from `openrtc/runtime`
18
+ * - RuntimeClient is still re-exported as Client
19
+ */
20
+ import { createWasmClient, createIpcClient } from './runtime/factories';
21
+ import { createCompatTauriIpcBridge } from './runtime/ipc';
22
+ function resolveOptions(options) {
23
+ // Map new option names to old ones (new names take priority)
24
+ const resolved = { ...options };
25
+ // auth → authMode
26
+ if ('auth' in options && !options.authMode) {
27
+ const auth = options.auth;
28
+ resolved.authMode = auth === 'none' ? 'anonymous' : auth;
29
+ }
30
+ // identity → nodeIdPersistence
31
+ if ('identity' in options && !options.nodeIdPersistence) {
32
+ const identity = options.identity;
33
+ resolved.nodeIdPersistence = identity === 'temporary' ? 'ephemeral' : identity;
34
+ }
35
+ // namespace → storagePrefix
36
+ if ('namespace' in options && !options.storagePrefix) {
37
+ resolved.storagePrefix = options.namespace;
38
+ }
39
+ // privacy → strictMode
40
+ if ('privacy' in options && options.strictMode === undefined) {
41
+ resolved.strictMode = !!options.privacy;
42
+ }
43
+ return resolved;
44
+ }
45
+ function wrapAssembly(assembly) {
46
+ const runtime = assembly.runtime;
47
+ // Create a proxy that delegates to RuntimeClient but adds assembly helpers
48
+ const client = Object.create(runtime);
49
+ // Assembly auth helpers as direct methods
50
+ Object.defineProperties(client, {
51
+ setAuth: {
52
+ value: (context) => assembly.setAuthContext(context),
53
+ enumerable: true,
54
+ writable: true,
55
+ configurable: true,
56
+ },
57
+ setAuthContext: {
58
+ value: (context) => assembly.setAuthContext(context),
59
+ enumerable: true,
60
+ writable: true,
61
+ configurable: true,
62
+ },
63
+ syncPlutoAuth: {
64
+ value: (options) => assembly.syncPlutoAuth(options),
65
+ enumerable: true,
66
+ writable: true,
67
+ configurable: true,
68
+ },
69
+ bindPlutoAuthHost: {
70
+ value: () => assembly.bindPlutoAuthHost(),
71
+ enumerable: true,
72
+ writable: true,
73
+ configurable: true,
74
+ },
75
+ signIn: {
76
+ value: () => assembly.signInWithPluto(),
77
+ enumerable: true,
78
+ writable: true,
79
+ configurable: true,
80
+ },
81
+ signOut: {
82
+ value: () => assembly.signOut(),
83
+ enumerable: true,
84
+ writable: true,
85
+ configurable: true,
86
+ },
87
+ // Backward compat: .runtime returns the RuntimeClient
88
+ runtime: {
89
+ get: () => runtime,
90
+ enumerable: false,
91
+ },
92
+ _assembly: {
93
+ get: () => assembly,
94
+ enumerable: false,
95
+ },
96
+ });
97
+ return client;
98
+ }
99
+ /**
100
+ * Create an OpenRTC client for browser/WASM environments.
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * import { OpenRTC } from 'openrtc';
105
+ * const client = OpenRTC({ apiKey: 'pk_live_...' });
106
+ * await client.connect();
107
+ * ```
108
+ */
109
+ export function OpenRTC(options) {
110
+ const resolved = resolveOptions(options);
111
+ const assembly = createWasmClient(resolved);
112
+ return wrapAssembly(assembly);
113
+ }
114
+ /**
115
+ * Create an OpenRTC client for native (Tauri/IPC) environments.
116
+ * Auto-detects the Tauri IPC bridge.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * import { OpenRTC } from 'openrtc';
121
+ * const client = OpenRTC.native({ apiKey: 'pk_live_...' });
122
+ * await client.connect();
123
+ * ```
124
+ */
125
+ OpenRTC.native = function (options, bridge) {
126
+ const resolved = resolveOptions(options);
127
+ const ipcBridge = bridge ?? createCompatTauriIpcBridge();
128
+ const assembly = createIpcClient(resolved, ipcBridge);
129
+ return wrapAssembly(assembly);
130
+ };
@@ -0,0 +1,7 @@
1
+ export declare class MediaTransport {
2
+ static sendTrack(track: MediaStreamTrack, sendFn: (data: Uint8Array) => Promise<void>): Promise<{
3
+ stop: () => void;
4
+ requestKeyFrame: () => void;
5
+ } | void>;
6
+ static receiveTrack(kind: 'audio' | 'video', onKeyFrameRequest?: () => void): MediaStreamTrack | null;
7
+ }
@@ -0,0 +1,262 @@
1
+ // Protocol-ish logic for transforming MediaStreamTracks into Chunks and back.
2
+ // Since we don't have VideoEncoder/VideoDecoder in standard ts environment (it's web only),
3
+ // we will just define the interfaces or classes here.
4
+ // We use type 'any' for WebCodecs interfaces to avoid TS errors in environemnts without @types/dom-webcodecs
5
+ export class MediaTransport {
6
+ // Encodes a track into the connection
7
+ static async sendTrack(track, sendFn) {
8
+ console.log(`[MediaTransport] sendTrack called for ${track.kind}`);
9
+ if ((typeof VideoEncoder === 'undefined') && (typeof AudioEncoder === 'undefined')) {
10
+ console.error("WebCodecs API not supported.");
11
+ return;
12
+ }
13
+ const processor = new window.MediaStreamTrackProcessor({ track });
14
+ const reader = processor.readable.getReader();
15
+ // Very basic encoder setup.
16
+ // In reality we need to negotiate codecs. We assume VP8 for video and Opus for audio.
17
+ let encoder = null;
18
+ if (track.kind === 'video') {
19
+ let seqNum = 0;
20
+ encoder = new window.VideoEncoder({
21
+ output: (chunk, metadata) => {
22
+ if (chunk.type === 'key') {
23
+ console.log(`[MediaTransport] Generated KEY frame for video. Size: ${chunk.byteLength}`);
24
+ }
25
+ // console.log(`[MediaTransport] Encoded video chunk. Key: ${chunk.type}, Size: ${chunk.byteLength}`);
26
+ // Send chunk
27
+ // We need to serialize the chunk.
28
+ // [MAGIC 1b][timestamp 8 bytes][isKey 1 byte][seq 2 bytes][data...]
29
+ const buffer = new ArrayBuffer(12 + chunk.byteLength);
30
+ const view = new DataView(buffer);
31
+ view.setUint8(0, 0x77); // Magic byte
32
+ view.setBigInt64(1, BigInt(chunk.timestamp), false); // Big Endian
33
+ view.setUint8(9, chunk.type === 'key' ? 1 : 0);
34
+ const seq = (seqNum++) % 65536;
35
+ view.setUint16(10, seq, false);
36
+ const data = new Uint8Array(buffer);
37
+ chunk.copyTo(new Uint8Array(buffer, 12));
38
+ sendFn(data).catch(e => {
39
+ const msg = (e.message || String(e)).toLowerCase();
40
+ if (msg.includes("connection is closed") || msg.includes("connection closed") || msg.includes("sending stopped by peer")) {
41
+ console.log(`[MediaTransport] Connection closed/stopped, stopping video encoder.`);
42
+ if (encoder) {
43
+ encoder.close();
44
+ encoder = null;
45
+ }
46
+ }
47
+ else {
48
+ console.error("Send media failed", e);
49
+ }
50
+ });
51
+ },
52
+ error: (e) => console.error("VideoEncoder error", e)
53
+ });
54
+ encoder.configure({
55
+ codec: 'vp8',
56
+ width: 640,
57
+ height: 480,
58
+ bitrate: 500000, // 500 Kbps - Reduce congestion
59
+ framerate: 24, // slightly lower framerate
60
+ latencyMode: 'realtime', // Critical for low latency streaming
61
+ });
62
+ }
63
+ else if (track.kind === 'audio') {
64
+ encoder = new window.AudioEncoder({
65
+ output: (chunk, metadata) => {
66
+ const buffer = new ArrayBuffer(9 + chunk.byteLength);
67
+ const view = new DataView(buffer);
68
+ view.setUint8(0, 0x77); // Magic
69
+ view.setBigInt64(1, BigInt(chunk.timestamp), false);
70
+ const data = new Uint8Array(buffer);
71
+ chunk.copyTo(new Uint8Array(buffer, 9));
72
+ sendFn(data).catch(e => {
73
+ const msg = (e.message || String(e)).toLowerCase();
74
+ if (msg.includes("connection is closed") || msg.includes("connection closed")) {
75
+ console.log(`[MediaTransport] Connection closed, stopping audio encoder.`);
76
+ if (encoder) {
77
+ encoder.close();
78
+ encoder = null;
79
+ }
80
+ }
81
+ else {
82
+ console.error("Send audio failed", e);
83
+ }
84
+ });
85
+ },
86
+ error: (e) => console.error("AudioEncoder error", e)
87
+ });
88
+ encoder.configure({
89
+ codec: 'opus',
90
+ sampleRate: 48000,
91
+ numberOfChannels: 1
92
+ });
93
+ }
94
+ // Read loop - Run asynchronously so we can return the controller immediately
95
+ (async () => {
96
+ try {
97
+ while (true) {
98
+ const { done, value } = await reader.read();
99
+ if (done) {
100
+ console.log(`[MediaTransport] Reader done for ${track.kind}`);
101
+ break;
102
+ }
103
+ if (value) {
104
+ try {
105
+ if (encoder && encoder.state !== 'closed') {
106
+ const forceKey = encoder.forceKeyFrame;
107
+ if (forceKey) {
108
+ console.log(`[MediaTransport] Forcing Key Frame now.`);
109
+ encoder.encode(value, { keyFrame: true });
110
+ encoder.forceKeyFrame = false;
111
+ }
112
+ else {
113
+ encoder.encode(value);
114
+ }
115
+ }
116
+ }
117
+ finally {
118
+ value.close();
119
+ }
120
+ }
121
+ }
122
+ }
123
+ catch (e) {
124
+ console.error(`[MediaTransport] Read loop error for ${track.kind}:`, e);
125
+ }
126
+ })();
127
+ // Return controller
128
+ return {
129
+ stop: () => {
130
+ // TODO: Close processor and reader
131
+ },
132
+ requestKeyFrame: () => {
133
+ if (encoder && encoder.state === 'configured') {
134
+ console.log(`[MediaTransport] Key Frame Requested for ${track.kind}`);
135
+ // We can't force it here directly because we are in a read loop.
136
+ // But we can set a flag that the loop checks.
137
+ encoder.forceKeyFrame = true;
138
+ }
139
+ else {
140
+ console.warn(`[MediaTransport] Impossible to request KeyFrame: Encoder state is ${encoder?.state}`);
141
+ }
142
+ }
143
+ };
144
+ }
145
+ // Receives chunks and outputs a MediaStreamTrack
146
+ static receiveTrack(kind, onKeyFrameRequest) {
147
+ if ((typeof VideoDecoder === 'undefined') && (typeof AudioDecoder === 'undefined')) {
148
+ console.warn("WebCodecs API not supported.");
149
+ return null;
150
+ }
151
+ const generator = new window.MediaStreamTrackGenerator({ kind });
152
+ const writer = generator.writable.getWriter();
153
+ let decoder = null;
154
+ // Throttling for PLI
155
+ let lastPli = 0;
156
+ let expectedSeq = null;
157
+ if (kind === 'video') {
158
+ decoder = new window.VideoDecoder({
159
+ output: (frame) => {
160
+ try {
161
+ writer.write(frame);
162
+ }
163
+ finally {
164
+ frame.close();
165
+ }
166
+ },
167
+ error: (e) => console.error("VideoDecoder error", e)
168
+ });
169
+ decoder.configure({
170
+ codec: 'vp8'
171
+ });
172
+ // Force wait for key frame immediately
173
+ decoder.waitingForKeyFrame = true;
174
+ }
175
+ else {
176
+ decoder = new window.AudioDecoder({
177
+ output: (data) => {
178
+ writer.write(data);
179
+ data.close();
180
+ },
181
+ error: (e) => console.error("AudioDecoder error", e)
182
+ });
183
+ decoder.configure({
184
+ codec: 'opus',
185
+ sampleRate: 48000,
186
+ numberOfChannels: 1
187
+ });
188
+ }
189
+ // Attach a method to the track object or return a controller?
190
+ // We will just return the track, but we need a way to feed data into the decoder.
191
+ // We can monkey-patch the track or store it in a map in PeerConnection.
192
+ generator.feed = (data) => {
193
+ if (decoder.state === 'closed')
194
+ return;
195
+ // Verify Magic
196
+ if (data.byteLength < 1 || data[0] !== 0x77) {
197
+ console.error("[MediaTransport] Invalid magic byte in media stream! Dropping packet.");
198
+ return;
199
+ }
200
+ if (kind === 'video') {
201
+ // Parse
202
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
203
+ const timestamp = Number(view.getBigInt64(1, false));
204
+ const isKey = view.getUint8(9) === 1;
205
+ const seq = view.getUint16(10, false);
206
+ const chunkData = data.slice(12);
207
+ // Sequence Check
208
+ if (expectedSeq !== null) {
209
+ if (seq !== expectedSeq) {
210
+ console.warn(`[MediaTransport] Sequence Mismatch! Expected ${expectedSeq}, got ${seq}. Dropping and requesting PLI.`);
211
+ decoder.waitingForKeyFrame = true;
212
+ if (onKeyFrameRequest)
213
+ onKeyFrameRequest();
214
+ // We also reset expectedSeq to this new one + 1?
215
+ // No, we better wait for KeyFrame.
216
+ // But we must update expectedSeq to avoid infinite loops if the stream just continues.
217
+ expectedSeq = (seq + 1) % 65536;
218
+ return;
219
+ }
220
+ }
221
+ expectedSeq = (seq + 1) % 65536;
222
+ const chunk = new window.EncodedVideoChunk({
223
+ type: isKey ? 'key' : 'delta',
224
+ timestamp: timestamp,
225
+ data: chunkData
226
+ });
227
+ if (decoder.state === 'configured' && !isKey && decoder.waitingForKeyFrame !== false) {
228
+ // Drop delta frames until we get a keyframe
229
+ const now = Date.now();
230
+ if (now - lastPli > 1000) {
231
+ console.log("[MediaTransport] Dropping delta frame, waiting for key frame. Requesting PLI.");
232
+ if (onKeyFrameRequest)
233
+ onKeyFrameRequest();
234
+ lastPli = now;
235
+ }
236
+ return;
237
+ }
238
+ if (isKey) {
239
+ decoder.waitingForKeyFrame = false;
240
+ }
241
+ try {
242
+ decoder.decode(chunk);
243
+ }
244
+ catch (e) {
245
+ console.error("[MediaTransport] Decode error:", e);
246
+ }
247
+ }
248
+ else {
249
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
250
+ const timestamp = Number(view.getBigInt64(1, false));
251
+ const chunkData = data.slice(9);
252
+ const chunk = new window.EncodedAudioChunk({
253
+ type: 'key', // Audio usually all key frames or dependent? Opus is complicated.
254
+ timestamp: timestamp,
255
+ data: chunkData
256
+ });
257
+ decoder.decode(chunk);
258
+ }
259
+ };
260
+ return generator;
261
+ }
262
+ }
@@ -0,0 +1,50 @@
1
+ import { Client } from '../core/Client';
2
+ import { Connection } from '../core/Connection';
3
+ export declare class PlutoPeerConnection extends EventTarget {
4
+ static defaultClient: Client | null;
5
+ static setDefaultClient(client: Client): void;
6
+ onicecandidate: ((ev: RTCPeerConnectionIceEvent) => any) | null;
7
+ oniceconnectionstatechange: ((ev: Event) => any) | null;
8
+ ontrack: ((ev: RTCTrackEvent) => any) | null;
9
+ onsignalingstatechange: ((ev: Event) => any) | null;
10
+ connectionState: RTCPeerConnectionState;
11
+ iceConnectionState: RTCIceConnectionState;
12
+ signalingState: RTCSignalingState;
13
+ localDescription: RTCSessionDescription | null;
14
+ remoteDescription: RTCSessionDescription | null;
15
+ private client;
16
+ private connection;
17
+ private get useMoq();
18
+ get transportInfo(): {
19
+ signaling: string;
20
+ media: string;
21
+ };
22
+ constructor(configuration?: RTCConfiguration, clientInstance?: Client);
23
+ createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit>;
24
+ createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit>;
25
+ setLocalDescription(desc: RTCSessionDescriptionInit): Promise<void>;
26
+ setRemoteDescription(desc: RTCSessionDescriptionInit): Promise<void>;
27
+ addIceCandidate(candidate: RTCIceCandidateInit | RTCIceCandidate): Promise<void>;
28
+ /**
29
+ * Manually provide an existing connection (e.g. from an incoming connection listener)
30
+ */
31
+ private bufferTracks;
32
+ handleIncomingConnection(connection: Connection): Promise<void>;
33
+ private receivers;
34
+ private setupHooks;
35
+ private handleLegacyMedia;
36
+ private moqDecoders;
37
+ private moqGenerators;
38
+ private handleMoQMedia;
39
+ private handleMoQData;
40
+ private fireTrackEvent;
41
+ addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender;
42
+ private mediaSenders;
43
+ private startIrohStream;
44
+ private startMoQStream;
45
+ createDataChannel(label: string): RTCDataChannel;
46
+ sendData(data: any): void;
47
+ private connect;
48
+ close(): void;
49
+ private updateState;
50
+ }