UNPKG

@mediamonks/fast-image-sequence

Version:

The fast-image-sequence-renderer is a powerful package for displaying image sequences at high frame rates on websites. Use it to create smooth animations, 360° product views, or video-like sequences from series of images. Zero dependencies.

689 lines (688 loc) 37 kB
//#region src/lib/Frame.ts var e = class { index; images = []; priority = 0; treePriority = 0; constructor(e) { this.index = e; } get image() { return this.images.find((e) => e.image !== void 0)?.image; } async getImage() { if (this.image !== void 0) return this.image; let e = this.images[this.images.length - 1]; if (!e) throw Error("Frame has no image sources"); return e.fetchImage(); } async fetchImage() { return this.images.find((e) => e.available)?.fetchImage(); } releaseImage() { this.images.forEach((e) => e.releaseImage()); } reset() { this.images.forEach((e) => e.reset()); } }; //#endregion //#region src/lib/LogToScreen.ts function t() { let e = document.createElement("pre"); return Object.assign(e.style, { position: "absolute", top: "0", left: "0", backgroundColor: "rgba(0, 0, 0, 0.5)", color: "white", padding: "8px", fontSize: "12px", zIndex: "1000", lineHeight: "20px", margin: 0, width: "calc(100% - 16px)" }), e; } function n(e, t) { e.textContent = `${t}`; } //#endregion //#region src/lib/ImageElement.ts function r(e) { (e instanceof ImageBitmap || typeof VideoFrame < "u" && e instanceof VideoFrame) && e.close(); } var i = class { available = !0; loading = !1; frame; _image; context; constructor(e, t) { this.context = e, this.frame = t; } get image() { if (this._image !== void 0 && !this.loading) return this._image; } set image(e) { e !== this._image && (this.releaseImage(), this._image = e); } get imageURL() { return this.context.getImageURL(this.frame.index); } reset() { this.releaseImage(), this._image = void 0; } async fetchImage() { return this.context.fetchImage(this); } releaseImage() { this._image &&= (r(this._image), void 0), this.loading = !1; } }, a = class e { static defaultOptions = { tarURL: void 0, imageURL: void 0, videoURL: void 0, useWorker: !S(), maxCachedImages: 32, maxConnectionLimit: 4, hierarchicalCacheFraction: .3, available: void 0, image: void 0, timeout: -1 }; options; index = 0; initialized = !1; context; images = []; downloadProgress = 0; constructor(t, n, r) { this.context = t, this.index = n, this.options = { ...e.defaultOptions, ...r }, this.initFrames(); } initFrames() { this.context.frames.forEach((e) => e.images[this.index] ||= new i(this, e)), this.images = this.context.frames.map((e) => e.images[this.index]); } get type() { return 2; } get maxCachedImages() { let e = this.initialized ? this.images.filter((e) => e.available).length : this.context.options.frames; return C(Math.floor(this.options.maxCachedImages), 1, e); } setMaxCachedImages(e, t) { return this.options.maxCachedImages = e, this.context.onLoadProgress(t); } getImageURL(e) {} checkImageAvailability() { for (let e of this.images) e.available = this.available(e, e.available); if (!this.images[0]?.available) throw Error(`No image available for index 0 in ImageSource${this.index} (${this.images[0]?.imageURL})`); } async loadResources() { this.checkImageAvailability(), this.initialized = !0; } process(e) { for (e(C(this.maxCachedImages * this.options.hierarchicalCacheFraction | 0, 0, this.maxCachedImages - 1)); this.releaseImageWithLowestPriority();); let { numLoading: t, numLoaded: n } = this.getLoadStatus(), i = this.options.maxConnectionLimit, a = this.images.filter((e) => e.available && e.image === void 0 && !e.loading).sort((e, t) => e.frame.priority - t.frame.priority), o = this.images.filter((e) => e.available && e.image !== void 0 || e.loading).sort((e, t) => t.frame.priority - e.frame.priority).shift()?.frame.priority ?? 1e10; for (; t < i && a.length > 0;) { let e = a.shift(); (n < this.maxCachedImages - t || e.frame.priority < o - .1) && !e.loading && (e.loading = !0, t++, this.fetchImage(e).then((t) => { e.loading ? (e.image = t, e.loading = !1) : r(t); }).catch((t) => { e.reset(), console.error(t); })); } } getLoadStatus() { let e = 0, t = 0; for (let n of this.images) n.loading && e++, n.image !== void 0 && t++; let n = this.maxCachedImages, r = Math.max(0, t - e) / Math.max(1, n); return this.downloadProgress < 1 && (r = this.downloadProgress / 2 + r / 2), { progress: r, numLoading: e, numLoaded: t, maxLoaded: n }; } async fetchImage(e) { return this.options.image ? this.options.image(e.frame.index) : new Promise((e, t) => { t("Not implemented"); }); } destruct() { this.images.forEach((e) => e.reset()); } available(e, t = !0) { return this.options.available ? t && this.options.available(e.frame.index) : t; } releaseImageWithLowestPriority() { let e = 0, t; for (let n of this.images) n.image === void 0 || n.loading || (e++, (!t || n.frame.priority > t.frame.priority) && (t = n)); return e > this.maxCachedImages && t ? (t.releaseImage(), !0) : !1; } }; //#endregion //#region src/lib/DownloadFile.ts function o(e, t) { return new Promise((n, r) => { let i = new XMLHttpRequest(); i.open("GET", e, !0), i.responseType = "arraybuffer", i.onprogress = function(e) { e.lengthComputable && t && t(e.loaded / e.total); }, i.onload = function() { i.status === 200 ? (t && t(1), n(i.response)) : r(/* @__PURE__ */ Error(`Error ${i.status}: ${i.statusText}`)); }, i.onerror = function() { r(/* @__PURE__ */ Error("Request failed")); }, i.send(); }); } function s(e, t) { return new Promise((n, r) => { e.onerror = (e) => r(e), e.decoding = "async", e.src = t, e.decode().then(() => n(e)).catch(r); }); } function c(e) { let t = new Blob([e], { type: "application/javascript" }); return new Worker(URL.createObjectURL(t)); } //#endregion //#region src/lib/Tarball.worker.js?raw var l = "let buffer;\n\nself.onmessage = async (e) => {\n if (e.data.cmd === 'init') {\n buffer = e.data.buffer;\n } else if (e.data.cmd === 'load') {\n await loadImage(e.data.offset, e.data.size, e.data.index);\n }\n};\n\nasync function loadImage(offset, size, index) {\n try {\n const view = new Uint8Array(buffer, offset, size);\n const blob = new Blob([view], {});\n const imageBitmap = await createImageBitmap(blob);\n postMessage({msg: 'done', imageBitmap, index}, [imageBitmap]);\n } catch (e) {\n postMessage({msg: 'error', index, message: String(e && e.message || e)});\n }\n}\n", u = class { fileInfo = []; buffer; options; worker; pending = /* @__PURE__ */ new Map(); defaultOptions = { useWorker: !0 }; constructor(e, t = {}) { this.buffer = e, this.options = { ...this.defaultOptions, ...t }; let n = 0; for (; n < e.byteLength - 512;) { let e = this.readFileName(n); if (e.length == 0) break; let t = this.readFileSize(n); this.fileInfo.push({ name: e, size: t, header_offset: n }), n += 512 + 512 * Math.trunc(t / 512), t % 512 && (n += 512); } } getInfo(e) { return this.fileInfo.find((t) => t.name === e || t.name.endsWith("/" + e)); } getImage(e, t) { return new Promise((n, r) => { let i = this.getInfo(e); if (!i) { r(/* @__PURE__ */ Error(`Tarball: file not found "${e}"`)); return; } if (this.pending.has(t)) { r(/* @__PURE__ */ Error("Image already loading from tar")); return; } if (this.options.useWorker) this.worker ||= this.createWorker(), this.pending.set(t, { resolve: n, reject: r }), this.worker.postMessage({ cmd: "load", offset: i.header_offset + 512, size: i.size, index: t }); else if (this.buffer) { let e = new Uint8Array(this.buffer, i.header_offset + 512, i.size); createImageBitmap(new Blob([e], { type: "image" })).then(n).catch(r); } else r(/* @__PURE__ */ Error("Tarball: buffer already released")); }); } destruct() { this.worker && this.worker.terminate(), this.worker = void 0, this.pending.clear(), this.buffer = void 0; } readFileName(e) { let t = new Uint8Array(this.buffer, e, 100), n = t.indexOf(0); return new TextDecoder().decode(t.slice(0, n)); } readFileSize(e) { let t = new Uint8Array(this.buffer, e + 124, 12), n = ""; for (let e = 0; e < 11; e++) n += String.fromCharCode(t[e]); return parseInt(n, 8); } createWorker() { let e = c(l); return e.addEventListener("message", (e) => { let t = this.pending.get(e.data.index); if (this.pending.delete(e.data.index), e.data.msg === "error") { t && t.reject(Error(e.data.message)); return; } t ? t.resolve(e.data.imageBitmap) : r(e.data.imageBitmap); }), e.postMessage({ cmd: "init", buffer: this.buffer }, [this.buffer]), e; } }, d = class extends a { tarball; get type() { return 1; } async loadResources() { if (this.options.tarURL !== void 0) { let e = await o(this.options.tarURL, (e) => this.downloadProgress = e); this.tarball = new u(e, { useWorker: this.options.useWorker }), this.context.log("Tarball", this.tarball); } return super.loadResources(); } getImageURL(e) { return this.options.imageURL ? this.options.imageURL(e) : void 0; } async fetchImage(e) { if (!e.available) throw Error(`Tarball image not available: ${e.imageURL}`); return await this.tarball.getImage(e.imageURL || "", e.frame.index); } destruct() { super.destruct(), this.tarball?.destruct(), this.tarball = void 0; } available(e, t = !0) { return t = t && e.imageURL !== void 0 && this.tarball?.getInfo(e.imageURL) !== void 0, super.available(e, t); } }, f = "self.onmessage = async (e) => {\n if (e.data.cmd === 'load') {\n await loadImage(e.data.url, e.data.index);\n }\n};\n\nasync function loadImage(url, index) {\n try {\n const response = await fetch(url);\n if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);\n const imageBitmap = await createImageBitmap(await response.blob());\n postMessage({msg: 'done', imageBitmap, index}, [imageBitmap]);\n } catch (e) {\n postMessage({msg: 'error', index, message: String(e && e.message || e)});\n }\n}\n", p = class { index = -1; worker; resolve; reject; constructor() { this.worker = c(f), this.worker.addEventListener("message", (e) => { if (e.data.index !== this.index) { e.data.imageBitmap && r(e.data.imageBitmap); return; } e.data.msg === "error" ? this.reject?.(Error(e.data.message)) : this.resolve ? this.resolve(e.data.imageBitmap) : r(e.data.imageBitmap); }); } load(e, t) { return this.index = e, new Promise((n, r) => { this.resolve = n, this.reject = r, this.worker.postMessage({ cmd: "load", url: t, index: e }); }); } abort() { this.index = -1, this.resolve = void 0, this.reject = void 0; } }, m = []; function h() { return m.length === 0 && m.push(new p()), m.shift(); } function g(e) { e.abort(), m.push(e); } //#endregion //#region src/lib/ImageSourceFetch.ts var _ = class extends a { get type() { return 0; } getImageURL(e) { return this.options.imageURL ? new URL(this.options.imageURL(e), window.location.href).href : void 0; } async fetchImage(e) { return new Promise((t, n) => { if (e.imageURL) if (this.options.useWorker) { let r = h(); r.load(this.index, e.imageURL).then((e) => { t(e), g(r); }).catch((e) => n(e)); } else { let r = new Image(); s(r, e.imageURL).then(() => { t(r); }).catch((e) => n(e)); } else n("Image url not set or image already loading"); }); } }, v = "// MP4 demuxer + WebCodecs VideoDecoder. Plain JS, loaded as worker source by VideoDecode.ts.\n\nconst emit = (msg, transfer) => self.postMessage(msg, transfer || []);\n\nlet buffer;\nlet view;\nlet samples = []; // display order; each entry: {offset, size, dts, isKey, decodeIndex, displayIndex}\nlet decodeOrder = []; // same entries, indexed by decodeIndex\nlet codec;\nlet description;\nlet codedWidth;\nlet codedHeight;\n\nlet decoder;\nlet pending = []; // [{requestId, displayIndex}]\n\nfunction readBox(offset) {\n if (offset + 8 > view.byteLength) return null;\n let size = view.getUint32(offset);\n const type = String.fromCharCode(view.getUint8(offset + 4), view.getUint8(offset + 5), view.getUint8(offset + 6), view.getUint8(offset + 7));\n let headerSize = 8;\n if (size === 1) {\n size = view.getUint32(offset + 8) * 0x100000000 + view.getUint32(offset + 12);\n headerSize = 16;\n } else if (size === 0) {\n size = view.byteLength - offset;\n }\n return {type, size, headerSize, offset, payloadOffset: offset + headerSize};\n}\n\nfunction walk(parent, callback) {\n const end = parent.offset + parent.size;\n let cursor = parent.payloadOffset;\n while (cursor < end) {\n const box = readBox(cursor);\n if (!box) break;\n if (callback(box) === false) return;\n cursor += box.size;\n }\n}\n\nfunction findChild(parent, type) {\n let result;\n walk(parent, box => {\n if (box.type === type) {\n result = box;\n return false;\n }\n });\n return result;\n}\n\nfunction findPath(parent, path) {\n let cur = parent;\n for (const seg of path) {\n cur = findChild(cur, seg);\n if (!cur) return undefined;\n }\n return cur;\n}\n\nfunction findVideoTrak(moov) {\n let result;\n walk(moov, box => {\n if (box.type !== 'trak') return;\n const hdlr = findPath(box, ['mdia', 'hdlr']);\n if (!hdlr) return;\n const handlerType = String.fromCharCode(view.getUint8(hdlr.payloadOffset + 8), view.getUint8(hdlr.payloadOffset + 9), view.getUint8(hdlr.payloadOffset + 10), view.getUint8(hdlr.payloadOffset + 11));\n if (handlerType === 'vide') {\n result = box;\n return false;\n }\n });\n return result;\n}\n\nfunction readSampleEntry(stbl) {\n const stsd = findChild(stbl, 'stsd');\n if (!stsd) throw new Error('stsd box not found');\n const entry = readBox(stsd.payloadOffset + 8);\n if (!entry) throw new Error('No sample entry in stsd');\n\n codedWidth = view.getUint16(entry.payloadOffset + 24);\n codedHeight = view.getUint16(entry.payloadOffset + 26);\n\n if (entry.type === 'avc1' || entry.type === 'avc3') {\n const config = readBox(entry.payloadOffset + 78);\n if (!config || config.type !== 'avcC') throw new Error('avcC box not found');\n const descBytes = new Uint8Array(buffer, config.payloadOffset, config.size - config.headerSize);\n description = descBytes.slice();\n const profile = view.getUint8(config.payloadOffset + 1);\n const compat = view.getUint8(config.payloadOffset + 2);\n const level = view.getUint8(config.payloadOffset + 3);\n codec = `avc1.${toHex2(profile)}${toHex2(compat)}${toHex2(level)}`;\n } else {\n throw new Error(`Unsupported video codec: ${entry.type}. Only H.264 (avc1/avc3) is supported.`);\n }\n}\n\nfunction toHex2(n) {\n return n.toString(16).padStart(2, '0').toUpperCase();\n}\n\nfunction readSampleTable(stbl) {\n const stsz = findChild(stbl, 'stsz');\n const stsc = findChild(stbl, 'stsc');\n const stco = findChild(stbl, 'stco') || findChild(stbl, 'co64');\n const stts = findChild(stbl, 'stts');\n const stss = findChild(stbl, 'stss');\n const ctts = findChild(stbl, 'ctts');\n if (!stsz || !stsc || !stco || !stts) throw new Error('Required stbl child boxes missing');\n\n const sizes = readStsz(stsz);\n const sampleCount = sizes.length;\n const offsets = readOffsets(stsc, stco, sampleCount, sizes);\n const dtsList = readStts(stts, sampleCount);\n const ctsOffsets = ctts ? readCtts(ctts, sampleCount) : null;\n const keySet = stss ? readStss(stss) : null;\n\n const decode = new Array(sampleCount);\n for (let i = 0; i < sampleCount; i++) {\n decode[i] = {\n offset: offsets[i],\n size: sizes[i],\n dts: dtsList[i],\n cts: ctsOffsets ? dtsList[i] + ctsOffsets[i] : dtsList[i],\n isKey: keySet ? keySet.has(i + 1) : true,\n decodeIndex: i,\n };\n }\n\n // Display order = sorted by CTS (stable). decodeOrder[k] = index into `samples` for k-th decode-order sample.\n const displayOrder = [...decode].sort((a, b) => a.cts - b.cts || a.decodeIndex - b.decodeIndex);\n samples = displayOrder.map((s, displayIndex) => ({\n offset: s.offset,\n size: s.size,\n dts: s.dts,\n isKey: s.isKey,\n decodeIndex: s.decodeIndex,\n displayIndex,\n }));\n\n decodeOrder = new Array(sampleCount);\n for (const s of samples) decodeOrder[s.decodeIndex] = s;\n}\n\nfunction readStsz(box) {\n const sampleSize = view.getUint32(box.payloadOffset + 4);\n const count = view.getUint32(box.payloadOffset + 8);\n const sizes = new Array(count);\n if (sampleSize !== 0) {\n for (let i = 0; i < count; i++) sizes[i] = sampleSize;\n } else {\n for (let i = 0; i < count; i++) sizes[i] = view.getUint32(box.payloadOffset + 12 + i * 4);\n }\n return sizes;\n}\n\nfunction readOffsets(stsc, stco, sampleCount, sizes) {\n const chunkCount = view.getUint32(stco.payloadOffset + 4);\n const is64 = stco.type === 'co64';\n const chunkOffsets = new Array(chunkCount);\n for (let i = 0; i < chunkCount; i++) {\n const base = stco.payloadOffset + 8 + i * (is64 ? 8 : 4);\n chunkOffsets[i] = is64 ? view.getUint32(base) * 0x100000000 + view.getUint32(base + 4) : view.getUint32(base);\n }\n\n const entryCount = view.getUint32(stsc.payloadOffset + 4);\n const entries = new Array(entryCount);\n for (let i = 0; i < entryCount; i++) {\n const base = stsc.payloadOffset + 8 + i * 12;\n entries[i] = {firstChunk: view.getUint32(base), samplesPerChunk: view.getUint32(base + 4)};\n }\n\n const offsets = new Array(sampleCount);\n let sampleCursor = 0;\n for (let e = 0; e < entries.length && sampleCursor < sampleCount; e++) {\n const startChunk = entries[e].firstChunk - 1;\n const endChunk = e + 1 < entries.length ? entries[e + 1].firstChunk - 1 : chunkCount;\n const samplesPerChunk = entries[e].samplesPerChunk;\n for (let c = startChunk; c < endChunk && sampleCursor < sampleCount; c++) {\n let off = chunkOffsets[c];\n for (let s = 0; s < samplesPerChunk && sampleCursor < sampleCount; s++) {\n offsets[sampleCursor] = off;\n off += sizes[sampleCursor];\n sampleCursor++;\n }\n }\n }\n return offsets;\n}\n\nfunction readStts(box, sampleCount) {\n const entryCount = view.getUint32(box.payloadOffset + 4);\n const dts = new Array(sampleCount);\n let sampleCursor = 0;\n let t = 0;\n for (let i = 0; i < entryCount && sampleCursor < sampleCount; i++) {\n const count = view.getUint32(box.payloadOffset + 8 + i * 8);\n const delta = view.getUint32(box.payloadOffset + 12 + i * 8);\n for (let s = 0; s < count && sampleCursor < sampleCount; s++) {\n dts[sampleCursor++] = t;\n t += delta;\n }\n }\n return dts;\n}\n\nfunction readCtts(box, sampleCount) {\n const entryCount = view.getUint32(box.payloadOffset + 4);\n const offsets = new Array(sampleCount);\n let sampleCursor = 0;\n for (let i = 0; i < entryCount && sampleCursor < sampleCount; i++) {\n const count = view.getUint32(box.payloadOffset + 8 + i * 8);\n const offset = view.getInt32(box.payloadOffset + 12 + i * 8);\n for (let s = 0; s < count && sampleCursor < sampleCount; s++) offsets[sampleCursor++] = offset;\n }\n return offsets;\n}\n\nfunction readStss(box) {\n const count = view.getUint32(box.payloadOffset + 4);\n const set = new Set();\n for (let i = 0; i < count; i++) set.add(view.getUint32(box.payloadOffset + 8 + i * 4));\n return set;\n}\n\nfunction findKeyDecodeIndexAtOrBefore(decodeIndex) {\n for (let i = decodeIndex; i >= 0; i--) if (decodeOrder[i].isKey) return i;\n return 0;\n}\n\nfunction configureDecoder() {\n decoder = new VideoDecoder({\n output: onDecodedFrame,\n error: (e) => emit({cmd: 'error', message: String(e && e.message || e)}),\n });\n decoder.configure({codec, description, codedWidth, codedHeight, optimizeForLatency: true});\n}\n\nfunction onDecodedFrame(frame) {\n // We pass decodeIndex as the chunk timestamp; the decoder echoes it on the output frame.\n const decIndex = Math.round(Number(frame.timestamp));\n const displayIndex = decodeOrder[decIndex]?.displayIndex ?? -1;\n\n const matchIdx = pending.findIndex(p => p.displayIndex === displayIndex);\n if (matchIdx === -1) {\n frame.close();\n return;\n }\n const req = pending.splice(matchIdx, 1)[0];\n emit({cmd: 'frame', requestId: req.requestId, frame}, [frame]);\n}\n\nfunction handleInit(msg) {\n buffer = msg.buffer;\n view = new DataView(buffer);\n\n let moov;\n let cursor = 0;\n while (cursor < view.byteLength) {\n const box = readBox(cursor);\n if (!box) break;\n if (box.type === 'moov') {\n moov = box;\n break;\n }\n cursor += box.size;\n }\n if (!moov) throw new Error('moov box not found');\n\n const trak = findVideoTrak(moov);\n if (!trak) throw new Error('Video track not found');\n const stbl = findPath(trak, ['mdia', 'minf', 'stbl']);\n if (!stbl) throw new Error('stbl box not found');\n\n readSampleEntry(stbl);\n readSampleTable(stbl);\n\n configureDecoder();\n emit({cmd: 'ready', frames: samples.length, width: codedWidth, height: codedHeight});\n}\n\nasync function handleDecode(msg) {\n const displayIndex = msg.index;\n if (displayIndex < 0 || displayIndex >= samples.length) {\n emit({cmd: 'error', requestId: msg.requestId, message: `Frame index ${displayIndex} out of range`});\n return;\n }\n const targetDecodeIndex = samples[displayIndex].decodeIndex;\n const keyDecodeIndex = findKeyDecodeIndexAtOrBefore(targetDecodeIndex);\n\n // WebCodecs requires a keyframe after every flush(), so each request decodes its own\n // keyframe → target run from scratch. Reference state is not shared between requests.\n decoder.reset();\n decoder.configure({codec, description, codedWidth, codedHeight, optimizeForLatency: true});\n pending = [{requestId: msg.requestId, displayIndex}];\n for (let i = keyDecodeIndex; i <= targetDecodeIndex; i++) submitChunk(i);\n await decoder.flush();\n}\n\nfunction submitChunk(decodeIndex) {\n const s = decodeOrder[decodeIndex];\n const data = new Uint8Array(buffer, s.offset, s.size);\n const chunk = new EncodedVideoChunk({\n type: s.isKey ? 'key' : 'delta',\n timestamp: decodeIndex,\n data,\n });\n decoder.decode(chunk);\n}\n\nfunction handleDestroy() {\n if (decoder && decoder.state !== 'closed') decoder.close();\n decoder = undefined;\n pending = [];\n samples = [];\n decodeOrder = [];\n buffer = undefined;\n view = undefined;\n}\n\nself.addEventListener('message', async (e) => {\n try {\n const msg = e.data;\n if (msg.cmd === 'init') handleInit(msg);\n else if (msg.cmd === 'decode') await handleDecode(msg);\n else if (msg.cmd === 'destroy') handleDestroy();\n } catch (err) {\n emit({cmd: 'error', message: String(err && err.message || err)});\n }\n});\n", y = class { info; ready; worker; nextRequestId = 0; pending = /* @__PURE__ */ new Map(); readyResolve; readyReject; destructed = !1; constructor(e) { if (typeof VideoDecoder > "u") throw Error("WebCodecs VideoDecoder is not available in this browser"); this.worker = c(v), this.ready = new Promise((e, t) => { this.readyResolve = e, this.readyReject = t; }), this.worker.addEventListener("message", (e) => this.onMessage(e.data)), this.worker.addEventListener("error", (e) => { let t = e.message || "Worker error"; this.info ? console.error("VideoDecode worker error:", t) : this.readyReject(Error(t)); }), this.worker.postMessage({ cmd: "init", buffer: e }, [e]); } getFrame(e) { if (this.destructed) return Promise.reject(/* @__PURE__ */ Error("VideoDecode destructed")); let t = this.nextRequestId++; return new Promise((n, r) => { this.pending.set(t, { resolve: n, reject: r }), this.worker.postMessage({ cmd: "decode", requestId: t, index: e }); }); } destruct() { if (!this.destructed) { this.destructed = !0; for (let { reject: e } of this.pending.values()) e(/* @__PURE__ */ Error("VideoDecode destructed")); this.pending.clear(), this.worker.postMessage({ cmd: "destroy" }), this.worker.terminate(); } } onMessage(e) { if (e.cmd === "ready") this.info = { frames: e.frames, width: e.width, height: e.height }, this.readyResolve(this.info); else if (e.cmd === "frame") { let t = this.pending.get(e.requestId); t ? (this.pending.delete(e.requestId), t.resolve(e.frame)) : e.frame.close(); } else if (e.cmd === "error") if (e.requestId !== void 0 && this.pending.has(e.requestId)) this.pending.get(e.requestId).reject(Error(e.message)), this.pending.delete(e.requestId); else if (!this.info) this.readyReject(Error(e.message)); else { console.error("VideoDecode error:", e.message); for (let { reject: t } of this.pending.values()) t(Error(e.message)); this.pending.clear(); } } }, b = class extends a { decoder; constructor(e, t, n) { super(e, t, { ...n, maxConnectionLimit: 1, useWorker: !0 }); } get type() { return 3; } async loadResources() { if (this.options.videoURL !== void 0) { let e = await o(this.options.videoURL, (e) => this.downloadProgress = e); this.decoder = new y(e); let t = await this.decoder.ready; this.context.options.frames !== t.frames && this.context.log(`ImageSourceVideo: options.frames (${this.context.options.frames}) does not match video frame count (${t.frames})`); } return super.loadResources(); } async fetchImage(e) { if (!this.decoder) throw Error("VideoDecode not initialized"); return this.decoder.getFrame(e.frame.index); } destruct() { super.destruct(), this.decoder?.destruct(), this.decoder = void 0; } available(e, t = !0) { let n = this.decoder?.info; return t = t && n !== void 0 && e.frame.index < n.frames, super.available(e, t); } }, x = { 0: "image:", 1: "tar: ", 2: "code: ", 3: "video:" }; function S() { return typeof navigator < "u" && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } function C(e, t, n) { return Math.min(Math.max(e, t), n); } var w = class i { static defaultOptions = { frames: 1, src: [], loop: !1, poster: void 0, fillStyle: "#00000000", objectFit: "cover", clearCanvas: !1, showDebugInfo: !1, name: "FastImageSequence", horizontalAlign: .5, verticalAlign: .5, scale: 1 }; canvas; options; width = 0; height = 0; frame = 0; log; frames = []; sources = []; context; tickFunctions = []; startTime = -1; animationRequestId = 0; container; resizeObserver; mutationObserver; inViewportObserver; forceRedraw = !0; speed = 0; prevFrame = 0; direction = 1; lastFrameDrawn = -1; destructed = !1; logElement; initialized = !1; posterImage; timeFrameVisible = 0; lastImageDrawn; inViewport = !1; containerWidth = 0; containerHeight = 0; constructor(n, r) { if (this.options = { ...i.defaultOptions, ...r }, this.options.frames <= 0) throw Error("FastImageSequence: frames must be greater than 0"); this.container = n, this.canvas = document.createElement("canvas"), this.context = this.canvas.getContext("2d"), this.context.fillStyle = this.options.fillStyle, this.context.imageSmoothingQuality = "high", this.context.clearRect(0, 0, this.canvas.width, this.canvas.height), Object.assign(this.canvas.style, { width: "100%", height: "100%", margin: "0", display: "block" }), this.container.appendChild(this.canvas), this.resizeObserver = new ResizeObserver(() => { this.forceRedraw = !0, this.containerWidth = n.offsetWidth, this.containerHeight = n.offsetHeight, this.lastFrameDrawn < 0 && this.posterImage && this.drawImage(this.posterImage); }), this.resizeObserver.observe(this.canvas), this.mutationObserver = new MutationObserver(() => { this.container.isConnected || (console.error("FastImageSequence: container is not connected to the DOM, fast image sequence will be destroyed"), this.destruct()); }), this.mutationObserver.observe(n, { childList: !0 }), this.inViewportObserver = new IntersectionObserver((e) => { for (let t of e) this.inViewport = t.isIntersecting; }), this.inViewportObserver.observe(this.canvas), this.frames = Array.from({ length: this.options.frames }, (t, n) => new e(n)), this.setTreePriority(), this.log = this.options.showDebugInfo ? console.log : () => {}; let o = this.options.src instanceof Array ? this.options.src : [this.options.src]; this.sources = o.map((e, t) => e.videoURL === void 0 ? e.tarURL === void 0 ? e.imageURL === void 0 ? new a(this, t, e) : new _(this, t, e) : new d(this, t, e) : new b(this, t, e)), this.loadResources().then(() => { this.initialized = !0, this.log("Frames", this.frames), this.log("Options", this.options), this.options.showDebugInfo && (this.logElement = t(), this.container.appendChild(this.logElement), this.tick(() => this.logDebugStatus(this.logElement))), this.drawingLoop(-1); }); } get playing() { return this.speed !== 0; } get paused() { return !this.playing; } get loadProgress() { return this.sources.reduce((e, t) => e + t.getLoadStatus().progress, 0) / this.sources.length; } get progress() { return this.index / (this.options.frames - 1); } set progress(e) { this.frame = (this.options.frames - 1) * e; } set scale(e) { this.forceRedraw = this.options.scale !== e, this.options.scale = e; } get scale() { return this.options.scale; } set horizontalAlign(e) { this.forceRedraw = this.options.horizontalAlign !== e, this.options.horizontalAlign = e; } get horizontalAlign() { return this.options.horizontalAlign; } set verticalAlign(e) { this.forceRedraw = this.options.verticalAlign !== e, this.options.verticalAlign = e; } get verticalAlign() { return this.options.verticalAlign; } get src() { return this.sources[0]; } set frameCount(t) { for (let e of this.frames) e.reset(); this.forceRedraw = !0; let n = Math.max(1, t | 0); this.options.frames = n, n < this.frames.length ? this.frames = Array.from({ length: n }, (t, n) => new e(n)) : n > this.frames.length && (this.frames = this.frames.concat(Array.from({ length: n - this.frames.length }, (t, n) => new e(n + this.frames.length)))); for (let e of this.sources) e.initFrames(), e.checkImageAvailability(); this.setTreePriority(); } get frameCount() { return this.options.frames; } get index() { return this.wrapIndex(this.frame); } ready() { return new Promise((e) => { let t = () => { this.sources.every((e) => e.initialized) ? e() : setTimeout(t, 16); }; t(); }); } tick(e) { this.tickFunctions.push(e); } play(e = 30) { this.speed = e; } stop() { this.speed = 0; } async getFrameImage(e) { return await this.frames[this.wrapIndex(e)].fetchImage(); } async onLoadProgress(e) { let t = this.loadProgress; return new Promise((n) => { let r = () => { this.loadProgress >= 1 ? (e && e(1), n(!0)) : (e && t !== this.loadProgress && (e(this.loadProgress), t = this.loadProgress), setTimeout(r, 16)); }; r(); }); } destruct() { this.destructed || (this.destructed = !0, this.animationRequestId && cancelAnimationFrame(this.animationRequestId), this.resizeObserver.disconnect(), this.mutationObserver.disconnect(), this.inViewportObserver.disconnect(), this.container.removeChild(this.canvas), this.logElement &&= (this.container.removeChild(this.logElement), void 0), this.canvas.replaceWith(this.canvas.cloneNode(!0)), this.sources.forEach((e) => e.destruct()), this.frames.forEach((e) => e.releaseImage())); } setDisplayOptions(e) { this.options = { ...this.options, ...e }, this.forceRedraw = !0; } setTreePriority() { for (let e of this.frames) e.treePriority = 1 - e.index / (this.frameCount * 2); let e = 0; for (let t = 1; t <= this.frames.length; t = t * 2 + 1, e++) for (let e of this.frames) (e.index & t) === 0 && (e.treePriority += 1); for (let t of this.frames) t.treePriority = t.index === this.frameCount - 1 ? 0 : Math.max(0, 1 - t.treePriority / e); } setLoadingPriority(e = 0) { let t = this.index, n = (this.options.loop ? 2 : 1) / Math.max(1, this.frames.length); for (let e of this.frames) { let r = Math.abs(e.index + .25 - t); this.options.loop && (r = Math.min(r, this.options.frames - r)), e.priority = r * n + 1; } e > 0 && this.frames.sort((e, t) => e.treePriority - t.treePriority).slice(0, e).forEach((e) => e.priority = e.treePriority); } async loadResources() { if (this.options.poster) { this.log("Poster image", this.options.poster); let e = new Image(); e.src = this.options.poster, await e.decode().then(() => { this.posterImage = e, this.lastFrameDrawn < 0 && this.drawImage(this.posterImage); }).catch((e) => this.log(e)); } await Promise.all(this.sources.map((e) => e.loadResources())); let e = await this.getFrameImage(0); e && r(e); } wrapIndex(e) { let t = e | 0; return this.wrapFrame(t); } wrapFrame(e) { return this.options.loop ? (e % this.options.frames + this.options.frames) % this.options.frames : C(e, 0, this.options.frames - 1); } async drawingLoop(e = 0) { if (this.destructed) return; e /= 1e3; let t = 0; if (this.initialized && (t = this.startTime < 0 ? 1 / 60 : Math.min(e - this.startTime, 1 / 30)), this.startTime = e > 0 ? e : -1, this.frame - this.prevFrame < 0 && (this.direction = -1), this.frame - this.prevFrame > 0 && (this.direction = 1), this.frame += this.speed * t, this.frame = this.wrapFrame(this.frame), this.inViewport) { let e = this.index, t = Math.sign(this.frame - this.prevFrame), n = this.direction * t === -1 ? this.frames.length : 0, r, i = Infinity; for (let t of this.frames) { if (t.image === void 0) continue; let a = Math.abs(t.index - e); this.options.loop && (a = Math.min(a, this.options.frames - a)), a += n, a < i && (i = a, r = t); } r && this.drawFrame(r); } this.wrapIndex(this.frame) === this.wrapIndex(this.prevFrame) ? this.timeFrameVisible += t : this.timeFrameVisible = 0, this.process(), this.tickFunctions.forEach((e) => e(t)), this.prevFrame = this.frame, this.animationRequestId = requestAnimationFrame((e) => this.drawingLoop(e)); } drawFrame(e) { let t = e.image; !t || e.index >= this.options.frames || (this.lastFrameDrawn = e.index, this.drawImage(t)); } drawImage(e) { let t = e.naturalWidth || e.width || e.videoWidth || e.displayWidth || e.codedWidth, n = e.naturalHeight || e.height || e.videoHeight || e.displayHeight || e.codedHeight; this.width = Math.max(this.width, t), this.height = Math.max(this.height, n); let r = window.devicePixelRatio || 1, i = Math.max(1, Math.round(this.containerWidth * r)), a = Math.max(1, Math.round(this.containerHeight * r)); (this.canvas.width !== i || this.canvas.height !== a) && (this.canvas.width = i, this.canvas.height = a, this.context.imageSmoothingQuality = "high", this.forceRedraw = !0); let o = i / a, s = this.width / this.height, c = this.options.scale, l, u; this.options.objectFit === "contain" ? o > s ? (u = a, l = a * s) : (l = i, u = i / s) : o > s ? (l = i, u = i / s) : (u = a, l = a * s), l *= c, u *= c; let d = (i - l) * this.options.horizontalAlign, f = (a - u) * this.options.verticalAlign; (this.forceRedraw || this.options.clearCanvas) && this.context.clearRect(0, 0, i, a), (this.forceRedraw || this.options.clearCanvas || this.lastImageDrawn !== e) && (this.context.drawImage(e, 0, 0, t, n, d, f, l, u), this.lastImageDrawn = e), this.forceRedraw = !1; } process() { for (let e of this.sources) this.timeFrameVisible >= e.options.timeout / 1e3 && e.process((e = 0) => this.setLoadingPriority(e)); } logDebugStatus(e) { let t = (e) => `${Math.abs(e * 100).toFixed(1).padStart(5, " ")}%`, r = `${this.options.name} - frames: ${this.frames.length}, loop: ${this.options.loop}, objectFit: ${this.options.objectFit}\n loadProgress ${t(this.loadProgress)}, last frame drawn ${this.lastFrameDrawn}/${this.index}\n`; for (let e of this.sources) { let { progress: n, numLoading: i, numLoaded: a, maxLoaded: o } = e.getLoadStatus(); r += ` src[${e.index}] ${x[e.type] ?? "? "} ${t(n)}, numLoading: ${i}, numLoaded: ${a}/${o}${e.options.useWorker ? ", use worker" : ""}\n`; } n(e, r); } }; //#endregion export { C as n, S as r, w as t };