@voltkit-rtc/client 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +255 -4
  3. package/dist/cjs/adaptive.d.ts +104 -0
  4. package/dist/cjs/adaptive.d.ts.map +1 -0
  5. package/dist/cjs/adaptive.js +263 -0
  6. package/dist/cjs/adaptive.js.map +1 -0
  7. package/dist/cjs/devices.d.ts +42 -0
  8. package/dist/cjs/devices.d.ts.map +1 -0
  9. package/dist/cjs/devices.js +56 -0
  10. package/dist/cjs/devices.js.map +1 -0
  11. package/dist/cjs/diagnostics.d.ts +65 -0
  12. package/dist/cjs/diagnostics.d.ts.map +1 -0
  13. package/dist/cjs/diagnostics.js +69 -0
  14. package/dist/cjs/diagnostics.js.map +1 -0
  15. package/dist/cjs/fallback.d.ts +81 -0
  16. package/dist/cjs/fallback.d.ts.map +1 -0
  17. package/dist/cjs/fallback.js +148 -0
  18. package/dist/cjs/fallback.js.map +1 -0
  19. package/dist/cjs/health.d.ts +70 -0
  20. package/dist/cjs/health.d.ts.map +1 -0
  21. package/dist/cjs/health.js +116 -0
  22. package/dist/cjs/health.js.map +1 -0
  23. package/dist/cjs/index.d.ts +350 -0
  24. package/dist/cjs/index.d.ts.map +1 -1
  25. package/dist/cjs/index.js +1201 -36
  26. package/dist/cjs/index.js.map +1 -1
  27. package/dist/cjs/link.d.ts +99 -0
  28. package/dist/cjs/link.d.ts.map +1 -0
  29. package/dist/cjs/link.js +219 -0
  30. package/dist/cjs/link.js.map +1 -0
  31. package/dist/cjs/preflight.d.ts +79 -0
  32. package/dist/cjs/preflight.d.ts.map +1 -0
  33. package/dist/cjs/preflight.js +159 -0
  34. package/dist/cjs/preflight.js.map +1 -0
  35. package/dist/cjs/recording.d.ts +14 -0
  36. package/dist/cjs/recording.d.ts.map +1 -1
  37. package/dist/cjs/recording.js +51 -5
  38. package/dist/cjs/recording.js.map +1 -1
  39. package/dist/cjs/speakers.d.ts +34 -0
  40. package/dist/cjs/speakers.d.ts.map +1 -0
  41. package/dist/cjs/speakers.js +71 -0
  42. package/dist/cjs/speakers.js.map +1 -0
  43. package/dist/esm/adaptive.d.ts +104 -0
  44. package/dist/esm/adaptive.d.ts.map +1 -0
  45. package/dist/esm/adaptive.js +256 -0
  46. package/dist/esm/adaptive.js.map +1 -0
  47. package/dist/esm/devices.d.ts +42 -0
  48. package/dist/esm/devices.d.ts.map +1 -0
  49. package/dist/esm/devices.js +50 -0
  50. package/dist/esm/devices.js.map +1 -0
  51. package/dist/esm/diagnostics.d.ts +65 -0
  52. package/dist/esm/diagnostics.d.ts.map +1 -0
  53. package/dist/esm/diagnostics.js +65 -0
  54. package/dist/esm/diagnostics.js.map +1 -0
  55. package/dist/esm/fallback.d.ts +81 -0
  56. package/dist/esm/fallback.d.ts.map +1 -0
  57. package/dist/esm/fallback.js +142 -0
  58. package/dist/esm/fallback.js.map +1 -0
  59. package/dist/esm/health.d.ts +70 -0
  60. package/dist/esm/health.d.ts.map +1 -0
  61. package/dist/esm/health.js +111 -0
  62. package/dist/esm/health.js.map +1 -0
  63. package/dist/esm/index.d.ts +350 -0
  64. package/dist/esm/index.d.ts.map +1 -1
  65. package/dist/esm/index.js +1199 -36
  66. package/dist/esm/index.js.map +1 -1
  67. package/dist/esm/link.d.ts +99 -0
  68. package/dist/esm/link.d.ts.map +1 -0
  69. package/dist/esm/link.js +214 -0
  70. package/dist/esm/link.js.map +1 -0
  71. package/dist/esm/preflight.d.ts +79 -0
  72. package/dist/esm/preflight.d.ts.map +1 -0
  73. package/dist/esm/preflight.js +153 -0
  74. package/dist/esm/preflight.js.map +1 -0
  75. package/dist/esm/recording.d.ts +14 -0
  76. package/dist/esm/recording.d.ts.map +1 -1
  77. package/dist/esm/recording.js +51 -5
  78. package/dist/esm/recording.js.map +1 -1
  79. package/dist/esm/speakers.d.ts +34 -0
  80. package/dist/esm/speakers.d.ts.map +1 -0
  81. package/dist/esm/speakers.js +67 -0
  82. package/dist/esm/speakers.js.map +1 -0
  83. package/package.json +1 -1
  84. package/src/adaptive.ts +295 -0
  85. package/src/devices.ts +70 -0
  86. package/src/diagnostics.ts +94 -0
  87. package/src/fallback.ts +169 -0
  88. package/src/health.ts +127 -0
  89. package/src/index.ts +1192 -36
  90. package/src/link.ts +255 -0
  91. package/src/preflight.ts +191 -0
  92. package/src/recording.ts +59 -21
  93. package/src/speakers.ts +66 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Cameras, microphones and speakers (docs/implementation-progress/RTC_PERF_PROGRESS.md I1).
3
+ *
4
+ * What the big SDKs do, read from their code:
5
+ * - Amazon Chime (`DefaultDeviceController`): lists devices by kind, watches `devicechange` and tells observers
6
+ * which lists changed; a microphone that ends is replaced so audio keeps flowing, a camera that ends is stopped and
7
+ * reported (`audioInputStreamEnded` / `videoInputStreamEnded`) -- never swapped for another camera on its own.
8
+ * - LiveKit (`Room.handleDeviceChange`, `switchActiveDevice`): switches the published track to a chosen device; when
9
+ * the active audio device disappears it moves to the first available one.
10
+ * - Tencent TRTC: `DEVICE_CHANGED {type, action: add | remove | active}`.
11
+ *
12
+ * This module holds the pure parts: comparing device lists and building the constraints for a chosen device.
13
+ */
14
+ /** The browser's device list, optionally of one kind. Empty outside a browser. */
15
+ export async function enumerate(kind) {
16
+ const md = typeof navigator !== 'undefined' ? navigator.mediaDevices : undefined;
17
+ if (!md?.enumerateDevices)
18
+ return [];
19
+ const all = await md.enumerateDevices();
20
+ return all
21
+ .filter((d) => d.kind === 'audioinput' || d.kind === 'videoinput' || d.kind === 'audiooutput')
22
+ .filter((d) => !kind || d.kind === kind)
23
+ .map((d) => ({ deviceId: d.deviceId, groupId: d.groupId, kind: d.kind, label: d.label }));
24
+ }
25
+ const key = (d) => `${d.kind}:${d.deviceId}`;
26
+ /** What appeared and what went between two lists. */
27
+ export function diffDevices(before, after) {
28
+ const was = new Set(before.map(key));
29
+ const now = new Set(after.map(key));
30
+ return {
31
+ added: after.filter((d) => !was.has(key(d))),
32
+ removed: before.filter((d) => !now.has(key(d))),
33
+ };
34
+ }
35
+ /**
36
+ * The constraints for capturing `deviceId` (or the system default when null), keeping everything else the app asked
37
+ * for: size, frame rate, echo cancellation. `base` is what publish was given for that kind.
38
+ */
39
+ export function constraintsFor(base, deviceId) {
40
+ const rest = typeof base === 'object' && base !== null ? { ...base } : {};
41
+ delete rest.deviceId;
42
+ delete rest.groupId;
43
+ return deviceId === null ? rest : { ...rest, deviceId: { exact: deviceId } };
44
+ }
45
+ /** The device a live track captures, when it is a device capture at all (a canvas or a processed track has none). */
46
+ export function deviceIdOf(track) {
47
+ const id = track?.getSettings?.().deviceId;
48
+ return typeof id === 'string' && id.length > 0 ? id : null;
49
+ }
50
+ //# sourceMappingURL=devices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/devices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmBH,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAiB;IAC/C,MAAM,EAAE,GAAG,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;IACjF,IAAI,CAAC,EAAE,EAAE,gBAAgB;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,gBAAgB,EAAE,CAAC;IACxC,OAAO,GAAG;SACP,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC;SAC7F,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;SACvC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAC5G,CAAC;AAED,MAAM,GAAG,GAAG,CAAC,CAAc,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;AAE1D,qDAAqD;AACrD,MAAM,UAAU,WAAW,CAAC,MAA8B,EAAE,KAA6B;IACvF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;KAChD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAiD,EAAE,QAAuB;IACvG,MAAM,IAAI,GAA0B,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACjG,OAAO,IAAI,CAAC,QAAQ,CAAC;IACrB,OAAO,IAAI,CAAC,OAAO,CAAC;IACpB,OAAO,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC;AAC/E,CAAC;AAED,qHAAqH;AACrH,MAAM,UAAU,UAAU,CAAC,KAA0C;IACnE,MAAM,EAAE,GAAG,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC,QAAQ,CAAC;IAC3C,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Diagnostics for support (docs/implementation-progress/RTC_PERF_PROGRESS.md S4).
3
+ *
4
+ * Agora uploads its SDK log on request (`enableLogUpload`), Twilio sends call telemetry to Insights, Stream Video
5
+ * traces session events to its servers. Shipping telemetry needs a backend; what helps today is a snapshot an app can
6
+ * attach to a support ticket: the SDK's recent events (a ring buffer) and its state now.
7
+ */
8
+ /** This package's version; kept equal to package.json by a test. */
9
+ export declare const SDK_VERSION = "0.8.0";
10
+ /** How many recent events the log keeps. */
11
+ export declare const DIAGNOSTICS_LOG_SIZE = 200;
12
+ export interface DiagnosticsEntry {
13
+ /** Unix ms. */
14
+ at: number;
15
+ event: string;
16
+ /** A short summary of what the event carried (no media objects). */
17
+ detail?: string;
18
+ }
19
+ /** A ring buffer of the SDK's recent events. */
20
+ export declare class DiagnosticsLog {
21
+ private readonly now;
22
+ private readonly entries;
23
+ constructor(now?: () => number);
24
+ /** Keep an emitted event, when it is one worth keeping. */
25
+ record(event: string, args: unknown[]): void;
26
+ list(): DiagnosticsEntry[];
27
+ }
28
+ export interface DiagnosticsTransport {
29
+ state: string;
30
+ path: {
31
+ protocol: 'udp' | 'tcp' | null;
32
+ relayed: boolean;
33
+ } | null;
34
+ }
35
+ export interface Diagnostics {
36
+ sdk: string;
37
+ at: number;
38
+ userAgent: string;
39
+ connectionState: string;
40
+ room: string | null;
41
+ identity: string | null;
42
+ role: string | null;
43
+ transports: {
44
+ send: DiagnosticsTransport | null;
45
+ recv: DiagnosticsTransport | null;
46
+ };
47
+ network: {
48
+ rttMs: number | null;
49
+ availableKbps: number | null;
50
+ } | null;
51
+ published: {
52
+ source: string;
53
+ kind: string;
54
+ paused: boolean;
55
+ }[];
56
+ received: {
57
+ identity: string;
58
+ source: string;
59
+ kind: string;
60
+ paused: boolean;
61
+ }[];
62
+ /** Recent events, oldest first. */
63
+ log: DiagnosticsEntry[];
64
+ }
65
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/diagnostics.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,oEAAoE;AACpE,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAC/B,eAAe;IACf,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA6BD,gDAAgD;AAChD,qBAAa,cAAc;IAGb,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;gBAErB,GAAG,GAAE,MAAM,MAAiB;IAEzD,2DAA2D;IAC3D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAa5C,IAAI,IAAI,gBAAgB,EAAE;CAG3B;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE;QAAE,QAAQ,EAAE,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE;QAAE,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAAA;KAAE,CAAC;IACrF,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IACvE,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAC/D,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAChF,mCAAmC;IACnC,GAAG,EAAE,gBAAgB,EAAE,CAAC;CACzB"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Diagnostics for support (docs/implementation-progress/RTC_PERF_PROGRESS.md S4).
3
+ *
4
+ * Agora uploads its SDK log on request (`enableLogUpload`), Twilio sends call telemetry to Insights, Stream Video
5
+ * traces session events to its servers. Shipping telemetry needs a backend; what helps today is a snapshot an app can
6
+ * attach to a support ticket: the SDK's recent events (a ring buffer) and its state now.
7
+ */
8
+ /** This package's version; kept equal to package.json by a test. */
9
+ export const SDK_VERSION = '0.8.0';
10
+ /** How many recent events the log keeps. */
11
+ export const DIAGNOSTICS_LOG_SIZE = 200;
12
+ /** The events worth keeping for support, and how to summarise each. */
13
+ const SUMMARY = {
14
+ connectionStateChanged: ([e]) => { const s = e; return `${s.previous} → ${s.state}`; },
15
+ disconnected: ([reason]) => String(reason),
16
+ closed: ([reason]) => String(reason),
17
+ mustReconnect: ([e]) => e.reason,
18
+ mediaReconnecting: ([e]) => { const m = e; return `${m.transportId} ${m.reconnecting ? 'recovering' : 'recovered'}`; },
19
+ error: ([e]) => {
20
+ const err = e;
21
+ const message = err.message ?? '';
22
+ // RpcError's message already starts with its code.
23
+ return (err.code && !message.startsWith(`${err.code}:`) ? `${err.code}: ${message}` : message).slice(0, 300);
24
+ },
25
+ streamFallback: ([e]) => { const f = e; return `${f.producerId} ${f.fallback ? 'held' : 'back'} (${f.reason})`; },
26
+ mediaWarning: ([e]) => { const w = e; return `${w.code} ${w.active ? 'started' : 'cleared'}`; },
27
+ cpuConstrained: ([e]) => String(e.constrained),
28
+ videoLimited: ([e]) => e.reason,
29
+ localTrackEnded: ([e]) => { const t = e; return `${t.kind} ${t.recovered ? 'recovered' : 'off'}`; },
30
+ devicesChanged: ([e]) => { const d = e; return `+${d.added?.length ?? 0} -${d.removed?.length ?? 0}`; },
31
+ kicked: ([e]) => e.reason,
32
+ roomClosed: ([e]) => e.reason,
33
+ usageLimit: ([e]) => e.reason,
34
+ autoplayFailed: () => undefined,
35
+ trackEnded: ([e]) => e.producerId,
36
+ track: ([e]) => { const t = e; return `${t.identity} ${t.source}`; },
37
+ };
38
+ /** A ring buffer of the SDK's recent events. */
39
+ export class DiagnosticsLog {
40
+ now;
41
+ entries = [];
42
+ constructor(now = Date.now) {
43
+ this.now = now;
44
+ }
45
+ /** Keep an emitted event, when it is one worth keeping. */
46
+ record(event, args) {
47
+ const summarize = SUMMARY[event];
48
+ if (!summarize)
49
+ return;
50
+ let detail;
51
+ try {
52
+ detail = summarize(args);
53
+ }
54
+ catch {
55
+ detail = undefined;
56
+ }
57
+ this.entries.push(detail === undefined ? { at: this.now(), event } : { at: this.now(), event, detail });
58
+ if (this.entries.length > DIAGNOSTICS_LOG_SIZE)
59
+ this.entries.shift();
60
+ }
61
+ list() {
62
+ return this.entries.map((e) => ({ ...e }));
63
+ }
64
+ }
65
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/diagnostics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Audio-only fallback (docs/implementation-progress/RTC_PERF_PROGRESS.md H1).
3
+ *
4
+ * When this viewer's connection is so poor that even the sound breaks up, remote camera videos are paused at the
5
+ * server so what bandwidth there is goes to the audio, and they come back once the connection has recovered.
6
+ *
7
+ * - Agora's `setStreamFallbackOption(uid, AUDIO_ONLY)` drops to the low stream and then to audio only, and reports
8
+ * `stream-fallback` (fallback / recover).
9
+ * - Amazon Chime watches the audio it receives (`UnusableAudioWarningConnectionHealthPolicy`): about half of the
10
+ * expected packets missing over 15 one-second samples suggests stopping video, with a 60 s cool-down.
11
+ *
12
+ * This follows Chime's signal -- the audio actually received -- measured as RTP loss (sequence gaps) rather than
13
+ * packet counts, because Opus DTX sends almost nothing during silence and a count would read silence as loss.
14
+ * Extreme only: {@link FALLBACK_LOSS} of the audio lost over {@link FALLBACK_SAMPLES} samples, with at least
15
+ * {@link MIN_PACKETS} packets to judge by. Back after a hold ({@link FIRST_HOLD_MS}, doubled for each relapse within
16
+ * {@link RELAPSE_MS}, up to {@link MAX_HOLD_MS}) once the loss is under {@link RECOVER_LOSS} -- or nobody spoke
17
+ * enough to tell, in which case video comes back and a still-bad connection falls back again, holding longer.
18
+ *
19
+ * The SFU cutting a video for bandwidth by itself is reported separately, by the server (`reason: 'bandwidth'`).
20
+ * Pure: the decisions take counters and a clock.
21
+ */
22
+ import type { StatsEntry } from './stats.js';
23
+ /** How often the received audio is sampled while the fallback could be needed. */
24
+ export declare const PROTECT_SAMPLE_MS = 3000;
25
+ /** This share of the audio lost (RTP sequence gaps) over the window: fall back. */
26
+ export declare const FALLBACK_LOSS = 0.25;
27
+ /** Below this share lost over the recovery window: video may come back. */
28
+ export declare const RECOVER_LOSS = 0.05;
29
+ /** Samples judged for falling back (15 s at 3 s) -- at least 3 of them. */
30
+ export declare const FALLBACK_SAMPLES = 5;
31
+ /** Samples judged for coming back (12 s). */
32
+ export declare const RECOVER_SAMPLES = 4;
33
+ /** Fewer audio packets than this over the fallback window are too few to judge. */
34
+ export declare const MIN_PACKETS = 150;
35
+ /** Fewer than this over the recovery window: nobody spoke enough to tell. */
36
+ export declare const MIN_RECOVER_PACKETS = 30;
37
+ /** The first fallback lasts at least this long. */
38
+ export declare const FIRST_HOLD_MS = 20000;
39
+ /** A relapse doubles the hold, up to this. */
40
+ export declare const MAX_HOLD_MS = 160000;
41
+ /** Falling back again within this long of coming back is a relapse. */
42
+ export declare const RELAPSE_MS = 120000;
43
+ export interface AudioCounters {
44
+ received: number;
45
+ lost: number;
46
+ }
47
+ /** The received-audio counters of a receive transport's report, summed over its audio streams. */
48
+ export declare function readInboundAudio(report: Iterable<StatsEntry>): AudioCounters;
49
+ /**
50
+ * The sent-audio counters of a send transport's report: packets sent, and how many of them the server reported
51
+ * lost (its RTCP receiver reports). For the publisher side (Agora's setLocalPublishFallbackOption): `received` is what
52
+ * got through.
53
+ */
54
+ export declare function readOutboundAudio(report: Iterable<StatsEntry>): AudioCounters;
55
+ /** Decides when to fall back to audio only and when video may come back. */
56
+ export declare class AudioProtection {
57
+ private readonly onChange;
58
+ private readonly now;
59
+ private samples;
60
+ private previous;
61
+ private activeSince;
62
+ private hold;
63
+ private recoveredAt;
64
+ constructor(onChange: (active: boolean) => void, now?: () => number);
65
+ /** Whether remote camera videos are held back now. */
66
+ get active(): boolean;
67
+ /** How long the current (or next first) fallback holds, in ms. */
68
+ get holdMs(): number;
69
+ /**
70
+ * One reading of the cumulative counters. They only go up on one transport; lower ones are a new transport, and
71
+ * judging starts over from them.
72
+ */
73
+ feed(counters: AudioCounters): void;
74
+ /** Come back now (the app turned the fallback off). */
75
+ release(): void;
76
+ /** The transports went away (reconnect, close): forget everything, without telling. */
77
+ reset(): void;
78
+ private activate;
79
+ private recover;
80
+ }
81
+ //# sourceMappingURL=fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fallback.d.ts","sourceRoot":"","sources":["../../src/fallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,mFAAmF;AACnF,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,2EAA2E;AAC3E,eAAO,MAAM,YAAY,OAAO,CAAC;AACjC,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,6CAA6C;AAC7C,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,mFAAmF;AACnF,eAAO,MAAM,WAAW,MAAM,CAAC;AAC/B,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,mDAAmD;AACnD,eAAO,MAAM,aAAa,QAAS,CAAC;AACpC,8CAA8C;AAC9C,eAAO,MAAM,WAAW,SAAU,CAAC;AACnC,uEAAuE;AACvE,eAAO,MAAM,UAAU,SAAU,CAAC;AAElC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAID,kGAAkG;AAClG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAU5E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAW7E;AAED,4EAA4E;AAC5E,qBAAa,eAAe;IAQxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,IAAI,CAAiB;IAC7B,OAAO,CAAC,WAAW,CAAuB;gBAGvB,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,EACnC,GAAG,GAAE,MAAM,MAAiB;IAG/C,sDAAsD;IACtD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,kEAAkE;IAClE,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAsBnC,uDAAuD;IACvD,OAAO,IAAI,IAAI;IAIf,uFAAuF;IACvF,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,OAAO;CAMhB"}
@@ -0,0 +1,142 @@
1
+ /** How often the received audio is sampled while the fallback could be needed. */
2
+ export const PROTECT_SAMPLE_MS = 3000;
3
+ /** This share of the audio lost (RTP sequence gaps) over the window: fall back. */
4
+ export const FALLBACK_LOSS = 0.25;
5
+ /** Below this share lost over the recovery window: video may come back. */
6
+ export const RECOVER_LOSS = 0.05;
7
+ /** Samples judged for falling back (15 s at 3 s) -- at least 3 of them. */
8
+ export const FALLBACK_SAMPLES = 5;
9
+ /** Samples judged for coming back (12 s). */
10
+ export const RECOVER_SAMPLES = 4;
11
+ /** Fewer audio packets than this over the fallback window are too few to judge. */
12
+ export const MIN_PACKETS = 150;
13
+ /** Fewer than this over the recovery window: nobody spoke enough to tell. */
14
+ export const MIN_RECOVER_PACKETS = 30;
15
+ /** The first fallback lasts at least this long. */
16
+ export const FIRST_HOLD_MS = 20_000;
17
+ /** A relapse doubles the hold, up to this. */
18
+ export const MAX_HOLD_MS = 160_000;
19
+ /** Falling back again within this long of coming back is a relapse. */
20
+ export const RELAPSE_MS = 120_000;
21
+ const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
22
+ /** The received-audio counters of a receive transport's report, summed over its audio streams. */
23
+ export function readInboundAudio(report) {
24
+ let received = 0;
25
+ let lost = 0;
26
+ for (const e of report) {
27
+ if (typeof e !== 'object' || e === null || e.type !== 'inbound-rtp')
28
+ continue;
29
+ if (e.kind !== 'audio' && e.mediaType !== 'audio')
30
+ continue;
31
+ received += num(e.packetsReceived);
32
+ lost += Math.max(0, num(e.packetsLost));
33
+ }
34
+ return { received, lost };
35
+ }
36
+ /**
37
+ * The sent-audio counters of a send transport's report: packets sent, and how many of them the server reported
38
+ * lost (its RTCP receiver reports). For the publisher side (Agora's setLocalPublishFallbackOption): `received` is what
39
+ * got through.
40
+ */
41
+ export function readOutboundAudio(report) {
42
+ let sent = 0;
43
+ let lost = 0;
44
+ for (const e of report) {
45
+ if (typeof e !== 'object' || e === null)
46
+ continue;
47
+ const audio = e.kind === 'audio' || e.mediaType === 'audio';
48
+ if (!audio)
49
+ continue;
50
+ if (e.type === 'outbound-rtp')
51
+ sent += num(e.packetsSent);
52
+ else if (e.type === 'remote-inbound-rtp')
53
+ lost += Math.max(0, num(e.packetsLost));
54
+ }
55
+ return { received: Math.max(0, sent - lost), lost };
56
+ }
57
+ /** Decides when to fall back to audio only and when video may come back. */
58
+ export class AudioProtection {
59
+ onChange;
60
+ now;
61
+ samples = [];
62
+ previous = null;
63
+ activeSince = null;
64
+ hold = FIRST_HOLD_MS;
65
+ recoveredAt = null;
66
+ constructor(onChange, now = Date.now) {
67
+ this.onChange = onChange;
68
+ this.now = now;
69
+ }
70
+ /** Whether remote camera videos are held back now. */
71
+ get active() {
72
+ return this.activeSince !== null;
73
+ }
74
+ /** How long the current (or next first) fallback holds, in ms. */
75
+ get holdMs() {
76
+ return this.hold;
77
+ }
78
+ /**
79
+ * One reading of the cumulative counters. They only go up on one transport; lower ones are a new transport, and
80
+ * judging starts over from them.
81
+ */
82
+ feed(counters) {
83
+ const before = this.previous;
84
+ this.previous = counters;
85
+ if (before === null || counters.received < before.received || counters.lost < before.lost) {
86
+ this.samples = [];
87
+ return;
88
+ }
89
+ this.samples.push({ received: counters.received - before.received, lost: counters.lost - before.lost });
90
+ if (this.samples.length > Math.max(FALLBACK_SAMPLES, RECOVER_SAMPLES))
91
+ this.samples.shift();
92
+ if (this.activeSince === null) {
93
+ const window = this.samples.slice(-FALLBACK_SAMPLES);
94
+ if (window.length < 3)
95
+ return;
96
+ const { total, lost } = sum(window);
97
+ if (total >= MIN_PACKETS && lost / total >= FALLBACK_LOSS)
98
+ this.activate();
99
+ return;
100
+ }
101
+ if (this.now() - this.activeSince < this.hold || this.samples.length < RECOVER_SAMPLES)
102
+ return;
103
+ const { total, lost } = sum(this.samples.slice(-RECOVER_SAMPLES));
104
+ if (total < MIN_RECOVER_PACKETS || lost / total < RECOVER_LOSS)
105
+ this.recover();
106
+ }
107
+ /** Come back now (the app turned the fallback off). */
108
+ release() {
109
+ if (this.activeSince !== null)
110
+ this.recover();
111
+ }
112
+ /** The transports went away (reconnect, close): forget everything, without telling. */
113
+ reset() {
114
+ this.activeSince = null;
115
+ this.samples = [];
116
+ this.previous = null;
117
+ }
118
+ activate() {
119
+ const now = this.now();
120
+ const relapse = this.recoveredAt !== null && now - this.recoveredAt < RELAPSE_MS;
121
+ this.hold = relapse ? Math.min(this.hold * 2, MAX_HOLD_MS) : FIRST_HOLD_MS;
122
+ this.activeSince = now;
123
+ this.samples = [];
124
+ this.onChange(true);
125
+ }
126
+ recover() {
127
+ this.activeSince = null;
128
+ this.recoveredAt = this.now();
129
+ this.samples = [];
130
+ this.onChange(false);
131
+ }
132
+ }
133
+ function sum(samples) {
134
+ let total = 0;
135
+ let lost = 0;
136
+ for (const s of samples) {
137
+ total += s.received + s.lost;
138
+ lost += s.lost;
139
+ }
140
+ return { total, lost };
141
+ }
142
+ //# sourceMappingURL=fallback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../src/fallback.ts"],"names":[],"mappings":"AAuBA,kFAAkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AACtC,mFAAmF;AACnF,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC;AAClC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AACjC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAClC,6CAA6C;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AACjC,mFAAmF;AACnF,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAC/B,6EAA6E;AAC7E,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AACtC,mDAAmD;AACnD,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AACpC,8CAA8C;AAC9C,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AACnC,uEAAuE;AACvE,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC;AAOlC,MAAM,GAAG,GAAG,CAAC,KAAc,EAAU,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1G,kGAAkG;AAClG,MAAM,UAAU,gBAAgB,CAAC,MAA4B;IAC3D,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa;YAAE,SAAS;QAC9E,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,SAAS,KAAK,OAAO;YAAE,SAAS;QAC5D,QAAQ,IAAI,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;QACnC,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAA4B;IAC5D,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS;QAClD,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,SAAS,KAAK,OAAO,CAAC;QAC5D,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc;YAAE,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;aACrD,IAAI,CAAC,CAAC,IAAI,KAAK,oBAAoB;YAAE,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;AACtD,CAAC;AAED,4EAA4E;AAC5E,MAAM,OAAO,eAAe;IAQP;IACA;IARX,OAAO,GAAoB,EAAE,CAAC;IAC9B,QAAQ,GAAyB,IAAI,CAAC;IACtC,WAAW,GAAkB,IAAI,CAAC;IAClC,IAAI,GAAG,aAAa,CAAC;IACrB,WAAW,GAAkB,IAAI,CAAC;IAE1C,YACmB,QAAmC,EACnC,MAAoB,IAAI,CAAC,GAAG;QAD5B,aAAQ,GAAR,QAAQ,CAA2B;QACnC,QAAG,GAAH,GAAG,CAAyB;IAC5C,CAAC;IAEJ,sDAAsD;IACtD,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC;IACnC,CAAC;IAED,kEAAkE;IAClE,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,QAAuB;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;YAC1F,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACxG,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,eAAe,CAAC;YAAE,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAE5F,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,gBAAgB,CAAC,CAAC;YACrD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO;YAC9B,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;YACpC,IAAI,KAAK,IAAI,WAAW,IAAI,IAAI,GAAG,KAAK,IAAI,aAAa;gBAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3E,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,eAAe;YAAE,OAAO;QAC/F,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;QAClE,IAAI,KAAK,GAAG,mBAAmB,IAAI,IAAI,GAAG,KAAK,GAAG,YAAY;YAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IACjF,CAAC;IAED,uDAAuD;IACvD,OAAO;QACL,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;YAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAChD,CAAC;IAED,uFAAuF;IACvF,KAAK;QACH,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,CAAC;IAEO,QAAQ;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,KAAK,IAAI,IAAI,GAAG,GAAG,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC;QACjF,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;QAC3E,IAAI,CAAC,WAAW,GAAG,GAAG,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9B,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;CACF;AAED,SAAS,GAAG,CAAC,OAAiC;IAC5C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,KAAK,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC;QAC7B,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC;IACjB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Media health warnings (docs/implementation-progress/RTC_PERF_PROGRESS.md K1).
3
+ *
4
+ * - Agora's `exception` event pairs each problem with its recovery: FRAMERATE_INPUT_TOO_LOW / _RECOVER (the camera
5
+ * delivers too few frames), AUDIO_INPUT_LEVEL_TOO_LOW / _RECOVER, ...
6
+ * - LiveKit's `AudioSilenceDetected`: a microphone that delivers nothing but silence (a hardware mute switch, a
7
+ * broken device) right after it starts.
8
+ * - LiveKit watches the track's `mute` event (the OS took the device: a phone call, another app) and reacts after 5 s.
9
+ *
10
+ * Each warning is reported once when it starts (`active: true`) and once when it clears (`active: false`).
11
+ * Pure: the decisions take samples.
12
+ */
13
+ export type MediaWarningCode =
14
+ /** The microphone is on but delivers exact digital silence: a mute switch, a broken or wrongly chosen device. */
15
+ 'mic-silent'
16
+ /** The camera delivers far fewer frames than asked: usually a dark room (the camera exposes longer), or a busy device. */
17
+ | 'camera-fps-low'
18
+ /** The system took the camera from this page for now (a phone call, another app, a locked screen). */
19
+ | 'camera-interrupted'
20
+ /** The system took the microphone (a phone call, another app). */
21
+ | 'mic-interrupted';
22
+ export interface MediaWarning {
23
+ code: MediaWarningCode;
24
+ /** true when it starts, false when it clears. */
25
+ active: boolean;
26
+ }
27
+ /** How often the published camera and microphone are sampled while someone listens. */
28
+ export declare const HEALTH_SAMPLE_MS = 2000;
29
+ /** Samples of exact silence before mic-silent (6 s). */
30
+ export declare const SILENT_SAMPLES = 3;
31
+ /** Below this the level counts as exact digital silence (a real room's noise floor is well above it). */
32
+ export declare const SILENCE_LEVEL = 0.0001;
33
+ /** Fewer frames than this per second, when 24 or more were asked, is camera-fps-low. */
34
+ export declare const LOW_FPS = 15;
35
+ /** Samples below LOW_FPS before warning (6 s); samples at RECOVERED_FPS or more before clearing. */
36
+ export declare const LOW_FPS_SAMPLES = 3;
37
+ export declare const RECOVERED_FPS = 20;
38
+ export declare const RECOVER_SAMPLES = 2;
39
+ /** A system mute shorter than this is a glitch, not an interruption. */
40
+ export declare const INTERRUPTED_AFTER_MS = 2000;
41
+ /** One reading of the published devices. null: not published (or nothing measured). */
42
+ export interface HealthSample {
43
+ /** The microphone's level, while it is on. */
44
+ micLevel: number | null;
45
+ /** The frames per second the camera delivers, while it is on, and what was asked. */
46
+ cameraFps: number | null;
47
+ askedFps: number;
48
+ }
49
+ /** Decides mic-silent and camera-fps-low from samples. */
50
+ export declare class HealthDecisions {
51
+ private readonly report;
52
+ private silent;
53
+ private lowFps;
54
+ private goodFps;
55
+ private readonly activeNow;
56
+ constructor(report: (warning: MediaWarning) => void);
57
+ isActive(code: MediaWarningCode): boolean;
58
+ feed(sample: HealthSample): void;
59
+ /** A system interruption started or ended (decided by the caller's timer). */
60
+ interrupted(code: 'camera-interrupted' | 'mic-interrupted', active: boolean): void;
61
+ /** Everything clears (unpublished, closed): each active warning is reported cleared. */
62
+ clearAll(): void;
63
+ private set;
64
+ }
65
+ /** The microphone level and camera frame rate from a producer's statistics (media-source). */
66
+ export declare function readMediaSource(report: Iterable<Record<string, unknown>>): {
67
+ level: number | null;
68
+ fps: number | null;
69
+ };
70
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,gBAAgB;AAC1B,iHAAiH;AAC/G,YAAY;AACd,0HAA0H;GACxH,gBAAgB;AAClB,sGAAsG;GACpG,oBAAoB;AACtB,kEAAkE;GAChE,iBAAiB,CAAC;AAEtB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,iDAAiD;IACjD,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,wDAAwD;AACxD,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,yGAAyG;AACzG,eAAO,MAAM,aAAa,SAAS,CAAC;AACpC,wFAAwF;AACxF,eAAO,MAAM,OAAO,KAAK,CAAC;AAC1B,oGAAoG;AACpG,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,wEAAwE;AACxE,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,uFAAuF;AACvF,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,qFAAqF;IACrF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0DAA0D;AAC1D,qBAAa,eAAe;IAMd,OAAO,CAAC,QAAQ,CAAC,MAAM;IALnC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+B;gBAE5B,MAAM,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI;IAEpE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO;IAIzC,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IA6BhC,8EAA8E;IAC9E,WAAW,CAAC,IAAI,EAAE,oBAAoB,GAAG,iBAAiB,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAIlF,wFAAwF;IACxF,QAAQ,IAAI,IAAI;IAOhB,OAAO,CAAC,GAAG;CAMZ;AAED,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CASvH"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Media health warnings (docs/implementation-progress/RTC_PERF_PROGRESS.md K1).
3
+ *
4
+ * - Agora's `exception` event pairs each problem with its recovery: FRAMERATE_INPUT_TOO_LOW / _RECOVER (the camera
5
+ * delivers too few frames), AUDIO_INPUT_LEVEL_TOO_LOW / _RECOVER, ...
6
+ * - LiveKit's `AudioSilenceDetected`: a microphone that delivers nothing but silence (a hardware mute switch, a
7
+ * broken device) right after it starts.
8
+ * - LiveKit watches the track's `mute` event (the OS took the device: a phone call, another app) and reacts after 5 s.
9
+ *
10
+ * Each warning is reported once when it starts (`active: true`) and once when it clears (`active: false`).
11
+ * Pure: the decisions take samples.
12
+ */
13
+ /** How often the published camera and microphone are sampled while someone listens. */
14
+ export const HEALTH_SAMPLE_MS = 2000;
15
+ /** Samples of exact silence before mic-silent (6 s). */
16
+ export const SILENT_SAMPLES = 3;
17
+ /** Below this the level counts as exact digital silence (a real room's noise floor is well above it). */
18
+ export const SILENCE_LEVEL = 0.0001;
19
+ /** Fewer frames than this per second, when 24 or more were asked, is camera-fps-low. */
20
+ export const LOW_FPS = 15;
21
+ /** Samples below LOW_FPS before warning (6 s); samples at RECOVERED_FPS or more before clearing. */
22
+ export const LOW_FPS_SAMPLES = 3;
23
+ export const RECOVERED_FPS = 20;
24
+ export const RECOVER_SAMPLES = 2;
25
+ /** A system mute shorter than this is a glitch, not an interruption. */
26
+ export const INTERRUPTED_AFTER_MS = 2000;
27
+ /** Decides mic-silent and camera-fps-low from samples. */
28
+ export class HealthDecisions {
29
+ report;
30
+ silent = 0;
31
+ lowFps = 0;
32
+ goodFps = 0;
33
+ activeNow = new Set();
34
+ constructor(report) {
35
+ this.report = report;
36
+ }
37
+ isActive(code) {
38
+ return this.activeNow.has(code);
39
+ }
40
+ feed(sample) {
41
+ // mic-silent: exact silence held; any sound clears it. Off or unpublished: not a warning.
42
+ if (sample.micLevel === null) {
43
+ this.silent = 0;
44
+ this.set('mic-silent', false);
45
+ }
46
+ else if (sample.micLevel <= SILENCE_LEVEL) {
47
+ this.silent += 1;
48
+ if (this.silent >= SILENT_SAMPLES)
49
+ this.set('mic-silent', true);
50
+ }
51
+ else {
52
+ this.silent = 0;
53
+ this.set('mic-silent', false);
54
+ }
55
+ // camera-fps-low: only when a smooth rate was asked; cleared once it holds again.
56
+ if (sample.cameraFps === null || sample.askedFps < 24) {
57
+ this.lowFps = 0;
58
+ this.goodFps = 0;
59
+ this.set('camera-fps-low', false);
60
+ }
61
+ else if (sample.cameraFps < LOW_FPS) {
62
+ this.goodFps = 0;
63
+ this.lowFps += 1;
64
+ if (this.lowFps >= LOW_FPS_SAMPLES)
65
+ this.set('camera-fps-low', true);
66
+ }
67
+ else {
68
+ this.lowFps = 0;
69
+ if (sample.cameraFps >= RECOVERED_FPS)
70
+ this.goodFps += 1;
71
+ if (this.goodFps >= RECOVER_SAMPLES)
72
+ this.set('camera-fps-low', false);
73
+ }
74
+ }
75
+ /** A system interruption started or ended (decided by the caller's timer). */
76
+ interrupted(code, active) {
77
+ this.set(code, active);
78
+ }
79
+ /** Everything clears (unpublished, closed): each active warning is reported cleared. */
80
+ clearAll() {
81
+ for (const code of [...this.activeNow])
82
+ this.set(code, false);
83
+ this.silent = 0;
84
+ this.lowFps = 0;
85
+ this.goodFps = 0;
86
+ }
87
+ set(code, active) {
88
+ if (this.activeNow.has(code) === active)
89
+ return;
90
+ if (active)
91
+ this.activeNow.add(code);
92
+ else
93
+ this.activeNow.delete(code);
94
+ this.report({ code, active });
95
+ }
96
+ }
97
+ /** The microphone level and camera frame rate from a producer's statistics (media-source). */
98
+ export function readMediaSource(report) {
99
+ let level = null;
100
+ let fps = null;
101
+ for (const s of report) {
102
+ if (s.type !== 'media-source')
103
+ continue;
104
+ if (typeof s.audioLevel === 'number')
105
+ level = s.audioLevel;
106
+ if (typeof s.framesPerSecond === 'number')
107
+ fps = s.framesPerSecond;
108
+ }
109
+ return { level, fps };
110
+ }
111
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/health.ts"],"names":[],"mappings":"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"}