livekit-server-sdk 2.17.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -15
- package/dist/EgressClient.cjs +14 -0
- package/dist/EgressClient.cjs.map +1 -1
- package/dist/EgressClient.d.cts +7 -1
- package/dist/EgressClient.d.ts +7 -1
- package/dist/EgressClient.d.ts.map +1 -1
- package/dist/EgressClient.js +15 -0
- package/dist/EgressClient.js.map +1 -1
- package/dist/TwirpRPC.cjs +5 -0
- package/dist/TwirpRPC.cjs.map +1 -1
- package/dist/TwirpRPC.d.cts +2 -1
- package/dist/TwirpRPC.d.ts +2 -1
- package/dist/TwirpRPC.d.ts.map +1 -1
- package/dist/TwirpRPC.js +4 -0
- package/dist/TwirpRPC.js.map +1 -1
- package/dist/crypto/digest.cjs +4 -16
- package/dist/crypto/digest.cjs.map +1 -1
- package/dist/crypto/digest.d.ts.map +1 -1
- package/dist/crypto/digest.js +4 -6
- package/dist/crypto/digest.js.map +1 -1
- package/dist/crypto/uuid.cjs +25 -17
- package/dist/crypto/uuid.cjs.map +1 -1
- package/dist/crypto/uuid.d.cts +2 -1
- package/dist/crypto/uuid.d.ts +2 -1
- package/dist/crypto/uuid.d.ts.map +1 -1
- package/dist/crypto/uuid.js +23 -6
- package/dist/crypto/uuid.js.map +1 -1
- package/dist/index.cjs +16 -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 +16 -0
- package/dist/index.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 +3 -3
- package/src/EgressClient.ts +16 -0
- package/src/TwirpRPC.test.ts +76 -4
- package/src/TwirpRPC.ts +8 -1
- package/src/crypto/digest.ts +5 -7
- package/src/crypto/uuid.ts +24 -6
- package/src/index.ts +8 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livekit-server-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Server-side SDK for LiveKit",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"require": "dist/index.cjs",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@bufbuild/protobuf": "^1.10.1",
|
|
34
|
-
"@livekit/protocol": "
|
|
34
|
+
"@livekit/protocol": "1.51.0",
|
|
35
35
|
"jose": "^5.1.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"vitest": "^4.0.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
|
-
"node": ">=
|
|
51
|
+
"node": ">=19"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"prebuild": "node -p \"'export const SDK_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
|
package/src/EgressClient.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
ListEgressResponse,
|
|
19
19
|
ParticipantEgressRequest,
|
|
20
20
|
RoomCompositeEgressRequest,
|
|
21
|
+
StartEgressRequest,
|
|
21
22
|
StopEgressRequest,
|
|
22
23
|
TrackCompositeEgressRequest,
|
|
23
24
|
TrackEgressRequest,
|
|
@@ -580,6 +581,21 @@ export class EgressClient extends ServiceBase {
|
|
|
580
581
|
return EgressInfo.fromJson(data, { ignoreUnknownFields: true });
|
|
581
582
|
}
|
|
582
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Starts an egress using the unified v2 request.
|
|
586
|
+
*
|
|
587
|
+
* @param request - the egress request
|
|
588
|
+
*/
|
|
589
|
+
async startEgress(request: StartEgressRequest): Promise<EgressInfo> {
|
|
590
|
+
const data = await this.rpc.request(
|
|
591
|
+
svc,
|
|
592
|
+
'StartEgress',
|
|
593
|
+
request.toJson(),
|
|
594
|
+
await this.authHeader({ roomRecord: true }),
|
|
595
|
+
);
|
|
596
|
+
return EgressInfo.fromJson(data, { ignoreUnknownFields: true });
|
|
597
|
+
}
|
|
598
|
+
|
|
583
599
|
/**
|
|
584
600
|
* @param egressId -
|
|
585
601
|
* @param layout -
|
package/src/TwirpRPC.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { ServerError, SipCallError } from './TwirpRPC.js';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { REQUEST_ID_HEADER, ServerError, SipCallError, TwirpRpc } from './TwirpRPC.js';
|
|
6
6
|
|
|
7
7
|
describe('SipCallError', () => {
|
|
8
8
|
it('renders the SIP status, Twirp code, and extra metadata', () => {
|
|
@@ -11,7 +11,7 @@ describe('SipCallError', () => {
|
|
|
11
11
|
'Too Many Requests',
|
|
12
12
|
'twirp error: sip status 486',
|
|
13
13
|
429,
|
|
14
|
-
'
|
|
14
|
+
'failed_precondition',
|
|
15
15
|
{
|
|
16
16
|
sip_status_code: '486',
|
|
17
17
|
sip_status: 'Busy Here',
|
|
@@ -30,7 +30,7 @@ describe('SipCallError', () => {
|
|
|
30
30
|
expect(printed).toContain('SipCallError');
|
|
31
31
|
expect(printed).toContain('486');
|
|
32
32
|
expect(printed).toContain('Busy Here');
|
|
33
|
-
expect(printed).toContain('
|
|
33
|
+
expect(printed).toContain('failed_precondition');
|
|
34
34
|
expect(printed).toContain('region=us-east'); // other metadata is surfaced
|
|
35
35
|
expect(printed).not.toContain('error_details'); // opaque blob is omitted
|
|
36
36
|
});
|
|
@@ -40,3 +40,75 @@ describe('SipCallError', () => {
|
|
|
40
40
|
expect(err.message).toBe('boom');
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
describe('request id', () => {
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const okResponse = () =>
|
|
50
|
+
({ ok: true, status: 200, json: async () => ({}) }) as unknown as Response;
|
|
51
|
+
|
|
52
|
+
const errorResponse = (status: number) =>
|
|
53
|
+
({
|
|
54
|
+
ok: false,
|
|
55
|
+
status,
|
|
56
|
+
statusText: 'Service Unavailable',
|
|
57
|
+
headers: { get: () => null },
|
|
58
|
+
text: async () => 'unavailable',
|
|
59
|
+
}) as unknown as Response;
|
|
60
|
+
|
|
61
|
+
// The header lets the server dedup a request that the SDK replayed.
|
|
62
|
+
it('stamps a request id on every call', async () => {
|
|
63
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(okResponse());
|
|
64
|
+
|
|
65
|
+
const rpc = new TwirpRpc('https://test.livekit.cloud', 'livekit', { failover: false });
|
|
66
|
+
await rpc.request('RoomService', 'CreateRoom', {}, {});
|
|
67
|
+
await rpc.request('RoomService', 'CreateRoom', {}, {});
|
|
68
|
+
|
|
69
|
+
const ids = fetchSpy.mock.calls.map(
|
|
70
|
+
([, init]) => (init!.headers as Record<string, string>)[REQUEST_ID_HEADER],
|
|
71
|
+
);
|
|
72
|
+
expect(ids[0]).toBeTruthy();
|
|
73
|
+
expect(ids[1]).toBeTruthy();
|
|
74
|
+
// A new logical call is a new request, so it gets its own id.
|
|
75
|
+
expect(ids[0]).not.toBe(ids[1]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// The id is generated once per logical call, so every failover attempt must
|
|
79
|
+
// carry the same value.
|
|
80
|
+
it('keeps the same request id across failover attempts', async () => {
|
|
81
|
+
let attempt = 0;
|
|
82
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
|
83
|
+
// Region discovery, not a replay of the request itself.
|
|
84
|
+
if (`${input}`.endsWith('/settings/regions')) {
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
status: 200,
|
|
88
|
+
headers: { get: () => 'max-age=0' },
|
|
89
|
+
json: async () => ({
|
|
90
|
+
regions: [
|
|
91
|
+
{ url: 'https://r1.retryid.livekit.cloud' },
|
|
92
|
+
{ url: 'https://r2.retryid.livekit.cloud' },
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
} as unknown as Response;
|
|
96
|
+
}
|
|
97
|
+
attempt += 1;
|
|
98
|
+
return attempt < 3 ? errorResponse(503) : okResponse();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const rpc = new TwirpRpc('https://primary.retryid.livekit.cloud', 'livekit', {
|
|
102
|
+
failoverBackoffMs: 0,
|
|
103
|
+
});
|
|
104
|
+
await rpc.request('RoomService', 'CreateRoom', {}, {});
|
|
105
|
+
|
|
106
|
+
const ids = fetchSpy.mock.calls
|
|
107
|
+
.filter(([input]) => !`${input}`.endsWith('/settings/regions'))
|
|
108
|
+
.map(([, init]) => (init!.headers as Record<string, string>)[REQUEST_ID_HEADER]);
|
|
109
|
+
|
|
110
|
+
expect(ids).toHaveLength(3);
|
|
111
|
+
expect(ids[0]).toBeTruthy();
|
|
112
|
+
expect(new Set(ids).size).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
package/src/TwirpRPC.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
import type { JsonValue } from '@bufbuild/protobuf';
|
|
5
|
+
import { randomUUID } from './crypto/uuid.js';
|
|
5
6
|
import {
|
|
6
7
|
FAILOVER_BACKOFF_BASE_MS,
|
|
7
8
|
failoverAttempts,
|
|
@@ -16,6 +17,11 @@ import { SDK_VERSION } from './version.js';
|
|
|
16
17
|
// setting User-Agent via fetch and silently drop it; Node honors it.
|
|
17
18
|
const USER_AGENT = `livekit-server-sdk-node/${SDK_VERSION}`;
|
|
18
19
|
|
|
20
|
+
// Carries a per-request idempotency key. The SDK's auto-retries (see failover)
|
|
21
|
+
// keep the same key across attempts, so the server can identify and deduplicate
|
|
22
|
+
// repeated requests.
|
|
23
|
+
export const REQUEST_ID_HEADER = 'X-Livekit-Request-Id';
|
|
24
|
+
|
|
19
25
|
// twirp RPC adapter for client implementation
|
|
20
26
|
|
|
21
27
|
type Options = {
|
|
@@ -175,11 +181,12 @@ export class TwirpRpc {
|
|
|
175
181
|
): Promise<any> {
|
|
176
182
|
const path = `${this.prefix}/${this.pkg}.${service}/${method}`;
|
|
177
183
|
const body = JSON.stringify(data);
|
|
178
|
-
const requestHeaders = {
|
|
184
|
+
const requestHeaders: Record<string, string> = {
|
|
179
185
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
180
186
|
'User-Agent': USER_AGENT,
|
|
181
187
|
...headers,
|
|
182
188
|
};
|
|
189
|
+
requestHeaders[REQUEST_ID_HEADER] = await randomUUID();
|
|
183
190
|
|
|
184
191
|
const origin = new URL(this.host);
|
|
185
192
|
const maxAttempts = failoverAttempts(
|
package/src/crypto/digest.ts
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Web Crypto only — no import('node:crypto') so isolate/edge bundlers can resolve this module
|
|
6
6
|
export async function digest(data: string): Promise<ArrayBuffer> {
|
|
7
|
-
if (globalThis.crypto?.subtle) {
|
|
8
|
-
|
|
9
|
-
return crypto.subtle.digest('SHA-256', encoder.encode(data));
|
|
10
|
-
} else {
|
|
11
|
-
const nodeCrypto = await import('node:crypto');
|
|
12
|
-
return nodeCrypto.createHash('sha256').update(data).digest();
|
|
7
|
+
if (!globalThis.crypto?.subtle) {
|
|
8
|
+
throw new Error('Web Crypto API is required (globalThis.crypto.subtle)');
|
|
13
9
|
}
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
return crypto.subtle.digest('SHA-256', encoder.encode(data));
|
|
14
12
|
}
|
package/src/crypto/uuid.ts
CHANGED
|
@@ -2,12 +2,30 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Web Crypto only — no import('node:crypto') so isolate/edge bundlers can resolve this module
|
|
6
6
|
export async function getRandomBytes(size: number = 16): Promise<Uint8Array> {
|
|
7
|
-
if (globalThis.crypto) {
|
|
8
|
-
|
|
9
|
-
} else {
|
|
10
|
-
const nodeCrypto = await import('node:crypto');
|
|
11
|
-
return nodeCrypto.getRandomValues(new Uint8Array(size));
|
|
7
|
+
if (!globalThis.crypto?.getRandomValues) {
|
|
8
|
+
throw new Error('Web Crypto API is required (globalThis.crypto.getRandomValues)');
|
|
12
9
|
}
|
|
10
|
+
return crypto.getRandomValues(new Uint8Array(size));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// A random RFC 4122 v4 UUID. Prefers the platform's randomUUID (Node 19+, edge
|
|
14
|
+
// runtimes, browsers in a secure context) and otherwise formats random bytes,
|
|
15
|
+
// so it works everywhere getRandomBytes does.
|
|
16
|
+
export async function randomUUID(): Promise<string> {
|
|
17
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
18
|
+
return crypto.randomUUID();
|
|
19
|
+
}
|
|
20
|
+
const bytes = await getRandomBytes(16);
|
|
21
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40; // version 4
|
|
22
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80; // variant 1
|
|
23
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
24
|
+
return [
|
|
25
|
+
hex.slice(0, 8),
|
|
26
|
+
hex.slice(8, 12),
|
|
27
|
+
hex.slice(12, 16),
|
|
28
|
+
hex.slice(16, 20),
|
|
29
|
+
hex.slice(20),
|
|
30
|
+
].join('-');
|
|
13
31
|
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export {
|
|
|
24
24
|
EncodedFileType,
|
|
25
25
|
EncodingOptions,
|
|
26
26
|
EncodingOptionsPreset,
|
|
27
|
+
FileOutput,
|
|
27
28
|
GCPUpload,
|
|
28
29
|
ImageCodec,
|
|
29
30
|
ImageFileSuffix,
|
|
@@ -38,10 +39,13 @@ export {
|
|
|
38
39
|
IngressVideoEncodingPreset,
|
|
39
40
|
IngressVideoOptions,
|
|
40
41
|
JobRestartPolicy,
|
|
42
|
+
MediaSource,
|
|
43
|
+
Output,
|
|
41
44
|
ParticipantEgressRequest,
|
|
42
45
|
ParticipantInfo,
|
|
43
46
|
ParticipantInfo_State,
|
|
44
47
|
ParticipantPermission,
|
|
48
|
+
ParticipantVideo,
|
|
45
49
|
Room,
|
|
46
50
|
RoomAgentDispatch,
|
|
47
51
|
RoomCompositeEgressRequest,
|
|
@@ -61,14 +65,18 @@ export {
|
|
|
61
65
|
SIPCallStatus,
|
|
62
66
|
SegmentedFileOutput,
|
|
63
67
|
SegmentedFileProtocol,
|
|
68
|
+
StartEgressRequest,
|
|
69
|
+
StorageConfig,
|
|
64
70
|
StreamOutput,
|
|
65
71
|
StreamProtocol,
|
|
72
|
+
TemplateSource,
|
|
66
73
|
TrackCompositeEgressRequest,
|
|
67
74
|
TrackEgressRequest,
|
|
68
75
|
TrackInfo,
|
|
69
76
|
TrackSource,
|
|
70
77
|
TrackType,
|
|
71
78
|
WebEgressRequest,
|
|
79
|
+
WebSource,
|
|
72
80
|
VideoCodec,
|
|
73
81
|
WebhookConfig,
|
|
74
82
|
} from '@livekit/protocol';
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "2.
|
|
1
|
+
export const SDK_VERSION = "2.19.0";
|