@harmoni-org/sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +694 -0
- package/dist/index.d.mts +712 -0
- package/dist/index.d.ts +712 -0
- package/dist/index.js +2054 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2013 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2054 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var axios = require('axios');
|
|
6
|
+
var socket_ioClient = require('socket.io-client');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
11
|
+
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/core/errors/ApiError.ts
|
|
19
|
+
var ApiError = class _ApiError extends Error {
|
|
20
|
+
constructor(message, status, code, details) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.isApiError = true;
|
|
23
|
+
this.name = "ApiError";
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.details = details;
|
|
27
|
+
if (Error.captureStackTrace) {
|
|
28
|
+
Error.captureStackTrace(this, _ApiError);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
toJSON() {
|
|
32
|
+
return {
|
|
33
|
+
message: this.message,
|
|
34
|
+
code: this.code,
|
|
35
|
+
status: this.status,
|
|
36
|
+
details: this.details
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
static isApiError(error) {
|
|
40
|
+
return error instanceof _ApiError || error?.isApiError === true;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var NetworkError = class extends ApiError {
|
|
44
|
+
constructor(message = "Network error occurred") {
|
|
45
|
+
super(message, 0, "NETWORK_ERROR");
|
|
46
|
+
this.name = "NetworkError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var TimeoutError = class extends ApiError {
|
|
50
|
+
constructor(message = "Request timeout") {
|
|
51
|
+
super(message, 408, "TIMEOUT_ERROR");
|
|
52
|
+
this.name = "TimeoutError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var UnauthorizedError = class extends ApiError {
|
|
56
|
+
constructor(message = "Unauthorized") {
|
|
57
|
+
super(message, 401, "UNAUTHORIZED");
|
|
58
|
+
this.name = "UnauthorizedError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var ForbiddenError = class extends ApiError {
|
|
62
|
+
constructor(message = "Forbidden") {
|
|
63
|
+
super(message, 403, "FORBIDDEN");
|
|
64
|
+
this.name = "ForbiddenError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var NotFoundError = class extends ApiError {
|
|
68
|
+
constructor(message = "Resource not found") {
|
|
69
|
+
super(message, 404, "NOT_FOUND");
|
|
70
|
+
this.name = "NotFoundError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var ValidationError = class extends ApiError {
|
|
74
|
+
constructor(message = "Validation failed", details) {
|
|
75
|
+
super(message, 422, "VALIDATION_ERROR", details);
|
|
76
|
+
this.name = "ValidationError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/core/http/HttpClient.ts
|
|
81
|
+
var HttpClient = class {
|
|
82
|
+
constructor(config) {
|
|
83
|
+
this.config = config;
|
|
84
|
+
this.authToken = null;
|
|
85
|
+
this.retryCount = /* @__PURE__ */ new Map();
|
|
86
|
+
this.client = axios__default.default.create({
|
|
87
|
+
baseURL: config.baseURL,
|
|
88
|
+
timeout: config.timeout || 3e4,
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
...config.headers
|
|
92
|
+
},
|
|
93
|
+
withCredentials: config.withCredentials || false
|
|
94
|
+
});
|
|
95
|
+
this.setupInterceptors();
|
|
96
|
+
}
|
|
97
|
+
setupInterceptors() {
|
|
98
|
+
this.client.interceptors.request.use(
|
|
99
|
+
(config) => {
|
|
100
|
+
const requestConfig = config;
|
|
101
|
+
if (this.authToken && !requestConfig.skipAuth) {
|
|
102
|
+
config.headers = config.headers || {};
|
|
103
|
+
config.headers.Authorization = `Bearer ${this.authToken}`;
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
},
|
|
107
|
+
(error) => {
|
|
108
|
+
return Promise.reject(this.handleError(error));
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
this.client.interceptors.response.use(
|
|
112
|
+
(response) => response,
|
|
113
|
+
async (error) => {
|
|
114
|
+
const originalRequest = error.config;
|
|
115
|
+
if (error.response?.status === 401 && !originalRequest._retry && this.refreshTokenFn) {
|
|
116
|
+
originalRequest._retry = true;
|
|
117
|
+
try {
|
|
118
|
+
const newToken = await this.refreshTokenFn();
|
|
119
|
+
this.setAuthToken(newToken);
|
|
120
|
+
if (!originalRequest.headers) {
|
|
121
|
+
originalRequest.headers = {};
|
|
122
|
+
}
|
|
123
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
124
|
+
return this.client.request(originalRequest);
|
|
125
|
+
} catch (refreshError) {
|
|
126
|
+
this.authToken = null;
|
|
127
|
+
return Promise.reject(this.handleError(refreshError));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (this.shouldRetry(error, originalRequest)) {
|
|
131
|
+
return this.retryRequest(originalRequest);
|
|
132
|
+
}
|
|
133
|
+
return Promise.reject(this.handleError(error));
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
shouldRetry(error, config) {
|
|
138
|
+
if (!config || config.skipRetry) return false;
|
|
139
|
+
const retryConfig = this.config.retryConfig;
|
|
140
|
+
if (!retryConfig || !retryConfig.maxRetries) return false;
|
|
141
|
+
const method = config.method?.toUpperCase();
|
|
142
|
+
const safeToRetry = method === "GET" || method === "HEAD" || method === "OPTIONS";
|
|
143
|
+
if (!safeToRetry && !config._retry) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const requestId = this.getRequestId(config);
|
|
147
|
+
const currentRetries = this.retryCount.get(requestId) || 0;
|
|
148
|
+
if (currentRetries >= retryConfig.maxRetries) {
|
|
149
|
+
this.retryCount.delete(requestId);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const retryableStatuses = retryConfig.retryableStatuses || [408, 429, 500, 502, 503, 504];
|
|
153
|
+
return error.response ? retryableStatuses.includes(error.response.status) : true;
|
|
154
|
+
}
|
|
155
|
+
async retryRequest(config) {
|
|
156
|
+
const requestId = this.getRequestId(config);
|
|
157
|
+
const currentRetries = this.retryCount.get(requestId) || 0;
|
|
158
|
+
this.retryCount.set(requestId, currentRetries + 1);
|
|
159
|
+
const delay = this.config.retryConfig?.retryDelay || 1e3;
|
|
160
|
+
const backoffDelay = delay * Math.pow(2, currentRetries);
|
|
161
|
+
await this.sleep(backoffDelay);
|
|
162
|
+
config._retryCount = currentRetries + 1;
|
|
163
|
+
return this.client.request(config);
|
|
164
|
+
}
|
|
165
|
+
getRequestId(config) {
|
|
166
|
+
return `${config.method}-${config.url}`;
|
|
167
|
+
}
|
|
168
|
+
sleep(ms) {
|
|
169
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
170
|
+
}
|
|
171
|
+
handleError(error) {
|
|
172
|
+
if (ApiError.isApiError(error)) {
|
|
173
|
+
return error;
|
|
174
|
+
}
|
|
175
|
+
if (axios__default.default.isAxiosError(error)) {
|
|
176
|
+
const axiosError = error;
|
|
177
|
+
if (!axiosError.response) {
|
|
178
|
+
if (axiosError.code === "ECONNABORTED") {
|
|
179
|
+
return new TimeoutError();
|
|
180
|
+
}
|
|
181
|
+
return new NetworkError(axiosError.message);
|
|
182
|
+
}
|
|
183
|
+
const status = axiosError.response.status;
|
|
184
|
+
const data = axiosError.response.data;
|
|
185
|
+
const message = data?.message || axiosError.message;
|
|
186
|
+
switch (status) {
|
|
187
|
+
case 401:
|
|
188
|
+
return new UnauthorizedError(message);
|
|
189
|
+
case 403:
|
|
190
|
+
return new ForbiddenError(message);
|
|
191
|
+
case 404:
|
|
192
|
+
return new NotFoundError(message);
|
|
193
|
+
case 422:
|
|
194
|
+
return new ValidationError(message, data);
|
|
195
|
+
default:
|
|
196
|
+
return new ApiError(message, status, data?.code, data);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return new ApiError(error instanceof Error ? error.message : "An unknown error occurred", 500);
|
|
200
|
+
}
|
|
201
|
+
// Public methods
|
|
202
|
+
setAuthToken(token) {
|
|
203
|
+
this.authToken = token;
|
|
204
|
+
}
|
|
205
|
+
getAuthToken() {
|
|
206
|
+
return this.authToken;
|
|
207
|
+
}
|
|
208
|
+
setRefreshTokenFunction(fn) {
|
|
209
|
+
this.refreshTokenFn = fn;
|
|
210
|
+
}
|
|
211
|
+
addRequestInterceptor(interceptor) {
|
|
212
|
+
return this.client.interceptors.request.use(
|
|
213
|
+
interceptor.onFulfilled,
|
|
214
|
+
interceptor.onRejected
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
addResponseInterceptor(interceptor) {
|
|
218
|
+
return this.client.interceptors.response.use(interceptor.onFulfilled, interceptor.onRejected);
|
|
219
|
+
}
|
|
220
|
+
removeRequestInterceptor(id) {
|
|
221
|
+
this.client.interceptors.request.eject(id);
|
|
222
|
+
}
|
|
223
|
+
removeResponseInterceptor(id) {
|
|
224
|
+
this.client.interceptors.response.eject(id);
|
|
225
|
+
}
|
|
226
|
+
// HTTP methods
|
|
227
|
+
async get(url, config) {
|
|
228
|
+
const response = await this.client.get(url, config);
|
|
229
|
+
return response.data;
|
|
230
|
+
}
|
|
231
|
+
async post(url, data, config) {
|
|
232
|
+
const requestConfig = { ...config };
|
|
233
|
+
if (data instanceof FormData) {
|
|
234
|
+
requestConfig.headers = {
|
|
235
|
+
...requestConfig.headers,
|
|
236
|
+
"Content-Type": "multipart/form-data"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const response = await this.client.post(url, data, requestConfig);
|
|
240
|
+
return response.data;
|
|
241
|
+
}
|
|
242
|
+
async put(url, data, config) {
|
|
243
|
+
const requestConfig = { ...config };
|
|
244
|
+
if (data instanceof FormData) {
|
|
245
|
+
requestConfig.headers = {
|
|
246
|
+
...requestConfig.headers,
|
|
247
|
+
"Content-Type": "multipart/form-data"
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const response = await this.client.put(url, data, requestConfig);
|
|
251
|
+
return response.data;
|
|
252
|
+
}
|
|
253
|
+
async patch(url, data, config) {
|
|
254
|
+
const requestConfig = { ...config };
|
|
255
|
+
if (data instanceof FormData) {
|
|
256
|
+
requestConfig.headers = {
|
|
257
|
+
...requestConfig.headers,
|
|
258
|
+
"Content-Type": "multipart/form-data"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const response = await this.client.patch(url, data, requestConfig);
|
|
262
|
+
return response.data;
|
|
263
|
+
}
|
|
264
|
+
async delete(url, config) {
|
|
265
|
+
const response = await this.client.delete(url, config);
|
|
266
|
+
return response.data;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/modules/auth/AuthModule.ts
|
|
271
|
+
var AuthModule = class {
|
|
272
|
+
constructor(http) {
|
|
273
|
+
this.http = http;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Authenticates the user by calling the login endpoint.
|
|
277
|
+
* @param credentials - The login request payload containing emailOrUsername and password.
|
|
278
|
+
* @returns The logged-in user information including the access token.
|
|
279
|
+
*/
|
|
280
|
+
async login(credentials) {
|
|
281
|
+
const response = await this.http.post("/auth/login", credentials, {
|
|
282
|
+
skipAuth: true
|
|
283
|
+
});
|
|
284
|
+
const { user, accessToken, refreshToken } = response;
|
|
285
|
+
this.http.setAuthToken(accessToken);
|
|
286
|
+
const normalizedUser = {
|
|
287
|
+
...user,
|
|
288
|
+
id: user.userId || user.id,
|
|
289
|
+
token: accessToken,
|
|
290
|
+
refreshToken
|
|
291
|
+
};
|
|
292
|
+
return normalizedUser;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Registers a new user by calling the registration endpoint.
|
|
296
|
+
* @param registrationData - The registration request payload containing username, password, and optionally email.
|
|
297
|
+
* @returns The newly created user information including the access token.
|
|
298
|
+
*/
|
|
299
|
+
async register(data) {
|
|
300
|
+
const response = await this.http.post("/auth/register", data, {
|
|
301
|
+
skipAuth: true
|
|
302
|
+
});
|
|
303
|
+
const { user, accessToken, refreshToken } = response;
|
|
304
|
+
this.http.setAuthToken(accessToken);
|
|
305
|
+
const normalizedUser = {
|
|
306
|
+
...user,
|
|
307
|
+
id: user.userId || user.id,
|
|
308
|
+
token: accessToken,
|
|
309
|
+
refreshToken
|
|
310
|
+
};
|
|
311
|
+
return normalizedUser;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Verifies if the provided JWT token is valid.
|
|
315
|
+
* @returns The user information if the token is valid.
|
|
316
|
+
*/
|
|
317
|
+
async verifyToken() {
|
|
318
|
+
const response = await this.http.get("/auth/verify-token");
|
|
319
|
+
if (response.user && response.user.userId) {
|
|
320
|
+
response.user.id = response.user.userId;
|
|
321
|
+
}
|
|
322
|
+
return response;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Refreshes the access token using the stored refresh token.
|
|
326
|
+
* @returns The newly issued access token.
|
|
327
|
+
*/
|
|
328
|
+
async refreshAccessToken() {
|
|
329
|
+
const response = await this.http.get("/auth/refresh-token");
|
|
330
|
+
const { accessToken } = response;
|
|
331
|
+
this.http.setAuthToken(accessToken);
|
|
332
|
+
return accessToken;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Checks if the provided email is unique (not already registered).
|
|
336
|
+
* @param email - The email address to check.
|
|
337
|
+
* @returns True if the email is unique, otherwise false.
|
|
338
|
+
*/
|
|
339
|
+
async isEmailUnique(email) {
|
|
340
|
+
const response = await this.http.get(`/auth/check-email/${email}`, {
|
|
341
|
+
skipAuth: true
|
|
342
|
+
});
|
|
343
|
+
return response.isUnique;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Checks if the provided username is unique (not already taken).
|
|
347
|
+
* @param username - The username to check.
|
|
348
|
+
* @returns True if the username is unique, otherwise false.
|
|
349
|
+
*/
|
|
350
|
+
async isUsernameUnique(username) {
|
|
351
|
+
const response = await this.http.get(
|
|
352
|
+
`/auth/check-username/${username}`,
|
|
353
|
+
{ skipAuth: true }
|
|
354
|
+
);
|
|
355
|
+
return response.isUnique;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Logout the current user
|
|
359
|
+
*/
|
|
360
|
+
async logout() {
|
|
361
|
+
this.http.setAuthToken(null);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Request password reset
|
|
365
|
+
*/
|
|
366
|
+
async requestPasswordReset(email) {
|
|
367
|
+
await this.http.post("/auth/password/reset-request", { email }, { skipAuth: true });
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Reset password with token
|
|
371
|
+
*/
|
|
372
|
+
async resetPassword(token, newPassword) {
|
|
373
|
+
await this.http.post("/auth/password/reset", { token, newPassword }, { skipAuth: true });
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Change password for authenticated user
|
|
377
|
+
*/
|
|
378
|
+
async changePassword(currentPassword, newPassword) {
|
|
379
|
+
await this.http.post("/auth/password/change", {
|
|
380
|
+
currentPassword,
|
|
381
|
+
newPassword
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Verify email with token
|
|
386
|
+
*/
|
|
387
|
+
async verifyEmail(token) {
|
|
388
|
+
await this.http.post("/auth/email/verify", { token }, { skipAuth: true });
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Resend verification email
|
|
392
|
+
*/
|
|
393
|
+
async resendVerificationEmail(email) {
|
|
394
|
+
await this.http.post("/auth/email/resend", { email }, { skipAuth: true });
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/modules/user/UserModule.ts
|
|
399
|
+
var UserModule = class {
|
|
400
|
+
constructor(http) {
|
|
401
|
+
this.http = http;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get user by ID
|
|
405
|
+
*/
|
|
406
|
+
async getById(userId) {
|
|
407
|
+
return this.http.get(`/users/${userId}`);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Get current user profile
|
|
411
|
+
*/
|
|
412
|
+
async getCurrentUser() {
|
|
413
|
+
return this.http.get("/users/me");
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Update current user profile
|
|
417
|
+
*/
|
|
418
|
+
async updateProfile(data) {
|
|
419
|
+
return this.http.patch("/users/me", data);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Delete current user account
|
|
423
|
+
*/
|
|
424
|
+
async deleteAccount() {
|
|
425
|
+
await this.http.delete("/users/me");
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Get list of users with pagination and filters
|
|
429
|
+
*/
|
|
430
|
+
async list(params) {
|
|
431
|
+
return this.http.get("/users", { params });
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Search users by query
|
|
435
|
+
*/
|
|
436
|
+
async search(query, params) {
|
|
437
|
+
return this.http.get("/users/search", {
|
|
438
|
+
params: { query, ...params }
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Upload user avatar
|
|
443
|
+
*/
|
|
444
|
+
async uploadAvatar(file, fileName) {
|
|
445
|
+
const formData = new FormData();
|
|
446
|
+
formData.append("avatar", file, fileName);
|
|
447
|
+
return this.http.post("/users/me/avatar", formData);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Delete user avatar
|
|
451
|
+
*/
|
|
452
|
+
async deleteAvatar() {
|
|
453
|
+
await this.http.delete("/users/me/avatar");
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/modules/watchTogether/WatchTogetherError.ts
|
|
458
|
+
var WatchTogetherError = class _WatchTogetherError extends Error {
|
|
459
|
+
constructor(message, code, details) {
|
|
460
|
+
super(message);
|
|
461
|
+
this.code = code;
|
|
462
|
+
this.details = details;
|
|
463
|
+
this.isWatchTogetherError = true;
|
|
464
|
+
this.name = "WatchTogetherError";
|
|
465
|
+
if (Error.captureStackTrace) {
|
|
466
|
+
Error.captureStackTrace(this, _WatchTogetherError);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
toJSON() {
|
|
470
|
+
return {
|
|
471
|
+
name: this.name,
|
|
472
|
+
message: this.message,
|
|
473
|
+
code: this.code,
|
|
474
|
+
details: this.details
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
static isWatchTogetherError(error) {
|
|
478
|
+
return error instanceof _WatchTogetherError || error?.isWatchTogetherError === true;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// src/types/actions.ts
|
|
483
|
+
var USER_ACTIONS = {
|
|
484
|
+
// Core playback actions (synced with Watch Together)
|
|
485
|
+
PLAY: "PLAY",
|
|
486
|
+
PAUSE: "PAUSE",
|
|
487
|
+
SEEK: "SEEK",
|
|
488
|
+
// File/media actions
|
|
489
|
+
FILE_UPDATE: "FILE_UPDATE",
|
|
490
|
+
// Advanced actions (may not be synced)
|
|
491
|
+
STOP: "STOP",
|
|
492
|
+
VOLUME_UPDATE: "VOLUME_UPDATE",
|
|
493
|
+
ENTER_FULLSCREEN: "ENTER_FULLSCREEN",
|
|
494
|
+
EXIT_FULLSCREEN: "EXIT_FULLSCREEN",
|
|
495
|
+
SPEED_UPDATE: "SPEED_UPDATE",
|
|
496
|
+
AUDIO_DELAY_UPDATE: "AUDIO_DELAY_UPDATE",
|
|
497
|
+
SUBTITLE_DELAY_UPDATE: "SUBTITLE_DELAY_UPDATE",
|
|
498
|
+
ASPECT_RATIO_UPDATE: "ASPECT_RATIO_UPDATE",
|
|
499
|
+
TOGGLE_REPEAT: "TOGGLE_REPEAT",
|
|
500
|
+
TOGGLE_LOOP: "TOGGLE_LOOP",
|
|
501
|
+
VIDEO_EFFECT_UPDATE: "VIDEO_EFFECT_UPDATE",
|
|
502
|
+
AUDIO_FILTER_UPDATE: "AUDIO_FILTER_UPDATE",
|
|
503
|
+
TOGGLE_RANDOM: "TOGGLE_RANDOM",
|
|
504
|
+
API_VERSION_UPDATE: "API_VERSION_UPDATE",
|
|
505
|
+
PLAYLIST_UPDATE: "PLAYLIST_UPDATE",
|
|
506
|
+
TITLE_OR_CHAPTER_UPDATE: "TITLE_OR_CHAPTER_UPDATE",
|
|
507
|
+
NO_CHANGE: "NO_CHANGE"
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// src/types/watchTogether.ts
|
|
511
|
+
var SyncActions = {
|
|
512
|
+
PLAY: "play",
|
|
513
|
+
PAUSE: "pause",
|
|
514
|
+
SEEK: "seek"
|
|
515
|
+
};
|
|
516
|
+
var WatchTogetherErrorCodes = {
|
|
517
|
+
ROOM_NOT_FOUND: "ROOM_NOT_FOUND",
|
|
518
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
519
|
+
ROOM_FULL: "ROOM_FULL",
|
|
520
|
+
INVALID_PASSWORD: "INVALID_PASSWORD",
|
|
521
|
+
NOT_IN_ROOM: "NOT_IN_ROOM",
|
|
522
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
523
|
+
CONNECTION_ERROR: "CONNECTION_ERROR",
|
|
524
|
+
ALREADY_IN_ROOM: "ALREADY_IN_ROOM"
|
|
525
|
+
};
|
|
526
|
+
var WatchTogetherConstants = {
|
|
527
|
+
NAMESPACE: "/watch-together",
|
|
528
|
+
SYNC_CHECK_INTERVAL_MS: 3e4,
|
|
529
|
+
// 30 seconds
|
|
530
|
+
SYNC_TOLERANCE_SECONDS: 1,
|
|
531
|
+
// Time difference tolerance
|
|
532
|
+
MAX_RECONNECT_ATTEMPTS: 5,
|
|
533
|
+
RECONNECT_DELAY_MS: 2e3
|
|
534
|
+
};
|
|
535
|
+
var WatchTogetherIDs = {
|
|
536
|
+
JOIN_ROOM_INPUT: "join-room-code",
|
|
537
|
+
JOIN_AND_INVITE_SECTION: "join-invite-section",
|
|
538
|
+
JOIN_BUTTON: "join-room-button",
|
|
539
|
+
START_BUTTON: "watchtogether-start-button",
|
|
540
|
+
LEAVE_BUTTON: "watchtogether-leave-button",
|
|
541
|
+
JOINED_ROOM_SECTION: "joined-room-section",
|
|
542
|
+
ACTION_SECTION: "watchtogether-action-section",
|
|
543
|
+
CREATE_ROOM_BUTTON: "watchtogether-create-room-button"
|
|
544
|
+
};
|
|
545
|
+
var SECTION_NAMES = {
|
|
546
|
+
JOIN_AND_INVITE: "JOIN_AND_INVITE",
|
|
547
|
+
JOINED_ROOM: "JOINED_ROOM"
|
|
548
|
+
};
|
|
549
|
+
var WatchTogetherMessages = {
|
|
550
|
+
START_SUCCESS: "WatchTogether session started successfully!",
|
|
551
|
+
JOIN_SUCCESS: "Joined the WatchTogether session!",
|
|
552
|
+
LEAVE_SUCCESS: "You have left the WatchTogether session.",
|
|
553
|
+
INVALID_ARGS: "Missing required arguments. Please check the usage.",
|
|
554
|
+
CONNECTION_SUCCESS: "Connected to WatchTogether service.",
|
|
555
|
+
DISCONNECTED: "Disconnected from WatchTogether service."
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// src/modules/watchTogether/WatchTogetherModule.ts
|
|
559
|
+
var WatchTogetherModule = class {
|
|
560
|
+
// Flag for persistent connection
|
|
561
|
+
constructor(http, config) {
|
|
562
|
+
this.http = http;
|
|
563
|
+
this.config = config;
|
|
564
|
+
this.socket = null;
|
|
565
|
+
this.currentRoom = null;
|
|
566
|
+
this.syncInterval = null;
|
|
567
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
568
|
+
this.connectionStatus = {
|
|
569
|
+
connected: false,
|
|
570
|
+
connecting: false,
|
|
571
|
+
authenticated: false,
|
|
572
|
+
inRoom: false,
|
|
573
|
+
roomId: null,
|
|
574
|
+
error: null
|
|
575
|
+
};
|
|
576
|
+
this.shouldStayConnected = false;
|
|
577
|
+
if (config.autoConnect || config.persistentConnection) {
|
|
578
|
+
this.initializePersistentConnection();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Initialize persistent connection (always on)
|
|
583
|
+
*/
|
|
584
|
+
initializePersistentConnection() {
|
|
585
|
+
this.shouldStayConnected = true;
|
|
586
|
+
this.connect().catch(() => {
|
|
587
|
+
setTimeout(() => {
|
|
588
|
+
if (this.shouldStayConnected) {
|
|
589
|
+
this.initializePersistentConnection();
|
|
590
|
+
}
|
|
591
|
+
}, 2e3);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Connect to the Watch Together service
|
|
596
|
+
*/
|
|
597
|
+
async connect() {
|
|
598
|
+
if (this.socket?.connected) {
|
|
599
|
+
this.updateConnectionStatus({ connected: true, connecting: false });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const isPersistent = this.config.persistentConnection || this.shouldStayConnected;
|
|
603
|
+
const token = this.http.getAuthToken();
|
|
604
|
+
if (!token && !isPersistent) {
|
|
605
|
+
throw new WatchTogetherError("Authentication required. Please login first.", "UNAUTHORIZED");
|
|
606
|
+
}
|
|
607
|
+
this.updateConnectionStatus({ connecting: true, error: null });
|
|
608
|
+
const namespace = this.config.namespace || WatchTogetherConstants.NAMESPACE;
|
|
609
|
+
const url = `${this.config.baseURL}${namespace}`;
|
|
610
|
+
const socketOptions = {
|
|
611
|
+
query: { connectionId: "watchTogether" },
|
|
612
|
+
forceNew: !this.socket,
|
|
613
|
+
// Reuse socket if exists
|
|
614
|
+
reconnection: true,
|
|
615
|
+
reconnectionAttempts: Infinity,
|
|
616
|
+
// Infinite reconnection attempts
|
|
617
|
+
reconnectionDelay: 1e3,
|
|
618
|
+
reconnectionDelayMax: 5e3,
|
|
619
|
+
timeout: 1e4
|
|
620
|
+
};
|
|
621
|
+
if (token) {
|
|
622
|
+
socketOptions.extraHeaders = {
|
|
623
|
+
Authorization: `Bearer ${token}`
|
|
624
|
+
};
|
|
625
|
+
this.updateConnectionStatus({ authenticated: true });
|
|
626
|
+
}
|
|
627
|
+
this.socket = socket_ioClient.io(url, socketOptions);
|
|
628
|
+
this.setupSocketListeners();
|
|
629
|
+
return new Promise((resolve, reject) => {
|
|
630
|
+
const timeout = setTimeout(() => {
|
|
631
|
+
if (!isPersistent) {
|
|
632
|
+
reject(new WatchTogetherError("Connection timeout", "CONNECTION_ERROR"));
|
|
633
|
+
} else {
|
|
634
|
+
resolve();
|
|
635
|
+
}
|
|
636
|
+
}, 1e4);
|
|
637
|
+
this.socket.once("connect", () => {
|
|
638
|
+
clearTimeout(timeout);
|
|
639
|
+
this.updateConnectionStatus({
|
|
640
|
+
connected: true,
|
|
641
|
+
connecting: false,
|
|
642
|
+
error: null
|
|
643
|
+
});
|
|
644
|
+
this.emit("connect", void 0);
|
|
645
|
+
resolve();
|
|
646
|
+
});
|
|
647
|
+
this.socket.once("connect_error", (error) => {
|
|
648
|
+
clearTimeout(timeout);
|
|
649
|
+
this.updateConnectionStatus({
|
|
650
|
+
connected: false,
|
|
651
|
+
connecting: false,
|
|
652
|
+
error: error.message
|
|
653
|
+
});
|
|
654
|
+
if (!isPersistent) {
|
|
655
|
+
reject(new WatchTogetherError(error.message || "Connection failed", "CONNECTION_ERROR"));
|
|
656
|
+
} else {
|
|
657
|
+
resolve();
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Disconnect from the Watch Together service
|
|
664
|
+
*/
|
|
665
|
+
disconnect() {
|
|
666
|
+
this.shouldStayConnected = false;
|
|
667
|
+
this.stopSyncCheck();
|
|
668
|
+
if (this.socket) {
|
|
669
|
+
this.socket.removeAllListeners();
|
|
670
|
+
this.socket.disconnect();
|
|
671
|
+
this.socket = null;
|
|
672
|
+
}
|
|
673
|
+
this.currentRoom = null;
|
|
674
|
+
this.updateConnectionStatus({
|
|
675
|
+
connected: false,
|
|
676
|
+
connecting: false,
|
|
677
|
+
authenticated: false,
|
|
678
|
+
inRoom: false,
|
|
679
|
+
roomId: null
|
|
680
|
+
});
|
|
681
|
+
this.emit("disconnect", "manual");
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Enable persistent connection mode (always on)
|
|
685
|
+
*/
|
|
686
|
+
enablePersistentConnection() {
|
|
687
|
+
if (!this.shouldStayConnected) {
|
|
688
|
+
this.shouldStayConnected = true;
|
|
689
|
+
this.initializePersistentConnection();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Disable persistent connection mode
|
|
694
|
+
*/
|
|
695
|
+
disablePersistentConnection() {
|
|
696
|
+
this.shouldStayConnected = false;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Create a new room
|
|
700
|
+
*/
|
|
701
|
+
async createRoom(options) {
|
|
702
|
+
this.ensureConnected();
|
|
703
|
+
return new Promise((resolve, reject) => {
|
|
704
|
+
const timeout = setTimeout(() => {
|
|
705
|
+
reject(new WatchTogetherError("Create room timeout", "CONNECTION_ERROR"));
|
|
706
|
+
}, 5e3);
|
|
707
|
+
this.socket.once("roomState", (room) => {
|
|
708
|
+
clearTimeout(timeout);
|
|
709
|
+
this.currentRoom = room;
|
|
710
|
+
this.startSyncCheck();
|
|
711
|
+
resolve(room);
|
|
712
|
+
});
|
|
713
|
+
this.socket.once("error", (error) => {
|
|
714
|
+
clearTimeout(timeout);
|
|
715
|
+
reject(new WatchTogetherError(error.message, error.code));
|
|
716
|
+
});
|
|
717
|
+
this.socket.emit("createRoom", options || {});
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Join an existing room or create a new one
|
|
722
|
+
*/
|
|
723
|
+
async joinRoom(options) {
|
|
724
|
+
this.ensureConnected();
|
|
725
|
+
return new Promise((resolve, reject) => {
|
|
726
|
+
const timeout = setTimeout(() => {
|
|
727
|
+
reject(new WatchTogetherError("Join room timeout", "CONNECTION_ERROR"));
|
|
728
|
+
}, 5e3);
|
|
729
|
+
this.socket.once("roomState", (room) => {
|
|
730
|
+
clearTimeout(timeout);
|
|
731
|
+
this.currentRoom = room;
|
|
732
|
+
this.updateConnectionStatus({
|
|
733
|
+
inRoom: true,
|
|
734
|
+
roomId: room.roomId
|
|
735
|
+
});
|
|
736
|
+
this.startSyncCheck();
|
|
737
|
+
resolve(room);
|
|
738
|
+
});
|
|
739
|
+
this.socket.once("error", (error) => {
|
|
740
|
+
clearTimeout(timeout);
|
|
741
|
+
reject(new WatchTogetherError(error.message, error.code));
|
|
742
|
+
});
|
|
743
|
+
this.socket.emit("joinRoom", options);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Leave the current room
|
|
748
|
+
*/
|
|
749
|
+
leaveRoom() {
|
|
750
|
+
if (!this.currentRoom) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
this.ensureConnected();
|
|
754
|
+
this.stopSyncCheck();
|
|
755
|
+
this.socket.emit("leaveRoom", { roomId: this.currentRoom.roomId });
|
|
756
|
+
this.currentRoom = null;
|
|
757
|
+
this.updateConnectionStatus({
|
|
758
|
+
inRoom: false,
|
|
759
|
+
roomId: null
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Play the video (synced to all users)
|
|
764
|
+
*/
|
|
765
|
+
play() {
|
|
766
|
+
this.ensureInRoom();
|
|
767
|
+
this.updateSyncState("play");
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Pause the video (synced to all users)
|
|
771
|
+
*/
|
|
772
|
+
pause() {
|
|
773
|
+
this.ensureInRoom();
|
|
774
|
+
this.updateSyncState("pause");
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Seek to a specific time (synced to all users)
|
|
778
|
+
*/
|
|
779
|
+
seek(timeInSeconds) {
|
|
780
|
+
this.ensureInRoom();
|
|
781
|
+
this.updateSyncState("seek", timeInSeconds);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Request sync state from server
|
|
785
|
+
*/
|
|
786
|
+
requestSync() {
|
|
787
|
+
this.ensureInRoom();
|
|
788
|
+
this.socket.emit("checkSync", { roomId: this.currentRoom.roomId });
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Update current file information
|
|
792
|
+
*/
|
|
793
|
+
updateFileInfo(fileInfo) {
|
|
794
|
+
this.ensureInRoom();
|
|
795
|
+
this.socket.emit("updateFileInfo", {
|
|
796
|
+
roomId: this.currentRoom.roomId,
|
|
797
|
+
fileInfo
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Send a chat message
|
|
802
|
+
*/
|
|
803
|
+
async sendMessage(text) {
|
|
804
|
+
this.ensureInRoom();
|
|
805
|
+
const tempMessageId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
806
|
+
return new Promise((resolve, reject) => {
|
|
807
|
+
const timeout = setTimeout(() => {
|
|
808
|
+
reject(new WatchTogetherError("Send message timeout", "CONNECTION_ERROR"));
|
|
809
|
+
}, 5e3);
|
|
810
|
+
this.socket.once("chat:ack", () => {
|
|
811
|
+
clearTimeout(timeout);
|
|
812
|
+
resolve();
|
|
813
|
+
});
|
|
814
|
+
this.socket.once("chat:error", (error) => {
|
|
815
|
+
clearTimeout(timeout);
|
|
816
|
+
reject(new WatchTogetherError(error.message, "CHAT_ERROR"));
|
|
817
|
+
});
|
|
818
|
+
this.socket.emit("chat:send", {
|
|
819
|
+
roomId: this.currentRoom.roomId,
|
|
820
|
+
text,
|
|
821
|
+
tempMessageId
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get chat history for current room
|
|
827
|
+
*/
|
|
828
|
+
getChatHistory() {
|
|
829
|
+
return this.currentRoom?.chat || [];
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Get current room state
|
|
833
|
+
*/
|
|
834
|
+
getCurrentRoom() {
|
|
835
|
+
return this.currentRoom;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Check if connected to the service
|
|
839
|
+
*/
|
|
840
|
+
isConnected() {
|
|
841
|
+
return this.socket?.connected || false;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Check if currently in a room
|
|
845
|
+
*/
|
|
846
|
+
isInRoom() {
|
|
847
|
+
return this.currentRoom !== null;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Get current connection status
|
|
851
|
+
*/
|
|
852
|
+
getConnectionStatus() {
|
|
853
|
+
return { ...this.connectionStatus };
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Check if user is online (connected to socket)
|
|
857
|
+
*/
|
|
858
|
+
isOnline() {
|
|
859
|
+
return this.connectionStatus.connected;
|
|
860
|
+
}
|
|
861
|
+
// ============================================
|
|
862
|
+
// EVENT LISTENERS
|
|
863
|
+
// ============================================
|
|
864
|
+
/**
|
|
865
|
+
* Listen for room state events
|
|
866
|
+
*/
|
|
867
|
+
onRoomState(callback) {
|
|
868
|
+
return this.addEventListener("roomState", callback);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Listen for room update events
|
|
872
|
+
*/
|
|
873
|
+
onRoomUpdate(callback) {
|
|
874
|
+
return this.addEventListener("roomUpdates", callback);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Listen for sync state events
|
|
878
|
+
*/
|
|
879
|
+
onSyncState(callback) {
|
|
880
|
+
return this.addEventListener("syncState", callback);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Listen for chat messages
|
|
884
|
+
*/
|
|
885
|
+
onChatMessage(callback) {
|
|
886
|
+
return this.addEventListener("chat:receive", callback);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Listen for system messages
|
|
890
|
+
*/
|
|
891
|
+
onSystemMessage(callback) {
|
|
892
|
+
return this.addEventListener("newMessage", callback);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Listen for chat history
|
|
896
|
+
*/
|
|
897
|
+
onChatHistory(callback) {
|
|
898
|
+
return this.addEventListener("chatHistory", callback);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Listen for errors
|
|
902
|
+
*/
|
|
903
|
+
onError(callback) {
|
|
904
|
+
return this.addEventListener("error", callback);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Listen for connection events
|
|
908
|
+
*/
|
|
909
|
+
onConnect(callback) {
|
|
910
|
+
return this.addEventListener("connect", callback);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Listen for disconnection events
|
|
914
|
+
*/
|
|
915
|
+
onDisconnect(callback) {
|
|
916
|
+
return this.addEventListener("disconnect", callback);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Listen for connection status changes
|
|
920
|
+
*/
|
|
921
|
+
onConnectionStatusChange(callback) {
|
|
922
|
+
return this.addEventListener("connectionStatusChange", callback);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Listen for online/offline status
|
|
926
|
+
*/
|
|
927
|
+
onOnlineStatusChange(callback) {
|
|
928
|
+
return this.addEventListener("onlineStatusChange", callback);
|
|
929
|
+
}
|
|
930
|
+
// ============================================
|
|
931
|
+
// PRIVATE METHODS
|
|
932
|
+
// ============================================
|
|
933
|
+
setupSocketListeners() {
|
|
934
|
+
if (!this.socket) return;
|
|
935
|
+
this.socket.on("roomState", (room) => {
|
|
936
|
+
this.currentRoom = room;
|
|
937
|
+
this.updateConnectionStatus({
|
|
938
|
+
inRoom: true,
|
|
939
|
+
roomId: room.roomId
|
|
940
|
+
});
|
|
941
|
+
this.emit("roomState", room);
|
|
942
|
+
});
|
|
943
|
+
this.socket.on("roomUpdates", (update) => {
|
|
944
|
+
if (this.currentRoom && update.roomUpdates) {
|
|
945
|
+
this.currentRoom = {
|
|
946
|
+
...this.currentRoom,
|
|
947
|
+
...update.roomUpdates
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
this.emit("roomUpdates", update);
|
|
951
|
+
});
|
|
952
|
+
this.socket.on("syncState", (syncState) => {
|
|
953
|
+
if (this.currentRoom) {
|
|
954
|
+
this.currentRoom.syncState = syncState;
|
|
955
|
+
}
|
|
956
|
+
this.emit("syncState", syncState);
|
|
957
|
+
});
|
|
958
|
+
this.socket.on("chat:receive", (data) => {
|
|
959
|
+
if (this.currentRoom) {
|
|
960
|
+
this.currentRoom.chat.push(data.message);
|
|
961
|
+
}
|
|
962
|
+
this.emit("chat:receive", data.message);
|
|
963
|
+
});
|
|
964
|
+
this.socket.on("newMessage", (data) => {
|
|
965
|
+
const message = { ...data.message, systemMessage: true };
|
|
966
|
+
if (this.currentRoom) {
|
|
967
|
+
this.currentRoom.chat.push(message);
|
|
968
|
+
}
|
|
969
|
+
this.emit("newMessage", message);
|
|
970
|
+
});
|
|
971
|
+
this.socket.on("chatHistory", (data) => {
|
|
972
|
+
if (this.currentRoom) {
|
|
973
|
+
this.currentRoom.chat = data.messages;
|
|
974
|
+
}
|
|
975
|
+
this.emit("chatHistory", data.messages);
|
|
976
|
+
});
|
|
977
|
+
this.socket.on("chat:ack", (data) => {
|
|
978
|
+
this.emit("chat:ack", data.message);
|
|
979
|
+
});
|
|
980
|
+
this.socket.on("chat:error", (data) => {
|
|
981
|
+
this.emit("chat:error", data);
|
|
982
|
+
});
|
|
983
|
+
this.socket.on("error", (data) => {
|
|
984
|
+
this.updateConnectionStatus({ error: data.message });
|
|
985
|
+
this.emit("error", data);
|
|
986
|
+
});
|
|
987
|
+
this.socket.on("connect", () => {
|
|
988
|
+
this.updateConnectionStatus({
|
|
989
|
+
connected: true,
|
|
990
|
+
connecting: false,
|
|
991
|
+
error: null
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
this.socket.on("disconnect", (reason) => {
|
|
995
|
+
this.stopSyncCheck();
|
|
996
|
+
this.updateConnectionStatus({
|
|
997
|
+
connected: false,
|
|
998
|
+
error: `Disconnected: ${reason}`
|
|
999
|
+
});
|
|
1000
|
+
this.emit("disconnect", reason);
|
|
1001
|
+
if (this.shouldStayConnected) {
|
|
1002
|
+
setTimeout(() => {
|
|
1003
|
+
if (this.shouldStayConnected && !this.socket?.connected) {
|
|
1004
|
+
this.connect().catch(() => {
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}, 2e3);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
this.socket.on("connect_error", async (error) => {
|
|
1011
|
+
this.updateConnectionStatus({
|
|
1012
|
+
connected: false,
|
|
1013
|
+
connecting: false,
|
|
1014
|
+
error: error.message
|
|
1015
|
+
});
|
|
1016
|
+
if (error.message.includes("Unauthorized") || error.message.includes("401")) {
|
|
1017
|
+
try {
|
|
1018
|
+
await this.refreshTokenAndReconnect();
|
|
1019
|
+
} catch {
|
|
1020
|
+
this.emit("error", {
|
|
1021
|
+
code: "UNAUTHORIZED",
|
|
1022
|
+
message: "Session expired. Please login again."
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (this.shouldStayConnected) {
|
|
1027
|
+
setTimeout(() => {
|
|
1028
|
+
if (this.shouldStayConnected && !this.socket?.connected) {
|
|
1029
|
+
this.connect().catch(() => {
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}, 3e3);
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
this.socket.on("reconnect_attempt", (attempt) => {
|
|
1036
|
+
this.updateConnectionStatus({
|
|
1037
|
+
connecting: true,
|
|
1038
|
+
error: `Reconnecting (attempt ${attempt})...`
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
this.socket.on("reconnect", (_attempt) => {
|
|
1042
|
+
this.updateConnectionStatus({
|
|
1043
|
+
connected: true,
|
|
1044
|
+
connecting: false,
|
|
1045
|
+
error: null
|
|
1046
|
+
});
|
|
1047
|
+
this.emit("connect", void 0);
|
|
1048
|
+
});
|
|
1049
|
+
this.socket.on("reconnect_failed", () => {
|
|
1050
|
+
this.updateConnectionStatus({
|
|
1051
|
+
connected: false,
|
|
1052
|
+
connecting: false,
|
|
1053
|
+
error: "Reconnection failed"
|
|
1054
|
+
});
|
|
1055
|
+
if (this.shouldStayConnected) {
|
|
1056
|
+
setTimeout(() => {
|
|
1057
|
+
if (this.shouldStayConnected) {
|
|
1058
|
+
this.connect().catch(() => {
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}, 5e3);
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
async refreshTokenAndReconnect() {
|
|
1066
|
+
if (!this.shouldStayConnected) {
|
|
1067
|
+
this.disconnect();
|
|
1068
|
+
throw new WatchTogetherError("Session expired. Please login again.", "UNAUTHORIZED");
|
|
1069
|
+
}
|
|
1070
|
+
this.updateConnectionStatus({ authenticated: false });
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Update authentication token after refresh
|
|
1074
|
+
*/
|
|
1075
|
+
updateAuthToken(token) {
|
|
1076
|
+
if (this.socket && this.socket.io.opts.extraHeaders) {
|
|
1077
|
+
this.socket.io.opts.extraHeaders.Authorization = `Bearer ${token}`;
|
|
1078
|
+
this.updateConnectionStatus({ authenticated: true, error: null });
|
|
1079
|
+
if (!this.socket.connected) {
|
|
1080
|
+
this.socket.connect();
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Update connection status and emit events
|
|
1086
|
+
*/
|
|
1087
|
+
updateConnectionStatus(updates) {
|
|
1088
|
+
const previousOnlineStatus = this.connectionStatus.connected;
|
|
1089
|
+
this.connectionStatus = {
|
|
1090
|
+
...this.connectionStatus,
|
|
1091
|
+
...updates
|
|
1092
|
+
};
|
|
1093
|
+
this.emit("connectionStatusChange", this.connectionStatus);
|
|
1094
|
+
if (previousOnlineStatus !== this.connectionStatus.connected) {
|
|
1095
|
+
this.emit("onlineStatusChange", this.connectionStatus.connected);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
updateSyncState(action, value) {
|
|
1099
|
+
this.ensureInRoom();
|
|
1100
|
+
const payload = {
|
|
1101
|
+
roomId: this.currentRoom.roomId,
|
|
1102
|
+
action
|
|
1103
|
+
};
|
|
1104
|
+
if (action === "seek" && value !== void 0) {
|
|
1105
|
+
payload.value = value;
|
|
1106
|
+
}
|
|
1107
|
+
this.socket.emit("updateSyncState", payload);
|
|
1108
|
+
}
|
|
1109
|
+
startSyncCheck() {
|
|
1110
|
+
this.stopSyncCheck();
|
|
1111
|
+
this.syncInterval = setInterval(() => {
|
|
1112
|
+
if (this.currentRoom && this.socket?.connected) {
|
|
1113
|
+
this.socket.emit("checkSync", { roomId: this.currentRoom.roomId });
|
|
1114
|
+
}
|
|
1115
|
+
}, WatchTogetherConstants.SYNC_CHECK_INTERVAL_MS);
|
|
1116
|
+
}
|
|
1117
|
+
stopSyncCheck() {
|
|
1118
|
+
if (this.syncInterval) {
|
|
1119
|
+
clearInterval(this.syncInterval);
|
|
1120
|
+
this.syncInterval = null;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
ensureConnected() {
|
|
1124
|
+
if (!this.socket?.connected) {
|
|
1125
|
+
throw new WatchTogetherError(
|
|
1126
|
+
"Not connected. Please call connect() first.",
|
|
1127
|
+
"CONNECTION_ERROR"
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
ensureInRoom() {
|
|
1132
|
+
this.ensureConnected();
|
|
1133
|
+
if (!this.currentRoom) {
|
|
1134
|
+
throw new WatchTogetherError(
|
|
1135
|
+
"Not in a room. Please join or create a room first.",
|
|
1136
|
+
"NOT_IN_ROOM"
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
addEventListener(event, callback) {
|
|
1141
|
+
if (!this.eventListeners.has(event)) {
|
|
1142
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
1143
|
+
}
|
|
1144
|
+
this.eventListeners.get(event).add(callback);
|
|
1145
|
+
return () => {
|
|
1146
|
+
const listeners = this.eventListeners.get(event);
|
|
1147
|
+
if (listeners) {
|
|
1148
|
+
listeners.delete(callback);
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
emit(event, data) {
|
|
1153
|
+
const listeners = this.eventListeners.get(event);
|
|
1154
|
+
if (listeners) {
|
|
1155
|
+
listeners.forEach((callback) => {
|
|
1156
|
+
try {
|
|
1157
|
+
callback(data);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
console.error(`Error in event listener for ${event}:`, error);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
// src/sdk/HarmoniSDK.ts
|
|
1167
|
+
var HarmoniSDK = class {
|
|
1168
|
+
constructor(config) {
|
|
1169
|
+
this.httpClient = new HttpClient(config);
|
|
1170
|
+
if (config.autoRefreshToken) {
|
|
1171
|
+
this.setupAutoRefresh();
|
|
1172
|
+
}
|
|
1173
|
+
this.auth = new AuthModule(this.httpClient);
|
|
1174
|
+
this.user = new UserModule(this.httpClient);
|
|
1175
|
+
this.watchTogether = new WatchTogetherModule(this.httpClient, {
|
|
1176
|
+
baseURL: config.baseURL,
|
|
1177
|
+
namespace: config.watchTogetherNamespace,
|
|
1178
|
+
persistentConnection: config.persistentConnection
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Setup automatic token refresh
|
|
1183
|
+
*/
|
|
1184
|
+
setupAutoRefresh() {
|
|
1185
|
+
this.httpClient.setRefreshTokenFunction(async () => {
|
|
1186
|
+
const accessToken = await this.auth.refreshAccessToken();
|
|
1187
|
+
this.storeAccessToken(accessToken);
|
|
1188
|
+
return accessToken;
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Set authentication token
|
|
1193
|
+
*/
|
|
1194
|
+
setAuthToken(token) {
|
|
1195
|
+
this.httpClient.setAuthToken(token);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Get current authentication token
|
|
1199
|
+
*/
|
|
1200
|
+
getAuthToken() {
|
|
1201
|
+
return this.httpClient.getAuthToken();
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Store access token (override this method or use storage)
|
|
1205
|
+
*/
|
|
1206
|
+
storeAccessToken(accessToken) {
|
|
1207
|
+
if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
|
|
1208
|
+
const storage = globalThis.localStorage;
|
|
1209
|
+
storage?.setItem("harmoni_access_token", accessToken);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Get the underlying HTTP client (for advanced usage)
|
|
1214
|
+
*/
|
|
1215
|
+
getHttpClient() {
|
|
1216
|
+
return this.httpClient;
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
// src/types/player.ts
|
|
1221
|
+
var PlayerConstants = {
|
|
1222
|
+
DEFAULT_POLLING_INTERVAL_MS: 2e3,
|
|
1223
|
+
SEEK_THRESHOLD_SECONDS: 0.5,
|
|
1224
|
+
COMMAND_STACK_TIMEOUT_MS: 3e3,
|
|
1225
|
+
MIN_INTERVAL_MS: 100
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
// src/utils/string.ts
|
|
1229
|
+
var string_exports = {};
|
|
1230
|
+
__export(string_exports, {
|
|
1231
|
+
camelToSnake: () => camelToSnake,
|
|
1232
|
+
capitalize: () => capitalize,
|
|
1233
|
+
isValidEmail: () => isValidEmail,
|
|
1234
|
+
randomString: () => randomString,
|
|
1235
|
+
slugify: () => slugify,
|
|
1236
|
+
snakeToCamel: () => snakeToCamel,
|
|
1237
|
+
titleCase: () => titleCase,
|
|
1238
|
+
truncate: () => truncate
|
|
1239
|
+
});
|
|
1240
|
+
function capitalize(str) {
|
|
1241
|
+
if (!str) return "";
|
|
1242
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
1243
|
+
}
|
|
1244
|
+
function titleCase(str) {
|
|
1245
|
+
if (!str) return "";
|
|
1246
|
+
return str.toLowerCase().split(" ").map((word) => capitalize(word)).join(" ");
|
|
1247
|
+
}
|
|
1248
|
+
function camelToSnake(str) {
|
|
1249
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1250
|
+
}
|
|
1251
|
+
function snakeToCamel(str) {
|
|
1252
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1253
|
+
}
|
|
1254
|
+
function truncate(str, length, suffix = "...") {
|
|
1255
|
+
if (!str || str.length <= length) return str;
|
|
1256
|
+
return str.slice(0, length - suffix.length) + suffix;
|
|
1257
|
+
}
|
|
1258
|
+
function isValidEmail(email) {
|
|
1259
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1260
|
+
return emailRegex.test(email);
|
|
1261
|
+
}
|
|
1262
|
+
function randomString(length) {
|
|
1263
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1264
|
+
let result = "";
|
|
1265
|
+
for (let i = 0; i < length; i++) {
|
|
1266
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1267
|
+
}
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
function slugify(str) {
|
|
1271
|
+
return str.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/utils/date.ts
|
|
1275
|
+
var date_exports = {};
|
|
1276
|
+
__export(date_exports, {
|
|
1277
|
+
addDays: () => addDays,
|
|
1278
|
+
addHours: () => addHours,
|
|
1279
|
+
formatDate: () => formatDate,
|
|
1280
|
+
isFuture: () => isFuture,
|
|
1281
|
+
isPast: () => isPast,
|
|
1282
|
+
isToday: () => isToday,
|
|
1283
|
+
isValidDate: () => isValidDate,
|
|
1284
|
+
timeAgo: () => timeAgo,
|
|
1285
|
+
toISOString: () => toISOString
|
|
1286
|
+
});
|
|
1287
|
+
function toISOString(date) {
|
|
1288
|
+
return new Date(date).toISOString();
|
|
1289
|
+
}
|
|
1290
|
+
function isValidDate(date) {
|
|
1291
|
+
return date instanceof Date && !isNaN(date.getTime());
|
|
1292
|
+
}
|
|
1293
|
+
function timeAgo(date) {
|
|
1294
|
+
const now = /* @__PURE__ */ new Date();
|
|
1295
|
+
const past = new Date(date);
|
|
1296
|
+
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1e3);
|
|
1297
|
+
if (diffInSeconds < 60) return "just now";
|
|
1298
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
|
1299
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
|
1300
|
+
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
|
1301
|
+
if (diffInSeconds < 2592e3) return `${Math.floor(diffInSeconds / 604800)} weeks ago`;
|
|
1302
|
+
if (diffInSeconds < 31536e3) return `${Math.floor(diffInSeconds / 2592e3)} months ago`;
|
|
1303
|
+
return `${Math.floor(diffInSeconds / 31536e3)} years ago`;
|
|
1304
|
+
}
|
|
1305
|
+
function addDays(date, days) {
|
|
1306
|
+
const result = new Date(date);
|
|
1307
|
+
result.setDate(result.getDate() + days);
|
|
1308
|
+
return result;
|
|
1309
|
+
}
|
|
1310
|
+
function addHours(date, hours) {
|
|
1311
|
+
const result = new Date(date);
|
|
1312
|
+
result.setHours(result.getHours() + hours);
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
function formatDate(date, format = "YYYY-MM-DD") {
|
|
1316
|
+
const d = new Date(date);
|
|
1317
|
+
const year = d.getFullYear();
|
|
1318
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
1319
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1320
|
+
const hours = String(d.getHours()).padStart(2, "0");
|
|
1321
|
+
const minutes = String(d.getMinutes()).padStart(2, "0");
|
|
1322
|
+
const seconds = String(d.getSeconds()).padStart(2, "0");
|
|
1323
|
+
return format.replace("YYYY", String(year)).replace("MM", month).replace("DD", day).replace("HH", hours).replace("mm", minutes).replace("ss", seconds);
|
|
1324
|
+
}
|
|
1325
|
+
function isToday(date) {
|
|
1326
|
+
const today = /* @__PURE__ */ new Date();
|
|
1327
|
+
const checkDate = new Date(date);
|
|
1328
|
+
return checkDate.getDate() === today.getDate() && checkDate.getMonth() === today.getMonth() && checkDate.getFullYear() === today.getFullYear();
|
|
1329
|
+
}
|
|
1330
|
+
function isPast(date) {
|
|
1331
|
+
return new Date(date).getTime() < Date.now();
|
|
1332
|
+
}
|
|
1333
|
+
function isFuture(date) {
|
|
1334
|
+
return new Date(date).getTime() > Date.now();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/utils/object.ts
|
|
1338
|
+
var object_exports = {};
|
|
1339
|
+
__export(object_exports, {
|
|
1340
|
+
deepClone: () => deepClone,
|
|
1341
|
+
deepMerge: () => deepMerge,
|
|
1342
|
+
getNestedValue: () => getNestedValue,
|
|
1343
|
+
isEmpty: () => isEmpty,
|
|
1344
|
+
isObject: () => isObject,
|
|
1345
|
+
omit: () => omit,
|
|
1346
|
+
pick: () => pick,
|
|
1347
|
+
setNestedValue: () => setNestedValue
|
|
1348
|
+
});
|
|
1349
|
+
function deepClone(obj) {
|
|
1350
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
1351
|
+
if (obj instanceof Date) return new Date(obj.getTime());
|
|
1352
|
+
if (obj instanceof Array) return obj.map((item) => deepClone(item));
|
|
1353
|
+
if (obj instanceof Object) {
|
|
1354
|
+
const clonedObj = {};
|
|
1355
|
+
for (const key in obj) {
|
|
1356
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
1357
|
+
clonedObj[key] = deepClone(obj[key]);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return clonedObj;
|
|
1361
|
+
}
|
|
1362
|
+
return obj;
|
|
1363
|
+
}
|
|
1364
|
+
function deepMerge(target, source) {
|
|
1365
|
+
const output = { ...target };
|
|
1366
|
+
if (isObject(target) && isObject(source)) {
|
|
1367
|
+
Object.keys(source).forEach((key) => {
|
|
1368
|
+
const sourceValue = source[key];
|
|
1369
|
+
const targetValue = target[key];
|
|
1370
|
+
if (isObject(sourceValue) && isObject(targetValue)) {
|
|
1371
|
+
output[key] = deepMerge(
|
|
1372
|
+
targetValue,
|
|
1373
|
+
sourceValue
|
|
1374
|
+
);
|
|
1375
|
+
} else {
|
|
1376
|
+
output[key] = sourceValue;
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
return output;
|
|
1381
|
+
}
|
|
1382
|
+
function isObject(value) {
|
|
1383
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1384
|
+
}
|
|
1385
|
+
function pick(obj, keys) {
|
|
1386
|
+
const result = {};
|
|
1387
|
+
keys.forEach((key) => {
|
|
1388
|
+
if (key in obj) {
|
|
1389
|
+
result[key] = obj[key];
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
return result;
|
|
1393
|
+
}
|
|
1394
|
+
function omit(obj, keys) {
|
|
1395
|
+
const result = { ...obj };
|
|
1396
|
+
keys.forEach((key) => {
|
|
1397
|
+
delete result[key];
|
|
1398
|
+
});
|
|
1399
|
+
return result;
|
|
1400
|
+
}
|
|
1401
|
+
function isEmpty(obj) {
|
|
1402
|
+
if (obj === null || obj === void 0) return true;
|
|
1403
|
+
if (typeof obj === "string" || Array.isArray(obj)) return obj.length === 0;
|
|
1404
|
+
if (typeof obj === "object") return Object.keys(obj).length === 0;
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
function getNestedValue(obj, path) {
|
|
1408
|
+
const keys = path.split(".");
|
|
1409
|
+
let result = obj;
|
|
1410
|
+
for (const key of keys) {
|
|
1411
|
+
if (result && typeof result === "object" && key in result) {
|
|
1412
|
+
result = result[key];
|
|
1413
|
+
} else {
|
|
1414
|
+
return void 0;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return result;
|
|
1418
|
+
}
|
|
1419
|
+
function setNestedValue(obj, path, value) {
|
|
1420
|
+
const keys = path.split(".");
|
|
1421
|
+
const lastKey = keys.pop();
|
|
1422
|
+
if (!lastKey) return;
|
|
1423
|
+
let current = obj;
|
|
1424
|
+
for (const key of keys) {
|
|
1425
|
+
if (!(key in current) || typeof current[key] !== "object") {
|
|
1426
|
+
current[key] = {};
|
|
1427
|
+
}
|
|
1428
|
+
current = current[key];
|
|
1429
|
+
}
|
|
1430
|
+
current[lastKey] = value;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/utils/validation.ts
|
|
1434
|
+
var validation_exports = {};
|
|
1435
|
+
__export(validation_exports, {
|
|
1436
|
+
maxLength: () => maxLength,
|
|
1437
|
+
minLength: () => minLength,
|
|
1438
|
+
required: () => required,
|
|
1439
|
+
validateEmail: () => validateEmail,
|
|
1440
|
+
validatePassword: () => validatePassword,
|
|
1441
|
+
validatePhone: () => validatePhone,
|
|
1442
|
+
validateUrl: () => validateUrl
|
|
1443
|
+
});
|
|
1444
|
+
function validateEmail(email) {
|
|
1445
|
+
if (!email) {
|
|
1446
|
+
return { valid: false, message: "Email is required" };
|
|
1447
|
+
}
|
|
1448
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1449
|
+
if (!emailRegex.test(email)) {
|
|
1450
|
+
return { valid: false, message: "Invalid email format" };
|
|
1451
|
+
}
|
|
1452
|
+
return { valid: true };
|
|
1453
|
+
}
|
|
1454
|
+
function validatePassword(password, options = {}) {
|
|
1455
|
+
const {
|
|
1456
|
+
minLength: minLength2 = 8,
|
|
1457
|
+
requireUppercase = true,
|
|
1458
|
+
requireLowercase = true,
|
|
1459
|
+
requireNumbers = true,
|
|
1460
|
+
requireSpecialChars = true
|
|
1461
|
+
} = options;
|
|
1462
|
+
if (!password) {
|
|
1463
|
+
return { valid: false, message: "Password is required" };
|
|
1464
|
+
}
|
|
1465
|
+
if (password.length < minLength2) {
|
|
1466
|
+
return { valid: false, message: `Password must be at least ${minLength2} characters` };
|
|
1467
|
+
}
|
|
1468
|
+
if (requireUppercase && !/[A-Z]/.test(password)) {
|
|
1469
|
+
return { valid: false, message: "Password must contain at least one uppercase letter" };
|
|
1470
|
+
}
|
|
1471
|
+
if (requireLowercase && !/[a-z]/.test(password)) {
|
|
1472
|
+
return { valid: false, message: "Password must contain at least one lowercase letter" };
|
|
1473
|
+
}
|
|
1474
|
+
if (requireNumbers && !/\d/.test(password)) {
|
|
1475
|
+
return { valid: false, message: "Password must contain at least one number" };
|
|
1476
|
+
}
|
|
1477
|
+
if (requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
|
1478
|
+
return { valid: false, message: "Password must contain at least one special character" };
|
|
1479
|
+
}
|
|
1480
|
+
return { valid: true };
|
|
1481
|
+
}
|
|
1482
|
+
function validateUrl(url) {
|
|
1483
|
+
if (!url) {
|
|
1484
|
+
return { valid: false, message: "URL is required" };
|
|
1485
|
+
}
|
|
1486
|
+
try {
|
|
1487
|
+
new URL(url);
|
|
1488
|
+
return { valid: true };
|
|
1489
|
+
} catch {
|
|
1490
|
+
return { valid: false, message: "Invalid URL format" };
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function validatePhone(phone) {
|
|
1494
|
+
if (!phone) {
|
|
1495
|
+
return { valid: false, message: "Phone number is required" };
|
|
1496
|
+
}
|
|
1497
|
+
const phoneRegex = /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,9}$/;
|
|
1498
|
+
if (!phoneRegex.test(phone)) {
|
|
1499
|
+
return { valid: false, message: "Invalid phone number format" };
|
|
1500
|
+
}
|
|
1501
|
+
return { valid: true };
|
|
1502
|
+
}
|
|
1503
|
+
function required(value, fieldName = "Field") {
|
|
1504
|
+
if (value === null || value === void 0 || value === "") {
|
|
1505
|
+
return { valid: false, message: `${fieldName} is required` };
|
|
1506
|
+
}
|
|
1507
|
+
return { valid: true };
|
|
1508
|
+
}
|
|
1509
|
+
function minLength(value, min, fieldName = "Field") {
|
|
1510
|
+
if (value.length < min) {
|
|
1511
|
+
return { valid: false, message: `${fieldName} must be at least ${min} characters` };
|
|
1512
|
+
}
|
|
1513
|
+
return { valid: true };
|
|
1514
|
+
}
|
|
1515
|
+
function maxLength(value, max, fieldName = "Field") {
|
|
1516
|
+
if (value.length > max) {
|
|
1517
|
+
return { valid: false, message: `${fieldName} must be at most ${max} characters` };
|
|
1518
|
+
}
|
|
1519
|
+
return { valid: true };
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/utils/storage.ts
|
|
1523
|
+
var isBrowser = typeof globalThis !== "undefined" && "localStorage" in globalThis;
|
|
1524
|
+
var Storage = class {
|
|
1525
|
+
constructor(type = "local") {
|
|
1526
|
+
this.memoryStorage = /* @__PURE__ */ new Map();
|
|
1527
|
+
if (isBrowser) {
|
|
1528
|
+
const global = globalThis;
|
|
1529
|
+
this.storage = type === "local" ? global.localStorage ?? null : global.sessionStorage ?? null;
|
|
1530
|
+
} else {
|
|
1531
|
+
this.storage = null;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Set item in storage
|
|
1536
|
+
*/
|
|
1537
|
+
set(key, value) {
|
|
1538
|
+
try {
|
|
1539
|
+
const serialized = JSON.stringify(value);
|
|
1540
|
+
if (this.storage) {
|
|
1541
|
+
this.storage.setItem(key, serialized);
|
|
1542
|
+
} else {
|
|
1543
|
+
this.memoryStorage.set(key, serialized);
|
|
1544
|
+
}
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1547
|
+
console.error(`Error saving to storage:`, error);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Get item from storage
|
|
1553
|
+
*/
|
|
1554
|
+
get(key) {
|
|
1555
|
+
try {
|
|
1556
|
+
let item;
|
|
1557
|
+
if (this.storage) {
|
|
1558
|
+
item = this.storage.getItem(key);
|
|
1559
|
+
} else {
|
|
1560
|
+
item = this.memoryStorage.get(key);
|
|
1561
|
+
}
|
|
1562
|
+
if (!item) return null;
|
|
1563
|
+
return JSON.parse(item);
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1566
|
+
console.error(`Error reading from storage:`, error);
|
|
1567
|
+
}
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Remove item from storage
|
|
1573
|
+
*/
|
|
1574
|
+
remove(key) {
|
|
1575
|
+
try {
|
|
1576
|
+
if (this.storage) {
|
|
1577
|
+
this.storage.removeItem(key);
|
|
1578
|
+
} else {
|
|
1579
|
+
this.memoryStorage.delete(key);
|
|
1580
|
+
}
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1583
|
+
console.error(`Error removing from storage:`, error);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Clear all items from storage
|
|
1589
|
+
*/
|
|
1590
|
+
clear() {
|
|
1591
|
+
try {
|
|
1592
|
+
if (this.storage) {
|
|
1593
|
+
this.storage.clear();
|
|
1594
|
+
} else {
|
|
1595
|
+
this.memoryStorage.clear();
|
|
1596
|
+
}
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1599
|
+
console.error(`Error clearing storage:`, error);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Check if key exists in storage
|
|
1605
|
+
*/
|
|
1606
|
+
has(key) {
|
|
1607
|
+
if (this.storage) {
|
|
1608
|
+
return this.storage.getItem(key) !== null;
|
|
1609
|
+
} else {
|
|
1610
|
+
return this.memoryStorage.has(key);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Get all keys from storage
|
|
1615
|
+
*/
|
|
1616
|
+
keys() {
|
|
1617
|
+
if (this.storage) {
|
|
1618
|
+
return Object.keys(this.storage);
|
|
1619
|
+
} else {
|
|
1620
|
+
return Array.from(this.memoryStorage.keys());
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Check if storage is available (browser environment)
|
|
1625
|
+
*/
|
|
1626
|
+
isAvailable() {
|
|
1627
|
+
return this.storage !== null;
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
var localStorage = new Storage("local");
|
|
1631
|
+
var sessionStorage = new Storage("session");
|
|
1632
|
+
|
|
1633
|
+
// src/player/stateDetection.ts
|
|
1634
|
+
function identifyUserActions(oldState, newState, intervalMs) {
|
|
1635
|
+
const actions = [];
|
|
1636
|
+
if (!oldState || !newState) return [];
|
|
1637
|
+
const normalizedInterval = Math.max(intervalMs, PlayerConstants.MIN_INTERVAL_MS);
|
|
1638
|
+
if (oldState.isPlaying !== newState.isPlaying) {
|
|
1639
|
+
actions.push({
|
|
1640
|
+
event: newState.isPlaying ? USER_ACTIONS.PLAY : USER_ACTIONS.PAUSE,
|
|
1641
|
+
value: {
|
|
1642
|
+
isPlaying: newState.isPlaying,
|
|
1643
|
+
currentTime: newState.currentTime
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
const timeDifference = Math.abs(oldState.currentTime - newState.currentTime);
|
|
1648
|
+
const expectedTimeAdvance = normalizedInterval / 1e3 * (newState.isPlaying ? 1 : 0);
|
|
1649
|
+
const seekThreshold = Math.max(PlayerConstants.SEEK_THRESHOLD_SECONDS, expectedTimeAdvance * 1.5);
|
|
1650
|
+
if (timeDifference > expectedTimeAdvance + seekThreshold) {
|
|
1651
|
+
actions.push({
|
|
1652
|
+
event: USER_ACTIONS.SEEK,
|
|
1653
|
+
value: { currentTime: newState.currentTime }
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
if (oldState.filename !== newState.filename) {
|
|
1657
|
+
actions.push({
|
|
1658
|
+
event: USER_ACTIONS.FILE_UPDATE,
|
|
1659
|
+
value: {
|
|
1660
|
+
filename: newState.filename === "no-input" ? null : newState.filename
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
if (oldState.loop !== newState.loop) {
|
|
1665
|
+
actions.push({
|
|
1666
|
+
event: USER_ACTIONS.TOGGLE_LOOP,
|
|
1667
|
+
value: { loop: newState.loop }
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
if (oldState.repeat !== newState.repeat) {
|
|
1671
|
+
actions.push({
|
|
1672
|
+
event: USER_ACTIONS.TOGGLE_REPEAT,
|
|
1673
|
+
value: { repeat: newState.repeat }
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
if (oldState.title !== newState.title || oldState.chapter !== newState.chapter) {
|
|
1677
|
+
actions.push({
|
|
1678
|
+
event: USER_ACTIONS.TITLE_OR_CHAPTER_UPDATE,
|
|
1679
|
+
value: { title: newState.title, chapter: newState.chapter }
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
return actions;
|
|
1683
|
+
}
|
|
1684
|
+
function normalizePlayerState(state) {
|
|
1685
|
+
return {
|
|
1686
|
+
isPlaying: state.isPlaying ?? false,
|
|
1687
|
+
currentTime: Number.isFinite(state.currentTime) ? state.currentTime : 0,
|
|
1688
|
+
loop: state.loop ?? false,
|
|
1689
|
+
repeat: state.repeat ?? false,
|
|
1690
|
+
title: state.title ?? "",
|
|
1691
|
+
chapter: state.chapter ?? "",
|
|
1692
|
+
filename: state.filename ?? "",
|
|
1693
|
+
duration: Number.isFinite(state.duration) ? state.duration : 0,
|
|
1694
|
+
filepath: state.filepath ?? ""
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/player/PlayerStatusMonitor.ts
|
|
1699
|
+
var PlayerStatusMonitor = class {
|
|
1700
|
+
constructor(player, onStateChange, onStop, pollingIntervalMs = PlayerConstants.DEFAULT_POLLING_INTERVAL_MS) {
|
|
1701
|
+
this.player = player;
|
|
1702
|
+
this.onStateChange = onStateChange;
|
|
1703
|
+
this.onStop = onStop;
|
|
1704
|
+
this.pollingIntervalMs = pollingIntervalMs;
|
|
1705
|
+
this.lastKnownState = null;
|
|
1706
|
+
this.lastCheckTime = 0;
|
|
1707
|
+
this.pollingTimer = null;
|
|
1708
|
+
this.isRunning = false;
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Starts monitoring the player status.
|
|
1712
|
+
*/
|
|
1713
|
+
start() {
|
|
1714
|
+
if (this.isRunning) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
this.isRunning = true;
|
|
1718
|
+
this.lastCheckTime = Date.now();
|
|
1719
|
+
this.checkStatus();
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Stops monitoring the player status.
|
|
1723
|
+
*/
|
|
1724
|
+
stop() {
|
|
1725
|
+
this.isRunning = false;
|
|
1726
|
+
if (this.pollingTimer) {
|
|
1727
|
+
clearTimeout(this.pollingTimer);
|
|
1728
|
+
this.pollingTimer = null;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Check if monitor is currently running.
|
|
1733
|
+
*/
|
|
1734
|
+
getIsRunning() {
|
|
1735
|
+
return this.isRunning;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Get the last known player state.
|
|
1739
|
+
*/
|
|
1740
|
+
getLastKnownState() {
|
|
1741
|
+
return this.lastKnownState;
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Manually update the last known state (used when app initiates actions).
|
|
1745
|
+
*/
|
|
1746
|
+
updateLastKnownState(state) {
|
|
1747
|
+
this.lastKnownState = state;
|
|
1748
|
+
}
|
|
1749
|
+
async checkStatus() {
|
|
1750
|
+
if (!this.isRunning) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
const isRunning = await this.player.checkPlayerRunning();
|
|
1755
|
+
if (!isRunning) {
|
|
1756
|
+
this.isRunning = false;
|
|
1757
|
+
this.onStop();
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const currentTime = Date.now();
|
|
1761
|
+
const timeSinceLastCheck = this.lastCheckTime > 0 ? currentTime - this.lastCheckTime : this.pollingIntervalMs;
|
|
1762
|
+
this.lastCheckTime = currentTime;
|
|
1763
|
+
const currentState = await this.player.getStatus();
|
|
1764
|
+
if (this.lastKnownState) {
|
|
1765
|
+
const detectedActions = identifyUserActions(
|
|
1766
|
+
this.lastKnownState,
|
|
1767
|
+
currentState,
|
|
1768
|
+
timeSinceLastCheck
|
|
1769
|
+
);
|
|
1770
|
+
if (detectedActions.length > 0) {
|
|
1771
|
+
const source = this.determineActionSource(detectedActions);
|
|
1772
|
+
this.onStateChange(detectedActions, source);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
this.lastKnownState = currentState;
|
|
1776
|
+
if (this.isRunning) {
|
|
1777
|
+
this.pollingTimer = setTimeout(() => this.checkStatus(), this.pollingIntervalMs);
|
|
1778
|
+
}
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
console.error("Error fetching player status:", error);
|
|
1781
|
+
this.isRunning = false;
|
|
1782
|
+
this.onStop();
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Determines if the action came from app sync or user interaction.
|
|
1787
|
+
* Should be overridden to integrate with command tracking.
|
|
1788
|
+
*/
|
|
1789
|
+
determineActionSource(_actions) {
|
|
1790
|
+
return "user";
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
// src/player/HTML5VideoController.ts
|
|
1795
|
+
var HTML5VideoController = class {
|
|
1796
|
+
constructor(videoElementId) {
|
|
1797
|
+
this.videoElementId = videoElementId;
|
|
1798
|
+
this.video = null;
|
|
1799
|
+
this.statusMonitor = null;
|
|
1800
|
+
}
|
|
1801
|
+
async start(onReady) {
|
|
1802
|
+
if (typeof document === "undefined") {
|
|
1803
|
+
throw new Error("HTML5VideoController requires a browser environment");
|
|
1804
|
+
}
|
|
1805
|
+
this.video = document.getElementById(this.videoElementId);
|
|
1806
|
+
if (!this.video) {
|
|
1807
|
+
throw new Error(`Video element with id "${this.videoElementId}" not found`);
|
|
1808
|
+
}
|
|
1809
|
+
if (this.video.readyState >= 2) {
|
|
1810
|
+
onReady?.();
|
|
1811
|
+
} else {
|
|
1812
|
+
this.video.addEventListener("canplay", () => onReady?.(), { once: true });
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
async play() {
|
|
1816
|
+
if (!this.video) {
|
|
1817
|
+
throw new Error("Player not initialized");
|
|
1818
|
+
}
|
|
1819
|
+
await this.video.play();
|
|
1820
|
+
}
|
|
1821
|
+
async pause() {
|
|
1822
|
+
if (!this.video) {
|
|
1823
|
+
throw new Error("Player not initialized");
|
|
1824
|
+
}
|
|
1825
|
+
this.video.pause();
|
|
1826
|
+
}
|
|
1827
|
+
async stop() {
|
|
1828
|
+
if (this.video) {
|
|
1829
|
+
this.video.pause();
|
|
1830
|
+
this.video.currentTime = 0;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
async seek(timeInSeconds) {
|
|
1834
|
+
if (!this.video) {
|
|
1835
|
+
throw new Error("Player not initialized");
|
|
1836
|
+
}
|
|
1837
|
+
this.video.currentTime = timeInSeconds;
|
|
1838
|
+
}
|
|
1839
|
+
async loadMedia(source) {
|
|
1840
|
+
if (!this.video) {
|
|
1841
|
+
throw new Error("Player not initialized");
|
|
1842
|
+
}
|
|
1843
|
+
this.video.src = source;
|
|
1844
|
+
this.video.load();
|
|
1845
|
+
}
|
|
1846
|
+
async getStatus() {
|
|
1847
|
+
if (!this.video) {
|
|
1848
|
+
throw new Error("Player not initialized");
|
|
1849
|
+
}
|
|
1850
|
+
return {
|
|
1851
|
+
isPlaying: !this.video.paused && !this.video.ended,
|
|
1852
|
+
currentTime: this.video.currentTime,
|
|
1853
|
+
loop: this.video.loop,
|
|
1854
|
+
repeat: false,
|
|
1855
|
+
title: "",
|
|
1856
|
+
chapter: "",
|
|
1857
|
+
filename: this.video.src.split("/").pop() || "",
|
|
1858
|
+
duration: Number.isFinite(this.video.duration) ? this.video.duration : 0,
|
|
1859
|
+
filepath: this.video.src
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async checkPlayerRunning() {
|
|
1863
|
+
return this.video !== null && this.video.readyState >= 2;
|
|
1864
|
+
}
|
|
1865
|
+
async quit() {
|
|
1866
|
+
if (this.video) {
|
|
1867
|
+
this.video.pause();
|
|
1868
|
+
this.video.src = "";
|
|
1869
|
+
}
|
|
1870
|
+
this.statusMonitor?.stop();
|
|
1871
|
+
}
|
|
1872
|
+
observeStatusChange(onChange, onStop, intervalMs = 500) {
|
|
1873
|
+
this.statusMonitor?.stop();
|
|
1874
|
+
this.statusMonitor = new PlayerStatusMonitor(this, onChange, onStop, intervalMs);
|
|
1875
|
+
this.statusMonitor.start();
|
|
1876
|
+
}
|
|
1877
|
+
async displayMessage(message, durationSeconds = 5) {
|
|
1878
|
+
console.log(`[Player OSD] ${message} (${durationSeconds}s)`);
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Get the underlying HTMLVideoElement (for advanced usage).
|
|
1882
|
+
*/
|
|
1883
|
+
getVideoElement() {
|
|
1884
|
+
return this.video;
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
// src/player/SyncedPlayer.ts
|
|
1889
|
+
var SyncedPlayer = class {
|
|
1890
|
+
constructor(player, watchTogether, options) {
|
|
1891
|
+
this.player = player;
|
|
1892
|
+
this.watchTogether = watchTogether;
|
|
1893
|
+
this.options = options;
|
|
1894
|
+
this.commandStack = [];
|
|
1895
|
+
this.currentUserId = null;
|
|
1896
|
+
this.unsubscribers = [];
|
|
1897
|
+
this.currentUserId = options.getCurrentUserId();
|
|
1898
|
+
this.setupListeners();
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Setup listeners for sync and player changes.
|
|
1902
|
+
*/
|
|
1903
|
+
setupListeners() {
|
|
1904
|
+
const unsubRoomUpdate = this.watchTogether.onRoomUpdate((update) => {
|
|
1905
|
+
this.handleRemoteSyncUpdate(update);
|
|
1906
|
+
});
|
|
1907
|
+
this.unsubscribers.push(unsubRoomUpdate);
|
|
1908
|
+
this.player.observeStatusChange(
|
|
1909
|
+
(actions, _source) => this.handleLocalPlayerChange(actions),
|
|
1910
|
+
() => {
|
|
1911
|
+
this.options.onPlayerStopped?.();
|
|
1912
|
+
}
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Handle sync updates from other users.
|
|
1917
|
+
*/
|
|
1918
|
+
async handleRemoteSyncUpdate(update) {
|
|
1919
|
+
const { action, userId } = update.metadata;
|
|
1920
|
+
if (userId === this.currentUserId) {
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
this.commandStack.push({ action, timestamp: Date.now() });
|
|
1924
|
+
try {
|
|
1925
|
+
switch (action) {
|
|
1926
|
+
case "play":
|
|
1927
|
+
await this.player.play();
|
|
1928
|
+
break;
|
|
1929
|
+
case "pause":
|
|
1930
|
+
await this.player.pause();
|
|
1931
|
+
break;
|
|
1932
|
+
case "seek": {
|
|
1933
|
+
const time = update.roomUpdates.syncState?.time;
|
|
1934
|
+
if (time !== void 0) {
|
|
1935
|
+
await this.player.seek(time);
|
|
1936
|
+
}
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
console.error("Error applying remote sync update:", error);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Handle local player changes and broadcast to room.
|
|
1946
|
+
*/
|
|
1947
|
+
handleLocalPlayerChange(actions) {
|
|
1948
|
+
for (const action of actions) {
|
|
1949
|
+
const isFromSync = this.popMatchingCommand(action.event);
|
|
1950
|
+
if (isFromSync) {
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
switch (action.event) {
|
|
1955
|
+
case USER_ACTIONS.PLAY:
|
|
1956
|
+
this.watchTogether.play();
|
|
1957
|
+
break;
|
|
1958
|
+
case USER_ACTIONS.PAUSE:
|
|
1959
|
+
this.watchTogether.pause();
|
|
1960
|
+
break;
|
|
1961
|
+
case USER_ACTIONS.SEEK: {
|
|
1962
|
+
const time = action.value?.currentTime;
|
|
1963
|
+
if (time !== void 0) {
|
|
1964
|
+
this.watchTogether.seek(time);
|
|
1965
|
+
}
|
|
1966
|
+
break;
|
|
1967
|
+
}
|
|
1968
|
+
case USER_ACTIONS.FILE_UPDATE: {
|
|
1969
|
+
const filename = action.value?.filename;
|
|
1970
|
+
if (filename) {
|
|
1971
|
+
}
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
console.error("Error broadcasting player action:", error);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Check if a matching command exists in the stack (from sync).
|
|
1982
|
+
* Returns true if found (and removes it from stack).
|
|
1983
|
+
*/
|
|
1984
|
+
popMatchingCommand(event) {
|
|
1985
|
+
const now = Date.now();
|
|
1986
|
+
this.commandStack = this.commandStack.filter(
|
|
1987
|
+
(cmd) => now - cmd.timestamp < PlayerConstants.COMMAND_STACK_TIMEOUT_MS
|
|
1988
|
+
);
|
|
1989
|
+
const index = this.commandStack.findIndex(
|
|
1990
|
+
(cmd) => cmd.action === this.eventToAction(event) && now - cmd.timestamp < PlayerConstants.COMMAND_STACK_TIMEOUT_MS
|
|
1991
|
+
);
|
|
1992
|
+
if (index !== -1) {
|
|
1993
|
+
this.commandStack.splice(index, 1);
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Map user action event to sync action name.
|
|
2000
|
+
*/
|
|
2001
|
+
eventToAction(event) {
|
|
2002
|
+
const mapping = {
|
|
2003
|
+
[USER_ACTIONS.PLAY]: "play",
|
|
2004
|
+
[USER_ACTIONS.PAUSE]: "pause",
|
|
2005
|
+
[USER_ACTIONS.SEEK]: "seek"
|
|
2006
|
+
};
|
|
2007
|
+
return mapping[event] || event.toLowerCase();
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Cleanup listeners.
|
|
2011
|
+
*/
|
|
2012
|
+
destroy() {
|
|
2013
|
+
this.unsubscribers.forEach((unsub) => unsub());
|
|
2014
|
+
this.unsubscribers = [];
|
|
2015
|
+
this.commandStack = [];
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
exports.ApiError = ApiError;
|
|
2020
|
+
exports.AuthModule = AuthModule;
|
|
2021
|
+
exports.ForbiddenError = ForbiddenError;
|
|
2022
|
+
exports.HTML5VideoController = HTML5VideoController;
|
|
2023
|
+
exports.HarmoniSDK = HarmoniSDK;
|
|
2024
|
+
exports.HttpClient = HttpClient;
|
|
2025
|
+
exports.NetworkError = NetworkError;
|
|
2026
|
+
exports.NotFoundError = NotFoundError;
|
|
2027
|
+
exports.PlayerConstants = PlayerConstants;
|
|
2028
|
+
exports.PlayerStatusMonitor = PlayerStatusMonitor;
|
|
2029
|
+
exports.SECTION_NAMES = SECTION_NAMES;
|
|
2030
|
+
exports.Storage = Storage;
|
|
2031
|
+
exports.SyncActions = SyncActions;
|
|
2032
|
+
exports.SyncedPlayer = SyncedPlayer;
|
|
2033
|
+
exports.TimeoutError = TimeoutError;
|
|
2034
|
+
exports.USER_ACTIONS = USER_ACTIONS;
|
|
2035
|
+
exports.UnauthorizedError = UnauthorizedError;
|
|
2036
|
+
exports.UserModule = UserModule;
|
|
2037
|
+
exports.ValidationError = ValidationError;
|
|
2038
|
+
exports.WatchTogetherConstants = WatchTogetherConstants;
|
|
2039
|
+
exports.WatchTogetherError = WatchTogetherError;
|
|
2040
|
+
exports.WatchTogetherErrorCodes = WatchTogetherErrorCodes;
|
|
2041
|
+
exports.WatchTogetherIDs = WatchTogetherIDs;
|
|
2042
|
+
exports.WatchTogetherMessages = WatchTogetherMessages;
|
|
2043
|
+
exports.WatchTogetherModule = WatchTogetherModule;
|
|
2044
|
+
exports.dateUtils = date_exports;
|
|
2045
|
+
exports.default = HarmoniSDK;
|
|
2046
|
+
exports.identifyUserActions = identifyUserActions;
|
|
2047
|
+
exports.localStorage = localStorage;
|
|
2048
|
+
exports.normalizePlayerState = normalizePlayerState;
|
|
2049
|
+
exports.objectUtils = object_exports;
|
|
2050
|
+
exports.sessionStorage = sessionStorage;
|
|
2051
|
+
exports.stringUtils = string_exports;
|
|
2052
|
+
exports.validationUtils = validation_exports;
|
|
2053
|
+
//# sourceMappingURL=index.js.map
|
|
2054
|
+
//# sourceMappingURL=index.js.map
|