@livekit/rtc-node 0.13.27 → 0.13.29
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/dist/ffi_client.cjs +11 -7
- package/dist/ffi_client.cjs.map +1 -1
- package/dist/ffi_client.d.cts +2 -0
- package/dist/ffi_client.d.ts +2 -0
- package/dist/ffi_client.d.ts.map +1 -1
- package/dist/ffi_client.js +11 -7
- package/dist/ffi_client.js.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/participant.cjs +3 -0
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -1
- package/dist/participant.d.ts +2 -1
- package/dist/participant.d.ts.map +1 -1
- package/dist/participant.js +3 -0
- package/dist/participant.js.map +1 -1
- package/dist/room.cjs +77 -0
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +20 -1
- package/dist/room.d.ts +20 -1
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +77 -0
- package/dist/room.js.map +1 -1
- package/dist/track_publication.cjs +11 -0
- package/dist/track_publication.cjs.map +1 -1
- package/dist/track_publication.d.cts +9 -0
- package/dist/track_publication.d.ts +9 -0
- package/dist/track_publication.d.ts.map +1 -1
- package/dist/track_publication.js +11 -0
- package/dist/track_publication.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -5
- package/src/ffi_client.ts +6 -0
- package/src/index.ts +11 -2
- package/src/participant.ts +5 -0
- package/src/room.ts +109 -2
- package/src/tests/e2e.test.ts +157 -0
- package/src/track_publication.ts +12 -0
- package/src/version.ts +1 -1
|
@@ -46,6 +46,17 @@ class TrackPublication {
|
|
|
46
46
|
var _a;
|
|
47
47
|
return (_a = this.info) == null ? void 0 : _a.encryptionType;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Update the publication's info in place. Used by the SDK when the
|
|
51
|
+
* server re-issues IDs / metadata for an existing publication (e.g.
|
|
52
|
+
* after a full reconnect). Application code holding a cached
|
|
53
|
+
* publication reference continues to read fresh values via the
|
|
54
|
+
* unchanged object identity.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
updateInfo(info) {
|
|
58
|
+
this.info = info;
|
|
59
|
+
}
|
|
49
60
|
}
|
|
50
61
|
class LocalTrackPublication extends TrackPublication {
|
|
51
62
|
constructor(ownedInfo) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/track_publication.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { EncryptionType } from '@livekit/rtc-ffi-bindings';\nimport type { SetSubscribedResponse } from '@livekit/rtc-ffi-bindings';\nimport { SetSubscribedRequest } from '@livekit/rtc-ffi-bindings';\nimport type {\n OwnedTrackPublication,\n TrackKind,\n TrackPublicationInfo,\n TrackSource,\n} from '@livekit/rtc-ffi-bindings';\nimport { FfiHandle } from '@livekit/rtc-ffi-bindings';\nimport { FfiClient } from './ffi_client.js';\nimport type { Track } from './track.js';\n\nexport abstract class TrackPublication {\n /** @internal */\n ffiHandle: FfiHandle;\n\n /** @internal */\n info?: TrackPublicationInfo;\n track?: Track;\n\n constructor(ownedInfo: OwnedTrackPublication) {\n this.info = ownedInfo.info;\n this.ffiHandle = new FfiHandle(ownedInfo.handle!.id!);\n }\n\n get sid(): string | undefined {\n return this.info?.sid;\n }\n\n get name(): string | undefined {\n return this.info?.name;\n }\n\n get kind(): TrackKind | undefined {\n return this.info?.kind;\n }\n\n get source(): TrackSource | undefined {\n return this.info?.source;\n }\n\n get simulcasted(): boolean | undefined {\n return this.info?.simulcasted;\n }\n\n get width(): number | undefined {\n return this.info?.width;\n }\n\n get height(): number | undefined {\n return this.info?.height;\n }\n\n get mimeType(): string | undefined {\n return this.info?.mimeType;\n }\n\n get muted(): boolean | undefined {\n return this.info?.muted;\n }\n\n get encryptionType(): EncryptionType | undefined {\n return this.info?.encryptionType;\n }\n}\n\nexport class LocalTrackPublication extends TrackPublication {\n private firstSubscription: Promise<void>;\n private firstSubscriptionResolver: (() => void) | null = null;\n\n constructor(ownedInfo: OwnedTrackPublication) {\n super(ownedInfo);\n this.firstSubscription = new Promise<void>((resolve) => {\n this.firstSubscriptionResolver = resolve;\n });\n }\n\n async waitForSubscription(): Promise<void> {\n await this.firstSubscription;\n }\n\n /** @internal */\n resolveFirstSubscription(): void {\n if (this.firstSubscriptionResolver) {\n this.firstSubscriptionResolver();\n this.firstSubscriptionResolver = null;\n }\n }\n}\n\nexport class RemoteTrackPublication extends TrackPublication {\n subscribed: boolean = false;\n\n constructor(ownedInfo: OwnedTrackPublication) {\n super(ownedInfo);\n }\n\n setSubscribed(subscribed: boolean) {\n const req = new SetSubscribedRequest({\n subscribe: subscribed,\n publicationHandle: this.ffiHandle.handle,\n });\n\n FfiClient.instance.request<SetSubscribedResponse>({\n message: { case: 'setSubscribed', value: req },\n });\n }\n}\n"],"mappings":"AAKA,SAAS,4BAA4B;AAOrC,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAGnB,MAAe,iBAAiB;AAAA,EAQrC,YAAY,WAAkC;AAC5C,SAAK,OAAO,UAAU;AACtB,SAAK,YAAY,IAAI,UAAU,UAAU,OAAQ,EAAG;AAAA,EACtD;AAAA,EAEA,IAAI,MAA0B;AA7BhC;AA8BI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,OAA2B;AAjCjC;AAkCI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,OAA8B;AArCpC;AAsCI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,SAAkC;AAzCxC;AA0CI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,cAAmC;AA7CzC;AA8CI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,QAA4B;AAjDlC;AAkDI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,SAA6B;AArDnC;AAsDI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,WAA+B;AAzDrC;AA0DI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,QAA6B;AA7DnC;AA8DI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,iBAA6C;AAjEnD;AAkEI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AACF;AAEO,MAAM,8BAA8B,iBAAiB;AAAA,EAI1D,YAAY,WAAkC;AAC5C,UAAM,SAAS;AAHjB,SAAQ,4BAAiD;AAIvD,SAAK,oBAAoB,IAAI,QAAc,CAAC,YAAY;AACtD,WAAK,4BAA4B;AAAA,IACnC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,sBAAqC;AACzC,UAAM,KAAK;AAAA,EACb;AAAA;AAAA,EAGA,2BAAiC;AAC/B,QAAI,KAAK,2BAA2B;AAClC,WAAK,0BAA0B;AAC/B,WAAK,4BAA4B;AAAA,IACnC;AAAA,EACF;AACF;AAEO,MAAM,+BAA+B,iBAAiB;AAAA,EAG3D,YAAY,WAAkC;AAC5C,UAAM,SAAS;AAHjB,sBAAsB;AAAA,EAItB;AAAA,EAEA,cAAc,YAAqB;AACjC,UAAM,MAAM,IAAI,qBAAqB;AAAA,MACnC,WAAW;AAAA,MACX,mBAAmB,KAAK,UAAU;AAAA,IACpC,CAAC;AAED,cAAU,SAAS,QAA+B;AAAA,MAChD,SAAS,EAAE,MAAM,iBAAiB,OAAO,IAAI;AAAA,IAC/C,CAAC;AAAA,EACH;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/track_publication.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { EncryptionType } from '@livekit/rtc-ffi-bindings';\nimport type { SetSubscribedResponse } from '@livekit/rtc-ffi-bindings';\nimport { SetSubscribedRequest } from '@livekit/rtc-ffi-bindings';\nimport type {\n OwnedTrackPublication,\n TrackKind,\n TrackPublicationInfo,\n TrackSource,\n} from '@livekit/rtc-ffi-bindings';\nimport { FfiHandle } from '@livekit/rtc-ffi-bindings';\nimport { FfiClient } from './ffi_client.js';\nimport type { Track } from './track.js';\n\nexport abstract class TrackPublication {\n /** @internal */\n ffiHandle: FfiHandle;\n\n /** @internal */\n info?: TrackPublicationInfo;\n track?: Track;\n\n constructor(ownedInfo: OwnedTrackPublication) {\n this.info = ownedInfo.info;\n this.ffiHandle = new FfiHandle(ownedInfo.handle!.id!);\n }\n\n get sid(): string | undefined {\n return this.info?.sid;\n }\n\n get name(): string | undefined {\n return this.info?.name;\n }\n\n get kind(): TrackKind | undefined {\n return this.info?.kind;\n }\n\n get source(): TrackSource | undefined {\n return this.info?.source;\n }\n\n get simulcasted(): boolean | undefined {\n return this.info?.simulcasted;\n }\n\n get width(): number | undefined {\n return this.info?.width;\n }\n\n get height(): number | undefined {\n return this.info?.height;\n }\n\n get mimeType(): string | undefined {\n return this.info?.mimeType;\n }\n\n get muted(): boolean | undefined {\n return this.info?.muted;\n }\n\n get encryptionType(): EncryptionType | undefined {\n return this.info?.encryptionType;\n }\n\n /**\n * Update the publication's info in place. Used by the SDK when the\n * server re-issues IDs / metadata for an existing publication (e.g.\n * after a full reconnect). Application code holding a cached\n * publication reference continues to read fresh values via the\n * unchanged object identity.\n * @internal\n */\n updateInfo(info: TrackPublicationInfo): void {\n this.info = info;\n }\n}\n\nexport class LocalTrackPublication extends TrackPublication {\n private firstSubscription: Promise<void>;\n private firstSubscriptionResolver: (() => void) | null = null;\n\n constructor(ownedInfo: OwnedTrackPublication) {\n super(ownedInfo);\n this.firstSubscription = new Promise<void>((resolve) => {\n this.firstSubscriptionResolver = resolve;\n });\n }\n\n async waitForSubscription(): Promise<void> {\n await this.firstSubscription;\n }\n\n /** @internal */\n resolveFirstSubscription(): void {\n if (this.firstSubscriptionResolver) {\n this.firstSubscriptionResolver();\n this.firstSubscriptionResolver = null;\n }\n }\n}\n\nexport class RemoteTrackPublication extends TrackPublication {\n subscribed: boolean = false;\n\n constructor(ownedInfo: OwnedTrackPublication) {\n super(ownedInfo);\n }\n\n setSubscribed(subscribed: boolean) {\n const req = new SetSubscribedRequest({\n subscribe: subscribed,\n publicationHandle: this.ffiHandle.handle,\n });\n\n FfiClient.instance.request<SetSubscribedResponse>({\n message: { case: 'setSubscribed', value: req },\n });\n }\n}\n"],"mappings":"AAKA,SAAS,4BAA4B;AAOrC,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAGnB,MAAe,iBAAiB;AAAA,EAQrC,YAAY,WAAkC;AAC5C,SAAK,OAAO,UAAU;AACtB,SAAK,YAAY,IAAI,UAAU,UAAU,OAAQ,EAAG;AAAA,EACtD;AAAA,EAEA,IAAI,MAA0B;AA7BhC;AA8BI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,OAA2B;AAjCjC;AAkCI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,OAA8B;AArCpC;AAsCI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,SAAkC;AAzCxC;AA0CI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,cAAmC;AA7CzC;AA8CI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,QAA4B;AAjDlC;AAkDI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,SAA6B;AArDnC;AAsDI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,WAA+B;AAzDrC;AA0DI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,QAA6B;AA7DnC;AA8DI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA,EAEA,IAAI,iBAA6C;AAjEnD;AAkEI,YAAO,UAAK,SAAL,mBAAW;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,WAAW,MAAkC;AAC3C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,8BAA8B,iBAAiB;AAAA,EAI1D,YAAY,WAAkC;AAC5C,UAAM,SAAS;AAHjB,SAAQ,4BAAiD;AAIvD,SAAK,oBAAoB,IAAI,QAAc,CAAC,YAAY;AACtD,WAAK,4BAA4B;AAAA,IACnC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,sBAAqC;AACzC,UAAM,KAAK;AAAA,EACb;AAAA;AAAA,EAGA,2BAAiC;AAC/B,QAAI,KAAK,2BAA2B;AAClC,WAAK,0BAA0B;AAC/B,WAAK,4BAA4B;AAAA,IACnC;AAAA,EACF;AACF;AAEO,MAAM,+BAA+B,iBAAiB;AAAA,EAG3D,YAAY,WAAkC;AAC5C,UAAM,SAAS;AAHjB,sBAAsB;AAAA,EAItB;AAAA,EAEA,cAAc,YAAqB;AACjC,UAAM,MAAM,IAAI,qBAAqB;AAAA,MACnC,WAAW;AAAA,MACX,mBAAmB,KAAK,UAAU;AAAA,IACpC,CAAC;AAED,cAAU,SAAS,QAA+B;AAAA,MAChD,SAAS,EAAE,MAAM,iBAAiB,OAAO,IAAI;AAAA,IAC/C,CAAC;AAAA,EACH;AACF;","names":[]}
|
package/dist/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
SDK_VERSION: () => SDK_VERSION
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const SDK_VERSION = "0.13.
|
|
24
|
+
const SDK_VERSION = "0.13.29";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
SDK_VERSION
|
package/dist/version.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.29\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,MAAM,cAAc;","names":[]}
|
package/dist/version.d.cts
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const SDK_VERSION = \"0.13.29\";\n"],"mappings":"AAAO,MAAM,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "LiveKit RTC Node",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "LiveKit",
|
|
6
|
-
"version": "0.13.
|
|
6
|
+
"version": "0.13.29",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"require": "dist/index.cjs",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
@@ -32,19 +32,19 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@datastructures-js/deque": "1.0.8",
|
|
34
34
|
"@livekit/mutex": "^1.0.0",
|
|
35
|
+
"@livekit/rtc-ffi-bindings": "0.12.60",
|
|
35
36
|
"@livekit/typed-emitter": "^3.0.0",
|
|
36
|
-
"@livekit/rtc-ffi-bindings": "0.12.52-patch.0",
|
|
37
37
|
"pino": "^9.0.0",
|
|
38
38
|
"pino-pretty": "^13.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@bufbuild/protobuf": "^1.10.1",
|
|
42
42
|
"@types/node": "^22.13.10",
|
|
43
|
-
"vitest": "^3.0.0",
|
|
44
43
|
"prettier": "^3.0.3",
|
|
45
44
|
"tsup": "^8.3.5",
|
|
46
45
|
"typescript": "5.8.2",
|
|
47
|
-
"
|
|
46
|
+
"vitest": "^3.0.0",
|
|
47
|
+
"livekit-server-sdk": "2.15.3"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">= 18"
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"prebuild": "node -p \"'export const SDK_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
|
|
54
54
|
"build": "pnpm prebuild && tsup --onSuccess \"tsc --declaration --emitDeclarationOnly\"",
|
|
55
55
|
"lint": "eslint -f unix \"src/**/*.ts\" --ignore-pattern \"src/proto/*\"",
|
|
56
|
-
"test": "vitest run src"
|
|
56
|
+
"test": "vitest run src",
|
|
57
|
+
"test:e2e": "node scripts/run-e2e.mjs"
|
|
57
58
|
}
|
|
58
59
|
}
|
package/src/ffi_client.ts
CHANGED
|
@@ -41,6 +41,8 @@ export class FfiClient extends (EventEmitter as new () => TypedEmitter<FfiClient
|
|
|
41
41
|
return globalThis._ffiClientInstance;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
private _nextRequestAsyncId = BigInt(0);
|
|
45
|
+
|
|
44
46
|
constructor() {
|
|
45
47
|
super();
|
|
46
48
|
this.setMaxListeners(0);
|
|
@@ -70,6 +72,10 @@ export class FfiClient extends (EventEmitter as new () => TypedEmitter<FfiClient
|
|
|
70
72
|
return livekitRetrievePtr(data);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
getNextRequestAsyncId() {
|
|
76
|
+
return ++this._nextRequestAsyncId;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
async waitFor<T>(
|
|
74
80
|
predicate: (ev: FfiEvent) => boolean,
|
|
75
81
|
options?: { signal?: AbortSignal },
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,11 @@ export type { E2EEOptions, KeyProviderOptions } from './e2ee.js';
|
|
|
15
15
|
export { dispose } from './ffi_client.js';
|
|
16
16
|
export { LocalParticipant, Participant, RemoteParticipant } from './participant.js';
|
|
17
17
|
export { EncryptionState, EncryptionType } from '@livekit/rtc-ffi-bindings';
|
|
18
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
DisconnectReason,
|
|
20
|
+
ParticipantKind,
|
|
21
|
+
ParticipantKindDetail,
|
|
22
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
19
23
|
export {
|
|
20
24
|
ConnectionQuality,
|
|
21
25
|
ConnectionState,
|
|
@@ -25,7 +29,12 @@ export {
|
|
|
25
29
|
IceTransportType,
|
|
26
30
|
TrackPublishOptions,
|
|
27
31
|
} from '@livekit/rtc-ffi-bindings';
|
|
28
|
-
export {
|
|
32
|
+
export {
|
|
33
|
+
SimulateScenarioKind,
|
|
34
|
+
StreamState,
|
|
35
|
+
TrackKind,
|
|
36
|
+
TrackSource,
|
|
37
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
29
38
|
export { VideoBufferType, VideoCodec, VideoRotation } from '@livekit/rtc-ffi-bindings';
|
|
30
39
|
export { ConnectError, Room, RoomEvent, type RoomOptions, type RtcConfiguration } from './room.js';
|
|
31
40
|
export { RpcError, type PerformRpcParams, type RpcInvocationData } from './rpc.js';
|
package/src/participant.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type OwnedParticipant,
|
|
8
8
|
type ParticipantInfo,
|
|
9
9
|
ParticipantKind,
|
|
10
|
+
type ParticipantKindDetail,
|
|
10
11
|
} from '@livekit/rtc-ffi-bindings';
|
|
11
12
|
import {
|
|
12
13
|
ChatMessage as ChatMessageModel,
|
|
@@ -128,6 +129,10 @@ export abstract class Participant {
|
|
|
128
129
|
return this.info.kind ?? ParticipantKind.STANDARD;
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
get kindDetails(): ParticipantKindDetail[] {
|
|
133
|
+
return this.info.kindDetails ?? [];
|
|
134
|
+
}
|
|
135
|
+
|
|
131
136
|
get disconnectReason(): DisconnectReason | undefined {
|
|
132
137
|
if (this.info.disconnectReason === DisconnectReason.UNKNOWN_REASON) {
|
|
133
138
|
return undefined;
|
package/src/room.ts
CHANGED
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
import { Mutex } from '@livekit/mutex';
|
|
5
5
|
import { EncryptionState, type EncryptionType } from '@livekit/rtc-ffi-bindings';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
FfiEvent,
|
|
8
|
+
GetSessionStatsCallback,
|
|
9
|
+
GetSessionStatsResponse,
|
|
10
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
7
11
|
import { DisconnectReason, type OwnedParticipant } from '@livekit/rtc-ffi-bindings';
|
|
8
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
DataStream_Trailer,
|
|
14
|
+
DisconnectCallback,
|
|
15
|
+
TrackPublicationInfo,
|
|
16
|
+
} from '@livekit/rtc-ffi-bindings';
|
|
9
17
|
import {
|
|
10
18
|
type ConnectCallback,
|
|
11
19
|
ConnectRequest,
|
|
@@ -20,7 +28,11 @@ import {
|
|
|
20
28
|
RoomOptions as FfiRoomOptions,
|
|
21
29
|
type IceServer,
|
|
22
30
|
IceTransportType,
|
|
31
|
+
type ReadyForRoomEventResponse,
|
|
23
32
|
type RoomInfo,
|
|
33
|
+
type SimulateScenarioCallback,
|
|
34
|
+
type SimulateScenarioKind,
|
|
35
|
+
type SimulateScenarioResponse,
|
|
24
36
|
} from '@livekit/rtc-ffi-bindings';
|
|
25
37
|
import { TrackKind } from '@livekit/rtc-ffi-bindings';
|
|
26
38
|
import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
|
|
@@ -184,6 +196,36 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
184
196
|
return this.sidPromise;
|
|
185
197
|
}
|
|
186
198
|
|
|
199
|
+
async getRtcStats() {
|
|
200
|
+
if (!this.isConnected || !this.ffiHandle) {
|
|
201
|
+
throw new Error('getRtcStats requires a connected room');
|
|
202
|
+
}
|
|
203
|
+
// subscribe before issuing the request
|
|
204
|
+
const requestId = FfiClient.instance.getNextRequestAsyncId();
|
|
205
|
+
const cbPromise = FfiClient.instance.waitFor<GetSessionStatsCallback>(
|
|
206
|
+
(ev: FfiEvent) =>
|
|
207
|
+
ev.message.case === 'getSessionStats' && ev.message.value.asyncId === requestId,
|
|
208
|
+
{ signal: this.disconnectController.signal },
|
|
209
|
+
);
|
|
210
|
+
FfiClient.instance.request<GetSessionStatsResponse>({
|
|
211
|
+
message: {
|
|
212
|
+
case: 'getSessionStats',
|
|
213
|
+
value: {
|
|
214
|
+
roomHandle: this.ffiHandle.handle,
|
|
215
|
+
requestAsyncId: requestId,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const cb = await cbPromise;
|
|
220
|
+
if (cb.message.case === 'error') {
|
|
221
|
+
throw new Error(cb.message.value);
|
|
222
|
+
} else if (cb.message.case === 'result') {
|
|
223
|
+
return cb.message.value;
|
|
224
|
+
} else {
|
|
225
|
+
throw new Error('could not retrieve rtc stats');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
187
229
|
get numParticipants(): number {
|
|
188
230
|
return this.info?.numParticipants ?? 0;
|
|
189
231
|
}
|
|
@@ -285,6 +327,15 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
285
327
|
}
|
|
286
328
|
}
|
|
287
329
|
this.updateConnectionState(ConnectionState.CONN_CONNECTED);
|
|
330
|
+
// Release the FFI room event gate after our listener and local state are ready.
|
|
331
|
+
FfiClient.instance.request<ReadyForRoomEventResponse>({
|
|
332
|
+
message: {
|
|
333
|
+
case: 'readyForRoomEvent',
|
|
334
|
+
value: {
|
|
335
|
+
roomHandle: this.ffiHandle.handle,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
});
|
|
288
339
|
break;
|
|
289
340
|
case 'error':
|
|
290
341
|
default:
|
|
@@ -325,6 +376,36 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
325
376
|
this.removeAllListeners();
|
|
326
377
|
}
|
|
327
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Trigger a reconnection / chaos scenario for testing. Most useful in
|
|
381
|
+
* tests to deterministically force a Resume (signal-only reconnect that
|
|
382
|
+
* preserves the PeerConnection and existing publications) or a full
|
|
383
|
+
* reconnect (the SDK rebuilds the RtcSession and re-publishes existing
|
|
384
|
+
* local tracks; `RoomEvent.Reconnected` fires).
|
|
385
|
+
*/
|
|
386
|
+
async simulateScenario(scenario: SimulateScenarioKind): Promise<void> {
|
|
387
|
+
if (!this.isConnected || !this.ffiHandle) {
|
|
388
|
+
throw new Error('simulateScenario requires a connected room');
|
|
389
|
+
}
|
|
390
|
+
const res = FfiClient.instance.request<SimulateScenarioResponse>({
|
|
391
|
+
message: {
|
|
392
|
+
case: 'simulateScenario',
|
|
393
|
+
value: {
|
|
394
|
+
roomHandle: this.ffiHandle.handle,
|
|
395
|
+
scenario,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
const cb = await FfiClient.instance.waitFor<SimulateScenarioCallback>(
|
|
400
|
+
(ev: FfiEvent) =>
|
|
401
|
+
ev.message.case === 'simulateScenario' && ev.message.value.asyncId === res.asyncId,
|
|
402
|
+
{ signal: this.disconnectController.signal },
|
|
403
|
+
);
|
|
404
|
+
if (cb.error) {
|
|
405
|
+
throw new Error(`simulateScenario failed: ${cb.error}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
328
409
|
private updateConnectionState(newState: ConnectionState) {
|
|
329
410
|
if (this._connectionState === newState) {
|
|
330
411
|
return;
|
|
@@ -481,6 +562,19 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
481
562
|
const publication = this.localParticipant.trackPublications.get(ev.value.publicationSid!);
|
|
482
563
|
this.localParticipant.trackPublications.delete(ev.value.publicationSid!);
|
|
483
564
|
this.emit(RoomEvent.LocalTrackUnpublished, publication!, this.localParticipant!);
|
|
565
|
+
} else if ((ev.case as string) == 'localTrackRepublished') {
|
|
566
|
+
const value = (ev as any).value;
|
|
567
|
+
const previousSid: string = value.previousSid!;
|
|
568
|
+
const newInfo: TrackPublicationInfo = value.info!;
|
|
569
|
+
const publication = this.localParticipant.trackPublications.get(previousSid);
|
|
570
|
+
if (publication) {
|
|
571
|
+
publication.updateInfo(newInfo);
|
|
572
|
+
this.localParticipant.trackPublications.delete(previousSid);
|
|
573
|
+
this.localParticipant.trackPublications.set(publication.sid!, publication);
|
|
574
|
+
this.emit(RoomEvent.LocalTrackRepublished, publication, previousSid, this.localParticipant);
|
|
575
|
+
} else {
|
|
576
|
+
log.warn(`RoomEvent.LocalTrackRepublished: previous publication not found: ${previousSid}`);
|
|
577
|
+
}
|
|
484
578
|
} else if (ev.case == 'localTrackSubscribed') {
|
|
485
579
|
const publication = this.localParticipant.trackPublications.get(ev.value.trackSid!);
|
|
486
580
|
if (publication) {
|
|
@@ -903,6 +997,18 @@ export type RoomCallbacks = {
|
|
|
903
997
|
publication: LocalTrackPublication,
|
|
904
998
|
participant: LocalParticipant,
|
|
905
999
|
) => void;
|
|
1000
|
+
/**
|
|
1001
|
+
* Fired when the SDK auto-republished a local track during a full
|
|
1002
|
+
* reconnect. The publication object's identity is preserved (the same
|
|
1003
|
+
* instance is updated in place with the new server-assigned SIDs);
|
|
1004
|
+
* `previousSid` is provided for callers that key external state on the
|
|
1005
|
+
* old SID and need to reconcile.
|
|
1006
|
+
*/
|
|
1007
|
+
localTrackRepublished: (
|
|
1008
|
+
publication: LocalTrackPublication,
|
|
1009
|
+
previousSid: string,
|
|
1010
|
+
participant: LocalParticipant,
|
|
1011
|
+
) => void;
|
|
906
1012
|
localTrackSubscribed: (track: LocalTrack) => void;
|
|
907
1013
|
trackPublished: (publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
|
|
908
1014
|
trackUnpublished: (publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
|
|
@@ -960,6 +1066,7 @@ export enum RoomEvent {
|
|
|
960
1066
|
ParticipantDisconnected = 'participantDisconnected',
|
|
961
1067
|
LocalTrackPublished = 'localTrackPublished',
|
|
962
1068
|
LocalTrackUnpublished = 'localTrackUnpublished',
|
|
1069
|
+
LocalTrackRepublished = 'localTrackRepublished',
|
|
963
1070
|
LocalTrackSubscribed = 'localTrackSubscribed',
|
|
964
1071
|
TrackPublished = 'trackPublished',
|
|
965
1072
|
TrackUnpublished = 'trackUnpublished',
|
package/src/tests/e2e.test.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
Room,
|
|
16
16
|
RoomEvent,
|
|
17
17
|
RpcError,
|
|
18
|
+
SimulateScenarioKind,
|
|
19
|
+
TrackKind,
|
|
18
20
|
TrackPublishOptions,
|
|
19
21
|
TrackSource,
|
|
20
22
|
dispose,
|
|
@@ -682,4 +684,159 @@ describeE2E('livekit-rtc e2e', () => {
|
|
|
682
684
|
},
|
|
683
685
|
testTimeoutMs,
|
|
684
686
|
);
|
|
687
|
+
|
|
688
|
+
// -- Reconnect scenarios --
|
|
689
|
+
//
|
|
690
|
+
// Both tests verify the user-visible behavior: after the scenario fires,
|
|
691
|
+
// the subscriber continues to receive the publisher's tone. The full
|
|
692
|
+
// reconnect test additionally asserts there is exactly one audio
|
|
693
|
+
// publication on each side (regression: duplicate-publish bug).
|
|
694
|
+
|
|
695
|
+
const runReconnectScenario = async (scenario: SimulateScenarioKind) => {
|
|
696
|
+
const { rooms } = await connectTestRooms(2);
|
|
697
|
+
const [subRoom, pubRoom] = rooms;
|
|
698
|
+
|
|
699
|
+
const pubRateHz = 48_000;
|
|
700
|
+
const source = new AudioSource(pubRateHz, 1);
|
|
701
|
+
const track = LocalAudioTrack.createAudioTrack('reconnect_tone', source);
|
|
702
|
+
const opts = new TrackPublishOptions();
|
|
703
|
+
opts.source = TrackSource.SOURCE_MICROPHONE;
|
|
704
|
+
await pubRoom!.localParticipant!.publishTrack(track, opts);
|
|
705
|
+
|
|
706
|
+
let tonePhase = 0;
|
|
707
|
+
const samplesPer10ms = Math.floor(pubRateHz / 100);
|
|
708
|
+
const amplitude = 0.8 * 32767;
|
|
709
|
+
const sineHz = 60;
|
|
710
|
+
let toneRunning = true;
|
|
711
|
+
const toneTask = (async () => {
|
|
712
|
+
while (toneRunning) {
|
|
713
|
+
const frame = AudioFrame.create(pubRateHz, 1, samplesPer10ms);
|
|
714
|
+
for (let s = 0; s < samplesPer10ms; s++) {
|
|
715
|
+
frame.data[s] = Math.round(
|
|
716
|
+
amplitude * Math.sin((2 * Math.PI * sineHz * tonePhase) / pubRateHz),
|
|
717
|
+
);
|
|
718
|
+
tonePhase++;
|
|
719
|
+
}
|
|
720
|
+
await source.captureFrame(frame);
|
|
721
|
+
}
|
|
722
|
+
})();
|
|
723
|
+
|
|
724
|
+
// Subscriber-side: re-attach an AudioStream every time TrackSubscribed
|
|
725
|
+
// fires (a full reconnect may issue TrackUnsubscribed → TrackSubscribed
|
|
726
|
+
// with a fresh remote track).
|
|
727
|
+
const sub = {
|
|
728
|
+
lastFrameAt: 0,
|
|
729
|
+
collectFromMs: Number.POSITIVE_INFINITY,
|
|
730
|
+
collected: [] as Int16Array[],
|
|
731
|
+
readers: [] as ReturnType<AudioStream['getReader']>[],
|
|
732
|
+
};
|
|
733
|
+
const attach = (remoteTrack: unknown) => {
|
|
734
|
+
const stream = new AudioStream(remoteTrack as any, {
|
|
735
|
+
sampleRate: pubRateHz,
|
|
736
|
+
numChannels: 1,
|
|
737
|
+
});
|
|
738
|
+
const reader = stream.getReader();
|
|
739
|
+
sub.readers.push(reader);
|
|
740
|
+
(async () => {
|
|
741
|
+
try {
|
|
742
|
+
while (true) {
|
|
743
|
+
const { done, value } = await reader.read();
|
|
744
|
+
if (done) break;
|
|
745
|
+
sub.lastFrameAt = Date.now();
|
|
746
|
+
if (sub.lastFrameAt >= sub.collectFromMs) {
|
|
747
|
+
sub.collected.push(channelSamples(value, 0));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
751
|
+
// reader released
|
|
752
|
+
}
|
|
753
|
+
})();
|
|
754
|
+
};
|
|
755
|
+
subRoom!.on(RoomEvent.TrackSubscribed, (t) => attach(t));
|
|
756
|
+
|
|
757
|
+
try {
|
|
758
|
+
await waitFor(() => sub.lastFrameAt > 0 && Date.now() - sub.lastFrameAt < 500, {
|
|
759
|
+
timeoutMs: 10_000,
|
|
760
|
+
debugName: 'initial audio flow',
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const simulateAt = Date.now();
|
|
764
|
+
await pubRoom!.simulateScenario(scenario);
|
|
765
|
+
|
|
766
|
+
// Wait for audio to actually flow again post-simulate: a frame
|
|
767
|
+
// received well after the simulate AND a fresh latest-frame timestamp.
|
|
768
|
+
await waitFor(
|
|
769
|
+
() => sub.lastFrameAt >= simulateAt + 500 && Date.now() - sub.lastFrameAt < 300,
|
|
770
|
+
{ timeoutMs: 30_000, debugName: 'audio re-established after simulate' },
|
|
771
|
+
);
|
|
772
|
+
// Drain post-recovery buffer/jitter, then collect a 2s window of
|
|
773
|
+
// steady-state samples for tone detection.
|
|
774
|
+
await delay(1_500);
|
|
775
|
+
sub.collected.length = 0;
|
|
776
|
+
sub.collectFromMs = Date.now();
|
|
777
|
+
await waitFor(() => sub.collected.reduce((a, s) => a + s.length, 0) >= pubRateHz * 2, {
|
|
778
|
+
timeoutMs: 15_000,
|
|
779
|
+
debugName: 'post-simulate audio sampling',
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const totalLen = sub.collected.reduce((a, s) => a + s.length, 0);
|
|
783
|
+
const concat = new Int16Array(totalLen);
|
|
784
|
+
let off = 0;
|
|
785
|
+
for (const s of sub.collected) {
|
|
786
|
+
concat.set(s, off);
|
|
787
|
+
off += s.length;
|
|
788
|
+
}
|
|
789
|
+
const detected = estimateFreqHz(concat, pubRateHz);
|
|
790
|
+
// Wider tolerance than the clean-path sine test: post-reconnect
|
|
791
|
+
// audio has brief discontinuities, and the autocorrelation is
|
|
792
|
+
// integer-lag (next neighbors to 60Hz are exactly 80Hz/40Hz), so
|
|
793
|
+
// ±20Hz lands right on the failure boundary under CI load.
|
|
794
|
+
expect(Math.abs(detected - sineHz)).toBeLessThan(25);
|
|
795
|
+
|
|
796
|
+
return { rooms, subRoom: subRoom!, pubRoom: pubRoom! };
|
|
797
|
+
} finally {
|
|
798
|
+
toneRunning = false;
|
|
799
|
+
await toneTask;
|
|
800
|
+
for (const r of sub.readers) {
|
|
801
|
+
try {
|
|
802
|
+
r.releaseLock();
|
|
803
|
+
} catch {
|
|
804
|
+
// ignore
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
await track.close();
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
itRaw(
|
|
812
|
+
'resume keeps audio flowing on the subscriber side',
|
|
813
|
+
async () => {
|
|
814
|
+
const { rooms } = await runReconnectScenario(SimulateScenarioKind.SIMULATE_SIGNAL_RECONNECT);
|
|
815
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
816
|
+
},
|
|
817
|
+
testTimeoutMs * 4,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
itRaw(
|
|
821
|
+
'full reconnect keeps audio flowing and ends with one publication on the subscriber',
|
|
822
|
+
async () => {
|
|
823
|
+
const { rooms, subRoom, pubRoom } = await runReconnectScenario(
|
|
824
|
+
SimulateScenarioKind.SIMULATE_FULL_RECONNECT,
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
// Regression: subscriber must see exactly ONE audio publication after
|
|
829
|
+
// recovery — not duplicates from the auto-republish path.
|
|
830
|
+
const subscriberAudioPubs = Array.from(
|
|
831
|
+
subRoom.remoteParticipants
|
|
832
|
+
.get(pubRoom.localParticipant!.identity)!
|
|
833
|
+
.trackPublications.values(),
|
|
834
|
+
).filter((p) => p.kind === TrackKind.KIND_AUDIO);
|
|
835
|
+
expect(subscriberAudioPubs.length).toBe(1);
|
|
836
|
+
} finally {
|
|
837
|
+
await Promise.all(rooms.map((r) => r.disconnect()));
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
testTimeoutMs * 4,
|
|
841
|
+
);
|
|
685
842
|
});
|
package/src/track_publication.ts
CHANGED
|
@@ -66,6 +66,18 @@ export abstract class TrackPublication {
|
|
|
66
66
|
get encryptionType(): EncryptionType | undefined {
|
|
67
67
|
return this.info?.encryptionType;
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Update the publication's info in place. Used by the SDK when the
|
|
72
|
+
* server re-issues IDs / metadata for an existing publication (e.g.
|
|
73
|
+
* after a full reconnect). Application code holding a cached
|
|
74
|
+
* publication reference continues to read fresh values via the
|
|
75
|
+
* unchanged object identity.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
updateInfo(info: TrackPublicationInfo): void {
|
|
79
|
+
this.info = info;
|
|
80
|
+
}
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
export class LocalTrackPublication extends TrackPublication {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.13.
|
|
1
|
+
export const SDK_VERSION = "0.13.29";
|