@pickleball/expo-sdk 0.1.1 → 1.0.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 +65 -0
- package/README.md +163 -40
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +47 -0
- package/dist/advanced.d.cts +3 -0
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.js +47 -0
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
- package/dist/chunk-2JHULJZ7.js +265 -0
- package/dist/chunk-62YV3PCD.cjs +139 -0
- package/dist/chunk-6F3HN6US.js +50 -0
- package/dist/chunk-6YE5BL3A.cjs +50 -0
- package/dist/chunk-7OX2DSKI.cjs +33 -0
- package/dist/chunk-7Z7TN5N3.js +318 -0
- package/dist/chunk-DHDX3ZOO.js +800 -0
- package/dist/chunk-GXQYMPHE.cjs +9 -0
- package/dist/chunk-IUIRVKJI.cjs +2290 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-LCWGNGEN.cjs +265 -0
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/chunk-XGP2MN5R.js +2290 -0
- package/dist/chunk-XL4CISVQ.js +9 -0
- package/dist/chunk-YGSQ25UA.cjs +800 -0
- package/dist/chunk-ZSKSXOCZ.js +139 -0
- package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
- package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
- package/dist/device-agent/index.cjs +1427 -0
- package/dist/device-agent/index.d.cts +493 -0
- package/dist/device-agent/index.d.ts +493 -0
- package/dist/device-agent/index.js +1427 -0
- package/dist/engine-BSvSdCSE.d.cts +741 -0
- package/dist/engine-BSvSdCSE.d.ts +741 -0
- package/dist/index.cjs +426 -6174
- package/dist/index.d.cts +299 -1400
- package/dist/index.d.ts +299 -1400
- package/dist/index.js +413 -6063
- 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 +52 -71
- package/dist/plugin.d.cts +28 -1
- package/dist/plugin.d.ts +28 -1
- package/dist/plugin.js +26 -23
- package/dist/realtime/index.cjs +31 -0
- package/dist/realtime/index.d.cts +15 -0
- package/dist/realtime/index.d.ts +15 -0
- package/dist/realtime/index.js +31 -0
- 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/ui/index.cjs +1297 -0
- package/dist/ui/index.d.cts +493 -0
- package/dist/ui/index.d.ts +493 -0
- package/dist/ui/index.js +1297 -0
- package/dist/version.cjs +4 -32
- package/dist/version.js +2 -1
- package/package.json +95 -15
- package/src/advanced.ts +49 -0
- package/src/auto-resume.tsx +259 -0
- package/src/camera-controls.ts +99 -0
- package/src/camera-grant-provider.ts +392 -0
- package/src/camera-settings.ts +133 -0
- package/src/contracts.ts +73 -0
- package/src/device-agent/agent.ts +696 -0
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +60 -0
- package/src/device-agent/convex-beat-transport.ts +140 -0
- package/src/device-agent/convex-command-subscription.ts +101 -0
- package/src/device-agent/convex-control-channel.ts +88 -0
- package/src/device-agent/http-transport.ts +164 -0
- package/src/device-agent/index.ts +26 -0
- package/src/device-agent/mqtt-channel.ts +379 -0
- package/src/device-agent/mqtt-codec.ts +347 -0
- package/src/device-agent/tournament.ts +352 -0
- package/src/engine.ts +601 -20
- package/src/errors.ts +16 -0
- package/src/http-session-provider.ts +21 -183
- package/src/index.ts +117 -14
- package/src/local-video-quality.ts +43 -0
- 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 +283 -77
- package/src/plugin.ts +54 -20
- package/src/provider.tsx +129 -22
- package/src/realtime/index.ts +56 -0
- package/src/resume-policy.ts +85 -0
- package/src/resume.ts +132 -0
- package/src/rtmp-room-adapter.ts +348 -0
- package/src/session-grant.ts +256 -0
- package/src/ui/camera-screen.tsx +484 -0
- package/src/ui/controls-sheet.tsx +445 -0
- package/src/ui/copy.ts +326 -0
- package/src/ui/helpers.ts +105 -0
- package/src/ui/hooks.ts +59 -0
- package/src/ui/index.ts +42 -0
- package/src/ui/preview-view.tsx +39 -0
- package/src/ui/primitives.tsx +304 -0
- package/src/uplink-check.ts +173 -0
- package/dist/chunk-VHLUJQG5.js +0 -17
- package/src/livekit-room-adapter.ts +0 -353
- package/src/livestream-screen.tsx +0 -980
|
@@ -0,0 +1,2290 @@
|
|
|
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;
|
|
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 CAPTURE_ORIENTATION_KEY = "@pickleball/expo-sdk/capture-orientation";
|
|
190
|
+
var DEFAULT_STANDBY_TIMEOUT_MS = 15 * 6e4;
|
|
191
|
+
var TOKEN_REFRESH_MARGIN_MS = 2 * 6e4;
|
|
192
|
+
var COMMAND_POLL_STANDBY_MS = 2e3;
|
|
193
|
+
var COMMAND_POLL_LIVE_MS = 4e3;
|
|
194
|
+
var COMMAND_POLL_MAX_BACKOFF_MS = 3e4;
|
|
195
|
+
var COMMAND_POLL_RECONCILE_MS = 3e4;
|
|
196
|
+
var activeStates = /* @__PURE__ */ new Set([
|
|
197
|
+
"standby",
|
|
198
|
+
"live",
|
|
199
|
+
"reconnecting"
|
|
200
|
+
]);
|
|
201
|
+
var inMemorySessionStates = /* @__PURE__ */ new Set([
|
|
202
|
+
"connecting",
|
|
203
|
+
"standby",
|
|
204
|
+
"live",
|
|
205
|
+
"reconnecting",
|
|
206
|
+
"ending"
|
|
207
|
+
]);
|
|
208
|
+
var StaleOperationError = class extends Error {
|
|
209
|
+
};
|
|
210
|
+
var LEGAL_LIVESTREAM_TRANSITIONS = {
|
|
211
|
+
idle: ["preparing"],
|
|
212
|
+
preparing: ["preview", "ending", "error"],
|
|
213
|
+
preview: ["connecting", "preparing", "ending", "error"],
|
|
214
|
+
connecting: ["standby", "live", "ending", "error"],
|
|
215
|
+
standby: ["live", "reconnecting", "ending", "error"],
|
|
216
|
+
live: ["standby", "reconnecting", "ending", "error"],
|
|
217
|
+
reconnecting: ["standby", "live", "ending", "error"],
|
|
218
|
+
ending: ["ended", "error"],
|
|
219
|
+
ended: ["preparing"],
|
|
220
|
+
error: ["preparing", "reconnecting", "ending", "ended"]
|
|
221
|
+
};
|
|
222
|
+
var LivestreamEngine = (_class = class {
|
|
223
|
+
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);_class.prototype.__init19.call(this);_class.prototype.__init20.call(this);_class.prototype.__init21.call(this);_class.prototype.__init22.call(this);
|
|
224
|
+
this.dependencies = dependencies;
|
|
225
|
+
this.now = _nullishCoalesce(dependencies.now, () => ( Date.now));
|
|
226
|
+
this.snapshot = {
|
|
227
|
+
state: "idle",
|
|
228
|
+
error: null,
|
|
229
|
+
config: null,
|
|
230
|
+
sessionId: null,
|
|
231
|
+
previewTrack: null,
|
|
232
|
+
isMicrophoneEnabled: true,
|
|
233
|
+
// Camera SAU mặc định: thiết bị quay sân luôn dùng camera sau, bắt người
|
|
234
|
+
// 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
|
|
235
|
+
// (LiveKitRoomAdapter.isFront, module RTMP cameraPosition) — lệch một chỗ
|
|
236
|
+
// là nhãn UI và hình thực tế ngược nhau vĩnh viễn.
|
|
237
|
+
isCameraFront: false,
|
|
238
|
+
captureOrientation: null,
|
|
239
|
+
deviceMetrics: null,
|
|
240
|
+
cameras: [],
|
|
241
|
+
selectedCameraId: null,
|
|
242
|
+
elapsedMs: 0,
|
|
243
|
+
isOnline: dependencies.network.isOnline(),
|
|
244
|
+
adoptableSessionId: null
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
__init() {this.listeners = /* @__PURE__ */ new Set()}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
__init2() {this.startupRecovered = false}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__init3() {this.commandTail = Promise.resolve()}
|
|
262
|
+
__init4() {this.commandCount = 0}
|
|
263
|
+
__init5() {this.roomUnsubscribe = () => void 0}
|
|
264
|
+
__init6() {this.networkUnsubscribe = () => void 0}
|
|
265
|
+
__init7() {this.networkSubscribed = false}
|
|
266
|
+
__init8() {this.latestAppState = "active"}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
/** Đang trong vòng quay số lại — xem doRetry() để biết vì sao cần cờ này. */
|
|
272
|
+
__init9() {this.redialing = false}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
/** Huỷ subscription websocket lệnh của phiên hiện tại (nếu đang mở). */
|
|
279
|
+
|
|
280
|
+
/** Khoá phiên+token đang subscribe — đổi thì resubscribe (token refresh/đổi phiên). */
|
|
281
|
+
|
|
282
|
+
/** Websocket đã đẩy được ít nhất một lần → coi là sống, cho phép hạ nhịp poll. */
|
|
283
|
+
__init10() {this.commandSubscriptionActive = false}
|
|
284
|
+
/** issuedAt của lệnh cuối đã xử lý — server chỉ trả lệnh mới hơn mốc này. */
|
|
285
|
+
__init11() {this.commandCursor = 0}
|
|
286
|
+
__init12() {this.commandPollBackoffMs = 0}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
/** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
|
|
290
|
+
__init13() {this.publishing = false}
|
|
291
|
+
/** Tổng thời gian đã phát trước lần tạm dừng gần nhất. */
|
|
292
|
+
__init14() {this.pausedElapsedMs = 0}
|
|
293
|
+
|
|
294
|
+
__init15() {this.refreshRetryAttempt = 0}
|
|
295
|
+
__init16() {this.operationGeneration = 0}
|
|
296
|
+
__init17() {this.liveTimersStarted = false}
|
|
297
|
+
__init18() {this.keepAwakeActive = false}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
__init19() {this.sessionTerminations = /* @__PURE__ */ new Map()}
|
|
301
|
+
|
|
302
|
+
__init20() {this.disposed = false}
|
|
303
|
+
__init21() {this.getSnapshot = () => this.snapshot}
|
|
304
|
+
__init22() {this.subscribe = (listener) => {
|
|
305
|
+
this.listeners.add(listener);
|
|
306
|
+
return () => this.listeners.delete(listener);
|
|
307
|
+
}}
|
|
308
|
+
async initialize() {
|
|
309
|
+
if (this.disposed) return;
|
|
310
|
+
this.ensureNetworkSubscription();
|
|
311
|
+
if (this.startupRecovered) {
|
|
312
|
+
try {
|
|
313
|
+
await this.replayPendingEnd();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
this.pendingReplayError = error;
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!this.startupRecoveryPromise) {
|
|
320
|
+
this.startupRecoveryPromise = this.recoverStartupSession().catch((error) => {
|
|
321
|
+
this.pendingReplayError = error;
|
|
322
|
+
}).finally(() => {
|
|
323
|
+
this.startupRecovered = true;
|
|
324
|
+
this.startupRecoveryPromise = void 0;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
await this.startupRecoveryPromise;
|
|
328
|
+
}
|
|
329
|
+
prepare() {
|
|
330
|
+
if (this.preparePromise) return this.preparePromise;
|
|
331
|
+
if (this.snapshot.state === "preview") return Promise.resolve();
|
|
332
|
+
if (["connecting", "live", "reconnecting", "ending"].includes(
|
|
333
|
+
this.snapshot.state
|
|
334
|
+
) || this.snapshot.state === "error" && this.snapshot.sessionId) {
|
|
335
|
+
return Promise.reject(
|
|
336
|
+
new SessionConflictError("Cannot prepare while a session is active")
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
this.preparePromise = this.enqueueLifecycle(() => this.doPrepare()).finally(() => {
|
|
340
|
+
this.preparePromise = void 0;
|
|
341
|
+
});
|
|
342
|
+
return this.preparePromise;
|
|
343
|
+
}
|
|
344
|
+
async doPrepare() {
|
|
345
|
+
const generation = this.operationGeneration;
|
|
346
|
+
this.ensureNotDisposed();
|
|
347
|
+
this.transition("preparing");
|
|
348
|
+
try {
|
|
349
|
+
await this.ensurePendingEndResolved();
|
|
350
|
+
this.ensureOperation(generation);
|
|
351
|
+
this.dependencies.runtime.assertSupported();
|
|
352
|
+
const bootstrap = await this.dependencies.sessionProvider.bootstrap(
|
|
353
|
+
this.dependencies.bootstrapInput
|
|
354
|
+
);
|
|
355
|
+
this.ensureOperation(generation);
|
|
356
|
+
if (!isSdkRemoteConfig(bootstrap.config)) {
|
|
357
|
+
throw new InvalidResponseError(
|
|
358
|
+
"The session provider returned invalid SDK config"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (!bootstrap.config.enabled || !bootstrap.rolloutEnabled) {
|
|
362
|
+
throw new SdkDisabledError(bootstrap.message);
|
|
363
|
+
}
|
|
364
|
+
if (!isSdkVersionSupported(
|
|
365
|
+
this.dependencies.bootstrapInput.sdkVersion,
|
|
366
|
+
bootstrap.config.minSdkVersion
|
|
367
|
+
)) {
|
|
368
|
+
throw new UnsupportedSdkError();
|
|
369
|
+
}
|
|
370
|
+
this.update({ config: bootstrap.config });
|
|
371
|
+
const room = this.ensureRoom();
|
|
372
|
+
const permitted = room.requestPermissions ? await room.requestPermissions() : await this.dependencies.runtime.requestPermissions();
|
|
373
|
+
this.ensureOperation(generation);
|
|
374
|
+
if (!permitted) {
|
|
375
|
+
throw new PermissionDeniedError();
|
|
376
|
+
}
|
|
377
|
+
if (!this.snapshot.captureOrientation) {
|
|
378
|
+
const stored = await this.dependencies.storage.getItem(CAPTURE_ORIENTATION_KEY).catch(() => null);
|
|
379
|
+
this.ensureOperation(generation);
|
|
380
|
+
if (stored === "portrait" || stored === "landscape") {
|
|
381
|
+
this.update({ captureOrientation: stored });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (this.snapshot.captureOrientation) {
|
|
385
|
+
await _optionalChain([room, 'access', _ => _.setCaptureOrientation, 'optionalCall', _2 => _2(this.snapshot.captureOrientation)]);
|
|
386
|
+
}
|
|
387
|
+
const previewTrack = await room.preparePreview(
|
|
388
|
+
bootstrap.config.videoQuality
|
|
389
|
+
);
|
|
390
|
+
if (!this.isOperationCurrent(generation)) {
|
|
391
|
+
await this.releaseRoom();
|
|
392
|
+
throw new StaleOperationError();
|
|
393
|
+
}
|
|
394
|
+
let cameras = [];
|
|
395
|
+
try {
|
|
396
|
+
cameras = await _asyncNullishCoalesce(await _optionalChain([room, 'access', _3 => _3.listCameras, 'optionalCall', _4 => _4()]), async () => ( []));
|
|
397
|
+
} catch (e2) {
|
|
398
|
+
}
|
|
399
|
+
if (!this.isOperationCurrent(generation)) {
|
|
400
|
+
await this.releaseRoom();
|
|
401
|
+
throw new StaleOperationError();
|
|
402
|
+
}
|
|
403
|
+
this.update({ previewTrack, cameras });
|
|
404
|
+
this.transition("preview");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.fail(error);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
start(input, options) {
|
|
413
|
+
if (this.startPromise) return this.startPromise;
|
|
414
|
+
if (this.snapshot.state === "live" || this.snapshot.state === "standby") {
|
|
415
|
+
return Promise.resolve();
|
|
416
|
+
}
|
|
417
|
+
if (this.snapshot.state !== "preview" && !this.preparePromise) {
|
|
418
|
+
return Promise.reject(
|
|
419
|
+
new SessionConflictError("Livestream preview is not ready")
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
this.startPromise = this.enqueueLifecycle(
|
|
423
|
+
() => this.doStart(input, _nullishCoalesce(_optionalChain([options, 'optionalAccess', _5 => _5.autoPublish]), () => ( true)))
|
|
424
|
+
).finally(() => {
|
|
425
|
+
this.startPromise = void 0;
|
|
426
|
+
});
|
|
427
|
+
return this.startPromise;
|
|
428
|
+
}
|
|
429
|
+
async doStart(input, autoPublish) {
|
|
430
|
+
const generation = this.operationGeneration;
|
|
431
|
+
let activeGrant;
|
|
432
|
+
this.ensureNotDisposed();
|
|
433
|
+
this.ensureState("preview");
|
|
434
|
+
if (!input.consentVersion.trim()) throw new ConsentRequiredError();
|
|
435
|
+
if (!input.externalSessionId.trim() || !input.title.trim()) {
|
|
436
|
+
const error = new ConsentRequiredError("Session ID and title are required");
|
|
437
|
+
this.fail(error);
|
|
438
|
+
}
|
|
439
|
+
if (!this.dependencies.network.isOnline()) {
|
|
440
|
+
this.fail(new NetworkUnavailableError());
|
|
441
|
+
}
|
|
442
|
+
this.transition("connecting");
|
|
443
|
+
this.ensureBackgroundGraceTimer();
|
|
444
|
+
try {
|
|
445
|
+
await this.ensurePendingEndResolved(input.externalSessionId);
|
|
446
|
+
this.ensureOperation(generation);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.fail(error);
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const grant = await this.dependencies.sessionProvider.start({
|
|
455
|
+
...input,
|
|
456
|
+
standby: !autoPublish
|
|
457
|
+
});
|
|
458
|
+
activeGrant = grant;
|
|
459
|
+
if (!this.isOperationCurrent(generation)) {
|
|
460
|
+
await this.abandonGrant(grant);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!isSdkRemoteConfig(grant.config)) {
|
|
464
|
+
await this.abandonGrant(grant);
|
|
465
|
+
throw new InvalidResponseError(
|
|
466
|
+
"The session provider returned invalid SDK config"
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
this.ownedSessionId = grant.sessionId;
|
|
470
|
+
if (this.snapshot.adoptableSessionId === grant.sessionId) {
|
|
471
|
+
this.update({ adoptableSessionId: null });
|
|
472
|
+
}
|
|
473
|
+
this.grant = grant;
|
|
474
|
+
this.commandCursor = 0;
|
|
475
|
+
this.commandPollBackoffMs = 0;
|
|
476
|
+
this.liveTimersStarted = false;
|
|
477
|
+
this.keepAwakeActive = false;
|
|
478
|
+
this.publishing = autoPublish;
|
|
479
|
+
this.pausedElapsedMs = 0;
|
|
480
|
+
this.update({
|
|
481
|
+
config: grant.config,
|
|
482
|
+
sessionId: grant.sessionId,
|
|
483
|
+
elapsedMs: 0
|
|
484
|
+
});
|
|
485
|
+
await this.dependencies.storage.setItem(
|
|
486
|
+
PENDING_END_KEY,
|
|
487
|
+
JSON.stringify({ sessionId: grant.sessionId, pendingEnd: null })
|
|
488
|
+
);
|
|
489
|
+
if (!this.isOperationCurrent(generation)) {
|
|
490
|
+
await this.abandonGrant(grant);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
await this.requireRoom().connect({
|
|
494
|
+
serverUrl: grant.serverUrl,
|
|
495
|
+
participantToken: grant.participantToken,
|
|
496
|
+
microphoneEnabled: this.snapshot.isMicrophoneEnabled,
|
|
497
|
+
publish: autoPublish
|
|
498
|
+
});
|
|
499
|
+
if (!this.isOperationCurrent(generation)) {
|
|
500
|
+
await this.abandonGrant(grant);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await this.enterSession(generation, autoPublish);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
506
|
+
if (activeGrant) await this.abandonGrant(activeGrant);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.fail(error);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
stop(reason = "user") {
|
|
513
|
+
if (this.stopPromise) return this.stopPromise;
|
|
514
|
+
if (this.snapshot.state === "ended" || this.snapshot.state === "idle") {
|
|
515
|
+
return Promise.resolve();
|
|
516
|
+
}
|
|
517
|
+
this.stopPromise = this.enqueueLifecycle(() => this.doStop(reason)).finally(() => {
|
|
518
|
+
this.stopPromise = void 0;
|
|
519
|
+
});
|
|
520
|
+
return this.stopPromise;
|
|
521
|
+
}
|
|
522
|
+
async doStop(reason) {
|
|
523
|
+
if (this.disposed) return;
|
|
524
|
+
const sessionId = this.ownedSessionId;
|
|
525
|
+
const termination = sessionId ? this.ensureSessionTermination(sessionId, reason) : void 0;
|
|
526
|
+
this.transition("ending");
|
|
527
|
+
this.clearTimers();
|
|
528
|
+
const roomCleanup = this.releaseRoom(_optionalChain([termination, 'optionalAccess', _6 => _6.markerReady]));
|
|
529
|
+
await Promise.allSettled([
|
|
530
|
+
roomCleanup,
|
|
531
|
+
this.dependencies.runtime.deactivateKeepAwake()
|
|
532
|
+
]);
|
|
533
|
+
this.keepAwakeActive = false;
|
|
534
|
+
if (sessionId) {
|
|
535
|
+
await _optionalChain([termination, 'optionalAccess', _7 => _7.completion]);
|
|
536
|
+
void this.sendTelemetry("session_ended", { reason });
|
|
537
|
+
}
|
|
538
|
+
this.grant = void 0;
|
|
539
|
+
this.ownedSessionId = void 0;
|
|
540
|
+
this.startedAt = void 0;
|
|
541
|
+
this.publishing = false;
|
|
542
|
+
this.pausedElapsedMs = 0;
|
|
543
|
+
this.update({ previewTrack: null, sessionId: null });
|
|
544
|
+
this.transition("ended");
|
|
545
|
+
}
|
|
546
|
+
retry() {
|
|
547
|
+
if (this.retryPromise) return this.retryPromise;
|
|
548
|
+
this.retryPromise = this.enqueueLifecycle(() => this.doRetry()).finally(() => {
|
|
549
|
+
this.retryPromise = void 0;
|
|
550
|
+
});
|
|
551
|
+
return this.retryPromise;
|
|
552
|
+
}
|
|
553
|
+
async doRetry() {
|
|
554
|
+
const generation = this.operationGeneration;
|
|
555
|
+
this.ensureNotDisposed();
|
|
556
|
+
if (this.snapshot.state === "error" && !this.snapshot.sessionId) {
|
|
557
|
+
this.update({ error: null });
|
|
558
|
+
return this.prepare();
|
|
559
|
+
}
|
|
560
|
+
if (this.snapshot.state === "error" && this.snapshot.sessionId) {
|
|
561
|
+
this.transition("reconnecting");
|
|
562
|
+
}
|
|
563
|
+
this.ensureState("reconnecting");
|
|
564
|
+
const sessionId = this.snapshot.sessionId;
|
|
565
|
+
if (!sessionId || !this.grant) throw new Error("Missing session grant");
|
|
566
|
+
const activeGrant = this.grant;
|
|
567
|
+
try {
|
|
568
|
+
const expiresAt = _nullishCoalesce(_optionalChain([this, 'access', _8 => _8.grant, 'optionalAccess', _9 => _9.tokenExpiresAt]), () => ( 0));
|
|
569
|
+
const tokenStillFresh = expiresAt > 0 && expiresAt - Date.now() >= TOKEN_REFRESH_MARGIN_MS;
|
|
570
|
+
if (!this.redialing || !tokenStillFresh) {
|
|
571
|
+
await this.refreshCredentials(generation);
|
|
572
|
+
}
|
|
573
|
+
this.ensureOperation(generation);
|
|
574
|
+
await this.requireRoom().connect({
|
|
575
|
+
serverUrl: this.grant.serverUrl,
|
|
576
|
+
participantToken: this.grant.participantToken,
|
|
577
|
+
microphoneEnabled: this.snapshot.isMicrophoneEnabled,
|
|
578
|
+
publish: this.publishing
|
|
579
|
+
});
|
|
580
|
+
this.ensureOperation(generation);
|
|
581
|
+
this.clearReconnectTimer();
|
|
582
|
+
await this.enterSession(generation, this.publishing);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
585
|
+
await this.abandonGrant(activeGrant);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const httpStatus = error.httpStatus;
|
|
589
|
+
if (httpStatus === 404 || httpStatus === 409) {
|
|
590
|
+
this.stopRedialing();
|
|
591
|
+
void this.stop("user");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (this.redialing && this.snapshot.sessionId) {
|
|
595
|
+
this.update({ error: normalizeError(error) });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
this.fail(error);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async switchCamera() {
|
|
602
|
+
if (!this.snapshot.previewTrack) return;
|
|
603
|
+
const generation = this.operationGeneration;
|
|
604
|
+
await this.requireRoom().switchCamera();
|
|
605
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
606
|
+
this.update({
|
|
607
|
+
isCameraFront: !this.snapshot.isCameraFront,
|
|
608
|
+
selectedCameraId: null
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
/** Chọn một ống kính cụ thể từ snapshot.cameras. */
|
|
612
|
+
async selectCamera(deviceId) {
|
|
613
|
+
if (!this.snapshot.previewTrack) return;
|
|
614
|
+
if (deviceId === this.snapshot.selectedCameraId) return;
|
|
615
|
+
const camera = this.snapshot.cameras.find(
|
|
616
|
+
(item) => item.deviceId === deviceId
|
|
617
|
+
);
|
|
618
|
+
if (!camera) return;
|
|
619
|
+
const room = this.requireRoom();
|
|
620
|
+
if (!room.selectCamera) return;
|
|
621
|
+
const generation = this.operationGeneration;
|
|
622
|
+
await room.selectCamera(camera.deviceId, camera.facing);
|
|
623
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
624
|
+
this.update({
|
|
625
|
+
selectedCameraId: camera.deviceId,
|
|
626
|
+
...camera.facing === "unknown" ? {} : { isCameraFront: camera.facing === "front" }
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
/** Chọn kiểu quay trước khi live; không đổi khi phiên đang hoạt động. */
|
|
630
|
+
async setCaptureOrientation(orientation) {
|
|
631
|
+
if (orientation === this.snapshot.captureOrientation) return;
|
|
632
|
+
if (this.snapshot.sessionId || activeStates.has(this.snapshot.state)) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const generation = this.operationGeneration;
|
|
636
|
+
if (this.snapshot.previewTrack) {
|
|
637
|
+
await _optionalChain([this, 'access', _10 => _10.requireRoom, 'call', _11 => _11(), 'access', _12 => _12.setCaptureOrientation, 'optionalCall', _13 => _13(orientation)]);
|
|
638
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
639
|
+
}
|
|
640
|
+
this.update({ captureOrientation: orientation });
|
|
641
|
+
void this.dependencies.storage.setItem(CAPTURE_ORIENTATION_KEY, orientation).catch(() => void 0);
|
|
642
|
+
}
|
|
643
|
+
async setMicrophoneEnabled(enabled) {
|
|
644
|
+
const generation = this.operationGeneration;
|
|
645
|
+
await this.requireRoom().setMicrophoneEnabled(enabled);
|
|
646
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
647
|
+
this.update({ isMicrophoneEnabled: enabled });
|
|
648
|
+
}
|
|
649
|
+
handleRoomEvent(event) {
|
|
650
|
+
if (this.disposed) return;
|
|
651
|
+
if (event.type === "command") {
|
|
652
|
+
if (event.action === "start") void this.startPublishing();
|
|
653
|
+
else if (event.action === "stop") void this.stopPublishing();
|
|
654
|
+
else if (event.action === "end") void this.stop("user");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (event.type === "reconnecting" && (this.snapshot.state === "live" || this.snapshot.state === "standby")) {
|
|
658
|
+
this.transition("reconnecting");
|
|
659
|
+
void this.sendTelemetry("reconnect", { phase: "started" });
|
|
660
|
+
if (_optionalChain([this, 'access', _14 => _14.room, 'optionalAccess', _15 => _15.needsManualReconnect])) {
|
|
661
|
+
this.clearReconnectTimer();
|
|
662
|
+
this.scheduleRedial(0);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const configured = _nullishCoalesce(_optionalChain([this, 'access', _16 => _16.snapshot, 'access', _17 => _17.config, 'optionalAccess', _18 => _18.reconnectTimeoutMs]), () => ( 3e4));
|
|
666
|
+
this.clearReconnectTimer();
|
|
667
|
+
this.reconnectTimer = setTimeout(() => {
|
|
668
|
+
void this.stop("network_timeout");
|
|
669
|
+
}, configured);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (event.type === "reconnected" && this.snapshot.state === "reconnecting") {
|
|
673
|
+
this.clearReconnectTimer();
|
|
674
|
+
this.stopRedialing();
|
|
675
|
+
this.transition(this.publishing ? "live" : "standby");
|
|
676
|
+
void this.sendTelemetry("reconnect", { phase: "recovered" });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (event.type === "media-error") {
|
|
680
|
+
if (_optionalChain([this, 'access', _19 => _19.room, 'optionalAccess', _20 => _20.needsManualReconnect]) && activeStates.has(this.snapshot.state)) {
|
|
681
|
+
this.handleRoomEvent({ type: "reconnecting" });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.recordError(event.error);
|
|
685
|
+
}
|
|
686
|
+
if (event.type === "disconnected" && activeStates.has(this.snapshot.state)) {
|
|
687
|
+
this.handleRoomEvent({ type: "reconnecting" });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
handleAppState(state) {
|
|
691
|
+
if (this.disposed) return;
|
|
692
|
+
this.latestAppState = state;
|
|
693
|
+
if (state === "active") {
|
|
694
|
+
if (this.backgroundTimer) clearTimeout(this.backgroundTimer);
|
|
695
|
+
this.backgroundTimer = void 0;
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
this.ensureBackgroundGraceTimer();
|
|
699
|
+
}
|
|
700
|
+
ensureBackgroundGraceTimer() {
|
|
701
|
+
if (this.latestAppState === "active" || !["connecting", "standby", "live", "reconnecting"].includes(
|
|
702
|
+
this.snapshot.state
|
|
703
|
+
) || this.backgroundTimer) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const grace = _nullishCoalesce(_optionalChain([this, 'access', _21 => _21.snapshot, 'access', _22 => _22.config, 'optionalAccess', _23 => _23.backgroundGraceMs]), () => ( 15e3));
|
|
707
|
+
this.backgroundTimer = setTimeout(() => {
|
|
708
|
+
this.backgroundTimer = void 0;
|
|
709
|
+
if (this.latestAppState !== "active") {
|
|
710
|
+
void this.stop("background_timeout");
|
|
711
|
+
}
|
|
712
|
+
}, grace);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* `keepSession` = engine bị tháo nhưng phiên phải sống tiếp để lần mở app sau
|
|
716
|
+
* nhận lại. Mặc định false: giữ NGUYÊN hành vi cũ cho mọi SDK đối tác.
|
|
717
|
+
*
|
|
718
|
+
* Lưu ý dispose KHÔNG BAO GIỜ là đường của "người dùng bấm dừng" — nút dừng đi
|
|
719
|
+
* qua doStop, mà doStop xoá ownedSessionId trước, nên tới đây sessionId đã là
|
|
720
|
+
* undefined. Vì vậy nhánh end ở đây luôn là teardown NGOÀI Ý MUỐN (app bị giết,
|
|
721
|
+
* component remount), đúng chỗ cần giữ phiên.
|
|
722
|
+
*/
|
|
723
|
+
dispose(options) {
|
|
724
|
+
if (this.disposePromise) return this.disposePromise;
|
|
725
|
+
if (this.disposed) return Promise.resolve();
|
|
726
|
+
this.disposed = true;
|
|
727
|
+
this.operationGeneration += 1;
|
|
728
|
+
const sessionId = _optionalChain([options, 'optionalAccess', _24 => _24.keepSession]) === true ? void 0 : this.ownedSessionId;
|
|
729
|
+
let releaseEnd = () => void 0;
|
|
730
|
+
const endBarrier = new Promise((resolve) => {
|
|
731
|
+
releaseEnd = resolve;
|
|
732
|
+
});
|
|
733
|
+
const termination = sessionId ? this.ensureSessionTermination(sessionId, "error", endBarrier) : void 0;
|
|
734
|
+
const roomCleanup = this.releaseRoom(_optionalChain([termination, 'optionalAccess', _25 => _25.markerReady]));
|
|
735
|
+
this.clearTimers();
|
|
736
|
+
_optionalChain([this, 'access', _26 => _26.dependencies, 'access', _27 => _27.telemetry, 'optionalAccess', _28 => _28.dispose, 'optionalCall', _29 => _29()]);
|
|
737
|
+
_optionalChain([this, 'access', _30 => _30.dependencies, 'access', _31 => _31.commandChannel, 'optionalAccess', _32 => _32.dispose, 'optionalCall', _33 => _33()]);
|
|
738
|
+
this.pendingCriticalTelemetry = void 0;
|
|
739
|
+
this.networkUnsubscribe();
|
|
740
|
+
this.listeners.clear();
|
|
741
|
+
this.keepAwakeActive = false;
|
|
742
|
+
this.grant = void 0;
|
|
743
|
+
this.ownedSessionId = void 0;
|
|
744
|
+
this.disposePromise = (async () => {
|
|
745
|
+
await _optionalChain([termination, 'optionalAccess', _34 => _34.markerReady]);
|
|
746
|
+
await Promise.allSettled([
|
|
747
|
+
roomCleanup,
|
|
748
|
+
this.dependencies.runtime.deactivateKeepAwake()
|
|
749
|
+
]);
|
|
750
|
+
releaseEnd();
|
|
751
|
+
await _optionalChain([termination, 'optionalAccess', _35 => _35.completion]);
|
|
752
|
+
})();
|
|
753
|
+
return this.disposePromise;
|
|
754
|
+
}
|
|
755
|
+
async replayPendingEnd() {
|
|
756
|
+
if (this.ownsInMemorySession()) return;
|
|
757
|
+
if (this.pendingReplayPromise) return this.pendingReplayPromise;
|
|
758
|
+
this.pendingReplayPromise = this.doReplayPendingEnd().finally(() => {
|
|
759
|
+
this.pendingReplayPromise = void 0;
|
|
760
|
+
});
|
|
761
|
+
return this.pendingReplayPromise;
|
|
762
|
+
}
|
|
763
|
+
async doReplayPendingEnd() {
|
|
764
|
+
const pending = await this.readPendingEnd();
|
|
765
|
+
if (!pending || pending.pendingEnd === null) return;
|
|
766
|
+
if (!this.dependencies.network.isOnline()) return;
|
|
767
|
+
await this.dependencies.sessionProvider.end({
|
|
768
|
+
sessionId: pending.sessionId,
|
|
769
|
+
reason: _nullishCoalesce(pending.pendingEnd, () => ( "error"))
|
|
770
|
+
});
|
|
771
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
772
|
+
this.pendingReplayError = void 0;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Phiên tưởng đáng nhận lại hoá ra đã chết phía server → BỎ dấu để máy arm
|
|
776
|
+
* phiên mới được.
|
|
777
|
+
*
|
|
778
|
+
* Thiếu hàm này là máy kẹt cứng: dấu vẫn nằm đó chặn mọi lượt start (đúng vai
|
|
779
|
+
* trò chống phiên song song của nó), mà lượt start duy nhất được cho qua lại là
|
|
780
|
+
* lượt nhận lại chính cái phiên đã chết — không bao giờ thành công. Đã dính
|
|
781
|
+
* thật 18/08/2026: lưới standby 30 phút kết thúc phiên trong lúc máy ngủ, mở
|
|
782
|
+
* lại thì app đứng ở "error" vĩnh viễn.
|
|
783
|
+
*/
|
|
784
|
+
async abandonAdoptableSession(sessionId) {
|
|
785
|
+
if (this.snapshot.adoptableSessionId !== sessionId) return;
|
|
786
|
+
try {
|
|
787
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
788
|
+
} catch (e3) {
|
|
789
|
+
}
|
|
790
|
+
this.update({ adoptableSessionId: null });
|
|
791
|
+
}
|
|
792
|
+
async recoverStartupSession() {
|
|
793
|
+
let pending = await this.readPendingEnd();
|
|
794
|
+
if (!pending || this.ownsInMemorySession()) return;
|
|
795
|
+
if (pending.pendingEnd === null) {
|
|
796
|
+
const keep = await _optionalChain([this, 'access', _36 => _36.dependencies, 'access', _37 => _37.keepSessionOnRestart, 'optionalCall', _38 => _38()]) === true;
|
|
797
|
+
console.warn(
|
|
798
|
+
`[picklive] khoi dong: phien ${pending.sessionId} chua duoc dung co chu y -> ${keep ? "GIU de nhan lai" : "ket thuc"}`
|
|
799
|
+
);
|
|
800
|
+
if (keep) {
|
|
801
|
+
this.update({ adoptableSessionId: pending.sessionId });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
pending = { sessionId: pending.sessionId, pendingEnd: "error" };
|
|
805
|
+
await this.dependencies.storage.setItem(
|
|
806
|
+
PENDING_END_KEY,
|
|
807
|
+
JSON.stringify(pending)
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if (!this.dependencies.network.isOnline()) return;
|
|
811
|
+
try {
|
|
812
|
+
await this.dependencies.sessionProvider.end({
|
|
813
|
+
sessionId: pending.sessionId,
|
|
814
|
+
reason: _nullishCoalesce(pending.pendingEnd, () => ( "error"))
|
|
815
|
+
});
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (!isSessionGoneError(error)) throw error;
|
|
818
|
+
}
|
|
819
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
820
|
+
this.pendingReplayError = void 0;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Dấu phiên cũ chặn thao tác mới cho tới khi nó được giải quyết.
|
|
824
|
+
*
|
|
825
|
+
* `externalSessionId` = id lượt start đang xin đi qua; bỏ trống nghĩa là
|
|
826
|
+
* prepare (mở camera), vốn phải luôn được phép khi đang định nhận lại — không
|
|
827
|
+
* mở được camera thì không nhận lại được, và đó chính là bẫy làm máy kẹt cứng ở
|
|
828
|
+
* màn lỗi "The previous livestream is still pending end".
|
|
829
|
+
*/
|
|
830
|
+
async ensurePendingEndResolved(externalSessionId) {
|
|
831
|
+
await this.initialize();
|
|
832
|
+
const pending = await this.readPendingEnd();
|
|
833
|
+
if (!pending) return;
|
|
834
|
+
if (pending.pendingEnd === null && this.snapshot.adoptableSessionId === pending.sessionId && (externalSessionId === void 0 || externalSessionId === `resume-${pending.sessionId}`)) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (!this.dependencies.network.isOnline()) {
|
|
838
|
+
throw new NetworkUnavailableError(
|
|
839
|
+
"Connect to the network to finish the previous livestream"
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
if (this.pendingReplayError instanceof TypeError) {
|
|
843
|
+
throw new NetworkUnavailableError(void 0, {
|
|
844
|
+
cause: this.pendingReplayError
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (this.snapshot.state === "connecting" && !this.ownedSessionId && pending.pendingEnd !== null) {
|
|
848
|
+
try {
|
|
849
|
+
await this.dependencies.sessionProvider.end({
|
|
850
|
+
sessionId: pending.sessionId,
|
|
851
|
+
reason: pending.pendingEnd
|
|
852
|
+
});
|
|
853
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
854
|
+
this.pendingReplayError = void 0;
|
|
855
|
+
return;
|
|
856
|
+
} catch (error) {
|
|
857
|
+
this.pendingReplayError = error;
|
|
858
|
+
if (error instanceof TypeError) {
|
|
859
|
+
throw new NetworkUnavailableError(void 0, { cause: error });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
throw new SessionConflictError(
|
|
864
|
+
pending.pendingEnd === null ? "Another livestream session is still active" : "The previous livestream is still pending end"
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
async readPendingEnd() {
|
|
868
|
+
const raw = await this.dependencies.storage.getItem(PENDING_END_KEY);
|
|
869
|
+
if (!raw) return null;
|
|
870
|
+
try {
|
|
871
|
+
const value = JSON.parse(raw);
|
|
872
|
+
if (typeof value.sessionId !== "string") throw new TypeError();
|
|
873
|
+
if (value.pendingEnd === null) {
|
|
874
|
+
return { sessionId: value.sessionId, pendingEnd: null };
|
|
875
|
+
}
|
|
876
|
+
if ([
|
|
877
|
+
"user",
|
|
878
|
+
"background_timeout",
|
|
879
|
+
"network_timeout",
|
|
880
|
+
"standby_timeout",
|
|
881
|
+
"error"
|
|
882
|
+
].includes(String(value.pendingEnd))) {
|
|
883
|
+
return {
|
|
884
|
+
sessionId: value.sessionId,
|
|
885
|
+
pendingEnd: value.pendingEnd
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
} catch (e4) {
|
|
889
|
+
}
|
|
890
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
ownsInMemorySession() {
|
|
894
|
+
return Boolean(this.ownedSessionId) || inMemorySessionStates.has(this.snapshot.state);
|
|
895
|
+
}
|
|
896
|
+
startTimers() {
|
|
897
|
+
if (this.liveTimersStarted) return;
|
|
898
|
+
this.liveTimersStarted = true;
|
|
899
|
+
const interval = Math.max(
|
|
900
|
+
1e4,
|
|
901
|
+
_nullishCoalesce(_optionalChain([this, 'access', _39 => _39.snapshot, 'access', _40 => _40.config, 'optionalAccess', _41 => _41.telemetryIntervalMs]), () => ( 6e4))
|
|
902
|
+
);
|
|
903
|
+
this.heartbeatTimer = setInterval(() => {
|
|
904
|
+
void this.sendHeartbeat();
|
|
905
|
+
}, interval);
|
|
906
|
+
_optionalChain([this, 'access', _42 => _42.heartbeatTimer, 'access', _43 => _43.unref, 'optionalCall', _44 => _44()]);
|
|
907
|
+
this.elapsedTimer = setInterval(() => {
|
|
908
|
+
if (this.startedAt !== void 0) {
|
|
909
|
+
this.update({ elapsedMs: this.now() - this.startedAt });
|
|
910
|
+
}
|
|
911
|
+
}, 1e3);
|
|
912
|
+
_optionalChain([this, 'access', _45 => _45.elapsedTimer, 'access', _46 => _46.unref, 'optionalCall', _47 => _47()]);
|
|
913
|
+
this.scheduleTokenRefresh();
|
|
914
|
+
this.scheduleMaxSessionDuration();
|
|
915
|
+
}
|
|
916
|
+
clearTimers() {
|
|
917
|
+
if (this.backgroundTimer) clearTimeout(this.backgroundTimer);
|
|
918
|
+
this.clearStandbyTimer();
|
|
919
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
920
|
+
if (this.elapsedTimer) clearInterval(this.elapsedTimer);
|
|
921
|
+
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
922
|
+
if (this.maxSessionTimer) clearTimeout(this.maxSessionTimer);
|
|
923
|
+
this.clearReconnectTimer();
|
|
924
|
+
this.stopCommandPolling();
|
|
925
|
+
this.stopCommandSubscription();
|
|
926
|
+
this.backgroundTimer = void 0;
|
|
927
|
+
this.heartbeatTimer = void 0;
|
|
928
|
+
this.elapsedTimer = void 0;
|
|
929
|
+
this.tokenRefreshTimer = void 0;
|
|
930
|
+
this.maxSessionTimer = void 0;
|
|
931
|
+
this.sessionDeadline = void 0;
|
|
932
|
+
this.liveTimersStarted = false;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Quay số lại với backoff cho adapter cần reconnect thủ công. Mỗi nhịp gọi
|
|
936
|
+
* retry() (refresh credentials + room.connect + enterSession). Thành công →
|
|
937
|
+
* enterSession phát "publishing" → nhánh "reconnected" dọn cả hai timer.
|
|
938
|
+
* Thất bại → doRetry() đưa state về "error" (vẫn còn sessionId) và nhịp sau
|
|
939
|
+
* retry() lại kéo về "reconnecting" — lặp tới khi doom timer 30s kết liễu.
|
|
940
|
+
* Backoff thưa dần để không dội server khi ingest thật sự sập.
|
|
941
|
+
*/
|
|
942
|
+
scheduleRedial(attempt) {
|
|
943
|
+
this.clearRedialTimer();
|
|
944
|
+
this.redialing = true;
|
|
945
|
+
if (!this.dependencies.network.isOnline()) return;
|
|
946
|
+
const delays = [0, 1e3, 2e3, 4e3, 8e3, 15e3, 3e4];
|
|
947
|
+
const delay = delays[Math.min(attempt, delays.length - 1)];
|
|
948
|
+
this.redialTimer = setTimeout(() => {
|
|
949
|
+
if (this.disposed) return;
|
|
950
|
+
const s = this.snapshot.state;
|
|
951
|
+
if (s !== "reconnecting" && !(s === "error" && this.snapshot.sessionId)) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
void this.retry().catch(() => {
|
|
955
|
+
}).finally(() => {
|
|
956
|
+
if (this.disposed) return;
|
|
957
|
+
const after = this.snapshot.state;
|
|
958
|
+
if (after === "reconnecting" || after === "error" && this.snapshot.sessionId) {
|
|
959
|
+
this.scheduleRedial(attempt + 1);
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}, delay);
|
|
963
|
+
}
|
|
964
|
+
clearRedialTimer() {
|
|
965
|
+
if (this.redialTimer) clearTimeout(this.redialTimer);
|
|
966
|
+
this.redialTimer = void 0;
|
|
967
|
+
}
|
|
968
|
+
stopRedialing() {
|
|
969
|
+
this.redialing = false;
|
|
970
|
+
this.clearRedialTimer();
|
|
971
|
+
}
|
|
972
|
+
clearReconnectTimer() {
|
|
973
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
974
|
+
this.reconnectTimer = void 0;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Poll lệnh chỉ khi phiên là device-rtmp (grant rtmp://) và đang standby/
|
|
978
|
+
* live. Ngoài hai trạng thái đó không có gì để điều khiển: connecting chưa
|
|
979
|
+
* có phiên chạy, reconnecting đã có reconnect timer lo, ending/ended thì thôi.
|
|
980
|
+
*/
|
|
981
|
+
commandPollingWanted() {
|
|
982
|
+
const grant = this.grant;
|
|
983
|
+
return Boolean(
|
|
984
|
+
this.dependencies.commandChannel && grant && /^rtmps?:\/\//i.test(grant.serverUrl) && // "reconnecting" PHẢI có trong danh sách này. Sau khi bỏ trần 75 giây,
|
|
985
|
+
// máy có thể quay số hàng giờ; nếu trong lúc đó trọng tài kết thúc trận
|
|
986
|
+
// hoặc nhả sân, đây là đường duy nhất để máy biết mà thôi quay số vào một
|
|
987
|
+
// phiên đã chết (poll trả status "ended" → nhánh ở pollSessionCommands).
|
|
988
|
+
// Trước đây chỉ poll ở standby/live nên máy mất mạng là mù hoàn toàn.
|
|
989
|
+
activeStates.has(this.snapshot.state)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
syncCommandPolling() {
|
|
993
|
+
if (this.disposed || !this.commandPollingWanted()) {
|
|
994
|
+
this.stopCommandPolling();
|
|
995
|
+
this.stopCommandSubscription();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
this.syncCommandSubscription();
|
|
999
|
+
if (!this.commandPollTimer) this.scheduleCommandPoll(0);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Mở/đổi websocket nhận lệnh cho phiên hiện tại. Chỉ máy first-party có
|
|
1003
|
+
* dependency này (đối tác qua proxy thì undefined → chỉ poll). Resubscribe
|
|
1004
|
+
* khi đổi phiên HOẶC token (refresh) vì cả hai đi vào khoá.
|
|
1005
|
+
*/
|
|
1006
|
+
syncCommandSubscription() {
|
|
1007
|
+
const subscription = this.dependencies.commandSubscription;
|
|
1008
|
+
const grant = this.grant;
|
|
1009
|
+
if (!subscription || !grant) return;
|
|
1010
|
+
const key = `${grant.sessionId}:${grant.telemetry.token}`;
|
|
1011
|
+
if (this.commandSubscriptionKey === key) return;
|
|
1012
|
+
this.stopCommandSubscription();
|
|
1013
|
+
this.commandSubscriptionKey = key;
|
|
1014
|
+
const sessionId = grant.sessionId;
|
|
1015
|
+
this.commandUnsubscribe = subscription.subscribe(
|
|
1016
|
+
grant.telemetry,
|
|
1017
|
+
sessionId,
|
|
1018
|
+
(commands) => {
|
|
1019
|
+
if (this.disposed || _optionalChain([this, 'access', _48 => _48.grant, 'optionalAccess', _49 => _49.sessionId]) !== sessionId) return;
|
|
1020
|
+
this.commandSubscriptionActive = true;
|
|
1021
|
+
this.applyCommands(commands);
|
|
1022
|
+
},
|
|
1023
|
+
() => {
|
|
1024
|
+
this.commandSubscriptionActive = false;
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
stopCommandSubscription() {
|
|
1029
|
+
if (this.commandUnsubscribe) {
|
|
1030
|
+
try {
|
|
1031
|
+
this.commandUnsubscribe();
|
|
1032
|
+
} catch (e5) {
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
this.commandUnsubscribe = void 0;
|
|
1036
|
+
this.commandSubscriptionKey = void 0;
|
|
1037
|
+
this.commandSubscriptionActive = false;
|
|
1038
|
+
}
|
|
1039
|
+
stopCommandPolling() {
|
|
1040
|
+
if (this.commandPollTimer) clearTimeout(this.commandPollTimer);
|
|
1041
|
+
this.commandPollTimer = void 0;
|
|
1042
|
+
this.commandPollBackoffMs = 0;
|
|
1043
|
+
}
|
|
1044
|
+
scheduleCommandPoll(delayMs) {
|
|
1045
|
+
this.commandPollTimer = setTimeout(() => {
|
|
1046
|
+
this.commandPollTimer = void 0;
|
|
1047
|
+
void this.pollSessionCommands();
|
|
1048
|
+
}, delayMs);
|
|
1049
|
+
_optionalChain([this, 'access', _50 => _50.commandPollTimer, 'access', _51 => _51.unref, 'optionalCall', _52 => _52()]);
|
|
1050
|
+
}
|
|
1051
|
+
commandPollInterval() {
|
|
1052
|
+
if (this.commandSubscriptionActive) return COMMAND_POLL_RECONCILE_MS;
|
|
1053
|
+
return this.snapshot.state === "live" ? COMMAND_POLL_LIVE_MS : COMMAND_POLL_STANDBY_MS;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Áp lệnh từ poll HOẶC subscription vào cùng một state machine. Subscription
|
|
1057
|
+
* trả cả lệnh CŨ (after:0) nên phải lọc theo con trỏ ở đây; poll đã lọc
|
|
1058
|
+
* server-side (after=cursor) nên bước lọc này vô hại với nó.
|
|
1059
|
+
*/
|
|
1060
|
+
applyCommands(commands) {
|
|
1061
|
+
for (const command of commands) {
|
|
1062
|
+
if (command.issuedAt <= this.commandCursor) continue;
|
|
1063
|
+
this.commandCursor = command.issuedAt;
|
|
1064
|
+
this.handleRoomEvent({ type: "command", action: command.action });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async pollSessionCommands() {
|
|
1068
|
+
if (!this.commandPollingWanted()) return;
|
|
1069
|
+
const channel = this.dependencies.commandChannel;
|
|
1070
|
+
const grant = this.grant;
|
|
1071
|
+
if (!channel || !grant) return;
|
|
1072
|
+
const sessionId = grant.sessionId;
|
|
1073
|
+
let result = null;
|
|
1074
|
+
try {
|
|
1075
|
+
result = await channel.poll(grant.telemetry, this.commandCursor);
|
|
1076
|
+
this.commandPollBackoffMs = 0;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
const status = _optionalChain([error, 'optionalAccess', _53 => _53.status]);
|
|
1079
|
+
if (status === 401) {
|
|
1080
|
+
void this.refreshCredentials().catch(() => void 0);
|
|
1081
|
+
}
|
|
1082
|
+
if (status === 409 && _optionalChain([this, 'access', _54 => _54.grant, 'optionalAccess', _55 => _55.sessionId]) === sessionId) {
|
|
1083
|
+
void this.stop("user");
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
this.commandPollBackoffMs = Math.min(
|
|
1087
|
+
COMMAND_POLL_MAX_BACKOFF_MS,
|
|
1088
|
+
Math.max(this.commandPollInterval(), this.commandPollBackoffMs * 2)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
if (this.disposed || _optionalChain([this, 'access', _56 => _56.grant, 'optionalAccess', _57 => _57.sessionId]) !== sessionId) return;
|
|
1092
|
+
if (result) {
|
|
1093
|
+
this.applyCommands(result.commands);
|
|
1094
|
+
if (result.commands.length === 0) {
|
|
1095
|
+
if (result.status === "ended" && activeStates.has(this.snapshot.state)) {
|
|
1096
|
+
void this.stop("user");
|
|
1097
|
+
} else if (result.status === "live" && this.snapshot.state === "standby") {
|
|
1098
|
+
void this.startPublishing();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (!this.commandPollingWanted() || this.commandPollTimer) return;
|
|
1103
|
+
this.scheduleCommandPoll(
|
|
1104
|
+
this.commandPollBackoffMs || this.commandPollInterval()
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
/** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
|
|
1108
|
+
async sendHeartbeat() {
|
|
1109
|
+
let metrics = null;
|
|
1110
|
+
try {
|
|
1111
|
+
metrics = await _asyncNullishCoalesce(await _optionalChain([this, 'access', _58 => _58.dependencies, 'access', _59 => _59.deviceMetrics, 'optionalCall', _60 => _60()]), async () => ( null));
|
|
1112
|
+
} catch (e6) {
|
|
1113
|
+
metrics = null;
|
|
1114
|
+
}
|
|
1115
|
+
if (this.disposed) return;
|
|
1116
|
+
if (metrics) this.update({ deviceMetrics: metrics });
|
|
1117
|
+
return this.sendTelemetry(
|
|
1118
|
+
"heartbeat",
|
|
1119
|
+
metrics ? { ...metrics } : void 0
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
async sendTelemetry(name, data) {
|
|
1123
|
+
if (this.disposed || !this.dependencies.telemetry || !this.grant) return;
|
|
1124
|
+
const grant = this.grant;
|
|
1125
|
+
const pending = {
|
|
1126
|
+
credentials: grant.telemetry,
|
|
1127
|
+
event: {
|
|
1128
|
+
sessionId: grant.sessionId,
|
|
1129
|
+
at: this.now(),
|
|
1130
|
+
name,
|
|
1131
|
+
...data ? { data } : {}
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
if (this.telemetryInFlight) {
|
|
1135
|
+
if (name === "session_ended" && !this.pendingCriticalTelemetry) {
|
|
1136
|
+
this.pendingCriticalTelemetry = pending;
|
|
1137
|
+
}
|
|
1138
|
+
return this.telemetryInFlight;
|
|
1139
|
+
}
|
|
1140
|
+
return this.startTelemetrySend(pending);
|
|
1141
|
+
}
|
|
1142
|
+
startTelemetrySend(pending) {
|
|
1143
|
+
const operation = (async () => {
|
|
1144
|
+
try {
|
|
1145
|
+
await _optionalChain([this, 'access', _61 => _61.dependencies, 'access', _62 => _62.telemetry, 'optionalAccess', _63 => _63.send, 'call', _64 => _64(pending.credentials, [
|
|
1146
|
+
pending.event
|
|
1147
|
+
])]);
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
if (_optionalChain([error, 'optionalAccess', _65 => _65.status]) === 401) {
|
|
1150
|
+
try {
|
|
1151
|
+
await this.refreshCredentials();
|
|
1152
|
+
if (this.grant) {
|
|
1153
|
+
await _optionalChain([this, 'access', _66 => _66.dependencies, 'access', _67 => _67.telemetry, 'optionalAccess', _68 => _68.send, 'call', _69 => _69(this.grant.telemetry, [
|
|
1154
|
+
pending.event
|
|
1155
|
+
])]);
|
|
1156
|
+
}
|
|
1157
|
+
} catch (e7) {
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
})();
|
|
1162
|
+
const tracked = operation.finally(() => {
|
|
1163
|
+
if (this.telemetryInFlight !== tracked) return;
|
|
1164
|
+
this.telemetryInFlight = void 0;
|
|
1165
|
+
const critical = this.pendingCriticalTelemetry;
|
|
1166
|
+
this.pendingCriticalTelemetry = void 0;
|
|
1167
|
+
if (critical && !this.disposed) this.startTelemetrySend(critical);
|
|
1168
|
+
});
|
|
1169
|
+
this.telemetryInFlight = tracked;
|
|
1170
|
+
return this.telemetryInFlight;
|
|
1171
|
+
}
|
|
1172
|
+
refreshCredentials(generation = this.operationGeneration) {
|
|
1173
|
+
if (this.credentialRefreshPromise) return this.credentialRefreshPromise;
|
|
1174
|
+
const sessionId = this.snapshot.sessionId;
|
|
1175
|
+
if (!sessionId || !this.grant) return Promise.resolve();
|
|
1176
|
+
this.credentialRefreshPromise = this.dependencies.sessionProvider.refresh(sessionId).then((refreshed) => {
|
|
1177
|
+
this.ensureOperation(generation);
|
|
1178
|
+
if (!isSdkRemoteConfig(refreshed.config)) {
|
|
1179
|
+
throw new InvalidResponseError(
|
|
1180
|
+
"The session provider returned invalid SDK config"
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
if (!this.grant || this.snapshot.sessionId !== sessionId || !activeStates.has(this.snapshot.state)) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
this.grant = {
|
|
1187
|
+
...this.grant,
|
|
1188
|
+
participantToken: _nullishCoalesce(refreshed.participantToken, () => ( this.grant.participantToken)),
|
|
1189
|
+
tokenExpiresAt: _nullishCoalesce(refreshed.tokenExpiresAt, () => ( this.grant.tokenExpiresAt)),
|
|
1190
|
+
telemetry: _nullishCoalesce(refreshed.telemetry, () => ( this.grant.telemetry)),
|
|
1191
|
+
config: refreshed.config
|
|
1192
|
+
};
|
|
1193
|
+
this.syncCommandSubscription();
|
|
1194
|
+
this.update({ config: refreshed.config });
|
|
1195
|
+
this.refreshRetryAttempt = 0;
|
|
1196
|
+
if (this.liveTimersStarted) {
|
|
1197
|
+
this.restartHeartbeat();
|
|
1198
|
+
this.scheduleTokenRefresh();
|
|
1199
|
+
this.scheduleMaxSessionDuration();
|
|
1200
|
+
}
|
|
1201
|
+
}).finally(() => {
|
|
1202
|
+
this.credentialRefreshPromise = void 0;
|
|
1203
|
+
});
|
|
1204
|
+
return this.credentialRefreshPromise;
|
|
1205
|
+
}
|
|
1206
|
+
transition(state) {
|
|
1207
|
+
const previous = this.snapshot.state;
|
|
1208
|
+
if (previous === state) return;
|
|
1209
|
+
if (!LEGAL_LIVESTREAM_TRANSITIONS[previous].includes(state)) {
|
|
1210
|
+
throw new SessionConflictError(
|
|
1211
|
+
`Illegal livestream transition: ${previous} -> ${state}`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
this.update({ state, error: state === "error" ? this.snapshot.error : null });
|
|
1215
|
+
if (state === "standby") this.startStandbyTimer();
|
|
1216
|
+
else this.clearStandbyTimer();
|
|
1217
|
+
this.syncCommandPolling();
|
|
1218
|
+
if (this.snapshot.sessionId) {
|
|
1219
|
+
void this.sendTelemetry("state_changed", { from: previous, to: state });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
startStandbyTimer() {
|
|
1223
|
+
this.clearStandbyTimer();
|
|
1224
|
+
const timeout = _nullishCoalesce(_optionalChain([this, 'access', _70 => _70.snapshot, 'access', _71 => _71.config, 'optionalAccess', _72 => _72.standbyTimeoutMs]), () => ( DEFAULT_STANDBY_TIMEOUT_MS));
|
|
1225
|
+
this.standbyTimer = setTimeout(() => {
|
|
1226
|
+
this.standbyTimer = void 0;
|
|
1227
|
+
if (this.snapshot.state === "standby") {
|
|
1228
|
+
void this.stop("standby_timeout");
|
|
1229
|
+
}
|
|
1230
|
+
}, timeout);
|
|
1231
|
+
_optionalChain([this, 'access', _73 => _73.standbyTimer, 'access', _74 => _74.unref, 'optionalCall', _75 => _75()]);
|
|
1232
|
+
}
|
|
1233
|
+
clearStandbyTimer() {
|
|
1234
|
+
if (this.standbyTimer) clearTimeout(this.standbyTimer);
|
|
1235
|
+
this.standbyTimer = void 0;
|
|
1236
|
+
}
|
|
1237
|
+
fail(error) {
|
|
1238
|
+
const normalized = this.recordError(error);
|
|
1239
|
+
throw normalized;
|
|
1240
|
+
}
|
|
1241
|
+
recordError(error) {
|
|
1242
|
+
const normalized = normalizeError(error);
|
|
1243
|
+
const state = this.snapshot.state;
|
|
1244
|
+
if (state !== "error" && !LEGAL_LIVESTREAM_TRANSITIONS[state].includes("error")) {
|
|
1245
|
+
return normalized;
|
|
1246
|
+
}
|
|
1247
|
+
this.update({ error: normalized });
|
|
1248
|
+
this.transition("error");
|
|
1249
|
+
void this.sendTelemetry("error", { code: normalized.code });
|
|
1250
|
+
return normalized;
|
|
1251
|
+
}
|
|
1252
|
+
update(patch) {
|
|
1253
|
+
if (this.disposed) return;
|
|
1254
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
1255
|
+
for (const listener of this.listeners) listener();
|
|
1256
|
+
}
|
|
1257
|
+
ensureState(expected) {
|
|
1258
|
+
if (this.snapshot.state !== expected) {
|
|
1259
|
+
throw new Error(`Expected livestream state ${expected}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
ensureNotDisposed() {
|
|
1263
|
+
if (this.disposed) throw new Error("Livestream engine has been disposed");
|
|
1264
|
+
}
|
|
1265
|
+
ensureRoom() {
|
|
1266
|
+
if (!this.room) {
|
|
1267
|
+
this.room = this.dependencies.roomFactory();
|
|
1268
|
+
this.roomUnsubscribe = this.room.subscribe(
|
|
1269
|
+
(event) => this.handleRoomEvent(event)
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
return this.room;
|
|
1273
|
+
}
|
|
1274
|
+
ensureNetworkSubscription() {
|
|
1275
|
+
if (this.networkSubscribed || this.disposed) return;
|
|
1276
|
+
this.networkSubscribed = true;
|
|
1277
|
+
this.networkUnsubscribe = this.dependencies.network.subscribe((online) => {
|
|
1278
|
+
if (this.disposed) return;
|
|
1279
|
+
this.update({ isOnline: online });
|
|
1280
|
+
if (online && this.startupRecovered) {
|
|
1281
|
+
void this.replayPendingEnd().catch((error) => {
|
|
1282
|
+
this.pendingReplayError = error;
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (online && _optionalChain([this, 'access', _76 => _76.room, 'optionalAccess', _77 => _77.needsManualReconnect])) {
|
|
1286
|
+
const s = this.snapshot.state;
|
|
1287
|
+
if (s === "reconnecting" || s === "error" && this.snapshot.sessionId) {
|
|
1288
|
+
this.scheduleRedial(0);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
requireRoom() {
|
|
1294
|
+
if (!this.room) throw new Error("Livestream Room has not been prepared");
|
|
1295
|
+
return this.room;
|
|
1296
|
+
}
|
|
1297
|
+
releaseRoom(beforeDispose = Promise.resolve()) {
|
|
1298
|
+
const unsubscribe = this.roomUnsubscribe;
|
|
1299
|
+
this.roomUnsubscribe = () => void 0;
|
|
1300
|
+
const room = this.room;
|
|
1301
|
+
this.room = void 0;
|
|
1302
|
+
try {
|
|
1303
|
+
unsubscribe();
|
|
1304
|
+
} catch (e8) {
|
|
1305
|
+
}
|
|
1306
|
+
if (!room) return Promise.resolve();
|
|
1307
|
+
return beforeDispose.then(() => room.dispose());
|
|
1308
|
+
}
|
|
1309
|
+
enqueueLifecycle(command) {
|
|
1310
|
+
this.commandCount += 1;
|
|
1311
|
+
let result;
|
|
1312
|
+
if (this.commandCount === 1) {
|
|
1313
|
+
try {
|
|
1314
|
+
result = Promise.resolve(command());
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
result = Promise.reject(error);
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
result = this.commandTail.then(command, command);
|
|
1320
|
+
}
|
|
1321
|
+
this.commandTail = result.then(
|
|
1322
|
+
() => void 0,
|
|
1323
|
+
() => void 0
|
|
1324
|
+
);
|
|
1325
|
+
return result.finally(() => {
|
|
1326
|
+
this.commandCount -= 1;
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
scheduleTokenRefresh(delayOverride) {
|
|
1330
|
+
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
1331
|
+
this.tokenRefreshTimer = void 0;
|
|
1332
|
+
if (!this.grant || !this.snapshot.sessionId) return;
|
|
1333
|
+
const horizon = Math.min(this.grant.tokenExpiresAt, this.grant.telemetry.expiresAt);
|
|
1334
|
+
const remaining = horizon - this.now();
|
|
1335
|
+
if (delayOverride === void 0 && remaining <= 6e4) return;
|
|
1336
|
+
const delay = _nullishCoalesce(delayOverride, () => ( Math.max(1e3, remaining - 6e4)));
|
|
1337
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
1338
|
+
void this.refreshCredentials().catch(() => {
|
|
1339
|
+
if (this.disposed) return;
|
|
1340
|
+
const retryDelay = Math.min(
|
|
1341
|
+
3e4,
|
|
1342
|
+
1e3 * 2 ** Math.min(this.refreshRetryAttempt, 5)
|
|
1343
|
+
);
|
|
1344
|
+
this.refreshRetryAttempt += 1;
|
|
1345
|
+
this.scheduleTokenRefresh(retryDelay);
|
|
1346
|
+
});
|
|
1347
|
+
}, delay);
|
|
1348
|
+
_optionalChain([this, 'access', _78 => _78.tokenRefreshTimer, 'access', _79 => _79.unref, 'optionalCall', _80 => _80()]);
|
|
1349
|
+
}
|
|
1350
|
+
scheduleMaxSessionDuration() {
|
|
1351
|
+
if (this.startedAt === void 0 || !this.snapshot.config) return;
|
|
1352
|
+
const proposed = this.startedAt + this.snapshot.config.maxSessionDurationMs;
|
|
1353
|
+
this.sessionDeadline = Math.min(_nullishCoalesce(this.sessionDeadline, () => ( proposed)), proposed);
|
|
1354
|
+
if (this.maxSessionTimer) clearTimeout(this.maxSessionTimer);
|
|
1355
|
+
const delay = Math.max(0, this.sessionDeadline - this.now());
|
|
1356
|
+
this.maxSessionTimer = setTimeout(() => {
|
|
1357
|
+
void this.stop("error");
|
|
1358
|
+
}, delay);
|
|
1359
|
+
_optionalChain([this, 'access', _81 => _81.maxSessionTimer, 'access', _82 => _82.unref, 'optionalCall', _83 => _83()]);
|
|
1360
|
+
}
|
|
1361
|
+
restartHeartbeat() {
|
|
1362
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1363
|
+
const interval = Math.max(
|
|
1364
|
+
1e4,
|
|
1365
|
+
_nullishCoalesce(_optionalChain([this, 'access', _84 => _84.snapshot, 'access', _85 => _85.config, 'optionalAccess', _86 => _86.telemetryIntervalMs]), () => ( 6e4))
|
|
1366
|
+
);
|
|
1367
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1368
|
+
void this.sendHeartbeat();
|
|
1369
|
+
}, interval);
|
|
1370
|
+
_optionalChain([this, 'access', _87 => _87.heartbeatTimer, 'access', _88 => _88.unref, 'optionalCall', _89 => _89()]);
|
|
1371
|
+
}
|
|
1372
|
+
async enterSession(generation, publish) {
|
|
1373
|
+
this.ensureOperation(generation);
|
|
1374
|
+
if (!this.keepAwakeActive) {
|
|
1375
|
+
await this.dependencies.runtime.activateKeepAwake();
|
|
1376
|
+
if (!this.isOperationCurrent(generation)) {
|
|
1377
|
+
await this.dependencies.runtime.deactivateKeepAwake();
|
|
1378
|
+
throw new StaleOperationError();
|
|
1379
|
+
}
|
|
1380
|
+
this.keepAwakeActive = true;
|
|
1381
|
+
}
|
|
1382
|
+
this.publishing = publish;
|
|
1383
|
+
if (publish && this.startedAt === void 0) {
|
|
1384
|
+
this.startedAt = this.now() - this.pausedElapsedMs;
|
|
1385
|
+
this.sessionDeadline = void 0;
|
|
1386
|
+
this.update({ elapsedMs: this.pausedElapsedMs });
|
|
1387
|
+
}
|
|
1388
|
+
this.startTimers();
|
|
1389
|
+
this.transition(publish ? "live" : "standby");
|
|
1390
|
+
this.ensureBackgroundGraceTimer();
|
|
1391
|
+
}
|
|
1392
|
+
/** Bắt đầu phát khi đang standby (từ nút trên app hoặc lệnh dashboard). */
|
|
1393
|
+
async startPublishing() {
|
|
1394
|
+
if (this.disposed || this.snapshot.state !== "standby") return;
|
|
1395
|
+
const generation = this.operationGeneration;
|
|
1396
|
+
try {
|
|
1397
|
+
const grant = this.grant;
|
|
1398
|
+
if (grant && this.dependencies.sessionProvider.publish) {
|
|
1399
|
+
await this.dependencies.sessionProvider.publish(grant.sessionId);
|
|
1400
|
+
if (!this.isOperationCurrent(generation) || this.snapshot.state !== "standby") {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const room = this.requireRoom();
|
|
1405
|
+
if (room.setPublishing) {
|
|
1406
|
+
await room.setPublishing(true);
|
|
1407
|
+
} else if (this.grant) {
|
|
1408
|
+
await room.connect({
|
|
1409
|
+
serverUrl: this.grant.serverUrl,
|
|
1410
|
+
participantToken: this.grant.participantToken,
|
|
1411
|
+
microphoneEnabled: this.snapshot.isMicrophoneEnabled,
|
|
1412
|
+
publish: true
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
1416
|
+
this.publishing = true;
|
|
1417
|
+
this.startedAt = this.now() - this.pausedElapsedMs;
|
|
1418
|
+
this.update({ elapsedMs: this.pausedElapsedMs });
|
|
1419
|
+
this.transition("live");
|
|
1420
|
+
void this.sendTelemetry("state_changed", { state: "live" });
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
this.recordError(error);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
/** 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. */
|
|
1429
|
+
async stopPublishing() {
|
|
1430
|
+
if (this.disposed || this.snapshot.state !== "live") return;
|
|
1431
|
+
const generation = this.operationGeneration;
|
|
1432
|
+
const room = this.requireRoom();
|
|
1433
|
+
if (room.setPublishing) {
|
|
1434
|
+
await room.setPublishing(false);
|
|
1435
|
+
} else {
|
|
1436
|
+
await room.disconnect(false);
|
|
1437
|
+
}
|
|
1438
|
+
if (!this.isOperationCurrent(generation)) return;
|
|
1439
|
+
this.publishing = false;
|
|
1440
|
+
this.pausedElapsedMs = this.startedAt !== void 0 ? this.now() - this.startedAt : 0;
|
|
1441
|
+
this.startedAt = void 0;
|
|
1442
|
+
this.update({ elapsedMs: this.pausedElapsedMs });
|
|
1443
|
+
this.transition("standby");
|
|
1444
|
+
void this.sendTelemetry("state_changed", { state: "standby" });
|
|
1445
|
+
const grant = this.grant;
|
|
1446
|
+
if (grant && this.dependencies.sessionProvider.unpublish) {
|
|
1447
|
+
await this.dependencies.sessionProvider.unpublish(grant.sessionId).catch(() => void 0);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
ensureSessionTermination(sessionId, reason, beforeEnd = Promise.resolve()) {
|
|
1451
|
+
const existing = this.sessionTerminations.get(sessionId);
|
|
1452
|
+
if (existing) return existing;
|
|
1453
|
+
let markerReady;
|
|
1454
|
+
try {
|
|
1455
|
+
markerReady = Promise.resolve(
|
|
1456
|
+
this.dependencies.storage.setItem(
|
|
1457
|
+
PENDING_END_KEY,
|
|
1458
|
+
JSON.stringify({ sessionId, pendingEnd: reason })
|
|
1459
|
+
)
|
|
1460
|
+
).catch(() => {
|
|
1461
|
+
});
|
|
1462
|
+
} catch (e9) {
|
|
1463
|
+
markerReady = Promise.resolve();
|
|
1464
|
+
}
|
|
1465
|
+
const completion = Promise.all([markerReady, beforeEnd]).then(async () => {
|
|
1466
|
+
try {
|
|
1467
|
+
await this.dependencies.sessionProvider.end({ sessionId, reason });
|
|
1468
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
1469
|
+
} catch (e10) {
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
const termination = { markerReady, completion };
|
|
1473
|
+
this.sessionTerminations.set(sessionId, termination);
|
|
1474
|
+
return termination;
|
|
1475
|
+
}
|
|
1476
|
+
async abandonGrant(grant) {
|
|
1477
|
+
if (this.ownedSessionId === grant.sessionId) {
|
|
1478
|
+
this.ownedSessionId = void 0;
|
|
1479
|
+
this.grant = void 0;
|
|
1480
|
+
}
|
|
1481
|
+
const termination = this.ensureSessionTermination(grant.sessionId, "error");
|
|
1482
|
+
const roomCleanup = this.releaseRoom(termination.markerReady);
|
|
1483
|
+
await termination.markerReady;
|
|
1484
|
+
if (this.disposed) {
|
|
1485
|
+
await Promise.allSettled([roomCleanup, termination.completion]);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
await Promise.allSettled([
|
|
1489
|
+
roomCleanup,
|
|
1490
|
+
this.dependencies.runtime.deactivateKeepAwake(),
|
|
1491
|
+
termination.completion
|
|
1492
|
+
]);
|
|
1493
|
+
}
|
|
1494
|
+
isOperationCurrent(generation) {
|
|
1495
|
+
return !this.disposed && generation === this.operationGeneration;
|
|
1496
|
+
}
|
|
1497
|
+
ensureOperation(generation) {
|
|
1498
|
+
if (!this.isOperationCurrent(generation)) throw new StaleOperationError();
|
|
1499
|
+
}
|
|
1500
|
+
}, _class);
|
|
1501
|
+
|
|
1502
|
+
// src/uplink-check.ts
|
|
1503
|
+
var VIDEO_BITRATE_BPS = Object.freeze({
|
|
1504
|
+
720: 35e5,
|
|
1505
|
+
1080: 6e6
|
|
1506
|
+
});
|
|
1507
|
+
var AUDIO_BITRATE_BPS = 128e3;
|
|
1508
|
+
var DEFAULT_UPLOAD_URL = "https://speed.cloudflare.com/__up";
|
|
1509
|
+
var WARMUP_BYTES = 512 * 1024;
|
|
1510
|
+
function requiredMbpsFor(settings) {
|
|
1511
|
+
return (VIDEO_BITRATE_BPS[settings.resolution] + AUDIO_BITRATE_BPS) / 1e6;
|
|
1512
|
+
}
|
|
1513
|
+
function verdictFor(uploadMbps, requiredMbps) {
|
|
1514
|
+
if (uploadMbps >= requiredMbps * 1.8) return "good";
|
|
1515
|
+
if (uploadMbps >= requiredMbps * 1.25) return "ok";
|
|
1516
|
+
return "low";
|
|
1517
|
+
}
|
|
1518
|
+
function chunk(bytes) {
|
|
1519
|
+
const buffer = new Uint8Array(bytes);
|
|
1520
|
+
for (let i = 0; i < bytes; i += 977) buffer[i] = i & 255;
|
|
1521
|
+
return buffer;
|
|
1522
|
+
}
|
|
1523
|
+
function resolveFetch(explicit) {
|
|
1524
|
+
if (explicit) return explicit;
|
|
1525
|
+
try {
|
|
1526
|
+
const expoFetch = _optionalChain([_chunk7OX2DSKIcjs.__require.call(void 0, "expo/fetch"), 'optionalAccess', _90 => _90.fetch]);
|
|
1527
|
+
if (typeof expoFetch === "function") return expoFetch;
|
|
1528
|
+
} catch (e11) {
|
|
1529
|
+
}
|
|
1530
|
+
return globalThis.fetch;
|
|
1531
|
+
}
|
|
1532
|
+
async function runUplinkCheck(options) {
|
|
1533
|
+
const fetchImplementation = resolveFetch(options.fetch);
|
|
1534
|
+
const uploadUrl = _nullishCoalesce(options.uploadUrl, () => ( DEFAULT_UPLOAD_URL));
|
|
1535
|
+
const measureMs = _nullishCoalesce(options.measureMs, () => ( 6e3));
|
|
1536
|
+
const totalTimeoutMs = _nullishCoalesce(options.totalTimeoutMs, () => ( 25e3));
|
|
1537
|
+
const chunkBytes = _nullishCoalesce(options.chunkBytes, () => ( 2 * 1024 * 1024));
|
|
1538
|
+
async function post(body2, timeoutMs) {
|
|
1539
|
+
const abort = new AbortController();
|
|
1540
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
1541
|
+
try {
|
|
1542
|
+
const response = await fetchImplementation(uploadUrl, {
|
|
1543
|
+
method: "POST",
|
|
1544
|
+
body: body2,
|
|
1545
|
+
signal: abort.signal
|
|
1546
|
+
});
|
|
1547
|
+
await response.text().catch(() => void 0);
|
|
1548
|
+
} finally {
|
|
1549
|
+
clearTimeout(timer);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async function measurePing(url) {
|
|
1553
|
+
let best = null;
|
|
1554
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1555
|
+
const abort = new AbortController();
|
|
1556
|
+
const timer = setTimeout(() => abort.abort(), 4e3);
|
|
1557
|
+
const started2 = Date.now();
|
|
1558
|
+
try {
|
|
1559
|
+
await fetchImplementation(url, { method: "HEAD", signal: abort.signal });
|
|
1560
|
+
const elapsed = Date.now() - started2;
|
|
1561
|
+
if (best === null || elapsed < best) best = elapsed;
|
|
1562
|
+
} catch (e12) {
|
|
1563
|
+
} finally {
|
|
1564
|
+
clearTimeout(timer);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return best;
|
|
1568
|
+
}
|
|
1569
|
+
const deadline = Date.now() + totalTimeoutMs;
|
|
1570
|
+
const requiredMbps = requiredMbpsFor(options);
|
|
1571
|
+
const isAbort = (error) => _optionalChain([error, 'optionalAccess', _91 => _91.name]) === "AbortError";
|
|
1572
|
+
const warmupBytes = Math.min(WARMUP_BYTES, chunkBytes);
|
|
1573
|
+
const warmupStarted = Date.now();
|
|
1574
|
+
try {
|
|
1575
|
+
await post(chunk(warmupBytes), _nullishCoalesce(options.warmupTimeoutMs, () => ( 1e4)));
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
if (!isAbort(error)) throw error;
|
|
1578
|
+
const seconds = Math.max(1, (Date.now() - warmupStarted) / 1e3);
|
|
1579
|
+
const uploadMbps2 = warmupBytes * 8 / seconds / 1e6;
|
|
1580
|
+
return { uploadMbps: uploadMbps2, pingMs: null, requiredMbps, verdict: "low" };
|
|
1581
|
+
}
|
|
1582
|
+
const body = chunk(chunkBytes);
|
|
1583
|
+
let sentBytes = 0;
|
|
1584
|
+
const started = Date.now();
|
|
1585
|
+
while (Date.now() - started < measureMs && Date.now() < deadline) {
|
|
1586
|
+
try {
|
|
1587
|
+
await post(body, Math.max(3e3, deadline - Date.now()));
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
if (!isAbort(error)) throw error;
|
|
1590
|
+
break;
|
|
1591
|
+
}
|
|
1592
|
+
sentBytes += body.byteLength;
|
|
1593
|
+
}
|
|
1594
|
+
const elapsedSeconds = Math.max((Date.now() - started) / 1e3, 1e-3);
|
|
1595
|
+
const uploadMbps = sentBytes * 8 / elapsedSeconds / 1e6;
|
|
1596
|
+
return {
|
|
1597
|
+
uploadMbps,
|
|
1598
|
+
pingMs: options.pingUrl ? await measurePing(options.pingUrl) : null,
|
|
1599
|
+
requiredMbps,
|
|
1600
|
+
verdict: verdictFor(uploadMbps, requiredMbps)
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/rtmp-room-adapter.ts
|
|
1605
|
+
var VIDEO_BITRATE = VIDEO_BITRATE_BPS;
|
|
1606
|
+
var BITRATE_FLOOR = 8e5;
|
|
1607
|
+
var DEGRADE_FACTOR = 0.7;
|
|
1608
|
+
var RECOVER_FACTOR = 1.15;
|
|
1609
|
+
var RECOVER_AFTER_SUFFICIENT = 5;
|
|
1610
|
+
function nextBitrateState(state, event) {
|
|
1611
|
+
if (!event.sufficient) {
|
|
1612
|
+
return {
|
|
1613
|
+
...state,
|
|
1614
|
+
current: Math.max(BITRATE_FLOOR, Math.round(state.current * DEGRADE_FACTOR)),
|
|
1615
|
+
sufficientStreak: 0
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
const streak = state.sufficientStreak + 1;
|
|
1619
|
+
if (streak < RECOVER_AFTER_SUFFICIENT || state.current >= state.ceiling) {
|
|
1620
|
+
return { ...state, sufficientStreak: Math.min(streak, RECOVER_AFTER_SUFFICIENT) };
|
|
1621
|
+
}
|
|
1622
|
+
return {
|
|
1623
|
+
...state,
|
|
1624
|
+
current: Math.min(state.ceiling, Math.round(state.current * RECOVER_FACTOR)),
|
|
1625
|
+
sufficientStreak: 0
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
var RtmpRoomAdapter = (_class2 = class {
|
|
1629
|
+
constructor(bindings, options = {}) {;_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);
|
|
1630
|
+
this.bindings = bindings;
|
|
1631
|
+
this.options = options;
|
|
1632
|
+
const request = _optionalChain([bindings, 'access', _92 => _92.requestPermissions, 'optionalAccess', _93 => _93.bind, 'call', _94 => _94(bindings)]);
|
|
1633
|
+
if (request) this.requestPermissions = request;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* RTMP không tự hồi: native chỉ phát hiện đứt (watchdog iOS / onDisconnect
|
|
1637
|
+
* Android) và báo "disconnected" — không tự quay số lại. Engine thấy cờ này
|
|
1638
|
+
* là chạy vòng redial backoff. Thiếu cờ: phiên ngồi chờ "reconnected" không
|
|
1639
|
+
* bao giờ tới rồi chết network_timeout sau 30s (prod 17/08/2026).
|
|
1640
|
+
*/
|
|
1641
|
+
__init23() {this.needsManualReconnect = true}
|
|
1642
|
+
__init24() {this.listeners = /* @__PURE__ */ new Set()}
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
__init25() {this.quality = 1080}
|
|
1646
|
+
__init26() {this.connected = false}
|
|
1647
|
+
__init27() {this.prepared = false}
|
|
1648
|
+
/** Khung người dùng chọn trên UI (9:16/16:9) — ưu tiên hơn hướng cầm máy. */
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Chỉ tồn tại khi bindings hỗ trợ — engine kiểm tra truthiness để quyết định
|
|
1653
|
+
* fallback về runtime.requestPermissions() (getUserMedia), nên KHÔNG được
|
|
1654
|
+
* khai báo method luôn-có rồi tự fallback bên trong.
|
|
1655
|
+
*/
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Phần tuỳ chọn của prepare(). Giữ dạng spread có điều kiện: truyền
|
|
1659
|
+
* `frameRate: undefined` xuống Expo Record là ghi đè mặc định của native
|
|
1660
|
+
* bằng null chứ không phải "bỏ qua trường".
|
|
1661
|
+
*/
|
|
1662
|
+
prepareOverrides() {
|
|
1663
|
+
return {
|
|
1664
|
+
...this.options.frameRate !== void 0 ? { frameRate: this.options.frameRate } : {},
|
|
1665
|
+
...this.options.cameraId !== void 0 ? { cameraId: this.options.cameraId } : {}
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
async preparePreview(quality) {
|
|
1669
|
+
this.quality = quality;
|
|
1670
|
+
await this.bindings.prepare({
|
|
1671
|
+
quality,
|
|
1672
|
+
landscape: this.captureOrientation ? this.captureOrientation === "landscape" : _nullishCoalesce(_optionalChain([this, 'access', _95 => _95.bindings, 'access', _96 => _96.isLandscape, 'optionalCall', _97 => _97()]), () => ( true)),
|
|
1673
|
+
...this.prepareOverrides()
|
|
1674
|
+
});
|
|
1675
|
+
this.prepared = true;
|
|
1676
|
+
return { kind: "rtmp" };
|
|
1677
|
+
}
|
|
1678
|
+
async connect(input) {
|
|
1679
|
+
if (!/^rtmps?:\/\//i.test(input.serverUrl)) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`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).`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
const url = `${input.serverUrl}${input.participantToken}`;
|
|
1685
|
+
await this.bindings.setMicrophoneEnabled(input.microphoneEnabled);
|
|
1686
|
+
await this.bindings.connect({
|
|
1687
|
+
url,
|
|
1688
|
+
videoBitrate: VIDEO_BITRATE[this.quality],
|
|
1689
|
+
// publish !== false → mặc định phát ngay; false = vào standby.
|
|
1690
|
+
publish: input.publish !== false
|
|
1691
|
+
});
|
|
1692
|
+
this.connected = true;
|
|
1693
|
+
this.bitrateState = {
|
|
1694
|
+
current: VIDEO_BITRATE[this.quality],
|
|
1695
|
+
ceiling: VIDEO_BITRATE[this.quality],
|
|
1696
|
+
sufficientStreak: 0
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
async setPublishing(enabled) {
|
|
1700
|
+
await this.bindings.setPublishing(enabled);
|
|
1701
|
+
}
|
|
1702
|
+
async disconnect(_stopTracks) {
|
|
1703
|
+
if (!this.connected) return;
|
|
1704
|
+
this.connected = false;
|
|
1705
|
+
this.bitrateState = void 0;
|
|
1706
|
+
await this.bindings.disconnect();
|
|
1707
|
+
}
|
|
1708
|
+
async switchCamera() {
|
|
1709
|
+
await this.bindings.switchCamera();
|
|
1710
|
+
}
|
|
1711
|
+
/** Ống kính vật lý (0.5x siêu rộng · 1x · tele) — không có thì UI kẹt ở mặc định. */
|
|
1712
|
+
async listCameras() {
|
|
1713
|
+
return this.bindings.listCameras().map((camera) => ({
|
|
1714
|
+
deviceId: camera.deviceId,
|
|
1715
|
+
label: camera.label,
|
|
1716
|
+
facing: camera.facing === "front" || camera.facing === "environment" ? camera.facing : "unknown"
|
|
1717
|
+
}));
|
|
1718
|
+
}
|
|
1719
|
+
async selectCamera(deviceId, facing) {
|
|
1720
|
+
await this.bindings.selectCamera(deviceId, facing);
|
|
1721
|
+
}
|
|
1722
|
+
async setMicrophoneEnabled(enabled) {
|
|
1723
|
+
await this.bindings.setMicrophoneEnabled(enabled);
|
|
1724
|
+
}
|
|
1725
|
+
/** Hạ chất lượng khi mạng yếu — thay cho BWE mà WebRTC cho sẵn. */
|
|
1726
|
+
async setVideoBitrate(bitsPerSecond) {
|
|
1727
|
+
await this.bindings.setVideoBitrate(bitsPerSecond);
|
|
1728
|
+
}
|
|
1729
|
+
async setCaptureOrientation(orientation) {
|
|
1730
|
+
this.captureOrientation = orientation;
|
|
1731
|
+
if (this.prepared && !this.connected) {
|
|
1732
|
+
await this.bindings.prepare({
|
|
1733
|
+
quality: this.quality,
|
|
1734
|
+
landscape: orientation === "landscape",
|
|
1735
|
+
...this.prepareOverrides()
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
subscribe(listener) {
|
|
1740
|
+
this.listeners.add(listener);
|
|
1741
|
+
if (!this.nativeSubscription) {
|
|
1742
|
+
this.nativeSubscription = this.bindings.addStatusListener((event) => {
|
|
1743
|
+
const mapped = mapStatus(event);
|
|
1744
|
+
if (mapped) this.emit(mapped);
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
if (!this.bandwidthSubscription && this.bindings.addBandwidthListener) {
|
|
1748
|
+
this.bandwidthSubscription = this.bindings.addBandwidthListener(
|
|
1749
|
+
(event) => this.handleBandwidth(event)
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
return () => {
|
|
1753
|
+
this.listeners.delete(listener);
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
async dispose() {
|
|
1757
|
+
_optionalChain([this, 'access', _98 => _98.nativeSubscription, 'optionalAccess', _99 => _99.remove, 'call', _100 => _100()]);
|
|
1758
|
+
this.nativeSubscription = void 0;
|
|
1759
|
+
_optionalChain([this, 'access', _101 => _101.bandwidthSubscription, 'optionalAccess', _102 => _102.remove, 'call', _103 => _103()]);
|
|
1760
|
+
this.bandwidthSubscription = void 0;
|
|
1761
|
+
this.listeners.clear();
|
|
1762
|
+
this.connected = false;
|
|
1763
|
+
this.bitrateState = void 0;
|
|
1764
|
+
await this.bindings.dispose();
|
|
1765
|
+
}
|
|
1766
|
+
handleBandwidth(event) {
|
|
1767
|
+
const state = this.bitrateState;
|
|
1768
|
+
if (!this.connected || !state) return;
|
|
1769
|
+
const next = nextBitrateState(state, event);
|
|
1770
|
+
this.bitrateState = next;
|
|
1771
|
+
if (next.current !== state.current) {
|
|
1772
|
+
void this.bindings.setVideoBitrate(next.current).catch(() => void 0);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
emit(event) {
|
|
1776
|
+
for (const listener of this.listeners) listener(event);
|
|
1777
|
+
}
|
|
1778
|
+
}, _class2);
|
|
1779
|
+
function mapStatus(event) {
|
|
1780
|
+
switch (event.status) {
|
|
1781
|
+
case "error":
|
|
1782
|
+
return { type: "media-error", error: new Error(event.message) };
|
|
1783
|
+
case "disconnected":
|
|
1784
|
+
return { type: "disconnected", reason: event.reason };
|
|
1785
|
+
case "connecting":
|
|
1786
|
+
return { type: "reconnecting" };
|
|
1787
|
+
case "publishing":
|
|
1788
|
+
return { type: "reconnected" };
|
|
1789
|
+
// "idle"/"connected" là bước trung gian, engine không cần biết.
|
|
1790
|
+
default:
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// src/native-runtime.ts
|
|
1796
|
+
function createRtmpRuntime() {
|
|
1797
|
+
const { Platform, PermissionsAndroid } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native");
|
|
1798
|
+
return {
|
|
1799
|
+
assertSupported() {
|
|
1800
|
+
if (Platform.OS !== "ios" && Platform.OS !== "android") {
|
|
1801
|
+
throw new UnsupportedRuntimeError();
|
|
1802
|
+
}
|
|
1803
|
+
try {
|
|
1804
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native");
|
|
1805
|
+
} catch (e13) {
|
|
1806
|
+
throw new UnsupportedRuntimeError(
|
|
1807
|
+
"@pickleball/rtmp-native is required: install it and rebuild the Development Build"
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
async requestPermissions() {
|
|
1812
|
+
if (Platform.OS === "android") {
|
|
1813
|
+
try {
|
|
1814
|
+
const granted = await PermissionsAndroid.requestMultiple([
|
|
1815
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
1816
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
|
|
1817
|
+
]);
|
|
1818
|
+
return granted[PermissionsAndroid.PERMISSIONS.CAMERA] === PermissionsAndroid.RESULTS.GRANTED && granted[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === PermissionsAndroid.RESULTS.GRANTED;
|
|
1819
|
+
} catch (e14) {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
try {
|
|
1824
|
+
const native = _chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native").default;
|
|
1825
|
+
if (typeof _optionalChain([native, 'optionalAccess', _104 => _104.requestPermissions]) === "function") {
|
|
1826
|
+
return await native.requestPermissions() !== false;
|
|
1827
|
+
}
|
|
1828
|
+
} catch (e15) {
|
|
1829
|
+
}
|
|
1830
|
+
return true;
|
|
1831
|
+
},
|
|
1832
|
+
activateKeepAwake: () => Promise.resolve(
|
|
1833
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").activateKeepAwakeAsync("@pickleball/expo-sdk")
|
|
1834
|
+
).catch(() => void 0),
|
|
1835
|
+
// deactivate trả promise và REJECT khi activity đã chết (sau crash/reload) —
|
|
1836
|
+
// không catch là thêm một "Uncaught (in promise)" gây nhiễu log.
|
|
1837
|
+
deactivateKeepAwake: () => {
|
|
1838
|
+
void Promise.resolve(
|
|
1839
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").deactivateKeepAwake("@pickleball/expo-sdk")
|
|
1840
|
+
).catch(() => void 0);
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
var createDefaultRuntime = createRtmpRuntime;
|
|
1845
|
+
function createDefaultStorage() {
|
|
1846
|
+
let installed = false;
|
|
1847
|
+
const install = () => {
|
|
1848
|
+
if (installed) return;
|
|
1849
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "expo-sqlite/localStorage/install");
|
|
1850
|
+
installed = true;
|
|
1851
|
+
};
|
|
1852
|
+
return {
|
|
1853
|
+
getItem: async (key) => {
|
|
1854
|
+
install();
|
|
1855
|
+
return globalThis.localStorage.getItem(key);
|
|
1856
|
+
},
|
|
1857
|
+
setItem: async (key, value) => {
|
|
1858
|
+
install();
|
|
1859
|
+
globalThis.localStorage.setItem(key, value);
|
|
1860
|
+
},
|
|
1861
|
+
removeItem: async (key) => {
|
|
1862
|
+
install();
|
|
1863
|
+
globalThis.localStorage.removeItem(key);
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function createDefaultNetwork() {
|
|
1868
|
+
const NetInfo = _chunk7OX2DSKIcjs.__require.call(void 0, "@react-native-community/netinfo").default;
|
|
1869
|
+
let online = true;
|
|
1870
|
+
void NetInfo.fetch().then((state) => {
|
|
1871
|
+
online = state.isConnected !== false;
|
|
1872
|
+
});
|
|
1873
|
+
return {
|
|
1874
|
+
isOnline: () => online,
|
|
1875
|
+
subscribe(listener) {
|
|
1876
|
+
return NetInfo.addEventListener((state) => {
|
|
1877
|
+
online = state.isConnected !== false;
|
|
1878
|
+
listener(online);
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function createDefaultTelemetry(fetchImplementation = globalThis.fetch, timeoutMs = 1e4) {
|
|
1884
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
1885
|
+
return {
|
|
1886
|
+
async send(credentials, events) {
|
|
1887
|
+
const body = JSON.stringify(events.slice(0, 20));
|
|
1888
|
+
if (body.length > 24 * 1024) return;
|
|
1889
|
+
const controller = new AbortController();
|
|
1890
|
+
controllers.add(controller);
|
|
1891
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1892
|
+
try {
|
|
1893
|
+
const response = await fetchImplementation(credentials.endpoint, {
|
|
1894
|
+
method: "POST",
|
|
1895
|
+
headers: {
|
|
1896
|
+
"Content-Type": "application/json",
|
|
1897
|
+
Authorization: `Bearer ${credentials.token}`
|
|
1898
|
+
},
|
|
1899
|
+
body,
|
|
1900
|
+
signal: controller.signal,
|
|
1901
|
+
redirect: "error"
|
|
1902
|
+
});
|
|
1903
|
+
if (typeof response.text === "function") await response.text();
|
|
1904
|
+
if (!response.ok) {
|
|
1905
|
+
throw Object.assign(new Error("Telemetry rejected"), {
|
|
1906
|
+
status: response.status
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
} finally {
|
|
1910
|
+
clearTimeout(timeout);
|
|
1911
|
+
controllers.delete(controller);
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1914
|
+
dispose() {
|
|
1915
|
+
for (const controller of controllers) controller.abort();
|
|
1916
|
+
controllers.clear();
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function createDefaultRoomFactory() {
|
|
1921
|
+
return createRtmpRoomFactory();
|
|
1922
|
+
}
|
|
1923
|
+
function createDefaultCommandChannel(fetchImplementation = globalThis.fetch, timeoutMs = 1e4) {
|
|
1924
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
1925
|
+
return {
|
|
1926
|
+
async poll(credentials, after) {
|
|
1927
|
+
const endpoint = credentials.endpoint.replace(/\/telemetry$/, "/commands");
|
|
1928
|
+
const controller = new AbortController();
|
|
1929
|
+
controllers.add(controller);
|
|
1930
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1931
|
+
try {
|
|
1932
|
+
const response = await fetchImplementation(endpoint, {
|
|
1933
|
+
method: "POST",
|
|
1934
|
+
headers: {
|
|
1935
|
+
"Content-Type": "application/json",
|
|
1936
|
+
Authorization: `Bearer ${credentials.token}`
|
|
1937
|
+
},
|
|
1938
|
+
body: JSON.stringify({ after }),
|
|
1939
|
+
signal: controller.signal,
|
|
1940
|
+
redirect: "error"
|
|
1941
|
+
});
|
|
1942
|
+
if (!response.ok) {
|
|
1943
|
+
if (typeof response.text === "function") await response.text();
|
|
1944
|
+
throw Object.assign(new Error("Command poll rejected"), {
|
|
1945
|
+
status: response.status
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
const envelope = await response.json();
|
|
1949
|
+
return parseCommandPollResult(_optionalChain([envelope, 'optionalAccess', _105 => _105.data]));
|
|
1950
|
+
} finally {
|
|
1951
|
+
clearTimeout(timeout);
|
|
1952
|
+
controllers.delete(controller);
|
|
1953
|
+
}
|
|
1954
|
+
},
|
|
1955
|
+
dispose() {
|
|
1956
|
+
for (const controller of controllers) controller.abort();
|
|
1957
|
+
controllers.clear();
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function parseCommandPollResult(value) {
|
|
1962
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1963
|
+
throw new Error("Command poll returned an invalid payload");
|
|
1964
|
+
}
|
|
1965
|
+
const record = value;
|
|
1966
|
+
const status = record.status;
|
|
1967
|
+
if (status !== "scheduled" && status !== "live" && status !== "ended") {
|
|
1968
|
+
throw new Error("Command poll returned an invalid status");
|
|
1969
|
+
}
|
|
1970
|
+
const commands = [];
|
|
1971
|
+
if (Array.isArray(record.commands)) {
|
|
1972
|
+
for (const item of record.commands) {
|
|
1973
|
+
if (item === null || typeof item !== "object") continue;
|
|
1974
|
+
const command = item;
|
|
1975
|
+
if (typeof command.id !== "string" || command.action !== "start" && command.action !== "stop" && command.action !== "end" || typeof command.issuedAt !== "number" || !Number.isFinite(command.issuedAt)) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
commands.push({
|
|
1979
|
+
id: command.id,
|
|
1980
|
+
action: command.action,
|
|
1981
|
+
issuedAt: command.issuedAt
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return { commands, status };
|
|
1986
|
+
}
|
|
1987
|
+
function createNativeRtmpBindings() {
|
|
1988
|
+
const native = _chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native").default;
|
|
1989
|
+
const { Platform, PermissionsAndroid } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native");
|
|
1990
|
+
return {
|
|
1991
|
+
prepare: (options) => native.prepare(options),
|
|
1992
|
+
// Android: xin quyền THUẦN, không mở camera như getUserMedia của WebRTC.
|
|
1993
|
+
// Máy MediaTek (Xiaomi tanzanite) chỉ cho một camera mở cùng lúc — camera
|
|
1994
|
+
// WebRTC chưa nhả (~400ms) là HaishinKit dính "Too many users (-87)" và
|
|
1995
|
+
// preview đen. iOS không có giới hạn đó nên để engine fallback getUserMedia.
|
|
1996
|
+
...Platform.OS === "android" ? {
|
|
1997
|
+
async requestPermissions() {
|
|
1998
|
+
const granted = await PermissionsAndroid.requestMultiple([
|
|
1999
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
2000
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
|
|
2001
|
+
]);
|
|
2002
|
+
return granted[PermissionsAndroid.PERMISSIONS.CAMERA] === PermissionsAndroid.RESULTS.GRANTED && granted[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === PermissionsAndroid.RESULTS.GRANTED;
|
|
2003
|
+
}
|
|
2004
|
+
} : {},
|
|
2005
|
+
isLandscape() {
|
|
2006
|
+
const { width, height } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native").Dimensions.get("window");
|
|
2007
|
+
return width > height;
|
|
2008
|
+
},
|
|
2009
|
+
connect: (options) => native.connect(options),
|
|
2010
|
+
setPublishing: (enabled) => native.setPublishing(enabled),
|
|
2011
|
+
disconnect: () => native.disconnect(),
|
|
2012
|
+
switchCamera: () => native.switchCamera(),
|
|
2013
|
+
listCameras: () => native.listCameras(),
|
|
2014
|
+
selectCamera: (deviceId, facing) => native.selectCamera(deviceId, facing),
|
|
2015
|
+
setMicrophoneEnabled: (enabled) => native.setMicrophoneEnabled(enabled),
|
|
2016
|
+
setVideoBitrate: (bps) => native.setVideoBitrate(bps),
|
|
2017
|
+
dispose: () => native.dispose(),
|
|
2018
|
+
addStatusListener: (listener) => native.addListener("onStatusChanged", listener),
|
|
2019
|
+
addBandwidthListener: (listener) => native.addListener("onBandwidth", listener)
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
function createRtmpRoomFactory(options) {
|
|
2023
|
+
return () => new RtmpRoomAdapter(createNativeRtmpBindings(), options);
|
|
2024
|
+
}
|
|
2025
|
+
function createDefaultDeviceMetrics() {
|
|
2026
|
+
return async () => {
|
|
2027
|
+
const metrics = {};
|
|
2028
|
+
const notes = [];
|
|
2029
|
+
try {
|
|
2030
|
+
const Battery = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-battery");
|
|
2031
|
+
const level = await Battery.getBatteryLevelAsync();
|
|
2032
|
+
if (typeof level === "number" && level >= 0) {
|
|
2033
|
+
metrics.batteryPct = Math.round(level * 100);
|
|
2034
|
+
} else {
|
|
2035
|
+
notes.push(`battery level=${String(level)}`);
|
|
2036
|
+
}
|
|
2037
|
+
const state = await Battery.getBatteryStateAsync();
|
|
2038
|
+
if (typeof state === "number") metrics.charging = state === 2 || state === 3;
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
notes.push(`battery require/call: ${String(error)}`);
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
let FileSystem;
|
|
2044
|
+
try {
|
|
2045
|
+
FileSystem = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-file-system/legacy");
|
|
2046
|
+
} catch (e16) {
|
|
2047
|
+
FileSystem = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-file-system");
|
|
2048
|
+
}
|
|
2049
|
+
if (FileSystem.getFreeDiskStorageAsync) {
|
|
2050
|
+
const freeBytes = await FileSystem.getFreeDiskStorageAsync();
|
|
2051
|
+
metrics.storageFreeGb = Math.round(freeBytes / 1e8) / 10;
|
|
2052
|
+
} else {
|
|
2053
|
+
notes.push("filesystem: getFreeDiskStorageAsync missing");
|
|
2054
|
+
}
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
notes.push(`filesystem: ${String(error)}`);
|
|
2057
|
+
}
|
|
2058
|
+
try {
|
|
2059
|
+
const Device = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-device");
|
|
2060
|
+
if (typeof Device.totalMemory === "number") {
|
|
2061
|
+
metrics.ramTotalMb = Math.round(Device.totalMemory / 1048576);
|
|
2062
|
+
}
|
|
2063
|
+
} catch (e17) {
|
|
2064
|
+
}
|
|
2065
|
+
try {
|
|
2066
|
+
const native = _chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native").default;
|
|
2067
|
+
const health = await native.getDeviceHealth();
|
|
2068
|
+
if (typeof _optionalChain([health, 'optionalAccess', _106 => _106.thermal]) === "number") metrics.thermal = health.thermal;
|
|
2069
|
+
if (typeof _optionalChain([health, 'optionalAccess', _107 => _107.batteryTempC]) === "number") {
|
|
2070
|
+
metrics.batteryTempC = Math.round(health.batteryTempC * 10) / 10;
|
|
2071
|
+
}
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
notes.push(`device health: ${String(error)}`);
|
|
2074
|
+
}
|
|
2075
|
+
try {
|
|
2076
|
+
const NetInfo = _chunk7OX2DSKIcjs.__require.call(void 0, "@react-native-community/netinfo").default;
|
|
2077
|
+
const status = await NetInfo.fetch();
|
|
2078
|
+
if (_optionalChain([status, 'optionalAccess', _108 => _108.type])) {
|
|
2079
|
+
metrics.netType = status.type === "wifi" ? "WiFi" : status.type === "cellular" ? String(
|
|
2080
|
+
_nullishCoalesce(_optionalChain([status, 'access', _109 => _109.details, 'optionalAccess', _110 => _110.cellularGeneration]), () => ( "cellular"))
|
|
2081
|
+
).toUpperCase() : status.type;
|
|
2082
|
+
}
|
|
2083
|
+
const details = _optionalChain([status, 'optionalAccess', _111 => _111.details]);
|
|
2084
|
+
if (typeof _optionalChain([details, 'optionalAccess', _112 => _112.ipAddress]) === "string" && details.ipAddress) {
|
|
2085
|
+
metrics.localIp = details.ipAddress;
|
|
2086
|
+
}
|
|
2087
|
+
if (typeof _optionalChain([details, 'optionalAccess', _113 => _113.strength]) === "number" && Number.isFinite(details.strength)) {
|
|
2088
|
+
metrics.signalPct = Math.round(details.strength);
|
|
2089
|
+
}
|
|
2090
|
+
if (typeof _optionalChain([details, 'optionalAccess', _114 => _114.linkSpeed]) === "number" && Number.isFinite(details.linkSpeed)) {
|
|
2091
|
+
metrics.linkMbps = Math.round(details.linkSpeed);
|
|
2092
|
+
}
|
|
2093
|
+
if (typeof _optionalChain([details, 'optionalAccess', _115 => _115.frequency]) === "number" && Number.isFinite(details.frequency) && details.frequency > 0) {
|
|
2094
|
+
metrics.freqMhz = Math.round(details.frequency);
|
|
2095
|
+
}
|
|
2096
|
+
} catch (e18) {
|
|
2097
|
+
}
|
|
2098
|
+
if (notes.length > 0) console.log("[pickleball] metrics notes:", notes.join(" | "));
|
|
2099
|
+
return Object.keys(metrics).length > 0 ? metrics : null;
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/provider.tsx
|
|
2104
|
+
var _react = require('react'); var _react2 = _interopRequireDefault(_react);
|
|
2105
|
+
var _reactnative = require('react-native');
|
|
2106
|
+
var _jsxruntime = require('react/jsx-runtime');
|
|
2107
|
+
var defaultLivestreamTheme = {
|
|
2108
|
+
background: "#0a0a0b",
|
|
2109
|
+
foreground: "#f5f6f7",
|
|
2110
|
+
accent: "#b4ff39",
|
|
2111
|
+
danger: "#fb7185",
|
|
2112
|
+
muted: "#6c7079",
|
|
2113
|
+
overlay: "rgba(10,10,11,0.82)",
|
|
2114
|
+
radius: 14,
|
|
2115
|
+
surface: "#131417",
|
|
2116
|
+
elevated: "#1b1d21",
|
|
2117
|
+
hairline: "rgba(255,255,255,0.09)",
|
|
2118
|
+
ink: "#0a0a0b",
|
|
2119
|
+
textSecondary: "#a7abb3",
|
|
2120
|
+
radiusSm: 6
|
|
2121
|
+
};
|
|
2122
|
+
var LivestreamHostContext = _react2.default.createContext(null);
|
|
2123
|
+
function useLivestreamHost() {
|
|
2124
|
+
const value = _react2.default.use(LivestreamHostContext);
|
|
2125
|
+
if (!value) {
|
|
2126
|
+
throw new ExpoSdkConfigurationError(
|
|
2127
|
+
"useLivestreamHost must be used inside PickleballLiveProvider"
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
return value;
|
|
2131
|
+
}
|
|
2132
|
+
var LivestreamContext = _react2.default.createContext(null);
|
|
2133
|
+
function PickleballLiveProvider(props) {
|
|
2134
|
+
const engineRef = _react.useRef.call(void 0, null);
|
|
2135
|
+
const keepSessionRef = _react.useRef.call(void 0, props.keepSessionOnRestart);
|
|
2136
|
+
keepSessionRef.current = props.keepSessionOnRestart;
|
|
2137
|
+
const resolvedKeepRef = _react.useRef.call(void 0, typeof props.keepSessionOnRestart === "function");
|
|
2138
|
+
_react.useEffect.call(void 0, () => {
|
|
2139
|
+
const value2 = props.keepSessionOnRestart;
|
|
2140
|
+
if (typeof value2 !== "function") {
|
|
2141
|
+
resolvedKeepRef.current = value2 === true;
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
resolvedKeepRef.current = true;
|
|
2145
|
+
let cancelled = false;
|
|
2146
|
+
void Promise.resolve().then(() => value2()).then((keep) => {
|
|
2147
|
+
if (!cancelled) resolvedKeepRef.current = keep === true;
|
|
2148
|
+
}).catch(() => void 0);
|
|
2149
|
+
return () => {
|
|
2150
|
+
cancelled = true;
|
|
2151
|
+
};
|
|
2152
|
+
}, [props.keepSessionOnRestart]);
|
|
2153
|
+
const hostRef = _react.useRef.call(void 0, null);
|
|
2154
|
+
const effectGenerationRef = _react.useRef.call(void 0, 0);
|
|
2155
|
+
if (!engineRef.current) {
|
|
2156
|
+
const deviceMetrics = _nullishCoalesce(props.deviceMetrics, () => ( createDefaultDeviceMetrics()));
|
|
2157
|
+
const dependencies = {
|
|
2158
|
+
sessionProvider: props.sessionProvider,
|
|
2159
|
+
bootstrapInput: props.bootstrapInput,
|
|
2160
|
+
runtime: _nullishCoalesce(props.runtime, () => ( createDefaultRuntime())),
|
|
2161
|
+
storage: _nullishCoalesce(props.storage, () => ( createDefaultStorage())),
|
|
2162
|
+
network: _nullishCoalesce(props.network, () => ( createDefaultNetwork())),
|
|
2163
|
+
telemetry: _nullishCoalesce(props.telemetry, () => ( createDefaultTelemetry())),
|
|
2164
|
+
commandChannel: _nullishCoalesce(props.commandChannel, () => ( createDefaultCommandChannel())),
|
|
2165
|
+
...props.commandSubscription ? { commandSubscription: props.commandSubscription } : {},
|
|
2166
|
+
deviceMetrics,
|
|
2167
|
+
roomFactory: _nullishCoalesce(props.roomFactory, () => ( createDefaultRoomFactory())),
|
|
2168
|
+
// Đọc qua ref để engine luôn thấy giá trị MỚI NHẤT: engine chỉ dựng một lần
|
|
2169
|
+
// (engineRef), còn prop thì đổi theo việc máy được gán/gỡ sân. Prop dạng
|
|
2170
|
+
// hàm được gọi xuyên qua (engine tự await) — đường cho app đọc thẳng đĩa.
|
|
2171
|
+
keepSessionOnRestart: () => {
|
|
2172
|
+
const value2 = keepSessionRef.current;
|
|
2173
|
+
return typeof value2 === "function" ? value2() : value2 === true;
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
engineRef.current = new LivestreamEngine(dependencies);
|
|
2177
|
+
hostRef.current = {
|
|
2178
|
+
engine: engineRef.current,
|
|
2179
|
+
bootstrapInput: props.bootstrapInput,
|
|
2180
|
+
deviceMetrics
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
const engine = engineRef.current;
|
|
2184
|
+
const snapshot = _react.useSyncExternalStore.call(void 0,
|
|
2185
|
+
engine.subscribe,
|
|
2186
|
+
engine.getSnapshot,
|
|
2187
|
+
engine.getSnapshot
|
|
2188
|
+
);
|
|
2189
|
+
_react.useEffect.call(void 0, () => {
|
|
2190
|
+
const effectGeneration = ++effectGenerationRef.current;
|
|
2191
|
+
void engine.initialize();
|
|
2192
|
+
const subscription = _reactnative.AppState.addEventListener("change", (state) => {
|
|
2193
|
+
engine.handleAppState(
|
|
2194
|
+
state === "active" ? "active" : state === "background" ? "background" : "inactive"
|
|
2195
|
+
);
|
|
2196
|
+
});
|
|
2197
|
+
return () => {
|
|
2198
|
+
subscription.remove();
|
|
2199
|
+
queueMicrotask(() => {
|
|
2200
|
+
if (effectGenerationRef.current === effectGeneration) {
|
|
2201
|
+
void engine.dispose({ keepSession: resolvedKeepRef.current });
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
};
|
|
2205
|
+
}, [engine]);
|
|
2206
|
+
_react.useEffect.call(void 0, () => {
|
|
2207
|
+
if (snapshot.error) _optionalChain([props, 'access', _116 => _116.onError, 'optionalCall', _117 => _117(snapshot.error)]);
|
|
2208
|
+
}, [props.onError, snapshot.error]);
|
|
2209
|
+
const actions = _react.useMemo.call(void 0,
|
|
2210
|
+
() => ({
|
|
2211
|
+
prepare: () => engine.prepare(),
|
|
2212
|
+
start: (input, options) => engine.start(input, options),
|
|
2213
|
+
startPublishing: () => engine.startPublishing(),
|
|
2214
|
+
stopPublishing: () => engine.stopPublishing(),
|
|
2215
|
+
stop: (reason) => engine.stop(reason),
|
|
2216
|
+
retry: () => engine.retry(),
|
|
2217
|
+
switchCamera: () => engine.switchCamera(),
|
|
2218
|
+
setMicrophoneEnabled: (enabled) => engine.setMicrophoneEnabled(enabled),
|
|
2219
|
+
setCaptureOrientation: (orientation) => engine.setCaptureOrientation(orientation),
|
|
2220
|
+
selectCamera: (deviceId) => engine.selectCamera(deviceId)
|
|
2221
|
+
}),
|
|
2222
|
+
[engine]
|
|
2223
|
+
);
|
|
2224
|
+
const locale = _nullishCoalesce(props.locale, () => ( "vi"));
|
|
2225
|
+
const theme = _react.useMemo.call(void 0,
|
|
2226
|
+
() => ({ ...defaultLivestreamTheme, ...props.theme }),
|
|
2227
|
+
[props.theme]
|
|
2228
|
+
);
|
|
2229
|
+
const value = _react.useMemo.call(void 0,
|
|
2230
|
+
() => ({ ...snapshot, locale, theme, ...actions }),
|
|
2231
|
+
[snapshot, locale, theme, actions]
|
|
2232
|
+
);
|
|
2233
|
+
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 }) });
|
|
2234
|
+
}
|
|
2235
|
+
function useLivestream() {
|
|
2236
|
+
const value = _react2.default.use(LivestreamContext);
|
|
2237
|
+
if (!value) {
|
|
2238
|
+
throw new ExpoSdkConfigurationError(
|
|
2239
|
+
"useLivestream must be used inside PickleballLiveProvider"
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
return value;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
|
|
2265
|
+
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
|
|
2278
|
+
|
|
2279
|
+
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
|
|
2284
|
+
|
|
2285
|
+
|
|
2286
|
+
|
|
2287
|
+
|
|
2288
|
+
|
|
2289
|
+
|
|
2290
|
+
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.VIDEO_BITRATE_BPS = VIDEO_BITRATE_BPS; exports.AUDIO_BITRATE_BPS = AUDIO_BITRATE_BPS; exports.DEFAULT_UPLOAD_URL = DEFAULT_UPLOAD_URL; exports.requiredMbpsFor = requiredMbpsFor; exports.verdictFor = verdictFor; exports.runUplinkCheck = runUplinkCheck; exports.RtmpRoomAdapter = RtmpRoomAdapter; exports.createRtmpRuntime = createRtmpRuntime; 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;
|