@chunkify/analytics 0.1.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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -0
  3. package/dist/core.d.ts +144 -0
  4. package/dist/core.d.ts.map +1 -0
  5. package/dist/core.js +660 -0
  6. package/dist/core.js.map +1 -0
  7. package/dist/datadog.d.ts +8 -0
  8. package/dist/datadog.d.ts.map +1 -0
  9. package/dist/datadog.js +15 -0
  10. package/dist/datadog.js.map +1 -0
  11. package/dist/event-types.d.ts +3 -0
  12. package/dist/event-types.d.ts.map +1 -0
  13. package/dist/event-types.js +5 -0
  14. package/dist/event-types.js.map +1 -0
  15. package/dist/hlsjs.d.ts +10 -0
  16. package/dist/hlsjs.d.ts.map +1 -0
  17. package/dist/hlsjs.js +185 -0
  18. package/dist/hlsjs.js.map +1 -0
  19. package/dist/http.d.ts +4 -0
  20. package/dist/http.d.ts.map +1 -0
  21. package/dist/http.js +43 -0
  22. package/dist/http.js.map +1 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +7 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mapper.d.ts +11 -0
  28. package/dist/mapper.d.ts.map +1 -0
  29. package/dist/mapper.js +11 -0
  30. package/dist/mapper.js.map +1 -0
  31. package/dist/monitor.d.ts +6 -0
  32. package/dist/monitor.d.ts.map +1 -0
  33. package/dist/monitor.js +14 -0
  34. package/dist/monitor.js.map +1 -0
  35. package/dist/options.d.ts +4 -0
  36. package/dist/options.d.ts.map +1 -0
  37. package/dist/options.js +26 -0
  38. package/dist/options.js.map +1 -0
  39. package/dist/otlp.d.ts +4 -0
  40. package/dist/otlp.d.ts.map +1 -0
  41. package/dist/otlp.js +56 -0
  42. package/dist/otlp.js.map +1 -0
  43. package/dist/player.d.ts +8 -0
  44. package/dist/player.d.ts.map +1 -0
  45. package/dist/player.js +93 -0
  46. package/dist/player.js.map +1 -0
  47. package/dist/posthog.d.ts +8 -0
  48. package/dist/posthog.d.ts.map +1 -0
  49. package/dist/posthog.js +15 -0
  50. package/dist/posthog.js.map +1 -0
  51. package/dist/shaka.d.ts +26 -0
  52. package/dist/shaka.d.ts.map +1 -0
  53. package/dist/shaka.js +164 -0
  54. package/dist/shaka.js.map +1 -0
  55. package/dist/videojs.d.ts +11 -0
  56. package/dist/videojs.d.ts.map +1 -0
  57. package/dist/videojs.js +77 -0
  58. package/dist/videojs.js.map +1 -0
  59. package/package.json +75 -0
  60. package/src/core.ts +774 -0
  61. package/src/datadog.ts +24 -0
  62. package/src/event-types.ts +6 -0
  63. package/src/hlsjs.ts +192 -0
  64. package/src/http.ts +43 -0
  65. package/src/index.ts +11 -0
  66. package/src/mapper.ts +22 -0
  67. package/src/monitor.ts +15 -0
  68. package/src/options.ts +28 -0
  69. package/src/otlp.ts +70 -0
  70. package/src/player.ts +96 -0
  71. package/src/posthog.ts +24 -0
  72. package/src/shaka.ts +181 -0
  73. package/src/videojs.ts +89 -0
package/dist/core.js ADDED
@@ -0,0 +1,660 @@
1
+ import { analyticsEventTypes } from "./event-types.js";
2
+ import { assertAnalyticsOptions } from "./options.js";
3
+ const sdkVersion = "0.1.0"; // x-release-please-version
4
+ function finiteNonNegative(value) {
5
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
6
+ }
7
+ function nonNegativeDelta(current, baseline) {
8
+ return current === null || baseline === null ? null : finiteNonNegative(current - baseline);
9
+ }
10
+ function finitePositive(value) {
11
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
12
+ }
13
+ function intrinsicVideoSize(media) {
14
+ try {
15
+ const customMedia = media;
16
+ const video = (customMedia.nativeEl?.localName === "video" ? customMedia.nativeEl : media);
17
+ const width = finitePositive(video.videoWidth);
18
+ const height = finitePositive(video.videoHeight);
19
+ return width !== null && height !== null ? { width, height } : { width: null, height: null };
20
+ }
21
+ catch {
22
+ return { width: null, height: null };
23
+ }
24
+ }
25
+ function normalizedRendition(rendition) {
26
+ if (!rendition)
27
+ return null;
28
+ const result = {
29
+ width: finiteNonNegative(rendition.width),
30
+ height: finiteNonNegative(rendition.height),
31
+ codec: typeof rendition.codec === "string" && rendition.codec ? rendition.codec : null,
32
+ bitrate_bps: finiteNonNegative(rendition.bitrate_bps),
33
+ frame_rate: finiteNonNegative(rendition.frame_rate),
34
+ };
35
+ return Object.values(result).every((value) => value === null) ? null : result;
36
+ }
37
+ function renditionKey(rendition) {
38
+ return JSON.stringify([rendition.width, rendition.height, rendition.codec, rendition.bitrate_bps, rendition.frame_rate]);
39
+ }
40
+ function renditionPlayingTime(current, now) {
41
+ if (!current.renditionTrackingAvailable)
42
+ return null;
43
+ const times = new Map([...current.renditionTimes].map(([key, value]) => [key, { ...value }]));
44
+ if (current.activeRendition && current.activeRenditionSince !== null) {
45
+ const key = renditionKey(current.activeRendition);
46
+ const entry = times.get(key) ?? { ...current.activeRendition, playing_time_ms: 0 };
47
+ entry.playing_time_ms += Math.max(0, now - current.activeRenditionSince);
48
+ times.set(key, entry);
49
+ }
50
+ return [...times.values()];
51
+ }
52
+ const emptyConnectorMetrics = {
53
+ selected_rendition_width: null,
54
+ selected_rendition_height: null,
55
+ selected_rendition_codec: null,
56
+ selected_rendition_bitrate_bps: null,
57
+ rendition_switch_count: null,
58
+ delivery_request_count: null,
59
+ delivery_transferred_bytes: null,
60
+ delivery_throughput_bps: null,
61
+ };
62
+ function connectorMetrics(connector, baseline) {
63
+ if (!connector || !baseline)
64
+ return { ...emptyConnectorMetrics };
65
+ let current;
66
+ try {
67
+ current = connector.snapshot();
68
+ }
69
+ catch {
70
+ return { ...emptyConnectorMetrics };
71
+ }
72
+ const switches = nonNegativeDelta(current.renditionSwitchCount, baseline.renditionSwitchCount);
73
+ const requests = nonNegativeDelta(current.deliveryRequestCount, baseline.deliveryRequestCount);
74
+ const bytes = nonNegativeDelta(current.deliveryTransferredBytes, baseline.deliveryTransferredBytes);
75
+ const deliveryDuration = nonNegativeDelta(current.deliveryDurationMs, baseline.deliveryDurationMs);
76
+ return {
77
+ selected_rendition_width: finiteNonNegative(current.renditionWidth),
78
+ selected_rendition_height: finiteNonNegative(current.renditionHeight),
79
+ selected_rendition_codec: typeof current.renditionCodec === "string" && current.renditionCodec ? current.renditionCodec : null,
80
+ selected_rendition_bitrate_bps: finiteNonNegative(current.renditionBitrate),
81
+ rendition_switch_count: switches,
82
+ delivery_request_count: requests,
83
+ delivery_transferred_bytes: bytes,
84
+ delivery_throughput_bps: bytes !== null && deliveryDuration !== null && deliveryDuration > 0
85
+ ? bytes * 8 * 1000 / deliveryDuration : null,
86
+ };
87
+ }
88
+ function deliveryHostname(media) {
89
+ try {
90
+ const url = new URL(media.currentSrc || media.src);
91
+ return url.protocol === "http:" || url.protocol === "https:" ? url.hostname : null;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ function duration(media) {
98
+ return Number.isFinite(media.duration) && media.duration >= 0 ? media.duration : null;
99
+ }
100
+ function mediaErrorCategory(code) {
101
+ return code === 1 ? "aborted" : code === 2 ? "network" : code === 3 ? "decode"
102
+ : code === 4 ? "unsupported" : "unknown";
103
+ }
104
+ function frameCounts(media) {
105
+ const video = media;
106
+ if (typeof video.getVideoPlaybackQuality === "function") {
107
+ try {
108
+ const quality = video.getVideoPlaybackQuality();
109
+ if (Number.isInteger(quality.droppedVideoFrames) && quality.droppedVideoFrames >= 0 &&
110
+ Number.isInteger(quality.totalVideoFrames) && quality.totalVideoFrames >= 0) {
111
+ return { dropped: quality.droppedVideoFrames, total: quality.totalVideoFrames };
112
+ }
113
+ }
114
+ catch {
115
+ // Treat an inaccessible browser counter like an unavailable measurement.
116
+ }
117
+ }
118
+ const webkit = media;
119
+ if (!Number.isInteger(webkit.webkitDecodedFrameCount) || (webkit.webkitDecodedFrameCount ?? -1) < 0 ||
120
+ !Number.isInteger(webkit.webkitDroppedFrameCount) || (webkit.webkitDroppedFrameCount ?? -1) < 0)
121
+ return null;
122
+ return { dropped: webkit.webkitDroppedFrameCount, total: webkit.webkitDecodedFrameCount };
123
+ }
124
+ export function attachAnalytics(media, options) {
125
+ return createAnalyticsSession(media, options, false);
126
+ }
127
+ /** Engine adapters call this when they own source-change events. */
128
+ export function attachAnalyticsWithManagedSource(media, options) {
129
+ return createAnalyticsSession(media, options, true);
130
+ }
131
+ function createAnalyticsSession(media, options, sourceManagedByEngine) {
132
+ assertAnalyticsOptions(options);
133
+ const frameVideo = media;
134
+ const { videoId, environment = null, viewerId = null, snapshotIntervalMs = 30000 } = options;
135
+ const destinations = [...options.destinations];
136
+ const includedEvents = new Set((options.includeEvents ?? analyticsEventTypes)
137
+ .map((event) => `chunkify.video.${event}`));
138
+ let view = null;
139
+ let destroyed = false;
140
+ let wantsPlay = false;
141
+ let seeking = false;
142
+ let snapshotTimer = null;
143
+ let connector = null;
144
+ let unsubscribeConnector = null;
145
+ let unsubscribeRenditions = null;
146
+ let selectedRendition = null;
147
+ function refreshIntrinsicVideoSize(current) {
148
+ const size = intrinsicVideoSize(media);
149
+ if (size.width === null || size.height === null)
150
+ return;
151
+ current.intrinsicVideoWidth = size.width;
152
+ current.intrinsicVideoHeight = size.height;
153
+ }
154
+ function cancelFirstVideoFrame(current) {
155
+ if (current.frameCallbackId === null)
156
+ return;
157
+ const callbackId = current.frameCallbackId;
158
+ current.frameCallbackId = null;
159
+ try {
160
+ frameVideo.cancelVideoFrameCallback?.(callbackId);
161
+ }
162
+ catch { }
163
+ }
164
+ function observeFirstVideoFrame(current) {
165
+ if (typeof frameVideo.requestVideoFrameCallback !== "function" ||
166
+ typeof frameVideo.cancelVideoFrameCallback !== "function")
167
+ return;
168
+ try {
169
+ current.frameCallbackId = frameVideo.requestVideoFrameCallback((observedAt) => {
170
+ if (current.frameCallbackId === null)
171
+ return;
172
+ current.frameCallbackId = null;
173
+ if (view !== current)
174
+ return;
175
+ // WebKit can report a presentationTime outside performance.now()'s clock.
176
+ if (!Number.isFinite(observedAt) || observedAt < current.startedAt)
177
+ return;
178
+ current.firstVideoFrame = observedAt - current.startedAt;
179
+ });
180
+ }
181
+ catch {
182
+ // Frame timing is optional; an inaccessible browser API leaves it unavailable.
183
+ }
184
+ }
185
+ function closeActiveRendition(now) {
186
+ if (!view || !view.activeRendition || view.activeRenditionSince === null)
187
+ return;
188
+ const key = renditionKey(view.activeRendition);
189
+ const entry = view.renditionTimes.get(key) ?? { ...view.activeRendition, playing_time_ms: 0 };
190
+ entry.playing_time_ms += Math.max(0, now - view.activeRenditionSince);
191
+ view.renditionTimes.set(key, entry);
192
+ view.activeRendition = null;
193
+ view.activeRenditionSince = null;
194
+ }
195
+ function onRenditionChanged(rendition) {
196
+ const next = normalizedRendition(rendition);
197
+ const previous = selectedRendition;
198
+ if (previous && next && renditionKey(previous) === renditionKey(next))
199
+ return;
200
+ const now = performance.now();
201
+ closeActiveRendition(now);
202
+ selectedRendition = next;
203
+ if (view && view.playingSince !== null && selectedRendition) {
204
+ view.activeRendition = selectedRendition;
205
+ view.activeRenditionSince = now;
206
+ }
207
+ if (view && previous && next)
208
+ emitRenditionChange(view, previous, next);
209
+ }
210
+ function playheadSeconds() {
211
+ return Number.isFinite(media.currentTime) && media.currentTime >= 0 ? media.currentTime : null;
212
+ }
213
+ function envelope(current, event_name) {
214
+ return {
215
+ schema_version: 1, event_name, event_id: crypto.randomUUID(), sequence: ++current.sequence,
216
+ occurred_at: new Date().toISOString(), view_id: current.id, video_id: videoId,
217
+ sdk_name: "@chunkify/analytics", sdk_version: sdkVersion,
218
+ connector_name: connector?.name ?? "html-media", connector_version: connector?.version ?? sdkVersion,
219
+ player_name: connector?.playerName ?? null, player_version: connector?.playerVersion ?? null,
220
+ delivery_hostname: current.hostname, environment, viewer_id: viewerId,
221
+ reason: null, startup_outcome: null, error_category: null, error_code: null,
222
+ error_fatal: null, error_playhead_seconds: null,
223
+ };
224
+ }
225
+ function deliver(event) {
226
+ for (const destination of destinations) {
227
+ try {
228
+ // A failing destination must not interrupt playback or another destination.
229
+ void Promise.resolve(destination.send(event)).catch(() => { });
230
+ }
231
+ catch {
232
+ // Synchronous delivery failures are isolated for the same reason.
233
+ }
234
+ }
235
+ }
236
+ function changePlaybackState(next, current = view) {
237
+ if (!current || current.playbackState === next)
238
+ return;
239
+ if (current.playbackState === "ended" || current.playbackState === "stopped" ||
240
+ (current.playbackState === "error" && next !== "ended" && next !== "stopped"))
241
+ return;
242
+ const now = performance.now();
243
+ const previous = current.playbackState;
244
+ const elapsed = Math.max(0, now - current.playbackStateSince);
245
+ current.playbackState = next;
246
+ current.playbackStateSince = now;
247
+ if (!includedEvents.has("chunkify.video.playback_state_changed"))
248
+ return;
249
+ deliver({ ...envelope(current, "chunkify.video.playback_state_changed"),
250
+ event_name: "chunkify.video.playback_state_changed", playhead_seconds: playheadSeconds(),
251
+ from_state: previous, to_state: next, previous_state_duration_ms: elapsed });
252
+ }
253
+ function emitRenditionChange(current, previous, selected) {
254
+ if (!includedEvents.has("chunkify.video.rendition_changed"))
255
+ return;
256
+ deliver({ ...envelope(current, "chunkify.video.rendition_changed"),
257
+ event_name: "chunkify.video.rendition_changed", playhead_seconds: playheadSeconds(),
258
+ previous_rendition_width: previous.width, previous_rendition_height: previous.height,
259
+ previous_rendition_codec: previous.codec, previous_rendition_bitrate_bps: previous.bitrate_bps,
260
+ previous_rendition_frame_rate: previous.frame_rate,
261
+ selected_rendition_width: selected.width, selected_rendition_height: selected.height,
262
+ selected_rendition_codec: selected.codec, selected_rendition_bitrate_bps: selected.bitrate_bps,
263
+ selected_rendition_frame_rate: selected.frame_rate });
264
+ }
265
+ function emit(current, event_name, reason = null, error = null) {
266
+ if (!includedEvents.has(event_name))
267
+ return;
268
+ const now = performance.now();
269
+ const playingTime = current.playing + (current.playingSince === null ? 0 : now - current.playingSince);
270
+ const rebufferDuration = current.rebufferDuration + (current.rebufferSince === null ? 0 : now - current.rebufferSince);
271
+ const seekDuration = current.seekDuration + (current.seekSince === null ? 0 : now - current.seekSince);
272
+ const rebufferDenominator = playingTime + rebufferDuration;
273
+ const frames = frameCounts(media);
274
+ let viewFrames = null;
275
+ if (frames && current.frameBaseline && current.framePrevious && current.frameMetricsValid) {
276
+ if (frames.dropped < current.framePrevious.dropped || frames.total < current.framePrevious.total) {
277
+ current.frameMetricsValid = false;
278
+ }
279
+ else {
280
+ viewFrames = {
281
+ dropped: frames.dropped - current.frameBaseline.dropped,
282
+ total: frames.total - current.frameBaseline.total,
283
+ };
284
+ }
285
+ }
286
+ if (frames)
287
+ current.framePrevious = frames;
288
+ const engineMetrics = connectorMetrics(connector, current.connectorBaseline);
289
+ let startupOutcome = null;
290
+ if (event_name === "chunkify.video.view_ended") {
291
+ if (current.startup !== null)
292
+ startupOutcome = "started";
293
+ else if (current.fatalErrorBeforeStart)
294
+ startupOutcome = "failed_before_start";
295
+ else
296
+ startupOutcome = "ended_before_start";
297
+ }
298
+ refreshIntrinsicVideoSize(current);
299
+ const event = {
300
+ ...envelope(current, event_name), event_name, reason, startup_outcome: startupOutcome,
301
+ error_category: error?.category ?? null,
302
+ error_code: error?.code ?? null,
303
+ error_fatal: error?.fatal ?? null,
304
+ error_playhead_seconds: error?.playhead ?? null,
305
+ media_duration_seconds: current.duration,
306
+ startup_time_ms: current.startup,
307
+ first_video_frame_time_ms: current.firstVideoFrame,
308
+ playing_time_ms: playingTime,
309
+ rebuffer_count: current.rebufferCount,
310
+ rebuffer_duration_ms: rebufferDuration,
311
+ rebuffer_ratio: rebufferDenominator > 0 ? rebufferDuration / rebufferDenominator : null,
312
+ seek_count: current.seekCount,
313
+ seek_duration_ms: seekDuration,
314
+ maximum_playhead_seconds: current.maximumPlayhead,
315
+ furthest_progress_ratio: current.duration === null || current.duration === 0 ? null : Math.min(1, current.maximumPlayhead / current.duration),
316
+ video_intrinsic_width_css_px: current.intrinsicVideoWidth,
317
+ video_intrinsic_height_css_px: current.intrinsicVideoHeight,
318
+ dropped_video_frames: viewFrames?.dropped ?? null,
319
+ total_video_frames: viewFrames?.total ?? null,
320
+ ...engineMetrics,
321
+ rendition_playing_time: renditionPlayingTime(current, now),
322
+ };
323
+ deliver(event);
324
+ }
325
+ function startAttempt() {
326
+ if (destroyed)
327
+ return;
328
+ wantsPlay = true;
329
+ if (view) {
330
+ if (view.playbackState === "paused")
331
+ changePlaybackState("resuming");
332
+ return;
333
+ }
334
+ const initialFrames = frameCounts(media);
335
+ const initialVideoSize = intrinsicVideoSize(media);
336
+ const startedAt = performance.now();
337
+ const current = { id: crypto.randomUUID(), sequence: 0, startedAt,
338
+ startup: null, firstVideoFrame: null, frameCallbackId: null,
339
+ playing: 0, playingSince: null,
340
+ rebufferCount: 0, rebufferDuration: 0, rebufferSince: null,
341
+ seekCount: 0, seekDuration: 0, seekSince: null,
342
+ maximumPlayhead: Number.isFinite(media.currentTime) ? Math.max(0, media.currentTime) : 0,
343
+ nextSnapshotAt: snapshotIntervalMs,
344
+ duration: duration(media),
345
+ intrinsicVideoWidth: initialVideoSize.width, intrinsicVideoHeight: initialVideoSize.height,
346
+ hostname: deliveryHostname(media), source: media.src || media.currentSrc,
347
+ // NETWORK_IDLE (1) and NETWORK_LOADING (2) already have a selected resource.
348
+ resourceStarted: media.readyState > 0 || media.networkState === 1 || media.networkState === 2,
349
+ errorEmitted: false, fatalErrorBeforeStart: false,
350
+ frameBaseline: initialFrames, framePrevious: initialFrames,
351
+ frameMetricsValid: initialFrames !== null,
352
+ connectorBaseline: connector ? safeConnectorSnapshot(connector) : null,
353
+ renditionTrackingAvailable: Boolean(connector?.subscribeToRenditions),
354
+ renditionTimes: new Map(), activeRendition: null, activeRenditionSince: null,
355
+ playbackState: "startup", playbackStateSince: startedAt };
356
+ view = current;
357
+ if (includedEvents.size > 0)
358
+ observeFirstVideoFrame(current);
359
+ emit(current, "chunkify.video.view_started");
360
+ if (view !== current)
361
+ return;
362
+ if (media.error)
363
+ reportMediaError();
364
+ }
365
+ function stopPlaying() {
366
+ if (snapshotTimer !== null) {
367
+ clearTimeout(snapshotTimer);
368
+ snapshotTimer = null;
369
+ }
370
+ if (!view || view.playingSince === null)
371
+ return;
372
+ const now = performance.now();
373
+ closeActiveRendition(now);
374
+ view.playing += now - view.playingSince;
375
+ view.playingSince = null;
376
+ }
377
+ function stopRebuffering() {
378
+ if (!view || view.rebufferSince === null)
379
+ return;
380
+ view.rebufferDuration += performance.now() - view.rebufferSince;
381
+ view.rebufferSince = null;
382
+ }
383
+ function stopSeeking() {
384
+ if (!view || view.seekSince === null)
385
+ return;
386
+ view.seekDuration += performance.now() - view.seekSince;
387
+ view.seekSince = null;
388
+ }
389
+ function updatePlaybackPosition() {
390
+ if (!view)
391
+ return;
392
+ refreshIntrinsicVideoSize(view);
393
+ if (!Number.isFinite(media.currentTime))
394
+ return;
395
+ view.maximumPlayhead = Math.max(view.maximumPlayhead, media.currentTime);
396
+ }
397
+ function reportMediaError() {
398
+ if (!view)
399
+ return;
400
+ const current = view;
401
+ cancelFirstVideoFrame(current);
402
+ if (current.startup === null)
403
+ current.fatalErrorBeforeStart = true;
404
+ if (current.errorEmitted)
405
+ return;
406
+ updatePlaybackPosition();
407
+ current.errorEmitted = true;
408
+ const code = media.error && Number.isFinite(media.error.code) ? media.error.code : null;
409
+ emit(current, "chunkify.video.playback_error", null, {
410
+ category: mediaErrorCategory(code), code, fatal: true,
411
+ playhead: Number.isFinite(media.currentTime) ? Math.max(0, media.currentTime) : null,
412
+ });
413
+ if (view === current)
414
+ changePlaybackState("error");
415
+ }
416
+ function reportConnectorError(error) {
417
+ if (!view)
418
+ return;
419
+ const current = view;
420
+ if (error.fatal)
421
+ cancelFirstVideoFrame(current);
422
+ if (error.fatal && current.startup === null)
423
+ current.fatalErrorBeforeStart = true;
424
+ if (current.errorEmitted)
425
+ return;
426
+ if (error.fatal) {
427
+ stopPlaying();
428
+ stopRebuffering();
429
+ stopSeeking();
430
+ seeking = false;
431
+ wantsPlay = false;
432
+ }
433
+ updatePlaybackPosition();
434
+ current.errorEmitted = true;
435
+ emit(current, "chunkify.video.playback_error", null, {
436
+ category: error.category,
437
+ code: typeof error.code === "string" || typeof error.code === "number" ? error.code : null,
438
+ fatal: Boolean(error.fatal),
439
+ playhead: Number.isFinite(media.currentTime) ? Math.max(0, media.currentTime) : null,
440
+ });
441
+ if (error.fatal && view === current)
442
+ changePlaybackState("error");
443
+ }
444
+ function setConnector(next) {
445
+ if (destroyed)
446
+ return;
447
+ const previousUnsubscribe = unsubscribeConnector;
448
+ const previousRenditionUnsubscribe = unsubscribeRenditions;
449
+ unsubscribeConnector = null;
450
+ unsubscribeRenditions = null;
451
+ try {
452
+ previousUnsubscribe?.();
453
+ }
454
+ catch { }
455
+ try {
456
+ previousRenditionUnsubscribe?.();
457
+ }
458
+ catch { }
459
+ if (destroyed)
460
+ return;
461
+ onRenditionChanged(null);
462
+ connector = next;
463
+ if (view)
464
+ view.connectorBaseline = connector ? safeConnectorSnapshot(connector) : null;
465
+ if (view && connector?.subscribeToRenditions)
466
+ view.renditionTrackingAvailable = true;
467
+ if (connector) {
468
+ let unsubscribe = null;
469
+ try {
470
+ unsubscribe = connector.subscribeToErrors(reportConnectorError);
471
+ }
472
+ catch { }
473
+ let unsubscribeRendition = null;
474
+ try {
475
+ unsubscribeRendition = connector.subscribeToRenditions?.(onRenditionChanged) ?? null;
476
+ }
477
+ catch { }
478
+ if (destroyed) {
479
+ try {
480
+ unsubscribe?.();
481
+ }
482
+ catch { }
483
+ try {
484
+ unsubscribeRendition?.();
485
+ }
486
+ catch { }
487
+ return;
488
+ }
489
+ unsubscribeConnector = unsubscribe;
490
+ unsubscribeRenditions = unsubscribeRendition;
491
+ }
492
+ }
493
+ function scheduleSnapshot() {
494
+ if (!view || !includedEvents.has("chunkify.video.view_snapshot") || snapshotTimer !== null)
495
+ return;
496
+ const playingSince = view.playingSince;
497
+ if (playingSince === null)
498
+ return;
499
+ const current = view;
500
+ const playingTime = current.playing + performance.now() - playingSince;
501
+ snapshotTimer = globalThis.setTimeout(() => {
502
+ snapshotTimer = null;
503
+ if (view !== current || current.playingSince === null)
504
+ return;
505
+ updatePlaybackPosition();
506
+ metadata();
507
+ emit(current, "chunkify.video.view_snapshot");
508
+ const currentPlayingTime = current.playing + performance.now() - current.playingSince;
509
+ const nextFutureInterval = (Math.floor(currentPlayingTime / snapshotIntervalMs) + 1) * snapshotIntervalMs;
510
+ current.nextSnapshotAt = Math.max(current.nextSnapshotAt + snapshotIntervalMs, nextFutureInterval);
511
+ if (view === current)
512
+ scheduleSnapshot();
513
+ }, Math.max(0, current.nextSnapshotAt - playingTime));
514
+ }
515
+ function playing() {
516
+ if (!view || !wantsPlay || seeking)
517
+ return;
518
+ refreshIntrinsicVideoSize(view);
519
+ const now = performance.now();
520
+ view.resourceStarted = true;
521
+ view.startup ??= now - view.startedAt;
522
+ stopRebuffering();
523
+ view.playingSince ??= now;
524
+ if (view.activeRenditionSince === null && selectedRendition) {
525
+ view.activeRendition = selectedRendition;
526
+ view.activeRenditionSince = now;
527
+ }
528
+ changePlaybackState("playing");
529
+ scheduleSnapshot();
530
+ }
531
+ function finish(reason) {
532
+ if (!view)
533
+ return;
534
+ const current = view;
535
+ cancelFirstVideoFrame(current);
536
+ stopPlaying();
537
+ stopRebuffering();
538
+ stopSeeking();
539
+ updatePlaybackPosition();
540
+ view = null;
541
+ wantsPlay = false;
542
+ seeking = false;
543
+ changePlaybackState(reason === "ended" ? "ended" : "stopped", current);
544
+ emit(current, "chunkify.video.view_ended", reason);
545
+ }
546
+ function metadata() {
547
+ if (!view)
548
+ return;
549
+ view.duration = duration(media);
550
+ refreshIntrinsicVideoSize(view);
551
+ view.hostname = deliveryHostname(media);
552
+ }
553
+ const listeners = {
554
+ play: startAttempt, playing,
555
+ waiting() {
556
+ stopPlaying();
557
+ if (view && view.startup !== null && wantsPlay && !seeking && view.rebufferSince === null) {
558
+ view.rebufferCount++;
559
+ view.rebufferSince = performance.now();
560
+ changePlaybackState("buffering");
561
+ }
562
+ },
563
+ error() {
564
+ stopPlaying();
565
+ stopRebuffering();
566
+ stopSeeking();
567
+ seeking = false;
568
+ wantsPlay = false;
569
+ reportMediaError();
570
+ },
571
+ ended() { finish("ended"); },
572
+ emptied() {
573
+ if (sourceManagedByEngine)
574
+ return;
575
+ if (!view)
576
+ return;
577
+ // A new attempt can begin after src changes but before the old resource's
578
+ // queued emptied event arrives. That reset does not belong to the new view.
579
+ if (view.resourceStarted || (view.source && view.source !== (media.src || media.currentSrc)))
580
+ finish("source_changed");
581
+ },
582
+ loadstart() {
583
+ if (!view)
584
+ return;
585
+ const source = media.src || media.currentSrc;
586
+ if (!sourceManagedByEngine && view.source && view.source !== source) {
587
+ finish("source_changed");
588
+ return;
589
+ }
590
+ // <source> selection can first expose the URL after the initial attempt.
591
+ view.source = source;
592
+ view.resourceStarted = true;
593
+ },
594
+ pause() {
595
+ stopPlaying();
596
+ stopRebuffering();
597
+ wantsPlay = false;
598
+ if (view && !media.ended)
599
+ changePlaybackState("paused");
600
+ },
601
+ seeking() {
602
+ stopPlaying();
603
+ stopRebuffering();
604
+ updatePlaybackPosition();
605
+ if (view && !seeking) {
606
+ view.seekCount++;
607
+ view.seekSince = performance.now();
608
+ }
609
+ seeking = true;
610
+ changePlaybackState("seeking");
611
+ },
612
+ seeked() {
613
+ updatePlaybackPosition();
614
+ stopSeeking();
615
+ seeking = false;
616
+ changePlaybackState(wantsPlay ? "resuming" : "paused");
617
+ },
618
+ timeupdate: updatePlaybackPosition,
619
+ loadedmetadata: metadata, durationchange: metadata,
620
+ };
621
+ for (const [name, listener] of Object.entries(listeners))
622
+ media.addEventListener(name, listener);
623
+ const page = media.ownerDocument.defaultView;
624
+ const pagehide = () => finish("page_exit");
625
+ page?.addEventListener("pagehide", pagehide);
626
+ return {
627
+ startAttempt,
628
+ sourceChanged() { finish("source_changed"); },
629
+ setConnector,
630
+ destroy() {
631
+ if (destroyed)
632
+ return;
633
+ destroyed = true;
634
+ for (const [name, listener] of Object.entries(listeners))
635
+ media.removeEventListener(name, listener);
636
+ page?.removeEventListener("pagehide", pagehide);
637
+ finish("destroyed");
638
+ try {
639
+ unsubscribeConnector?.();
640
+ }
641
+ catch { }
642
+ try {
643
+ unsubscribeRenditions?.();
644
+ }
645
+ catch { }
646
+ unsubscribeConnector = null;
647
+ unsubscribeRenditions = null;
648
+ connector = null;
649
+ },
650
+ };
651
+ }
652
+ function safeConnectorSnapshot(connector) {
653
+ try {
654
+ return connector.snapshot();
655
+ }
656
+ catch {
657
+ return null;
658
+ }
659
+ }
660
+ //# sourceMappingURL=core.js.map