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