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.
- package/README.md +106 -0
- package/dist/OpenRTC.d.ts +71 -0
- package/dist/OpenRTC.js +130 -0
- package/dist/api/MediaTransport.d.ts +7 -0
- package/dist/api/MediaTransport.js +262 -0
- package/dist/api/PlutoPeerConnection.d.ts +50 -0
- package/dist/api/PlutoPeerConnection.js +434 -0
- package/dist/api/PlutoWebSocket.d.ts +24 -0
- package/dist/api/PlutoWebSocket.js +113 -0
- package/dist/api/PlutoWebTransport.d.ts +29 -0
- package/dist/api/PlutoWebTransport.js +105 -0
- package/dist/auth/host.d.ts +73 -0
- package/dist/auth/host.js +513 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/internal.d.ts +1 -0
- package/dist/auth/internal.js +1 -0
- package/dist/compat/index.d.ts +20 -0
- package/dist/compat/index.js +20 -0
- package/dist/core/Client.d.ts +352 -0
- package/dist/core/Client.js +3689 -0
- package/dist/core/Connection.d.ts +67 -0
- package/dist/core/Connection.js +404 -0
- package/dist/core/IEngineBridge.d.ts +18 -0
- package/dist/core/IEngineBridge.js +1 -0
- package/dist/core/ISignalingTransport.d.ts +13 -0
- package/dist/core/ISignalingTransport.js +1 -0
- package/dist/core/PeerLifecycleManager.d.ts +43 -0
- package/dist/core/PeerLifecycleManager.js +330 -0
- package/dist/core/PeerProjectionStore.d.ts +43 -0
- package/dist/core/PeerProjectionStore.js +327 -0
- package/dist/core/PlutoRTC.d.ts +13 -0
- package/dist/core/PlutoRTC.js +26 -0
- package/dist/core/Room.d.ts +47 -0
- package/dist/core/Room.js +376 -0
- package/dist/core/Signaling.d.ts +39 -0
- package/dist/core/Signaling.js +147 -0
- package/dist/core/bridges/BridgeCodecs.d.ts +19 -0
- package/dist/core/bridges/BridgeCodecs.js +317 -0
- package/dist/core/bridges/BrowserFirestoreRooms.d.ts +36 -0
- package/dist/core/bridges/BrowserFirestoreRooms.js +169 -0
- package/dist/core/bridges/BrowserFirestoreSignaling.d.ts +111 -0
- package/dist/core/bridges/BrowserFirestoreSignaling.js +1010 -0
- package/dist/core/bridges/NativeIpcBridge.d.ts +156 -0
- package/dist/core/bridges/NativeIpcBridge.js +1472 -0
- package/dist/core/bridges/RtdbPresence.d.ts +122 -0
- package/dist/core/bridges/RtdbPresence.js +201 -0
- package/dist/core/bridges/TauriBridge.d.ts +8 -0
- package/dist/core/bridges/TauriBridge.js +7 -0
- package/dist/core/bridges/WasmBridge.d.ts +233 -0
- package/dist/core/bridges/WasmBridge.js +1811 -0
- package/dist/core/config.d.ts +31 -0
- package/dist/core/config.js +138 -0
- package/dist/core/transports/ITransport.d.ts +12 -0
- package/dist/core/transports/ITransport.js +1 -0
- package/dist/core/transports/LocalDaemonBridge.d.ts +41 -0
- package/dist/core/transports/LocalDaemonBridge.js +227 -0
- package/dist/core/transports/TauriSignaling.d.ts +28 -0
- package/dist/core/transports/TauriSignaling.js +373 -0
- package/dist/core/transports/TransportFactory.d.ts +9 -0
- package/dist/core/transports/TransportFactory.js +59 -0
- package/dist/core/transports/WasmSignaling.d.ts +78 -0
- package/dist/core/transports/WasmSignaling.js +522 -0
- package/dist/core/transports/WebRTCTransport.d.ts +23 -0
- package/dist/core/transports/WebRTCTransport.js +132 -0
- package/dist/core/trust.d.ts +23 -0
- package/dist/core/trust.js +99 -0
- package/dist/core/types.d.ts +559 -0
- package/dist/core/types.js +19 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/moq/MoQMediaTransport.d.ts +6 -0
- package/dist/moq/MoQMediaTransport.js +106 -0
- package/dist/moq/MoQPublisher.d.ts +22 -0
- package/dist/moq/MoQPublisher.js +179 -0
- package/dist/moq/MoQSession.d.ts +35 -0
- package/dist/moq/MoQSession.js +166 -0
- package/dist/moq/MoQSubscriber.d.ts +19 -0
- package/dist/moq/MoQSubscriber.js +235 -0
- package/dist/moq/common.d.ts +47 -0
- package/dist/moq/common.js +304 -0
- package/dist/moq/messages.d.ts +105 -0
- package/dist/moq/messages.js +367 -0
- package/dist/presets/plutoHosted.d.ts +13 -0
- package/dist/presets/plutoHosted.js +18 -0
- package/dist/runtime/DelegatingRuntimeAdapter.d.ts +68 -0
- package/dist/runtime/DelegatingRuntimeAdapter.js +250 -0
- package/dist/runtime/IpcRuntimeAdapter.d.ts +9 -0
- package/dist/runtime/IpcRuntimeAdapter.js +13 -0
- package/dist/runtime/RuntimeClient.d.ts +257 -0
- package/dist/runtime/RuntimeClient.js +1484 -0
- package/dist/runtime/TauriRuntimeAdapter.d.ts +1 -0
- package/dist/runtime/TauriRuntimeAdapter.js +1 -0
- package/dist/runtime/WasmRuntimeAdapter.d.ts +6 -0
- package/dist/runtime/WasmRuntimeAdapter.js +11 -0
- package/dist/runtime/factories.d.ts +24 -0
- package/dist/runtime/factories.js +66 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/ipc.d.ts +8 -0
- package/dist/runtime/ipc.js +65 -0
- package/dist/runtime/ipcContract.d.ts +46 -0
- package/dist/runtime/ipcContract.js +1 -0
- package/dist/runtime/tauri.d.ts +16 -0
- package/dist/runtime/tauri.js +22 -0
- package/dist/runtime/types.d.ts +110 -0
- package/dist/runtime/types.js +1 -0
- package/dist/transports/TauriTransport.d.ts +17 -0
- package/dist/transports/TauriTransport.js +49 -0
- package/dist/transports/WebTransport.d.ts +23 -0
- package/dist/transports/WebTransport.js +71 -0
- package/dist/utils/debug.d.ts +4 -0
- package/dist/utils/debug.js +33 -0
- package/dist/utils/runtime.d.ts +1 -0
- package/dist/utils/runtime.js +10 -0
- package/package.json +97 -0
- package/wasm/README.md +64 -0
- package/wasm/openrtc.d.ts +284 -0
- package/wasm/openrtc.js +2132 -0
- package/wasm/openrtc_bg.wasm +0 -0
- package/wasm/openrtc_bg.wasm.d.ts +110 -0
- package/wasm/package.json +16 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class MoQMediaTransport {
|
|
2
|
+
constructor(publisher) {
|
|
3
|
+
this.publisher = publisher;
|
|
4
|
+
}
|
|
5
|
+
async sendTrack(track, namespace, name) {
|
|
6
|
+
// Create publisher track
|
|
7
|
+
const trackPub = await this.publisher.createTrack(namespace, name);
|
|
8
|
+
if (!trackPub) {
|
|
9
|
+
console.log(`[MoQ] Track ${namespace}/${name} already being published. Skipping.`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if ((typeof VideoEncoder === 'undefined') && (typeof AudioEncoder === 'undefined')) {
|
|
13
|
+
console.error("WebCodecs API not supported.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const processor = new window.MediaStreamTrackProcessor({ track });
|
|
17
|
+
const reader = processor.readable.getReader();
|
|
18
|
+
let encoder = null;
|
|
19
|
+
if (track.kind === 'video') {
|
|
20
|
+
encoder = new window.VideoEncoder({
|
|
21
|
+
output: (chunk, metadata) => {
|
|
22
|
+
// Framing: [timestamp 8B big-endian][encoded data]
|
|
23
|
+
const buffer = new ArrayBuffer(8 + chunk.byteLength);
|
|
24
|
+
const view = new DataView(buffer);
|
|
25
|
+
view.setBigInt64(0, BigInt(chunk.timestamp), false);
|
|
26
|
+
const data = new Uint8Array(buffer);
|
|
27
|
+
chunk.copyTo(new Uint8Array(buffer, 8));
|
|
28
|
+
trackPub.push(data, { isKey: chunk.type === 'key', timestamp: chunk.timestamp });
|
|
29
|
+
},
|
|
30
|
+
error: (e) => console.error("MoQ VideoEncoder error", e)
|
|
31
|
+
});
|
|
32
|
+
encoder.configure({
|
|
33
|
+
codec: 'vp09.00.10.08',
|
|
34
|
+
width: 640,
|
|
35
|
+
height: 480,
|
|
36
|
+
bitrate: 500000,
|
|
37
|
+
framerate: 24,
|
|
38
|
+
latencyMode: 'realtime',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Audio
|
|
43
|
+
let lastAudioKeyTimestamp = null;
|
|
44
|
+
const AUDIO_GROUP_INTERVAL_US = 100000; // ~100ms (matches reference moq-dev)
|
|
45
|
+
encoder = new window.AudioEncoder({
|
|
46
|
+
output: (chunk, metadata) => {
|
|
47
|
+
// Determine if this audio chunk starts a new group
|
|
48
|
+
let isKey = false;
|
|
49
|
+
if (lastAudioKeyTimestamp === null ||
|
|
50
|
+
chunk.timestamp - lastAudioKeyTimestamp >= AUDIO_GROUP_INTERVAL_US) {
|
|
51
|
+
isKey = true;
|
|
52
|
+
lastAudioKeyTimestamp = chunk.timestamp;
|
|
53
|
+
}
|
|
54
|
+
// Framing: [timestamp 8B big-endian][encoded data]
|
|
55
|
+
const buffer = new ArrayBuffer(8 + chunk.byteLength);
|
|
56
|
+
const view = new DataView(buffer);
|
|
57
|
+
view.setBigInt64(0, BigInt(chunk.timestamp), false);
|
|
58
|
+
const data = new Uint8Array(buffer);
|
|
59
|
+
chunk.copyTo(new Uint8Array(buffer, 8));
|
|
60
|
+
trackPub.push(data, { isKey, timestamp: chunk.timestamp });
|
|
61
|
+
},
|
|
62
|
+
error: (e) => console.error("MoQ AudioEncoder error", e)
|
|
63
|
+
});
|
|
64
|
+
encoder.configure({
|
|
65
|
+
codec: 'opus',
|
|
66
|
+
sampleRate: 48000,
|
|
67
|
+
numberOfChannels: 1
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Read Loop
|
|
71
|
+
(async () => {
|
|
72
|
+
let frameCount = 0;
|
|
73
|
+
try {
|
|
74
|
+
while (true) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done)
|
|
77
|
+
break;
|
|
78
|
+
if (value) {
|
|
79
|
+
try {
|
|
80
|
+
if (encoder && encoder.state !== 'closed') {
|
|
81
|
+
// For video: force a key frame every ~2 seconds (48 frames at 24fps)
|
|
82
|
+
if (track.kind === 'video' && frameCount % 48 === 0) {
|
|
83
|
+
encoder.encode(value, { keyFrame: true });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
encoder.encode(value);
|
|
87
|
+
}
|
|
88
|
+
frameCount++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
value.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error(`Read error`, e);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (trackPub && trackPub.close)
|
|
102
|
+
trackPub.close();
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MoQSession } from './MoQSession';
|
|
2
|
+
export declare class MoQPublisher {
|
|
3
|
+
private session;
|
|
4
|
+
private tracks;
|
|
5
|
+
private activePublishers;
|
|
6
|
+
constructor(session: MoQSession);
|
|
7
|
+
private pendingAnnounces;
|
|
8
|
+
announce(namespace: string): Promise<void>;
|
|
9
|
+
onAnnounceResult(namespace: string, error?: {
|
|
10
|
+
code: number;
|
|
11
|
+
reason: string;
|
|
12
|
+
}): void;
|
|
13
|
+
onSubscribe(msg: any): void;
|
|
14
|
+
onUnsubscribe(subscribeId: number): void;
|
|
15
|
+
createTrack(namespace: string, name: string): Promise<{
|
|
16
|
+
close: () => void;
|
|
17
|
+
push: (data: Uint8Array, init: {
|
|
18
|
+
isKey: boolean;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
} | null>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { BufferWriter, Mutex } from './common';
|
|
2
|
+
export class MoQPublisher {
|
|
3
|
+
constructor(session) {
|
|
4
|
+
this.tracks = new Map();
|
|
5
|
+
this.activePublishers = new Map();
|
|
6
|
+
this.pendingAnnounces = new Map();
|
|
7
|
+
this.session = session;
|
|
8
|
+
}
|
|
9
|
+
async announce(namespace) {
|
|
10
|
+
// Send ANNOUNCE (MsgType 0x6)
|
|
11
|
+
console.log(`[MoQ] Announcing namespace: ${namespace}`);
|
|
12
|
+
const msg = {
|
|
13
|
+
type: 0x6, // ANNOUNCE
|
|
14
|
+
trackNamespace: namespace,
|
|
15
|
+
params: {} // Draft-07: Announce Params
|
|
16
|
+
};
|
|
17
|
+
return new Promise(async (resolve, reject) => {
|
|
18
|
+
this.pendingAnnounces.set(namespace, { resolve, reject });
|
|
19
|
+
// Set a timeout strictly for the announce response
|
|
20
|
+
const timeout = setTimeout(() => {
|
|
21
|
+
if (this.pendingAnnounces.has(namespace)) {
|
|
22
|
+
this.pendingAnnounces.delete(namespace);
|
|
23
|
+
reject(new Error("Announce timed out"));
|
|
24
|
+
}
|
|
25
|
+
}, 5000);
|
|
26
|
+
try {
|
|
27
|
+
await this.session.sendControlMessage(msg);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
clearTimeout(timeout);
|
|
31
|
+
this.pendingAnnounces.delete(namespace);
|
|
32
|
+
reject(e);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
onAnnounceResult(namespace, error) {
|
|
37
|
+
const pending = this.pendingAnnounces.get(namespace);
|
|
38
|
+
if (pending) {
|
|
39
|
+
this.pendingAnnounces.delete(namespace);
|
|
40
|
+
if (error && error.code !== 0) { // Code 0 is usually success/no-error in some contexts, but here we expect OK msg for success.
|
|
41
|
+
// Actually ANNOUNCE_ERROR is strictly error.
|
|
42
|
+
pending.reject(new Error(`Announce failed: code=${error.code} reason=${error.reason}`));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
pending.resolve();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
onSubscribe(msg) {
|
|
50
|
+
const trackId = `${msg.trackNamespace}/${msg.trackName}`;
|
|
51
|
+
console.log(`[MoQ] Publisher received SUBSCRIBE for ${trackId} -> subId=${msg.subscribeId} alias=${msg.trackAlias}`);
|
|
52
|
+
this.tracks.set(trackId, { subscribeId: msg.subscribeId, trackAlias: msg.trackAlias });
|
|
53
|
+
}
|
|
54
|
+
onUnsubscribe(subscribeId) {
|
|
55
|
+
for (const [key, val] of this.tracks.entries()) {
|
|
56
|
+
if (val.subscribeId === subscribeId) {
|
|
57
|
+
console.log(`[MoQ] Publisher received UNSUBSCRIBE for ${key} (subId=${subscribeId})`);
|
|
58
|
+
this.tracks.delete(key);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async createTrack(namespace, name) {
|
|
64
|
+
const trackId = `${namespace}/${name}`;
|
|
65
|
+
if (this.activePublishers.has(trackId)) {
|
|
66
|
+
// console.log(`[MoQ] Already publishing ${trackId} - skipping duplicate publisher.`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// We return a pusher immediately. Data will be dropped inside push() if no subscriber exists.
|
|
70
|
+
// Log occasionally?
|
|
71
|
+
// console.log(`[MoQ] Push ${data.byteLength} bytes to ${trackId}`);
|
|
72
|
+
let currentGroupWriter = null;
|
|
73
|
+
let currentGroupId = 0;
|
|
74
|
+
let currentObjectId = 0;
|
|
75
|
+
const mutex = new Mutex(); // Serialize access to writer
|
|
76
|
+
const closeCurrentGroup = () => {
|
|
77
|
+
if (currentGroupWriter) {
|
|
78
|
+
const oldWriter = currentGroupWriter;
|
|
79
|
+
currentGroupWriter = null;
|
|
80
|
+
// Fire-and-forget: close old stream in background
|
|
81
|
+
// This prevents blocking audio push() during group transition
|
|
82
|
+
oldWriter.close().catch(() => { });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
console.log(`[MoQ] Stopping publisher for ${trackId}`);
|
|
87
|
+
closeCurrentGroup();
|
|
88
|
+
this.activePublishers.delete(trackId);
|
|
89
|
+
};
|
|
90
|
+
const openNewGroup = async () => {
|
|
91
|
+
closeCurrentGroup();
|
|
92
|
+
currentGroupId++;
|
|
93
|
+
currentObjectId = 0;
|
|
94
|
+
// Resolve track info dynamically
|
|
95
|
+
const trackInfo = this.tracks.get(trackId);
|
|
96
|
+
if (trackInfo === undefined) {
|
|
97
|
+
// console.warn(`[MoQ] Dropping group for ${trackId} - No alias/subscription`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { subscribeId, trackAlias: currentAlias } = trackInfo;
|
|
101
|
+
// Open UniStream
|
|
102
|
+
const stream = await this.session.transport.createUnidirectionalStream();
|
|
103
|
+
const writer = stream.getWriter();
|
|
104
|
+
if (currentGroupId <= 2) {
|
|
105
|
+
console.log(`[MoQ] Opening Group ${currentGroupId} for ${trackId} (subId=${subscribeId} alias=${currentAlias})`);
|
|
106
|
+
}
|
|
107
|
+
const w = new BufferWriter();
|
|
108
|
+
w.writeVarInt(0x04); // StreamType: STREAM_HEADER_SUBGROUP (0x4)
|
|
109
|
+
w.writeVarInt(subscribeId); // Subscribe ID (Draft-07)
|
|
110
|
+
w.writeVarInt(currentAlias); // Track Alias
|
|
111
|
+
w.writeVarInt(currentGroupId); // Group ID
|
|
112
|
+
w.writeVarInt(0); // Subgroup ID (0 for now)
|
|
113
|
+
w.writeUint8(0); // Publisher Priority (8-bit)
|
|
114
|
+
await writer.write(w.toUint8Array());
|
|
115
|
+
currentGroupWriter = writer;
|
|
116
|
+
};
|
|
117
|
+
const publisher = {
|
|
118
|
+
close: cleanup,
|
|
119
|
+
push: async (data, init) => {
|
|
120
|
+
await mutex.dispatch(async () => {
|
|
121
|
+
// Check if we have a valid alias (subscription active)
|
|
122
|
+
// If not, we MUST NOT send data (Relay will STOP_SENDING)
|
|
123
|
+
if (!this.tracks.has(trackId)) {
|
|
124
|
+
// We are not subscribed yet. Drop data.
|
|
125
|
+
// Ideally we buffer keyframes, but for now just drop to avoid crash.
|
|
126
|
+
// process.stdout.write('.'); // dropped
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Decision logic:
|
|
130
|
+
// 1. If isKey, we MUST start a new group (GOP boundary).
|
|
131
|
+
// 2. If !isKey but we have a writer, we continue current group.
|
|
132
|
+
// 3. If !isKey and NO writer, we MUST DROP the frame (cannot start group with delta).
|
|
133
|
+
if (init.isKey) {
|
|
134
|
+
await openNewGroup();
|
|
135
|
+
}
|
|
136
|
+
else if (!currentGroupWriter) {
|
|
137
|
+
// Orphan delta — drop it to enforce Group=Keyframe start
|
|
138
|
+
// This happens if group was closed (error/timeout) or joining mid-stream logic
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// If openNewGroup failed (e.g. no alias), currentGroupWriter is null
|
|
142
|
+
if (!currentGroupWriter)
|
|
143
|
+
return;
|
|
144
|
+
// Write Object
|
|
145
|
+
// ObjectHeader:
|
|
146
|
+
// VarInt ObjectId
|
|
147
|
+
// VarInt Length
|
|
148
|
+
// Payload
|
|
149
|
+
const w = new BufferWriter();
|
|
150
|
+
w.writeVarInt(currentObjectId++);
|
|
151
|
+
w.writeVarInt(data.length);
|
|
152
|
+
w.writeBytes(data);
|
|
153
|
+
if (currentGroupWriter) {
|
|
154
|
+
try {
|
|
155
|
+
// @ts-ignore
|
|
156
|
+
await currentGroupWriter.write(w.toUint8Array());
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
// Check for STOP_SENDING or similar WebTransport errors
|
|
160
|
+
const msg = e.message || "";
|
|
161
|
+
if (msg.includes("STOP_SENDING") || e.name === "WebTransportError") {
|
|
162
|
+
// STOP_SENDING on a Group Stream just means the subscriber dropped this specific group
|
|
163
|
+
// (e.g. because it was late or they skipped ahead).
|
|
164
|
+
// We should continue publishing the next group.
|
|
165
|
+
console.warn(`[MoQ] Group stream closed by remote (STOP_SENDING) for ${trackId}.`);
|
|
166
|
+
currentGroupWriter = null;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error(`[MoQ] Writer error for ${trackId}:`, e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
this.activePublishers.set(trackId, publisher);
|
|
177
|
+
return publisher;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MoQPublisher } from './MoQPublisher';
|
|
2
|
+
import { MoQSubscriber } from './MoQSubscriber';
|
|
3
|
+
export interface TransportInterface {
|
|
4
|
+
createBidirectionalStream(): Promise<{
|
|
5
|
+
readable: ReadableStream<Uint8Array>;
|
|
6
|
+
writable: WritableStream<Uint8Array>;
|
|
7
|
+
}>;
|
|
8
|
+
createUnidirectionalStream(): Promise<WritableStream<Uint8Array>>;
|
|
9
|
+
incomingBidirectionalStreams: ReadableStream<any>;
|
|
10
|
+
incomingUnidirectionalStreams: ReadableStream<ReadableStream<Uint8Array>>;
|
|
11
|
+
close(): void;
|
|
12
|
+
ready: Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare class MoQSession {
|
|
15
|
+
transport: TransportInterface;
|
|
16
|
+
private controlStreamWriter;
|
|
17
|
+
private controlStreamReader;
|
|
18
|
+
private role;
|
|
19
|
+
setupComplete: boolean;
|
|
20
|
+
private setupResolve;
|
|
21
|
+
private setupPromise;
|
|
22
|
+
maxSubscribeId: number;
|
|
23
|
+
private streamBuffer;
|
|
24
|
+
publisher: MoQPublisher;
|
|
25
|
+
subscriber: MoQSubscriber;
|
|
26
|
+
constructor(transport: TransportInterface, onObject?: (ns: string, name: string, obj: Uint8Array, meta: any) => void, role?: number);
|
|
27
|
+
/** Wait for handshake to complete before sending ANNOUNCE/SUBSCRIBE */
|
|
28
|
+
waitForSetup(): Promise<void>;
|
|
29
|
+
init(): Promise<void>;
|
|
30
|
+
private readUniStreams;
|
|
31
|
+
sendControlMessage(msg: any): Promise<void>;
|
|
32
|
+
private readLoop;
|
|
33
|
+
private processBuffer;
|
|
34
|
+
private handleControlMessage;
|
|
35
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { MoQSerializer, MsgType } from './messages';
|
|
2
|
+
import { StreamBuffer } from './common';
|
|
3
|
+
import { MoQPublisher } from './MoQPublisher';
|
|
4
|
+
import { MoQSubscriber } from './MoQSubscriber';
|
|
5
|
+
export class MoQSession {
|
|
6
|
+
constructor(transport, onObject, role = 3) {
|
|
7
|
+
this.controlStreamWriter = null;
|
|
8
|
+
this.controlStreamReader = null;
|
|
9
|
+
this.setupComplete = false;
|
|
10
|
+
this.setupResolve = null;
|
|
11
|
+
this.maxSubscribeId = 0;
|
|
12
|
+
this.streamBuffer = new StreamBuffer();
|
|
13
|
+
this.transport = transport;
|
|
14
|
+
this.role = role;
|
|
15
|
+
this.setupPromise = new Promise((resolve) => {
|
|
16
|
+
this.setupResolve = resolve;
|
|
17
|
+
});
|
|
18
|
+
this.publisher = new MoQPublisher(this);
|
|
19
|
+
this.subscriber = new MoQSubscriber(this, onObject || ((ns, n, o, m) => console.log(`[MoQ] Object: ${ns}/${n}`, m)));
|
|
20
|
+
}
|
|
21
|
+
/** Wait for handshake to complete before sending ANNOUNCE/SUBSCRIBE */
|
|
22
|
+
async waitForSetup() {
|
|
23
|
+
return this.setupPromise;
|
|
24
|
+
}
|
|
25
|
+
async init() {
|
|
26
|
+
await this.transport.ready;
|
|
27
|
+
// Initiate Control Stream
|
|
28
|
+
// For Client, we open a BiStream.
|
|
29
|
+
const stream = await this.transport.createBidirectionalStream();
|
|
30
|
+
this.controlStreamWriter = stream.writable.getWriter();
|
|
31
|
+
this.controlStreamReader = stream.readable.getReader();
|
|
32
|
+
// Send CLIENT_SETUP
|
|
33
|
+
const setupMsg = {
|
|
34
|
+
type: MsgType.CLIENT_SETUP,
|
|
35
|
+
supportedVersions: [0xff000007],
|
|
36
|
+
role: this.role,
|
|
37
|
+
};
|
|
38
|
+
await this.sendControlMessage(setupMsg);
|
|
39
|
+
// Start Reading Control
|
|
40
|
+
this.readLoop();
|
|
41
|
+
// Start Reading UniStreams (Data)
|
|
42
|
+
this.readUniStreams();
|
|
43
|
+
}
|
|
44
|
+
async readUniStreams() {
|
|
45
|
+
const uniReader = this.transport.incomingUnidirectionalStreams.getReader();
|
|
46
|
+
try {
|
|
47
|
+
while (true) {
|
|
48
|
+
const { done, value } = await uniReader.read();
|
|
49
|
+
if (done)
|
|
50
|
+
break;
|
|
51
|
+
if (value && this.subscriber) {
|
|
52
|
+
this.subscriber.handleIncomingUniStream(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error("UniStream Loop Error", e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async sendControlMessage(msg) {
|
|
61
|
+
const bytes = MoQSerializer.serialize(msg);
|
|
62
|
+
if (!this.controlStreamWriter)
|
|
63
|
+
throw new Error("Control stream not open");
|
|
64
|
+
await this.controlStreamWriter.write(bytes);
|
|
65
|
+
}
|
|
66
|
+
async readLoop() {
|
|
67
|
+
if (!this.controlStreamReader)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
while (true) {
|
|
71
|
+
const { done, value } = await this.controlStreamReader.read();
|
|
72
|
+
if (done) {
|
|
73
|
+
console.warn("[MoQ] Control Stream Closed by Remote");
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (value) {
|
|
77
|
+
console.log(`[MoQ] Control Stream RX: ${value.byteLength} bytes`);
|
|
78
|
+
this.streamBuffer.append(value);
|
|
79
|
+
this.processBuffer();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error("Control stream error", e);
|
|
85
|
+
}
|
|
86
|
+
console.log("[MoQ] Read Loop Exited");
|
|
87
|
+
}
|
|
88
|
+
processBuffer() {
|
|
89
|
+
while (true) {
|
|
90
|
+
// console.log(`[MoQ] Processing Buffer. Offset: ${(this.streamBuffer as any).offset}, Remaining: ${(this.streamBuffer as any).buffer.length - (this.streamBuffer as any).offset}`);
|
|
91
|
+
// Try to deserialize a message
|
|
92
|
+
let msg;
|
|
93
|
+
try {
|
|
94
|
+
msg = MoQSerializer.deserialize(this.streamBuffer);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
console.error("[MoQ] Deserialization Error:", e);
|
|
98
|
+
// fatal?
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (!msg) {
|
|
102
|
+
// Not enough data yet (encountered EOF during parse)
|
|
103
|
+
// Rewind happens in deserialize if it catches EOF
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
// Msg Successfully Parsed
|
|
107
|
+
this.handleControlMessage(msg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
handleControlMessage(msg) {
|
|
111
|
+
switch (msg.type) {
|
|
112
|
+
case MsgType.SERVER_SETUP:
|
|
113
|
+
console.log(`[MoQ] Handshake Complete. Server selected version: 0x${msg.selectedVersion.toString(16)}`);
|
|
114
|
+
this.setupComplete = true;
|
|
115
|
+
if (this.setupResolve) {
|
|
116
|
+
this.setupResolve();
|
|
117
|
+
this.setupResolve = null;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case MsgType.MAX_SUBSCRIBE_ID:
|
|
121
|
+
this.maxSubscribeId = msg.subscribeId;
|
|
122
|
+
console.log(`[MoQ] Max Subscribe ID updated: ${this.maxSubscribeId}`);
|
|
123
|
+
break;
|
|
124
|
+
case MsgType.SUBSCRIBE:
|
|
125
|
+
// Relay (or Peer) wants to subscribe to our track.
|
|
126
|
+
this.publisher.onSubscribe(msg);
|
|
127
|
+
// Send SUBSCRIBE_OK
|
|
128
|
+
const subOk = {
|
|
129
|
+
type: MsgType.SUBSCRIBE_OK,
|
|
130
|
+
subscribeId: msg.subscribeId,
|
|
131
|
+
expires: 0
|
|
132
|
+
};
|
|
133
|
+
this.sendControlMessage(subOk).catch(console.error);
|
|
134
|
+
break;
|
|
135
|
+
case MsgType.SUBSCRIBE_OK:
|
|
136
|
+
console.log(`[MoQ] SUBSCRIBE_OK for id=${msg.subscribeId}`);
|
|
137
|
+
this.subscriber.onSubscribeOk(msg.subscribeId);
|
|
138
|
+
break;
|
|
139
|
+
case MsgType.SUBSCRIBE_ERROR:
|
|
140
|
+
console.error(`[MoQ] SUBSCRIBE_ERROR for id=${msg.subscribeId} code=${msg.code}`);
|
|
141
|
+
this.subscriber.onSubscribeError(msg.subscribeId, msg.code).catch(console.error);
|
|
142
|
+
break;
|
|
143
|
+
case MsgType.ANNOUNCE_OK:
|
|
144
|
+
console.log(`[MoQ] ANNOUNCE_OK for ns=${msg.trackNamespace}`);
|
|
145
|
+
this.publisher.onAnnounceResult(msg.trackNamespace);
|
|
146
|
+
break;
|
|
147
|
+
case MsgType.ANNOUNCE_ERROR:
|
|
148
|
+
console.error(`[MoQ] ANNOUNCE_ERROR for ns=${msg.trackNamespace} code=${msg.code}`);
|
|
149
|
+
this.publisher.onAnnounceResult(msg.trackNamespace, { code: msg.code, reason: msg.reasonPhrase || "Unknown" });
|
|
150
|
+
break;
|
|
151
|
+
case MsgType.UNSUBSCRIBE:
|
|
152
|
+
// Relay (or Peer) wants to stop receiving our track.
|
|
153
|
+
this.publisher.onUnsubscribe(msg.subscribeId);
|
|
154
|
+
break;
|
|
155
|
+
case MsgType.SUBSCRIBE_DONE:
|
|
156
|
+
// Relay (or Peer) says the stream is finished.
|
|
157
|
+
this.subscriber.onSubscribeDone(msg.subscribeId, msg.code, msg.reason);
|
|
158
|
+
break;
|
|
159
|
+
case MsgType.GOAWAY:
|
|
160
|
+
console.warn(`[MoQ] GOAWAY received`);
|
|
161
|
+
break;
|
|
162
|
+
default:
|
|
163
|
+
console.log(`[MoQ] Received Control Message type=0x${msg.type.toString(16)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MoQSession } from './MoQSession';
|
|
2
|
+
export declare class MoQSubscriber {
|
|
3
|
+
private session;
|
|
4
|
+
private onObject;
|
|
5
|
+
constructor(session: MoQSession, onObject: (ns: string, name: string, obj: Uint8Array, meta: any) => void);
|
|
6
|
+
private aliasMap;
|
|
7
|
+
private nextAlias;
|
|
8
|
+
private nextSubscribeId;
|
|
9
|
+
private pendingSubscriptions;
|
|
10
|
+
private deliveryState;
|
|
11
|
+
private activeSubscriptions;
|
|
12
|
+
subscribe(namespace: string, name: string): Promise<void>;
|
|
13
|
+
onSubscribeOk(subscribeId: number): void;
|
|
14
|
+
onSubscribeError(subscribeId: number, code: number): Promise<void>;
|
|
15
|
+
onSubscribeDone(subscribeId: number, code: number, reason: string): void;
|
|
16
|
+
private wakeDelivery;
|
|
17
|
+
private runDeliveryLoop;
|
|
18
|
+
handleIncomingUniStream(stream: ReadableStream<Uint8Array>): Promise<void>;
|
|
19
|
+
}
|