@mdcrtrs/player 0.1.3
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 +51 -0
- package/README.md +77 -0
- package/dist/index.cjs +667 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +87 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +628 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// src/components/player.tsx
|
|
2
|
+
import { memo, useCallback as useCallback3, useEffect as useEffect4, useMemo, useRef as useRef5 } from "react";
|
|
3
|
+
import MuxPlayer from "@mux/mux-player-react";
|
|
4
|
+
import "player.style/sutro";
|
|
5
|
+
|
|
6
|
+
// src/hooks/use-playback-session.ts
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
|
|
9
|
+
// src/types.ts
|
|
10
|
+
var PLATFORM = {
|
|
11
|
+
apiBase: "https://api.mediacreators.io",
|
|
12
|
+
playbackBase: "https://playback.mediacreators.io",
|
|
13
|
+
dataEnvKey: "fhri2484lqcn6gk89r013ckmb"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/lib/api.ts
|
|
17
|
+
var ApiError = class extends Error {
|
|
18
|
+
constructor(message, status) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.name = "ApiError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
async function fetchWithRetry(url, init, maxRetries = 3) {
|
|
25
|
+
let lastError = new Error("Unknown fetch error");
|
|
26
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(url, init);
|
|
29
|
+
if (res.ok || res.status < 500) {
|
|
30
|
+
return res;
|
|
31
|
+
}
|
|
32
|
+
lastError = new ApiError(`HTTP ${res.status}`, res.status);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
35
|
+
}
|
|
36
|
+
if (attempt < maxRetries - 1) {
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 500));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw lastError;
|
|
41
|
+
}
|
|
42
|
+
async function fetchSessionToken(creatorId, fanToken) {
|
|
43
|
+
const headers = {};
|
|
44
|
+
if (fanToken) {
|
|
45
|
+
headers["X-Fan-Token"] = fanToken;
|
|
46
|
+
}
|
|
47
|
+
const url = `${PLATFORM.apiBase}/v1/mos/${encodeURIComponent(creatorId)}/playback-session`;
|
|
48
|
+
const res = await fetchWithRetry(url, { method: "POST", headers });
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new ApiError(`Failed to fetch session token: ${res.status}`, res.status);
|
|
51
|
+
}
|
|
52
|
+
const json = await res.json();
|
|
53
|
+
return { token: json.token, expiresAt: json.expires_at };
|
|
54
|
+
}
|
|
55
|
+
async function fetchPlaybackTokens(creatorId, playbackId, duration, sessionToken) {
|
|
56
|
+
const params = new URLSearchParams({ duration: String(duration) });
|
|
57
|
+
const url = `${PLATFORM.playbackBase}/v1/${encodeURIComponent(creatorId)}/tokens/${encodeURIComponent(playbackId)}?${params}`;
|
|
58
|
+
const res = await fetchWithRetry(url, {
|
|
59
|
+
headers: { Authorization: `Bearer ${sessionToken}` }
|
|
60
|
+
});
|
|
61
|
+
if (res.status === 401 || res.status === 403) {
|
|
62
|
+
throw new ApiError("Session token invalid or expired", res.status);
|
|
63
|
+
}
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new ApiError(`Failed to fetch playback tokens: ${res.status}`, res.status);
|
|
66
|
+
}
|
|
67
|
+
const json = await res.json();
|
|
68
|
+
return {
|
|
69
|
+
video: json.tokens.video,
|
|
70
|
+
thumbnail: json.tokens.thumbnail,
|
|
71
|
+
storyboard: json.tokens.storyboard,
|
|
72
|
+
expiresIn: json.expires_in
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function saveWatchProgress(creatorId, assetId, position, duration, fanToken, completed) {
|
|
76
|
+
const url = `${PLATFORM.apiBase}/v1/mos/${encodeURIComponent(creatorId)}/watch-progress/${encodeURIComponent(assetId)}`;
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(url, {
|
|
79
|
+
method: "PUT",
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
"X-Fan-Token": fanToken
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({ position, duration, completed: completed != null ? completed : false }),
|
|
85
|
+
keepalive: true
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) return { ok: false, status: res.status };
|
|
88
|
+
return { ok: true };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return { ok: false, status: 0 };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function getWatchProgress(creatorId, assetId, fanToken) {
|
|
94
|
+
const url = `${PLATFORM.apiBase}/v1/mos/${encodeURIComponent(creatorId)}/watch-progress/${encodeURIComponent(assetId)}`;
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
headers: { "X-Fan-Token": fanToken }
|
|
98
|
+
});
|
|
99
|
+
if (res.status === 401 || res.status === 404) return null;
|
|
100
|
+
if (!res.ok) return null;
|
|
101
|
+
const json = await res.json();
|
|
102
|
+
return json.data;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/hooks/use-playback-session.ts
|
|
109
|
+
function scheduleRefresh(expiresIn, refreshFn, timerRef) {
|
|
110
|
+
if (timerRef.current !== null) {
|
|
111
|
+
clearTimeout(timerRef.current);
|
|
112
|
+
}
|
|
113
|
+
const refreshAtMs = expiresIn * 0.8 * 1e3;
|
|
114
|
+
timerRef.current = setTimeout(async () => {
|
|
115
|
+
try {
|
|
116
|
+
await refreshFn();
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("[player/session] Scheduled refresh failed:", err);
|
|
119
|
+
}
|
|
120
|
+
}, refreshAtMs);
|
|
121
|
+
}
|
|
122
|
+
function cancelRefresh(timerRef) {
|
|
123
|
+
if (timerRef.current !== null) {
|
|
124
|
+
clearTimeout(timerRef.current);
|
|
125
|
+
timerRef.current = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function usePlaybackSession(creatorId, playbackId, duration, fanToken) {
|
|
129
|
+
const [tokens, setTokens] = useState(null);
|
|
130
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
131
|
+
const [error, setError] = useState(null);
|
|
132
|
+
const sessionTokenRef = useRef(null);
|
|
133
|
+
const sessionExpiresAtRef = useRef(null);
|
|
134
|
+
const tokensRef = useRef(null);
|
|
135
|
+
const timerRef = useRef(null);
|
|
136
|
+
const visibilityHandlerRef = useRef(null);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
let cancelled = false;
|
|
139
|
+
tokensRef.current = null;
|
|
140
|
+
setTokens(null);
|
|
141
|
+
setIsLoading(true);
|
|
142
|
+
setError(null);
|
|
143
|
+
async function refresh() {
|
|
144
|
+
if (!sessionTokenRef.current || !sessionExpiresAtRef.current) return;
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const sessionExpiry = sessionExpiresAtRef.current.getTime();
|
|
147
|
+
let sessionToken = sessionTokenRef.current;
|
|
148
|
+
if (sessionExpiry - now < 6e4) {
|
|
149
|
+
const newSession = await fetchSessionToken(creatorId, fanToken);
|
|
150
|
+
sessionToken = newSession.token;
|
|
151
|
+
sessionTokenRef.current = newSession.token;
|
|
152
|
+
sessionExpiresAtRef.current = new Date(newSession.expiresAt);
|
|
153
|
+
}
|
|
154
|
+
const nextTokens = await fetchPlaybackTokens(creatorId, playbackId, duration, sessionToken);
|
|
155
|
+
if (cancelled) return;
|
|
156
|
+
tokensRef.current = nextTokens;
|
|
157
|
+
setTokens(nextTokens);
|
|
158
|
+
scheduleRefresh(nextTokens.expiresIn, refresh, timerRef);
|
|
159
|
+
}
|
|
160
|
+
async function initialize() {
|
|
161
|
+
try {
|
|
162
|
+
const session = await fetchSessionToken(creatorId, fanToken);
|
|
163
|
+
if (cancelled) return;
|
|
164
|
+
sessionTokenRef.current = session.token;
|
|
165
|
+
sessionExpiresAtRef.current = new Date(session.expiresAt);
|
|
166
|
+
const initialTokens = await fetchPlaybackTokens(creatorId, playbackId, duration, session.token);
|
|
167
|
+
if (cancelled) return;
|
|
168
|
+
tokensRef.current = initialTokens;
|
|
169
|
+
setTokens(initialTokens);
|
|
170
|
+
scheduleRefresh(initialTokens.expiresIn, refresh, timerRef);
|
|
171
|
+
visibilityHandlerRef.current = () => {
|
|
172
|
+
if (document.visibilityState !== "visible") return;
|
|
173
|
+
const current = tokensRef.current;
|
|
174
|
+
if (!sessionTokenRef.current || !sessionExpiresAtRef.current || !current) return;
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const sessionExpiry = sessionExpiresAtRef.current.getTime();
|
|
177
|
+
const approxTokenExpiry = now + current.expiresIn * 1e3;
|
|
178
|
+
const sessionExpired = sessionExpiry <= now;
|
|
179
|
+
const tokenNearExpiry = approxTokenExpiry - now < current.expiresIn * 0.2 * 1e3;
|
|
180
|
+
if (sessionExpired || tokenNearExpiry) {
|
|
181
|
+
cancelRefresh(timerRef);
|
|
182
|
+
refresh().catch((err) => {
|
|
183
|
+
if (!cancelled) setError(err);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
document.addEventListener("visibilitychange", visibilityHandlerRef.current);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (!cancelled) {
|
|
190
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
191
|
+
setTokens(null);
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
if (!cancelled) setIsLoading(false);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
void initialize();
|
|
198
|
+
return () => {
|
|
199
|
+
cancelled = true;
|
|
200
|
+
cancelRefresh(timerRef);
|
|
201
|
+
if (visibilityHandlerRef.current) {
|
|
202
|
+
document.removeEventListener("visibilitychange", visibilityHandlerRef.current);
|
|
203
|
+
visibilityHandlerRef.current = null;
|
|
204
|
+
}
|
|
205
|
+
sessionTokenRef.current = null;
|
|
206
|
+
sessionExpiresAtRef.current = null;
|
|
207
|
+
};
|
|
208
|
+
}, [creatorId, playbackId, duration, fanToken]);
|
|
209
|
+
return { tokens, isLoading, error };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/hooks/use-player-events.ts
|
|
213
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
214
|
+
var QUARTILES = [25, 50, 75, 100];
|
|
215
|
+
function usePlayerEvents(events, assetId) {
|
|
216
|
+
const firedQuartiles = useRef2(/* @__PURE__ */ new Set());
|
|
217
|
+
const prevTime = useRef2(0);
|
|
218
|
+
const eventsRef = useRef2(events);
|
|
219
|
+
eventsRef.current = events;
|
|
220
|
+
useEffect2(() => {
|
|
221
|
+
firedQuartiles.current = /* @__PURE__ */ new Set();
|
|
222
|
+
prevTime.current = 0;
|
|
223
|
+
}, [assetId]);
|
|
224
|
+
return {
|
|
225
|
+
handleTimeUpdate: (currentTime, duration) => {
|
|
226
|
+
var _a, _b, _c, _d, _e, _f;
|
|
227
|
+
if (!duration || duration <= 0) return;
|
|
228
|
+
const pct = currentTime / duration * 100;
|
|
229
|
+
(_b = (_a = eventsRef.current).onTimeUpdate) == null ? void 0 : _b.call(_a, currentTime, duration, pct);
|
|
230
|
+
for (const q of QUARTILES) {
|
|
231
|
+
if (pct >= q && !firedQuartiles.current.has(q)) {
|
|
232
|
+
firedQuartiles.current.add(q);
|
|
233
|
+
(_d = (_c = eventsRef.current).onQuartile) == null ? void 0 : _d.call(_c, q);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const delta = Math.abs(currentTime - prevTime.current);
|
|
237
|
+
if (delta > 2 && prevTime.current > 0) {
|
|
238
|
+
(_f = (_e = eventsRef.current).onSeeked) == null ? void 0 : _f.call(_e, prevTime.current, currentTime);
|
|
239
|
+
}
|
|
240
|
+
prevTime.current = currentTime;
|
|
241
|
+
},
|
|
242
|
+
handlePause: (currentTime) => {
|
|
243
|
+
var _a, _b;
|
|
244
|
+
(_b = (_a = eventsRef.current).onPause) == null ? void 0 : _b.call(_a, currentTime);
|
|
245
|
+
},
|
|
246
|
+
handlePlay: (currentTime) => {
|
|
247
|
+
var _a, _b;
|
|
248
|
+
(_b = (_a = eventsRef.current).onPlay) == null ? void 0 : _b.call(_a, currentTime);
|
|
249
|
+
},
|
|
250
|
+
handleComplete: () => {
|
|
251
|
+
var _a, _b, _c, _d;
|
|
252
|
+
if (!firedQuartiles.current.has(100)) {
|
|
253
|
+
firedQuartiles.current.add(100);
|
|
254
|
+
(_b = (_a = eventsRef.current).onQuartile) == null ? void 0 : _b.call(_a, 100);
|
|
255
|
+
}
|
|
256
|
+
(_d = (_c = eventsRef.current).onComplete) == null ? void 0 : _d.call(_c);
|
|
257
|
+
},
|
|
258
|
+
handleReady: () => {
|
|
259
|
+
var _a, _b;
|
|
260
|
+
(_b = (_a = eventsRef.current).onReady) == null ? void 0 : _b.call(_a);
|
|
261
|
+
},
|
|
262
|
+
handleError: (message) => {
|
|
263
|
+
var _a, _b;
|
|
264
|
+
(_b = (_a = eventsRef.current).onError) == null ? void 0 : _b.call(_a, { code: "PLAYER_ERROR", message });
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/hooks/use-volume.ts
|
|
270
|
+
import { useCallback, useRef as useRef3 } from "react";
|
|
271
|
+
var STORAGE_KEY = "mc-player-volume";
|
|
272
|
+
function getStoredVolume() {
|
|
273
|
+
try {
|
|
274
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
275
|
+
if (raw === null) return null;
|
|
276
|
+
const parsed = parseFloat(raw);
|
|
277
|
+
if (Number.isNaN(parsed) || parsed < 0 || parsed > 1) return null;
|
|
278
|
+
return parsed;
|
|
279
|
+
} catch (e) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function saveVolume(volume) {
|
|
284
|
+
try {
|
|
285
|
+
window.localStorage.setItem(STORAGE_KEY, String(volume));
|
|
286
|
+
} catch (e) {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function useVolume() {
|
|
290
|
+
const initialVolume = useRef3(getStoredVolume());
|
|
291
|
+
const debounceTimer = useRef3(null);
|
|
292
|
+
const handleVolumeChange = useCallback((volume) => {
|
|
293
|
+
if (debounceTimer.current !== null) clearTimeout(debounceTimer.current);
|
|
294
|
+
debounceTimer.current = setTimeout(() => saveVolume(volume), 500);
|
|
295
|
+
}, []);
|
|
296
|
+
return { initialVolume: initialVolume.current, handleVolumeChange };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/hooks/use-watch-progress.ts
|
|
300
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef4, useState as useState2 } from "react";
|
|
301
|
+
var SAVE_DEBOUNCE_MS = 1e4;
|
|
302
|
+
function useWatchProgress(creatorId, assetId, fanToken, autoResume, onResume) {
|
|
303
|
+
const [resumePosition, setResumePosition] = useState2(null);
|
|
304
|
+
const [authExpired, setAuthExpired] = useState2(false);
|
|
305
|
+
const debounceTimer = useRef4(null);
|
|
306
|
+
const onResumeRef = useRef4(onResume);
|
|
307
|
+
onResumeRef.current = onResume;
|
|
308
|
+
const stoppedRef = useRef4(false);
|
|
309
|
+
useEffect3(() => {
|
|
310
|
+
stoppedRef.current = false;
|
|
311
|
+
setAuthExpired(false);
|
|
312
|
+
setResumePosition(null);
|
|
313
|
+
if (!fanToken || !autoResume) return;
|
|
314
|
+
let cancelled = false;
|
|
315
|
+
void getWatchProgress(creatorId, assetId, fanToken).then((record) => {
|
|
316
|
+
var _a;
|
|
317
|
+
if (cancelled || !record || record.completed) return;
|
|
318
|
+
if (record.position > 5) {
|
|
319
|
+
setResumePosition(record.position);
|
|
320
|
+
(_a = onResumeRef.current) == null ? void 0 : _a.call(onResumeRef, record.position);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return () => {
|
|
324
|
+
cancelled = true;
|
|
325
|
+
if (debounceTimer.current !== null) {
|
|
326
|
+
clearTimeout(debounceTimer.current);
|
|
327
|
+
debounceTimer.current = null;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}, [creatorId, assetId, fanToken, autoResume]);
|
|
331
|
+
const saveProgress = useCallback2(
|
|
332
|
+
(position, duration) => {
|
|
333
|
+
if (!fanToken || stoppedRef.current) return;
|
|
334
|
+
if (debounceTimer.current !== null) clearTimeout(debounceTimer.current);
|
|
335
|
+
debounceTimer.current = setTimeout(() => {
|
|
336
|
+
const completed = duration > 0 && position / duration >= 0.95;
|
|
337
|
+
void saveWatchProgress(creatorId, assetId, position, duration, fanToken, completed).then((result) => {
|
|
338
|
+
if (!result.ok && result.status === 401) {
|
|
339
|
+
stoppedRef.current = true;
|
|
340
|
+
setAuthExpired(true);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}, SAVE_DEBOUNCE_MS);
|
|
344
|
+
},
|
|
345
|
+
[creatorId, assetId, fanToken]
|
|
346
|
+
);
|
|
347
|
+
return { resumePosition, saveProgress, authExpired };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/lib/session.ts
|
|
351
|
+
var SESSION_STORAGE_KEY = "mc-player-session";
|
|
352
|
+
function getSessionId() {
|
|
353
|
+
try {
|
|
354
|
+
const existing = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
355
|
+
if (existing) return existing;
|
|
356
|
+
const generated = crypto.randomUUID();
|
|
357
|
+
window.sessionStorage.setItem(SESSION_STORAGE_KEY, generated);
|
|
358
|
+
return generated;
|
|
359
|
+
} catch (e) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/lib/theme.ts
|
|
365
|
+
function normalizeHex(hex) {
|
|
366
|
+
const sanitized = hex.trim().replace(/^#/, "");
|
|
367
|
+
if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(sanitized)) {
|
|
368
|
+
return "#1a1a1a";
|
|
369
|
+
}
|
|
370
|
+
if (sanitized.length === 3) {
|
|
371
|
+
return `#${sanitized.split("").map((c) => c + c).join("")}`;
|
|
372
|
+
}
|
|
373
|
+
return `#${sanitized}`;
|
|
374
|
+
}
|
|
375
|
+
function hexToRgb(hex) {
|
|
376
|
+
const full = normalizeHex(hex).slice(1);
|
|
377
|
+
return {
|
|
378
|
+
r: parseInt(full.slice(0, 2), 16),
|
|
379
|
+
g: parseInt(full.slice(2, 4), 16),
|
|
380
|
+
b: parseInt(full.slice(4, 6), 16)
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function relativeLuminance(hex) {
|
|
384
|
+
const { r, g, b } = hexToRgb(hex);
|
|
385
|
+
const linear = [r, g, b].map((channel) => {
|
|
386
|
+
const s = channel / 255;
|
|
387
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
388
|
+
});
|
|
389
|
+
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
|
390
|
+
}
|
|
391
|
+
function hexToHSL(hex) {
|
|
392
|
+
const { r, g, b } = hexToRgb(hex);
|
|
393
|
+
const rNorm = r / 255;
|
|
394
|
+
const gNorm = g / 255;
|
|
395
|
+
const bNorm = b / 255;
|
|
396
|
+
const max = Math.max(rNorm, gNorm, bNorm);
|
|
397
|
+
const min = Math.min(rNorm, gNorm, bNorm);
|
|
398
|
+
const delta = max - min;
|
|
399
|
+
let h = 0;
|
|
400
|
+
if (delta !== 0) {
|
|
401
|
+
if (max === rNorm) {
|
|
402
|
+
h = (gNorm - bNorm) / delta % 6;
|
|
403
|
+
} else if (max === gNorm) {
|
|
404
|
+
h = (bNorm - rNorm) / delta + 2;
|
|
405
|
+
} else {
|
|
406
|
+
h = (rNorm - gNorm) / delta + 4;
|
|
407
|
+
}
|
|
408
|
+
h = Math.round(h * 60);
|
|
409
|
+
if (h < 0) h += 360;
|
|
410
|
+
}
|
|
411
|
+
const l = (max + min) / 2;
|
|
412
|
+
const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
|
|
413
|
+
return {
|
|
414
|
+
h,
|
|
415
|
+
s: Math.round(s * 100),
|
|
416
|
+
l: Math.round(l * 100)
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function hslToHex({ h, s, l }) {
|
|
420
|
+
const sNorm = s / 100;
|
|
421
|
+
const lNorm = l / 100;
|
|
422
|
+
const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
|
|
423
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
424
|
+
const m = lNorm - c / 2;
|
|
425
|
+
let r = 0;
|
|
426
|
+
let g = 0;
|
|
427
|
+
let b = 0;
|
|
428
|
+
if (h < 60) {
|
|
429
|
+
;
|
|
430
|
+
[r, g, b] = [c, x, 0];
|
|
431
|
+
} else if (h < 120) {
|
|
432
|
+
;
|
|
433
|
+
[r, g, b] = [x, c, 0];
|
|
434
|
+
} else if (h < 180) {
|
|
435
|
+
;
|
|
436
|
+
[r, g, b] = [0, c, x];
|
|
437
|
+
} else if (h < 240) {
|
|
438
|
+
;
|
|
439
|
+
[r, g, b] = [0, x, c];
|
|
440
|
+
} else if (h < 300) {
|
|
441
|
+
;
|
|
442
|
+
[r, g, b] = [x, 0, c];
|
|
443
|
+
} else {
|
|
444
|
+
;
|
|
445
|
+
[r, g, b] = [c, 0, x];
|
|
446
|
+
}
|
|
447
|
+
const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, "0");
|
|
448
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
449
|
+
}
|
|
450
|
+
function clamp(value, min, max) {
|
|
451
|
+
return Math.min(max, Math.max(min, value));
|
|
452
|
+
}
|
|
453
|
+
function deriveColors(primaryHex) {
|
|
454
|
+
const brand = normalizeHex(primaryHex);
|
|
455
|
+
const hsl = hexToHSL(brand);
|
|
456
|
+
const lum = relativeLuminance(brand);
|
|
457
|
+
const accentL = clamp(hsl.l < 42 ? 50 : hsl.l > 78 ? 70 : hsl.l, 38, 72);
|
|
458
|
+
const accent = hslToHex({
|
|
459
|
+
h: hsl.h,
|
|
460
|
+
s: clamp(hsl.s, 0, 100),
|
|
461
|
+
l: accentL
|
|
462
|
+
});
|
|
463
|
+
let primary;
|
|
464
|
+
if (lum >= 0.72) {
|
|
465
|
+
primary = "#1a1a1a";
|
|
466
|
+
} else if (lum >= 0.58 && hsl.s < 22) {
|
|
467
|
+
primary = brand;
|
|
468
|
+
} else {
|
|
469
|
+
primary = "#ffffff";
|
|
470
|
+
}
|
|
471
|
+
const secondary = lum >= 0.72 ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.14)";
|
|
472
|
+
return { primary, secondary, accent };
|
|
473
|
+
}
|
|
474
|
+
function deriveTheme(primaryColor) {
|
|
475
|
+
return deriveColors(primaryColor);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/components/player.tsx
|
|
479
|
+
import { jsx } from "react/jsx-runtime";
|
|
480
|
+
function decodeJwtSub(token) {
|
|
481
|
+
try {
|
|
482
|
+
const payloadPart = token.split(".")[1];
|
|
483
|
+
if (!payloadPart) return null;
|
|
484
|
+
const padded = payloadPart + "==".slice((payloadPart.length % 4 || 4) - 2);
|
|
485
|
+
const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
486
|
+
const json = JSON.parse(atob(base64));
|
|
487
|
+
return typeof json.sub === "string" ? json.sub : null;
|
|
488
|
+
} catch (e) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
var MediaCreatorsPlayer = memo(function MediaCreatorsPlayer2({
|
|
493
|
+
creatorId,
|
|
494
|
+
asset,
|
|
495
|
+
auth,
|
|
496
|
+
theme,
|
|
497
|
+
playerTheme,
|
|
498
|
+
poster,
|
|
499
|
+
metadata,
|
|
500
|
+
autoResume,
|
|
501
|
+
className,
|
|
502
|
+
onTimeUpdate,
|
|
503
|
+
onQuartile,
|
|
504
|
+
onPause,
|
|
505
|
+
onPlay,
|
|
506
|
+
onSeeked,
|
|
507
|
+
onComplete,
|
|
508
|
+
onError,
|
|
509
|
+
onReady,
|
|
510
|
+
onResume
|
|
511
|
+
}) {
|
|
512
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
513
|
+
const durationHint = (_a = asset.duration) != null ? _a : 3600;
|
|
514
|
+
const { tokens, isLoading, error } = usePlaybackSession(creatorId, asset.playbackId, durationHint, auth == null ? void 0 : auth.fanToken);
|
|
515
|
+
const colors = useMemo(() => {
|
|
516
|
+
var _a2;
|
|
517
|
+
return deriveTheme((_a2 = theme == null ? void 0 : theme.primaryColor) != null ? _a2 : "#1a1a1a");
|
|
518
|
+
}, [theme == null ? void 0 : theme.primaryColor]);
|
|
519
|
+
const { initialVolume, handleVolumeChange } = useVolume();
|
|
520
|
+
const playerRef = useRef5(null);
|
|
521
|
+
const seekDoneRef = useRef5(false);
|
|
522
|
+
const events = usePlayerEvents(
|
|
523
|
+
{ onTimeUpdate, onQuartile, onPause, onPlay, onSeeked, onComplete, onError, onReady, onResume },
|
|
524
|
+
asset.assetId
|
|
525
|
+
);
|
|
526
|
+
const { resumePosition, saveProgress } = useWatchProgress(
|
|
527
|
+
creatorId,
|
|
528
|
+
asset.assetId,
|
|
529
|
+
auth == null ? void 0 : auth.fanToken,
|
|
530
|
+
autoResume,
|
|
531
|
+
onResume
|
|
532
|
+
);
|
|
533
|
+
useEffect4(() => {
|
|
534
|
+
if (error) {
|
|
535
|
+
onError == null ? void 0 : onError({ code: "SESSION_ERROR", message: error.message });
|
|
536
|
+
}
|
|
537
|
+
}, [error, onError]);
|
|
538
|
+
useEffect4(() => {
|
|
539
|
+
seekDoneRef.current = false;
|
|
540
|
+
}, [asset.assetId]);
|
|
541
|
+
useEffect4(() => {
|
|
542
|
+
if (seekDoneRef.current) return;
|
|
543
|
+
if (resumePosition === null || !playerRef.current) return;
|
|
544
|
+
const el = playerRef.current;
|
|
545
|
+
if (typeof el.currentTime === "number") {
|
|
546
|
+
el.currentTime = resumePosition;
|
|
547
|
+
seekDoneRef.current = true;
|
|
548
|
+
}
|
|
549
|
+
}, [resumePosition, tokens]);
|
|
550
|
+
const handlePlayerRef = useCallback3(
|
|
551
|
+
(el) => {
|
|
552
|
+
playerRef.current = el;
|
|
553
|
+
if (el && initialVolume !== null) {
|
|
554
|
+
;
|
|
555
|
+
el.volume = initialVolume;
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
[initialVolume]
|
|
559
|
+
);
|
|
560
|
+
const viewerUserId = (_d = (_c = (_b = metadata == null ? void 0 : metadata.viewerUserId) != null ? _b : (auth == null ? void 0 : auth.fanToken) ? decodeJwtSub(auth.fanToken) : null) != null ? _c : getSessionId()) != null ? _d : void 0;
|
|
561
|
+
if (isLoading || !tokens) {
|
|
562
|
+
return /* @__PURE__ */ jsx("div", { className, style: { width: "100%", aspectRatio: "16 / 9", background: "#111" } });
|
|
563
|
+
}
|
|
564
|
+
return /* @__PURE__ */ jsx(
|
|
565
|
+
MuxPlayer,
|
|
566
|
+
{
|
|
567
|
+
ref: handlePlayerRef,
|
|
568
|
+
className,
|
|
569
|
+
theme: playerTheme,
|
|
570
|
+
playbackId: asset.playbackId,
|
|
571
|
+
poster,
|
|
572
|
+
envKey: PLATFORM.dataEnvKey,
|
|
573
|
+
tokens: {
|
|
574
|
+
playback: tokens.video,
|
|
575
|
+
thumbnail: tokens.thumbnail,
|
|
576
|
+
storyboard: tokens.storyboard
|
|
577
|
+
},
|
|
578
|
+
primaryColor: colors.primary,
|
|
579
|
+
secondaryColor: colors.secondary,
|
|
580
|
+
accentColor: colors.accent,
|
|
581
|
+
metadata: {
|
|
582
|
+
video_id: asset.assetId,
|
|
583
|
+
video_title: asset.title,
|
|
584
|
+
video_stream_type: "on-demand",
|
|
585
|
+
viewer_user_id: viewerUserId,
|
|
586
|
+
sub_property_id: creatorId,
|
|
587
|
+
player_name: (_e = metadata == null ? void 0 : metadata.playerName) != null ? _e : "mediacreators-sdk",
|
|
588
|
+
custom_1: (_f = metadata == null ? void 0 : metadata.custom1) != null ? _f : creatorId,
|
|
589
|
+
custom_2: (_g = metadata == null ? void 0 : metadata.custom2) != null ? _g : "sdk"
|
|
590
|
+
},
|
|
591
|
+
playsInline: true,
|
|
592
|
+
style: { width: "100%", aspectRatio: "16 / 9" },
|
|
593
|
+
onLoadedData: () => events.handleReady(),
|
|
594
|
+
onTimeUpdate: (e) => {
|
|
595
|
+
const el = e.target;
|
|
596
|
+
if (!el) return;
|
|
597
|
+
events.handleTimeUpdate(el.currentTime, el.duration);
|
|
598
|
+
if (auth == null ? void 0 : auth.fanToken) {
|
|
599
|
+
saveProgress(el.currentTime, el.duration);
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
onPause: (e) => {
|
|
603
|
+
var _a2;
|
|
604
|
+
const el = e.target;
|
|
605
|
+
events.handlePause((_a2 = el == null ? void 0 : el.currentTime) != null ? _a2 : 0);
|
|
606
|
+
},
|
|
607
|
+
onPlay: (e) => {
|
|
608
|
+
var _a2;
|
|
609
|
+
const el = e.target;
|
|
610
|
+
events.handlePlay((_a2 = el == null ? void 0 : el.currentTime) != null ? _a2 : 0);
|
|
611
|
+
},
|
|
612
|
+
onEnded: () => events.handleComplete(),
|
|
613
|
+
onVolumeChange: (e) => {
|
|
614
|
+
const el = e.target;
|
|
615
|
+
if (el && typeof el.volume === "number") {
|
|
616
|
+
handleVolumeChange(el.volume);
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
onError: () => events.handleError("Playback error")
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
export {
|
|
624
|
+
MediaCreatorsPlayer,
|
|
625
|
+
usePlaybackSession,
|
|
626
|
+
useWatchProgress
|
|
627
|
+
};
|
|
628
|
+
//# sourceMappingURL=index.js.map
|