itube-modern-player 0.7.1 → 0.7.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/README.md +11 -2
- package/dist/core.cjs +1 -4
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +452 -494
- package/dist/core.js.map +1 -1
- package/dist/dom-Bq7EQenh.cjs +5 -0
- package/dist/dom-Bq7EQenh.cjs.map +1 -0
- package/dist/dom-DrpWbY0y.js +87 -0
- package/dist/dom-DrpWbY0y.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -19
- package/dist/index.js.map +1 -1
- package/dist/itube-modern-player.iife.js +2 -2
- package/dist/itube-modern-player.iife.js.map +1 -1
- package/dist/lazy.cjs +1 -1
- package/dist/lazy.cjs.map +1 -1
- package/dist/lazy.js +56 -44
- package/dist/lazy.js.map +1 -1
- package/dist/player.d.ts +12 -0
- package/package.json +1 -1
package/dist/lazy.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const L=require("./dom-Bq7EQenh.cjs"),I='<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.14v13.72c0 .8.87 1.3 1.56.88l10.54-6.86a1.05 1.05 0 0 0 0-1.76L9.56 4.26C8.87 3.84 8 4.34 8 5.14Z"/></svg>';function N(d,E={}){const i=typeof d=="string"?document.querySelector(d):d;if(!i)throw new Error(`[itube-player] mount target not found: ${String(d)}`);const{loadOn:l="interaction",...s}=E,v=Array.isArray(s.source)?s.source[0]:s.source,u=i.children.length>0;let t;if(u)t=i;else{t=document.createElement("div"),t.className="imp-player imp-player--lazy";const e=s.styling;if(e?.themeColor&&t.style.setProperty("--imp-accent",e.themeColor),e?.borderRadius!==void 0){const a=typeof e.borderRadius=="number"?`${e.borderRadius}px`:e.borderRadius;t.style.setProperty("--imp-radius",a)}e?.playButtonStyle==="inverted"&&t.classList.add("imp-player--play-inverted"),s.className&&t.classList.add(s.className);const n=document.createElement("div");n.className="imp-poster";const r=document.createElement("div");if(r.className="imp-poster__image",v?.poster){const a=document.createElement("img");a.className="imp-cover-img",a.alt="",a.decoding="async",a.fetchPriority="high",a.src=v.poster,r.append(a)}else r.hidden=!0;const o=document.createElement("button");o.type="button",o.className="imp-btn imp-poster__play",o.setAttribute("aria-label","Play"),L.applyIcon(o,s.icons?.bigPlay??I),n.append(r,o),t.append(n),i.append(t)}let h,b;const g=new Promise((e,n)=>{h=e,b=n});g.catch(()=>{});const m={ready:g,player:null,el:t,destroy(){y=!0,P(),u||t.remove(),m.player?.destroy()}};let y=!1,f=!1;const p=[],P=()=>{for(const e of p.splice(0))e()};let w=!1;const c=e=>{w||(w=e),!(f||y)&&(f=!0,P(),Promise.resolve().then(()=>require("./index.cjs")).then(({Player:n})=>{if(y)return;u?i.textContent="":t.remove();const r=w?{...s,playOnInit:!0,autoplay:!0}:l==="interaction"||l==="placeholder"?{...s,playOnInit:!1,autoplay:!1}:s,o=new n(i,r);m.player=o,h(o)},n=>{f=!1,b(n)}))};if(t.addEventListener("click",()=>c(!0)),l==="immediate")c(!1);else if(l==="placeholder"){const e=()=>c(!1),n=["pointerenter","pointerdown","touchstart","focusin"];for(const r of n)t.addEventListener(r,e,{once:!0,passive:!0}),p.push(()=>t.removeEventListener(r,e))}else if(l==="visible"&&typeof IntersectionObserver<"u"){const e=new IntersectionObserver(n=>{n.some(r=>r.isIntersecting)&&c(!1)});e.observe(t),p.push(()=>e.disconnect())}else{const e=()=>c(!1),n=["pointerdown","keydown","touchstart","wheel"];for(const r of n)document.addEventListener(r,e,{once:!0,passive:!0}),p.push(()=>document.removeEventListener(r,e))}return m}exports.createLazyPlayer=N;
|
|
2
2
|
//# sourceMappingURL=lazy.cjs.map
|
package/dist/lazy.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy.cjs","sources":["../src/lazy.ts"],"sourcesContent":["/**\n * Micro entry (`itube-modern-player/lazy`) for bundle-deferred setups.\n *\n * Renders an instant poster placeholder (pure DOM, a couple of KB) and pulls\n * the real player chunk on the first user interaction — the pattern used to\n * be hand-rolled around fluid-player; here it is built in.\n *\n * ```ts\n * import { createLazyPlayer } from 'itube-modern-player/lazy'\n * import 'itube-modern-player/style.css'\n *\n * const lazy = createLazyPlayer('#mount', {\n * source: { src: 'stream.m3u8', poster: 'poster.jpg', title: '…' },\n * loadOn: 'interaction',\n * })\n * lazy.ready.then((player) => player.on('ended', …))\n * ```\n */\nimport type { Player } from './player'\nimport type { PlayerOptions } from './types'\n\nexport interface LazyPlayerOptions extends PlayerOptions {\n /**\n * When to download the player bundle:\n * - `\"interaction\"` (default) — first pointerdown / keydown / touch anywhere on the page;\n * - `\"placeholder\"` — any interaction with the placeholder AREA (hover,\n * pointer press, touch, keyboard focus) — mobile-friendly;\n * - `\"visible\"` — when the placeholder enters the viewport;\n * - `\"immediate\"` — right away (you still get the split chunk + instant placeholder).\n *\n * A click on the placeholder always loads immediately and starts playback.\n * Background triggers (`\"interaction\"` / `\"placeholder\"`) never start\n * playback, even with `playOnInit: true` in the options.\n */\n loadOn?: 'interaction' | 'placeholder' | 'visible' | 'immediate'\n}\n\nexport interface LazyPlayer {\n /** Resolves with the real `Player` once the chunk is loaded and constructed. */\n ready: Promise<Player>\n /** The instance, `null` until loaded. */\n player: Player | null\n /** Placeholder root (removed when the player takes over). */\n el: HTMLElement\n destroy(): void\n}\n\nconst PLAY_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M8 5.14v13.72c0 .8.87 1.3 1.56.88l10.54-6.86a1.05 1.05 0 0 0 0-1.76L9.56 4.26C8.87 3.84 8 4.34 8 5.14Z\"/></svg>'\n\nexport function createLazyPlayer(target: string | HTMLElement, options: LazyPlayerOptions = {}): LazyPlayer {\n const mount = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target\n if (!mount) throw new Error(`[itube-player] mount target not found: ${String(target)}`)\n\n const { loadOn = 'interaction', ...playerOptions } = options\n const first = Array.isArray(playerOptions.source) ? playerOptions.source[0] : playerOptions.source\n\n // Placeholder reuses the player's own CSS classes — pixel-identical to the\n // real preview poster, so the swap is invisible.\n const ph = document.createElement('div')\n ph.className = 'imp-player imp-player--lazy'\n const poster = document.createElement('div')\n poster.className = 'imp-poster'\n // Same div > <img> markup as the real player's PosterOverlay — the swap on\n // load is invisible, and the poster <img> is a proper LCP candidate.\n const image = document.createElement('div')\n image.className = 'imp-poster__image'\n if (first?.poster) {\n const img = document.createElement('img')\n img.className = 'imp-cover-img'\n img.alt = ''\n img.decoding = 'async'\n img.fetchPriority = 'high'\n img.src = first.poster\n image.append(img)\n } else {\n image.hidden = true\n }\n const btn = document.createElement('button')\n btn.type = 'button'\n btn.className = 'imp-btn imp-poster__play'\n btn.setAttribute('aria-label', 'Play')\n btn.innerHTML = PLAY_SVG\n poster.append(image, btn)\n ph.append(poster)\n mount.append(ph)\n\n let resolveReady!: (p: Player) => void\n let rejectReady!: (e: unknown) => void\n const ready = new Promise<Player>((resolve, reject) => {\n resolveReady = resolve\n rejectReady = reject\n })\n // Consumers may ignore `ready` entirely — that must not crash the app.\n ready.catch(() => {})\n\n const api: LazyPlayer = {\n ready,\n player: null,\n el: ph,\n destroy() {\n destroyed = true\n cleanup()\n ph.remove()\n api.player?.destroy()\n },\n }\n\n let destroyed = false\n let loading = false\n const disposers: Array<() => void> = []\n const cleanup = () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n\n // Autoplay intent is accumulated, not tied to the call that started the\n // download: a poster click DURING an in-flight background load ('interaction'\n // trigger) must still start playback once the chunk arrives.\n let wantAutoplay = false\n const load = (autoplay: boolean) => {\n wantAutoplay ||= autoplay\n if (loading || destroyed) return\n loading = true\n cleanup()\n // The full entry (not ./player) so built-in locales arrive with the chunk.\n import('./index').then(\n ({ Player: PlayerCtor }) => {\n if (destroyed) return\n ph.remove()\n // 'interaction'/'placeholder' background preload must be INVISIBLE: a\n // consumer-set `playOnInit: true` (or legacy `autoplay`) would otherwise\n // turn \"preload on first interaction\" into \"surprise-play on the first\n // click anywhere on the page\". Play starts only from a real placeholder\n // click (wantAutoplay). Honored as-is for 'immediate' and 'visible'.\n // Both keys are overridden — `playOnInit` wins over the alias in Player.\n const effective = wantAutoplay\n ? { ...playerOptions, playOnInit: true, autoplay: true }\n : loadOn === 'interaction' || loadOn === 'placeholder'\n ? { ...playerOptions, playOnInit: false, autoplay: false }\n : playerOptions\n const player = new PlayerCtor(mount, effective)\n api.player = player\n resolveReady(player)\n },\n (err) => {\n loading = false\n rejectReady(err)\n },\n )\n }\n\n // Click always wins: load + play. NOT in `disposers` — it must survive the\n // load-start cleanup so a click during an in-flight background load still\n // registers the autoplay intent (the listener dies with ph.remove()).\n ph.addEventListener('click', () => load(true))\n\n if (loadOn === 'immediate') {\n load(false)\n } else if (loadOn === 'placeholder') {\n // Any interaction with the placeholder AREA preloads the bundle: hover\n // (desktop), pointer press / touch (mobile), keyboard focus. Playback\n // still starts only from the click (load(true) above).\n const onPh = () => load(false)\n const events: Array<keyof HTMLElementEventMap> = ['pointerenter', 'pointerdown', 'touchstart', 'focusin']\n for (const name of events) {\n ph.addEventListener(name, onPh, { once: true, passive: true })\n disposers.push(() => ph.removeEventListener(name, onPh))\n }\n } else if (loadOn === 'visible' && typeof IntersectionObserver !== 'undefined') {\n const io = new IntersectionObserver((entries) => {\n if (entries.some((e) => e.isIntersecting)) load(false)\n })\n io.observe(ph)\n disposers.push(() => io.disconnect())\n } else {\n const onFirstInteraction = () => load(false)\n const events: Array<keyof DocumentEventMap> = ['pointerdown', 'keydown', 'touchstart', 'wheel']\n for (const name of events) {\n document.addEventListener(name, onFirstInteraction, { once: true, passive: true })\n disposers.push(() => document.removeEventListener(name, onFirstInteraction))\n }\n }\n\n return api\n}\n"],"names":["PLAY_SVG","createLazyPlayer","target","options","mount","loadOn","playerOptions","first","ph","poster","image","img","btn","resolveReady","rejectReady","ready","resolve","reject","api","destroyed","cleanup","loading","disposers","dispose","wantAutoplay","load","autoplay","PlayerCtor","effective","player","err","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lazy.cjs","sources":["../src/lazy.ts"],"sourcesContent":["/**\n * Micro entry (`itube-modern-player/lazy`) for bundle-deferred setups.\n *\n * Renders an instant poster placeholder (pure DOM, a couple of KB) and pulls\n * the real player chunk on the first user interaction — the pattern used to\n * be hand-rolled around fluid-player; here it is built in.\n *\n * ```ts\n * import { createLazyPlayer } from 'itube-modern-player/lazy'\n * import 'itube-modern-player/style.css'\n *\n * const lazy = createLazyPlayer('#mount', {\n * source: { src: 'stream.m3u8', poster: 'poster.jpg', title: '…' },\n * loadOn: 'interaction',\n * })\n * lazy.ready.then((player) => player.on('ended', …))\n * ```\n */\nimport { applyIcon } from './core/dom'\nimport type { Player } from './player'\nimport type { PlayerOptions } from './types'\n\nexport interface LazyPlayerOptions extends PlayerOptions {\n /**\n * When to download the player bundle:\n * - `\"interaction\"` (default) — first pointerdown / keydown / touch anywhere on the page;\n * - `\"placeholder\"` — any interaction with the placeholder AREA (hover,\n * pointer press, touch, keyboard focus) — mobile-friendly;\n * - `\"visible\"` — when the placeholder enters the viewport;\n * - `\"immediate\"` — right away (you still get the split chunk + instant placeholder).\n *\n * A click on the placeholder always loads immediately and starts playback.\n * Background triggers (`\"interaction\"` / `\"placeholder\"`) never start\n * playback, even with `playOnInit: true` in the options.\n */\n loadOn?: 'interaction' | 'placeholder' | 'visible' | 'immediate'\n}\n\nexport interface LazyPlayer {\n /** Resolves with the real `Player` once the chunk is loaded and constructed. */\n ready: Promise<Player>\n /** The instance, `null` until loaded. */\n player: Player | null\n /** Placeholder root (removed when the player takes over). */\n el: HTMLElement\n destroy(): void\n}\n\nconst PLAY_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M8 5.14v13.72c0 .8.87 1.3 1.56.88l10.54-6.86a1.05 1.05 0 0 0 0-1.76L9.56 4.26C8.87 3.84 8 4.34 8 5.14Z\"/></svg>'\n\nexport function createLazyPlayer(target: string | HTMLElement, options: LazyPlayerOptions = {}): LazyPlayer {\n const mount = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target\n if (!mount) throw new Error(`[itube-player] mount target not found: ${String(target)}`)\n\n const { loadOn = 'interaction', ...playerOptions } = options\n const first = Array.isArray(playerOptions.source) ? playerOptions.source[0] : playerOptions.source\n\n // SSR-friendly adoption: when the mount ALREADY has content (a server-\n // rendered poster — any markup, its own classes are fine), it becomes the\n // placeholder as-is. No client-side DOM is created, the SSR <img> stays the\n // page's LCP candidate; we only attach the load triggers and swap the\n // content for the player when the bundle arrives.\n const adopted = mount.children.length > 0\n let ph: HTMLElement\n if (adopted) {\n ph = mount\n } else {\n // Placeholder reuses the player's own CSS classes — pixel-identical to the\n // real preview poster, so the swap is invisible.\n ph = document.createElement('div')\n ph.className = 'imp-player imp-player--lazy'\n // Styling parity with the real player: the same knobs paint the placeholder.\n const styling = playerOptions.styling\n if (styling?.themeColor) ph.style.setProperty('--imp-accent', styling.themeColor)\n if (styling?.borderRadius !== undefined) {\n const r = typeof styling.borderRadius === 'number' ? `${styling.borderRadius}px` : styling.borderRadius\n ph.style.setProperty('--imp-radius', r)\n }\n if (styling?.playButtonStyle === 'inverted') ph.classList.add('imp-player--play-inverted')\n if (playerOptions.className) ph.classList.add(playerOptions.className)\n const poster = document.createElement('div')\n poster.className = 'imp-poster'\n // Same div > <img> markup as the real player's PosterOverlay — the swap on\n // load is invisible, and the poster <img> is a proper LCP candidate.\n const image = document.createElement('div')\n image.className = 'imp-poster__image'\n if (first?.poster) {\n const img = document.createElement('img')\n img.className = 'imp-cover-img'\n img.alt = ''\n img.decoding = 'async'\n img.fetchPriority = 'high'\n img.src = first.poster\n image.append(img)\n } else {\n image.hidden = true\n }\n const btn = document.createElement('button')\n btn.type = 'button'\n btn.className = 'imp-btn imp-poster__play'\n btn.setAttribute('aria-label', 'Play')\n // The consumer's custom big-play icon (options.icons.bigPlay) must show on\n // the placeholder too — a default triangle swapping to the custom icon on\n // load reads as a glitch.\n applyIcon(btn, playerOptions.icons?.bigPlay ?? PLAY_SVG)\n poster.append(image, btn)\n ph.append(poster)\n mount.append(ph)\n }\n\n let resolveReady!: (p: Player) => void\n let rejectReady!: (e: unknown) => void\n const ready = new Promise<Player>((resolve, reject) => {\n resolveReady = resolve\n rejectReady = reject\n })\n // Consumers may ignore `ready` entirely — that must not crash the app.\n ready.catch(() => {})\n\n const api: LazyPlayer = {\n ready,\n player: null,\n el: ph,\n destroy() {\n destroyed = true\n cleanup()\n // Adopted mode: the placeholder IS the consumer's mount node (with their\n // SSR markup) — never remove it, just stop managing it.\n if (!adopted) ph.remove()\n api.player?.destroy()\n },\n }\n\n let destroyed = false\n let loading = false\n const disposers: Array<() => void> = []\n const cleanup = () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n\n // Autoplay intent is accumulated, not tied to the call that started the\n // download: a poster click DURING an in-flight background load ('interaction'\n // trigger) must still start playback once the chunk arrives.\n let wantAutoplay = false\n const load = (autoplay: boolean) => {\n wantAutoplay ||= autoplay\n if (loading || destroyed) return\n loading = true\n cleanup()\n // The full entry (not ./player) so built-in locales arrive with the chunk.\n import('./index').then(\n ({ Player: PlayerCtor }) => {\n if (destroyed) return\n // Swap the placeholder for the player: created one is removed whole;\n // an adopted (SSR) mount keeps the node, only its content is cleared.\n if (adopted) mount.textContent = ''\n else ph.remove()\n // 'interaction'/'placeholder' background preload must be INVISIBLE: a\n // consumer-set `playOnInit: true` (or legacy `autoplay`) would otherwise\n // turn \"preload on first interaction\" into \"surprise-play on the first\n // click anywhere on the page\". Play starts only from a real placeholder\n // click (wantAutoplay). Honored as-is for 'immediate' and 'visible'.\n // Both keys are overridden — `playOnInit` wins over the alias in Player.\n const effective = wantAutoplay\n ? { ...playerOptions, playOnInit: true, autoplay: true }\n : loadOn === 'interaction' || loadOn === 'placeholder'\n ? { ...playerOptions, playOnInit: false, autoplay: false }\n : playerOptions\n const player = new PlayerCtor(mount, effective)\n api.player = player\n resolveReady(player)\n },\n (err) => {\n loading = false\n rejectReady(err)\n },\n )\n }\n\n // Click always wins: load + play. NOT in `disposers` — it must survive the\n // load-start cleanup so a click during an in-flight background load still\n // registers the autoplay intent (the listener dies with ph.remove()).\n ph.addEventListener('click', () => load(true))\n\n if (loadOn === 'immediate') {\n load(false)\n } else if (loadOn === 'placeholder') {\n // Any interaction with the placeholder AREA preloads the bundle: hover\n // (desktop), pointer press / touch (mobile), keyboard focus. Playback\n // still starts only from the click (load(true) above).\n const onPh = () => load(false)\n const events: Array<keyof HTMLElementEventMap> = ['pointerenter', 'pointerdown', 'touchstart', 'focusin']\n for (const name of events) {\n ph.addEventListener(name, onPh, { once: true, passive: true })\n disposers.push(() => ph.removeEventListener(name, onPh))\n }\n } else if (loadOn === 'visible' && typeof IntersectionObserver !== 'undefined') {\n const io = new IntersectionObserver((entries) => {\n if (entries.some((e) => e.isIntersecting)) load(false)\n })\n io.observe(ph)\n disposers.push(() => io.disconnect())\n } else {\n const onFirstInteraction = () => load(false)\n const events: Array<keyof DocumentEventMap> = ['pointerdown', 'keydown', 'touchstart', 'wheel']\n for (const name of events) {\n document.addEventListener(name, onFirstInteraction, { once: true, passive: true })\n disposers.push(() => document.removeEventListener(name, onFirstInteraction))\n }\n }\n\n return api\n}\n"],"names":["PLAY_SVG","createLazyPlayer","target","options","mount","loadOn","playerOptions","first","adopted","ph","styling","r","poster","image","img","btn","applyIcon","resolveReady","rejectReady","ready","resolve","reject","api","destroyed","cleanup","loading","disposers","dispose","wantAutoplay","load","autoplay","PlayerCtor","effective","player","err","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":"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"}
|
package/dist/lazy.js
CHANGED
|
@@ -1,66 +1,78 @@
|
|
|
1
|
+
import { a as P } from "./dom-DrpWbY0y.js";
|
|
1
2
|
const I = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.14v13.72c0 .8.87 1.3 1.56.88l10.54-6.86a1.05 1.05 0 0 0 0-1.76L9.56 4.26C8.87 3.84 8 4.34 8 5.14Z"/></svg>';
|
|
2
|
-
function O(
|
|
3
|
-
const
|
|
4
|
-
if (!
|
|
5
|
-
const { loadOn:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const e =
|
|
12
|
-
e
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
function O(p, L = {}) {
|
|
4
|
+
const i = typeof p == "string" ? document.querySelector(p) : p;
|
|
5
|
+
if (!i) throw new Error(`[itube-player] mount target not found: ${String(p)}`);
|
|
6
|
+
const { loadOn: l = "interaction", ...o } = L, v = Array.isArray(o.source) ? o.source[0] : o.source, u = i.children.length > 0;
|
|
7
|
+
let t;
|
|
8
|
+
if (u)
|
|
9
|
+
t = i;
|
|
10
|
+
else {
|
|
11
|
+
t = document.createElement("div"), t.className = "imp-player imp-player--lazy";
|
|
12
|
+
const e = o.styling;
|
|
13
|
+
if (e?.themeColor && t.style.setProperty("--imp-accent", e.themeColor), e?.borderRadius !== void 0) {
|
|
14
|
+
const a = typeof e.borderRadius == "number" ? `${e.borderRadius}px` : e.borderRadius;
|
|
15
|
+
t.style.setProperty("--imp-radius", a);
|
|
16
|
+
}
|
|
17
|
+
e?.playButtonStyle === "inverted" && t.classList.add("imp-player--play-inverted"), o.className && t.classList.add(o.className);
|
|
18
|
+
const n = document.createElement("div");
|
|
19
|
+
n.className = "imp-poster";
|
|
20
|
+
const s = document.createElement("div");
|
|
21
|
+
if (s.className = "imp-poster__image", v?.poster) {
|
|
22
|
+
const a = document.createElement("img");
|
|
23
|
+
a.className = "imp-cover-img", a.alt = "", a.decoding = "async", a.fetchPriority = "high", a.src = v.poster, s.append(a);
|
|
24
|
+
} else
|
|
25
|
+
s.hidden = !0;
|
|
26
|
+
const r = document.createElement("button");
|
|
27
|
+
r.type = "button", r.className = "imp-btn imp-poster__play", r.setAttribute("aria-label", "Play"), P(r, o.icons?.bigPlay ?? I), n.append(s, r), t.append(n), i.append(t);
|
|
28
|
+
}
|
|
17
29
|
let h, b;
|
|
18
|
-
const g = new Promise((e,
|
|
19
|
-
h = e, b =
|
|
30
|
+
const g = new Promise((e, n) => {
|
|
31
|
+
h = e, b = n;
|
|
20
32
|
});
|
|
21
33
|
g.catch(() => {
|
|
22
34
|
});
|
|
23
35
|
const m = {
|
|
24
36
|
ready: g,
|
|
25
37
|
player: null,
|
|
26
|
-
el:
|
|
38
|
+
el: t,
|
|
27
39
|
destroy() {
|
|
28
|
-
|
|
40
|
+
y = !0, w(), u || t.remove(), m.player?.destroy();
|
|
29
41
|
}
|
|
30
42
|
};
|
|
31
|
-
let
|
|
32
|
-
const
|
|
33
|
-
for (const e of
|
|
43
|
+
let y = !1, f = !1;
|
|
44
|
+
const d = [], w = () => {
|
|
45
|
+
for (const e of d.splice(0)) e();
|
|
34
46
|
};
|
|
35
47
|
let E = !1;
|
|
36
|
-
const
|
|
37
|
-
E || (E = e), !(
|
|
38
|
-
({ Player:
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
m.player =
|
|
48
|
+
const c = (e) => {
|
|
49
|
+
E || (E = e), !(f || y) && (f = !0, w(), import("./index.js").then(
|
|
50
|
+
({ Player: n }) => {
|
|
51
|
+
if (y) return;
|
|
52
|
+
u ? i.textContent = "" : t.remove();
|
|
53
|
+
const s = E ? { ...o, playOnInit: !0, autoplay: !0 } : l === "interaction" || l === "placeholder" ? { ...o, playOnInit: !1, autoplay: !1 } : o, r = new n(i, s);
|
|
54
|
+
m.player = r, h(r);
|
|
43
55
|
},
|
|
44
|
-
(
|
|
45
|
-
|
|
56
|
+
(n) => {
|
|
57
|
+
f = !1, b(n);
|
|
46
58
|
}
|
|
47
59
|
));
|
|
48
60
|
};
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
else if (
|
|
52
|
-
const e = () =>
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
} else if (
|
|
56
|
-
const e = new IntersectionObserver((
|
|
57
|
-
|
|
61
|
+
if (t.addEventListener("click", () => c(!0)), l === "immediate")
|
|
62
|
+
c(!1);
|
|
63
|
+
else if (l === "placeholder") {
|
|
64
|
+
const e = () => c(!1), n = ["pointerenter", "pointerdown", "touchstart", "focusin"];
|
|
65
|
+
for (const s of n)
|
|
66
|
+
t.addEventListener(s, e, { once: !0, passive: !0 }), d.push(() => t.removeEventListener(s, e));
|
|
67
|
+
} else if (l === "visible" && typeof IntersectionObserver < "u") {
|
|
68
|
+
const e = new IntersectionObserver((n) => {
|
|
69
|
+
n.some((s) => s.isIntersecting) && c(!1);
|
|
58
70
|
});
|
|
59
|
-
e.observe(
|
|
71
|
+
e.observe(t), d.push(() => e.disconnect());
|
|
60
72
|
} else {
|
|
61
|
-
const e = () =>
|
|
62
|
-
for (const
|
|
63
|
-
document.addEventListener(
|
|
73
|
+
const e = () => c(!1), n = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
74
|
+
for (const s of n)
|
|
75
|
+
document.addEventListener(s, e, { once: !0, passive: !0 }), d.push(() => document.removeEventListener(s, e));
|
|
64
76
|
}
|
|
65
77
|
return m;
|
|
66
78
|
}
|
package/dist/lazy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy.js","sources":["../src/lazy.ts"],"sourcesContent":["/**\n * Micro entry (`itube-modern-player/lazy`) for bundle-deferred setups.\n *\n * Renders an instant poster placeholder (pure DOM, a couple of KB) and pulls\n * the real player chunk on the first user interaction — the pattern used to\n * be hand-rolled around fluid-player; here it is built in.\n *\n * ```ts\n * import { createLazyPlayer } from 'itube-modern-player/lazy'\n * import 'itube-modern-player/style.css'\n *\n * const lazy = createLazyPlayer('#mount', {\n * source: { src: 'stream.m3u8', poster: 'poster.jpg', title: '…' },\n * loadOn: 'interaction',\n * })\n * lazy.ready.then((player) => player.on('ended', …))\n * ```\n */\nimport type { Player } from './player'\nimport type { PlayerOptions } from './types'\n\nexport interface LazyPlayerOptions extends PlayerOptions {\n /**\n * When to download the player bundle:\n * - `\"interaction\"` (default) — first pointerdown / keydown / touch anywhere on the page;\n * - `\"placeholder\"` — any interaction with the placeholder AREA (hover,\n * pointer press, touch, keyboard focus) — mobile-friendly;\n * - `\"visible\"` — when the placeholder enters the viewport;\n * - `\"immediate\"` — right away (you still get the split chunk + instant placeholder).\n *\n * A click on the placeholder always loads immediately and starts playback.\n * Background triggers (`\"interaction\"` / `\"placeholder\"`) never start\n * playback, even with `playOnInit: true` in the options.\n */\n loadOn?: 'interaction' | 'placeholder' | 'visible' | 'immediate'\n}\n\nexport interface LazyPlayer {\n /** Resolves with the real `Player` once the chunk is loaded and constructed. */\n ready: Promise<Player>\n /** The instance, `null` until loaded. */\n player: Player | null\n /** Placeholder root (removed when the player takes over). */\n el: HTMLElement\n destroy(): void\n}\n\nconst PLAY_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M8 5.14v13.72c0 .8.87 1.3 1.56.88l10.54-6.86a1.05 1.05 0 0 0 0-1.76L9.56 4.26C8.87 3.84 8 4.34 8 5.14Z\"/></svg>'\n\nexport function createLazyPlayer(target: string | HTMLElement, options: LazyPlayerOptions = {}): LazyPlayer {\n const mount = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target\n if (!mount) throw new Error(`[itube-player] mount target not found: ${String(target)}`)\n\n const { loadOn = 'interaction', ...playerOptions } = options\n const first = Array.isArray(playerOptions.source) ? playerOptions.source[0] : playerOptions.source\n\n // Placeholder reuses the player's own CSS classes — pixel-identical to the\n // real preview poster, so the swap is invisible.\n const ph = document.createElement('div')\n ph.className = 'imp-player imp-player--lazy'\n const poster = document.createElement('div')\n poster.className = 'imp-poster'\n // Same div > <img> markup as the real player's PosterOverlay — the swap on\n // load is invisible, and the poster <img> is a proper LCP candidate.\n const image = document.createElement('div')\n image.className = 'imp-poster__image'\n if (first?.poster) {\n const img = document.createElement('img')\n img.className = 'imp-cover-img'\n img.alt = ''\n img.decoding = 'async'\n img.fetchPriority = 'high'\n img.src = first.poster\n image.append(img)\n } else {\n image.hidden = true\n }\n const btn = document.createElement('button')\n btn.type = 'button'\n btn.className = 'imp-btn imp-poster__play'\n btn.setAttribute('aria-label', 'Play')\n btn.innerHTML = PLAY_SVG\n poster.append(image, btn)\n ph.append(poster)\n mount.append(ph)\n\n let resolveReady!: (p: Player) => void\n let rejectReady!: (e: unknown) => void\n const ready = new Promise<Player>((resolve, reject) => {\n resolveReady = resolve\n rejectReady = reject\n })\n // Consumers may ignore `ready` entirely — that must not crash the app.\n ready.catch(() => {})\n\n const api: LazyPlayer = {\n ready,\n player: null,\n el: ph,\n destroy() {\n destroyed = true\n cleanup()\n ph.remove()\n api.player?.destroy()\n },\n }\n\n let destroyed = false\n let loading = false\n const disposers: Array<() => void> = []\n const cleanup = () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n\n // Autoplay intent is accumulated, not tied to the call that started the\n // download: a poster click DURING an in-flight background load ('interaction'\n // trigger) must still start playback once the chunk arrives.\n let wantAutoplay = false\n const load = (autoplay: boolean) => {\n wantAutoplay ||= autoplay\n if (loading || destroyed) return\n loading = true\n cleanup()\n // The full entry (not ./player) so built-in locales arrive with the chunk.\n import('./index').then(\n ({ Player: PlayerCtor }) => {\n if (destroyed) return\n ph.remove()\n // 'interaction'/'placeholder' background preload must be INVISIBLE: a\n // consumer-set `playOnInit: true` (or legacy `autoplay`) would otherwise\n // turn \"preload on first interaction\" into \"surprise-play on the first\n // click anywhere on the page\". Play starts only from a real placeholder\n // click (wantAutoplay). Honored as-is for 'immediate' and 'visible'.\n // Both keys are overridden — `playOnInit` wins over the alias in Player.\n const effective = wantAutoplay\n ? { ...playerOptions, playOnInit: true, autoplay: true }\n : loadOn === 'interaction' || loadOn === 'placeholder'\n ? { ...playerOptions, playOnInit: false, autoplay: false }\n : playerOptions\n const player = new PlayerCtor(mount, effective)\n api.player = player\n resolveReady(player)\n },\n (err) => {\n loading = false\n rejectReady(err)\n },\n )\n }\n\n // Click always wins: load + play. NOT in `disposers` — it must survive the\n // load-start cleanup so a click during an in-flight background load still\n // registers the autoplay intent (the listener dies with ph.remove()).\n ph.addEventListener('click', () => load(true))\n\n if (loadOn === 'immediate') {\n load(false)\n } else if (loadOn === 'placeholder') {\n // Any interaction with the placeholder AREA preloads the bundle: hover\n // (desktop), pointer press / touch (mobile), keyboard focus. Playback\n // still starts only from the click (load(true) above).\n const onPh = () => load(false)\n const events: Array<keyof HTMLElementEventMap> = ['pointerenter', 'pointerdown', 'touchstart', 'focusin']\n for (const name of events) {\n ph.addEventListener(name, onPh, { once: true, passive: true })\n disposers.push(() => ph.removeEventListener(name, onPh))\n }\n } else if (loadOn === 'visible' && typeof IntersectionObserver !== 'undefined') {\n const io = new IntersectionObserver((entries) => {\n if (entries.some((e) => e.isIntersecting)) load(false)\n })\n io.observe(ph)\n disposers.push(() => io.disconnect())\n } else {\n const onFirstInteraction = () => load(false)\n const events: Array<keyof DocumentEventMap> = ['pointerdown', 'keydown', 'touchstart', 'wheel']\n for (const name of events) {\n document.addEventListener(name, onFirstInteraction, { once: true, passive: true })\n disposers.push(() => document.removeEventListener(name, onFirstInteraction))\n }\n }\n\n return api\n}\n"],"names":["PLAY_SVG","createLazyPlayer","target","options","mount","loadOn","playerOptions","first","ph","poster","image","img","btn","resolveReady","rejectReady","ready","resolve","reject","api","destroyed","cleanup","loading","disposers","dispose","wantAutoplay","load","autoplay","PlayerCtor","effective","player","err","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":"AA+CA,MAAMA,IACJ;AAEK,SAASC,EAAiBC,GAA8BC,IAA6B,IAAgB;AAC1G,QAAMC,IAAQ,OAAOF,KAAW,WAAW,SAAS,cAA2BA,CAAM,IAAIA;AACzF,MAAI,CAACE,EAAO,OAAM,IAAI,MAAM,0CAA0C,OAAOF,CAAM,CAAC,EAAE;AAEtF,QAAM,EAAE,QAAAG,IAAS,eAAe,GAAGC,MAAkBH,GAC/CI,IAAQ,MAAM,QAAQD,EAAc,MAAM,IAAIA,EAAc,OAAO,CAAC,IAAIA,EAAc,QAItFE,IAAK,SAAS,cAAc,KAAK;AACvC,EAAAA,EAAG,YAAY;AACf,QAAMC,IAAS,SAAS,cAAc,KAAK;AAC3C,EAAAA,EAAO,YAAY;AAGnB,QAAMC,IAAQ,SAAS,cAAc,KAAK;AAE1C,MADAA,EAAM,YAAY,qBACdH,GAAO,QAAQ;AACjB,UAAMI,IAAM,SAAS,cAAc,KAAK;AACxC,IAAAA,EAAI,YAAY,iBAChBA,EAAI,MAAM,IACVA,EAAI,WAAW,SACfA,EAAI,gBAAgB,QACpBA,EAAI,MAAMJ,EAAM,QAChBG,EAAM,OAAOC,CAAG;AAAA,EAClB;AACE,IAAAD,EAAM,SAAS;AAEjB,QAAME,IAAM,SAAS,cAAc,QAAQ;AAC3C,EAAAA,EAAI,OAAO,UACXA,EAAI,YAAY,4BAChBA,EAAI,aAAa,cAAc,MAAM,GACrCA,EAAI,YAAYZ,GAChBS,EAAO,OAAOC,GAAOE,CAAG,GACxBJ,EAAG,OAAOC,CAAM,GAChBL,EAAM,OAAOI,CAAE;AAEf,MAAIK,GACAC;AACJ,QAAMC,IAAQ,IAAI,QAAgB,CAACC,GAASC,MAAW;AACrD,IAAAJ,IAAeG,GACfF,IAAcG;AAAA,EAChB,CAAC;AAED,EAAAF,EAAM,MAAM,MAAM;AAAA,EAAC,CAAC;AAEpB,QAAMG,IAAkB;AAAA,IACtB,OAAAH;AAAA,IACA,QAAQ;AAAA,IACR,IAAIP;AAAA,IACJ,UAAU;AACR,MAAAW,IAAY,IACZC,EAAA,GACAZ,EAAG,OAAA,GACHU,EAAI,QAAQ,QAAA;AAAA,IACd;AAAA,EAAA;AAGF,MAAIC,IAAY,IACZE,IAAU;AACd,QAAMC,IAA+B,CAAA,GAC/BF,IAAU,MAAM;AACpB,eAAWG,KAAWD,EAAU,OAAO,CAAC,EAAG,CAAAC,EAAA;AAAA,EAC7C;AAKA,MAAIC,IAAe;AACnB,QAAMC,IAAO,CAACC,MAAsB;AAElC,IADAF,UAAiBE,IACb,EAAAL,KAAWF,OACfE,IAAU,IACVD,EAAA,GAEA,OAAO,YAAS,EAAE;AAAA,MAChB,CAAC,EAAE,QAAQO,QAAiB;AAC1B,YAAIR,EAAW;AACf,QAAAX,EAAG,OAAA;AAOH,cAAMoB,IAAYJ,IACd,EAAE,GAAGlB,GAAe,YAAY,IAAM,UAAU,OAChDD,MAAW,iBAAiBA,MAAW,gBACrC,EAAE,GAAGC,GAAe,YAAY,IAAO,UAAU,GAAA,IACjDA,GACAuB,IAAS,IAAIF,EAAWvB,GAAOwB,CAAS;AAC9C,QAAAV,EAAI,SAASW,GACbhB,EAAagB,CAAM;AAAA,MACrB;AAAA,MACA,CAACC,MAAQ;AACP,QAAAT,IAAU,IACVP,EAAYgB,CAAG;AAAA,MACjB;AAAA,IAAA;AAAA,EAEJ;AAOA,MAFAtB,EAAG,iBAAiB,SAAS,MAAMiB,EAAK,EAAI,CAAC,GAEzCpB,MAAW;AACb,IAAAoB,EAAK,EAAK;AAAA,WACDpB,MAAW,eAAe;AAInC,UAAM0B,IAAO,MAAMN,EAAK,EAAK,GACvBO,IAA2C,CAAC,gBAAgB,eAAe,cAAc,SAAS;AACxG,eAAWC,KAAQD;AACjB,MAAAxB,EAAG,iBAAiByB,GAAMF,GAAM,EAAE,MAAM,IAAM,SAAS,IAAM,GAC7DT,EAAU,KAAK,MAAMd,EAAG,oBAAoByB,GAAMF,CAAI,CAAC;AAAA,EAE3D,WAAW1B,MAAW,aAAa,OAAO,uBAAyB,KAAa;AAC9E,UAAM6B,IAAK,IAAI,qBAAqB,CAACC,MAAY;AAC/C,MAAIA,EAAQ,KAAK,CAACC,MAAMA,EAAE,cAAc,OAAQ,EAAK;AAAA,IACvD,CAAC;AACD,IAAAF,EAAG,QAAQ1B,CAAE,GACbc,EAAU,KAAK,MAAMY,EAAG,WAAA,CAAY;AAAA,EACtC,OAAO;AACL,UAAMG,IAAqB,MAAMZ,EAAK,EAAK,GACrCO,IAAwC,CAAC,eAAe,WAAW,cAAc,OAAO;AAC9F,eAAWC,KAAQD;AACjB,eAAS,iBAAiBC,GAAMI,GAAoB,EAAE,MAAM,IAAM,SAAS,IAAM,GACjFf,EAAU,KAAK,MAAM,SAAS,oBAAoBW,GAAMI,CAAkB,CAAC;AAAA,EAE/E;AAEA,SAAOnB;AACT;"}
|
|
1
|
+
{"version":3,"file":"lazy.js","sources":["../src/lazy.ts"],"sourcesContent":["/**\n * Micro entry (`itube-modern-player/lazy`) for bundle-deferred setups.\n *\n * Renders an instant poster placeholder (pure DOM, a couple of KB) and pulls\n * the real player chunk on the first user interaction — the pattern used to\n * be hand-rolled around fluid-player; here it is built in.\n *\n * ```ts\n * import { createLazyPlayer } from 'itube-modern-player/lazy'\n * import 'itube-modern-player/style.css'\n *\n * const lazy = createLazyPlayer('#mount', {\n * source: { src: 'stream.m3u8', poster: 'poster.jpg', title: '…' },\n * loadOn: 'interaction',\n * })\n * lazy.ready.then((player) => player.on('ended', …))\n * ```\n */\nimport { applyIcon } from './core/dom'\nimport type { Player } from './player'\nimport type { PlayerOptions } from './types'\n\nexport interface LazyPlayerOptions extends PlayerOptions {\n /**\n * When to download the player bundle:\n * - `\"interaction\"` (default) — first pointerdown / keydown / touch anywhere on the page;\n * - `\"placeholder\"` — any interaction with the placeholder AREA (hover,\n * pointer press, touch, keyboard focus) — mobile-friendly;\n * - `\"visible\"` — when the placeholder enters the viewport;\n * - `\"immediate\"` — right away (you still get the split chunk + instant placeholder).\n *\n * A click on the placeholder always loads immediately and starts playback.\n * Background triggers (`\"interaction\"` / `\"placeholder\"`) never start\n * playback, even with `playOnInit: true` in the options.\n */\n loadOn?: 'interaction' | 'placeholder' | 'visible' | 'immediate'\n}\n\nexport interface LazyPlayer {\n /** Resolves with the real `Player` once the chunk is loaded and constructed. */\n ready: Promise<Player>\n /** The instance, `null` until loaded. */\n player: Player | null\n /** Placeholder root (removed when the player takes over). */\n el: HTMLElement\n destroy(): void\n}\n\nconst PLAY_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M8 5.14v13.72c0 .8.87 1.3 1.56.88l10.54-6.86a1.05 1.05 0 0 0 0-1.76L9.56 4.26C8.87 3.84 8 4.34 8 5.14Z\"/></svg>'\n\nexport function createLazyPlayer(target: string | HTMLElement, options: LazyPlayerOptions = {}): LazyPlayer {\n const mount = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target\n if (!mount) throw new Error(`[itube-player] mount target not found: ${String(target)}`)\n\n const { loadOn = 'interaction', ...playerOptions } = options\n const first = Array.isArray(playerOptions.source) ? playerOptions.source[0] : playerOptions.source\n\n // SSR-friendly adoption: when the mount ALREADY has content (a server-\n // rendered poster — any markup, its own classes are fine), it becomes the\n // placeholder as-is. No client-side DOM is created, the SSR <img> stays the\n // page's LCP candidate; we only attach the load triggers and swap the\n // content for the player when the bundle arrives.\n const adopted = mount.children.length > 0\n let ph: HTMLElement\n if (adopted) {\n ph = mount\n } else {\n // Placeholder reuses the player's own CSS classes — pixel-identical to the\n // real preview poster, so the swap is invisible.\n ph = document.createElement('div')\n ph.className = 'imp-player imp-player--lazy'\n // Styling parity with the real player: the same knobs paint the placeholder.\n const styling = playerOptions.styling\n if (styling?.themeColor) ph.style.setProperty('--imp-accent', styling.themeColor)\n if (styling?.borderRadius !== undefined) {\n const r = typeof styling.borderRadius === 'number' ? `${styling.borderRadius}px` : styling.borderRadius\n ph.style.setProperty('--imp-radius', r)\n }\n if (styling?.playButtonStyle === 'inverted') ph.classList.add('imp-player--play-inverted')\n if (playerOptions.className) ph.classList.add(playerOptions.className)\n const poster = document.createElement('div')\n poster.className = 'imp-poster'\n // Same div > <img> markup as the real player's PosterOverlay — the swap on\n // load is invisible, and the poster <img> is a proper LCP candidate.\n const image = document.createElement('div')\n image.className = 'imp-poster__image'\n if (first?.poster) {\n const img = document.createElement('img')\n img.className = 'imp-cover-img'\n img.alt = ''\n img.decoding = 'async'\n img.fetchPriority = 'high'\n img.src = first.poster\n image.append(img)\n } else {\n image.hidden = true\n }\n const btn = document.createElement('button')\n btn.type = 'button'\n btn.className = 'imp-btn imp-poster__play'\n btn.setAttribute('aria-label', 'Play')\n // The consumer's custom big-play icon (options.icons.bigPlay) must show on\n // the placeholder too — a default triangle swapping to the custom icon on\n // load reads as a glitch.\n applyIcon(btn, playerOptions.icons?.bigPlay ?? PLAY_SVG)\n poster.append(image, btn)\n ph.append(poster)\n mount.append(ph)\n }\n\n let resolveReady!: (p: Player) => void\n let rejectReady!: (e: unknown) => void\n const ready = new Promise<Player>((resolve, reject) => {\n resolveReady = resolve\n rejectReady = reject\n })\n // Consumers may ignore `ready` entirely — that must not crash the app.\n ready.catch(() => {})\n\n const api: LazyPlayer = {\n ready,\n player: null,\n el: ph,\n destroy() {\n destroyed = true\n cleanup()\n // Adopted mode: the placeholder IS the consumer's mount node (with their\n // SSR markup) — never remove it, just stop managing it.\n if (!adopted) ph.remove()\n api.player?.destroy()\n },\n }\n\n let destroyed = false\n let loading = false\n const disposers: Array<() => void> = []\n const cleanup = () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n\n // Autoplay intent is accumulated, not tied to the call that started the\n // download: a poster click DURING an in-flight background load ('interaction'\n // trigger) must still start playback once the chunk arrives.\n let wantAutoplay = false\n const load = (autoplay: boolean) => {\n wantAutoplay ||= autoplay\n if (loading || destroyed) return\n loading = true\n cleanup()\n // The full entry (not ./player) so built-in locales arrive with the chunk.\n import('./index').then(\n ({ Player: PlayerCtor }) => {\n if (destroyed) return\n // Swap the placeholder for the player: created one is removed whole;\n // an adopted (SSR) mount keeps the node, only its content is cleared.\n if (adopted) mount.textContent = ''\n else ph.remove()\n // 'interaction'/'placeholder' background preload must be INVISIBLE: a\n // consumer-set `playOnInit: true` (or legacy `autoplay`) would otherwise\n // turn \"preload on first interaction\" into \"surprise-play on the first\n // click anywhere on the page\". Play starts only from a real placeholder\n // click (wantAutoplay). Honored as-is for 'immediate' and 'visible'.\n // Both keys are overridden — `playOnInit` wins over the alias in Player.\n const effective = wantAutoplay\n ? { ...playerOptions, playOnInit: true, autoplay: true }\n : loadOn === 'interaction' || loadOn === 'placeholder'\n ? { ...playerOptions, playOnInit: false, autoplay: false }\n : playerOptions\n const player = new PlayerCtor(mount, effective)\n api.player = player\n resolveReady(player)\n },\n (err) => {\n loading = false\n rejectReady(err)\n },\n )\n }\n\n // Click always wins: load + play. NOT in `disposers` — it must survive the\n // load-start cleanup so a click during an in-flight background load still\n // registers the autoplay intent (the listener dies with ph.remove()).\n ph.addEventListener('click', () => load(true))\n\n if (loadOn === 'immediate') {\n load(false)\n } else if (loadOn === 'placeholder') {\n // Any interaction with the placeholder AREA preloads the bundle: hover\n // (desktop), pointer press / touch (mobile), keyboard focus. Playback\n // still starts only from the click (load(true) above).\n const onPh = () => load(false)\n const events: Array<keyof HTMLElementEventMap> = ['pointerenter', 'pointerdown', 'touchstart', 'focusin']\n for (const name of events) {\n ph.addEventListener(name, onPh, { once: true, passive: true })\n disposers.push(() => ph.removeEventListener(name, onPh))\n }\n } else if (loadOn === 'visible' && typeof IntersectionObserver !== 'undefined') {\n const io = new IntersectionObserver((entries) => {\n if (entries.some((e) => e.isIntersecting)) load(false)\n })\n io.observe(ph)\n disposers.push(() => io.disconnect())\n } else {\n const onFirstInteraction = () => load(false)\n const events: Array<keyof DocumentEventMap> = ['pointerdown', 'keydown', 'touchstart', 'wheel']\n for (const name of events) {\n document.addEventListener(name, onFirstInteraction, { once: true, passive: true })\n disposers.push(() => document.removeEventListener(name, onFirstInteraction))\n }\n }\n\n return api\n}\n"],"names":["PLAY_SVG","createLazyPlayer","target","options","mount","loadOn","playerOptions","first","adopted","ph","styling","r","poster","image","img","btn","applyIcon","resolveReady","rejectReady","ready","resolve","reject","api","destroyed","cleanup","loading","disposers","dispose","wantAutoplay","load","autoplay","PlayerCtor","effective","player","err","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":"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"}
|
package/dist/player.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export declare class Player {
|
|
|
37
37
|
private externalAdvance;
|
|
38
38
|
private abort;
|
|
39
39
|
private sources;
|
|
40
|
+
/** Sources detected as broken (poster failed to load) — "next" skips them. */
|
|
41
|
+
private deadSources;
|
|
40
42
|
private currentIndex;
|
|
41
43
|
private sourceController;
|
|
42
44
|
private loadToken;
|
|
@@ -130,9 +132,19 @@ export declare class Player {
|
|
|
130
132
|
get playlist(): VideoSource[];
|
|
131
133
|
get index(): number;
|
|
132
134
|
get source(): VideoSource | null;
|
|
135
|
+
/**
|
|
136
|
+
* Mark a source as broken (e.g. its poster came back 4xx/5xx while building
|
|
137
|
+
* the next-up preview — the video itself won't load either). Every "next"
|
|
138
|
+
* computation (hover preview, end overlay, `next()`, auto-advance) skips
|
|
139
|
+
* dead sources from then on. The playlist panel still lists them.
|
|
140
|
+
*/
|
|
141
|
+
markSourceDead(source: VideoSource): void;
|
|
142
|
+
/** Linear index of the next non-dead item; -1 when nothing playable is ahead. */
|
|
143
|
+
private nextAliveIndex;
|
|
133
144
|
/**
|
|
134
145
|
* The item that `next()` would play in linear order (for the next-up preview).
|
|
135
146
|
* `null` in shuffle mode (the pick is random) or when there is no next item.
|
|
147
|
+
* Skips sources marked dead via `markSourceDead`.
|
|
136
148
|
*/
|
|
137
149
|
get nextSource(): VideoSource | null;
|
|
138
150
|
get hasPlaylist(): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "itube-modern-player",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Lightweight, framework-agnostic HTML5 video player: HLS streams, playlists, chapters, VTT subtitles, sprite previews, VAST ads. Ships with a Vue 3 wrapper.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|