@livekit/rtc-node 0.0.1

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/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all",
4
+ "semi": true,
5
+ "tabWidth": 2,
6
+ "printWidth": 100,
7
+ }
package/Cargo.toml ADDED
@@ -0,0 +1,21 @@
1
+ [package]
2
+ edition = "2021"
3
+ name = "rtc-node"
4
+ version = "0.0.1"
5
+
6
+ [lib]
7
+ crate-type = ["cdylib"]
8
+
9
+ [dependencies]
10
+ napi = { version = "2.12.2", default-features = false, features = ["napi6"] }
11
+ napi-derive = "2.12.2"
12
+ livekit-ffi = { path = "./rust-sdks/livekit-ffi" }
13
+ prost = "0.12"
14
+ prost-types = "0.12"
15
+ log = "0.4.20"
16
+
17
+ [build-dependencies]
18
+ napi-build = "2.0.1"
19
+
20
+ [profile.release]
21
+ lto = true
package/build.rs ADDED
@@ -0,0 +1,5 @@
1
+ extern crate napi_build;
2
+
3
+ fn main() {
4
+ napi_build::setup();
5
+ }
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+ # Copyright 2023 LiveKit, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # This script requires protobuf-compiler and https://github.com/nipunn1313/mypy-protobuf
16
+
17
+ FFI_PROTOCOL=./rust-sdks/livekit-ffi/protocol
18
+ FFI_OUT_NODE=./src/proto
19
+
20
+ # ffi
21
+
22
+ protoc \
23
+ -I=$FFI_PROTOCOL \
24
+ --es_out $FFI_OUT_NODE \
25
+ --es_opt target=ts \
26
+ $FFI_PROTOCOL/audio_frame.proto \
27
+ $FFI_PROTOCOL/ffi.proto \
28
+ $FFI_PROTOCOL/handle.proto \
29
+ $FFI_PROTOCOL/participant.proto \
30
+ $FFI_PROTOCOL/room.proto \
31
+ $FFI_PROTOCOL/track.proto \
32
+ $FFI_PROTOCOL/video_frame.proto \
33
+ $FFI_PROTOCOL/e2ee.proto \
34
+ $FFI_PROTOCOL/stats.proto
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@livekit/rtc-node",
3
+ "version": "0.0.1",
4
+ "main": "lib/index.js",
5
+ "types": "src/index.ts",
6
+ "napi": {
7
+ "name": "rtc-node",
8
+ "triples": {
9
+ "defaults": false,
10
+ "additional": [
11
+ "aarch64-apple-darwin",
12
+ "x86_64-apple-darwin",
13
+ "aarch64-unknown-linux-gnu",
14
+ "x86_64-unknown-linux-gnu",
15
+ "x86_64-pc-windows-msvc"
16
+ ]
17
+ }
18
+ },
19
+ "license": "Apache-2.0",
20
+ "dependencies": {
21
+ "@bufbuild/protobuf": "^1.4.2"
22
+ },
23
+ "devDependencies": {
24
+ "@napi-rs/cli": "^2.16.5",
25
+ "@types/node": "^20.9.2",
26
+ "typed-emitter": "^2.1.0",
27
+ "typescript": "^5.2.2",
28
+ "prettier": "^3.0.3"
29
+ },
30
+ "engines": {
31
+ "node": ">= 10"
32
+ },
33
+ "scripts": {
34
+ "artifacts": "napi artifacts",
35
+ "build": "tsc && napi build --platform --release --dts src/native.d.ts --js src/native.js --pipe \"prettier -w\"",
36
+ "build:debug": "napi build --platform",
37
+ "prepublishOnly": "napi prepublish -t npm",
38
+ "universal": "napi universal",
39
+ "version": "napi version"
40
+ },
41
+ "optionalDependencies": {
42
+ "@livekit/rtc-node-darwin-arm64": "0.0.1",
43
+ "@livekit/rtc-node-darwin-x64": "0.0.1",
44
+ "@livekit/rtc-node-linux-arm64-gnu": "0.0.1",
45
+ "@livekit/rtc-node-linux-x64-gnu": "0.0.1",
46
+ "@livekit/rtc-node-win32-x64-msvc": "0.0.1"
47
+ }
48
+ }
@@ -0,0 +1,45 @@
1
+ import { FfiClient, FfiHandle } from './ffi_client';
2
+ import { AudioFrameBufferInfo, OwnedAudioFrameBuffer } from './proto/audio_frame_pb';
3
+
4
+ export class AudioFrame {
5
+ data: Uint16Array;
6
+ sampleRate: number;
7
+ channels: number;
8
+ samplesPerChannel: number;
9
+
10
+ constructor(data: Uint16Array, sampleRate: number, channels: number, samplesPerChannel: number) {
11
+ this.data = data;
12
+ this.sampleRate = sampleRate;
13
+ this.channels = channels;
14
+ this.samplesPerChannel = samplesPerChannel;
15
+ }
16
+
17
+ static create(sampleRate: number, channels: number, samplesPerChannel: number): AudioFrame {
18
+ const data = new Uint16Array(channels * samplesPerChannel);
19
+ return new AudioFrame(data, sampleRate, channels, samplesPerChannel);
20
+ }
21
+
22
+ /** @internal */
23
+ static fromOwnedInfo(owned: OwnedAudioFrameBuffer): AudioFrame {
24
+ let info = owned.info;
25
+ let len = info.numChannels * info.samplesPerChannel * 2; // c_int16
26
+ let data = FfiClient.instance.copyBuffer(info.dataPtr, len);
27
+ new FfiHandle(owned.handle.id).dispose();
28
+ return new AudioFrame(
29
+ new Uint16Array(data.buffer),
30
+ info.sampleRate,
31
+ info.numChannels,
32
+ info.samplesPerChannel,
33
+ );
34
+ }
35
+
36
+ /** @internal */
37
+ protoInfo(): AudioFrameBufferInfo {
38
+ return new AudioFrameBufferInfo({
39
+ dataPtr: FfiClient.instance.retrievePtr(new Uint8Array(this.data.buffer)),
40
+ sampleRate: this.sampleRate,
41
+ numChannels: this.channels,
42
+ samplesPerChannel: this.samplesPerChannel,
43
+ });
44
+ }
45
+ }
@@ -0,0 +1,64 @@
1
+ import EventEmitter from 'events';
2
+ import TypedEmitter from 'typed-emitter';
3
+ import { FfiHandle } from './native';
4
+ import { AudioFrame } from './audio_frame';
5
+ import {
6
+ AudioSourceInfo,
7
+ AudioSourceType,
8
+ CaptureAudioFrameCallback,
9
+ CaptureAudioFrameRequest,
10
+ CaptureAudioFrameResponse,
11
+ NewAudioSourceRequest,
12
+ NewAudioSourceResponse,
13
+ } from './proto/audio_frame_pb';
14
+ import { FfiClient, FfiRequest } from './ffi_client';
15
+
16
+ export class AudioSource {
17
+ /** @internal */
18
+ info: AudioSourceInfo;
19
+ /** @internal */
20
+ ffiHandle: FfiHandle;
21
+
22
+ sampleRate: number;
23
+ numChannels: number;
24
+
25
+ constructor(sampleRate: number, numChannels: number) {
26
+ this.sampleRate = sampleRate;
27
+ this.numChannels = numChannels;
28
+
29
+ let req = new NewAudioSourceRequest({
30
+ type: AudioSourceType.AUDIO_SOURCE_NATIVE,
31
+ sampleRate: sampleRate,
32
+ numChannels: numChannels,
33
+ });
34
+
35
+ let res = FfiClient.instance.request<NewAudioSourceResponse>({
36
+ message: {
37
+ case: 'newAudioSource',
38
+ value: req,
39
+ },
40
+ });
41
+
42
+ this.info = res.source.info;
43
+ this.ffiHandle = new FfiHandle(res.source.handle.id);
44
+ }
45
+
46
+ async captureFrame(frame: AudioFrame) {
47
+ let req = new CaptureAudioFrameRequest({
48
+ sourceHandle: this.ffiHandle.handle,
49
+ buffer: frame.protoInfo(),
50
+ });
51
+
52
+ let res = FfiClient.instance.request<CaptureAudioFrameResponse>({
53
+ message: { case: 'captureAudioFrame', value: req },
54
+ });
55
+
56
+ let cb = await FfiClient.instance.waitFor<CaptureAudioFrameCallback>((ev) => {
57
+ return ev.message.case == 'captureAudioFrame' && ev.message.value.asyncId == res.asyncId;
58
+ });
59
+
60
+ if (cb.error) {
61
+ throw new Error(cb.error);
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,74 @@
1
+ import { AudioFrame } from './audio_frame';
2
+ import { FfiClient, FfiClientEvent, FfiEvent, FfiHandle, FfiRequest } from './ffi_client';
3
+ import {
4
+ AudioStreamInfo,
5
+ AudioStreamType,
6
+ NewAudioStreamRequest,
7
+ NewAudioStreamResponse,
8
+ } from './proto/audio_frame_pb';
9
+ import { Track } from './track';
10
+ import EventEmitter from 'events';
11
+ import TypedEmitter from 'typed-emitter';
12
+
13
+ export type AudioStreamCallbacks = {
14
+ frameReceived: (frame: AudioFrame) => void;
15
+ };
16
+
17
+ export enum AudioStreamEvent {
18
+ FrameReceived = 'frameReceived',
19
+ }
20
+
21
+ export class AudioStream extends (EventEmitter as new () => TypedEmitter<AudioStreamCallbacks>) {
22
+ /** @internal */
23
+ info: AudioStreamInfo;
24
+ /** @internal */
25
+ ffiHandle: FfiHandle;
26
+
27
+ track: Track;
28
+
29
+ constructor(track: Track) {
30
+ super();
31
+ this.track = track;
32
+
33
+ let req = new NewAudioStreamRequest({
34
+ type: AudioStreamType.AUDIO_STREAM_NATIVE,
35
+ trackHandle: track.ffi_handle.handle,
36
+ });
37
+
38
+ let res = FfiClient.instance.request<NewAudioStreamResponse>({
39
+ message: {
40
+ case: 'newAudioStream',
41
+ value: req,
42
+ },
43
+ });
44
+
45
+ this.info = res.stream.info;
46
+ this.ffiHandle = new FfiHandle(res.stream.handle.id);
47
+
48
+ FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onEvent);
49
+ }
50
+
51
+ private onEvent(ev: FfiEvent) {
52
+ if (
53
+ ev.message.case != 'audioStreamEvent' ||
54
+ ev.message.value.streamHandle != this.ffiHandle.handle
55
+ ) {
56
+ return;
57
+ }
58
+
59
+ let streamEvent = ev.message.value.message;
60
+ switch (streamEvent.case) {
61
+ case 'frameReceived':
62
+ let frame = AudioFrame.fromOwnedInfo(streamEvent.value.frame);
63
+ this.emit(AudioStreamEvent.FrameReceived, frame);
64
+ break;
65
+ case 'eos':
66
+ FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
67
+ break;
68
+ }
69
+ }
70
+
71
+ close() {
72
+ this.ffiHandle.dispose();
73
+ }
74
+ }
package/src/e2ee.ts ADDED
@@ -0,0 +1,311 @@
1
+ import { FfiClient, FfiRequest } from './ffi_client';
2
+ import {
3
+ E2eeManagerGetFrameCryptorsResponse,
4
+ E2eeManagerSetEnabledRequest,
5
+ E2eeRequest,
6
+ E2eeResponse,
7
+ EncryptionType,
8
+ FrameCryptorSetEnabledRequest,
9
+ FrameCryptorSetKeyIndexRequest,
10
+ GetKeyRequest,
11
+ GetKeyResponse,
12
+ GetSharedKeyRequest,
13
+ GetSharedKeyResponse,
14
+ RatchetKeyRequest,
15
+ RatchetKeyResponse,
16
+ RatchetSharedKeyRequest,
17
+ RatchetSharedKeyResponse,
18
+ SetKeyRequest,
19
+ SetSharedKeyRequest,
20
+ } from './proto/e2ee_pb';
21
+
22
+ const DEFAULT_RATCHET_SALT = new TextEncoder().encode('LKFrameEncryptionKey');
23
+ const DEFAULT_RATCHET_WINDOW_SIZE = 16;
24
+ const DEFAULT_FAILURE_TOLERANCE = -1;
25
+
26
+ export interface KeyProviderOptions {
27
+ sharedKey?: Uint8Array;
28
+ ratchetSalt: Uint8Array;
29
+ ratchetWindowSize: number;
30
+ failureTolerance: number;
31
+ }
32
+
33
+ export const defaultKeyProviderOptions: KeyProviderOptions = {
34
+ ratchetSalt: DEFAULT_RATCHET_SALT,
35
+ ratchetWindowSize: DEFAULT_RATCHET_WINDOW_SIZE,
36
+ failureTolerance: DEFAULT_FAILURE_TOLERANCE,
37
+ };
38
+
39
+ export interface E2EEOptions {
40
+ keyProviderOptions: KeyProviderOptions;
41
+ encryptionType: EncryptionType;
42
+ }
43
+
44
+ export const defaultE2EEOptions: E2EEOptions = {
45
+ keyProviderOptions: defaultKeyProviderOptions,
46
+ encryptionType: EncryptionType.GCM,
47
+ };
48
+
49
+ export class KeyProvider {
50
+ private roomHandle: bigint;
51
+ options: KeyProviderOptions;
52
+
53
+ /** internal */
54
+ constructor(roomHandle: bigint, opts: KeyProviderOptions) {
55
+ this.roomHandle = roomHandle;
56
+ this.options = opts;
57
+ }
58
+
59
+ setSharedKey(sharedKey: Uint8Array, keyIndex: number) {
60
+ let req = new E2eeRequest({
61
+ roomHandle: this.roomHandle,
62
+ message: {
63
+ case: 'setSharedKey',
64
+ value: new SetSharedKeyRequest({
65
+ keyIndex: keyIndex,
66
+ sharedKey: sharedKey,
67
+ }),
68
+ },
69
+ });
70
+
71
+ FfiClient.instance.request({
72
+ message: {
73
+ case: 'e2ee',
74
+ value: req,
75
+ },
76
+ });
77
+ }
78
+
79
+ exportSharedKey(keyIndex: number): Uint8Array {
80
+ let req = new E2eeRequest({
81
+ roomHandle: this.roomHandle,
82
+ message: {
83
+ case: 'getSharedKey',
84
+ value: new GetSharedKeyRequest({
85
+ keyIndex: keyIndex,
86
+ }),
87
+ },
88
+ });
89
+
90
+ let res = FfiClient.instance.request<E2eeResponse>({
91
+ message: {
92
+ case: 'e2ee',
93
+ value: req,
94
+ },
95
+ });
96
+
97
+ return (res.message.value as GetSharedKeyResponse).key;
98
+ }
99
+
100
+ ratchetSharedKey(keyIndex: number): Uint8Array {
101
+ let req = new E2eeRequest({
102
+ roomHandle: this.roomHandle,
103
+ message: {
104
+ case: 'ratchetSharedKey',
105
+ value: new RatchetSharedKeyRequest({
106
+ keyIndex: keyIndex,
107
+ }),
108
+ },
109
+ });
110
+
111
+ let res = FfiClient.instance.request<E2eeResponse>({
112
+ message: {
113
+ case: 'e2ee',
114
+ value: req,
115
+ },
116
+ });
117
+
118
+ return (res.message.value as RatchetSharedKeyResponse).newKey;
119
+ }
120
+
121
+ setKey(participantIdentity: string, keyIndex: number) {
122
+ let req = new E2eeRequest({
123
+ roomHandle: this.roomHandle,
124
+ message: {
125
+ case: 'setKey',
126
+ value: new SetKeyRequest({
127
+ keyIndex: keyIndex,
128
+ participantIdentity: participantIdentity,
129
+ }),
130
+ },
131
+ });
132
+
133
+ FfiClient.instance.request({
134
+ message: {
135
+ case: 'e2ee',
136
+ value: req,
137
+ },
138
+ });
139
+ }
140
+
141
+ exportKey(participantIdentity: string, keyIndex: number): Uint8Array {
142
+ let req = new E2eeRequest({
143
+ roomHandle: this.roomHandle,
144
+ message: {
145
+ case: 'getKey',
146
+ value: new GetKeyRequest({
147
+ keyIndex: keyIndex,
148
+ participantIdentity: participantIdentity,
149
+ }),
150
+ },
151
+ });
152
+
153
+ let res = FfiClient.instance.request<E2eeResponse>({
154
+ message: {
155
+ case: 'e2ee',
156
+ value: req,
157
+ },
158
+ });
159
+
160
+ return (res.message.value as GetKeyResponse).key;
161
+ }
162
+
163
+ ratchetKey(participantIdentity: string, keyIndex: number): Uint8Array {
164
+ let req = new E2eeRequest({
165
+ roomHandle: this.roomHandle,
166
+ message: {
167
+ case: 'ratchetKey',
168
+ value: new RatchetKeyRequest({
169
+ keyIndex: keyIndex,
170
+ participantIdentity: participantIdentity,
171
+ }),
172
+ },
173
+ });
174
+
175
+ let res = FfiClient.instance.request<E2eeResponse>({
176
+ message: {
177
+ case: 'e2ee',
178
+ value: req,
179
+ },
180
+ });
181
+
182
+ return (res.message.value as RatchetKeyResponse).newKey;
183
+ }
184
+ }
185
+
186
+ export class FrameCryptor {
187
+ private roomHandle = 0n;
188
+ participantIdentity: string;
189
+ keyIndex: number;
190
+ enabled: boolean;
191
+
192
+ constructor(roomHandle: bigint, participantIdentity: string, keyIndex: number, enabled: boolean) {
193
+ this.roomHandle = roomHandle;
194
+ this.participantIdentity = participantIdentity;
195
+ this.keyIndex = keyIndex;
196
+ this.enabled = enabled;
197
+ }
198
+
199
+ setEnabled(enabled: boolean) {
200
+ this.enabled = enabled;
201
+ let req = new E2eeRequest({
202
+ roomHandle: this.roomHandle,
203
+ message: {
204
+ case: 'cryptorSetEnabled',
205
+ value: new FrameCryptorSetEnabledRequest({
206
+ participantIdentity: this.participantIdentity,
207
+ enabled: this.enabled,
208
+ }),
209
+ },
210
+ });
211
+
212
+ FfiClient.instance.request({
213
+ message: {
214
+ case: 'e2ee',
215
+ value: req,
216
+ },
217
+ });
218
+ }
219
+
220
+ setKeyIndex(keyIndex: number) {
221
+ this.keyIndex = keyIndex;
222
+ let req = new E2eeRequest({
223
+ roomHandle: this.roomHandle,
224
+ message: {
225
+ case: 'cryptorSetKeyIndex',
226
+ value: new FrameCryptorSetKeyIndexRequest({
227
+ participantIdentity: this.participantIdentity,
228
+ keyIndex: this.keyIndex,
229
+ }),
230
+ },
231
+ });
232
+
233
+ FfiClient.instance.request({
234
+ message: {
235
+ case: 'e2ee',
236
+ value: req,
237
+ },
238
+ });
239
+ }
240
+ }
241
+
242
+ export class E2EEManager {
243
+ private roomHandle = 0n;
244
+ private options: E2EEOptions;
245
+ private keyProvider?: KeyProvider;
246
+
247
+ enabled: boolean;
248
+
249
+ constructor(roomHandle: bigint, opts?: E2EEOptions) {
250
+ this.roomHandle = roomHandle;
251
+ this.enabled = opts !== undefined;
252
+
253
+ if (opts !== undefined) {
254
+ const options = { ...defaultE2EEOptions, ...opts };
255
+
256
+ this.options = options;
257
+ this.keyProvider = new KeyProvider(roomHandle, options.keyProviderOptions);
258
+ }
259
+ }
260
+
261
+ setEnabled(enabled: boolean) {
262
+ this.enabled = enabled;
263
+ let req = new E2eeRequest({
264
+ roomHandle: this.roomHandle,
265
+ message: {
266
+ case: 'managerSetEnabled',
267
+ value: new E2eeManagerSetEnabledRequest({
268
+ enabled: this.enabled,
269
+ }),
270
+ },
271
+ });
272
+
273
+ FfiClient.instance.request({
274
+ message: {
275
+ case: 'e2ee',
276
+ value: req,
277
+ },
278
+ });
279
+ }
280
+
281
+ frameCryptors(): FrameCryptor[] {
282
+ let req = new E2eeRequest({
283
+ roomHandle: this.roomHandle,
284
+ message: {
285
+ case: 'managerGetFrameCryptors',
286
+ value: {},
287
+ },
288
+ });
289
+
290
+ let res = FfiClient.instance.request<E2eeResponse>({
291
+ message: {
292
+ case: 'e2ee',
293
+ value: req,
294
+ },
295
+ });
296
+
297
+ let frameCryptors = (
298
+ res.message.value as E2eeManagerGetFrameCryptorsResponse
299
+ ).frameCryptors.map(
300
+ (cryptor) =>
301
+ new FrameCryptor(
302
+ this.roomHandle,
303
+ cryptor.participantIdentity,
304
+ cryptor.keyIndex,
305
+ cryptor.enabled,
306
+ ),
307
+ );
308
+
309
+ return frameCryptors;
310
+ }
311
+ }
@@ -0,0 +1,68 @@
1
+ import { FfiRequest, FfiResponse, FfiEvent } from './proto/ffi_pb';
2
+ import EventEmitter from 'events';
3
+ import TypedEmitter from 'typed-emitter';
4
+ import {
5
+ FfiHandle,
6
+ livekitFfiRequest,
7
+ livekitInitialize,
8
+ livekitCopyBuffer,
9
+ livekitRetrievePtr,
10
+ } from './native';
11
+ import { PartialMessage } from '@bufbuild/protobuf';
12
+
13
+ export { FfiHandle, FfiEvent, FfiResponse, FfiRequest };
14
+
15
+ export type FfiClientCallbacks = {
16
+ ffi_event: (event: FfiEvent) => void;
17
+ };
18
+
19
+ export enum FfiClientEvent {
20
+ FfiEvent = 'ffi_event',
21
+ }
22
+
23
+ export class FfiClient extends (EventEmitter as new () => TypedEmitter<FfiClientCallbacks>) {
24
+ static _client?: FfiClient;
25
+
26
+ /** @internal */
27
+ static get instance(): FfiClient {
28
+ if (!FfiClient._client) FfiClient._client = new FfiClient();
29
+
30
+ return FfiClient._client;
31
+ }
32
+
33
+ constructor() {
34
+ super();
35
+
36
+ livekitInitialize((event_data: Uint8Array) => {
37
+ let event = FfiEvent.fromBinary(event_data);
38
+ this.emit(FfiClientEvent.FfiEvent, event);
39
+ }, true);
40
+ }
41
+
42
+ request<T>(req: PartialMessage<FfiRequest>): T {
43
+ let request = new FfiRequest(req);
44
+ let req_data = request.toBinary();
45
+ let res_data = livekitFfiRequest(req_data);
46
+ return FfiResponse.fromBinary(res_data).message.value as T;
47
+ }
48
+
49
+ copyBuffer(ptr: bigint, len: number): Uint8Array {
50
+ return livekitCopyBuffer(ptr, len);
51
+ }
52
+
53
+ retrievePtr(data: Uint8Array): bigint {
54
+ return livekitRetrievePtr(data);
55
+ }
56
+
57
+ async waitFor<T>(predicate: (ev: FfiEvent) => boolean): Promise<T> {
58
+ return new Promise<T>((resolve, _) => {
59
+ let listener = (ev: FfiEvent) => {
60
+ if (predicate(ev)) {
61
+ this.off(FfiClientEvent.FfiEvent, listener);
62
+ resolve(ev.message.value as T);
63
+ }
64
+ };
65
+ this.on(FfiClientEvent.FfiEvent, listener);
66
+ });
67
+ }
68
+ }