@pjts/webrtc-client 0.0.0-stage → 0.0.2
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 +8 -2
- package/dist/channels.d.ts +20 -0
- package/dist/channels.js +56 -0
- package/dist/channels.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/media/audio.d.ts +25 -0
- package/dist/media/audio.js +50 -0
- package/dist/media/audio.js.map +1 -0
- package/dist/media/compose.d.ts +25 -0
- package/dist/media/compose.js +52 -0
- package/dist/media/compose.js.map +1 -0
- package/dist/media/quality.d.ts +83 -0
- package/dist/media/quality.js +156 -0
- package/dist/media/quality.js.map +1 -0
- package/dist/media/recorder.d.ts +38 -0
- package/dist/media/recorder.js +74 -0
- package/dist/media/recorder.js.map +1 -0
- package/dist/media/speaking.d.ts +29 -0
- package/dist/media/speaking.js +49 -0
- package/dist/media/speaking.js.map +1 -0
- package/dist/peer.d.ts +52 -0
- package/dist/peer.js +163 -0
- package/dist/peer.js.map +1 -0
- package/dist/sequencer.d.ts +18 -0
- package/dist/sequencer.js +53 -0
- package/dist/sequencer.js.map +1 -0
- package/dist/signalling.d.ts +46 -0
- package/dist/signalling.js +90 -0
- package/dist/signalling.js.map +1 -0
- package/package.json +35 -4
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Domain-Driven Design TypeScript Utilities / WebRTC Client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-free browser building blocks for two-party WebRTC calls, pairing with `@pjts/webrtc-signalling`:
|
|
4
|
+
|
|
5
|
+
- `createSignalling` — room inbox over `@pjts/websocket-client` with replay cursor, exactly-once delivery and an injected poll fallback
|
|
6
|
+
- `createPeer` — `RTCPeerConnection` with perfect negotiation (polite/impolite, optional non-initiator), ICE trickle, ICE restart, stats sampler
|
|
7
|
+
- `createChannels` — negotiated JSON data channels with fixed ids and an open-queue
|
|
8
|
+
- `createAudioMeter`, `createSpeakingDetector`, `mixTracks`, `composeVideos` — media utilities
|
|
9
|
+
- `createRecorder` — `MediaRecorder` wrapper with pause/resume and pause-free duration
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ChannelSpec {
|
|
2
|
+
/** Negotiated id — both peers must use the same id for the same name. */
|
|
3
|
+
id: number;
|
|
4
|
+
ordered?: boolean;
|
|
5
|
+
/** Messages buffered while the channel is not open; oldest dropped beyond this. Default 50. */
|
|
6
|
+
maxQueued?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TypedChannel<T> {
|
|
9
|
+
send(message: T): void;
|
|
10
|
+
onMessage(listener: (message: T) => void): () => void;
|
|
11
|
+
/** Called on every `open` — including after an ICE restart re-opened the channel. */
|
|
12
|
+
onOpen(listener: () => void): () => void;
|
|
13
|
+
isOpen(): boolean;
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* JSON data channels created with `negotiated: true` and fixed ids, so both peers create them
|
|
18
|
+
* independently — no in-band announcement, no ordering dependency on who connects first.
|
|
19
|
+
*/
|
|
20
|
+
export declare const createChannels: <M extends Record<string, unknown>>(pc: Pick<RTCPeerConnection, 'createDataChannel'>, spec: { [K in keyof M]: ChannelSpec; }) => { [K_1 in keyof M]: TypedChannel<M[K_1]>; };
|
package/dist/channels.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createChannels = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* JSON data channels created with `negotiated: true` and fixed ids, so both peers create them
|
|
6
|
+
* independently — no in-band announcement, no ordering dependency on who connects first.
|
|
7
|
+
*/
|
|
8
|
+
const createChannels = (pc, spec) => {
|
|
9
|
+
const result = {};
|
|
10
|
+
Object.keys(spec).forEach(name => {
|
|
11
|
+
const { id, ordered = true, maxQueued = 50 } = spec[name];
|
|
12
|
+
const dc = pc.createDataChannel(String(name), { negotiated: true, id, ordered });
|
|
13
|
+
const queue = [];
|
|
14
|
+
const messageListeners = new Set();
|
|
15
|
+
const openListeners = new Set();
|
|
16
|
+
dc.onopen = () => {
|
|
17
|
+
queue.splice(0).forEach(raw => dc.send(raw));
|
|
18
|
+
openListeners.forEach(l => l());
|
|
19
|
+
};
|
|
20
|
+
dc.onmessage = event => {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(String(event.data));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
messageListeners.forEach(l => l(parsed));
|
|
29
|
+
};
|
|
30
|
+
result[name] = {
|
|
31
|
+
send(message) {
|
|
32
|
+
const raw = JSON.stringify(message);
|
|
33
|
+
if (dc.readyState === 'open') {
|
|
34
|
+
dc.send(raw);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
queue.push(raw);
|
|
38
|
+
if (queue.length > maxQueued)
|
|
39
|
+
queue.shift();
|
|
40
|
+
},
|
|
41
|
+
onMessage(listener) {
|
|
42
|
+
messageListeners.add(listener);
|
|
43
|
+
return () => messageListeners.delete(listener);
|
|
44
|
+
},
|
|
45
|
+
onOpen(listener) {
|
|
46
|
+
openListeners.add(listener);
|
|
47
|
+
return () => openListeners.delete(listener);
|
|
48
|
+
},
|
|
49
|
+
isOpen: () => dc.readyState === 'open',
|
|
50
|
+
close: () => dc.close(),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
exports.createChannels = createChannels;
|
|
56
|
+
//# sourceMappingURL=channels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../src/channels.ts"],"names":[],"mappings":";;;AAiBA;;;GAGG;AACI,MAAM,cAAc,GAAG,CAC5B,EAAgD,EAChD,IAAqC,EACG,EAAE;IAC1C,MAAM,MAAM,GAAG,EAA4C,CAC1D;IAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAoB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QACpD,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAA;QACzD,MAAM,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;QAChF,MAAM,KAAK,GAAa,EAAE,CAAA;QAC1B,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAA+B,CAAA;QAC/D,MAAM,aAAa,GAAG,IAAI,GAAG,EAAc,CAAA;QAE3C,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE;YACf,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;YAC5C,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;QACjC,CAAC,CAAA;QACD,EAAE,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE;YACrB,IAAI,MAAsB,CAAA;YAC1B,IAAI;gBACF,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;aACxC;YAAC,MAAM;gBACN,OAAM;aACP;YACD,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;QAC1C,CAAC,CAAA;QAED,MAAM,CAAC,IAAI,CAAC,GAAG;YACb,IAAI,CAAC,OAAO;gBACV,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;gBACnC,IAAI,EAAE,CAAC,UAAU,KAAK,MAAM,EAAE;oBAC5B,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;oBACZ,OAAM;iBACP;gBACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBACf,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS;oBAAE,KAAK,CAAC,KAAK,EAAE,CAAA;YAC7C,CAAC;YACD,SAAS,CAAC,QAAQ;gBAChB,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;gBAC9B,OAAO,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;YAChD,CAAC;YACD,MAAM,CAAC,QAAQ;gBACb,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;gBAC3B,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;YAC7C,CAAC;YACD,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,UAAU,KAAK,MAAM;YACtC,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE;SACxB,CAAA;IACH,CAAC,CAAC,CAAA;IACF,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAjDY,QAAA,cAAc,kBAiD1B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './sequencer';
|
|
2
|
+
export * from './signalling';
|
|
3
|
+
export * from './peer';
|
|
4
|
+
export * from './channels';
|
|
5
|
+
export * from './media/speaking';
|
|
6
|
+
export * from './media/audio';
|
|
7
|
+
export * from './media/compose';
|
|
8
|
+
export * from './media/recorder';
|
|
9
|
+
export * from './media/quality';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./sequencer"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./signalling"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./peer"), exports);
|
|
7
|
+
tslib_1.__exportStar(require("./channels"), exports);
|
|
8
|
+
tslib_1.__exportStar(require("./media/speaking"), exports);
|
|
9
|
+
tslib_1.__exportStar(require("./media/audio"), exports);
|
|
10
|
+
tslib_1.__exportStar(require("./media/compose"), exports);
|
|
11
|
+
tslib_1.__exportStar(require("./media/recorder"), exports);
|
|
12
|
+
tslib_1.__exportStar(require("./media/quality"), exports);
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,sDAA2B;AAC3B,uDAA4B;AAC5B,iDAAsB;AACtB,qDAA0B;AAC1B,2DAAgC;AAChC,wDAA6B;AAC7B,0DAA+B;AAC/B,2DAAgC;AAChC,0DAA+B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LevelSample, SpeakingDetectorOptions } from './speaking';
|
|
2
|
+
export interface AudioMeterOptions extends SpeakingDetectorOptions {
|
|
3
|
+
audioContext: AudioContext;
|
|
4
|
+
/** Sampling interval. Default 50 ms. */
|
|
5
|
+
intervalMs?: number;
|
|
6
|
+
onLevel?: (sample: LevelSample & {
|
|
7
|
+
speaking: boolean;
|
|
8
|
+
}) => void;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
/** RMS level + speaking flag of one audio track, sampled from an `AnalyserNode`. */
|
|
12
|
+
export declare const createAudioMeter: (track: MediaStreamTrack, options: AudioMeterOptions) => {
|
|
13
|
+
detector: {
|
|
14
|
+
push(sample: LevelSample): boolean;
|
|
15
|
+
isSpeaking: () => boolean;
|
|
16
|
+
floorDb: () => number;
|
|
17
|
+
samplesBetween: (from: number, to: number) => LevelSample[];
|
|
18
|
+
};
|
|
19
|
+
stop(): void;
|
|
20
|
+
};
|
|
21
|
+
/** Sums audio tracks into one stream (for recording or a mixed speech-recognition input). */
|
|
22
|
+
export declare const mixTracks: (tracks: MediaStreamTrack[], audioContext: AudioContext) => {
|
|
23
|
+
stream: MediaStream;
|
|
24
|
+
stop: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mixTracks = exports.createAudioMeter = void 0;
|
|
4
|
+
const speaking_1 = require("./speaking");
|
|
5
|
+
/** RMS level + speaking flag of one audio track, sampled from an `AnalyserNode`. */
|
|
6
|
+
const createAudioMeter = (track, options) => {
|
|
7
|
+
const ctx = options.audioContext;
|
|
8
|
+
const now = options.now ?? (() => performance.now());
|
|
9
|
+
const source = ctx.createMediaStreamSource(new MediaStream([track]));
|
|
10
|
+
const analyser = ctx.createAnalyser();
|
|
11
|
+
analyser.fftSize = 1024;
|
|
12
|
+
source.connect(analyser);
|
|
13
|
+
const buffer = new Float32Array(analyser.fftSize);
|
|
14
|
+
const detector = (0, speaking_1.createSpeakingDetector)(options);
|
|
15
|
+
const timer = setInterval(() => {
|
|
16
|
+
analyser.getFloatTimeDomainData(buffer);
|
|
17
|
+
let sum = 0;
|
|
18
|
+
for (let i = 0; i < buffer.length; i++)
|
|
19
|
+
sum += buffer[i] * buffer[i];
|
|
20
|
+
const sample = { t: now(), rms: Math.sqrt(sum / buffer.length) };
|
|
21
|
+
const speaking = detector.push(sample);
|
|
22
|
+
options.onLevel?.({ ...sample, speaking });
|
|
23
|
+
}, options.intervalMs ?? 50);
|
|
24
|
+
return {
|
|
25
|
+
detector,
|
|
26
|
+
stop() {
|
|
27
|
+
clearInterval(timer);
|
|
28
|
+
source.disconnect();
|
|
29
|
+
analyser.disconnect();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
exports.createAudioMeter = createAudioMeter;
|
|
34
|
+
/** Sums audio tracks into one stream (for recording or a mixed speech-recognition input). */
|
|
35
|
+
const mixTracks = (tracks, audioContext) => {
|
|
36
|
+
const destination = audioContext.createMediaStreamDestination();
|
|
37
|
+
const sources = tracks
|
|
38
|
+
.filter(t => t.kind === 'audio')
|
|
39
|
+
.map(track => {
|
|
40
|
+
const source = audioContext.createMediaStreamSource(new MediaStream([track]));
|
|
41
|
+
source.connect(destination);
|
|
42
|
+
return source;
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
stream: destination.stream,
|
|
46
|
+
stop: () => sources.forEach(s => s.disconnect()),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
exports.mixTracks = mixTracks;
|
|
50
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../../src/media/audio.ts"],"names":[],"mappings":";;;AAAA,yCAAyF;AAUzF,oFAAoF;AAC7E,MAAM,gBAAgB,GAAG,CAAC,KAAuB,EAAE,OAA0B,EAAE,EAAE;IACtF,MAAM,GAAG,GAAG,OAAO,CAAC,YAAY,CAAA;IAChC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAA;IACpD,MAAM,MAAM,GAAG,GAAG,CAAC,uBAAuB,CAAC,IAAI,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACpE,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,EAAE,CAAA;IACrC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACxB,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACjD,MAAM,QAAQ,GAAG,IAAA,iCAAsB,EAAC,OAAO,CAAC,CAAA;IAEhD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,QAAQ,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAA;QACvC,IAAI,GAAG,GAAG,CAAC,CAAA;QACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;YAAE,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;QACpE,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAA;QAChE,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACtC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC5C,CAAC,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,CAAA;IAE5B,OAAO;QACL,QAAQ;QACR,IAAI;YACF,aAAa,CAAC,KAAK,CAAC,CAAA;YACpB,MAAM,CAAC,UAAU,EAAE,CAAA;YACnB,QAAQ,CAAC,UAAU,EAAE,CAAA;QACvB,CAAC;KACF,CAAA;AACH,CAAC,CAAA;AA3BY,QAAA,gBAAgB,oBA2B5B;AAED,6FAA6F;AACtF,MAAM,SAAS,GAAG,CAAC,MAA0B,EAAE,YAA0B,EAAE,EAAE;IAClF,MAAM,WAAW,GAAG,YAAY,CAAC,4BAA4B,EAAE,CAAA;IAC/D,MAAM,OAAO,GAAG,MAAM;SACnB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC;SAC/B,GAAG,CAAC,KAAK,CAAC,EAAE;QACX,MAAM,MAAM,GAAG,YAAY,CAAC,uBAAuB,CAAC,IAAI,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAC7E,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAC3B,OAAO,MAAM,CAAA;IACf,CAAC,CAAC,CAAA;IACJ,OAAO;QACL,MAAM,EAAE,WAAW,CAAC,MAAM;QAC1B,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;KACjD,CAAA;AACH,CAAC,CAAA;AAbY,QAAA,SAAS,aAarB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ComposeOptions {
|
|
2
|
+
width?: number;
|
|
3
|
+
height?: number;
|
|
4
|
+
fps?: number;
|
|
5
|
+
/** Canvas background (letterboxing). Default near-black. */
|
|
6
|
+
background?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Tile {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
w: number;
|
|
12
|
+
h: number;
|
|
13
|
+
}
|
|
14
|
+
/** Fit a source of `sw×sh` into a box, preserving aspect ratio (contain). */
|
|
15
|
+
export declare const fitContain: (sw: number, sh: number, box: Tile) => Tile;
|
|
16
|
+
/** Equal side-by-side boxes for `count` sources on a `width×height` canvas. */
|
|
17
|
+
export declare const sideBySide: (count: number, width: number, height: number) => Tile[];
|
|
18
|
+
/**
|
|
19
|
+
* Draws the given `<video>` elements side by side on a canvas and returns its captured stream — a single
|
|
20
|
+
* video track for `MediaRecorder`. Sources whose video is missing are left as background.
|
|
21
|
+
*/
|
|
22
|
+
export declare const composeVideos: (videos: HTMLVideoElement[], options?: ComposeOptions) => {
|
|
23
|
+
stream: MediaStream;
|
|
24
|
+
stop(): void;
|
|
25
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.composeVideos = exports.sideBySide = exports.fitContain = void 0;
|
|
4
|
+
/** Fit a source of `sw×sh` into a box, preserving aspect ratio (contain). */
|
|
5
|
+
const fitContain = (sw, sh, box) => {
|
|
6
|
+
if (!sw || !sh)
|
|
7
|
+
return box;
|
|
8
|
+
const scale = Math.min(box.w / sw, box.h / sh);
|
|
9
|
+
const w = sw * scale;
|
|
10
|
+
const h = sh * scale;
|
|
11
|
+
return { x: box.x + (box.w - w) / 2, y: box.y + (box.h - h) / 2, w, h };
|
|
12
|
+
};
|
|
13
|
+
exports.fitContain = fitContain;
|
|
14
|
+
/** Equal side-by-side boxes for `count` sources on a `width×height` canvas. */
|
|
15
|
+
const sideBySide = (count, width, height) => Array.from({ length: count }, (_, i) => ({ x: (width / count) * i, y: 0, w: width / count, h: height }));
|
|
16
|
+
exports.sideBySide = sideBySide;
|
|
17
|
+
/**
|
|
18
|
+
* Draws the given `<video>` elements side by side on a canvas and returns its captured stream — a single
|
|
19
|
+
* video track for `MediaRecorder`. Sources whose video is missing are left as background.
|
|
20
|
+
*/
|
|
21
|
+
const composeVideos = (videos, options = {}) => {
|
|
22
|
+
const width = options.width ?? 1280;
|
|
23
|
+
const height = options.height ?? 360;
|
|
24
|
+
const fps = options.fps ?? 15;
|
|
25
|
+
const canvas = document.createElement('canvas');
|
|
26
|
+
canvas.width = width;
|
|
27
|
+
canvas.height = height;
|
|
28
|
+
const ctx = canvas.getContext('2d');
|
|
29
|
+
const boxes = (0, exports.sideBySide)(videos.length, width, height);
|
|
30
|
+
const draw = () => {
|
|
31
|
+
ctx.fillStyle = options.background ?? '#1C1917';
|
|
32
|
+
ctx.fillRect(0, 0, width, height);
|
|
33
|
+
videos.forEach((video, i) => {
|
|
34
|
+
if (video.readyState < 2 || !video.videoWidth)
|
|
35
|
+
return;
|
|
36
|
+
const tile = (0, exports.fitContain)(video.videoWidth, video.videoHeight, boxes[i]);
|
|
37
|
+
ctx.drawImage(video, tile.x, tile.y, tile.w, tile.h);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
draw();
|
|
41
|
+
const timer = setInterval(draw, 1000 / fps);
|
|
42
|
+
const stream = canvas.captureStream(fps);
|
|
43
|
+
return {
|
|
44
|
+
stream,
|
|
45
|
+
stop() {
|
|
46
|
+
clearInterval(timer);
|
|
47
|
+
stream.getTracks().forEach(t => t.stop());
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
exports.composeVideos = composeVideos;
|
|
52
|
+
//# sourceMappingURL=compose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../../src/media/compose.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outgoing media quality: send profiles, sender parameters and an adaptive governor.
|
|
3
|
+
*
|
|
4
|
+
* Everything here works through `RTCRtpSender.setParameters()` — live, without renegotiation. The
|
|
5
|
+
* camera is captured once at the best profile and scaled down per sender, so switching levels never
|
|
6
|
+
* touches getUserMedia. Audio is always protected: it gets high priority and the video gives way first.
|
|
7
|
+
*/
|
|
8
|
+
/** 0 = best. */
|
|
9
|
+
export type QualityLevel = 0 | 1 | 2 | 3;
|
|
10
|
+
export interface VideoProfile {
|
|
11
|
+
label: 'full' | 'high' | 'medium' | 'low';
|
|
12
|
+
height: number;
|
|
13
|
+
frameRate: number;
|
|
14
|
+
/** bits per second */
|
|
15
|
+
maxBitrate: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const VIDEO_PROFILES: readonly VideoProfile[];
|
|
18
|
+
export declare const LOWEST_LEVEL: QualityLevel;
|
|
19
|
+
/** 720p — where adaptive mode starts; 1080p is earned with measured headroom. */
|
|
20
|
+
export declare const DEFAULT_LEVEL: QualityLevel;
|
|
21
|
+
/** Best level a camera track can feed without upscaling: 1080p only from a real 1080p capture. */
|
|
22
|
+
export declare const bestLevelFor: (trackHeight: number | undefined) => QualityLevel;
|
|
23
|
+
/** Screen share keeps full resolution (text stays readable) and gives up frame rate instead. */
|
|
24
|
+
export declare const SCREEN_PROFILE: {
|
|
25
|
+
readonly frameRate: 5;
|
|
26
|
+
readonly maxBitrate: 1200000;
|
|
27
|
+
};
|
|
28
|
+
/** Opus at 48 kbps is transparent for speech; the browser lowers it under congestion. */
|
|
29
|
+
export declare const AUDIO_MAX_BITRATE = 48000;
|
|
30
|
+
/**
|
|
31
|
+
* Camera capture constraints for the best profile — scaled down per sender afterwards. `ideal`, so a
|
|
32
|
+
* 720p webcam simply delivers 720p (and `bestLevelFor` then never offers 1080p).
|
|
33
|
+
*/
|
|
34
|
+
export declare const cameraConstraints: (deviceId?: string) => MediaTrackConstraints;
|
|
35
|
+
/** Voice processing on — the call is speech, not music. */
|
|
36
|
+
export declare const microphoneConstraints: (deviceId?: string) => MediaTrackConstraints;
|
|
37
|
+
/** How much to shrink a track of `trackHeight` to reach `targetHeight` (never upscales). */
|
|
38
|
+
export declare const scaleFor: (trackHeight: number | undefined, targetHeight: number) => number;
|
|
39
|
+
/**
|
|
40
|
+
* Apply a video level (or the screen profile) to a sender. Resolves `false` when the sender is not
|
|
41
|
+
* negotiated yet or the browser rejected the parameters — callers simply re-apply later.
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyVideoQuality(sender: RTCRtpSender, opts: {
|
|
44
|
+
level: QualityLevel;
|
|
45
|
+
screen?: boolean; /** false = audio-only: the encoder stops, the track stays. */
|
|
46
|
+
active?: boolean;
|
|
47
|
+
}): Promise<boolean>;
|
|
48
|
+
/** Speech first: the audio sender is prioritised over video on the wire and in the browser's pacer. */
|
|
49
|
+
export declare function applyAudioPriority(sender: RTCRtpSender): Promise<boolean>;
|
|
50
|
+
export interface QualitySample {
|
|
51
|
+
/** epoch ms */
|
|
52
|
+
t: number;
|
|
53
|
+
/** seconds */
|
|
54
|
+
roundTripTime: number | null;
|
|
55
|
+
/** 0..1 — loss of what WE send, as reported back by the remote side (preferred). */
|
|
56
|
+
outboundLoss: number | null;
|
|
57
|
+
/** 0..1 — loss of what we receive; used when the outbound figure is unknown. */
|
|
58
|
+
packetLoss: number | null;
|
|
59
|
+
/** bits per second — the congestion controller's estimate for the selected pair. */
|
|
60
|
+
availableOutgoingBitrate: number | null;
|
|
61
|
+
}
|
|
62
|
+
export interface QualityGovernorOptions {
|
|
63
|
+
/** Best level the governor may reach (camera capability, user or remote cap). Default 0. */
|
|
64
|
+
ceiling?: QualityLevel;
|
|
65
|
+
/** Where to start (and restart after `setCeiling`). Default `DEFAULT_LEVEL` (720p), clamped to the ceiling. */
|
|
66
|
+
start?: QualityLevel;
|
|
67
|
+
/** Sustained trouble before stepping down. Default 9 s. */
|
|
68
|
+
downAfterMs?: number;
|
|
69
|
+
/** Sustained health before stepping back up. Default 30 s. */
|
|
70
|
+
upAfterMs?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface QualityGovernor {
|
|
73
|
+
level(): QualityLevel;
|
|
74
|
+
/** Feed one stats sample; returns the step taken, if any. */
|
|
75
|
+
push(sample: QualitySample): 'down' | 'up' | null;
|
|
76
|
+
setCeiling(ceiling: QualityLevel, start?: QualityLevel): void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Step video down after `downAfterMs` of trouble (loss, latency, or the bandwidth estimate below what
|
|
80
|
+
* the current level needs) and back up after `upAfterMs` of headroom. Pure — the caller samples stats
|
|
81
|
+
* and applies the level, so this is unit-testable without a browser.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createQualityGovernor(options?: QualityGovernorOptions): QualityGovernor;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Outgoing media quality: send profiles, sender parameters and an adaptive governor.
|
|
4
|
+
*
|
|
5
|
+
* Everything here works through `RTCRtpSender.setParameters()` — live, without renegotiation. The
|
|
6
|
+
* camera is captured once at the best profile and scaled down per sender, so switching levels never
|
|
7
|
+
* touches getUserMedia. Audio is always protected: it gets high priority and the video gives way first.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.createQualityGovernor = exports.applyAudioPriority = exports.applyVideoQuality = exports.scaleFor = exports.microphoneConstraints = exports.cameraConstraints = exports.AUDIO_MAX_BITRATE = exports.SCREEN_PROFILE = exports.bestLevelFor = exports.DEFAULT_LEVEL = exports.LOWEST_LEVEL = exports.VIDEO_PROFILES = void 0;
|
|
11
|
+
exports.VIDEO_PROFILES = [
|
|
12
|
+
{ label: 'full', height: 1080, frameRate: 30, maxBitrate: 3_500_000 },
|
|
13
|
+
{ label: 'high', height: 720, frameRate: 30, maxBitrate: 1_500_000 },
|
|
14
|
+
{ label: 'medium', height: 480, frameRate: 24, maxBitrate: 750_000 },
|
|
15
|
+
{ label: 'low', height: 240, frameRate: 15, maxBitrate: 250_000 },
|
|
16
|
+
];
|
|
17
|
+
exports.LOWEST_LEVEL = 3;
|
|
18
|
+
/** 720p — where adaptive mode starts; 1080p is earned with measured headroom. */
|
|
19
|
+
exports.DEFAULT_LEVEL = 1;
|
|
20
|
+
/** Best level a camera track can feed without upscaling: 1080p only from a real 1080p capture. */
|
|
21
|
+
const bestLevelFor = (trackHeight) => ((trackHeight ?? 0) >= 1080 ? 0 : exports.DEFAULT_LEVEL);
|
|
22
|
+
exports.bestLevelFor = bestLevelFor;
|
|
23
|
+
/** Screen share keeps full resolution (text stays readable) and gives up frame rate instead. */
|
|
24
|
+
exports.SCREEN_PROFILE = { frameRate: 5, maxBitrate: 1_200_000 };
|
|
25
|
+
/** Opus at 48 kbps is transparent for speech; the browser lowers it under congestion. */
|
|
26
|
+
exports.AUDIO_MAX_BITRATE = 48_000;
|
|
27
|
+
/**
|
|
28
|
+
* Camera capture constraints for the best profile — scaled down per sender afterwards. `ideal`, so a
|
|
29
|
+
* 720p webcam simply delivers 720p (and `bestLevelFor` then never offers 1080p).
|
|
30
|
+
*/
|
|
31
|
+
const cameraConstraints = (deviceId) => ({
|
|
32
|
+
...(deviceId ? { deviceId: { exact: deviceId } } : {}),
|
|
33
|
+
width: { ideal: 1920 },
|
|
34
|
+
height: { ideal: 1080 },
|
|
35
|
+
frameRate: { ideal: 30, max: 30 },
|
|
36
|
+
});
|
|
37
|
+
exports.cameraConstraints = cameraConstraints;
|
|
38
|
+
/** Voice processing on — the call is speech, not music. */
|
|
39
|
+
const microphoneConstraints = (deviceId) => ({
|
|
40
|
+
...(deviceId ? { deviceId: { exact: deviceId } } : {}),
|
|
41
|
+
echoCancellation: true,
|
|
42
|
+
noiseSuppression: true,
|
|
43
|
+
autoGainControl: true,
|
|
44
|
+
});
|
|
45
|
+
exports.microphoneConstraints = microphoneConstraints;
|
|
46
|
+
/** How much to shrink a track of `trackHeight` to reach `targetHeight` (never upscales). */
|
|
47
|
+
const scaleFor = (trackHeight, targetHeight) => trackHeight && trackHeight > targetHeight ? trackHeight / targetHeight : 1;
|
|
48
|
+
exports.scaleFor = scaleFor;
|
|
49
|
+
const withEncoding = (sender, patch) => {
|
|
50
|
+
const params = sender.getParameters();
|
|
51
|
+
// Chrome returns an empty list before negotiation completes; setParameters would then throw.
|
|
52
|
+
if (!params.encodings?.length)
|
|
53
|
+
return Promise.resolve(false);
|
|
54
|
+
patch(params.encodings[0], params);
|
|
55
|
+
return sender.setParameters(params).then(() => true, () => false);
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Apply a video level (or the screen profile) to a sender. Resolves `false` when the sender is not
|
|
59
|
+
* negotiated yet or the browser rejected the parameters — callers simply re-apply later.
|
|
60
|
+
*/
|
|
61
|
+
function applyVideoQuality(sender, opts) {
|
|
62
|
+
const track = sender.track;
|
|
63
|
+
return withEncoding(sender, (enc, params) => {
|
|
64
|
+
enc.active = opts.active ?? true;
|
|
65
|
+
enc.priority = 'low';
|
|
66
|
+
enc.networkPriority = 'low';
|
|
67
|
+
if (opts.screen) {
|
|
68
|
+
enc.maxBitrate = exports.SCREEN_PROFILE.maxBitrate;
|
|
69
|
+
enc.maxFramerate = exports.SCREEN_PROFILE.frameRate;
|
|
70
|
+
enc.scaleResolutionDownBy = 1;
|
|
71
|
+
params.degradationPreference = 'maintain-resolution';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const profile = exports.VIDEO_PROFILES[opts.level];
|
|
75
|
+
enc.maxBitrate = profile.maxBitrate;
|
|
76
|
+
enc.maxFramerate = profile.frameRate;
|
|
77
|
+
enc.scaleResolutionDownBy = (0, exports.scaleFor)(track?.getSettings().height, profile.height);
|
|
78
|
+
params.degradationPreference = 'balanced';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
exports.applyVideoQuality = applyVideoQuality;
|
|
83
|
+
/** Speech first: the audio sender is prioritised over video on the wire and in the browser's pacer. */
|
|
84
|
+
function applyAudioPriority(sender) {
|
|
85
|
+
return withEncoding(sender, (enc) => {
|
|
86
|
+
enc.priority = 'high';
|
|
87
|
+
enc.networkPriority = 'high';
|
|
88
|
+
enc.maxBitrate = exports.AUDIO_MAX_BITRATE;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
exports.applyAudioPriority = applyAudioPriority;
|
|
92
|
+
const BAD_RTT = 0.4;
|
|
93
|
+
const BAD_LOSS = 0.05;
|
|
94
|
+
const GOOD_RTT = 0.25;
|
|
95
|
+
const GOOD_LOSS = 0.02;
|
|
96
|
+
/**
|
|
97
|
+
* Step video down after `downAfterMs` of trouble (loss, latency, or the bandwidth estimate below what
|
|
98
|
+
* the current level needs) and back up after `upAfterMs` of headroom. Pure — the caller samples stats
|
|
99
|
+
* and applies the level, so this is unit-testable without a browser.
|
|
100
|
+
*/
|
|
101
|
+
function createQualityGovernor(options = {}) {
|
|
102
|
+
const downAfter = options.downAfterMs ?? 9_000;
|
|
103
|
+
const upAfter = options.upAfterMs ?? 30_000;
|
|
104
|
+
let ceiling = options.ceiling ?? 0;
|
|
105
|
+
const clampStart = (start) => Math.max(ceiling, start ?? exports.DEFAULT_LEVEL);
|
|
106
|
+
let level = clampStart(options.start);
|
|
107
|
+
let badSince = null;
|
|
108
|
+
let goodSince = null;
|
|
109
|
+
const loss = (s) => s.outboundLoss ?? s.packetLoss;
|
|
110
|
+
const bad = (s) => (s.roundTripTime ?? 0) > BAD_RTT ||
|
|
111
|
+
(loss(s) ?? 0) > BAD_LOSS ||
|
|
112
|
+
(s.availableOutgoingBitrate !== null && s.availableOutgoingBitrate < exports.VIDEO_PROFILES[level].maxBitrate * 0.7);
|
|
113
|
+
const good = (s) => {
|
|
114
|
+
if (level <= ceiling)
|
|
115
|
+
return false;
|
|
116
|
+
const up = exports.VIDEO_PROFILES[level - 1];
|
|
117
|
+
// 1080p is only earned with a MEASURED estimate; lower levels may also return on loss/RTT alone.
|
|
118
|
+
const bitrateOk = s.availableOutgoingBitrate === null ? level - 1 > 0 : s.availableOutgoingBitrate > up.maxBitrate * 1.2;
|
|
119
|
+
return (s.roundTripTime ?? 0) < GOOD_RTT && (loss(s) ?? 0) < GOOD_LOSS && bitrateOk;
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
level: () => level,
|
|
123
|
+
setCeiling(next, start) {
|
|
124
|
+
ceiling = next;
|
|
125
|
+
level = clampStart(start);
|
|
126
|
+
badSince = goodSince = null;
|
|
127
|
+
},
|
|
128
|
+
push(s) {
|
|
129
|
+
if (bad(s)) {
|
|
130
|
+
goodSince = null;
|
|
131
|
+
badSince = badSince ?? s.t;
|
|
132
|
+
if (level < exports.LOWEST_LEVEL && s.t - badSince >= downAfter) {
|
|
133
|
+
level = (level + 1);
|
|
134
|
+
badSince = null;
|
|
135
|
+
return 'down';
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
badSince = null;
|
|
140
|
+
if (good(s)) {
|
|
141
|
+
goodSince = goodSince ?? s.t;
|
|
142
|
+
if (s.t - goodSince >= upAfter) {
|
|
143
|
+
level = (level - 1);
|
|
144
|
+
goodSince = null;
|
|
145
|
+
return 'up';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
goodSince = null;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
exports.createQualityGovernor = createQualityGovernor;
|
|
156
|
+
//# sourceMappingURL=quality.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quality.js","sourceRoot":"","sources":["../../src/media/quality.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type RecorderState = 'inactive' | 'recording' | 'paused';
|
|
2
|
+
export interface RecorderOptions {
|
|
3
|
+
stream: MediaStream;
|
|
4
|
+
/** Candidates in order of preference; the first supported one is used. */
|
|
5
|
+
mimeTypes: string[];
|
|
6
|
+
/** Chunk interval. Default 10 000 ms. */
|
|
7
|
+
timesliceMs?: number;
|
|
8
|
+
onChunk?: (chunk: Blob) => void;
|
|
9
|
+
/** Test seams. */
|
|
10
|
+
MediaRecorderImpl?: typeof MediaRecorder;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
export interface RecordingResult {
|
|
14
|
+
blob: Blob;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
/** Recorded time excluding pauses. */
|
|
17
|
+
durationMs: number;
|
|
18
|
+
sizeBytes: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const pickMimeType: (candidates: string[], isSupported: (type: string) => boolean) => string;
|
|
21
|
+
/** Duration bookkeeping that excludes paused spans. */
|
|
22
|
+
export declare const createClock: (now: () => number) => {
|
|
23
|
+
start: () => number;
|
|
24
|
+
pause: () => void;
|
|
25
|
+
elapsed: () => number;
|
|
26
|
+
};
|
|
27
|
+
/** `MediaRecorder` wrapper: pause/resume, chunked capture, and a single Blob on stop. */
|
|
28
|
+
export declare const createRecorder: (options: RecorderOptions) => {
|
|
29
|
+
mimeType: () => string;
|
|
30
|
+
state: () => RecorderState;
|
|
31
|
+
durationMs: () => number;
|
|
32
|
+
sizeBytes: () => number;
|
|
33
|
+
start(): void;
|
|
34
|
+
pause(): void;
|
|
35
|
+
resume(): void;
|
|
36
|
+
stop(): Promise<RecordingResult>;
|
|
37
|
+
};
|
|
38
|
+
export type SessionRecorder = ReturnType<typeof createRecorder>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRecorder = exports.createClock = exports.pickMimeType = void 0;
|
|
4
|
+
const pickMimeType = (candidates, isSupported) => candidates.find(type => isSupported(type)) ?? '';
|
|
5
|
+
exports.pickMimeType = pickMimeType;
|
|
6
|
+
/** Duration bookkeeping that excludes paused spans. */
|
|
7
|
+
const createClock = (now) => {
|
|
8
|
+
let accumulated = 0;
|
|
9
|
+
let since = null;
|
|
10
|
+
return {
|
|
11
|
+
start: () => (since = now()),
|
|
12
|
+
pause: () => {
|
|
13
|
+
if (since !== null)
|
|
14
|
+
accumulated += now() - since;
|
|
15
|
+
since = null;
|
|
16
|
+
},
|
|
17
|
+
elapsed: () => accumulated + (since !== null ? now() - since : 0),
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
exports.createClock = createClock;
|
|
21
|
+
/** `MediaRecorder` wrapper: pause/resume, chunked capture, and a single Blob on stop. */
|
|
22
|
+
const createRecorder = (options) => {
|
|
23
|
+
const Impl = options.MediaRecorderImpl ?? MediaRecorder;
|
|
24
|
+
const mimeType = (0, exports.pickMimeType)(options.mimeTypes, t => Impl.isTypeSupported(t));
|
|
25
|
+
const recorder = new Impl(options.stream, mimeType ? { mimeType } : undefined);
|
|
26
|
+
const chunks = [];
|
|
27
|
+
const clock = (0, exports.createClock)(options.now ?? (() => performance.now()));
|
|
28
|
+
let sizeBytes = 0;
|
|
29
|
+
recorder.ondataavailable = event => {
|
|
30
|
+
if (!event.data || event.data.size === 0)
|
|
31
|
+
return;
|
|
32
|
+
chunks.push(event.data);
|
|
33
|
+
sizeBytes += event.data.size;
|
|
34
|
+
options.onChunk?.(event.data);
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
mimeType: () => recorder.mimeType || mimeType,
|
|
38
|
+
state: () => recorder.state,
|
|
39
|
+
durationMs: () => clock.elapsed(),
|
|
40
|
+
sizeBytes: () => sizeBytes,
|
|
41
|
+
start() {
|
|
42
|
+
recorder.start(options.timesliceMs ?? 10_000);
|
|
43
|
+
clock.start();
|
|
44
|
+
},
|
|
45
|
+
pause() {
|
|
46
|
+
if (recorder.state !== 'recording')
|
|
47
|
+
return;
|
|
48
|
+
recorder.pause();
|
|
49
|
+
clock.pause();
|
|
50
|
+
},
|
|
51
|
+
resume() {
|
|
52
|
+
if (recorder.state !== 'paused')
|
|
53
|
+
return;
|
|
54
|
+
recorder.resume();
|
|
55
|
+
clock.start();
|
|
56
|
+
},
|
|
57
|
+
stop() {
|
|
58
|
+
return new Promise(resolve => {
|
|
59
|
+
const finish = () => {
|
|
60
|
+
const type = recorder.mimeType || mimeType || 'application/octet-stream';
|
|
61
|
+
const blob = new Blob(chunks, { type });
|
|
62
|
+
resolve({ blob, mimeType: type, durationMs: clock.elapsed(), sizeBytes: blob.size });
|
|
63
|
+
};
|
|
64
|
+
if (recorder.state === 'inactive')
|
|
65
|
+
return finish();
|
|
66
|
+
clock.pause();
|
|
67
|
+
recorder.onstop = finish;
|
|
68
|
+
recorder.stop();
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
exports.createRecorder = createRecorder;
|
|
74
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/media/recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface LevelSample {
|
|
2
|
+
/** Milliseconds (same clock as the caller's `now`). */
|
|
3
|
+
t: number;
|
|
4
|
+
/** RMS of the window, 0..1. */
|
|
5
|
+
rms: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SpeakingDetectorOptions {
|
|
8
|
+
/** Speaking when the level exceeds the noise floor by this many dB… Default 12. */
|
|
9
|
+
marginDb?: number;
|
|
10
|
+
/** …for at least this long. Default 150 ms. */
|
|
11
|
+
minSpeakingMs?: number;
|
|
12
|
+
/** Stay "speaking" this long after the level drops (avoids flicker between words). Default 300 ms. */
|
|
13
|
+
holdMs?: number;
|
|
14
|
+
/** Samples kept for `samplesBetween` (attribution). Default 1200 (~60 s at 50 ms). */
|
|
15
|
+
historySize?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const toDb: (rms: number) => number;
|
|
18
|
+
/**
|
|
19
|
+
* Pure speaking detector over RMS samples with an adaptive noise floor: the floor follows quiet passages
|
|
20
|
+
* quickly and loud ones slowly, so background noise is learnt while speech is not.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createSpeakingDetector: (options?: SpeakingDetectorOptions) => {
|
|
23
|
+
push(sample: LevelSample): boolean;
|
|
24
|
+
isSpeaking: () => boolean;
|
|
25
|
+
floorDb: () => number;
|
|
26
|
+
/** Samples with `from <= t <= to` — e.g. the window of a speech-recognition result. */
|
|
27
|
+
samplesBetween: (from: number, to: number) => LevelSample[];
|
|
28
|
+
};
|
|
29
|
+
export type SpeakingDetector = ReturnType<typeof createSpeakingDetector>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSpeakingDetector = exports.toDb = void 0;
|
|
4
|
+
const toDb = (rms) => (rms > 0 ? 20 * Math.log10(rms) : -100);
|
|
5
|
+
exports.toDb = toDb;
|
|
6
|
+
/**
|
|
7
|
+
* Pure speaking detector over RMS samples with an adaptive noise floor: the floor follows quiet passages
|
|
8
|
+
* quickly and loud ones slowly, so background noise is learnt while speech is not.
|
|
9
|
+
*/
|
|
10
|
+
const createSpeakingDetector = (options = {}) => {
|
|
11
|
+
const marginDb = options.marginDb ?? 12;
|
|
12
|
+
const minSpeakingMs = options.minSpeakingMs ?? 150;
|
|
13
|
+
const holdMs = options.holdMs ?? 300;
|
|
14
|
+
const historySize = options.historySize ?? 1200;
|
|
15
|
+
let floorDb = -60;
|
|
16
|
+
let aboveSince = null;
|
|
17
|
+
let lastAbove = -Infinity;
|
|
18
|
+
let speaking = false;
|
|
19
|
+
const history = [];
|
|
20
|
+
return {
|
|
21
|
+
push(sample) {
|
|
22
|
+
history.push(sample);
|
|
23
|
+
if (history.length > historySize)
|
|
24
|
+
history.shift();
|
|
25
|
+
const db = (0, exports.toDb)(sample.rms);
|
|
26
|
+
floorDb = db < floorDb ? floorDb + (db - floorDb) * 0.3 : floorDb + (db - floorDb) * 0.002;
|
|
27
|
+
floorDb = Math.max(floorDb, -90);
|
|
28
|
+
const above = db > floorDb + marginDb;
|
|
29
|
+
if (above) {
|
|
30
|
+
aboveSince = aboveSince ?? sample.t;
|
|
31
|
+
lastAbove = sample.t;
|
|
32
|
+
if (sample.t - aboveSince >= minSpeakingMs)
|
|
33
|
+
speaking = true;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
aboveSince = null;
|
|
37
|
+
if (sample.t - lastAbove > holdMs)
|
|
38
|
+
speaking = false;
|
|
39
|
+
}
|
|
40
|
+
return speaking;
|
|
41
|
+
},
|
|
42
|
+
isSpeaking: () => speaking,
|
|
43
|
+
floorDb: () => floorDb,
|
|
44
|
+
/** Samples with `from <= t <= to` — e.g. the window of a speech-recognition result. */
|
|
45
|
+
samplesBetween: (from, to) => history.filter(s => s.t >= from && s.t <= to),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
exports.createSpeakingDetector = createSpeakingDetector;
|
|
49
|
+
//# sourceMappingURL=speaking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speaking.js","sourceRoot":"","sources":["../../src/media/speaking.ts"],"names":[],"mappings":";;;AAkBO,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAA/D,QAAA,IAAI,QAA2D;AAE5E;;;GAGG;AACI,MAAM,sBAAsB,GAAG,CAAC,UAAmC,EAAE,EAAE,EAAE;IAC9E,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAA;IACvC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,GAAG,CAAA;IAClD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAA;IACpC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,IAAI,CAAA;IAE/C,IAAI,OAAO,GAAG,CAAC,EAAE,CAAA;IACjB,IAAI,UAAU,GAAkB,IAAI,CAAA;IACpC,IAAI,SAAS,GAAG,CAAC,QAAQ,CAAA;IACzB,IAAI,QAAQ,GAAG,KAAK,CAAA;IACpB,MAAM,OAAO,GAAkB,EAAE,CAAA;IAEjC,OAAO;QACL,IAAI,CAAC,MAAmB;YACtB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACpB,IAAI,OAAO,CAAC,MAAM,GAAG,WAAW;gBAAE,OAAO,CAAC,KAAK,EAAE,CAAA;YACjD,MAAM,EAAE,GAAG,IAAA,YAAI,EAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAC3B,OAAO,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,KAAK,CAAA;YAC1F,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAA;YAChC,MAAM,KAAK,GAAG,EAAE,GAAG,OAAO,GAAG,QAAQ,CAAA;YACrC,IAAI,KAAK,EAAE;gBACT,UAAU,GAAG,UAAU,IAAI,MAAM,CAAC,CAAC,CAAA;gBACnC,SAAS,GAAG,MAAM,CAAC,CAAC,CAAA;gBACpB,IAAI,MAAM,CAAC,CAAC,GAAG,UAAU,IAAI,aAAa;oBAAE,QAAQ,GAAG,IAAI,CAAA;aAC5D;iBAAM;gBACL,UAAU,GAAG,IAAI,CAAA;gBACjB,IAAI,MAAM,CAAC,CAAC,GAAG,SAAS,GAAG,MAAM;oBAAE,QAAQ,GAAG,KAAK,CAAA;aACpD;YACD,OAAO,QAAQ,CAAA;QACjB,CAAC;QACD,UAAU,EAAE,GAAG,EAAE,CAAC,QAAQ;QAC1B,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO;QACtB,uFAAuF;QACvF,cAAc,EAAE,CAAC,IAAY,EAAE,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;KAC5F,CAAA;AACH,CAAC,CAAA;AAnCY,QAAA,sBAAsB,0BAmClC"}
|
package/dist/peer.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type CandidateType = 'host' | 'srflx' | 'prflx' | 'relay' | 'unknown';
|
|
2
|
+
export interface PeerStats {
|
|
3
|
+
/** Seconds, from the selected candidate pair. */
|
|
4
|
+
roundTripTime: number | null;
|
|
5
|
+
/** Fraction of inbound RTP packets lost since the previous sample (0..1). */
|
|
6
|
+
packetLoss: number | null;
|
|
7
|
+
/** Local candidate type of the selected pair — `relay` means TURN. */
|
|
8
|
+
candidateType: CandidateType | null;
|
|
9
|
+
/** Fraction (0..1) of OUR packets the remote side reports lost (`remote-inbound-rtp`), worst stream. */
|
|
10
|
+
outboundLoss: number | null;
|
|
11
|
+
/** Bits per second — the congestion controller's send estimate for the selected pair. */
|
|
12
|
+
availableOutgoingBitrate: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface PeerSignalSender {
|
|
15
|
+
send(type: 'offer' | 'answer' | 'ice', payload: unknown): Promise<string | null> | void;
|
|
16
|
+
}
|
|
17
|
+
export interface PeerConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Perfect negotiation role: the polite peer yields on offer collisions (rolls back and answers),
|
|
20
|
+
* the impolite peer ignores the colliding offer.
|
|
21
|
+
*/
|
|
22
|
+
polite: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* When false, this peer does not send offers until it has completed one negotiation started by the
|
|
25
|
+
* other side — e.g. a guest who must wait for the host's offer. Default true.
|
|
26
|
+
*/
|
|
27
|
+
initiator?: boolean;
|
|
28
|
+
iceServers?: RTCIceServer[];
|
|
29
|
+
signalling: PeerSignalSender;
|
|
30
|
+
onTrack?: (event: RTCTrackEvent) => void;
|
|
31
|
+
onConnectionState?: (state: RTCPeerConnectionState, ice: RTCIceConnectionState) => void;
|
|
32
|
+
onStats?: (stats: PeerStats) => void;
|
|
33
|
+
/** Restart ICE after this long in `disconnected` (immediately on `failed`). Default 3000 ms; 0 disables. */
|
|
34
|
+
restartAfterMs?: number;
|
|
35
|
+
/** Stats sampling interval. Default 3000 ms; 0 disables. */
|
|
36
|
+
statsIntervalMs?: number;
|
|
37
|
+
/** Test seam. */
|
|
38
|
+
createPeerConnection?: (config: RTCConfiguration) => RTCPeerConnection;
|
|
39
|
+
}
|
|
40
|
+
export interface Peer {
|
|
41
|
+
pc: RTCPeerConnection;
|
|
42
|
+
/** Feed a remote `offer` / `answer` / `ice` signal. Other types are ignored. */
|
|
43
|
+
handleSignal(type: string, payload: unknown): Promise<void>;
|
|
44
|
+
restartIce(): void;
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* RTCPeerConnection with the "perfect negotiation" pattern (W3C WebRTC spec §10.7): either side may add
|
|
49
|
+
* or remove tracks at any time, glare is resolved by the polite/impolite roles, ICE candidates trickle,
|
|
50
|
+
* and a dropped connection is restarted with `restartIce()`.
|
|
51
|
+
*/
|
|
52
|
+
export declare const createPeer: (config: PeerConfig) => Peer;
|
package/dist/peer.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPeer = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* RTCPeerConnection with the "perfect negotiation" pattern (W3C WebRTC spec §10.7): either side may add
|
|
6
|
+
* or remove tracks at any time, glare is resolved by the polite/impolite roles, ICE candidates trickle,
|
|
7
|
+
* and a dropped connection is restarted with `restartIce()`.
|
|
8
|
+
*/
|
|
9
|
+
const createPeer = (config) => {
|
|
10
|
+
const pc = (config.createPeerConnection ?? (c => new RTCPeerConnection(c)))({
|
|
11
|
+
iceServers: config.iceServers ?? [],
|
|
12
|
+
});
|
|
13
|
+
const restartAfterMs = config.restartAfterMs ?? 3000;
|
|
14
|
+
const statsIntervalMs = config.statsIntervalMs ?? 3000;
|
|
15
|
+
let makingOffer = false;
|
|
16
|
+
let ignoreOffer = false;
|
|
17
|
+
let settingRemoteAnswer = false;
|
|
18
|
+
let mayOffer = config.initiator ?? true;
|
|
19
|
+
let closed = false;
|
|
20
|
+
let restartTimer = null;
|
|
21
|
+
let statsTimer = null;
|
|
22
|
+
let lastInbound = { lost: 0, received: 0 };
|
|
23
|
+
const send = (type, payload) => {
|
|
24
|
+
void config.signalling.send(type, payload);
|
|
25
|
+
};
|
|
26
|
+
pc.onnegotiationneeded = async () => {
|
|
27
|
+
if (!mayOffer || closed)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
makingOffer = true;
|
|
31
|
+
await pc.setLocalDescription();
|
|
32
|
+
send('offer', pc.localDescription?.toJSON?.() ?? pc.localDescription);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
makingOffer = false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
pc.onicecandidate = ({ candidate }) => {
|
|
39
|
+
if (candidate)
|
|
40
|
+
send('ice', candidate.toJSON?.() ?? candidate);
|
|
41
|
+
};
|
|
42
|
+
pc.ontrack = event => config.onTrack?.(event);
|
|
43
|
+
const clearRestart = () => {
|
|
44
|
+
if (restartTimer)
|
|
45
|
+
clearTimeout(restartTimer);
|
|
46
|
+
restartTimer = null;
|
|
47
|
+
};
|
|
48
|
+
const report = () => config.onConnectionState?.(pc.connectionState, pc.iceConnectionState);
|
|
49
|
+
pc.onconnectionstatechange = report;
|
|
50
|
+
pc.oniceconnectionstatechange = () => {
|
|
51
|
+
report();
|
|
52
|
+
if (restartAfterMs <= 0 || closed)
|
|
53
|
+
return;
|
|
54
|
+
const state = pc.iceConnectionState;
|
|
55
|
+
if (state === 'failed') {
|
|
56
|
+
clearRestart();
|
|
57
|
+
pc.restartIce();
|
|
58
|
+
}
|
|
59
|
+
else if (state === 'disconnected') {
|
|
60
|
+
clearRestart();
|
|
61
|
+
restartTimer = setTimeout(() => {
|
|
62
|
+
if (pc.iceConnectionState === 'disconnected' || pc.iceConnectionState === 'failed')
|
|
63
|
+
pc.restartIce();
|
|
64
|
+
}, restartAfterMs);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
clearRestart();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const handleDescription = async (description) => {
|
|
71
|
+
const readyForOffer = !makingOffer && (pc.signalingState === 'stable' || settingRemoteAnswer);
|
|
72
|
+
const offerCollision = description.type === 'offer' && !readyForOffer;
|
|
73
|
+
ignoreOffer = !config.polite && offerCollision;
|
|
74
|
+
if (ignoreOffer)
|
|
75
|
+
return;
|
|
76
|
+
settingRemoteAnswer = description.type === 'answer';
|
|
77
|
+
try {
|
|
78
|
+
await pc.setRemoteDescription(description); // rolls back a pending local offer when polite
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
settingRemoteAnswer = false;
|
|
82
|
+
}
|
|
83
|
+
if (description.type === 'offer') {
|
|
84
|
+
await pc.setLocalDescription();
|
|
85
|
+
send('answer', pc.localDescription?.toJSON?.() ?? pc.localDescription);
|
|
86
|
+
}
|
|
87
|
+
if (!mayOffer && pc.signalingState === 'stable') {
|
|
88
|
+
mayOffer = true;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const handleSignal = async (type, payload) => {
|
|
92
|
+
if (closed)
|
|
93
|
+
return;
|
|
94
|
+
if (type === 'offer' || type === 'answer') {
|
|
95
|
+
await handleDescription(payload);
|
|
96
|
+
}
|
|
97
|
+
else if (type === 'ice') {
|
|
98
|
+
try {
|
|
99
|
+
await pc.addIceCandidate(payload);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (!ignoreOffer)
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const sampleStats = async () => {
|
|
108
|
+
if (closed || !config.onStats)
|
|
109
|
+
return;
|
|
110
|
+
const report = await pc.getStats();
|
|
111
|
+
let pair = null;
|
|
112
|
+
let transportPairId = null;
|
|
113
|
+
const byId = new Map();
|
|
114
|
+
let lost = 0;
|
|
115
|
+
let received = 0;
|
|
116
|
+
let outboundLoss = null;
|
|
117
|
+
report.forEach((entry) => {
|
|
118
|
+
if (entry.type === 'remote-inbound-rtp' && typeof entry.fractionLost === 'number') {
|
|
119
|
+
outboundLoss = Math.max(outboundLoss ?? 0, entry.fractionLost);
|
|
120
|
+
}
|
|
121
|
+
byId.set(entry.id, entry);
|
|
122
|
+
if (entry.type === 'transport' && entry.selectedCandidatePairId)
|
|
123
|
+
transportPairId = entry.selectedCandidatePairId;
|
|
124
|
+
if (entry.type === 'candidate-pair' && (entry.selected || (entry.nominated && entry.state === 'succeeded'))) {
|
|
125
|
+
pair = pair ?? entry;
|
|
126
|
+
}
|
|
127
|
+
if (entry.type === 'inbound-rtp') {
|
|
128
|
+
lost += entry.packetsLost ?? 0;
|
|
129
|
+
received += entry.packetsReceived ?? 0;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (transportPairId && byId.has(transportPairId))
|
|
133
|
+
pair = byId.get(transportPairId);
|
|
134
|
+
const local = pair ? byId.get(pair.localCandidateId) : null;
|
|
135
|
+
const dLost = lost - lastInbound.lost;
|
|
136
|
+
const dReceived = received - lastInbound.received;
|
|
137
|
+
lastInbound = { lost, received };
|
|
138
|
+
config.onStats({
|
|
139
|
+
roundTripTime: pair?.currentRoundTripTime ?? null,
|
|
140
|
+
packetLoss: dLost + dReceived > 0 ? Math.max(0, dLost) / (dLost + dReceived) : null,
|
|
141
|
+
candidateType: local ? (local.candidateType ?? 'unknown') : null,
|
|
142
|
+
outboundLoss,
|
|
143
|
+
availableOutgoingBitrate: pair?.availableOutgoingBitrate ?? null,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
if (statsIntervalMs > 0 && config.onStats) {
|
|
147
|
+
statsTimer = setInterval(() => void sampleStats().catch(() => undefined), statsIntervalMs);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
pc,
|
|
151
|
+
handleSignal,
|
|
152
|
+
restartIce: () => pc.restartIce(),
|
|
153
|
+
close() {
|
|
154
|
+
closed = true;
|
|
155
|
+
clearRestart();
|
|
156
|
+
if (statsTimer)
|
|
157
|
+
clearInterval(statsTimer);
|
|
158
|
+
pc.close();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
exports.createPeer = createPeer;
|
|
163
|
+
//# sourceMappingURL=peer.js.map
|
package/dist/peer.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer.js","sourceRoot":"","sources":["../src/peer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Sequenced {
|
|
2
|
+
seq: number;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Exactly-once delivery of room signals arriving from several sources (subscribe replay, live push, poll).
|
|
6
|
+
*
|
|
7
|
+
* `seq` is monotonic per *room*, so one party's inbox has gaps (the other party's signals) — gaps are
|
|
8
|
+
* normal and cannot be detected from the stream. What can go wrong is a live frame overtaking the replay
|
|
9
|
+
* on (re)subscribe; `hold()` buffers everything until `release()`, which delivers the buffer sorted by seq.
|
|
10
|
+
* Outside a hold window items are delivered in arrival order, deduplicated by seq. `cursor()` is the
|
|
11
|
+
* highest seq delivered — the replay cursor for the next (re)subscribe or poll.
|
|
12
|
+
*/
|
|
13
|
+
export declare const createSequencer: <T extends Sequenced>(afterSeq: number, deliver: (item: T) => void) => {
|
|
14
|
+
push(item: T): void;
|
|
15
|
+
hold(): void;
|
|
16
|
+
release(): void;
|
|
17
|
+
cursor: () => number;
|
|
18
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSequencer = void 0;
|
|
4
|
+
const SEEN_LIMIT = 2000;
|
|
5
|
+
/**
|
|
6
|
+
* Exactly-once delivery of room signals arriving from several sources (subscribe replay, live push, poll).
|
|
7
|
+
*
|
|
8
|
+
* `seq` is monotonic per *room*, so one party's inbox has gaps (the other party's signals) — gaps are
|
|
9
|
+
* normal and cannot be detected from the stream. What can go wrong is a live frame overtaking the replay
|
|
10
|
+
* on (re)subscribe; `hold()` buffers everything until `release()`, which delivers the buffer sorted by seq.
|
|
11
|
+
* Outside a hold window items are delivered in arrival order, deduplicated by seq. `cursor()` is the
|
|
12
|
+
* highest seq delivered — the replay cursor for the next (re)subscribe or poll.
|
|
13
|
+
*/
|
|
14
|
+
const createSequencer = (afterSeq, deliver) => {
|
|
15
|
+
let cursor = afterSeq;
|
|
16
|
+
let holding = false;
|
|
17
|
+
const held = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const order = [];
|
|
20
|
+
const remember = (seq) => {
|
|
21
|
+
seen.add(seq);
|
|
22
|
+
order.push(seq);
|
|
23
|
+
if (order.length > SEEN_LIMIT)
|
|
24
|
+
seen.delete(order.shift());
|
|
25
|
+
};
|
|
26
|
+
const emit = (item) => {
|
|
27
|
+
if (seen.has(item.seq) || item.seq <= afterSeq)
|
|
28
|
+
return;
|
|
29
|
+
remember(item.seq);
|
|
30
|
+
cursor = Math.max(cursor, item.seq);
|
|
31
|
+
deliver(item);
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
push(item) {
|
|
35
|
+
if (!Number.isInteger(item?.seq))
|
|
36
|
+
return;
|
|
37
|
+
if (holding)
|
|
38
|
+
held.push(item);
|
|
39
|
+
else
|
|
40
|
+
emit(item);
|
|
41
|
+
},
|
|
42
|
+
hold() {
|
|
43
|
+
holding = true;
|
|
44
|
+
},
|
|
45
|
+
release() {
|
|
46
|
+
holding = false;
|
|
47
|
+
held.splice(0).sort((a, b) => a.seq - b.seq).forEach(emit);
|
|
48
|
+
},
|
|
49
|
+
cursor: () => cursor,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
exports.createSequencer = createSequencer;
|
|
53
|
+
//# sourceMappingURL=sequencer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequencer.js","sourceRoot":"","sources":["../src/sequencer.ts"],"names":[],"mappings":";;;AAIA,MAAM,UAAU,GAAG,IAAI,CAAA;AAEvB;;;;;;;;GAQG;AACI,MAAM,eAAe,GAAG,CAAsB,QAAgB,EAAE,OAA0B,EAAE,EAAE;IACnG,IAAI,MAAM,GAAG,QAAQ,CAAA;IACrB,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,MAAM,IAAI,GAAQ,EAAE,CAAA;IACpB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAE,EAAE;QAC/B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACf,IAAI,KAAK,CAAC,MAAM,GAAG,UAAU;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAG,CAAC,CAAA;IAC5D,CAAC,CAAA;IAED,MAAM,IAAI,GAAG,CAAC,IAAO,EAAE,EAAE;QACvB,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,QAAQ;YAAE,OAAM;QACtD,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;QACnC,OAAO,CAAC,IAAI,CAAC,CAAA;IACf,CAAC,CAAA;IAED,OAAO;QACL,IAAI,CAAC,IAAO;YACV,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC;gBAAE,OAAM;YACxC,IAAI,OAAO;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;;gBACvB,IAAI,CAAC,IAAI,CAAC,CAAA;QACjB,CAAC;QACD,IAAI;YACF,OAAO,GAAG,IAAI,CAAA;QAChB,CAAC;QACD,OAAO;YACL,OAAO,GAAG,KAAK,CAAA;YACf,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC5D,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM;KACrB,CAAA;AACH,CAAC,CAAA;AAnCY,QAAA,eAAe,mBAmC3B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { buildClient } from '@pjts/websocket-client';
|
|
2
|
+
/** Mirror of `@pjts/webrtc-signalling`'s `Signal` (kept local so the browser package has no server dependency). */
|
|
3
|
+
export interface RoomSignal<P = unknown> {
|
|
4
|
+
seq: number;
|
|
5
|
+
roomId: string;
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
type: string;
|
|
9
|
+
payload: P;
|
|
10
|
+
at: string;
|
|
11
|
+
}
|
|
12
|
+
export type SignallingState = 'connecting' | 'open' | 'polling' | 'closed';
|
|
13
|
+
export interface SignallingFallback {
|
|
14
|
+
/** Authoritative fetch of signals addressed to this party with `seq > afterSeq` (e.g. a REST endpoint). */
|
|
15
|
+
fetch(afterSeq: number): Promise<RoomSignal[]>;
|
|
16
|
+
}
|
|
17
|
+
export interface SignallingConfig {
|
|
18
|
+
/** WebSocket URL of the hub, e.g. `wss://api.example.com/api/v1/rtc/ws`. */
|
|
19
|
+
url: string;
|
|
20
|
+
roomId: string;
|
|
21
|
+
party: string;
|
|
22
|
+
/** Sent as the `bearer_<token>` subprotocol — never in the URL. */
|
|
23
|
+
token: string;
|
|
24
|
+
channelPrefix?: string;
|
|
25
|
+
/** Replay cursor — signals with `seq <= afterSeq` are never delivered. */
|
|
26
|
+
afterSeq?: number;
|
|
27
|
+
onSignal: (signal: RoomSignal) => void;
|
|
28
|
+
onState?: (state: SignallingState) => void;
|
|
29
|
+
fallback?: SignallingFallback;
|
|
30
|
+
/** How often connectivity is checked (and the fallback polled while the socket is down). Default 1000 ms. */
|
|
31
|
+
pollIntervalMs?: number;
|
|
32
|
+
/** Consecutive failed connectivity checks before polling starts. Default 2. */
|
|
33
|
+
pollAfterMisses?: number;
|
|
34
|
+
/** Test seam. */
|
|
35
|
+
clientFactory?: typeof buildClient;
|
|
36
|
+
}
|
|
37
|
+
export interface Signalling {
|
|
38
|
+
/** Subscribes (replaying after the cursor) — resolves once the replay is delivered. Rejects on nack. */
|
|
39
|
+
start(): Promise<void>;
|
|
40
|
+
/** Resolves `null` when the hub accepted the signal, otherwise the reason. */
|
|
41
|
+
send(type: string, payload?: unknown): Promise<string | null>;
|
|
42
|
+
cursor(): number;
|
|
43
|
+
state(): SignallingState;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare const createSignalling: (config: SignallingConfig) => Signalling;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSignalling = void 0;
|
|
4
|
+
const websocket_client_1 = require("@pjts/websocket-client");
|
|
5
|
+
const sequencer_1 = require("./sequencer");
|
|
6
|
+
const createSignalling = (config) => {
|
|
7
|
+
const channel = `${config.channelPrefix ?? 'rtc'}.${config.roomId}.${config.party}`;
|
|
8
|
+
const pollIntervalMs = config.pollIntervalMs ?? 1000;
|
|
9
|
+
const pollAfterMisses = config.pollAfterMisses ?? 2;
|
|
10
|
+
const sequencer = (0, sequencer_1.createSequencer)(config.afterSeq ?? 0, config.onSignal);
|
|
11
|
+
const client = (config.clientFactory ?? websocket_client_1.buildClient)({ url: config.url, token: config.token });
|
|
12
|
+
let current = 'connecting';
|
|
13
|
+
let misses = 0;
|
|
14
|
+
let polling = false;
|
|
15
|
+
let timer = null;
|
|
16
|
+
const setState = (next) => {
|
|
17
|
+
if (next === current)
|
|
18
|
+
return;
|
|
19
|
+
current = next;
|
|
20
|
+
config.onState?.(next);
|
|
21
|
+
};
|
|
22
|
+
const poll = async () => {
|
|
23
|
+
if (!config.fallback || polling)
|
|
24
|
+
return;
|
|
25
|
+
polling = true;
|
|
26
|
+
try {
|
|
27
|
+
const signals = await config.fallback.fetch(sequencer.cursor());
|
|
28
|
+
signals.slice().sort((a, b) => a.seq - b.seq).forEach(s => sequencer.push(s));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// keep trying on the next tick
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
polling = false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const tick = () => {
|
|
38
|
+
if (client.isConnected()) {
|
|
39
|
+
misses = 0;
|
|
40
|
+
setState('open');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
misses += 1;
|
|
44
|
+
if (misses >= pollAfterMisses && config.fallback) {
|
|
45
|
+
setState('polling');
|
|
46
|
+
void poll();
|
|
47
|
+
}
|
|
48
|
+
else if (current !== 'polling') {
|
|
49
|
+
setState('connecting');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
async start() {
|
|
54
|
+
timer = setInterval(tick, pollIntervalMs);
|
|
55
|
+
sequencer.hold();
|
|
56
|
+
try {
|
|
57
|
+
await client.subscribe(channel, frame => sequencer.push(frame), () => ({ afterSeq: sequencer.cursor() }));
|
|
58
|
+
setState('open');
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// A nack (unauthorized, replay failed) is final. A timeout means the socket is not reachable: the
|
|
62
|
+
// websocket client keeps the subscription and resubscribes on reconnect, and polling covers the gap.
|
|
63
|
+
const isTimeout = err instanceof Error && err.message.includes('timeout');
|
|
64
|
+
if (!isTimeout || !config.fallback) {
|
|
65
|
+
if (timer)
|
|
66
|
+
clearInterval(timer);
|
|
67
|
+
timer = null;
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
setState('polling');
|
|
71
|
+
await poll();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
sequencer.release();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
send: (type, payload = null) => client.send(channel, { type, payload: payload }),
|
|
78
|
+
cursor: () => sequencer.cursor(),
|
|
79
|
+
state: () => current,
|
|
80
|
+
close() {
|
|
81
|
+
if (timer)
|
|
82
|
+
clearInterval(timer);
|
|
83
|
+
timer = null;
|
|
84
|
+
client.disconnect();
|
|
85
|
+
setState('closed');
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
exports.createSignalling = createSignalling;
|
|
90
|
+
//# sourceMappingURL=signalling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signalling.js","sourceRoot":"","sources":["../src/signalling.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pjts/webrtc-client",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Piotr Józefów DDD utility package — browser WebRTC client: signalling, perfect negotiation, data channels, media utilities",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"author": "Piotr Józefów",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": "^18.14.0"
|
|
11
|
+
},
|
|
12
|
+
"engineStrict": true,
|
|
13
|
+
"scripts": {
|
|
14
|
+
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx",
|
|
15
|
+
"test": "NODE_ENV=test jest --coverage=false --verbose",
|
|
16
|
+
"test:watch": "NODE_ENV=test jest --coverage=false --watchAll --testNamePattern=",
|
|
17
|
+
"build": "rm -rf ./dist && tsc",
|
|
18
|
+
"package": "npm run build && npm publish --access=public",
|
|
19
|
+
"prepare": "npm install @pjts/websocket-client@latest & npm install @pjts/core@latest @pjts/websocket-server@latest --save-dev & npm version patch"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@pjts/websocket-client": "^0.0.59"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@pjts/core": "^0.0.90",
|
|
26
|
+
"@pjts/webrtc-signalling": "file:../webrtc-signalling",
|
|
27
|
+
"@pjts/websocket-server": "^0.0.60",
|
|
28
|
+
"@types/jest": "29.4.0",
|
|
29
|
+
"@types/node": "18.13.0",
|
|
30
|
+
"jest": "29.4.2",
|
|
31
|
+
"ts-jest": "29.0.5",
|
|
32
|
+
"typescript": "4.9.5"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://registry.npmjs.org"
|
|
36
|
+
}
|
|
37
|
+
}
|