@pickleball/expo-sdk 0.1.1 → 0.2.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/CHANGELOG.md +24 -0
- package/README.md +69 -0
- package/dist/advanced-BXDrDl1h.d.ts +316 -0
- package/dist/advanced-BdAa2Erv.d.cts +316 -0
- package/dist/advanced.cjs +35 -0
- package/dist/advanced.d.cts +3 -0
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.js +35 -0
- package/dist/chunk-2JHULJZ7.js +265 -0
- package/dist/chunk-6JOMESNT.js +9 -0
- package/dist/chunk-7OX2DSKI.cjs +33 -0
- package/dist/chunk-DHDX3ZOO.js +800 -0
- package/dist/chunk-FODE2W2H.js +1953 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-LCWGNGEN.cjs +265 -0
- package/dist/chunk-TBPR2QND.cjs +1953 -0
- package/dist/chunk-TRLASNPQ.cjs +9 -0
- package/dist/chunk-YGSQ25UA.cjs +800 -0
- package/dist/device-agent/index.cjs +498 -0
- package/dist/device-agent/index.d.cts +157 -0
- package/dist/device-agent/index.d.ts +157 -0
- package/dist/device-agent/index.js +498 -0
- package/dist/engine-CCSSqPbZ.d.cts +516 -0
- package/dist/engine-CCSSqPbZ.d.ts +516 -0
- package/dist/index.cjs +339 -5569
- package/dist/index.d.cts +22 -1406
- package/dist/index.d.ts +22 -1406
- package/dist/index.js +269 -5401
- package/dist/match/index.cjs +2456 -0
- package/dist/match/index.d.cts +253 -0
- package/dist/match/index.d.ts +253 -0
- package/dist/match/index.js +2456 -0
- package/dist/match/remote/index.cjs +21 -0
- package/dist/match/remote/index.d.cts +67 -0
- package/dist/match/remote/index.d.ts +67 -0
- package/dist/match/remote/index.js +21 -0
- package/dist/match/voice/index.cjs +23 -0
- package/dist/match/voice/index.d.cts +114 -0
- package/dist/match/voice/index.d.ts +114 -0
- package/dist/match/voice/index.js +23 -0
- package/dist/plugin.cjs +53 -70
- package/dist/plugin.d.cts +13 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.js +20 -15
- package/dist/types-CrmcX408.d.cts +176 -0
- package/dist/types-CrmcX408.d.ts +176 -0
- package/dist/types-DiKWjwvN.d.cts +96 -0
- package/dist/types-DiKWjwvN.d.ts +96 -0
- package/dist/version.cjs +4 -32
- package/dist/version.js +2 -1
- package/package.json +68 -4
- package/src/advanced.ts +47 -0
- package/src/device-agent/agent.ts +443 -0
- package/src/device-agent/component.tsx +49 -0
- package/src/device-agent/convex-control-channel.ts +82 -0
- package/src/device-agent/http-transport.ts +143 -0
- package/src/device-agent/index.ts +4 -0
- package/src/engine.ts +21 -5
- package/src/errors.ts +16 -0
- package/src/http-session-provider.ts +19 -13
- package/src/index.ts +38 -14
- package/src/livekit-room-adapter.ts +39 -2
- package/src/livestream-screen.tsx +26 -4
- package/src/match/index.ts +29 -0
- package/src/match/provider.tsx +4 -2
- package/src/match/remote/hid-gesture.ts +10 -2
- package/src/match/remote/types.ts +6 -4
- package/src/match/voice/scheduler.ts +6 -1
- package/src/match/voice/service.ts +16 -4
- package/src/native-runtime.ts +54 -4
- package/src/plugin.ts +35 -14
- package/src/provider.tsx +35 -4
- package/src/rtmp-room-adapter.ts +196 -0
- package/dist/chunk-VHLUJQG5.js +0 -17
|
@@ -0,0 +1,1953 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;
|
|
2
|
+
|
|
3
|
+
var _chunk7OX2DSKIcjs = require('./chunk-7OX2DSKI.cjs');
|
|
4
|
+
|
|
5
|
+
// src/contracts.ts
|
|
6
|
+
var SDK_TELEMETRY_EVENT_NAMES = [
|
|
7
|
+
"state_changed",
|
|
8
|
+
"connection_quality",
|
|
9
|
+
"reconnect",
|
|
10
|
+
"error",
|
|
11
|
+
"heartbeat",
|
|
12
|
+
"session_ended"
|
|
13
|
+
];
|
|
14
|
+
var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
15
|
+
var CONFIG_DURATION_LIMITS = {
|
|
16
|
+
reconnectTimeoutMs: 10 * 6e4,
|
|
17
|
+
backgroundGraceMs: 10 * 6e4,
|
|
18
|
+
telemetryIntervalMs: 60 * 6e4,
|
|
19
|
+
maxSessionDurationMs: 24 * 60 * 6e4
|
|
20
|
+
};
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function isPositiveDuration(value, maximum) {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maximum;
|
|
26
|
+
}
|
|
27
|
+
function isSdkRemoteConfig(value) {
|
|
28
|
+
return isRecord(value) && typeof value.enabled === "boolean" && value.protocolVersion === 1 && typeof value.minSdkVersion === "string" && SEMVER.test(value.minSdkVersion) && typeof value.recommendedSdkVersion === "string" && SEMVER.test(value.recommendedSdkVersion) && (value.videoQuality === 720 || value.videoQuality === 1080) && isPositiveDuration(
|
|
29
|
+
value.reconnectTimeoutMs,
|
|
30
|
+
CONFIG_DURATION_LIMITS.reconnectTimeoutMs
|
|
31
|
+
) && isPositiveDuration(
|
|
32
|
+
value.backgroundGraceMs,
|
|
33
|
+
CONFIG_DURATION_LIMITS.backgroundGraceMs
|
|
34
|
+
) && isPositiveDuration(
|
|
35
|
+
value.telemetryIntervalMs,
|
|
36
|
+
CONFIG_DURATION_LIMITS.telemetryIntervalMs
|
|
37
|
+
) && isPositiveDuration(
|
|
38
|
+
value.maxSessionDurationMs,
|
|
39
|
+
CONFIG_DURATION_LIMITS.maxSessionDurationMs
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
function compareSemver(left, right) {
|
|
43
|
+
const a = SEMVER.exec(left);
|
|
44
|
+
const b = SEMVER.exec(right);
|
|
45
|
+
if (!a || !b) throw new TypeError("Invalid semantic version");
|
|
46
|
+
for (let index = 1; index < 4; index += 1) {
|
|
47
|
+
const av = BigInt(a[index]);
|
|
48
|
+
const bv = BigInt(b[index]);
|
|
49
|
+
if (av < bv) return -1;
|
|
50
|
+
if (av > bv) return 1;
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
function isSdkVersionSupported(version, minimum) {
|
|
55
|
+
try {
|
|
56
|
+
return compareSemver(version, minimum) >= 0;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/errors.ts
|
|
63
|
+
var PickleballExpoError = class extends Error {
|
|
64
|
+
constructor(message, code, options) {
|
|
65
|
+
super(message, options);
|
|
66
|
+
this.code = code;
|
|
67
|
+
this.name = "PickleballExpoError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function namedError(name, code, fallback) {
|
|
71
|
+
return class extends PickleballExpoError {
|
|
72
|
+
constructor(message = fallback, options) {
|
|
73
|
+
super(message, code, options);
|
|
74
|
+
this.name = name;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var PermissionDeniedError = class extends namedError(
|
|
79
|
+
"PermissionDeniedError",
|
|
80
|
+
"PERMISSION_DENIED",
|
|
81
|
+
"Camera and microphone permissions are required"
|
|
82
|
+
) {
|
|
83
|
+
};
|
|
84
|
+
var NetworkUnavailableError = class extends namedError(
|
|
85
|
+
"NetworkUnavailableError",
|
|
86
|
+
"NETWORK_UNAVAILABLE",
|
|
87
|
+
"A network connection is required"
|
|
88
|
+
) {
|
|
89
|
+
};
|
|
90
|
+
var TokenExpiredError = class extends namedError(
|
|
91
|
+
"TokenExpiredError",
|
|
92
|
+
"TOKEN_EXPIRED",
|
|
93
|
+
"The livestream token expired"
|
|
94
|
+
) {
|
|
95
|
+
};
|
|
96
|
+
var UnsupportedSdkError = class extends namedError(
|
|
97
|
+
"UnsupportedSdkError",
|
|
98
|
+
"SDK_UPGRADE_REQUIRED",
|
|
99
|
+
"This SDK version is no longer supported"
|
|
100
|
+
) {
|
|
101
|
+
};
|
|
102
|
+
var UnsupportedRuntimeError = class extends namedError(
|
|
103
|
+
"UnsupportedRuntimeError",
|
|
104
|
+
"UNSUPPORTED_RUNTIME",
|
|
105
|
+
"Pickleball Live requires an iOS or Android Expo Development Build; Expo Go and web are unsupported"
|
|
106
|
+
) {
|
|
107
|
+
};
|
|
108
|
+
var SessionConflictError = class extends namedError(
|
|
109
|
+
"SessionConflictError",
|
|
110
|
+
"SESSION_CONFLICT",
|
|
111
|
+
"This livestream session conflicts with an active session"
|
|
112
|
+
) {
|
|
113
|
+
};
|
|
114
|
+
var RecordingFailureError = class extends namedError(
|
|
115
|
+
"RecordingFailureError",
|
|
116
|
+
"RECORDING_FAILED",
|
|
117
|
+
"The server recording could not be created"
|
|
118
|
+
) {
|
|
119
|
+
};
|
|
120
|
+
var ConsentRequiredError = class extends namedError(
|
|
121
|
+
"ConsentRequiredError",
|
|
122
|
+
"CONSENT_REQUIRED",
|
|
123
|
+
"Recording consent is required"
|
|
124
|
+
) {
|
|
125
|
+
};
|
|
126
|
+
var SdkDisabledError = class extends namedError(
|
|
127
|
+
"SdkDisabledError",
|
|
128
|
+
"SDK_DISABLED",
|
|
129
|
+
"Livestreaming is disabled for this app"
|
|
130
|
+
) {
|
|
131
|
+
};
|
|
132
|
+
var ExpoSdkConfigurationError = class extends namedError(
|
|
133
|
+
"ExpoSdkConfigurationError",
|
|
134
|
+
"CONFIGURATION_ERROR",
|
|
135
|
+
"The Expo SDK is not configured correctly"
|
|
136
|
+
) {
|
|
137
|
+
};
|
|
138
|
+
var InvalidResponseError = class extends namedError(
|
|
139
|
+
"InvalidResponseError",
|
|
140
|
+
"INVALID_RESPONSE",
|
|
141
|
+
"The session server returned an invalid response"
|
|
142
|
+
) {
|
|
143
|
+
};
|
|
144
|
+
var ExpoSdkTimeoutError = class extends namedError(
|
|
145
|
+
"ExpoSdkTimeoutError",
|
|
146
|
+
"TIMEOUT",
|
|
147
|
+
"The request timed out"
|
|
148
|
+
) {
|
|
149
|
+
};
|
|
150
|
+
var UnknownSdkError = class extends namedError(
|
|
151
|
+
"UnknownSdkError",
|
|
152
|
+
"UNKNOWN",
|
|
153
|
+
"An unknown livestream error occurred"
|
|
154
|
+
) {
|
|
155
|
+
};
|
|
156
|
+
var known = {
|
|
157
|
+
PERMISSION_DENIED: PermissionDeniedError,
|
|
158
|
+
NETWORK_UNAVAILABLE: NetworkUnavailableError,
|
|
159
|
+
TOKEN_EXPIRED: TokenExpiredError,
|
|
160
|
+
SDK_UPGRADE_REQUIRED: UnsupportedSdkError,
|
|
161
|
+
SESSION_CONFLICT: SessionConflictError,
|
|
162
|
+
RECORDING_FAILED: RecordingFailureError,
|
|
163
|
+
CONSENT_REQUIRED: ConsentRequiredError,
|
|
164
|
+
SDK_DISABLED: SdkDisabledError,
|
|
165
|
+
UNKNOWN: UnknownSdkError
|
|
166
|
+
};
|
|
167
|
+
function errorFromCode(code, message) {
|
|
168
|
+
const Constructor = _nullishCoalesce(known[code], () => ( UnknownSdkError));
|
|
169
|
+
return new Constructor(message);
|
|
170
|
+
}
|
|
171
|
+
function normalizeError(error) {
|
|
172
|
+
if (error instanceof PickleballExpoError) return error;
|
|
173
|
+
if (error instanceof Error) {
|
|
174
|
+
const code = error.code;
|
|
175
|
+
if (code) return errorFromCode(code, error.message);
|
|
176
|
+
if (error instanceof TypeError) {
|
|
177
|
+
return new NetworkUnavailableError(void 0, { cause: error });
|
|
178
|
+
}
|
|
179
|
+
return new UnknownSdkError(error.message, { cause: error });
|
|
180
|
+
}
|
|
181
|
+
return new UnknownSdkError();
|
|
182
|
+
}
|
|
183
|
+
function isSessionGoneError(error) {
|
|
184
|
+
return typeof error === "object" && error !== null && error.httpStatus === 404;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/engine.ts
|
|
188
|
+
var PENDING_END_KEY = "@pickleball/expo-sdk/pending-end";
|
|
189
|
+
var DEFAULT_STANDBY_TIMEOUT_MS = 15 * 6e4;
|
|
190
|
+
var activeStates = /* @__PURE__ */ new Set([
|
|
191
|
+
"standby",
|
|
192
|
+
"live",
|
|
193
|
+
"reconnecting"
|
|
194
|
+
]);
|
|
195
|
+
var inMemorySessionStates = /* @__PURE__ */ new Set([
|
|
196
|
+
"connecting",
|
|
197
|
+
"standby",
|
|
198
|
+
"live",
|
|
199
|
+
"reconnecting",
|
|
200
|
+
"ending"
|
|
201
|
+
]);
|
|
202
|
+
var StaleOperationError = class extends Error {
|
|
203
|
+
};
|
|
204
|
+
var LEGAL_LIVESTREAM_TRANSITIONS = {
|
|
205
|
+
idle: ["preparing"],
|
|
206
|
+
preparing: ["preview", "ending", "error"],
|
|
207
|
+
preview: ["connecting", "preparing", "ending", "error"],
|
|
208
|
+
connecting: ["standby", "live", "ending", "error"],
|
|
209
|
+
standby: ["live", "reconnecting", "ending", "error"],
|
|
210
|
+
live: ["standby", "reconnecting", "ending", "error"],
|
|
211
|
+
reconnecting: ["standby", "live", "ending", "error"],
|
|
212
|
+
ending: ["ended", "error"],
|
|
213
|
+
ended: ["preparing"],
|
|
214
|
+
error: ["preparing", "reconnecting", "ending", "ended"]
|
|
215
|
+
};
|
|
216
|
+
var LivestreamEngine = (_class = class {
|
|
217
|
+
constructor(dependencies) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);_class.prototype.__init17.call(this);_class.prototype.__init18.call(this);
|
|
218
|
+
this.dependencies = dependencies;
|
|
219
|
+
this.now = _nullishCoalesce(dependencies.now, () => ( Date.now));
|
|
220
|
+
this.snapshot = {
|
|
221
|
+
state: "idle",
|
|
222
|
+
error: null,
|
|
223
|
+
config: null,
|
|
224
|
+
sessionId: null,
|
|
225
|
+
previewTrack: null,
|
|
226
|
+
isMicrophoneEnabled: true,
|
|
227
|
+
// Camera SAU mặc định: thiết bị quay sân luôn dùng camera sau, bắt người
|
|
228
|
+
// dùng bấm đổi mỗi buổi là thừa. Phải khớp với mặc định của MỌI adapter
|
|
229
|
+
// (LiveKitRoomAdapter.isFront, module RTMP cameraPosition) — lệch một chỗ
|
|
230
|
+
// là nhãn UI và hình thực tế ngược nhau vĩnh viễn.
|
|
231
|
+
isCameraFront: false,
|
|
232
|
+
captureOrientation: null,
|
|
233
|
+
deviceMetrics: null,
|
|
234
|
+
cameras: [],
|
|
235
|
+
selectedCameraId: null,
|
|
236
|
+
elapsedMs: 0,
|
|
237
|
+
isOnline: dependencies.network.isOnline()
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
__init() {this.listeners = /* @__PURE__ */ new Set()}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
__init2() {this.startupRecovered = false}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
__init3() {this.commandTail = Promise.resolve()}
|
|
255
|
+
__init4() {this.commandCount = 0}
|
|
256
|
+
__init5() {this.roomUnsubscribe = () => void 0}
|
|
257
|
+
__init6() {this.networkUnsubscribe = () => void 0}
|
|
258
|
+
__init7() {this.networkSubscribed = false}
|
|
259
|
+
__init8() {this.latestAppState = "active"}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
/** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
|
|
270
|
+
__init9() {this.publishing = false}
|
|
271
|
+
/** Tổng thời gian đã phát trước lần tạm dừng gần nhất. */
|
|
272
|
+
__init10() {this.pausedElapsedMs = 0}
|
|
273
|
+
|
|
274
|
+
__init11() {this.refreshRetryAttempt = 0}
|
|
275
|
+
__init12() {this.operationGeneration = 0}
|
|
276
|
+
__init13() {this.liveTimersStarted = false}
|
|
277
|
+
__init14() {this.keepAwakeActive = false}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
__init15() {this.sessionTerminations = /* @__PURE__ */ new Map()}
|
|
281
|
+
|
|
282
|
+
__init16() {this.disposed = false}
|
|
283
|
+
__init17() {this.getSnapshot = () => this.snapshot}
|
|
284
|
+
__init18() {this.subscribe = (listener) => {
|
|
285
|
+
this.listeners.add(listener);
|
|
286
|
+
return () => this.listeners.delete(listener);
|
|
287
|
+
}}
|
|
288
|
+
async initialize() {
|
|
289
|
+
if (this.disposed) return;
|
|
290
|
+
this.ensureNetworkSubscription();
|
|
291
|
+
if (this.startupRecovered) {
|
|
292
|
+
try {
|
|
293
|
+
await this.replayPendingEnd();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.pendingReplayError = error;
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!this.startupRecoveryPromise) {
|
|
300
|
+
this.startupRecoveryPromise = this.recoverStartupSession().catch((error) => {
|
|
301
|
+
this.pendingReplayError = error;
|
|
302
|
+
}).finally(() => {
|
|
303
|
+
this.startupRecovered = true;
|
|
304
|
+
this.startupRecoveryPromise = void 0;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
await this.startupRecoveryPromise;
|
|
308
|
+
}
|
|
309
|
+
prepare() {
|
|
310
|
+
if (this.preparePromise) return this.preparePromise;
|
|
311
|
+
if (this.snapshot.state === "preview") return Promise.resolve();
|
|
312
|
+
if (["connecting", "live", "reconnecting", "ending"].includes(
|
|
313
|
+
this.snapshot.state
|
|
314
|
+
) || this.snapshot.state === "error" && this.snapshot.sessionId) {
|
|
315
|
+
return Promise.reject(
|
|
316
|
+
new SessionConflictError("Cannot prepare while a session is active")
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
this.preparePromise = this.enqueueLifecycle(() => this.doPrepare()).finally(() => {
|
|
320
|
+
this.preparePromise = void 0;
|
|
321
|
+
});
|
|
322
|
+
return this.preparePromise;
|
|
323
|
+
}
|
|
324
|
+
async doPrepare() {
|
|
325
|
+
const generation = this.operationGeneration;
|
|
326
|
+
this.ensureNotDisposed();
|
|
327
|
+
this.transition("preparing");
|
|
328
|
+
try {
|
|
329
|
+
await this.ensurePendingEndResolved();
|
|
330
|
+
this.ensureOperation(generation);
|
|
331
|
+
this.dependencies.runtime.assertSupported();
|
|
332
|
+
const bootstrap = await this.dependencies.sessionProvider.bootstrap(
|
|
333
|
+
this.dependencies.bootstrapInput
|
|
334
|
+
);
|
|
335
|
+
this.ensureOperation(generation);
|
|
336
|
+
if (!isSdkRemoteConfig(bootstrap.config)) {
|
|
337
|
+
throw new InvalidResponseError(
|
|
338
|
+
"The session provider returned invalid SDK config"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (!bootstrap.config.enabled || !bootstrap.rolloutEnabled) {
|
|
342
|
+
throw new SdkDisabledError(bootstrap.message);
|
|
343
|
+
}
|
|
344
|
+
if (!isSdkVersionSupported(
|
|
345
|
+
this.dependencies.bootstrapInput.sdkVersion,
|
|
346
|
+
bootstrap.config.minSdkVersion
|
|
347
|
+
)) {
|
|
348
|
+
throw new UnsupportedSdkError();
|
|
349
|
+
}
|
|
350
|
+
this.update({ config: bootstrap.config });
|
|
351
|
+
const permitted = await this.dependencies.runtime.requestPermissions();
|
|
352
|
+
this.ensureOperation(generation);
|
|
353
|
+
if (!permitted) {
|
|
354
|
+
throw new PermissionDeniedError();
|
|
355
|
+
}
|
|
356
|
+
const room = this.ensureRoom();
|
|
357
|
+
if (this.snapshot.captureOrientation) {
|
|
358
|
+
await _optionalChain([room, 'access', _ => _.setCaptureOrientation, 'optionalCall', _2 => _2(this.snapshot.captureOrientation)]);
|
|
359
|
+
}
|
|
360
|
+
const previewTrack = await room.preparePreview(
|
|
361
|
+
bootstrap.config.videoQuality
|
|
362
|
+
);
|
|
363
|
+
if (!this.isOperationCurrent(generation)) {
|
|
364
|
+
await this.releaseRoom();
|
|
365
|
+
throw new StaleOperationError();
|
|
366
|
+
}
|
|
367
|
+
let cameras = [];
|
|
368
|
+
try {
|
|
369
|
+
cameras = await _asyncNullishCoalesce(await _optionalChain([room, 'access', _3 => _3.listCameras, 'optionalCall', _4 => _4()]), async () => ( []));
|
|
370
|
+
} catch (e2) {
|
|
371
|
+
}
|
|
372
|
+
if (!this.isOperationCurrent(generation)) {
|
|
373
|
+
await this.releaseRoom();
|
|
374
|
+
throw new StaleOperationError();
|
|
375
|
+
}
|
|
376
|
+
this.update({ previewTrack, cameras });
|
|
377
|
+
this.transition("preview");
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
this.fail(error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
start(input, options) {
|
|
386
|
+
if (this.startPromise) return this.startPromise;
|
|
387
|
+
if (this.snapshot.state === "live" || this.snapshot.state === "standby") {
|
|
388
|
+
return Promise.resolve();
|
|
389
|
+
}
|
|
390
|
+
if (this.snapshot.state !== "preview" && !this.preparePromise) {
|
|
391
|
+
return Promise.reject(
|
|
392
|
+
new SessionConflictError("Livestream preview is not ready")
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
this.startPromise = this.enqueueLifecycle(
|
|
396
|
+
() => this.doStart(input, _nullishCoalesce(_optionalChain([options, 'optionalAccess', _5 => _5.autoPublish]), () => ( true)))
|
|
397
|
+
).finally(() => {
|
|
398
|
+
this.startPromise = void 0;
|
|
399
|
+
});
|
|
400
|
+
return this.startPromise;
|
|
401
|
+
}
|
|
402
|
+
async doStart(input, autoPublish) {
|
|
403
|
+
const generation = this.operationGeneration;
|
|
404
|
+
let activeGrant;
|
|
405
|
+
this.ensureNotDisposed();
|
|
406
|
+
this.ensureState("preview");
|
|
407
|
+
if (!input.consentVersion.trim()) throw new ConsentRequiredError();
|
|
408
|
+
if (!input.externalSessionId.trim() || !input.title.trim()) {
|
|
409
|
+
const error = new ConsentRequiredError("Session ID and title are required");
|
|
410
|
+
this.fail(error);
|
|
411
|
+
}
|
|
412
|
+
if (!this.dependencies.network.isOnline()) {
|
|
413
|
+
this.fail(new NetworkUnavailableError());
|
|
414
|
+
}
|
|
415
|
+
this.transition("connecting");
|
|
416
|
+
this.ensureBackgroundGraceTimer();
|
|
417
|
+
try {
|
|
418
|
+
await this.ensurePendingEndResolved();
|
|
419
|
+
this.ensureOperation(generation);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
this.fail(error);
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const grant = await this.dependencies.sessionProvider.start({
|
|
428
|
+
...input,
|
|
429
|
+
standby: !autoPublish
|
|
430
|
+
});
|
|
431
|
+
activeGrant = grant;
|
|
432
|
+
if (!this.isOperationCurrent(generation)) {
|
|
433
|
+
await this.abandonGrant(grant);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (!isSdkRemoteConfig(grant.config)) {
|
|
437
|
+
await this.abandonGrant(grant);
|
|
438
|
+
throw new InvalidResponseError(
|
|
439
|
+
"The session provider returned invalid SDK config"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
this.ownedSessionId = grant.sessionId;
|
|
443
|
+
this.grant = grant;
|
|
444
|
+
this.liveTimersStarted = false;
|
|
445
|
+
this.keepAwakeActive = false;
|
|
446
|
+
this.publishing = autoPublish;
|
|
447
|
+
this.pausedElapsedMs = 0;
|
|
448
|
+
this.update({
|
|
449
|
+
config: grant.config,
|
|
450
|
+
sessionId: grant.sessionId,
|
|
451
|
+
elapsedMs: 0
|
|
452
|
+
});
|
|
453
|
+
await this.dependencies.storage.setItem(
|
|
454
|
+
PENDING_END_KEY,
|
|
455
|
+
JSON.stringify({ sessionId: grant.sessionId, pendingEnd: null })
|
|
456
|
+
);
|
|
457
|
+
if (!this.isOperationCurrent(generation)) {
|
|
458
|
+
await this.abandonGrant(grant);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
await this.requireRoom().connect({
|
|
462
|
+
serverUrl: grant.serverUrl,
|
|
463
|
+
participantToken: grant.participantToken,
|
|
464
|
+
microphoneEnabled: this.snapshot.isMicrophoneEnabled,
|
|
465
|
+
publish: autoPublish
|
|
466
|
+
});
|
|
467
|
+
if (!this.isOperationCurrent(generation)) {
|
|
468
|
+
await this.abandonGrant(grant);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
await this.enterSession(generation, autoPublish);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
474
|
+
if (activeGrant) await this.abandonGrant(activeGrant);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.fail(error);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
stop(reason = "user") {
|
|
481
|
+
if (this.stopPromise) return this.stopPromise;
|
|
482
|
+
if (this.snapshot.state === "ended" || this.snapshot.state === "idle") {
|
|
483
|
+
return Promise.resolve();
|
|
484
|
+
}
|
|
485
|
+
this.stopPromise = this.enqueueLifecycle(() => this.doStop(reason)).finally(() => {
|
|
486
|
+
this.stopPromise = void 0;
|
|
487
|
+
});
|
|
488
|
+
return this.stopPromise;
|
|
489
|
+
}
|
|
490
|
+
async doStop(reason) {
|
|
491
|
+
if (this.disposed) return;
|
|
492
|
+
const sessionId = this.ownedSessionId;
|
|
493
|
+
const termination = sessionId ? this.ensureSessionTermination(sessionId, reason) : void 0;
|
|
494
|
+
this.transition("ending");
|
|
495
|
+
this.clearTimers();
|
|
496
|
+
const roomCleanup = this.releaseRoom(_optionalChain([termination, 'optionalAccess', _6 => _6.markerReady]));
|
|
497
|
+
await Promise.allSettled([
|
|
498
|
+
roomCleanup,
|
|
499
|
+
this.dependencies.runtime.deactivateKeepAwake()
|
|
500
|
+
]);
|
|
501
|
+
this.keepAwakeActive = false;
|
|
502
|
+
if (sessionId) {
|
|
503
|
+
await _optionalChain([termination, 'optionalAccess', _7 => _7.completion]);
|
|
504
|
+
void this.sendTelemetry("session_ended", { reason });
|
|
505
|
+
}
|
|
506
|
+
this.grant = void 0;
|
|
507
|
+
this.ownedSessionId = void 0;
|
|
508
|
+
this.startedAt = void 0;
|
|
509
|
+
this.publishing = false;
|
|
510
|
+
this.pausedElapsedMs = 0;
|
|
511
|
+
this.update({ previewTrack: null, sessionId: null });
|
|
512
|
+
this.transition("ended");
|
|
513
|
+
}
|
|
514
|
+
retry() {
|
|
515
|
+
if (this.retryPromise) return this.retryPromise;
|
|
516
|
+
this.retryPromise = this.enqueueLifecycle(() => this.doRetry()).finally(() => {
|
|
517
|
+
this.retryPromise = void 0;
|
|
518
|
+
});
|
|
519
|
+
return this.retryPromise;
|
|
520
|
+
}
|
|
521
|
+
async doRetry() {
|
|
522
|
+
const generation = this.operationGeneration;
|
|
523
|
+
this.ensureNotDisposed();
|
|
524
|
+
if (this.snapshot.state === "error" && !this.snapshot.sessionId) {
|
|
525
|
+
this.update({ error: null });
|
|
526
|
+
return this.prepare();
|
|
527
|
+
}
|
|
528
|
+
if (this.snapshot.state === "error" && this.snapshot.sessionId) {
|
|
529
|
+
this.transition("reconnecting");
|
|
530
|
+
}
|
|
531
|
+
this.ensureState("reconnecting");
|
|
532
|
+
const sessionId = this.snapshot.sessionId;
|
|
533
|
+
if (!sessionId || !this.grant) throw new Error("Missing session grant");
|
|
534
|
+
const activeGrant = this.grant;
|
|
535
|
+
try {
|
|
536
|
+
await this.refreshCredentials(generation);
|
|
537
|
+
this.ensureOperation(generation);
|
|
538
|
+
await this.requireRoom().connect({
|
|
539
|
+
serverUrl: this.grant.serverUrl,
|
|
540
|
+
participantToken: this.grant.participantToken,
|
|
541
|
+
microphoneEnabled: this.snapshot.isMicrophoneEnabled,
|
|
542
|
+
publish: this.publishing
|
|
543
|
+
});
|
|
544
|
+
this.ensureOperation(generation);
|
|
545
|
+
this.clearReconnectTimer();
|
|
546
|
+
await this.enterSession(generation, this.publishing);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
549
|
+
await this.abandonGrant(activeGrant);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
this.fail(error);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async switchCamera() {
|
|
556
|
+
if (!this.snapshot.previewTrack) return;
|
|
557
|
+
const generation = this.operationGeneration;
|
|
558
|
+
await this.requireRoom().switchCamera();
|
|
559
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
560
|
+
this.update({
|
|
561
|
+
isCameraFront: !this.snapshot.isCameraFront,
|
|
562
|
+
selectedCameraId: null
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
/** Chọn một ống kính cụ thể từ snapshot.cameras. */
|
|
566
|
+
async selectCamera(deviceId) {
|
|
567
|
+
if (!this.snapshot.previewTrack) return;
|
|
568
|
+
if (deviceId === this.snapshot.selectedCameraId) return;
|
|
569
|
+
const camera = this.snapshot.cameras.find(
|
|
570
|
+
(item) => item.deviceId === deviceId
|
|
571
|
+
);
|
|
572
|
+
if (!camera) return;
|
|
573
|
+
const room = this.requireRoom();
|
|
574
|
+
if (!room.selectCamera) return;
|
|
575
|
+
const generation = this.operationGeneration;
|
|
576
|
+
await room.selectCamera(camera.deviceId, camera.facing);
|
|
577
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
578
|
+
this.update({
|
|
579
|
+
selectedCameraId: camera.deviceId,
|
|
580
|
+
...camera.facing === "unknown" ? {} : { isCameraFront: camera.facing === "front" }
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/** Chọn kiểu quay trước khi live; không đổi khi phiên đang hoạt động. */
|
|
584
|
+
async setCaptureOrientation(orientation) {
|
|
585
|
+
if (orientation === this.snapshot.captureOrientation) return;
|
|
586
|
+
if (this.snapshot.sessionId || activeStates.has(this.snapshot.state)) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const generation = this.operationGeneration;
|
|
590
|
+
if (this.snapshot.previewTrack) {
|
|
591
|
+
await _optionalChain([this, 'access', _8 => _8.requireRoom, 'call', _9 => _9(), 'access', _10 => _10.setCaptureOrientation, 'optionalCall', _11 => _11(orientation)]);
|
|
592
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
593
|
+
}
|
|
594
|
+
this.update({ captureOrientation: orientation });
|
|
595
|
+
}
|
|
596
|
+
async setMicrophoneEnabled(enabled) {
|
|
597
|
+
const generation = this.operationGeneration;
|
|
598
|
+
await this.requireRoom().setMicrophoneEnabled(enabled);
|
|
599
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
600
|
+
this.update({ isMicrophoneEnabled: enabled });
|
|
601
|
+
}
|
|
602
|
+
handleRoomEvent(event) {
|
|
603
|
+
if (this.disposed) return;
|
|
604
|
+
if (event.type === "command") {
|
|
605
|
+
if (event.action === "start") void this.startPublishing();
|
|
606
|
+
else if (event.action === "stop") void this.stopPublishing();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (event.type === "reconnecting" && (this.snapshot.state === "live" || this.snapshot.state === "standby")) {
|
|
610
|
+
this.transition("reconnecting");
|
|
611
|
+
void this.sendTelemetry("reconnect", { phase: "started" });
|
|
612
|
+
const timeout = _nullishCoalesce(_optionalChain([this, 'access', _12 => _12.snapshot, 'access', _13 => _13.config, 'optionalAccess', _14 => _14.reconnectTimeoutMs]), () => ( 3e4));
|
|
613
|
+
this.clearReconnectTimer();
|
|
614
|
+
this.reconnectTimer = setTimeout(() => {
|
|
615
|
+
void this.stop("network_timeout");
|
|
616
|
+
}, timeout);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (event.type === "reconnected" && this.snapshot.state === "reconnecting") {
|
|
620
|
+
this.clearReconnectTimer();
|
|
621
|
+
this.transition(this.publishing ? "live" : "standby");
|
|
622
|
+
void this.sendTelemetry("reconnect", { phase: "recovered" });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (event.type === "media-error") this.recordError(event.error);
|
|
626
|
+
if (event.type === "disconnected" && activeStates.has(this.snapshot.state)) {
|
|
627
|
+
this.handleRoomEvent({ type: "reconnecting" });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
handleAppState(state) {
|
|
631
|
+
if (this.disposed) return;
|
|
632
|
+
this.latestAppState = state;
|
|
633
|
+
if (state === "active") {
|
|
634
|
+
if (this.backgroundTimer) clearTimeout(this.backgroundTimer);
|
|
635
|
+
this.backgroundTimer = void 0;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this.ensureBackgroundGraceTimer();
|
|
639
|
+
}
|
|
640
|
+
ensureBackgroundGraceTimer() {
|
|
641
|
+
if (this.latestAppState === "active" || !["connecting", "standby", "live", "reconnecting"].includes(
|
|
642
|
+
this.snapshot.state
|
|
643
|
+
) || this.backgroundTimer) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const grace = _nullishCoalesce(_optionalChain([this, 'access', _15 => _15.snapshot, 'access', _16 => _16.config, 'optionalAccess', _17 => _17.backgroundGraceMs]), () => ( 15e3));
|
|
647
|
+
this.backgroundTimer = setTimeout(() => {
|
|
648
|
+
this.backgroundTimer = void 0;
|
|
649
|
+
if (this.latestAppState !== "active") {
|
|
650
|
+
void this.stop("background_timeout");
|
|
651
|
+
}
|
|
652
|
+
}, grace);
|
|
653
|
+
}
|
|
654
|
+
dispose() {
|
|
655
|
+
if (this.disposePromise) return this.disposePromise;
|
|
656
|
+
if (this.disposed) return Promise.resolve();
|
|
657
|
+
this.disposed = true;
|
|
658
|
+
this.operationGeneration += 1;
|
|
659
|
+
const sessionId = this.ownedSessionId;
|
|
660
|
+
let releaseEnd = () => void 0;
|
|
661
|
+
const endBarrier = new Promise((resolve) => {
|
|
662
|
+
releaseEnd = resolve;
|
|
663
|
+
});
|
|
664
|
+
const termination = sessionId ? this.ensureSessionTermination(sessionId, "error", endBarrier) : void 0;
|
|
665
|
+
const roomCleanup = this.releaseRoom(_optionalChain([termination, 'optionalAccess', _18 => _18.markerReady]));
|
|
666
|
+
this.clearTimers();
|
|
667
|
+
_optionalChain([this, 'access', _19 => _19.dependencies, 'access', _20 => _20.telemetry, 'optionalAccess', _21 => _21.dispose, 'optionalCall', _22 => _22()]);
|
|
668
|
+
this.pendingCriticalTelemetry = void 0;
|
|
669
|
+
this.networkUnsubscribe();
|
|
670
|
+
this.listeners.clear();
|
|
671
|
+
this.keepAwakeActive = false;
|
|
672
|
+
this.grant = void 0;
|
|
673
|
+
this.ownedSessionId = void 0;
|
|
674
|
+
this.disposePromise = (async () => {
|
|
675
|
+
await _optionalChain([termination, 'optionalAccess', _23 => _23.markerReady]);
|
|
676
|
+
await Promise.allSettled([
|
|
677
|
+
roomCleanup,
|
|
678
|
+
this.dependencies.runtime.deactivateKeepAwake()
|
|
679
|
+
]);
|
|
680
|
+
releaseEnd();
|
|
681
|
+
await _optionalChain([termination, 'optionalAccess', _24 => _24.completion]);
|
|
682
|
+
})();
|
|
683
|
+
return this.disposePromise;
|
|
684
|
+
}
|
|
685
|
+
async replayPendingEnd() {
|
|
686
|
+
if (this.ownsInMemorySession()) return;
|
|
687
|
+
if (this.pendingReplayPromise) return this.pendingReplayPromise;
|
|
688
|
+
this.pendingReplayPromise = this.doReplayPendingEnd().finally(() => {
|
|
689
|
+
this.pendingReplayPromise = void 0;
|
|
690
|
+
});
|
|
691
|
+
return this.pendingReplayPromise;
|
|
692
|
+
}
|
|
693
|
+
async doReplayPendingEnd() {
|
|
694
|
+
const pending = await this.readPendingEnd();
|
|
695
|
+
if (!pending || pending.pendingEnd === null) return;
|
|
696
|
+
if (!this.dependencies.network.isOnline()) return;
|
|
697
|
+
await this.dependencies.sessionProvider.end({
|
|
698
|
+
sessionId: pending.sessionId,
|
|
699
|
+
reason: _nullishCoalesce(pending.pendingEnd, () => ( "error"))
|
|
700
|
+
});
|
|
701
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
702
|
+
this.pendingReplayError = void 0;
|
|
703
|
+
}
|
|
704
|
+
async recoverStartupSession() {
|
|
705
|
+
let pending = await this.readPendingEnd();
|
|
706
|
+
if (!pending || this.ownsInMemorySession()) return;
|
|
707
|
+
if (pending.pendingEnd === null) {
|
|
708
|
+
pending = { sessionId: pending.sessionId, pendingEnd: "error" };
|
|
709
|
+
await this.dependencies.storage.setItem(
|
|
710
|
+
PENDING_END_KEY,
|
|
711
|
+
JSON.stringify(pending)
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (!this.dependencies.network.isOnline()) return;
|
|
715
|
+
try {
|
|
716
|
+
await this.dependencies.sessionProvider.end({
|
|
717
|
+
sessionId: pending.sessionId,
|
|
718
|
+
reason: _nullishCoalesce(pending.pendingEnd, () => ( "error"))
|
|
719
|
+
});
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (!isSessionGoneError(error)) throw error;
|
|
722
|
+
}
|
|
723
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
724
|
+
this.pendingReplayError = void 0;
|
|
725
|
+
}
|
|
726
|
+
async ensurePendingEndResolved() {
|
|
727
|
+
await this.initialize();
|
|
728
|
+
const pending = await this.readPendingEnd();
|
|
729
|
+
if (!pending) return;
|
|
730
|
+
if (!this.dependencies.network.isOnline()) {
|
|
731
|
+
throw new NetworkUnavailableError(
|
|
732
|
+
"Connect to the network to finish the previous livestream"
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (this.pendingReplayError instanceof TypeError) {
|
|
736
|
+
throw new NetworkUnavailableError(void 0, {
|
|
737
|
+
cause: this.pendingReplayError
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (this.snapshot.state === "connecting" && !this.ownedSessionId && pending.pendingEnd !== null) {
|
|
741
|
+
try {
|
|
742
|
+
await this.dependencies.sessionProvider.end({
|
|
743
|
+
sessionId: pending.sessionId,
|
|
744
|
+
reason: pending.pendingEnd
|
|
745
|
+
});
|
|
746
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
747
|
+
this.pendingReplayError = void 0;
|
|
748
|
+
return;
|
|
749
|
+
} catch (error) {
|
|
750
|
+
this.pendingReplayError = error;
|
|
751
|
+
if (error instanceof TypeError) {
|
|
752
|
+
throw new NetworkUnavailableError(void 0, { cause: error });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
throw new SessionConflictError(
|
|
757
|
+
pending.pendingEnd === null ? "Another livestream session is still active" : "The previous livestream is still pending end"
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
async readPendingEnd() {
|
|
761
|
+
const raw = await this.dependencies.storage.getItem(PENDING_END_KEY);
|
|
762
|
+
if (!raw) return null;
|
|
763
|
+
try {
|
|
764
|
+
const value = JSON.parse(raw);
|
|
765
|
+
if (typeof value.sessionId !== "string") throw new TypeError();
|
|
766
|
+
if (value.pendingEnd === null) {
|
|
767
|
+
return { sessionId: value.sessionId, pendingEnd: null };
|
|
768
|
+
}
|
|
769
|
+
if ([
|
|
770
|
+
"user",
|
|
771
|
+
"background_timeout",
|
|
772
|
+
"network_timeout",
|
|
773
|
+
"standby_timeout",
|
|
774
|
+
"error"
|
|
775
|
+
].includes(String(value.pendingEnd))) {
|
|
776
|
+
return {
|
|
777
|
+
sessionId: value.sessionId,
|
|
778
|
+
pendingEnd: value.pendingEnd
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
} catch (e3) {
|
|
782
|
+
}
|
|
783
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
ownsInMemorySession() {
|
|
787
|
+
return Boolean(this.ownedSessionId) || inMemorySessionStates.has(this.snapshot.state);
|
|
788
|
+
}
|
|
789
|
+
startTimers() {
|
|
790
|
+
if (this.liveTimersStarted) return;
|
|
791
|
+
this.liveTimersStarted = true;
|
|
792
|
+
const interval = Math.max(
|
|
793
|
+
1e4,
|
|
794
|
+
_nullishCoalesce(_optionalChain([this, 'access', _25 => _25.snapshot, 'access', _26 => _26.config, 'optionalAccess', _27 => _27.telemetryIntervalMs]), () => ( 6e4))
|
|
795
|
+
);
|
|
796
|
+
this.heartbeatTimer = setInterval(() => {
|
|
797
|
+
void this.sendHeartbeat();
|
|
798
|
+
}, interval);
|
|
799
|
+
_optionalChain([this, 'access', _28 => _28.heartbeatTimer, 'access', _29 => _29.unref, 'optionalCall', _30 => _30()]);
|
|
800
|
+
this.elapsedTimer = setInterval(() => {
|
|
801
|
+
if (this.startedAt !== void 0) {
|
|
802
|
+
this.update({ elapsedMs: this.now() - this.startedAt });
|
|
803
|
+
}
|
|
804
|
+
}, 1e3);
|
|
805
|
+
_optionalChain([this, 'access', _31 => _31.elapsedTimer, 'access', _32 => _32.unref, 'optionalCall', _33 => _33()]);
|
|
806
|
+
this.scheduleTokenRefresh();
|
|
807
|
+
this.scheduleMaxSessionDuration();
|
|
808
|
+
}
|
|
809
|
+
clearTimers() {
|
|
810
|
+
if (this.backgroundTimer) clearTimeout(this.backgroundTimer);
|
|
811
|
+
this.clearStandbyTimer();
|
|
812
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
813
|
+
if (this.elapsedTimer) clearInterval(this.elapsedTimer);
|
|
814
|
+
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
815
|
+
if (this.maxSessionTimer) clearTimeout(this.maxSessionTimer);
|
|
816
|
+
this.clearReconnectTimer();
|
|
817
|
+
this.backgroundTimer = void 0;
|
|
818
|
+
this.heartbeatTimer = void 0;
|
|
819
|
+
this.elapsedTimer = void 0;
|
|
820
|
+
this.tokenRefreshTimer = void 0;
|
|
821
|
+
this.maxSessionTimer = void 0;
|
|
822
|
+
this.sessionDeadline = void 0;
|
|
823
|
+
this.liveTimersStarted = false;
|
|
824
|
+
}
|
|
825
|
+
clearReconnectTimer() {
|
|
826
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
827
|
+
this.reconnectTimer = void 0;
|
|
828
|
+
}
|
|
829
|
+
/** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
|
|
830
|
+
async sendHeartbeat() {
|
|
831
|
+
let metrics = null;
|
|
832
|
+
try {
|
|
833
|
+
metrics = await _asyncNullishCoalesce(await _optionalChain([this, 'access', _34 => _34.dependencies, 'access', _35 => _35.deviceMetrics, 'optionalCall', _36 => _36()]), async () => ( null));
|
|
834
|
+
} catch (e4) {
|
|
835
|
+
metrics = null;
|
|
836
|
+
}
|
|
837
|
+
if (this.disposed) return;
|
|
838
|
+
if (metrics) this.update({ deviceMetrics: metrics });
|
|
839
|
+
return this.sendTelemetry(
|
|
840
|
+
"heartbeat",
|
|
841
|
+
metrics ? { ...metrics } : void 0
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
async sendTelemetry(name, data) {
|
|
845
|
+
if (this.disposed || !this.dependencies.telemetry || !this.grant) return;
|
|
846
|
+
const grant = this.grant;
|
|
847
|
+
const pending = {
|
|
848
|
+
credentials: grant.telemetry,
|
|
849
|
+
event: {
|
|
850
|
+
sessionId: grant.sessionId,
|
|
851
|
+
at: this.now(),
|
|
852
|
+
name,
|
|
853
|
+
...data ? { data } : {}
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
if (this.telemetryInFlight) {
|
|
857
|
+
if (name === "session_ended" && !this.pendingCriticalTelemetry) {
|
|
858
|
+
this.pendingCriticalTelemetry = pending;
|
|
859
|
+
}
|
|
860
|
+
return this.telemetryInFlight;
|
|
861
|
+
}
|
|
862
|
+
return this.startTelemetrySend(pending);
|
|
863
|
+
}
|
|
864
|
+
startTelemetrySend(pending) {
|
|
865
|
+
const operation = (async () => {
|
|
866
|
+
try {
|
|
867
|
+
await _optionalChain([this, 'access', _37 => _37.dependencies, 'access', _38 => _38.telemetry, 'optionalAccess', _39 => _39.send, 'call', _40 => _40(pending.credentials, [
|
|
868
|
+
pending.event
|
|
869
|
+
])]);
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (_optionalChain([error, 'optionalAccess', _41 => _41.status]) === 401) {
|
|
872
|
+
try {
|
|
873
|
+
await this.refreshCredentials();
|
|
874
|
+
if (this.grant) {
|
|
875
|
+
await _optionalChain([this, 'access', _42 => _42.dependencies, 'access', _43 => _43.telemetry, 'optionalAccess', _44 => _44.send, 'call', _45 => _45(this.grant.telemetry, [
|
|
876
|
+
pending.event
|
|
877
|
+
])]);
|
|
878
|
+
}
|
|
879
|
+
} catch (e5) {
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
})();
|
|
884
|
+
const tracked = operation.finally(() => {
|
|
885
|
+
if (this.telemetryInFlight !== tracked) return;
|
|
886
|
+
this.telemetryInFlight = void 0;
|
|
887
|
+
const critical = this.pendingCriticalTelemetry;
|
|
888
|
+
this.pendingCriticalTelemetry = void 0;
|
|
889
|
+
if (critical && !this.disposed) this.startTelemetrySend(critical);
|
|
890
|
+
});
|
|
891
|
+
this.telemetryInFlight = tracked;
|
|
892
|
+
return this.telemetryInFlight;
|
|
893
|
+
}
|
|
894
|
+
refreshCredentials(generation = this.operationGeneration) {
|
|
895
|
+
if (this.credentialRefreshPromise) return this.credentialRefreshPromise;
|
|
896
|
+
const sessionId = this.snapshot.sessionId;
|
|
897
|
+
if (!sessionId || !this.grant) return Promise.resolve();
|
|
898
|
+
this.credentialRefreshPromise = this.dependencies.sessionProvider.refresh(sessionId).then((refreshed) => {
|
|
899
|
+
this.ensureOperation(generation);
|
|
900
|
+
if (!isSdkRemoteConfig(refreshed.config)) {
|
|
901
|
+
throw new InvalidResponseError(
|
|
902
|
+
"The session provider returned invalid SDK config"
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (!this.grant || this.snapshot.sessionId !== sessionId || !activeStates.has(this.snapshot.state)) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
this.grant = {
|
|
909
|
+
...this.grant,
|
|
910
|
+
participantToken: _nullishCoalesce(refreshed.participantToken, () => ( this.grant.participantToken)),
|
|
911
|
+
tokenExpiresAt: _nullishCoalesce(refreshed.tokenExpiresAt, () => ( this.grant.tokenExpiresAt)),
|
|
912
|
+
telemetry: _nullishCoalesce(refreshed.telemetry, () => ( this.grant.telemetry)),
|
|
913
|
+
config: refreshed.config
|
|
914
|
+
};
|
|
915
|
+
this.update({ config: refreshed.config });
|
|
916
|
+
this.refreshRetryAttempt = 0;
|
|
917
|
+
if (this.liveTimersStarted) {
|
|
918
|
+
this.restartHeartbeat();
|
|
919
|
+
this.scheduleTokenRefresh();
|
|
920
|
+
this.scheduleMaxSessionDuration();
|
|
921
|
+
}
|
|
922
|
+
}).finally(() => {
|
|
923
|
+
this.credentialRefreshPromise = void 0;
|
|
924
|
+
});
|
|
925
|
+
return this.credentialRefreshPromise;
|
|
926
|
+
}
|
|
927
|
+
transition(state) {
|
|
928
|
+
const previous = this.snapshot.state;
|
|
929
|
+
if (previous === state) return;
|
|
930
|
+
if (!LEGAL_LIVESTREAM_TRANSITIONS[previous].includes(state)) {
|
|
931
|
+
throw new SessionConflictError(
|
|
932
|
+
`Illegal livestream transition: ${previous} -> ${state}`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
this.update({ state, error: state === "error" ? this.snapshot.error : null });
|
|
936
|
+
if (state === "standby") this.startStandbyTimer();
|
|
937
|
+
else this.clearStandbyTimer();
|
|
938
|
+
if (this.snapshot.sessionId) {
|
|
939
|
+
void this.sendTelemetry("state_changed", { from: previous, to: state });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
startStandbyTimer() {
|
|
943
|
+
this.clearStandbyTimer();
|
|
944
|
+
const timeout = _nullishCoalesce(_optionalChain([this, 'access', _46 => _46.snapshot, 'access', _47 => _47.config, 'optionalAccess', _48 => _48.standbyTimeoutMs]), () => ( DEFAULT_STANDBY_TIMEOUT_MS));
|
|
945
|
+
this.standbyTimer = setTimeout(() => {
|
|
946
|
+
this.standbyTimer = void 0;
|
|
947
|
+
if (this.snapshot.state === "standby") {
|
|
948
|
+
void this.stop("standby_timeout");
|
|
949
|
+
}
|
|
950
|
+
}, timeout);
|
|
951
|
+
_optionalChain([this, 'access', _49 => _49.standbyTimer, 'access', _50 => _50.unref, 'optionalCall', _51 => _51()]);
|
|
952
|
+
}
|
|
953
|
+
clearStandbyTimer() {
|
|
954
|
+
if (this.standbyTimer) clearTimeout(this.standbyTimer);
|
|
955
|
+
this.standbyTimer = void 0;
|
|
956
|
+
}
|
|
957
|
+
fail(error) {
|
|
958
|
+
const normalized = this.recordError(error);
|
|
959
|
+
throw normalized;
|
|
960
|
+
}
|
|
961
|
+
recordError(error) {
|
|
962
|
+
const normalized = normalizeError(error);
|
|
963
|
+
const state = this.snapshot.state;
|
|
964
|
+
if (state !== "error" && !LEGAL_LIVESTREAM_TRANSITIONS[state].includes("error")) {
|
|
965
|
+
return normalized;
|
|
966
|
+
}
|
|
967
|
+
this.update({ error: normalized });
|
|
968
|
+
this.transition("error");
|
|
969
|
+
void this.sendTelemetry("error", { code: normalized.code });
|
|
970
|
+
return normalized;
|
|
971
|
+
}
|
|
972
|
+
update(patch) {
|
|
973
|
+
if (this.disposed) return;
|
|
974
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
975
|
+
for (const listener of this.listeners) listener();
|
|
976
|
+
}
|
|
977
|
+
ensureState(expected) {
|
|
978
|
+
if (this.snapshot.state !== expected) {
|
|
979
|
+
throw new Error(`Expected livestream state ${expected}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
ensureNotDisposed() {
|
|
983
|
+
if (this.disposed) throw new Error("Livestream engine has been disposed");
|
|
984
|
+
}
|
|
985
|
+
ensureRoom() {
|
|
986
|
+
if (!this.room) {
|
|
987
|
+
this.room = this.dependencies.roomFactory();
|
|
988
|
+
this.roomUnsubscribe = this.room.subscribe(
|
|
989
|
+
(event) => this.handleRoomEvent(event)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
return this.room;
|
|
993
|
+
}
|
|
994
|
+
ensureNetworkSubscription() {
|
|
995
|
+
if (this.networkSubscribed || this.disposed) return;
|
|
996
|
+
this.networkSubscribed = true;
|
|
997
|
+
this.networkUnsubscribe = this.dependencies.network.subscribe((online) => {
|
|
998
|
+
if (this.disposed) return;
|
|
999
|
+
this.update({ isOnline: online });
|
|
1000
|
+
if (online && this.startupRecovered) {
|
|
1001
|
+
void this.replayPendingEnd().catch((error) => {
|
|
1002
|
+
this.pendingReplayError = error;
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
requireRoom() {
|
|
1008
|
+
if (!this.room) throw new Error("Livestream Room has not been prepared");
|
|
1009
|
+
return this.room;
|
|
1010
|
+
}
|
|
1011
|
+
releaseRoom(beforeDispose = Promise.resolve()) {
|
|
1012
|
+
const unsubscribe = this.roomUnsubscribe;
|
|
1013
|
+
this.roomUnsubscribe = () => void 0;
|
|
1014
|
+
const room = this.room;
|
|
1015
|
+
this.room = void 0;
|
|
1016
|
+
try {
|
|
1017
|
+
unsubscribe();
|
|
1018
|
+
} catch (e6) {
|
|
1019
|
+
}
|
|
1020
|
+
if (!room) return Promise.resolve();
|
|
1021
|
+
return beforeDispose.then(() => room.dispose());
|
|
1022
|
+
}
|
|
1023
|
+
enqueueLifecycle(command) {
|
|
1024
|
+
this.commandCount += 1;
|
|
1025
|
+
let result;
|
|
1026
|
+
if (this.commandCount === 1) {
|
|
1027
|
+
try {
|
|
1028
|
+
result = Promise.resolve(command());
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
result = Promise.reject(error);
|
|
1031
|
+
}
|
|
1032
|
+
} else {
|
|
1033
|
+
result = this.commandTail.then(command, command);
|
|
1034
|
+
}
|
|
1035
|
+
this.commandTail = result.then(
|
|
1036
|
+
() => void 0,
|
|
1037
|
+
() => void 0
|
|
1038
|
+
);
|
|
1039
|
+
return result.finally(() => {
|
|
1040
|
+
this.commandCount -= 1;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
scheduleTokenRefresh(delayOverride) {
|
|
1044
|
+
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
1045
|
+
this.tokenRefreshTimer = void 0;
|
|
1046
|
+
if (!this.grant || !this.snapshot.sessionId) return;
|
|
1047
|
+
const delay = _nullishCoalesce(delayOverride, () => ( Math.max(1e3, this.grant.tokenExpiresAt - this.now() - 6e4)));
|
|
1048
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
1049
|
+
void this.refreshCredentials().catch(() => {
|
|
1050
|
+
if (this.disposed) return;
|
|
1051
|
+
const retryDelay = Math.min(
|
|
1052
|
+
3e4,
|
|
1053
|
+
1e3 * 2 ** Math.min(this.refreshRetryAttempt, 5)
|
|
1054
|
+
);
|
|
1055
|
+
this.refreshRetryAttempt += 1;
|
|
1056
|
+
this.scheduleTokenRefresh(retryDelay);
|
|
1057
|
+
});
|
|
1058
|
+
}, delay);
|
|
1059
|
+
_optionalChain([this, 'access', _52 => _52.tokenRefreshTimer, 'access', _53 => _53.unref, 'optionalCall', _54 => _54()]);
|
|
1060
|
+
}
|
|
1061
|
+
scheduleMaxSessionDuration() {
|
|
1062
|
+
if (this.startedAt === void 0 || !this.snapshot.config) return;
|
|
1063
|
+
const proposed = this.startedAt + this.snapshot.config.maxSessionDurationMs;
|
|
1064
|
+
this.sessionDeadline = Math.min(_nullishCoalesce(this.sessionDeadline, () => ( proposed)), proposed);
|
|
1065
|
+
if (this.maxSessionTimer) clearTimeout(this.maxSessionTimer);
|
|
1066
|
+
const delay = Math.max(0, this.sessionDeadline - this.now());
|
|
1067
|
+
this.maxSessionTimer = setTimeout(() => {
|
|
1068
|
+
void this.stop("error");
|
|
1069
|
+
}, delay);
|
|
1070
|
+
_optionalChain([this, 'access', _55 => _55.maxSessionTimer, 'access', _56 => _56.unref, 'optionalCall', _57 => _57()]);
|
|
1071
|
+
}
|
|
1072
|
+
restartHeartbeat() {
|
|
1073
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1074
|
+
const interval = Math.max(
|
|
1075
|
+
1e4,
|
|
1076
|
+
_nullishCoalesce(_optionalChain([this, 'access', _58 => _58.snapshot, 'access', _59 => _59.config, 'optionalAccess', _60 => _60.telemetryIntervalMs]), () => ( 6e4))
|
|
1077
|
+
);
|
|
1078
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1079
|
+
void this.sendHeartbeat();
|
|
1080
|
+
}, interval);
|
|
1081
|
+
_optionalChain([this, 'access', _61 => _61.heartbeatTimer, 'access', _62 => _62.unref, 'optionalCall', _63 => _63()]);
|
|
1082
|
+
}
|
|
1083
|
+
async enterSession(generation, publish) {
|
|
1084
|
+
this.ensureOperation(generation);
|
|
1085
|
+
if (!this.keepAwakeActive) {
|
|
1086
|
+
await this.dependencies.runtime.activateKeepAwake();
|
|
1087
|
+
if (!this.isOperationCurrent(generation)) {
|
|
1088
|
+
await this.dependencies.runtime.deactivateKeepAwake();
|
|
1089
|
+
throw new StaleOperationError();
|
|
1090
|
+
}
|
|
1091
|
+
this.keepAwakeActive = true;
|
|
1092
|
+
}
|
|
1093
|
+
this.publishing = publish;
|
|
1094
|
+
if (publish && this.startedAt === void 0) {
|
|
1095
|
+
this.startedAt = this.now() - this.pausedElapsedMs;
|
|
1096
|
+
this.sessionDeadline = void 0;
|
|
1097
|
+
this.update({ elapsedMs: this.pausedElapsedMs });
|
|
1098
|
+
}
|
|
1099
|
+
this.startTimers();
|
|
1100
|
+
this.transition(publish ? "live" : "standby");
|
|
1101
|
+
this.ensureBackgroundGraceTimer();
|
|
1102
|
+
}
|
|
1103
|
+
/** Bắt đầu phát khi đang standby (từ nút trên app hoặc lệnh dashboard). */
|
|
1104
|
+
async startPublishing() {
|
|
1105
|
+
if (this.disposed || this.snapshot.state !== "standby") return;
|
|
1106
|
+
const generation = this.operationGeneration;
|
|
1107
|
+
try {
|
|
1108
|
+
const grant = this.grant;
|
|
1109
|
+
if (grant && this.dependencies.sessionProvider.publish) {
|
|
1110
|
+
await this.dependencies.sessionProvider.publish(grant.sessionId);
|
|
1111
|
+
if (!this.isOperationCurrent(generation) || this.snapshot.state !== "standby") {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const room = this.requireRoom();
|
|
1116
|
+
if (room.setPublishing) {
|
|
1117
|
+
await room.setPublishing(true);
|
|
1118
|
+
} else if (this.grant) {
|
|
1119
|
+
await room.connect({
|
|
1120
|
+
serverUrl: this.grant.serverUrl,
|
|
1121
|
+
participantToken: this.grant.participantToken,
|
|
1122
|
+
microphoneEnabled: this.snapshot.isMicrophoneEnabled,
|
|
1123
|
+
publish: true
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
1127
|
+
this.publishing = true;
|
|
1128
|
+
this.startedAt = this.now() - this.pausedElapsedMs;
|
|
1129
|
+
this.update({ elapsedMs: this.pausedElapsedMs });
|
|
1130
|
+
this.transition("live");
|
|
1131
|
+
void this.sendTelemetry("state_changed", { state: "live" });
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
this.recordError(error);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/** Tạm dừng phát: ngắt kết nối LiveKit hoàn toàn về standby, KHÔNG kết thúc phiên. */
|
|
1140
|
+
async stopPublishing() {
|
|
1141
|
+
if (this.disposed || this.snapshot.state !== "live") return;
|
|
1142
|
+
const generation = this.operationGeneration;
|
|
1143
|
+
const room = this.requireRoom();
|
|
1144
|
+
if (room.setPublishing) {
|
|
1145
|
+
await room.setPublishing(false);
|
|
1146
|
+
} else {
|
|
1147
|
+
await room.disconnect(false);
|
|
1148
|
+
}
|
|
1149
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
1150
|
+
this.publishing = false;
|
|
1151
|
+
this.pausedElapsedMs = this.startedAt !== void 0 ? this.now() - this.startedAt : 0;
|
|
1152
|
+
this.startedAt = void 0;
|
|
1153
|
+
this.update({ elapsedMs: this.pausedElapsedMs });
|
|
1154
|
+
this.transition("standby");
|
|
1155
|
+
void this.sendTelemetry("state_changed", { state: "standby" });
|
|
1156
|
+
const grant = this.grant;
|
|
1157
|
+
if (grant && this.dependencies.sessionProvider.unpublish) {
|
|
1158
|
+
await this.dependencies.sessionProvider.unpublish(grant.sessionId).catch(() => void 0);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
ensureSessionTermination(sessionId, reason, beforeEnd = Promise.resolve()) {
|
|
1162
|
+
const existing = this.sessionTerminations.get(sessionId);
|
|
1163
|
+
if (existing) return existing;
|
|
1164
|
+
let markerReady;
|
|
1165
|
+
try {
|
|
1166
|
+
markerReady = Promise.resolve(
|
|
1167
|
+
this.dependencies.storage.setItem(
|
|
1168
|
+
PENDING_END_KEY,
|
|
1169
|
+
JSON.stringify({ sessionId, pendingEnd: reason })
|
|
1170
|
+
)
|
|
1171
|
+
).catch(() => {
|
|
1172
|
+
});
|
|
1173
|
+
} catch (e7) {
|
|
1174
|
+
markerReady = Promise.resolve();
|
|
1175
|
+
}
|
|
1176
|
+
const completion = Promise.all([markerReady, beforeEnd]).then(async () => {
|
|
1177
|
+
try {
|
|
1178
|
+
await this.dependencies.sessionProvider.end({ sessionId, reason });
|
|
1179
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
1180
|
+
} catch (e8) {
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
const termination = { markerReady, completion };
|
|
1184
|
+
this.sessionTerminations.set(sessionId, termination);
|
|
1185
|
+
return termination;
|
|
1186
|
+
}
|
|
1187
|
+
async abandonGrant(grant) {
|
|
1188
|
+
if (this.ownedSessionId === grant.sessionId) {
|
|
1189
|
+
this.ownedSessionId = void 0;
|
|
1190
|
+
this.grant = void 0;
|
|
1191
|
+
}
|
|
1192
|
+
const termination = this.ensureSessionTermination(grant.sessionId, "error");
|
|
1193
|
+
const roomCleanup = this.releaseRoom(termination.markerReady);
|
|
1194
|
+
await termination.markerReady;
|
|
1195
|
+
if (this.disposed) {
|
|
1196
|
+
await Promise.allSettled([roomCleanup, termination.completion]);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
await Promise.allSettled([
|
|
1200
|
+
roomCleanup,
|
|
1201
|
+
this.dependencies.runtime.deactivateKeepAwake(),
|
|
1202
|
+
termination.completion
|
|
1203
|
+
]);
|
|
1204
|
+
}
|
|
1205
|
+
isOperationCurrent(generation) {
|
|
1206
|
+
return !this.disposed && generation === this.operationGeneration;
|
|
1207
|
+
}
|
|
1208
|
+
ensureOperation(generation) {
|
|
1209
|
+
if (!this.isOperationCurrent(generation)) throw new StaleOperationError();
|
|
1210
|
+
}
|
|
1211
|
+
}, _class);
|
|
1212
|
+
|
|
1213
|
+
// src/livekit-room-adapter.ts
|
|
1214
|
+
var LiveKitRoomAdapter = (_class2 = class {
|
|
1215
|
+
constructor(bindings) {;_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);
|
|
1216
|
+
this.bindings = bindings;
|
|
1217
|
+
bindings.registerGlobals();
|
|
1218
|
+
this.room = bindings.createRoom();
|
|
1219
|
+
this.nativeListeners = [
|
|
1220
|
+
[bindings.events.reconnecting, () => this.emit({ type: "reconnecting" })],
|
|
1221
|
+
[bindings.events.reconnected, () => this.emit({ type: "reconnected" })],
|
|
1222
|
+
[bindings.events.disconnected, (reason) => {
|
|
1223
|
+
this.published = false;
|
|
1224
|
+
this.emit({ type: "disconnected", reason: String(_nullishCoalesce(reason, () => ( ""))) });
|
|
1225
|
+
}],
|
|
1226
|
+
[bindings.events.mediaDevicesError, (error) => this.emit({ type: "media-error", error })]
|
|
1227
|
+
];
|
|
1228
|
+
if (bindings.events.dataReceived) {
|
|
1229
|
+
this.nativeListeners.push([
|
|
1230
|
+
bindings.events.dataReceived,
|
|
1231
|
+
(payload) => {
|
|
1232
|
+
try {
|
|
1233
|
+
const text = new TextDecoder().decode(payload);
|
|
1234
|
+
const parsed = JSON.parse(text);
|
|
1235
|
+
if (_optionalChain([parsed, 'optionalAccess', _64 => _64.type]) === "cmd" && typeof parsed.action === "string") {
|
|
1236
|
+
this.emit({ type: "command", action: parsed.action });
|
|
1237
|
+
}
|
|
1238
|
+
} catch (e9) {
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
]);
|
|
1242
|
+
}
|
|
1243
|
+
for (const [event, listener] of this.nativeListeners) {
|
|
1244
|
+
this.room.on(event, listener);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
__init19() {this.listeners = /* @__PURE__ */ new Set()}
|
|
1249
|
+
__init20() {this.tracks = []}
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
// Khớp engine (isCameraFront: false) — xem ghi chú ở engine.ts.
|
|
1254
|
+
__init21() {this.isFront = false}
|
|
1255
|
+
__init22() {this.orientationOverride = null}
|
|
1256
|
+
__init23() {this.deviceOverride = null}
|
|
1257
|
+
__init24() {this.published = false}
|
|
1258
|
+
__init25() {this.disposed = false}
|
|
1259
|
+
__init26() {this.quality = 720}
|
|
1260
|
+
|
|
1261
|
+
async preparePreview(quality) {
|
|
1262
|
+
this.ensureNotDisposed();
|
|
1263
|
+
this.quality = quality;
|
|
1264
|
+
await this.ensurePreparedTracks();
|
|
1265
|
+
this.ensureNotDisposed();
|
|
1266
|
+
return this.preview;
|
|
1267
|
+
}
|
|
1268
|
+
async connect(input) {
|
|
1269
|
+
this.ensureNotDisposed();
|
|
1270
|
+
if (!/^wss?:\/\//i.test(input.serverUrl)) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
`Mode LiveKit c\u1EA7n endpoint wss:// nh\u01B0ng phi\xEAn tr\u1EA3 v\u1EC1 "${input.serverUrl}". App tr\xEAn server \u0111ang \u0111\u1EB7t deliveryMode kh\xE1c \u2014 \u0111\u1ED5i mode trong app cho kh\u1EDBp.`
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
if (!this.preview) throw new Error("Preview must be prepared before connecting");
|
|
1276
|
+
if (this.room.state !== "disconnected") await this.disconnect(false);
|
|
1277
|
+
await this.ensurePreparedTracks();
|
|
1278
|
+
if (!this.videoTrack || !this.audioTrack || !this.preview) {
|
|
1279
|
+
throw new Error("Camera and microphone tracks are unavailable");
|
|
1280
|
+
}
|
|
1281
|
+
await this.bindings.startAudioSession();
|
|
1282
|
+
await this.room.connect(input.serverUrl, input.participantToken, {
|
|
1283
|
+
autoSubscribe: false
|
|
1284
|
+
});
|
|
1285
|
+
this.published = false;
|
|
1286
|
+
if (input.microphoneEnabled) await _optionalChain([this, 'access', _65 => _65.audioTrack, 'access', _66 => _66.unmute, 'optionalCall', _67 => _67()]);
|
|
1287
|
+
else await _optionalChain([this, 'access', _68 => _68.audioTrack, 'access', _69 => _69.mute, 'optionalCall', _70 => _70()]);
|
|
1288
|
+
if (input.publish !== false) {
|
|
1289
|
+
await this.setPublishing(true);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/** Bật/tắt publish track khi đã trong phòng — standby ↔ live. */
|
|
1293
|
+
async setPublishing(enabled) {
|
|
1294
|
+
this.ensureNotDisposed();
|
|
1295
|
+
if (enabled === this.published) return;
|
|
1296
|
+
if (!this.videoTrack || !this.audioTrack || !this.preview) {
|
|
1297
|
+
throw new Error("Camera and microphone tracks are unavailable");
|
|
1298
|
+
}
|
|
1299
|
+
if (enabled) {
|
|
1300
|
+
const publication = await this.room.localParticipant.publishTrack(
|
|
1301
|
+
this.videoTrack,
|
|
1302
|
+
this.videoPublishOptions()
|
|
1303
|
+
);
|
|
1304
|
+
await this.room.localParticipant.publishTrack(this.audioTrack);
|
|
1305
|
+
this.preview.trackRef = {
|
|
1306
|
+
participant: this.room.localParticipant,
|
|
1307
|
+
publication,
|
|
1308
|
+
source: this.bindings.cameraSource
|
|
1309
|
+
};
|
|
1310
|
+
this.published = true;
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
await _optionalChain([this, 'access', _71 => _71.room, 'access', _72 => _72.localParticipant, 'access', _73 => _73.unpublishTrack, 'optionalCall', _74 => _74(this.videoTrack, false)]);
|
|
1314
|
+
await _optionalChain([this, 'access', _75 => _75.room, 'access', _76 => _76.localParticipant, 'access', _77 => _77.unpublishTrack, 'optionalCall', _78 => _78(this.audioTrack, false)]);
|
|
1315
|
+
delete this.preview.trackRef;
|
|
1316
|
+
this.published = false;
|
|
1317
|
+
}
|
|
1318
|
+
async disconnect(stopTracks = true) {
|
|
1319
|
+
this.ensureNotDisposed();
|
|
1320
|
+
await this.room.disconnect(stopTracks);
|
|
1321
|
+
this.published = false;
|
|
1322
|
+
if (this.preview) delete this.preview.trackRef;
|
|
1323
|
+
await this.bindings.stopAudioSession();
|
|
1324
|
+
if (stopTracks) this.releaseTracks(true);
|
|
1325
|
+
}
|
|
1326
|
+
captureResolution() {
|
|
1327
|
+
const [long, short] = this.quality === 1080 ? [1920, 1080] : [1280, 720];
|
|
1328
|
+
return { width: long, height: short, frameRate: 30 };
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Tùy chọn publish video. Mặc định của livekit-client (VP8, không encoding,
|
|
1332
|
+
* không degradation preference) khiến uplink yếu bóp bitrate xuống ~600kbps
|
|
1333
|
+
* mà VẪN giữ 1080 @ ~10fps → khối bể nát, tệ hơn 720p sạch.
|
|
1334
|
+
*
|
|
1335
|
+
* - videoCodec H.264: HW-encode trên Android, hiệu quả hơn VP8 ở bitrate thấp,
|
|
1336
|
+
* trùng preset egress (H264_*) nên bỏ được 1 lần transcode phía egress.
|
|
1337
|
+
* - simulcast tắt: viewer chỉ xem HLS do egress xuất; egress subscribe đúng
|
|
1338
|
+
* 1 lớp đã publish, nên simulcast chỉ thêm tải/bất ổn encoder (H.264 simulcast
|
|
1339
|
+
* kém tin cậy trên Android) mà không lợi gì cho người xem ở kiến trúc này.
|
|
1340
|
+
* - maintain-framerate: pickleball chuyển động nhanh; khi uplink không gánh nổi
|
|
1341
|
+
* mức đích thì HẠ ĐỘ PHÂN GIẢI (giữ nét-trên-điểm-ảnh) thay vì đóng băng fps.
|
|
1342
|
+
* - maxBitrate: trần theo mức chất lượng; BWE vẫn tự hạ dưới trần khi mạng yếu.
|
|
1343
|
+
*/
|
|
1344
|
+
videoPublishOptions() {
|
|
1345
|
+
return {
|
|
1346
|
+
source: this.bindings.cameraSource,
|
|
1347
|
+
simulcast: false,
|
|
1348
|
+
videoCodec: "h264",
|
|
1349
|
+
degradationPreference: "maintain-framerate",
|
|
1350
|
+
videoEncoding: {
|
|
1351
|
+
maxBitrate: this.quality === 1080 ? 45e5 : 25e5,
|
|
1352
|
+
maxFramerate: 30
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
async setCaptureOrientation(orientation) {
|
|
1357
|
+
this.ensureNotDisposed();
|
|
1358
|
+
this.orientationOverride = orientation;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* livekit-client restart() chỉ giữ deviceId/facingMode khi gọi getUserMedia
|
|
1362
|
+
* (vứt width/height/frameRate) nên track mới rơi về format mặc định
|
|
1363
|
+
* 640x480. Native applyConstraints thì ngược lại: chỉ xử lý
|
|
1364
|
+
* width/height/frameRate. Sau mỗi restartTrack phải nắn lại format trên
|
|
1365
|
+
* track mới bằng applyConstraints.
|
|
1366
|
+
*/
|
|
1367
|
+
async reapplyCaptureFormat() {
|
|
1368
|
+
const nativeTrack = _optionalChain([this, 'access', _79 => _79.videoTrack, 'optionalAccess', _80 => _80.mediaStreamTrack]);
|
|
1369
|
+
if (_optionalChain([nativeTrack, 'optionalAccess', _81 => _81.applyConstraints])) {
|
|
1370
|
+
await nativeTrack.applyConstraints(this.captureResolution());
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
async switchCamera() {
|
|
1374
|
+
this.ensureNotDisposed();
|
|
1375
|
+
const facingMode = this.isFront ? "environment" : "user";
|
|
1376
|
+
const nativeTrack = _optionalChain([this, 'access', _82 => _82.videoTrack, 'optionalAccess', _83 => _83.mediaStreamTrack]);
|
|
1377
|
+
if (typeof _optionalChain([this, 'access', _84 => _84.videoTrack, 'optionalAccess', _85 => _85.restartTrack]) === "function") {
|
|
1378
|
+
await this.videoTrack.restartTrack({ facingMode });
|
|
1379
|
+
await this.reapplyCaptureFormat();
|
|
1380
|
+
} else if (typeof _optionalChain([nativeTrack, 'optionalAccess', _86 => _86._switchCamera]) === "function") {
|
|
1381
|
+
nativeTrack._switchCamera();
|
|
1382
|
+
} else if (_optionalChain([nativeTrack, 'optionalAccess', _87 => _87.applyConstraints])) {
|
|
1383
|
+
await nativeTrack.applyConstraints({ facingMode });
|
|
1384
|
+
} else {
|
|
1385
|
+
throw new Error("Camera switching is unavailable");
|
|
1386
|
+
}
|
|
1387
|
+
this.deviceOverride = null;
|
|
1388
|
+
this.isFront = !this.isFront;
|
|
1389
|
+
}
|
|
1390
|
+
async listCameras() {
|
|
1391
|
+
this.ensureNotDisposed();
|
|
1392
|
+
return await _asyncNullishCoalesce(await _optionalChain([this, 'access', _88 => _88.bindings, 'access', _89 => _89.listCameras, 'optionalCall', _90 => _90()]), async () => ( []));
|
|
1393
|
+
}
|
|
1394
|
+
async selectCamera(deviceId, facing) {
|
|
1395
|
+
this.ensureNotDisposed();
|
|
1396
|
+
if (typeof _optionalChain([this, 'access', _91 => _91.videoTrack, 'optionalAccess', _92 => _92.restartTrack]) !== "function") {
|
|
1397
|
+
throw new Error("Camera selection is unavailable");
|
|
1398
|
+
}
|
|
1399
|
+
await this.videoTrack.restartTrack({ deviceId });
|
|
1400
|
+
await this.reapplyCaptureFormat();
|
|
1401
|
+
this.deviceOverride = deviceId;
|
|
1402
|
+
if (facing !== "unknown") this.isFront = facing === "front";
|
|
1403
|
+
}
|
|
1404
|
+
async setMicrophoneEnabled(enabled) {
|
|
1405
|
+
this.ensureNotDisposed();
|
|
1406
|
+
if (!this.audioTrack) return;
|
|
1407
|
+
if (enabled) await _optionalChain([this, 'access', _93 => _93.audioTrack, 'access', _94 => _94.unmute, 'optionalCall', _95 => _95()]);
|
|
1408
|
+
else await _optionalChain([this, 'access', _96 => _96.audioTrack, 'access', _97 => _97.mute, 'optionalCall', _98 => _98()]);
|
|
1409
|
+
}
|
|
1410
|
+
subscribe(listener) {
|
|
1411
|
+
this.ensureNotDisposed();
|
|
1412
|
+
this.listeners.add(listener);
|
|
1413
|
+
return () => this.listeners.delete(listener);
|
|
1414
|
+
}
|
|
1415
|
+
async dispose() {
|
|
1416
|
+
if (this.disposed) return;
|
|
1417
|
+
this.disposed = true;
|
|
1418
|
+
for (const [event, listener] of this.nativeListeners) {
|
|
1419
|
+
this.room.off(event, listener);
|
|
1420
|
+
}
|
|
1421
|
+
await this.room.disconnect(true);
|
|
1422
|
+
await this.bindings.stopAudioSession();
|
|
1423
|
+
this.releaseTracks(true);
|
|
1424
|
+
this.listeners.clear();
|
|
1425
|
+
}
|
|
1426
|
+
releaseTracks(clearPreview) {
|
|
1427
|
+
for (const track of this.tracks) track.stop();
|
|
1428
|
+
this.tracks = [];
|
|
1429
|
+
this.videoTrack = void 0;
|
|
1430
|
+
this.audioTrack = void 0;
|
|
1431
|
+
if (clearPreview) this.preview = void 0;
|
|
1432
|
+
else if (this.preview) delete this.preview.trackRef;
|
|
1433
|
+
}
|
|
1434
|
+
async ensurePreparedTracks() {
|
|
1435
|
+
this.ensureNotDisposed();
|
|
1436
|
+
const tracksReady = _optionalChain([this, 'access', _99 => _99.videoTrack, 'optionalAccess', _100 => _100.mediaStreamTrack, 'optionalAccess', _101 => _101.readyState]) !== "ended" && _optionalChain([this, 'access', _102 => _102.audioTrack, 'optionalAccess', _103 => _103.mediaStreamTrack, 'optionalAccess', _104 => _104.readyState]) !== "ended" && Boolean(this.videoTrack && this.audioTrack);
|
|
1437
|
+
if (tracksReady) return;
|
|
1438
|
+
if (this.tracks.length) this.releaseTracks(false);
|
|
1439
|
+
const resolution = this.captureResolution();
|
|
1440
|
+
const tracks = await this.bindings.createLocalTracks({
|
|
1441
|
+
audio: true,
|
|
1442
|
+
video: {
|
|
1443
|
+
...this.deviceOverride ? { deviceId: this.deviceOverride } : { facingMode: this.isFront ? "user" : "environment" },
|
|
1444
|
+
resolution
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
if (this.disposed) {
|
|
1448
|
+
for (const track of tracks) track.stop();
|
|
1449
|
+
throw new Error("LiveKit room adapter has been disposed");
|
|
1450
|
+
}
|
|
1451
|
+
this.tracks = tracks;
|
|
1452
|
+
this.videoTrack = this.tracks.find((track) => track.kind === "video");
|
|
1453
|
+
this.audioTrack = this.tracks.find((track) => track.kind === "audio");
|
|
1454
|
+
if (!this.videoTrack || !this.audioTrack) {
|
|
1455
|
+
this.releaseTracks(true);
|
|
1456
|
+
throw new Error("LiveKit did not create camera and microphone tracks");
|
|
1457
|
+
}
|
|
1458
|
+
if (this.preview) {
|
|
1459
|
+
this.preview.videoTrack = this.videoTrack;
|
|
1460
|
+
delete this.preview.trackRef;
|
|
1461
|
+
} else {
|
|
1462
|
+
this.preview = { videoTrack: this.videoTrack };
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
emit(event) {
|
|
1466
|
+
for (const listener of this.listeners) listener(event);
|
|
1467
|
+
}
|
|
1468
|
+
ensureNotDisposed() {
|
|
1469
|
+
if (this.disposed) {
|
|
1470
|
+
throw new Error("LiveKit room adapter has been disposed");
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}, _class2);
|
|
1474
|
+
|
|
1475
|
+
// src/rtmp-room-adapter.ts
|
|
1476
|
+
var VIDEO_BITRATE = {
|
|
1477
|
+
720: 35e5,
|
|
1478
|
+
1080: 6e6
|
|
1479
|
+
};
|
|
1480
|
+
var RtmpRoomAdapter = (_class3 = class {
|
|
1481
|
+
constructor(bindings) {;_class3.prototype.__init27.call(this);_class3.prototype.__init28.call(this);_class3.prototype.__init29.call(this);
|
|
1482
|
+
this.bindings = bindings;
|
|
1483
|
+
}
|
|
1484
|
+
__init27() {this.listeners = /* @__PURE__ */ new Set()}
|
|
1485
|
+
|
|
1486
|
+
__init28() {this.quality = 1080}
|
|
1487
|
+
__init29() {this.connected = false}
|
|
1488
|
+
async preparePreview(quality) {
|
|
1489
|
+
this.quality = quality;
|
|
1490
|
+
await this.bindings.prepare({
|
|
1491
|
+
quality,
|
|
1492
|
+
landscape: _nullishCoalesce(_optionalChain([this, 'access', _105 => _105.bindings, 'access', _106 => _106.isLandscape, 'optionalCall', _107 => _107()]), () => ( true))
|
|
1493
|
+
});
|
|
1494
|
+
return { kind: "rtmp" };
|
|
1495
|
+
}
|
|
1496
|
+
async connect(input) {
|
|
1497
|
+
if (!/^rtmps?:\/\//i.test(input.serverUrl)) {
|
|
1498
|
+
throw new Error(
|
|
1499
|
+
`Mode device-rtmp c\u1EA7n endpoint rtmp:// nh\u01B0ng phi\xEAn tr\u1EA3 v\u1EC1 "${input.serverUrl}". Ki\u1EC3m tra deliveryMode c\u1EE7a app tr\xEAn server (partnerApps.deliveryMode).`
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
const url = `${input.serverUrl}${input.participantToken}`;
|
|
1503
|
+
await this.bindings.setMicrophoneEnabled(input.microphoneEnabled);
|
|
1504
|
+
await this.bindings.connect({
|
|
1505
|
+
url,
|
|
1506
|
+
videoBitrate: VIDEO_BITRATE[this.quality],
|
|
1507
|
+
// publish !== false → mặc định phát ngay; false = vào standby.
|
|
1508
|
+
publish: input.publish !== false
|
|
1509
|
+
});
|
|
1510
|
+
this.connected = true;
|
|
1511
|
+
}
|
|
1512
|
+
async setPublishing(enabled) {
|
|
1513
|
+
await this.bindings.setPublishing(enabled);
|
|
1514
|
+
}
|
|
1515
|
+
async disconnect(_stopTracks) {
|
|
1516
|
+
if (!this.connected) return;
|
|
1517
|
+
this.connected = false;
|
|
1518
|
+
await this.bindings.disconnect();
|
|
1519
|
+
}
|
|
1520
|
+
async switchCamera() {
|
|
1521
|
+
await this.bindings.switchCamera();
|
|
1522
|
+
}
|
|
1523
|
+
/** Ống kính vật lý (0.5x siêu rộng · 1x · tele) — không có thì UI kẹt ở mặc định. */
|
|
1524
|
+
async listCameras() {
|
|
1525
|
+
return this.bindings.listCameras().map((camera) => ({
|
|
1526
|
+
deviceId: camera.deviceId,
|
|
1527
|
+
label: camera.label,
|
|
1528
|
+
facing: camera.facing === "front" || camera.facing === "environment" ? camera.facing : "unknown"
|
|
1529
|
+
}));
|
|
1530
|
+
}
|
|
1531
|
+
async selectCamera(deviceId, facing) {
|
|
1532
|
+
await this.bindings.selectCamera(deviceId, facing);
|
|
1533
|
+
}
|
|
1534
|
+
async setMicrophoneEnabled(enabled) {
|
|
1535
|
+
await this.bindings.setMicrophoneEnabled(enabled);
|
|
1536
|
+
}
|
|
1537
|
+
/** Hạ chất lượng khi mạng yếu — thay cho BWE mà WebRTC cho sẵn. */
|
|
1538
|
+
async setVideoBitrate(bitsPerSecond) {
|
|
1539
|
+
await this.bindings.setVideoBitrate(bitsPerSecond);
|
|
1540
|
+
}
|
|
1541
|
+
async setCaptureOrientation(_orientation) {
|
|
1542
|
+
}
|
|
1543
|
+
subscribe(listener) {
|
|
1544
|
+
this.listeners.add(listener);
|
|
1545
|
+
if (!this.nativeSubscription) {
|
|
1546
|
+
this.nativeSubscription = this.bindings.addStatusListener((event) => {
|
|
1547
|
+
const mapped = mapStatus(event);
|
|
1548
|
+
if (mapped) this.emit(mapped);
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
return () => {
|
|
1552
|
+
this.listeners.delete(listener);
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
async dispose() {
|
|
1556
|
+
_optionalChain([this, 'access', _108 => _108.nativeSubscription, 'optionalAccess', _109 => _109.remove, 'call', _110 => _110()]);
|
|
1557
|
+
this.nativeSubscription = void 0;
|
|
1558
|
+
this.listeners.clear();
|
|
1559
|
+
this.connected = false;
|
|
1560
|
+
await this.bindings.dispose();
|
|
1561
|
+
}
|
|
1562
|
+
emit(event) {
|
|
1563
|
+
for (const listener of this.listeners) listener(event);
|
|
1564
|
+
}
|
|
1565
|
+
}, _class3);
|
|
1566
|
+
function mapStatus(event) {
|
|
1567
|
+
switch (event.status) {
|
|
1568
|
+
case "error":
|
|
1569
|
+
return { type: "media-error", error: new Error(event.message) };
|
|
1570
|
+
case "disconnected":
|
|
1571
|
+
return { type: "disconnected", reason: event.reason };
|
|
1572
|
+
case "connecting":
|
|
1573
|
+
return { type: "reconnecting" };
|
|
1574
|
+
case "publishing":
|
|
1575
|
+
return { type: "reconnected" };
|
|
1576
|
+
// "idle"/"connected" là bước trung gian, engine không cần biết.
|
|
1577
|
+
default:
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/native-runtime.ts
|
|
1583
|
+
var globalsRegistered = false;
|
|
1584
|
+
function createNativeLiveKitBindings() {
|
|
1585
|
+
const reactNativeLiveKit = _chunk7OX2DSKIcjs.__require.call(void 0, "@livekit/react-native");
|
|
1586
|
+
const liveKit = _chunk7OX2DSKIcjs.__require.call(void 0, "livekit-client");
|
|
1587
|
+
return {
|
|
1588
|
+
registerGlobals() {
|
|
1589
|
+
if (!globalsRegistered) {
|
|
1590
|
+
reactNativeLiveKit.registerGlobals();
|
|
1591
|
+
globalsRegistered = true;
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
createRoom: () => new liveKit.Room({ adaptiveStream: false, dynacast: false }),
|
|
1595
|
+
createLocalTracks: liveKit.createLocalTracks,
|
|
1596
|
+
startAudioSession: () => reactNativeLiveKit.AudioSession.startAudioSession(),
|
|
1597
|
+
stopAudioSession: () => reactNativeLiveKit.AudioSession.stopAudioSession(),
|
|
1598
|
+
isLandscape() {
|
|
1599
|
+
const { width, height } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native").Dimensions.get("window");
|
|
1600
|
+
return width > height;
|
|
1601
|
+
},
|
|
1602
|
+
async listCameras() {
|
|
1603
|
+
const { mediaDevices } = _chunk7OX2DSKIcjs.__require.call(void 0, "@livekit/react-native-webrtc");
|
|
1604
|
+
const devices = await mediaDevices.enumerateDevices();
|
|
1605
|
+
return devices.filter((device) => device.kind === "videoinput" && device.deviceId).map((device) => ({
|
|
1606
|
+
deviceId: String(device.deviceId),
|
|
1607
|
+
label: String(_nullishCoalesce(device.label, () => ( ""))),
|
|
1608
|
+
facing: device.facing === "front" || device.facing === "environment" ? device.facing : "unknown"
|
|
1609
|
+
}));
|
|
1610
|
+
},
|
|
1611
|
+
cameraSource: liveKit.Track.Source.Camera,
|
|
1612
|
+
events: {
|
|
1613
|
+
reconnecting: liveKit.RoomEvent.Reconnecting,
|
|
1614
|
+
reconnected: liveKit.RoomEvent.Reconnected,
|
|
1615
|
+
disconnected: liveKit.RoomEvent.Disconnected,
|
|
1616
|
+
mediaDevicesError: liveKit.RoomEvent.MediaDevicesError,
|
|
1617
|
+
dataReceived: liveKit.RoomEvent.DataReceived
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function createDefaultRuntime() {
|
|
1622
|
+
const { NativeModules, Platform } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native");
|
|
1623
|
+
return {
|
|
1624
|
+
assertSupported() {
|
|
1625
|
+
if (Platform.OS !== "ios" && Platform.OS !== "android" || !NativeModules.LivekitReactNativeModule) {
|
|
1626
|
+
throw new UnsupportedRuntimeError();
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
async requestPermissions() {
|
|
1630
|
+
try {
|
|
1631
|
+
const { mediaDevices } = _chunk7OX2DSKIcjs.__require.call(void 0, "@livekit/react-native-webrtc");
|
|
1632
|
+
const stream = await mediaDevices.getUserMedia({ audio: true, video: true });
|
|
1633
|
+
for (const track of stream.getTracks()) track.stop();
|
|
1634
|
+
return true;
|
|
1635
|
+
} catch (e10) {
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
activateKeepAwake() {
|
|
1640
|
+
return _chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").activateKeepAwakeAsync(
|
|
1641
|
+
"@pickleball/expo-sdk"
|
|
1642
|
+
);
|
|
1643
|
+
},
|
|
1644
|
+
deactivateKeepAwake() {
|
|
1645
|
+
return _chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").deactivateKeepAwake(
|
|
1646
|
+
"@pickleball/expo-sdk"
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function createDefaultStorage() {
|
|
1652
|
+
let installed = false;
|
|
1653
|
+
const install = () => {
|
|
1654
|
+
if (installed) return;
|
|
1655
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "expo-sqlite/localStorage/install");
|
|
1656
|
+
installed = true;
|
|
1657
|
+
};
|
|
1658
|
+
return {
|
|
1659
|
+
getItem: async (key) => {
|
|
1660
|
+
install();
|
|
1661
|
+
return globalThis.localStorage.getItem(key);
|
|
1662
|
+
},
|
|
1663
|
+
setItem: async (key, value) => {
|
|
1664
|
+
install();
|
|
1665
|
+
globalThis.localStorage.setItem(key, value);
|
|
1666
|
+
},
|
|
1667
|
+
removeItem: async (key) => {
|
|
1668
|
+
install();
|
|
1669
|
+
globalThis.localStorage.removeItem(key);
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function createDefaultNetwork() {
|
|
1674
|
+
const NetInfo = _chunk7OX2DSKIcjs.__require.call(void 0, "@react-native-community/netinfo").default;
|
|
1675
|
+
let online = true;
|
|
1676
|
+
void NetInfo.fetch().then((state) => {
|
|
1677
|
+
online = state.isConnected !== false;
|
|
1678
|
+
});
|
|
1679
|
+
return {
|
|
1680
|
+
isOnline: () => online,
|
|
1681
|
+
subscribe(listener) {
|
|
1682
|
+
return NetInfo.addEventListener((state) => {
|
|
1683
|
+
online = state.isConnected !== false;
|
|
1684
|
+
listener(online);
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
function createDefaultTelemetry(fetchImplementation = globalThis.fetch, timeoutMs = 1e4) {
|
|
1690
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
1691
|
+
return {
|
|
1692
|
+
async send(credentials, events) {
|
|
1693
|
+
const body = JSON.stringify(events.slice(0, 20));
|
|
1694
|
+
if (body.length > 24 * 1024) return;
|
|
1695
|
+
const controller = new AbortController();
|
|
1696
|
+
controllers.add(controller);
|
|
1697
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1698
|
+
try {
|
|
1699
|
+
const response = await fetchImplementation(credentials.endpoint, {
|
|
1700
|
+
method: "POST",
|
|
1701
|
+
headers: {
|
|
1702
|
+
"Content-Type": "application/json",
|
|
1703
|
+
Authorization: `Bearer ${credentials.token}`
|
|
1704
|
+
},
|
|
1705
|
+
body,
|
|
1706
|
+
signal: controller.signal,
|
|
1707
|
+
redirect: "error"
|
|
1708
|
+
});
|
|
1709
|
+
if (typeof response.text === "function") await response.text();
|
|
1710
|
+
if (!response.ok) {
|
|
1711
|
+
throw Object.assign(new Error("Telemetry rejected"), {
|
|
1712
|
+
status: response.status
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
} finally {
|
|
1716
|
+
clearTimeout(timeout);
|
|
1717
|
+
controllers.delete(controller);
|
|
1718
|
+
}
|
|
1719
|
+
},
|
|
1720
|
+
dispose() {
|
|
1721
|
+
for (const controller of controllers) controller.abort();
|
|
1722
|
+
controllers.clear();
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function createDefaultRoomFactory() {
|
|
1727
|
+
return () => new LiveKitRoomAdapter(createNativeLiveKitBindings());
|
|
1728
|
+
}
|
|
1729
|
+
function createNativeRtmpBindings() {
|
|
1730
|
+
const native = _chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native").default;
|
|
1731
|
+
return {
|
|
1732
|
+
prepare: (options) => native.prepare(options),
|
|
1733
|
+
isLandscape() {
|
|
1734
|
+
const { width, height } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native").Dimensions.get("window");
|
|
1735
|
+
return width > height;
|
|
1736
|
+
},
|
|
1737
|
+
connect: (options) => native.connect(options),
|
|
1738
|
+
setPublishing: (enabled) => native.setPublishing(enabled),
|
|
1739
|
+
disconnect: () => native.disconnect(),
|
|
1740
|
+
switchCamera: () => native.switchCamera(),
|
|
1741
|
+
listCameras: () => native.listCameras(),
|
|
1742
|
+
selectCamera: (deviceId, facing) => native.selectCamera(deviceId, facing),
|
|
1743
|
+
setMicrophoneEnabled: (enabled) => native.setMicrophoneEnabled(enabled),
|
|
1744
|
+
setVideoBitrate: (bps) => native.setVideoBitrate(bps),
|
|
1745
|
+
dispose: () => native.dispose(),
|
|
1746
|
+
addStatusListener: (listener) => native.addListener("onStatusChanged", listener)
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function createRtmpRoomFactory() {
|
|
1750
|
+
return () => new RtmpRoomAdapter(createNativeRtmpBindings());
|
|
1751
|
+
}
|
|
1752
|
+
function createDefaultDeviceMetrics() {
|
|
1753
|
+
return async () => {
|
|
1754
|
+
const metrics = {};
|
|
1755
|
+
const notes = [];
|
|
1756
|
+
try {
|
|
1757
|
+
const Battery = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-battery");
|
|
1758
|
+
const level = await Battery.getBatteryLevelAsync();
|
|
1759
|
+
if (typeof level === "number" && level >= 0) {
|
|
1760
|
+
metrics.batteryPct = Math.round(level * 100);
|
|
1761
|
+
} else {
|
|
1762
|
+
notes.push(`battery level=${String(level)}`);
|
|
1763
|
+
}
|
|
1764
|
+
const state = await Battery.getBatteryStateAsync();
|
|
1765
|
+
if (typeof state === "number") metrics.charging = state === 2 || state === 3;
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
notes.push(`battery require/call: ${String(error)}`);
|
|
1768
|
+
}
|
|
1769
|
+
try {
|
|
1770
|
+
let FileSystem;
|
|
1771
|
+
try {
|
|
1772
|
+
FileSystem = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-file-system/legacy");
|
|
1773
|
+
} catch (e11) {
|
|
1774
|
+
FileSystem = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-file-system");
|
|
1775
|
+
}
|
|
1776
|
+
if (FileSystem.getFreeDiskStorageAsync) {
|
|
1777
|
+
const freeBytes = await FileSystem.getFreeDiskStorageAsync();
|
|
1778
|
+
metrics.storageFreeGb = Math.round(freeBytes / 1e8) / 10;
|
|
1779
|
+
} else {
|
|
1780
|
+
notes.push("filesystem: getFreeDiskStorageAsync missing");
|
|
1781
|
+
}
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
notes.push(`filesystem: ${String(error)}`);
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
const Device = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-device");
|
|
1787
|
+
if (typeof Device.totalMemory === "number") {
|
|
1788
|
+
metrics.ramTotalMb = Math.round(Device.totalMemory / 1048576);
|
|
1789
|
+
}
|
|
1790
|
+
} catch (e12) {
|
|
1791
|
+
}
|
|
1792
|
+
try {
|
|
1793
|
+
const NetInfo = _chunk7OX2DSKIcjs.__require.call(void 0, "@react-native-community/netinfo").default;
|
|
1794
|
+
const status = await NetInfo.fetch();
|
|
1795
|
+
if (_optionalChain([status, 'optionalAccess', _111 => _111.type])) {
|
|
1796
|
+
metrics.netType = status.type === "wifi" ? "WiFi" : status.type === "cellular" ? String(
|
|
1797
|
+
_nullishCoalesce(_optionalChain([status, 'access', _112 => _112.details, 'optionalAccess', _113 => _113.cellularGeneration]), () => ( "cellular"))
|
|
1798
|
+
).toUpperCase() : status.type;
|
|
1799
|
+
}
|
|
1800
|
+
} catch (e13) {
|
|
1801
|
+
}
|
|
1802
|
+
if (notes.length > 0) console.log("[pickleball] metrics notes:", notes.join(" | "));
|
|
1803
|
+
return Object.keys(metrics).length > 0 ? metrics : null;
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/provider.tsx
|
|
1808
|
+
var _react = require('react'); var _react2 = _interopRequireDefault(_react);
|
|
1809
|
+
var _reactnative = require('react-native');
|
|
1810
|
+
var _jsxruntime = require('react/jsx-runtime');
|
|
1811
|
+
var defaultLivestreamTheme = {
|
|
1812
|
+
background: "#0a0a0b",
|
|
1813
|
+
foreground: "#f5f6f7",
|
|
1814
|
+
accent: "#b4ff39",
|
|
1815
|
+
danger: "#fb7185",
|
|
1816
|
+
muted: "#6c7079",
|
|
1817
|
+
overlay: "rgba(10,10,11,0.82)",
|
|
1818
|
+
radius: 14,
|
|
1819
|
+
surface: "#131417",
|
|
1820
|
+
elevated: "#1b1d21",
|
|
1821
|
+
hairline: "rgba(255,255,255,0.09)",
|
|
1822
|
+
ink: "#0a0a0b",
|
|
1823
|
+
textSecondary: "#a7abb3",
|
|
1824
|
+
radiusSm: 6
|
|
1825
|
+
};
|
|
1826
|
+
var LivestreamHostContext = _react2.default.createContext(null);
|
|
1827
|
+
function useLivestreamHost() {
|
|
1828
|
+
const value = _react2.default.use(LivestreamHostContext);
|
|
1829
|
+
if (!value) {
|
|
1830
|
+
throw new ExpoSdkConfigurationError(
|
|
1831
|
+
"useLivestreamHost must be used inside PickleballLiveProvider"
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
return value;
|
|
1835
|
+
}
|
|
1836
|
+
var LivestreamContext = _react2.default.createContext(null);
|
|
1837
|
+
function PickleballLiveProvider(props) {
|
|
1838
|
+
const engineRef = _react.useRef.call(void 0, null);
|
|
1839
|
+
const hostRef = _react.useRef.call(void 0, null);
|
|
1840
|
+
const effectGenerationRef = _react.useRef.call(void 0, 0);
|
|
1841
|
+
if (!engineRef.current) {
|
|
1842
|
+
const deviceMetrics = _nullishCoalesce(props.deviceMetrics, () => ( createDefaultDeviceMetrics()));
|
|
1843
|
+
const dependencies = {
|
|
1844
|
+
sessionProvider: props.sessionProvider,
|
|
1845
|
+
bootstrapInput: props.bootstrapInput,
|
|
1846
|
+
runtime: _nullishCoalesce(props.runtime, () => ( createDefaultRuntime())),
|
|
1847
|
+
storage: _nullishCoalesce(props.storage, () => ( createDefaultStorage())),
|
|
1848
|
+
network: _nullishCoalesce(props.network, () => ( createDefaultNetwork())),
|
|
1849
|
+
telemetry: _nullishCoalesce(props.telemetry, () => ( createDefaultTelemetry())),
|
|
1850
|
+
deviceMetrics,
|
|
1851
|
+
roomFactory: _nullishCoalesce(props.roomFactory, () => ( createDefaultRoomFactory()))
|
|
1852
|
+
};
|
|
1853
|
+
engineRef.current = new LivestreamEngine(dependencies);
|
|
1854
|
+
hostRef.current = {
|
|
1855
|
+
engine: engineRef.current,
|
|
1856
|
+
bootstrapInput: props.bootstrapInput,
|
|
1857
|
+
deviceMetrics
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
const engine = engineRef.current;
|
|
1861
|
+
const snapshot = _react.useSyncExternalStore.call(void 0,
|
|
1862
|
+
engine.subscribe,
|
|
1863
|
+
engine.getSnapshot,
|
|
1864
|
+
engine.getSnapshot
|
|
1865
|
+
);
|
|
1866
|
+
_react.useEffect.call(void 0, () => {
|
|
1867
|
+
const effectGeneration = ++effectGenerationRef.current;
|
|
1868
|
+
void engine.initialize();
|
|
1869
|
+
const subscription = _reactnative.AppState.addEventListener("change", (state) => {
|
|
1870
|
+
engine.handleAppState(
|
|
1871
|
+
state === "active" ? "active" : state === "background" ? "background" : "inactive"
|
|
1872
|
+
);
|
|
1873
|
+
});
|
|
1874
|
+
return () => {
|
|
1875
|
+
subscription.remove();
|
|
1876
|
+
queueMicrotask(() => {
|
|
1877
|
+
if (effectGenerationRef.current === effectGeneration) {
|
|
1878
|
+
void engine.dispose();
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
};
|
|
1882
|
+
}, [engine]);
|
|
1883
|
+
_react.useEffect.call(void 0, () => {
|
|
1884
|
+
if (snapshot.error) _optionalChain([props, 'access', _114 => _114.onError, 'optionalCall', _115 => _115(snapshot.error)]);
|
|
1885
|
+
}, [props.onError, snapshot.error]);
|
|
1886
|
+
const value = {
|
|
1887
|
+
...snapshot,
|
|
1888
|
+
locale: _nullishCoalesce(props.locale, () => ( "vi")),
|
|
1889
|
+
theme: { ...defaultLivestreamTheme, ...props.theme },
|
|
1890
|
+
prepare: () => engine.prepare(),
|
|
1891
|
+
start: (input, options) => engine.start(input, options),
|
|
1892
|
+
startPublishing: () => engine.startPublishing(),
|
|
1893
|
+
stopPublishing: () => engine.stopPublishing(),
|
|
1894
|
+
stop: (reason) => engine.stop(reason),
|
|
1895
|
+
retry: () => engine.retry(),
|
|
1896
|
+
switchCamera: () => engine.switchCamera(),
|
|
1897
|
+
setMicrophoneEnabled: (enabled) => engine.setMicrophoneEnabled(enabled),
|
|
1898
|
+
setCaptureOrientation: (orientation) => engine.setCaptureOrientation(orientation),
|
|
1899
|
+
selectCamera: (deviceId) => engine.selectCamera(deviceId)
|
|
1900
|
+
};
|
|
1901
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, LivestreamHostContext.Provider, { value: hostRef.current, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, LivestreamContext.Provider, { value, children: props.children }) });
|
|
1902
|
+
}
|
|
1903
|
+
function useLivestream() {
|
|
1904
|
+
const value = _react2.default.use(LivestreamContext);
|
|
1905
|
+
if (!value) {
|
|
1906
|
+
throw new ExpoSdkConfigurationError(
|
|
1907
|
+
"useLivestream must be used inside PickleballLiveProvider"
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
return value;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
|
|
1924
|
+
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
exports.SDK_TELEMETRY_EVENT_NAMES = SDK_TELEMETRY_EVENT_NAMES; exports.isSdkRemoteConfig = isSdkRemoteConfig; exports.compareSemver = compareSemver; exports.isSdkVersionSupported = isSdkVersionSupported; exports.PickleballExpoError = PickleballExpoError; exports.PermissionDeniedError = PermissionDeniedError; exports.NetworkUnavailableError = NetworkUnavailableError; exports.TokenExpiredError = TokenExpiredError; exports.UnsupportedSdkError = UnsupportedSdkError; exports.UnsupportedRuntimeError = UnsupportedRuntimeError; exports.SessionConflictError = SessionConflictError; exports.RecordingFailureError = RecordingFailureError; exports.ConsentRequiredError = ConsentRequiredError; exports.SdkDisabledError = SdkDisabledError; exports.ExpoSdkConfigurationError = ExpoSdkConfigurationError; exports.InvalidResponseError = InvalidResponseError; exports.ExpoSdkTimeoutError = ExpoSdkTimeoutError; exports.UnknownSdkError = UnknownSdkError; exports.errorFromCode = errorFromCode; exports.normalizeError = normalizeError; exports.isSessionGoneError = isSessionGoneError; exports.PENDING_END_KEY = PENDING_END_KEY; exports.LEGAL_LIVESTREAM_TRANSITIONS = LEGAL_LIVESTREAM_TRANSITIONS; exports.LivestreamEngine = LivestreamEngine; exports.LiveKitRoomAdapter = LiveKitRoomAdapter; exports.RtmpRoomAdapter = RtmpRoomAdapter; exports.createNativeLiveKitBindings = createNativeLiveKitBindings; exports.createDefaultRuntime = createDefaultRuntime; exports.createDefaultStorage = createDefaultStorage; exports.createDefaultNetwork = createDefaultNetwork; exports.createDefaultTelemetry = createDefaultTelemetry; exports.createDefaultRoomFactory = createDefaultRoomFactory; exports.createNativeRtmpBindings = createNativeRtmpBindings; exports.createRtmpRoomFactory = createRtmpRoomFactory; exports.createDefaultDeviceMetrics = createDefaultDeviceMetrics; exports.defaultLivestreamTheme = defaultLivestreamTheme; exports.useLivestreamHost = useLivestreamHost; exports.PickleballLiveProvider = PickleballLiveProvider; exports.useLivestream = useLivestream;
|