@moviie/player-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/index.cjs +768 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +270 -0
- package/dist/index.d.ts +270 -0
- package/dist/index.js +735 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var playerTypes = require('@moviie/player-types');
|
|
5
|
+
|
|
6
|
+
// src/client/moviie-client.ts
|
|
7
|
+
|
|
8
|
+
// src/constants.ts
|
|
9
|
+
var MOVIIE_DEFAULT_API_BASE_URL = "https://api.moviie.ai/v1";
|
|
10
|
+
var MOVIIE_TELEMETRY_API_PATH_PREFIX = "/telemetry/v1";
|
|
11
|
+
var SDK_PUBLIC_API_KEY_PREFIX_PUBLISHABLE = "mvi_pub_";
|
|
12
|
+
var SDK_PLAYBACK_AUTH_ERROR_MESSAGE = {
|
|
13
|
+
KEY_REQUIRED: "Publishable API key is required for playback.",
|
|
14
|
+
PRIVATE_KEY_FORBIDDEN: "Do not use a private API key for playback; use a publishable key (mvi_pub_*)."
|
|
15
|
+
};
|
|
16
|
+
var HTTP_HEADER_MOVIIE_REASON = "x-moviie-reason";
|
|
17
|
+
var MOVIIE_BLOCK_REASON = {
|
|
18
|
+
BUNDLE: "bundle"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/config/moviie-endpoints.ts
|
|
22
|
+
var configuredApiBaseUrl = null;
|
|
23
|
+
var configuredEventsBaseUrl = null;
|
|
24
|
+
function normalizeEndpointBase(raw) {
|
|
25
|
+
return raw.trim().replace(/\/+$/, "");
|
|
26
|
+
}
|
|
27
|
+
function deriveMoviieTelemetryBaseUrlFromApiBaseUrl(apiBaseUrl) {
|
|
28
|
+
const trimmed = normalizeEndpointBase(apiBaseUrl);
|
|
29
|
+
const href = trimmed.includes("://") ? trimmed : `https://${trimmed}`;
|
|
30
|
+
const u = new URL(href);
|
|
31
|
+
return normalizeEndpointBase(`${u.origin}${MOVIIE_TELEMETRY_API_PATH_PREFIX}`);
|
|
32
|
+
}
|
|
33
|
+
function configureMoviieEndpoints(options) {
|
|
34
|
+
if (options.apiBaseUrl !== void 0) {
|
|
35
|
+
if (options.apiBaseUrl == null || options.apiBaseUrl.trim() === "") {
|
|
36
|
+
configuredApiBaseUrl = null;
|
|
37
|
+
} else {
|
|
38
|
+
configuredApiBaseUrl = normalizeEndpointBase(options.apiBaseUrl);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (options.eventsBaseUrl !== void 0) {
|
|
42
|
+
if (options.eventsBaseUrl == null || options.eventsBaseUrl.trim() === "") {
|
|
43
|
+
configuredEventsBaseUrl = null;
|
|
44
|
+
} else {
|
|
45
|
+
configuredEventsBaseUrl = normalizeEndpointBase(options.eventsBaseUrl);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function resetMoviieEndpointsConfiguration() {
|
|
50
|
+
configuredApiBaseUrl = null;
|
|
51
|
+
configuredEventsBaseUrl = null;
|
|
52
|
+
}
|
|
53
|
+
function getMoviieApiBaseUrl() {
|
|
54
|
+
return configuredApiBaseUrl ?? MOVIIE_DEFAULT_API_BASE_URL;
|
|
55
|
+
}
|
|
56
|
+
function getMoviieEventsBaseUrl() {
|
|
57
|
+
if (process.env.NODE_ENV === "test" && process.env.MOVIIE_PLAYER_SDK_TEST_EVENTS_BASE?.trim()) {
|
|
58
|
+
return normalizeEndpointBase(process.env.MOVIIE_PLAYER_SDK_TEST_EVENTS_BASE);
|
|
59
|
+
}
|
|
60
|
+
if (configuredEventsBaseUrl != null) {
|
|
61
|
+
return configuredEventsBaseUrl;
|
|
62
|
+
}
|
|
63
|
+
return deriveMoviieTelemetryBaseUrlFromApiBaseUrl(getMoviieApiBaseUrl());
|
|
64
|
+
}
|
|
65
|
+
var MOVIIE_CDN_BASE = "https://cdn.moviie.ai";
|
|
66
|
+
var MOVIIE_WATCH_BASE = "https://watch.moviie.ai";
|
|
67
|
+
|
|
68
|
+
// src/client/errors.ts
|
|
69
|
+
var MoviieAuthError = class extends Error {
|
|
70
|
+
code = "auth";
|
|
71
|
+
constructor(message = "Autentica\xE7\xE3o necess\xE1ria ou chave inv\xE1lida") {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "MoviieAuthError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var MoviieNotFoundError = class extends Error {
|
|
77
|
+
code = "not_found";
|
|
78
|
+
constructor(message = "Recurso n\xE3o encontrado") {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "MoviieNotFoundError";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var MoviieBundleBlockedError = class extends Error {
|
|
84
|
+
code = "bundle_blocked";
|
|
85
|
+
constructor(message = "Este aplicativo n\xE3o est\xE1 autorizado a reproduzir este v\xEDdeo") {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "MoviieBundleBlockedError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var MoviieReferrerBlockedError = class extends Error {
|
|
91
|
+
code = "referrer_blocked";
|
|
92
|
+
constructor(message = "Origem n\xE3o autorizada para reprodu\xE7\xE3o") {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "MoviieReferrerBlockedError";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var MoviieSubscriptionInactiveError = class extends Error {
|
|
98
|
+
code = "subscription_inactive";
|
|
99
|
+
constructor(message = "Reprodu\xE7\xE3o temporariamente indispon\xEDvel para esta organiza\xE7\xE3o") {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "MoviieSubscriptionInactiveError";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var MoviieNetworkError = class extends Error {
|
|
105
|
+
code = "network";
|
|
106
|
+
constructor(message = "Falha de rede. Tente novamente") {
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = "MoviieNetworkError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var MoviieRateLimitError = class extends Error {
|
|
112
|
+
code = "rate_limit";
|
|
113
|
+
constructor(message = "Muitas solicita\xE7\xF5es. Aguarde e tente novamente") {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "MoviieRateLimitError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/client/http.ts
|
|
120
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
121
|
+
var DEFAULT_RETRIES = 2;
|
|
122
|
+
function delay(ms) {
|
|
123
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
124
|
+
}
|
|
125
|
+
function backoffDelayMs(attempt) {
|
|
126
|
+
const base = 2 ** attempt * 200;
|
|
127
|
+
const jitter = Math.floor(Math.random() * 200);
|
|
128
|
+
return base + jitter;
|
|
129
|
+
}
|
|
130
|
+
function composeAbortSignals(external, internal) {
|
|
131
|
+
if (!external) return internal;
|
|
132
|
+
if (external.aborted) return external;
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const onAbort = (signal) => () => {
|
|
135
|
+
controller.abort(signal.reason);
|
|
136
|
+
};
|
|
137
|
+
external.addEventListener("abort", onAbort(external), { once: true });
|
|
138
|
+
internal.addEventListener("abort", onAbort(internal), { once: true });
|
|
139
|
+
return controller.signal;
|
|
140
|
+
}
|
|
141
|
+
async function fetchWithRetry(input, init = {}) {
|
|
142
|
+
const timeoutMs = init.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
143
|
+
const maxRetries = init.retries ?? DEFAULT_RETRIES;
|
|
144
|
+
const { timeoutMs: _t, retries: _r, signal: externalSignal, ...rest } = init;
|
|
145
|
+
let lastError;
|
|
146
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
147
|
+
if (externalSignal?.aborted) {
|
|
148
|
+
throw externalSignal.reason instanceof Error ? externalSignal.reason : new DOMException("Aborted", "AbortError");
|
|
149
|
+
}
|
|
150
|
+
const timeoutController = new AbortController();
|
|
151
|
+
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
152
|
+
const signal = composeAbortSignals(externalSignal, timeoutController.signal);
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(input, { ...rest, signal });
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
if (response.status === 429) {
|
|
157
|
+
throw new MoviieRateLimitError();
|
|
158
|
+
}
|
|
159
|
+
if (response.status >= 500 && response.status < 600 && attempt < maxRetries) {
|
|
160
|
+
await delay(backoffDelayMs(attempt));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
return response;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
lastError = error;
|
|
167
|
+
if (error instanceof MoviieRateLimitError) throw error;
|
|
168
|
+
if (externalSignal?.aborted) {
|
|
169
|
+
throw externalSignal.reason instanceof Error ? externalSignal.reason : new DOMException("Aborted", "AbortError");
|
|
170
|
+
}
|
|
171
|
+
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
172
|
+
if (isAbort && attempt < maxRetries) {
|
|
173
|
+
await delay(backoffDelayMs(attempt));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (attempt >= maxRetries) {
|
|
177
|
+
throw new MoviieNetworkError();
|
|
178
|
+
}
|
|
179
|
+
await delay(backoffDelayMs(attempt));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw lastError instanceof Error ? lastError : new MoviieNetworkError();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/client/client-headers.ts
|
|
186
|
+
var SEPARATOR = ";";
|
|
187
|
+
function sanitizeSegment(value) {
|
|
188
|
+
return value.replace(/[\r\n;]/g, "").trim();
|
|
189
|
+
}
|
|
190
|
+
function buildClientHeaders(params) {
|
|
191
|
+
const headers = {};
|
|
192
|
+
const key = params.publishableKey?.trim();
|
|
193
|
+
if (key) {
|
|
194
|
+
headers.Authorization = `Bearer ${key}`;
|
|
195
|
+
}
|
|
196
|
+
const info = params.clientInfo;
|
|
197
|
+
if (info) {
|
|
198
|
+
const parts = [];
|
|
199
|
+
if (info.bundleId) {
|
|
200
|
+
parts.push(`bundle_id=${sanitizeSegment(info.bundleId)}`);
|
|
201
|
+
}
|
|
202
|
+
parts.push(`platform=${sanitizeSegment(info.platform)}`);
|
|
203
|
+
const sdk = params.sdkVersion ?? info.sdkVersion;
|
|
204
|
+
if (sdk) {
|
|
205
|
+
parts.push(`sdk=${sanitizeSegment(sdk)}`);
|
|
206
|
+
}
|
|
207
|
+
headers["X-Moviie-Client"] = parts.join(SEPARATOR);
|
|
208
|
+
}
|
|
209
|
+
return headers;
|
|
210
|
+
}
|
|
211
|
+
var playbackPayloadSchema = zod.z.object({
|
|
212
|
+
uri: zod.z.string().url(),
|
|
213
|
+
contentType: zod.z.enum(["hls", "progressive"]),
|
|
214
|
+
expiresAt: zod.z.string().nullable(),
|
|
215
|
+
refreshAfter: zod.z.string().nullable(),
|
|
216
|
+
requestHeaders: zod.z.record(zod.z.string()).optional()
|
|
217
|
+
});
|
|
218
|
+
var moviiePlaybackDataResponseSchema = zod.z.object({
|
|
219
|
+
embedId: zod.z.string(),
|
|
220
|
+
title: zod.z.string(),
|
|
221
|
+
smartProgressEnabled: zod.z.boolean(),
|
|
222
|
+
durationSeconds: zod.z.number().nullable(),
|
|
223
|
+
isVertical: zod.z.boolean(),
|
|
224
|
+
videoWidthPx: zod.z.number().int().positive().nullable(),
|
|
225
|
+
videoHeightPx: zod.z.number().int().positive().nullable(),
|
|
226
|
+
profile: zod.z.string(),
|
|
227
|
+
playback: playbackPayloadSchema,
|
|
228
|
+
posterUrl: zod.z.string().nullable(),
|
|
229
|
+
branding: zod.z.object({
|
|
230
|
+
primaryColor: zod.z.string().nullable(),
|
|
231
|
+
showWatermark: zod.z.boolean(),
|
|
232
|
+
organizationName: zod.z.string().nullable()
|
|
233
|
+
}),
|
|
234
|
+
telemetry: zod.z.object({
|
|
235
|
+
bootstrapUrl: zod.z.string().url()
|
|
236
|
+
})
|
|
237
|
+
}).passthrough();
|
|
238
|
+
async function fetchPlaybackData(params) {
|
|
239
|
+
const response = await fetchWithRetry(params.url, {
|
|
240
|
+
headers: params.headers,
|
|
241
|
+
signal: params.signal,
|
|
242
|
+
method: "GET"
|
|
243
|
+
});
|
|
244
|
+
const reason = response.headers.get(HTTP_HEADER_MOVIIE_REASON)?.toLowerCase();
|
|
245
|
+
if (response.status === 401) {
|
|
246
|
+
throw new MoviieAuthError();
|
|
247
|
+
}
|
|
248
|
+
if (response.status === 402) {
|
|
249
|
+
throw new MoviieSubscriptionInactiveError();
|
|
250
|
+
}
|
|
251
|
+
if (response.status === 404) {
|
|
252
|
+
throw new MoviieNotFoundError();
|
|
253
|
+
}
|
|
254
|
+
if (response.status === 403) {
|
|
255
|
+
if (reason === MOVIIE_BLOCK_REASON.BUNDLE) {
|
|
256
|
+
throw new MoviieBundleBlockedError();
|
|
257
|
+
}
|
|
258
|
+
throw new MoviieReferrerBlockedError();
|
|
259
|
+
}
|
|
260
|
+
if (response.status >= 500) {
|
|
261
|
+
throw new MoviieNetworkError();
|
|
262
|
+
}
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw new MoviieNotFoundError("N\xE3o foi poss\xEDvel carregar a reprodu\xE7\xE3o");
|
|
265
|
+
}
|
|
266
|
+
const json = await response.json();
|
|
267
|
+
const parsed = moviiePlaybackDataResponseSchema.safeParse(json);
|
|
268
|
+
if (!parsed.success) {
|
|
269
|
+
throw new MoviieNotFoundError("Resposta de playback inv\xE1lida");
|
|
270
|
+
}
|
|
271
|
+
return parsed.data;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/client/moviie-client.ts
|
|
275
|
+
var videoEmbedResponseSchema = zod.z.object({
|
|
276
|
+
id: zod.z.string().uuid(),
|
|
277
|
+
embed_id: zod.z.string().uuid()
|
|
278
|
+
});
|
|
279
|
+
var MoviieClient = class {
|
|
280
|
+
constructor(options) {
|
|
281
|
+
this.options = options;
|
|
282
|
+
}
|
|
283
|
+
options;
|
|
284
|
+
headers(extra) {
|
|
285
|
+
return {
|
|
286
|
+
...buildClientHeaders({
|
|
287
|
+
publishableKey: this.options.publishableKey ?? void 0,
|
|
288
|
+
clientInfo: this.options.clientInfo ?? void 0,
|
|
289
|
+
sdkVersion: this.options.sdkVersion
|
|
290
|
+
}),
|
|
291
|
+
...extra
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async getPlayback(embedId, signal) {
|
|
295
|
+
const key = this.options.publishableKey?.trim();
|
|
296
|
+
if (!key) {
|
|
297
|
+
throw new MoviieAuthError(SDK_PLAYBACK_AUTH_ERROR_MESSAGE.KEY_REQUIRED);
|
|
298
|
+
}
|
|
299
|
+
if (!key.startsWith(SDK_PUBLIC_API_KEY_PREFIX_PUBLISHABLE)) {
|
|
300
|
+
throw new MoviieAuthError(SDK_PLAYBACK_AUTH_ERROR_MESSAGE.PRIVATE_KEY_FORBIDDEN);
|
|
301
|
+
}
|
|
302
|
+
const base = getMoviieApiBaseUrl().replace(/\/+$/, "");
|
|
303
|
+
const url = `${base}/embeds/${encodeURIComponent(embedId)}/playback`;
|
|
304
|
+
return fetchPlaybackData({
|
|
305
|
+
url,
|
|
306
|
+
headers: this.headers(),
|
|
307
|
+
signal
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async getVideo(videoId, signal) {
|
|
311
|
+
const key = this.options.publishableKey?.trim();
|
|
312
|
+
if (!key) {
|
|
313
|
+
throw new MoviieAuthError("Chave de API obrigat\xF3ria para buscar v\xEDdeo por ID");
|
|
314
|
+
}
|
|
315
|
+
if (key.startsWith(SDK_PUBLIC_API_KEY_PREFIX_PUBLISHABLE)) {
|
|
316
|
+
throw new MoviieAuthError("Chave publishable n\xE3o pode listar v\xEDdeos administrativos");
|
|
317
|
+
}
|
|
318
|
+
const base = getMoviieApiBaseUrl().replace(/\/+$/, "");
|
|
319
|
+
const url = `${base}/videos/${encodeURIComponent(videoId)}`;
|
|
320
|
+
const response = await fetchWithRetry(url, {
|
|
321
|
+
method: "GET",
|
|
322
|
+
headers: this.headers(),
|
|
323
|
+
signal
|
|
324
|
+
});
|
|
325
|
+
if (response.status === 401 || response.status === 403) {
|
|
326
|
+
throw new MoviieAuthError();
|
|
327
|
+
}
|
|
328
|
+
if (response.status === 404) {
|
|
329
|
+
throw new MoviieNotFoundError();
|
|
330
|
+
}
|
|
331
|
+
if (response.status >= 500) {
|
|
332
|
+
throw new MoviieNetworkError();
|
|
333
|
+
}
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
throw new MoviieNotFoundError();
|
|
336
|
+
}
|
|
337
|
+
const json = await response.json();
|
|
338
|
+
const parsed = videoEmbedResponseSchema.safeParse(json);
|
|
339
|
+
if (!parsed.success) {
|
|
340
|
+
throw new MoviieNotFoundError("Resposta de v\xEDdeo inv\xE1lida");
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
id: parsed.data.id,
|
|
344
|
+
embedId: parsed.data.embed_id
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async resolveEmbedIdFromVideoId(videoId, signal) {
|
|
348
|
+
const video = await this.getVideo(videoId, signal);
|
|
349
|
+
if (!video.embedId) {
|
|
350
|
+
throw new MoviieNotFoundError("Embed n\xE3o encontrado para este v\xEDdeo");
|
|
351
|
+
}
|
|
352
|
+
return video.embedId;
|
|
353
|
+
}
|
|
354
|
+
getEventsBaseUrl() {
|
|
355
|
+
return getMoviieEventsBaseUrl().replace(/\/+$/, "");
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var PLAYBACK_EVENT_TYPE = {
|
|
359
|
+
SESSION_START: "session_start",
|
|
360
|
+
PLAY_START: "play_start",
|
|
361
|
+
PAUSE: "pause",
|
|
362
|
+
RESUME: "resume",
|
|
363
|
+
HEARTBEAT: "heartbeat",
|
|
364
|
+
ENDED: "ended",
|
|
365
|
+
CTA_CLICK: "cta_click",
|
|
366
|
+
ERROR: "error"
|
|
367
|
+
};
|
|
368
|
+
var PLAYBACK_EVENT_TYPE_VALUES = Object.values(PLAYBACK_EVENT_TYPE);
|
|
369
|
+
var playbackEventTypeSchema = zod.z.enum(
|
|
370
|
+
PLAYBACK_EVENT_TYPE_VALUES
|
|
371
|
+
);
|
|
372
|
+
var TELEMETRY_TOKEN_CONFIG = {
|
|
373
|
+
INGEST_TOKEN_EXPIRY_SECONDS: 600,
|
|
374
|
+
REFRESH_TOKEN_EXPIRY_SECONDS: 86400,
|
|
375
|
+
REFRESH_BUFFER_SECONDS: 120
|
|
376
|
+
};
|
|
377
|
+
var HEARTBEAT_CONFIG = {
|
|
378
|
+
INTERVAL_SECONDS: 10,
|
|
379
|
+
WATCH_TIME_PER_HEARTBEAT_SECONDS: 10
|
|
380
|
+
};
|
|
381
|
+
var bootstrapResponseSchema = zod.z.object({
|
|
382
|
+
sessionId: zod.z.string().uuid(),
|
|
383
|
+
eventsIngestToken: zod.z.string(),
|
|
384
|
+
eventsRefreshToken: zod.z.string(),
|
|
385
|
+
ctaId: zod.z.string().optional()
|
|
386
|
+
});
|
|
387
|
+
var tokenRefreshRequestSchema = zod.z.object({
|
|
388
|
+
sessionId: zod.z.string().uuid(),
|
|
389
|
+
eventsRefreshToken: zod.z.string()
|
|
390
|
+
});
|
|
391
|
+
var tokenRefreshResponseSchema = zod.z.object({
|
|
392
|
+
eventsIngestToken: zod.z.string(),
|
|
393
|
+
expiresInSeconds: zod.z.number()
|
|
394
|
+
});
|
|
395
|
+
var playbackEventSchema = zod.z.object({
|
|
396
|
+
eventId: zod.z.string().uuid(),
|
|
397
|
+
sessionId: zod.z.string().uuid(),
|
|
398
|
+
eventType: playbackEventTypeSchema,
|
|
399
|
+
positionSeconds: zod.z.number().int().min(0).optional(),
|
|
400
|
+
ctaId: zod.z.string().optional(),
|
|
401
|
+
errorCode: zod.z.string().max(50).optional()
|
|
402
|
+
});
|
|
403
|
+
zod.z.object({
|
|
404
|
+
events: zod.z.array(playbackEventSchema).min(1).max(100),
|
|
405
|
+
ingestToken: zod.z.string().optional()
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// src/telemetry/heartbeat-scheduler.ts
|
|
409
|
+
var HeartbeatScheduler = class {
|
|
410
|
+
constructor(options) {
|
|
411
|
+
this.options = options;
|
|
412
|
+
}
|
|
413
|
+
options;
|
|
414
|
+
timer = null;
|
|
415
|
+
paused = false;
|
|
416
|
+
start() {
|
|
417
|
+
if (this.timer) return;
|
|
418
|
+
const ms = Math.max(1e3, this.options.intervalSeconds * 1e3);
|
|
419
|
+
this.timer = setInterval(() => {
|
|
420
|
+
if (!this.paused) {
|
|
421
|
+
this.options.onTick();
|
|
422
|
+
}
|
|
423
|
+
}, ms);
|
|
424
|
+
}
|
|
425
|
+
stop() {
|
|
426
|
+
if (this.timer) {
|
|
427
|
+
clearInterval(this.timer);
|
|
428
|
+
this.timer = null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
pause() {
|
|
432
|
+
this.paused = true;
|
|
433
|
+
}
|
|
434
|
+
resume() {
|
|
435
|
+
this.paused = false;
|
|
436
|
+
}
|
|
437
|
+
destroy() {
|
|
438
|
+
this.stop();
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/telemetry/token-manager.ts
|
|
443
|
+
var REFRESH_MIN_DELAY_MS = 5e3;
|
|
444
|
+
var REFRESH_RETRY_BACKOFF_MS = [5e3, 15e3, 6e4];
|
|
445
|
+
var TelemetryTokenManager = class {
|
|
446
|
+
constructor(options) {
|
|
447
|
+
this.options = options;
|
|
448
|
+
}
|
|
449
|
+
options;
|
|
450
|
+
ingestToken = null;
|
|
451
|
+
refreshToken = null;
|
|
452
|
+
sessionId = null;
|
|
453
|
+
refreshTimer = null;
|
|
454
|
+
refreshFailureCount = 0;
|
|
455
|
+
getSessionId() {
|
|
456
|
+
return this.sessionId;
|
|
457
|
+
}
|
|
458
|
+
getIngestToken() {
|
|
459
|
+
return this.ingestToken;
|
|
460
|
+
}
|
|
461
|
+
async bootstrap(bootstrapUrl) {
|
|
462
|
+
const response = await fetchWithRetry(bootstrapUrl, {
|
|
463
|
+
method: "GET",
|
|
464
|
+
headers: this.options.defaultHeaders
|
|
465
|
+
});
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
throw new Error(`Bootstrap falhou: ${response.status}`);
|
|
468
|
+
}
|
|
469
|
+
const json = await response.json();
|
|
470
|
+
const parsed = bootstrapResponseSchema.safeParse(json);
|
|
471
|
+
if (!parsed.success) {
|
|
472
|
+
throw new Error("Resposta de bootstrap inv\xE1lida");
|
|
473
|
+
}
|
|
474
|
+
this.sessionId = parsed.data.sessionId;
|
|
475
|
+
this.ingestToken = parsed.data.eventsIngestToken;
|
|
476
|
+
this.refreshToken = parsed.data.eventsRefreshToken;
|
|
477
|
+
const store = this.options.viewerTokenStore;
|
|
478
|
+
if (store && parsed.data.eventsRefreshToken) {
|
|
479
|
+
await store.set(parsed.data.eventsRefreshToken, TELEMETRY_TOKEN_CONFIG.REFRESH_TOKEN_EXPIRY_SECONDS);
|
|
480
|
+
}
|
|
481
|
+
this.refreshFailureCount = 0;
|
|
482
|
+
this.scheduleRefresh(TELEMETRY_TOKEN_CONFIG.INGEST_TOKEN_EXPIRY_SECONDS);
|
|
483
|
+
return parsed.data;
|
|
484
|
+
}
|
|
485
|
+
scheduleRefresh(expirySeconds) {
|
|
486
|
+
if (this.refreshTimer) {
|
|
487
|
+
clearTimeout(this.refreshTimer);
|
|
488
|
+
this.refreshTimer = null;
|
|
489
|
+
}
|
|
490
|
+
const refreshUrl = `${this.options.eventsBaseUrl.replace(/\/+$/, "")}/token/refresh`;
|
|
491
|
+
const delayMs = (expirySeconds - TELEMETRY_TOKEN_CONFIG.REFRESH_BUFFER_SECONDS) * 1e3;
|
|
492
|
+
this.refreshTimer = setTimeout(() => {
|
|
493
|
+
void this.refresh(refreshUrl);
|
|
494
|
+
}, Math.max(REFRESH_MIN_DELAY_MS, delayMs));
|
|
495
|
+
}
|
|
496
|
+
scheduleRefreshRetry() {
|
|
497
|
+
if (this.refreshTimer) {
|
|
498
|
+
clearTimeout(this.refreshTimer);
|
|
499
|
+
this.refreshTimer = null;
|
|
500
|
+
}
|
|
501
|
+
const refreshUrl = `${this.options.eventsBaseUrl.replace(/\/+$/, "")}/token/refresh`;
|
|
502
|
+
const index = Math.min(
|
|
503
|
+
this.refreshFailureCount,
|
|
504
|
+
REFRESH_RETRY_BACKOFF_MS.length - 1
|
|
505
|
+
);
|
|
506
|
+
const base = REFRESH_RETRY_BACKOFF_MS[index] ?? REFRESH_MIN_DELAY_MS;
|
|
507
|
+
const jitter = Math.floor(Math.random() * 1e3);
|
|
508
|
+
this.refreshTimer = setTimeout(() => {
|
|
509
|
+
void this.refresh(refreshUrl);
|
|
510
|
+
}, base + jitter);
|
|
511
|
+
}
|
|
512
|
+
async refresh(refreshUrl) {
|
|
513
|
+
if (!this.sessionId || !this.refreshToken) return;
|
|
514
|
+
const body = tokenRefreshRequestSchema.parse({
|
|
515
|
+
sessionId: this.sessionId,
|
|
516
|
+
eventsRefreshToken: this.refreshToken
|
|
517
|
+
});
|
|
518
|
+
try {
|
|
519
|
+
const response = await fetchWithRetry(refreshUrl, {
|
|
520
|
+
method: "POST",
|
|
521
|
+
headers: {
|
|
522
|
+
...this.options.defaultHeaders,
|
|
523
|
+
"Content-Type": "application/json"
|
|
524
|
+
},
|
|
525
|
+
body: JSON.stringify(body)
|
|
526
|
+
});
|
|
527
|
+
if (!response.ok) {
|
|
528
|
+
this.refreshFailureCount += 1;
|
|
529
|
+
this.scheduleRefreshRetry();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const json = await response.json();
|
|
533
|
+
const parsed = tokenRefreshResponseSchema.safeParse(json);
|
|
534
|
+
if (!parsed.success) {
|
|
535
|
+
this.refreshFailureCount += 1;
|
|
536
|
+
this.scheduleRefreshRetry();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.ingestToken = parsed.data.eventsIngestToken;
|
|
540
|
+
this.refreshFailureCount = 0;
|
|
541
|
+
const nextExpiry = parsed.data.expiresInSeconds > 0 ? parsed.data.expiresInSeconds : TELEMETRY_TOKEN_CONFIG.INGEST_TOKEN_EXPIRY_SECONDS;
|
|
542
|
+
this.scheduleRefresh(nextExpiry);
|
|
543
|
+
} catch {
|
|
544
|
+
this.refreshFailureCount += 1;
|
|
545
|
+
this.scheduleRefreshRetry();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
destroy() {
|
|
549
|
+
if (this.refreshTimer) {
|
|
550
|
+
clearTimeout(this.refreshTimer);
|
|
551
|
+
this.refreshTimer = null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/telemetry/telemetry-client.ts
|
|
557
|
+
var BATCH_MAX = 10;
|
|
558
|
+
var FLUSH_MS = 5e3;
|
|
559
|
+
function randomHex(length) {
|
|
560
|
+
let out = "";
|
|
561
|
+
while (out.length < length) {
|
|
562
|
+
out += Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
563
|
+
}
|
|
564
|
+
return out.slice(0, length);
|
|
565
|
+
}
|
|
566
|
+
function newEventId() {
|
|
567
|
+
const g = globalThis;
|
|
568
|
+
if (typeof g.crypto?.randomUUID === "function") {
|
|
569
|
+
return g.crypto.randomUUID();
|
|
570
|
+
}
|
|
571
|
+
return `${randomHex(8)}-${randomHex(4)}-4${randomHex(3)}-8${randomHex(3)}-${randomHex(12)}`;
|
|
572
|
+
}
|
|
573
|
+
var TelemetryClient = class {
|
|
574
|
+
constructor(options) {
|
|
575
|
+
this.options = options;
|
|
576
|
+
this.tokenManager = new TelemetryTokenManager({
|
|
577
|
+
eventsBaseUrl: options.eventsBaseUrl,
|
|
578
|
+
defaultHeaders: options.defaultHeaders,
|
|
579
|
+
viewerTokenStore: options.viewerTokenStore
|
|
580
|
+
});
|
|
581
|
+
this.heartbeat = new HeartbeatScheduler({
|
|
582
|
+
intervalSeconds: HEARTBEAT_CONFIG.INTERVAL_SECONDS,
|
|
583
|
+
onTick: () => {
|
|
584
|
+
const pos = typeof options.getPositionSeconds === "function" ? Math.floor(options.getPositionSeconds()) : this.positionSeconds;
|
|
585
|
+
void this.recordEvent({
|
|
586
|
+
eventType: PLAYBACK_EVENT_TYPE.HEARTBEAT,
|
|
587
|
+
positionSeconds: Math.max(0, pos)
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
options;
|
|
593
|
+
tokenManager;
|
|
594
|
+
heartbeat;
|
|
595
|
+
pending = [];
|
|
596
|
+
flushTimer = null;
|
|
597
|
+
positionSeconds = 0;
|
|
598
|
+
async bootstrap(params) {
|
|
599
|
+
const res = await this.tokenManager.bootstrap(params.bootstrapUrl);
|
|
600
|
+
return { sessionId: res.sessionId };
|
|
601
|
+
}
|
|
602
|
+
updatePosition(seconds) {
|
|
603
|
+
this.positionSeconds = Math.max(0, seconds);
|
|
604
|
+
}
|
|
605
|
+
async recordEvent(params) {
|
|
606
|
+
const sessionId = this.tokenManager.getSessionId();
|
|
607
|
+
if (!sessionId) return;
|
|
608
|
+
const payload = {
|
|
609
|
+
eventId: newEventId(),
|
|
610
|
+
sessionId,
|
|
611
|
+
eventType: params.eventType
|
|
612
|
+
};
|
|
613
|
+
if (params.positionSeconds !== void 0) {
|
|
614
|
+
payload.positionSeconds = params.positionSeconds;
|
|
615
|
+
}
|
|
616
|
+
if (params.ctaId !== void 0) {
|
|
617
|
+
payload.ctaId = params.ctaId;
|
|
618
|
+
}
|
|
619
|
+
if (params.errorCode !== void 0) {
|
|
620
|
+
payload.errorCode = params.errorCode;
|
|
621
|
+
}
|
|
622
|
+
this.pending.push(payload);
|
|
623
|
+
if (this.pending.length >= BATCH_MAX) {
|
|
624
|
+
await this.flush();
|
|
625
|
+
} else {
|
|
626
|
+
this.scheduleFlush();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
scheduleFlush() {
|
|
630
|
+
if (this.flushTimer) return;
|
|
631
|
+
this.flushTimer = setTimeout(() => {
|
|
632
|
+
this.flushTimer = null;
|
|
633
|
+
void this.flush();
|
|
634
|
+
}, FLUSH_MS);
|
|
635
|
+
}
|
|
636
|
+
async flush() {
|
|
637
|
+
if (this.flushTimer) {
|
|
638
|
+
clearTimeout(this.flushTimer);
|
|
639
|
+
this.flushTimer = null;
|
|
640
|
+
}
|
|
641
|
+
if (this.pending.length === 0) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const ingestToken = this.tokenManager.getIngestToken();
|
|
645
|
+
const sessionId = this.tokenManager.getSessionId();
|
|
646
|
+
if (!ingestToken || !sessionId) {
|
|
647
|
+
this.scheduleFlush();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const batch = this.pending.splice(0, BATCH_MAX);
|
|
651
|
+
const eventsUrl = `${this.options.eventsBaseUrl.replace(/\/+$/, "")}/events`;
|
|
652
|
+
try {
|
|
653
|
+
const response = await fetchWithRetry(eventsUrl, {
|
|
654
|
+
method: "POST",
|
|
655
|
+
headers: {
|
|
656
|
+
...this.options.defaultHeaders,
|
|
657
|
+
"Content-Type": "application/json",
|
|
658
|
+
Authorization: `Bearer ${ingestToken}`
|
|
659
|
+
},
|
|
660
|
+
body: JSON.stringify({ events: batch })
|
|
661
|
+
});
|
|
662
|
+
if (!response.ok && response.status >= 500) {
|
|
663
|
+
this.pending.unshift(...batch);
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
this.pending.unshift(...batch);
|
|
667
|
+
}
|
|
668
|
+
if (this.pending.length > 0) {
|
|
669
|
+
this.scheduleFlush();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
startHeartbeats() {
|
|
673
|
+
this.heartbeat.start();
|
|
674
|
+
}
|
|
675
|
+
stopHeartbeats() {
|
|
676
|
+
this.heartbeat.stop();
|
|
677
|
+
}
|
|
678
|
+
pauseHeartbeats() {
|
|
679
|
+
this.heartbeat.pause();
|
|
680
|
+
}
|
|
681
|
+
resumeHeartbeats() {
|
|
682
|
+
this.heartbeat.resume();
|
|
683
|
+
}
|
|
684
|
+
async destroy() {
|
|
685
|
+
this.heartbeat.destroy();
|
|
686
|
+
this.tokenManager.destroy();
|
|
687
|
+
await this.flush();
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// src/telemetry/viewer-token-store.ts
|
|
692
|
+
var MemoryViewerTokenStore = class {
|
|
693
|
+
value = null;
|
|
694
|
+
async get() {
|
|
695
|
+
return this.value;
|
|
696
|
+
}
|
|
697
|
+
async set(token, _ttlSec) {
|
|
698
|
+
this.value = token;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// src/playback/playback-refresh.ts
|
|
703
|
+
var REFRESH_BUFFER_MS = 6e4;
|
|
704
|
+
function planRefresh(params) {
|
|
705
|
+
const { expiresAt, refreshAfter } = params.playback;
|
|
706
|
+
if (!expiresAt && !refreshAfter) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
const anchorIso = refreshAfter ?? expiresAt;
|
|
710
|
+
if (!anchorIso) return null;
|
|
711
|
+
const anchor = Date.parse(anchorIso);
|
|
712
|
+
if (!Number.isFinite(anchor)) return null;
|
|
713
|
+
const target = anchor - REFRESH_BUFFER_MS;
|
|
714
|
+
const delayMs = Math.max(0, target - params.now.getTime());
|
|
715
|
+
return { delayMs };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/config/telemetry-bootstrap-url.ts
|
|
719
|
+
var BOOTSTRAP_SEGMENT_PATTERN = /\/bootstrap\/[^/]+\/?$/i;
|
|
720
|
+
var TELEMETRY_PATH_MARKER = "/telemetry";
|
|
721
|
+
function deriveTelemetryEventsBaseUrlFromBootstrapUrl(bootstrapUrl) {
|
|
722
|
+
try {
|
|
723
|
+
const u = new URL(bootstrapUrl);
|
|
724
|
+
const stripped = u.pathname.replace(BOOTSTRAP_SEGMENT_PATTERN, "");
|
|
725
|
+
if (!stripped.includes(TELEMETRY_PATH_MARKER)) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
const normalizedPath = stripped.replace(/\/+$/, "") || "/";
|
|
729
|
+
return `${u.origin}${normalizedPath}`;
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
Object.defineProperty(exports, "PLAYER_API_EVENTS", {
|
|
736
|
+
enumerable: true,
|
|
737
|
+
get: function () { return playerTypes.PLAYER_API_EVENTS; }
|
|
738
|
+
});
|
|
739
|
+
exports.HEARTBEAT_CONFIG = HEARTBEAT_CONFIG;
|
|
740
|
+
exports.MOVIIE_CDN_BASE = MOVIIE_CDN_BASE;
|
|
741
|
+
exports.MOVIIE_DEFAULT_API_BASE_URL = MOVIIE_DEFAULT_API_BASE_URL;
|
|
742
|
+
exports.MOVIIE_TELEMETRY_API_PATH_PREFIX = MOVIIE_TELEMETRY_API_PATH_PREFIX;
|
|
743
|
+
exports.MOVIIE_WATCH_BASE = MOVIIE_WATCH_BASE;
|
|
744
|
+
exports.MemoryViewerTokenStore = MemoryViewerTokenStore;
|
|
745
|
+
exports.MoviieAuthError = MoviieAuthError;
|
|
746
|
+
exports.MoviieBundleBlockedError = MoviieBundleBlockedError;
|
|
747
|
+
exports.MoviieClient = MoviieClient;
|
|
748
|
+
exports.MoviieNetworkError = MoviieNetworkError;
|
|
749
|
+
exports.MoviieNotFoundError = MoviieNotFoundError;
|
|
750
|
+
exports.MoviieRateLimitError = MoviieRateLimitError;
|
|
751
|
+
exports.MoviieReferrerBlockedError = MoviieReferrerBlockedError;
|
|
752
|
+
exports.MoviieSubscriptionInactiveError = MoviieSubscriptionInactiveError;
|
|
753
|
+
exports.PLAYBACK_EVENT_TYPE = PLAYBACK_EVENT_TYPE;
|
|
754
|
+
exports.SDK_PLAYBACK_AUTH_ERROR_MESSAGE = SDK_PLAYBACK_AUTH_ERROR_MESSAGE;
|
|
755
|
+
exports.SDK_PUBLIC_API_KEY_PREFIX_PUBLISHABLE = SDK_PUBLIC_API_KEY_PREFIX_PUBLISHABLE;
|
|
756
|
+
exports.TELEMETRY_TOKEN_CONFIG = TELEMETRY_TOKEN_CONFIG;
|
|
757
|
+
exports.TelemetryClient = TelemetryClient;
|
|
758
|
+
exports.buildClientHeaders = buildClientHeaders;
|
|
759
|
+
exports.configureMoviieEndpoints = configureMoviieEndpoints;
|
|
760
|
+
exports.deriveMoviieTelemetryBaseUrlFromApiBaseUrl = deriveMoviieTelemetryBaseUrlFromApiBaseUrl;
|
|
761
|
+
exports.deriveTelemetryEventsBaseUrlFromBootstrapUrl = deriveTelemetryEventsBaseUrlFromBootstrapUrl;
|
|
762
|
+
exports.fetchPlaybackData = fetchPlaybackData;
|
|
763
|
+
exports.getMoviieApiBaseUrl = getMoviieApiBaseUrl;
|
|
764
|
+
exports.getMoviieEventsBaseUrl = getMoviieEventsBaseUrl;
|
|
765
|
+
exports.planRefresh = planRefresh;
|
|
766
|
+
exports.resetMoviieEndpointsConfiguration = resetMoviieEndpointsConfiguration;
|
|
767
|
+
//# sourceMappingURL=index.cjs.map
|
|
768
|
+
//# sourceMappingURL=index.cjs.map
|