tiny-essentials 1.29.1 → 1.29.2

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 (128) hide show
  1. package/changelog/1/29/2.md +82 -0
  2. package/dist/v1/TinyBasicsEs.min.js +1 -1
  3. package/dist/v1/TinyEssentials.min.js +1 -1
  4. package/dist/v1/TinyFetchLoader.min.js +1 -0
  5. package/dist/v1/TinyImageLoader.min.js +1 -0
  6. package/dist/v1/TinyMediaCache.min.js +1 -0
  7. package/dist/v1/TinyMediaLoader.min.js +1 -0
  8. package/dist/v1/basics/index.cjs +5 -0
  9. package/dist/v1/basics/index.d.mts +2 -1
  10. package/dist/v1/basics/index.d.mts.map +1 -1
  11. package/dist/v1/basics/index.mjs +2 -1
  12. package/dist/v1/basics/media/youtube.cjs +275 -0
  13. package/dist/v1/basics/media/youtube.d.mts +107 -0
  14. package/dist/v1/basics/media/youtube.d.mts.map +1 -0
  15. package/dist/v1/basics/media/youtube.mjs +214 -0
  16. package/dist/v1/index.cjs +13 -0
  17. package/dist/v1/index.d.mts +6 -1
  18. package/dist/v1/index.d.mts.map +1 -1
  19. package/dist/v1/index.mjs +7 -2
  20. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.cjs +0 -1
  21. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts +0 -1
  22. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts.map +1 -1
  23. package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.mjs +0 -1
  24. package/dist/v1/libs/fs/plugins/OPFS/index.cjs +0 -8
  25. package/dist/v1/libs/fs/plugins/OPFS/index.d.mts +0 -8
  26. package/dist/v1/libs/fs/plugins/OPFS/index.d.mts.map +1 -1
  27. package/dist/v1/libs/fs/plugins/OPFS/index.mjs +0 -8
  28. package/dist/v1/libs/html/TinyHtml.cjs +1 -1
  29. package/dist/v1/libs/html/TinyHtml.d.mts +2 -2
  30. package/dist/v1/libs/html/TinyHtml.d.mts.map +1 -1
  31. package/dist/v1/libs/html/TinyHtml.mjs +1 -1
  32. package/dist/v1/libs/html/loader/TinyMediaLoader.cjs +1999 -0
  33. package/dist/v1/libs/html/loader/TinyMediaLoader.d.mts +999 -0
  34. package/dist/v1/libs/html/loader/TinyMediaLoader.d.mts.map +1 -0
  35. package/dist/v1/libs/html/loader/TinyMediaLoader.mjs +1852 -0
  36. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.cjs +77 -0
  37. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.d.mts +103 -0
  38. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.d.mts.map +1 -0
  39. package/dist/v1/libs/html/loader/beta/formats/TinyAudioLoader.mjs +67 -0
  40. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.cjs +630 -0
  41. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.d.mts +138 -0
  42. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.d.mts.map +1 -0
  43. package/dist/v1/libs/html/loader/beta/formats/TinyContentLoader.mjs +576 -0
  44. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.cjs +70 -0
  45. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.d.mts +100 -0
  46. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.d.mts.map +1 -0
  47. package/dist/v1/libs/html/loader/beta/formats/TinyVideoLoader.mjs +60 -0
  48. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.cjs +946 -0
  49. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.d.mts +67 -0
  50. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.d.mts.map +1 -0
  51. package/dist/v1/libs/html/loader/beta/probe/TinyMediaProbe.mjs +896 -0
  52. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.cjs +225 -0
  53. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.d.mts +149 -0
  54. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.d.mts.map +1 -0
  55. package/dist/v1/libs/html/loader/formats/TinyFetchLoader.mjs +197 -0
  56. package/dist/v1/libs/html/loader/formats/TinyImageLoader.cjs +1454 -0
  57. package/dist/v1/libs/html/loader/formats/TinyImageLoader.d.mts +200 -0
  58. package/dist/v1/libs/html/loader/formats/TinyImageLoader.d.mts.map +1 -0
  59. package/dist/v1/libs/html/loader/formats/TinyImageLoader.mjs +1317 -0
  60. package/dist/v1/libs/html/loader/probe/TinyImageProbe.cjs +668 -0
  61. package/dist/v1/libs/html/loader/probe/TinyImageProbe.d.mts +83 -0
  62. package/dist/v1/libs/html/loader/probe/TinyImageProbe.d.mts.map +1 -0
  63. package/dist/v1/libs/html/loader/probe/TinyImageProbe.mjs +626 -0
  64. package/dist/v1/libs/html/loader/utils/TinyBufferReader.cjs +277 -0
  65. package/dist/v1/libs/html/loader/utils/TinyBufferReader.d.mts +118 -0
  66. package/dist/v1/libs/html/loader/utils/TinyBufferReader.d.mts.map +1 -0
  67. package/dist/v1/libs/html/loader/utils/TinyBufferReader.mjs +247 -0
  68. package/dist/v1/libs/html/loader/utils/TinyIsoBox.cjs +128 -0
  69. package/dist/v1/libs/html/loader/utils/TinyIsoBox.d.mts +49 -0
  70. package/dist/v1/libs/html/loader/utils/TinyIsoBox.d.mts.map +1 -0
  71. package/dist/v1/libs/html/loader/utils/TinyIsoBox.mjs +116 -0
  72. package/dist/v1/libs/html/loader/utils/TinyMediaCache.cjs +796 -0
  73. package/dist/v1/libs/html/loader/utils/TinyMediaCache.d.mts +354 -0
  74. package/dist/v1/libs/html/loader/utils/TinyMediaCache.d.mts.map +1 -0
  75. package/dist/v1/libs/html/loader/utils/TinyMediaCache.mjs +734 -0
  76. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.cjs +302 -0
  77. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.d.mts +256 -0
  78. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.d.mts.map +1 -0
  79. package/dist/v1/libs/html/loader/utils/TinyMediaMirror.mjs +280 -0
  80. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.cjs +194 -0
  81. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.d.mts +138 -0
  82. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.d.mts.map +1 -0
  83. package/dist/v1/libs/html/loader/utils/TinyMediaProgress.mjs +175 -0
  84. package/dist/v1/libs/tools/TinyPkgExportValidator.cjs +359 -55
  85. package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts +64 -6
  86. package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts.map +1 -1
  87. package/dist/v1/libs/tools/TinyPkgExportValidator.mjs +320 -57
  88. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.cjs +0 -1
  89. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts +0 -1
  90. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts.map +1 -1
  91. package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.mjs +0 -1
  92. package/dist/v1/webTemplates/bootstrap/5.3/scss/background/_background-grid.scss +15 -0
  93. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/buttons/_buttons-contrast.scss +208 -0
  94. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/buttons/_buttons-ghost.scss +89 -0
  95. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/icon/_cube-icon.scss +65 -0
  96. package/dist/v1/webTemplates/bootstrap/5.3/scss/components/icon/_sphere-pulse.scss +39 -0
  97. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_badges.scss +90 -0
  98. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_bootstrap-overrides.scss +495 -0
  99. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_buttons.scss +390 -0
  100. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_colors.scss +213 -0
  101. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_effects.scss +153 -0
  102. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_forms-tones.scss +69 -0
  103. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_forms.scss +323 -0
  104. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_global.scss +31 -0
  105. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_links.scss +47 -0
  106. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_tables.scss +25 -0
  107. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_theme.scss +21 -0
  108. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_tokens.scss +161 -0
  109. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_utilities.scss +81 -0
  110. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/_variables.scss +63 -0
  111. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_form-tone-mixins.scss +236 -0
  112. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_sizing.scss +68 -0
  113. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_tone-mixins.scss +392 -0
  114. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/_typography.scss +112 -0
  115. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/background/_tiny_gradient_1.scss +131 -0
  116. package/dist/v1/webTemplates/bootstrap/5.3/scss/themes/deepstrap/plugins/colors/_pink.scss +47 -0
  117. package/docs/v1/Exports.md +33 -2
  118. package/docs/v1/README.md +9 -0
  119. package/docs/v1/basics/youtube.md +204 -0
  120. package/docs/v1/libs/html/loader/TinyFetchLoader.md +114 -0
  121. package/docs/v1/libs/html/loader/TinyImageLoader.md +141 -0
  122. package/docs/v1/libs/html/loader/TinyMediaCache.md +166 -0
  123. package/docs/v1/libs/html/loader/TinyMediaLoader.md +300 -0
  124. package/docs/v1/libs/tools/TinyPkgExportValidator.md +55 -0
  125. package/docs/v1/webTemplates/bootstrap/5.3/DeepStrap.md +177 -0
  126. package/package.json +244 -8
  127. /package/dist/v1/webTemplates/bootstrap/5.3/scss/input/{Placeholder-Colors-Fix.scss → _placeholder-colors-fix.scss} +0 -0
  128. /package/dist/v1/webTemplates/bootstrap/5.3/scss/spacing/{Spacing-Plus.scss → _spacing-plus.scss} +0 -0
@@ -0,0 +1,630 @@
1
+ 'use strict';
2
+
3
+ var TinyMediaLoader = require('../../TinyMediaLoader.cjs');
4
+ var TinyMediaProbe = require('../probe/TinyMediaProbe.cjs');
5
+
6
+ /**
7
+ * @fileoverview Concrete loader for medias.
8
+ */
9
+
10
+ /**
11
+ * Configuration options for {@link TinyContentLoader}.
12
+ * @template {HTMLVideoElement|HTMLAudioElement} HTMLContentElement
13
+ * @typedef {Object} VideoLoaderOptions
14
+ * @property {string} [src] - The media source URL.
15
+ * @property {number} [timeout] - Maximum time in milliseconds before the load is aborted.
16
+ * @property {string|null} [crossOrigin] - The CORS mode applied to the media.
17
+ * @property {boolean} [autoplay] - Whether the media should autoplay.
18
+ * @property {boolean} [loop] - Whether the media should loop.
19
+ * @property {boolean} [muted] - Whether the media should be muted.
20
+ * @property {boolean} [controls] - Whether the media should display controls (default true).
21
+ * @property {'none'|'metadata'|'auto'} [preload] - The preload strategy.
22
+ * @property {HTMLContentElement|null} [element] - An existing DOM element to adopt.
23
+ * @property {boolean} [progressive] - Whether to render progressively during fetch (default true).
24
+ * @property {boolean} [autoReload] - Whether an external `src` change triggers a reload.
25
+ * @property {boolean} [stream] - Whether to download the media through fetch for byte progress.
26
+ * @property {boolean} [useHeadRequest] - Whether to perform a HEAD request to fetch HTTP headers before downloading.
27
+ * @property {BodyInit} [body] - The request body forwarded to the transport (ignored by `GET`/`HEAD`).
28
+ * @property {import('../../utils/TinyMediaCache.mjs').default|null} [cache] - An optional shared memory cache.
29
+ */
30
+
31
+ /**
32
+ * @typedef {import('../../TinyMediaLoader.mjs').MediaEarlyDecoder} MediaEarlyDecoder
33
+ */
34
+
35
+ /**
36
+ * Maps media format identifiers to an ordered list of MIME types to attempt when probing.
37
+ * @typedef {Record<string, string[]>} TypesToTry
38
+ */
39
+
40
+ /**
41
+ * Extracts a partial metadata snapshot from a native media element.
42
+ * @param {HTMLVideoElement|HTMLAudioElement} media - The media element to probe.
43
+ * @returns {Partial<import('../../TinyMediaLoader.mjs').MediaMetadata>} A patch containing resolved dimensions and duration.
44
+ */
45
+ const getMetadata = (media) => {
46
+ /** @type {Partial<import('../../TinyMediaLoader.mjs').MediaMetadata>} */
47
+ const patch = {};
48
+ if (Number.isFinite(media.duration)) patch.duration = media.duration;
49
+ if (media instanceof HTMLVideoElement) {
50
+ if (Number.isFinite(media.videoWidth)) patch.width = media.videoWidth;
51
+ if (Number.isFinite(media.videoHeight)) patch.height = media.videoHeight;
52
+ }
53
+ return patch;
54
+ };
55
+
56
+ /**
57
+ * Base class for media loaders that support complex content types (audio and video)
58
+ * and optional Media Source Extensions (MSE) streaming.
59
+ * @extends {TinyMediaLoader<HTMLContentElement>}
60
+ * @template {HTMLVideoElement|HTMLAudioElement} HTMLContentElement
61
+ * @template {'video'|'audio'} Tag
62
+ * @beta
63
+ */
64
+ class TinyContentLoader extends TinyMediaLoader {
65
+ /** @type {HTMLContentElement|null} The underlying media element, or `null` before creation. */
66
+ #media;
67
+ /** @type {string|null} The CORS mode applied to the audio, or `null` for none. */
68
+ #crossOrigin;
69
+ /** @type {boolean} Whether the audio should autoplay. */
70
+ #autoplay;
71
+ /** @type {boolean} Whether the audio should loop. */
72
+ #loop;
73
+ /** @type {boolean} Whether the audio should be muted. */
74
+ #muted;
75
+ /** @type {boolean} Whether the audio should display controls. */
76
+ #controls;
77
+ /** @type {'none'|'metadata'|'auto'} The preload strategy. */
78
+ #preload;
79
+ /** @type {boolean} Whether to download the audio through fetch for byte progress. */
80
+ #stream;
81
+ /** @type {string|null} */
82
+ #mseUrl = null;
83
+ /** @type {Tag} */
84
+ #tag;
85
+ /** @type {TypesToTry} */
86
+ #typesToTry = {};
87
+
88
+ /**
89
+ * Initializes a content loader for a specific media tag and set of candidate MIME types.
90
+ * @param {Tag} tag - The HTML tag name (e.g., 'video' or 'audio').
91
+ * @param {TypesToTry} typesToTry - The mapping of format keys to candidate MIME types.
92
+ * @param {VideoLoaderOptions<HTMLContentElement>} [options] - The loader configuration.
93
+ * @throws {TypeError} If `crossOrigin` is not a string or null.
94
+ * @throws {TypeError} If `preload` is not a string.
95
+ */
96
+ constructor(tag, typesToTry, options = {}) {
97
+ super({ useHeadRequest: false, ...options });
98
+ const {
99
+ crossOrigin = null,
100
+ autoplay = false,
101
+ loop = false,
102
+ muted = false,
103
+ controls = true,
104
+ preload = 'metadata',
105
+ stream = false,
106
+ } = options;
107
+ if (crossOrigin !== null && typeof crossOrigin !== 'string') {
108
+ throw new TypeError('The "crossOrigin" option must be a string or null.');
109
+ }
110
+ if (typeof preload !== 'string') {
111
+ throw new TypeError('The "preload" option must be a string.');
112
+ }
113
+ if (typeof controls !== 'boolean') {
114
+ throw new TypeError('The "controls" option must be a boolean.');
115
+ }
116
+ this.#tag = tag;
117
+ this.#crossOrigin = crossOrigin;
118
+ this.#autoplay = Boolean(autoplay);
119
+ this.#loop = Boolean(loop);
120
+ this.#muted = Boolean(muted);
121
+ this.#preload = preload;
122
+ this.#stream = Boolean(stream);
123
+ this.#controls = Boolean(controls);
124
+ this.#media = null;
125
+ this.#typesToTry = typesToTry;
126
+
127
+ this._insertDecoder('video', (mime) => {
128
+ const probe = new TinyMediaProbe(mime);
129
+ return {
130
+ write: (chunk) => {
131
+ const size = probe.push(chunk);
132
+ if (size) {
133
+ this._emitMetadata(size, 'PROBE');
134
+ }
135
+ },
136
+ close: () => probe.close(),
137
+ };
138
+ });
139
+ }
140
+
141
+ /**
142
+ * The underlying media element managed by the loader.
143
+ * @returns {HTMLContentElement|null} The element instance, or null before it is created.
144
+ */
145
+ get media() {
146
+ return this.#media;
147
+ }
148
+
149
+ /**
150
+ * Indicates whether the loader downloads the media through fetch for byte-level progress.
151
+ * @returns {boolean} `true` when streaming is enabled, otherwise `false`.
152
+ */
153
+ get stream() {
154
+ return this.#stream;
155
+ }
156
+
157
+ /**
158
+ * Orchestrates the initial load of the media, wiring up DOM events and initiating
159
+ * either a native load or a streamed fetch.
160
+ * @override
161
+ * @protected
162
+ * @returns {Promise<void>} A promise that resolves once the media is ready and playback can start.
163
+ */
164
+ async _startLoad() {
165
+ const media = /** @type {HTMLContentElement} */ (this._resolveElement());
166
+ this.#media = media;
167
+
168
+ this._forwardEvents(media, [
169
+ 'loadstart',
170
+ 'loadedmetadata',
171
+ 'canplay',
172
+ 'canplaythrough',
173
+ 'play',
174
+ 'playing',
175
+ 'pause',
176
+ 'seeking',
177
+ 'seeked',
178
+ 'waiting',
179
+ 'stalled',
180
+ 'suspend',
181
+ 'emptied',
182
+ 'ended',
183
+ 'durationchange',
184
+ 'ratechange',
185
+ 'volumechange',
186
+ 'timeupdate',
187
+ 'abort',
188
+ ]);
189
+
190
+ const onMetadata = () => {
191
+ const patch = getMetadata(media);
192
+ if (Object.keys(patch).length > 0) this._emitMetadata(patch, 'LOADED_METADATA');
193
+ };
194
+
195
+ this._on(media, 'loadedmetadata', onMetadata);
196
+
197
+ this._on(media, 'durationchange', () => {
198
+ if (Number.isFinite(media.duration)) {
199
+ this._emitMetadata({ duration: media.duration }, 'DURATION_CHANGE');
200
+ }
201
+ });
202
+
203
+ // Time-based progress only makes sense when we are not downloading the file ourselves.
204
+ if (!this.#stream) {
205
+ this._on(media, 'progress', () => this.#updateTimeProgress());
206
+ this._on(media, 'loadedmetadata', () => this.#updateTimeProgress());
207
+ }
208
+
209
+ if (media.readyState >= 1) {
210
+ onMetadata();
211
+ }
212
+
213
+ if (this.#crossOrigin) {
214
+ media.crossOrigin = this.#crossOrigin;
215
+ }
216
+ media.preload = this.#preload;
217
+ media.autoplay = this.#autoplay;
218
+ media.loop = this.#loop;
219
+ media.muted = this.#muted;
220
+ if (media instanceof HTMLVideoElement) media.playsInline = true;
221
+ media.controls = this.#controls;
222
+
223
+ if (this.#stream) {
224
+ const ready = this._waitForMedia(media, 'loadeddata');
225
+ ready.catch(() => {});
226
+
227
+ await this.#streamInto(media, 0);
228
+
229
+ if (media.readyState < 2) {
230
+ await ready;
231
+ }
232
+ } else {
233
+ const ready = this._waitForMedia(media, 'loadeddata');
234
+ media.src = this.src;
235
+ this._flushMutations();
236
+ media.load();
237
+ await ready;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Downloads the media through fetch and pipes the data into a MediaSource (MSE)
243
+ * if available, enabling byte-level progress and seamless seeking.
244
+ * @param {HTMLContentElement} media - The target media element.
245
+ * @param {number} startByte - The byte offset to start the download from.
246
+ * @returns {Promise<void>} A promise that resolves when the stream is established.
247
+ */
248
+ async #streamInto(media, startByte) {
249
+ const cached = this._acquireFromCache();
250
+ if (cached) {
251
+ this.emit('cachehit', cached);
252
+ this._setInternalSrc(cached.objectUrl);
253
+ media.src = cached.objectUrl;
254
+ this._flushMutations();
255
+ media.load();
256
+ return;
257
+ }
258
+
259
+ /** @type {MediaSource|null} */
260
+ let mse = null;
261
+ /** @type {SourceBuffer|null} */
262
+ let sourceBuffer = null;
263
+ /** @type {BlobPart[]} */
264
+ let queue = [];
265
+ let isAppending = false;
266
+ let mseStarted = false;
267
+ let fallbackTriggered = false;
268
+ let appendedBytes = 0;
269
+
270
+ const infinityCheck = () => {
271
+ if (media.duration === Infinity && !fallbackTriggered) {
272
+ triggerFallback();
273
+ }
274
+ };
275
+ media.addEventListener('durationchange', infinityCheck);
276
+ media.addEventListener('loadedmetadata', infinityCheck);
277
+
278
+ // Isolated controller for the stream.
279
+ let fetchController = new AbortController();
280
+ const onLoaderAbort = () => fetchController.abort();
281
+ this.signal.signal.addEventListener('abort', onLoaderAbort);
282
+
283
+ const triggerFallback = () => {
284
+ if (fallbackTriggered) return;
285
+ fallbackTriggered = true;
286
+
287
+ media.removeEventListener('durationchange', infinityCheck);
288
+ media.removeEventListener('loadedmetadata', infinityCheck);
289
+
290
+ // Immediately release the network socket by aborting the fetch!
291
+ // This prevents the browser from freezing the network by queuing duplicate requests.
292
+ fetchController.abort();
293
+
294
+ const isMseUrl = this.#mseUrl && media.src === this.#mseUrl;
295
+
296
+ // Saves the current playback state to restore after fallback
297
+ const currentTime = media.currentTime;
298
+ const wasPlaying = !media.paused && media.readyState >= 2;
299
+
300
+ if (this.#mseUrl) {
301
+ URL.revokeObjectURL(this.#mseUrl);
302
+ this.#mseUrl = null;
303
+ }
304
+
305
+ if (isMseUrl || !media.hasAttribute('src')) {
306
+ this._setInternalSrc(null);
307
+ media.src = this.src;
308
+ this._flushMutations();
309
+
310
+ // Restore time in case of seek fallback
311
+ if (currentTime > 0) {
312
+ const onLoadedMetadata = () => {
313
+ media.currentTime = currentTime;
314
+ if (wasPlaying) media.play().catch(() => {});
315
+ media.removeEventListener('loadedmetadata', onLoadedMetadata);
316
+ };
317
+ media.addEventListener('loadedmetadata', onLoadedMetadata);
318
+ }
319
+ }
320
+ };
321
+
322
+ // Buffer cleanup function (Item 2: Eviction)
323
+ const evictOldBuffer = () => {
324
+ if (!sourceBuffer || sourceBuffer.updating || fallbackTriggered) return;
325
+ const currentTime = media.currentTime;
326
+ if (sourceBuffer.buffered.length > 0) {
327
+ const bufferStart = sourceBuffer.buffered.start(0);
328
+ // If there are more than 30 seconds behind, delete up to 10 seconds before the current moment
329
+ if (currentTime - bufferStart > 30) {
330
+ try {
331
+ sourceBuffer.remove(bufferStart, currentTime - 10);
332
+ } catch (e) {}
333
+ }
334
+ }
335
+ };
336
+
337
+ // ITEM 3: New seek listener outside of the isolated buffer (Avoids duplicate listeners)
338
+ if (!media.hasAttribute('data-seek-bound')) {
339
+ media.setAttribute('data-seek-bound', 'true');
340
+ this._on(media, 'seeking', () => {
341
+ if (fallbackTriggered || !mseStarted || !sourceBuffer) return;
342
+ const time = media.currentTime;
343
+ let isBuffered = false;
344
+ const b = sourceBuffer.buffered;
345
+
346
+ for (let i = 0; i < b.length; i++) {
347
+ // Allows a small margin of error (0.1s)
348
+ if (time >= b.start(i) && time <= b.end(i) + 0.1) {
349
+ isBuffered = true;
350
+ break;
351
+ }
352
+ }
353
+
354
+ if (!isBuffered) {
355
+ // Abort current fetch to jump to the new time
356
+ fetchController.abort();
357
+ if (sourceBuffer.updating) sourceBuffer.abort();
358
+ queue = [];
359
+ isAppending = false;
360
+
361
+ // Heuristic byte-range calculation (Ideally requires Sidx/moov atom demuxing, but solves 90% of CBR cases)
362
+ const totalBytes = this.metadata.size || this._getResourceSize();
363
+ const duration = this.metadata.duration || media.duration;
364
+
365
+ if (totalBytes > 0 && duration > 0) {
366
+ const estimatedByte = Math.floor((time / duration) * totalBytes);
367
+ // Generate new controller for the next request
368
+ fetchController = new AbortController();
369
+ startStream(estimatedByte); // Restarts the stream from the new byte
370
+ } else {
371
+ triggerFallback();
372
+ }
373
+ }
374
+ });
375
+ }
376
+
377
+ const appendNext = () => {
378
+ if (mseStarted && media.error) triggerFallback();
379
+ if (
380
+ isAppending ||
381
+ queue.length === 0 ||
382
+ !sourceBuffer ||
383
+ sourceBuffer.updating ||
384
+ fallbackTriggered
385
+ )
386
+ return;
387
+
388
+ isAppending = true;
389
+ try {
390
+ const blob = /** @type {BufferSource} */ (queue.shift());
391
+ appendedBytes += blob.byteLength;
392
+ sourceBuffer.appendBuffer(blob);
393
+ } catch (e) {
394
+ isAppending = false;
395
+ triggerFallback();
396
+ }
397
+ };
398
+
399
+ /** @param {number} byteOffset */
400
+ const startStream = async (byteOffset) => {
401
+ try {
402
+ const blob = await this._downloadStream(
403
+ this.src,
404
+ (chunk) => {
405
+ if (mseStarted && media.error) triggerFallback();
406
+
407
+ if (this.progressive && typeof MediaSource !== 'undefined' && !fallbackTriggered) {
408
+ if (!mseStarted) {
409
+ mseStarted = true;
410
+ mse = new MediaSource();
411
+ this.#mseUrl = URL.createObjectURL(mse);
412
+ this._setInternalSrc(this.#mseUrl);
413
+ media.src = this.#mseUrl;
414
+ this._flushMutations();
415
+
416
+ mse.addEventListener('sourceopen', () => {
417
+ const mime = this.metadata.type || this.#getMimeType();
418
+ let typesToTry = [mime];
419
+ for (const mimeName in this.#typesToTry) {
420
+ if (mime.includes(mimeName)) {
421
+ for (const type of this.#typesToTry[mimeName]) {
422
+ typesToTry.unshift(type.replace('{mime}', mime));
423
+ }
424
+ }
425
+ }
426
+
427
+ let selectedType = null;
428
+ for (const t of typesToTry) {
429
+ if (MediaSource.isTypeSupported(t)) {
430
+ selectedType = t;
431
+ break;
432
+ }
433
+ }
434
+
435
+ if (selectedType && mse) {
436
+ try {
437
+ sourceBuffer = mse.addSourceBuffer(selectedType);
438
+ sourceBuffer.addEventListener('updateend', () => {
439
+ isAppending = false;
440
+ evictOldBuffer(); // Executes the GC after each append
441
+
442
+ if (
443
+ !fallbackTriggered &&
444
+ sourceBuffer?.buffered.length === 0 &&
445
+ appendedBytes > 256 * 1024
446
+ ) {
447
+ triggerFallback();
448
+ return;
449
+ }
450
+ appendNext();
451
+ });
452
+ sourceBuffer.addEventListener('error', triggerFallback);
453
+ appendNext();
454
+ } catch {
455
+ triggerFallback();
456
+ }
457
+ } else {
458
+ triggerFallback();
459
+ }
460
+
461
+ if (sourceBuffer && byteOffset > 0) sourceBuffer.abort();
462
+ });
463
+ }
464
+
465
+ if (!fallbackTriggered) {
466
+ queue.push(chunk);
467
+ if (mse && mse.readyState === 'open' && sourceBuffer) appendNext();
468
+ }
469
+ }
470
+ },
471
+ byteOffset,
472
+ fetchController.signal,
473
+ );
474
+
475
+ if (mse && !fallbackTriggered) {
476
+ const endStream = () => {
477
+ if (mse?.readyState === 'open' && !media.error) {
478
+ if (sourceBuffer && (sourceBuffer.updating || queue.length > 0)) {
479
+ setTimeout(endStream, 50);
480
+ } else {
481
+ try {
482
+ mse.endOfStream();
483
+ } catch (e) {}
484
+ }
485
+ }
486
+ };
487
+ endStream();
488
+
489
+ if (byteOffset === 0 && blob.size > 0) this._cacheBlob(blob);
490
+ } else if (byteOffset === 0 && blob.size > 0) {
491
+ const url = this._cacheBlob(blob);
492
+ if (!fallbackTriggered || (media.paused && media.currentTime === 0)) {
493
+ this._setInternalSrc(url);
494
+ media.src = url;
495
+ this._flushMutations();
496
+ media.load();
497
+ }
498
+ }
499
+ } catch (err) {
500
+ if (err instanceof Error) {
501
+ if (
502
+ err.name === 'AbortError' &&
503
+ (fallbackTriggered || media.hasAttribute('data-seek-bound'))
504
+ )
505
+ return;
506
+ throw err;
507
+ }
508
+ }
509
+ };
510
+
511
+ try {
512
+ await startStream(startByte);
513
+ } finally {
514
+ this.signal.signal.removeEventListener('abort', onLoaderAbort);
515
+ media.removeEventListener('durationchange', infinityCheck);
516
+ media.removeEventListener('loadedmetadata', infinityCheck);
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Calculates the total buffered duration of the media and emits a progress event
522
+ * based on time instead of bytes.
523
+ * @returns {void} This method does not return a value.
524
+ */
525
+ #updateTimeProgress() {
526
+ const media = this.#media;
527
+ const { duration = 0, height = 0, width = 0 } = media ? getMetadata(media) : {};
528
+ if (!media || !Number.isFinite(duration) || duration === 0) {
529
+ return;
530
+ }
531
+ const buffered = media.buffered;
532
+ let loaded = 0;
533
+ for (let i = 0; i < buffered.length; i += 1) {
534
+ loaded += buffered.end(i) - buffered.start(i);
535
+ }
536
+ const tracker = this.progress;
537
+ this.emit('progress', {
538
+ loaded,
539
+ total: duration,
540
+ height,
541
+ width,
542
+ remaining: Math.max(0, duration - loaded),
543
+ percent: Math.min(100, (loaded / duration) * 100),
544
+ rate: 0,
545
+ elapsed: tracker ? tracker.elapsed : 0,
546
+ eta: 0,
547
+ chunks: buffered.length,
548
+ });
549
+ }
550
+
551
+ /**
552
+ * Derives a generic MIME type (e.g., 'video/mp4') based on the file extension
553
+ * of the current source URL.
554
+ * @returns {string} The derived MIME type, or a generic fallback if no extension is found.
555
+ */
556
+ #getMimeType() {
557
+ const clean = this.src.split('?')[0].split('#')[0];
558
+ const dot = clean.lastIndexOf('.');
559
+ const extension = dot === -1 ? '*' : clean.slice(dot + 1).toLowerCase();
560
+ return `${this.#tag}/${extension}`;
561
+ }
562
+
563
+ /**
564
+ * Extracts the metadata specific to content media (video/audio), such as
565
+ * dimensions and duration, to be merged into the final metadata object.
566
+ * @override
567
+ * @protected
568
+ * @returns {import('../../TinyMediaLoader.mjs').MediaMetadataDetails} The resolved metadata for the media.
569
+ */
570
+ _getMetadataDetails() {
571
+ const media = this.#media;
572
+ const patch = media ? getMetadata(media) : {};
573
+ const { duration = 0, width = 0, height = 0 } = patch;
574
+ return {
575
+ type: this.#getMimeType(),
576
+ width,
577
+ height,
578
+ duration: Number.isFinite(duration) ? duration : 0,
579
+ size: this._getResourceSize(),
580
+ };
581
+ }
582
+
583
+ /**
584
+ * Creates the appropriate HTML media element based on the configured tag.
585
+ * @override
586
+ * @protected
587
+ * @returns {HTMLContentElement} A newly created HTMLAudioElement or HTMLVideoElement.
588
+ */
589
+ _createElement() {
590
+ // @ts-ignore
591
+ return document.createElement(this.#tag);
592
+ }
593
+
594
+ /**
595
+ * Aborts the media load and cleans up any active MSE object URLs to release memory.
596
+ * @override
597
+ * @protected
598
+ * @returns {void}
599
+ */
600
+ _abort() {
601
+ if (this.#mseUrl) {
602
+ URL.revokeObjectURL(this.#mseUrl);
603
+ this.#mseUrl = null;
604
+ }
605
+ super._abort();
606
+ }
607
+
608
+ /**
609
+ * Releases MSE resources, revokes object URLs, and resets the media element
610
+ * to prevent memory leaks.
611
+ * @override
612
+ * @protected
613
+ * @returns {void} This method does not return a value.
614
+ */
615
+ _cleanup() {
616
+ if (this.#mseUrl) {
617
+ URL.revokeObjectURL(this.#mseUrl);
618
+ this.#mseUrl = null;
619
+ }
620
+ this._revokeObjectUrl();
621
+ if (this.#media) {
622
+ this.#media.pause();
623
+ this.#media.removeAttribute('src');
624
+ this.#media.load();
625
+ this._flushMutations();
626
+ }
627
+ }
628
+ }
629
+
630
+ module.exports = TinyContentLoader;