itube-modern-player 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -2
- package/dist/core.cjs +4 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +631 -548
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +22 -23
- package/dist/index.js.map +1 -1
- package/dist/itube-modern-player.iife.js +3 -3
- 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.d.ts +10 -2
- package/dist/lazy.js +56 -63
- package/dist/lazy.js.map +1 -1
- package/dist/placeholder.cjs +2 -0
- package/dist/placeholder.cjs.map +1 -0
- package/dist/placeholder.d.ts +32 -0
- package/dist/placeholder.js +42 -0
- package/dist/placeholder.js.map +1 -0
- package/dist/types.d.ts +19 -2
- package/dist/vue.cjs +1 -1
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.d.ts +68 -5
- package/dist/vue.js +131 -38
- package/dist/vue.js.map +1 -1
- package/package.json +7 -2
- package/dist/dom-Bq7EQenh.cjs +0 -5
- package/dist/dom-Bq7EQenh.cjs.map +0 -1
- package/dist/dom-DrpWbY0y.js +0 -87
- package/dist/dom-DrpWbY0y.js.map +0 -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 C=require("./placeholder.cjs");function S(a,L={}){const s=typeof a=="string"?document.querySelector(a):a;if(!s)throw new Error(`[itube-player] mount target not found: ${String(a)}`);const{loadOn:i="interaction",adConfig:d,...r}=L,O=Array.isArray(r.source)?r.source[0]:r.source,u=s.children.length>0;let n;if(u)n=s;else{const e=document.createElement("div");e.innerHTML=C.renderPlaceholder({poster:O?.poster,icon:r.icons?.bigPlay,styling:r.styling,className:r.className}),n=e.firstElementChild,s.append(n)}let v,h;const P=new Promise((e,o)=>{v=e,h=o});P.catch(()=>{});const f={ready:P,player:null,el:n,destroy(){p=!0,w(),u||n.remove(),f.player?.destroy()}};let p=!1,y=!1;const c=[],w=()=>{for(const e of c.splice(0))e()};let g=!1;const l=e=>{if(g||(g=e),y||p)return;y=!0,w();const o=Promise.resolve(typeof d=="function"?d():d).catch(()=>{});Promise.all([Promise.resolve().then(()=>require("./index.cjs")),o]).then(([{Player:t},b])=>{if(p)return;u?s.textContent="":n.remove();const m=b?{...r,adConfig:b}:r,I=g?{...m,playOnInit:!0,autoplay:!0}:i==="interaction"||i==="placeholder"?{...m,playOnInit:!1,autoplay:!1}:m,E=new t(s,I);f.player=E,v(E)},t=>{y=!1,h(t)})};if(n.addEventListener("click",()=>l(!0)),i==="immediate")l(!1);else if(i==="placeholder"){const e=()=>l(!1),o=["pointerenter","pointerdown","touchstart","focusin"];for(const t of o)n.addEventListener(t,e,{once:!0,passive:!0}),c.push(()=>n.removeEventListener(t,e))}else if(i==="visible"&&typeof IntersectionObserver<"u"){const e=new IntersectionObserver(o=>{o.some(t=>t.isIntersecting)&&l(!1)});e.observe(n),c.push(()=>e.disconnect())}else{const e=()=>l(!1),o=["pointerdown","keydown","touchstart","wheel"];for(const t of o)document.addEventListener(t,e,{once:!0,passive:!0}),c.push(()=>document.removeEventListener(t,e))}return f}exports.createLazyPlayer=S;
|
|
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 { 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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"}
|
|
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 { renderPlaceholder } from './placeholder'\nimport type { Player } from './player'\nimport type { AdsOptions, PlayerOptions } from './types'\n\n/** `adConfig` may be an async factory — resolved in parallel with the chunk download. */\nexport type AdConfigInput = AdsOptions | (() => AdsOptions | undefined | Promise<AdsOptions | undefined>)\n\nexport interface LazyPlayerOptions extends Omit<PlayerOptions, 'adConfig'> {\n /**\n * Ad configuration, or an async factory for it (e.g. a VAST-tag lookup).\n * A factory runs IN PARALLEL with the player-chunk download — no need to\n * orchestrate the promise yourself before constructing.\n */\n adConfig?: AdConfigInput\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\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', adConfig, ...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 // The single source of truth for the markup is `renderPlaceholder`\n // (shared with SSR consumers and the Vue wrapper) — the player's own CSS\n // classes make the swap pixel-invisible, custom bigPlay icon and styling\n // knobs included.\n const tpl = document.createElement('div')\n tpl.innerHTML = renderPlaceholder({\n poster: first?.poster,\n icon: playerOptions.icons?.bigPlay,\n styling: playerOptions.styling,\n className: playerOptions.className,\n })\n ph = tpl.firstElementChild as HTMLElement\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 ad config factory (VAST-tag lookup etc.) runs in parallel with the\n // chunk download — by construction time both are ready.\n const adConfigPromise = Promise.resolve(typeof adConfig === 'function' ? adConfig() : adConfig)\n .catch(() => undefined)\n // The full entry (not ./player) so built-in locales arrive with the chunk.\n Promise.all([import('./index'), adConfigPromise]).then(\n ([{ Player: PlayerCtor }, resolvedAdConfig]) => {\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 withAds: PlayerOptions = resolvedAdConfig\n ? { ...playerOptions, adConfig: resolvedAdConfig }\n : playerOptions\n const effective = wantAutoplay\n ? { ...withAds, playOnInit: true, autoplay: true }\n : loadOn === 'interaction' || loadOn === 'placeholder'\n ? { ...withAds, playOnInit: false, autoplay: false }\n : withAds\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":["createLazyPlayer","target","options","mount","loadOn","adConfig","playerOptions","first","adopted","ph","tpl","renderPlaceholder","resolveReady","rejectReady","ready","resolve","reject","api","destroyed","cleanup","loading","disposers","dispose","wantAutoplay","load","autoplay","adConfigPromise","PlayerCtor","resolvedAdConfig","withAds","effective","player","err","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":"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"}
|
package/dist/lazy.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Player } from './player';
|
|
2
|
-
import { PlayerOptions } from './types';
|
|
3
|
-
|
|
2
|
+
import { AdsOptions, PlayerOptions } from './types';
|
|
3
|
+
/** `adConfig` may be an async factory — resolved in parallel with the chunk download. */
|
|
4
|
+
export type AdConfigInput = AdsOptions | (() => AdsOptions | undefined | Promise<AdsOptions | undefined>);
|
|
5
|
+
export interface LazyPlayerOptions extends Omit<PlayerOptions, 'adConfig'> {
|
|
6
|
+
/**
|
|
7
|
+
* Ad configuration, or an async factory for it (e.g. a VAST-tag lookup).
|
|
8
|
+
* A factory runs IN PARALLEL with the player-chunk download — no need to
|
|
9
|
+
* orchestrate the promise yourself before constructing.
|
|
10
|
+
*/
|
|
11
|
+
adConfig?: AdConfigInput;
|
|
4
12
|
/**
|
|
5
13
|
* When to download the player bundle:
|
|
6
14
|
* - `"interaction"` (default) — first pointerdown / keydown / touch anywhere on the page;
|
package/dist/lazy.js
CHANGED
|
@@ -1,82 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
t = i;
|
|
1
|
+
import { renderPlaceholder as C } from "./placeholder.js";
|
|
2
|
+
function k(l, b = {}) {
|
|
3
|
+
const r = typeof l == "string" ? document.querySelector(l) : l;
|
|
4
|
+
if (!r) throw new Error(`[itube-player] mount target not found: ${String(l)}`);
|
|
5
|
+
const { loadOn: i = "interaction", adConfig: d, ...s } = b, I = Array.isArray(s.source) ? s.source[0] : s.source, f = r.children.length > 0;
|
|
6
|
+
let n;
|
|
7
|
+
if (f)
|
|
8
|
+
n = r;
|
|
10
9
|
else {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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);
|
|
10
|
+
const e = document.createElement("div");
|
|
11
|
+
e.innerHTML = C({
|
|
12
|
+
poster: I?.poster,
|
|
13
|
+
icon: s.icons?.bigPlay,
|
|
14
|
+
styling: s.styling,
|
|
15
|
+
className: s.className
|
|
16
|
+
}), n = e.firstElementChild, r.append(n);
|
|
28
17
|
}
|
|
29
|
-
let
|
|
30
|
-
const
|
|
31
|
-
|
|
18
|
+
let v, h;
|
|
19
|
+
const w = new Promise((e, o) => {
|
|
20
|
+
v = e, h = o;
|
|
32
21
|
});
|
|
33
|
-
|
|
22
|
+
w.catch(() => {
|
|
34
23
|
});
|
|
35
|
-
const
|
|
36
|
-
ready:
|
|
24
|
+
const p = {
|
|
25
|
+
ready: w,
|
|
37
26
|
player: null,
|
|
38
|
-
el:
|
|
27
|
+
el: n,
|
|
39
28
|
destroy() {
|
|
40
|
-
|
|
29
|
+
u = !0, P(), f || n.remove(), p.player?.destroy();
|
|
41
30
|
}
|
|
42
31
|
};
|
|
43
|
-
let
|
|
44
|
-
const
|
|
45
|
-
for (const e of
|
|
32
|
+
let u = !1, y = !1;
|
|
33
|
+
const c = [], P = () => {
|
|
34
|
+
for (const e of c.splice(0)) e();
|
|
46
35
|
};
|
|
47
|
-
let
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
let g = !1;
|
|
37
|
+
const a = (e) => {
|
|
38
|
+
if (g || (g = e), y || u) return;
|
|
39
|
+
y = !0, P();
|
|
40
|
+
const o = Promise.resolve(typeof d == "function" ? d() : d).catch(() => {
|
|
41
|
+
});
|
|
42
|
+
Promise.all([import("./index.js"), o]).then(
|
|
43
|
+
([{ Player: t }, E]) => {
|
|
44
|
+
if (u) return;
|
|
45
|
+
f ? r.textContent = "" : n.remove();
|
|
46
|
+
const m = E ? { ...s, adConfig: E } : s, O = g ? { ...m, playOnInit: !0, autoplay: !0 } : i === "interaction" || i === "placeholder" ? { ...m, playOnInit: !1, autoplay: !1 } : m, L = new t(r, O);
|
|
47
|
+
p.player = L, v(L);
|
|
55
48
|
},
|
|
56
|
-
(
|
|
57
|
-
|
|
49
|
+
(t) => {
|
|
50
|
+
y = !1, h(t);
|
|
58
51
|
}
|
|
59
|
-
)
|
|
52
|
+
);
|
|
60
53
|
};
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
else if (
|
|
64
|
-
const e = () =>
|
|
65
|
-
for (const
|
|
66
|
-
|
|
67
|
-
} else if (
|
|
68
|
-
const e = new IntersectionObserver((
|
|
69
|
-
|
|
54
|
+
if (n.addEventListener("click", () => a(!0)), i === "immediate")
|
|
55
|
+
a(!1);
|
|
56
|
+
else if (i === "placeholder") {
|
|
57
|
+
const e = () => a(!1), o = ["pointerenter", "pointerdown", "touchstart", "focusin"];
|
|
58
|
+
for (const t of o)
|
|
59
|
+
n.addEventListener(t, e, { once: !0, passive: !0 }), c.push(() => n.removeEventListener(t, e));
|
|
60
|
+
} else if (i === "visible" && typeof IntersectionObserver < "u") {
|
|
61
|
+
const e = new IntersectionObserver((o) => {
|
|
62
|
+
o.some((t) => t.isIntersecting) && a(!1);
|
|
70
63
|
});
|
|
71
|
-
e.observe(
|
|
64
|
+
e.observe(n), c.push(() => e.disconnect());
|
|
72
65
|
} else {
|
|
73
|
-
const e = () =>
|
|
74
|
-
for (const
|
|
75
|
-
document.addEventListener(
|
|
66
|
+
const e = () => a(!1), o = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
67
|
+
for (const t of o)
|
|
68
|
+
document.addEventListener(t, e, { once: !0, passive: !0 }), c.push(() => document.removeEventListener(t, e));
|
|
76
69
|
}
|
|
77
|
-
return
|
|
70
|
+
return p;
|
|
78
71
|
}
|
|
79
72
|
export {
|
|
80
|
-
|
|
73
|
+
k as createLazyPlayer
|
|
81
74
|
};
|
|
82
75
|
//# sourceMappingURL=lazy.js.map
|
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 { 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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"}
|
|
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 { renderPlaceholder } from './placeholder'\nimport type { Player } from './player'\nimport type { AdsOptions, PlayerOptions } from './types'\n\n/** `adConfig` may be an async factory — resolved in parallel with the chunk download. */\nexport type AdConfigInput = AdsOptions | (() => AdsOptions | undefined | Promise<AdsOptions | undefined>)\n\nexport interface LazyPlayerOptions extends Omit<PlayerOptions, 'adConfig'> {\n /**\n * Ad configuration, or an async factory for it (e.g. a VAST-tag lookup).\n * A factory runs IN PARALLEL with the player-chunk download — no need to\n * orchestrate the promise yourself before constructing.\n */\n adConfig?: AdConfigInput\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\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', adConfig, ...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 // The single source of truth for the markup is `renderPlaceholder`\n // (shared with SSR consumers and the Vue wrapper) — the player's own CSS\n // classes make the swap pixel-invisible, custom bigPlay icon and styling\n // knobs included.\n const tpl = document.createElement('div')\n tpl.innerHTML = renderPlaceholder({\n poster: first?.poster,\n icon: playerOptions.icons?.bigPlay,\n styling: playerOptions.styling,\n className: playerOptions.className,\n })\n ph = tpl.firstElementChild as HTMLElement\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 ad config factory (VAST-tag lookup etc.) runs in parallel with the\n // chunk download — by construction time both are ready.\n const adConfigPromise = Promise.resolve(typeof adConfig === 'function' ? adConfig() : adConfig)\n .catch(() => undefined)\n // The full entry (not ./player) so built-in locales arrive with the chunk.\n Promise.all([import('./index'), adConfigPromise]).then(\n ([{ Player: PlayerCtor }, resolvedAdConfig]) => {\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 withAds: PlayerOptions = resolvedAdConfig\n ? { ...playerOptions, adConfig: resolvedAdConfig }\n : playerOptions\n const effective = wantAutoplay\n ? { ...withAds, playOnInit: true, autoplay: true }\n : loadOn === 'interaction' || loadOn === 'placeholder'\n ? { ...withAds, playOnInit: false, autoplay: false }\n : withAds\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":["createLazyPlayer","target","options","mount","loadOn","adConfig","playerOptions","first","adopted","ph","tpl","renderPlaceholder","resolveReady","rejectReady","ready","resolve","reject","api","destroyed","cleanup","loading","disposers","dispose","wantAutoplay","load","autoplay","adConfigPromise","PlayerCtor","resolvedAdConfig","withAds","effective","player","err","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h='<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 m(e){if(e)return typeof e=="string"?e:e.src}function r(e){return e.replaceAll("&","&").replaceAll('"',""").replaceAll("<","<").replaceAll(">",">")}function $(e){return typeof e=="object"?"url"in e?`<img class="imp-icon-img" src="${r(e.url)}" alt="">`:e.svg:/^\s*</.test(e)?e:`<img class="imp-icon-img" src="${r(e)}" alt="">`}function v(e={},f={}){const{poster:n,icon:g,styling:i,className:c,playLabel:y="Play"}=e,o=f.inlineStyles===!0,t=typeof n=="string"?{src:n}:n,l=[];if(i?.themeColor&&l.push(`--imp-accent:${r(i.themeColor)}`),i?.borderRadius!==void 0){const p=typeof i.borderRadius=="number"?`${i.borderRadius}px`:i.borderRadius;l.push(`--imp-radius:${r(p)}`)}if(o){const p=t?.width&&t?.height?`${t.width}/${t.height}`:"16/9";l.push("position:relative","width:100%",`aspect-ratio:${p}`,"background:#000","overflow:hidden","cursor:pointer",`border-radius:${i?.borderRadius!==void 0?typeof i.borderRadius=="number"?`${i.borderRadius}px`:r(i.borderRadius):"8px"}`)}const a=["imp-player","imp-player--lazy"];i?.playButtonStyle==="inverted"&&a.push("imp-player--play-inverted"),c&&a.push(c);const s=[];t&&(s.push(`src="${r(t.src)}"`),t.srcset&&s.push(`srcset="${r(t.srcset)}"`),t.sizes&&s.push(`sizes="${r(t.sizes)}"`),t.width&&s.push(`width="${t.width}"`),t.height&&s.push(`height="${t.height}"`),s.push(`alt="${r(t.alt??"")}"`),s.push('decoding="async"','fetchpriority="high"'),o&&s.push('style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover"'));const d=i?.themeColor??"#e53935",b=i?.playButtonStyle==="inverted",u=o?`style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:84px;height:84px;border:0;border-radius:50%;padding:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;${b?`background:rgba(255,255,255,.92);color:${r(d)}`:`background:${r(d)};color:#fff`}"`:"";return`<div class="${a.join(" ")}"${l.length?` style="${l.join(";")}"`:""}><div class="imp-poster"${o?' style="position:absolute;inset:0"':""}><div class="imp-poster__image"${t?"":" hidden"}${o?' style="position:absolute;inset:0;overflow:hidden"':""}>`+(t?`<img class="imp-cover-img" ${s.join(" ")}>`:"")+`</div><button type="button" class="imp-btn imp-poster__play" aria-label="${r(y)}"${u?` ${u}`:""}>`+$(g??h)+"</button></div></div>"}exports.DEFAULT_PLAY_SVG=h;exports.posterSrc=m;exports.renderPlaceholder=v;
|
|
2
|
+
//# sourceMappingURL=placeholder.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholder.cjs","sources":["../src/placeholder.ts"],"sourcesContent":["/**\n * Isomorphic placeholder template (`itube-modern-player/placeholder`).\n *\n * A pure string builder with ZERO DOM dependencies — safe to call on the\n * server (Nuxt/Next/any SSR) and in the browser. It is the single source of\n * truth for the \"poster + play button\" markup: `createLazyPlayer` renders it\n * client-side, the Vue wrapper's `lazy` mode renders it in its template, and\n * SSR consumers can call it directly to ship the poster in the first HTML.\n */\nimport type { IconSource, PosterSource, StylingOptions } from './types'\n\n/** Default big-play triangle (same path as the player's `bigPlay` icon). */\nexport const DEFAULT_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\n/** Normalize a `PosterSource` to its plain URL (undefined when absent). */\nexport function posterSrc(poster: PosterSource | undefined | null): string | undefined {\n if (!poster) return undefined\n return typeof poster === 'string' ? poster : poster.src\n}\n\n/** Escape a string for use inside a double-quoted HTML attribute. */\nfunction esc(value: string): string {\n return value.replaceAll('&', '&').replaceAll('\"', '"').replaceAll('<', '<').replaceAll('>', '>')\n}\n\n/** Icon markup: raw SVG passes through, a URL becomes an `<img>`. */\nfunction iconMarkup(icon: IconSource): string {\n if (typeof icon === 'object') {\n return 'url' in icon\n ? `<img class=\"imp-icon-img\" src=\"${esc(icon.url)}\" alt=\"\">`\n : icon.svg\n }\n return /^\\s*</.test(icon) ? icon : `<img class=\"imp-icon-img\" src=\"${esc(icon)}\" alt=\"\">`\n}\n\nexport interface PlaceholderOptions {\n /** Poster URL or full `<img>` descriptor (srcset/sizes/width/height/alt). */\n poster?: PosterSource\n /** Big-play icon (defaults to the player's built-in triangle). */\n icon?: IconSource\n /** The same styling knobs the player takes — the placeholder matches it. */\n styling?: StylingOptions\n /** Extra class on the root (mirrors `PlayerOptions.className`). */\n className?: string\n /** aria-label of the play button. Default `\"Play\"`. */\n playLabel?: string\n}\n\nexport interface PlaceholderRenderFlags {\n /**\n * Emit self-contained inline styles so the placeholder looks right WITHOUT\n * `style.css` (SSR-above-the-fold / Vue `lazy` mode). Off by default —\n * `createLazyPlayer` relies on the player CSS classes for pixel parity\n * (including responsive media queries, which inline styles would break).\n */\n inlineStyles?: boolean\n}\n\n/**\n * Render the placeholder HTML string: poster `<img>` (LCP-grade attributes)\n * + centered play button, wrapped in the same classes the live player uses,\n * so the swap on load is invisible once `style.css` is present.\n */\nexport function renderPlaceholder(options: PlaceholderOptions = {}, flags: PlaceholderRenderFlags = {}): string {\n const { poster, icon, styling, className, playLabel = 'Play' } = options\n const inline = flags.inlineStyles === true\n const p = typeof poster === 'string' ? { src: poster } : poster\n\n // Root style: consumer styling knobs (always) + self-contained layout (inline mode).\n const rootStyles: string[] = []\n if (styling?.themeColor) rootStyles.push(`--imp-accent:${esc(styling.themeColor)}`)\n if (styling?.borderRadius !== undefined) {\n const r = typeof styling.borderRadius === 'number' ? `${styling.borderRadius}px` : styling.borderRadius\n rootStyles.push(`--imp-radius:${esc(r)}`)\n }\n if (inline) {\n const ratio = p?.width && p?.height ? `${p.width}/${p.height}` : '16/9'\n rootStyles.push(\n 'position:relative', 'width:100%', `aspect-ratio:${ratio}`, 'background:#000',\n 'overflow:hidden', 'cursor:pointer',\n `border-radius:${styling?.borderRadius !== undefined ? (typeof styling.borderRadius === 'number' ? `${styling.borderRadius}px` : esc(styling.borderRadius)) : '8px'}`,\n )\n }\n\n const rootClass = ['imp-player', 'imp-player--lazy']\n if (styling?.playButtonStyle === 'inverted') rootClass.push('imp-player--play-inverted')\n if (className) rootClass.push(className)\n\n const imgAttrs: string[] = []\n if (p) {\n imgAttrs.push(`src=\"${esc(p.src)}\"`)\n if (p.srcset) imgAttrs.push(`srcset=\"${esc(p.srcset)}\"`)\n if (p.sizes) imgAttrs.push(`sizes=\"${esc(p.sizes)}\"`)\n if (p.width) imgAttrs.push(`width=\"${p.width}\"`)\n if (p.height) imgAttrs.push(`height=\"${p.height}\"`)\n imgAttrs.push(`alt=\"${esc(p.alt ?? '')}\"`)\n imgAttrs.push('decoding=\"async\"', 'fetchpriority=\"high\"')\n if (inline) imgAttrs.push('style=\"position:absolute;inset:0;width:100%;height:100%;object-fit:cover\"')\n }\n\n // Play button: accent circle (or inverted — translucent white, accent icon).\n const accent = styling?.themeColor ?? '#e53935'\n const inverted = styling?.playButtonStyle === 'inverted'\n const btnStyle = inline\n ? `style=\"position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:84px;height:84px;border:0;border-radius:50%;padding:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;${\n inverted ? `background:rgba(255,255,255,.92);color:${esc(accent)}` : `background:${esc(accent)};color:#fff`\n }\"`\n : ''\n\n return (\n `<div class=\"${rootClass.join(' ')}\"${rootStyles.length ? ` style=\"${rootStyles.join(';')}\"` : ''}>` +\n `<div class=\"imp-poster\"${inline ? ' style=\"position:absolute;inset:0\"' : ''}>` +\n `<div class=\"imp-poster__image\"${p ? '' : ' hidden'}${inline ? ' style=\"position:absolute;inset:0;overflow:hidden\"' : ''}>` +\n (p ? `<img class=\"imp-cover-img\" ${imgAttrs.join(' ')}>` : '') +\n `</div>` +\n `<button type=\"button\" class=\"imp-btn imp-poster__play\" aria-label=\"${esc(playLabel)}\"${btnStyle ? ` ${btnStyle}` : ''}>` +\n iconMarkup(icon ?? DEFAULT_PLAY_SVG) +\n `</button>` +\n `</div>` +\n `</div>`\n )\n}\n"],"names":["DEFAULT_PLAY_SVG","posterSrc","poster","esc","value","iconMarkup","icon","renderPlaceholder","options","flags","styling","className","playLabel","inline","p","rootStyles","r","ratio","rootClass","imgAttrs","accent","inverted","btnStyle"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { IconSource, PosterSource, StylingOptions } from './types';
|
|
2
|
+
/** Default big-play triangle (same path as the player's `bigPlay` icon). */
|
|
3
|
+
export declare const DEFAULT_PLAY_SVG = "<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>";
|
|
4
|
+
/** Normalize a `PosterSource` to its plain URL (undefined when absent). */
|
|
5
|
+
export declare function posterSrc(poster: PosterSource | undefined | null): string | undefined;
|
|
6
|
+
export interface PlaceholderOptions {
|
|
7
|
+
/** Poster URL or full `<img>` descriptor (srcset/sizes/width/height/alt). */
|
|
8
|
+
poster?: PosterSource;
|
|
9
|
+
/** Big-play icon (defaults to the player's built-in triangle). */
|
|
10
|
+
icon?: IconSource;
|
|
11
|
+
/** The same styling knobs the player takes — the placeholder matches it. */
|
|
12
|
+
styling?: StylingOptions;
|
|
13
|
+
/** Extra class on the root (mirrors `PlayerOptions.className`). */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** aria-label of the play button. Default `"Play"`. */
|
|
16
|
+
playLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PlaceholderRenderFlags {
|
|
19
|
+
/**
|
|
20
|
+
* Emit self-contained inline styles so the placeholder looks right WITHOUT
|
|
21
|
+
* `style.css` (SSR-above-the-fold / Vue `lazy` mode). Off by default —
|
|
22
|
+
* `createLazyPlayer` relies on the player CSS classes for pixel parity
|
|
23
|
+
* (including responsive media queries, which inline styles would break).
|
|
24
|
+
*/
|
|
25
|
+
inlineStyles?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render the placeholder HTML string: poster `<img>` (LCP-grade attributes)
|
|
29
|
+
* + centered play button, wrapped in the same classes the live player uses,
|
|
30
|
+
* so the swap on load is invisible once `style.css` is present.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderPlaceholder(options?: PlaceholderOptions, flags?: PlaceholderRenderFlags): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const b = '<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 $(e) {
|
|
3
|
+
if (e)
|
|
4
|
+
return typeof e == "string" ? e : e.src;
|
|
5
|
+
}
|
|
6
|
+
function s(e) {
|
|
7
|
+
return e.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
8
|
+
}
|
|
9
|
+
function m(e) {
|
|
10
|
+
return typeof e == "object" ? "url" in e ? `<img class="imp-icon-img" src="${s(e.url)}" alt="">` : e.svg : /^\s*</.test(e) ? e : `<img class="imp-icon-img" src="${s(e)}" alt="">`;
|
|
11
|
+
}
|
|
12
|
+
function v(e = {}, h = {}) {
|
|
13
|
+
const { poster: l, icon: f, styling: i, className: c, playLabel: g = "Play" } = e, o = h.inlineStyles === !0, t = typeof l == "string" ? { src: l } : l, n = [];
|
|
14
|
+
if (i?.themeColor && n.push(`--imp-accent:${s(i.themeColor)}`), i?.borderRadius !== void 0) {
|
|
15
|
+
const p = typeof i.borderRadius == "number" ? `${i.borderRadius}px` : i.borderRadius;
|
|
16
|
+
n.push(`--imp-radius:${s(p)}`);
|
|
17
|
+
}
|
|
18
|
+
if (o) {
|
|
19
|
+
const p = t?.width && t?.height ? `${t.width}/${t.height}` : "16/9";
|
|
20
|
+
n.push(
|
|
21
|
+
"position:relative",
|
|
22
|
+
"width:100%",
|
|
23
|
+
`aspect-ratio:${p}`,
|
|
24
|
+
"background:#000",
|
|
25
|
+
"overflow:hidden",
|
|
26
|
+
"cursor:pointer",
|
|
27
|
+
`border-radius:${i?.borderRadius !== void 0 ? typeof i.borderRadius == "number" ? `${i.borderRadius}px` : s(i.borderRadius) : "8px"}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const a = ["imp-player", "imp-player--lazy"];
|
|
31
|
+
i?.playButtonStyle === "inverted" && a.push("imp-player--play-inverted"), c && a.push(c);
|
|
32
|
+
const r = [];
|
|
33
|
+
t && (r.push(`src="${s(t.src)}"`), t.srcset && r.push(`srcset="${s(t.srcset)}"`), t.sizes && r.push(`sizes="${s(t.sizes)}"`), t.width && r.push(`width="${t.width}"`), t.height && r.push(`height="${t.height}"`), r.push(`alt="${s(t.alt ?? "")}"`), r.push('decoding="async"', 'fetchpriority="high"'), o && r.push('style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover"'));
|
|
34
|
+
const d = i?.themeColor ?? "#e53935", y = i?.playButtonStyle === "inverted", u = o ? `style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:84px;height:84px;border:0;border-radius:50%;padding:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;${y ? `background:rgba(255,255,255,.92);color:${s(d)}` : `background:${s(d)};color:#fff`}"` : "";
|
|
35
|
+
return `<div class="${a.join(" ")}"${n.length ? ` style="${n.join(";")}"` : ""}><div class="imp-poster"${o ? ' style="position:absolute;inset:0"' : ""}><div class="imp-poster__image"${t ? "" : " hidden"}${o ? ' style="position:absolute;inset:0;overflow:hidden"' : ""}>` + (t ? `<img class="imp-cover-img" ${r.join(" ")}>` : "") + `</div><button type="button" class="imp-btn imp-poster__play" aria-label="${s(g)}"${u ? ` ${u}` : ""}>` + m(f ?? b) + "</button></div></div>";
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
b as DEFAULT_PLAY_SVG,
|
|
39
|
+
$ as posterSrc,
|
|
40
|
+
v as renderPlaceholder
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=placeholder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholder.js","sources":["../src/placeholder.ts"],"sourcesContent":["/**\n * Isomorphic placeholder template (`itube-modern-player/placeholder`).\n *\n * A pure string builder with ZERO DOM dependencies — safe to call on the\n * server (Nuxt/Next/any SSR) and in the browser. It is the single source of\n * truth for the \"poster + play button\" markup: `createLazyPlayer` renders it\n * client-side, the Vue wrapper's `lazy` mode renders it in its template, and\n * SSR consumers can call it directly to ship the poster in the first HTML.\n */\nimport type { IconSource, PosterSource, StylingOptions } from './types'\n\n/** Default big-play triangle (same path as the player's `bigPlay` icon). */\nexport const DEFAULT_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\n/** Normalize a `PosterSource` to its plain URL (undefined when absent). */\nexport function posterSrc(poster: PosterSource | undefined | null): string | undefined {\n if (!poster) return undefined\n return typeof poster === 'string' ? poster : poster.src\n}\n\n/** Escape a string for use inside a double-quoted HTML attribute. */\nfunction esc(value: string): string {\n return value.replaceAll('&', '&').replaceAll('\"', '"').replaceAll('<', '<').replaceAll('>', '>')\n}\n\n/** Icon markup: raw SVG passes through, a URL becomes an `<img>`. */\nfunction iconMarkup(icon: IconSource): string {\n if (typeof icon === 'object') {\n return 'url' in icon\n ? `<img class=\"imp-icon-img\" src=\"${esc(icon.url)}\" alt=\"\">`\n : icon.svg\n }\n return /^\\s*</.test(icon) ? icon : `<img class=\"imp-icon-img\" src=\"${esc(icon)}\" alt=\"\">`\n}\n\nexport interface PlaceholderOptions {\n /** Poster URL or full `<img>` descriptor (srcset/sizes/width/height/alt). */\n poster?: PosterSource\n /** Big-play icon (defaults to the player's built-in triangle). */\n icon?: IconSource\n /** The same styling knobs the player takes — the placeholder matches it. */\n styling?: StylingOptions\n /** Extra class on the root (mirrors `PlayerOptions.className`). */\n className?: string\n /** aria-label of the play button. Default `\"Play\"`. */\n playLabel?: string\n}\n\nexport interface PlaceholderRenderFlags {\n /**\n * Emit self-contained inline styles so the placeholder looks right WITHOUT\n * `style.css` (SSR-above-the-fold / Vue `lazy` mode). Off by default —\n * `createLazyPlayer` relies on the player CSS classes for pixel parity\n * (including responsive media queries, which inline styles would break).\n */\n inlineStyles?: boolean\n}\n\n/**\n * Render the placeholder HTML string: poster `<img>` (LCP-grade attributes)\n * + centered play button, wrapped in the same classes the live player uses,\n * so the swap on load is invisible once `style.css` is present.\n */\nexport function renderPlaceholder(options: PlaceholderOptions = {}, flags: PlaceholderRenderFlags = {}): string {\n const { poster, icon, styling, className, playLabel = 'Play' } = options\n const inline = flags.inlineStyles === true\n const p = typeof poster === 'string' ? { src: poster } : poster\n\n // Root style: consumer styling knobs (always) + self-contained layout (inline mode).\n const rootStyles: string[] = []\n if (styling?.themeColor) rootStyles.push(`--imp-accent:${esc(styling.themeColor)}`)\n if (styling?.borderRadius !== undefined) {\n const r = typeof styling.borderRadius === 'number' ? `${styling.borderRadius}px` : styling.borderRadius\n rootStyles.push(`--imp-radius:${esc(r)}`)\n }\n if (inline) {\n const ratio = p?.width && p?.height ? `${p.width}/${p.height}` : '16/9'\n rootStyles.push(\n 'position:relative', 'width:100%', `aspect-ratio:${ratio}`, 'background:#000',\n 'overflow:hidden', 'cursor:pointer',\n `border-radius:${styling?.borderRadius !== undefined ? (typeof styling.borderRadius === 'number' ? `${styling.borderRadius}px` : esc(styling.borderRadius)) : '8px'}`,\n )\n }\n\n const rootClass = ['imp-player', 'imp-player--lazy']\n if (styling?.playButtonStyle === 'inverted') rootClass.push('imp-player--play-inverted')\n if (className) rootClass.push(className)\n\n const imgAttrs: string[] = []\n if (p) {\n imgAttrs.push(`src=\"${esc(p.src)}\"`)\n if (p.srcset) imgAttrs.push(`srcset=\"${esc(p.srcset)}\"`)\n if (p.sizes) imgAttrs.push(`sizes=\"${esc(p.sizes)}\"`)\n if (p.width) imgAttrs.push(`width=\"${p.width}\"`)\n if (p.height) imgAttrs.push(`height=\"${p.height}\"`)\n imgAttrs.push(`alt=\"${esc(p.alt ?? '')}\"`)\n imgAttrs.push('decoding=\"async\"', 'fetchpriority=\"high\"')\n if (inline) imgAttrs.push('style=\"position:absolute;inset:0;width:100%;height:100%;object-fit:cover\"')\n }\n\n // Play button: accent circle (or inverted — translucent white, accent icon).\n const accent = styling?.themeColor ?? '#e53935'\n const inverted = styling?.playButtonStyle === 'inverted'\n const btnStyle = inline\n ? `style=\"position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:84px;height:84px;border:0;border-radius:50%;padding:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;${\n inverted ? `background:rgba(255,255,255,.92);color:${esc(accent)}` : `background:${esc(accent)};color:#fff`\n }\"`\n : ''\n\n return (\n `<div class=\"${rootClass.join(' ')}\"${rootStyles.length ? ` style=\"${rootStyles.join(';')}\"` : ''}>` +\n `<div class=\"imp-poster\"${inline ? ' style=\"position:absolute;inset:0\"' : ''}>` +\n `<div class=\"imp-poster__image\"${p ? '' : ' hidden'}${inline ? ' style=\"position:absolute;inset:0;overflow:hidden\"' : ''}>` +\n (p ? `<img class=\"imp-cover-img\" ${imgAttrs.join(' ')}>` : '') +\n `</div>` +\n `<button type=\"button\" class=\"imp-btn imp-poster__play\" aria-label=\"${esc(playLabel)}\"${btnStyle ? ` ${btnStyle}` : ''}>` +\n iconMarkup(icon ?? DEFAULT_PLAY_SVG) +\n `</button>` +\n `</div>` +\n `</div>`\n )\n}\n"],"names":["DEFAULT_PLAY_SVG","posterSrc","poster","esc","value","iconMarkup","icon","renderPlaceholder","options","flags","styling","className","playLabel","inline","p","rootStyles","r","ratio","rootClass","imgAttrs","accent","inverted","btnStyle"],"mappings":"AAYO,MAAMA,IACX;AAGK,SAASC,EAAUC,GAA6D;AACrF,MAAKA;AACL,WAAO,OAAOA,KAAW,WAAWA,IAASA,EAAO;AACtD;AAGA,SAASC,EAAIC,GAAuB;AAClC,SAAOA,EAAM,WAAW,KAAK,OAAO,EAAE,WAAW,KAAK,QAAQ,EAAE,WAAW,KAAK,MAAM,EAAE,WAAW,KAAK,MAAM;AAChH;AAGA,SAASC,EAAWC,GAA0B;AAC5C,SAAI,OAAOA,KAAS,WACX,SAASA,IACZ,kCAAkCH,EAAIG,EAAK,GAAG,CAAC,cAC/CA,EAAK,MAEJ,QAAQ,KAAKA,CAAI,IAAIA,IAAO,kCAAkCH,EAAIG,CAAI,CAAC;AAChF;AA8BO,SAASC,EAAkBC,IAA8B,IAAIC,IAAgC,CAAA,GAAY;AAC9G,QAAM,EAAE,QAAAP,GAAQ,MAAAI,GAAM,SAAAI,GAAS,WAAAC,GAAW,WAAAC,IAAY,WAAWJ,GAC3DK,IAASJ,EAAM,iBAAiB,IAChCK,IAAI,OAAOZ,KAAW,WAAW,EAAE,KAAKA,MAAWA,GAGnDa,IAAuB,CAAA;AAE7B,MADIL,GAAS,cAAYK,EAAW,KAAK,gBAAgBZ,EAAIO,EAAQ,UAAU,CAAC,EAAE,GAC9EA,GAAS,iBAAiB,QAAW;AACvC,UAAMM,IAAI,OAAON,EAAQ,gBAAiB,WAAW,GAAGA,EAAQ,YAAY,OAAOA,EAAQ;AAC3F,IAAAK,EAAW,KAAK,gBAAgBZ,EAAIa,CAAC,CAAC,EAAE;AAAA,EAC1C;AACA,MAAIH,GAAQ;AACV,UAAMI,IAAQH,GAAG,SAASA,GAAG,SAAS,GAAGA,EAAE,KAAK,IAAIA,EAAE,MAAM,KAAK;AACjE,IAAAC,EAAW;AAAA,MACT;AAAA,MAAqB;AAAA,MAAc,gBAAgBE,CAAK;AAAA,MAAI;AAAA,MAC5D;AAAA,MAAmB;AAAA,MACnB,iBAAiBP,GAAS,iBAAiB,SAAa,OAAOA,EAAQ,gBAAiB,WAAW,GAAGA,EAAQ,YAAY,OAAOP,EAAIO,EAAQ,YAAY,IAAK,KAAK;AAAA,IAAA;AAAA,EAEvK;AAEA,QAAMQ,IAAY,CAAC,cAAc,kBAAkB;AACnD,EAAIR,GAAS,oBAAoB,cAAYQ,EAAU,KAAK,2BAA2B,GACnFP,KAAWO,EAAU,KAAKP,CAAS;AAEvC,QAAMQ,IAAqB,CAAA;AAC3B,EAAIL,MACFK,EAAS,KAAK,QAAQhB,EAAIW,EAAE,GAAG,CAAC,GAAG,GAC/BA,EAAE,UAAQK,EAAS,KAAK,WAAWhB,EAAIW,EAAE,MAAM,CAAC,GAAG,GACnDA,EAAE,SAAOK,EAAS,KAAK,UAAUhB,EAAIW,EAAE,KAAK,CAAC,GAAG,GAChDA,EAAE,SAAOK,EAAS,KAAK,UAAUL,EAAE,KAAK,GAAG,GAC3CA,EAAE,UAAQK,EAAS,KAAK,WAAWL,EAAE,MAAM,GAAG,GAClDK,EAAS,KAAK,QAAQhB,EAAIW,EAAE,OAAO,EAAE,CAAC,GAAG,GACzCK,EAAS,KAAK,oBAAoB,sBAAsB,GACpDN,KAAQM,EAAS,KAAK,2EAA2E;AAIvG,QAAMC,IAASV,GAAS,cAAc,WAChCW,IAAWX,GAAS,oBAAoB,YACxCY,IAAWT,IACb,iNACEQ,IAAW,0CAA0ClB,EAAIiB,CAAM,CAAC,KAAK,cAAcjB,EAAIiB,CAAM,CAAC,aAChG,MACA;AAEJ,SACE,eAAeF,EAAU,KAAK,GAAG,CAAC,IAAIH,EAAW,SAAS,WAAWA,EAAW,KAAK,GAAG,CAAC,MAAM,EAAE,2BACvEF,IAAS,uCAAuC,EAAE,kCAC3CC,IAAI,KAAK,SAAS,GAAGD,IAAS,uDAAuD,EAAE,OACvHC,IAAI,8BAA8BK,EAAS,KAAK,GAAG,CAAC,MAAM,MAC3D,4EACsEhB,EAAIS,CAAS,CAAC,IAAIU,IAAW,IAAIA,CAAQ,KAAK,EAAE,MACtHjB,EAAWC,KAAQN,CAAgB,IACnC;AAIJ;"}
|
package/dist/types.d.ts
CHANGED
|
@@ -104,6 +104,20 @@ export type PreviewMetaItem = string | {
|
|
|
104
104
|
text: string;
|
|
105
105
|
icon?: IconSource;
|
|
106
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Poster: a plain URL, or a full `<img>` descriptor for LCP-grade rendering
|
|
109
|
+
* (responsive `srcset`/`sizes`, intrinsic `width`/`height` against layout
|
|
110
|
+
* shift, `alt`). The descriptor is used wherever the poster renders as the
|
|
111
|
+
* hero image (player poster, lazy/SSR placeholder); thumbnails use `src`.
|
|
112
|
+
*/
|
|
113
|
+
export type PosterSource = string | {
|
|
114
|
+
src: string;
|
|
115
|
+
srcset?: string;
|
|
116
|
+
sizes?: string;
|
|
117
|
+
width?: number;
|
|
118
|
+
height?: number;
|
|
119
|
+
alt?: string;
|
|
120
|
+
};
|
|
107
121
|
/** Everything the player knows about one video. */
|
|
108
122
|
export interface VideoSource {
|
|
109
123
|
/** Media URL. `.m3u8` is detected automatically and routed through hls.js. */
|
|
@@ -112,8 +126,11 @@ export interface VideoSource {
|
|
|
112
126
|
type?: string;
|
|
113
127
|
title?: string;
|
|
114
128
|
description?: string;
|
|
115
|
-
/**
|
|
116
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Poster image, also used as the preview poster before the first play.
|
|
131
|
+
* A plain URL or a full descriptor (`{ src, srcset, sizes, width, height, alt }`).
|
|
132
|
+
*/
|
|
133
|
+
poster?: PosterSource;
|
|
117
134
|
/** Duration in seconds — used in playlist UI before metadata is loaded. */
|
|
118
135
|
duration?: number;
|
|
119
136
|
/** URL of a WebVTT file describing preview thumbnails (sprite via `#xywh=`). */
|
package/dist/vue.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const
|
|
1
|
+
"use strict";var M=Object.create;var k=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var N=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var B=(e,a,s,i)=>{if(a&&typeof a=="object"||typeof a=="function")for(let r of z(a))!_.call(e,r)&&r!==s&&k(e,r,{get:()=>a[r],enumerable:!(i=j(a,r))||i.enumerable});return e};var H=(e,a,s)=>(s=e!=null?M(N(e)):{},B(a||!e||!e.__esModule?k(s,"default",{value:e,enumerable:!0}):s,e));Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const n=require("vue"),R=require("./placeholder.cjs"),E=["ready","play","pause","ended","timeupdate","progress","volumechange","ratechange","seeking","seeked","sourcechange","playlistitemchange","advancerequest","chapterchange","scenetypechange","fullscreenchange","pipchange","qualitychange","subtitlechange","relatedshow","relatedclick","action","customaction","adstart","adend","adskip","adclick","adpause","adresume","aderror","error","destroy"];function F(){return import("itube-modern-player/style.css").catch(()=>{})}const C=n.defineComponent({name:"ITubePlayer",props:{source:{type:[Object,Array],required:!1,default:void 0},options:{type:Object,default:()=>({})},lazy:{type:Boolean,default:!1},loadOn:{type:String,default:"placeholder"},adConfig:{type:[Object,Function],default:void 0}},emits:[...E],setup(e,{emit:a,slots:s,expose:i}){const r=n.ref(null),S=n.ref(null),p=n.ref(null),u=n.shallowRef(null),P=n.ref(!1);let y=!1,h=!1,m=!1;const d=[],w=()=>{for(const t of d.splice(0))t()},I=n.computed(()=>Array.isArray(e.source)?e.source[0]:e.source),T=n.computed(()=>R.renderPlaceholder({poster:I.value?.poster,icon:e.options.icons?.bigPlay,styling:e.options.styling,className:e.options.className},{inlineStyles:!0})),f=t=>{if(m||(m=t),h||y)return;h=!0,w();const l=e.adConfig??e.options.adConfig,c=Promise.resolve(typeof l=="function"?l():l).catch(()=>{});Promise.all([Promise.resolve().then(()=>require("./index.cjs")),c,F()]).then(([{Player:o},v])=>{if(y||!r.value)return;const g={...e.options,source:e.source,...v?{adConfig:v}:{},...s.pauseScreen?{pauseScreen:!0}:{}};e.lazy&&(g.playOnInit=m,g.autoplay=m);const b=new o(r.value,g),A=a;for(const O of E)b.on(O,L=>A(O,L));s.pauseScreen&&p.value&&b.setPauseScreenContent(p.value),u.value=b,P.value=!0},o=>{h=!1,console.error("[itube-player] failed to load the player chunk",o)})};n.onMounted(()=>{if(!e.lazy){f(!1);return}const t=S.value;if(e.loadOn==="immediate"||!t){f(!1);return}const l=()=>f(!1);if(e.loadOn==="interaction"){const c=["pointerdown","keydown","touchstart","wheel"];for(const o of c)document.addEventListener(o,l,{once:!0,passive:!0}),d.push(()=>document.removeEventListener(o,l))}else if(e.loadOn==="visible"&&typeof IntersectionObserver<"u"){const c=new IntersectionObserver(o=>{o.some(v=>v.isIntersecting)&&l()});c.observe(t),d.push(()=>c.disconnect())}else{const c=["pointerenter","pointerdown","touchstart","focusin"];for(const o of c)t.addEventListener(o,l,{once:!0,passive:!0}),d.push(()=>t.removeEventListener(o,l))}}),n.watch(()=>e.source,t=>{t&&u.value&&u.value.load(t)},{deep:!0}),n.onBeforeUnmount(()=>{y=!0,w(),u.value?.destroy(),u.value=null}),i({player:u});const q=()=>{u.value?.play()};return()=>{const t=[];return e.lazy&&!P.value&&t.push(n.h("div",{key:"imp-ph",ref:S,class:"imp-vue-placeholder",innerHTML:T.value,onClick:()=>f(!0)})),t.push(n.h("div",{key:"imp-mount",ref:r})),s.pauseScreen&&t.push(n.h("div",{ref:p,class:"imp-vue-pause-slot"},s.pauseScreen({player:u.value,close:q}))),n.h("div",{class:"imp-vue-host"},t)}}});exports.ITubePlayer=C;exports.default=C;
|
|
2
2
|
//# sourceMappingURL=vue.cjs.map
|
package/dist/vue.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.cjs","sources":["../src/vue.ts"],"sourcesContent":["/**\n * Vue 3 wrapper — a separate entry (`itube-modern-player/vue`) so that plain\n * JS consumers never pull Vue into their graph.\n *\n * ```vue\n * <ITubePlayer :source=\"video\" :options=\"{ seekStep: 5 }\" @ended=\"onEnded\">\n * <template #pauseScreen>\n * <MyPromo />\n * </template>\n * </ITubePlayer>\n * ```\n */\nimport {\n defineComponent,\n h,\n onBeforeUnmount,\n onMounted,\n ref,\n shallowRef,\n watch,\n type PropType,\n} from 'vue'\n// Import via the main entry so the built-in locales are registered for Vue users too.\nimport { Player } from './index'\nimport type { PlayerEventMap, PlayerOptions, VideoSource } from './types'\n\nconst EVENT_NAMES = [\n 'ready', 'play', 'pause', 'ended', 'timeupdate', 'progress', 'volumechange',\n 'ratechange', 'seeking', 'seeked', 'sourcechange', 'playlistitemchange', 'advancerequest',\n 'chapterchange', 'scenetypechange', 'fullscreenchange', 'pipchange', 'qualitychange',\n 'subtitlechange', 'relatedshow', 'relatedclick', 'action', 'customaction',\n 'adstart', 'adend', 'adskip', 'adclick', 'adpause', 'adresume', 'aderror', 'error', 'destroy',\n] as const satisfies ReadonlyArray<keyof PlayerEventMap>\n\nexport const ITubePlayer = defineComponent({\n name: 'ITubePlayer',\n props: {\n /** Single video or a playlist array. */\n source: {\n type: [Object, Array] as PropType<VideoSource | VideoSource[]>,\n required: false,\n default: undefined,\n },\n /** Everything else from PlayerOptions (source inside is ignored in favor of the prop). */\n options: {\n type: Object as PropType<Omit<PlayerOptions, 'source'>>,\n default: () => ({}),\n },\n },\n emits: [...EVENT_NAMES] as unknown as Record<(typeof EVENT_NAMES)[number], (payload: unknown) => boolean>,\n setup(props, { emit, slots, expose }) {\n const mountEl = ref<HTMLElement | null>(null)\n const slotEl = ref<HTMLElement | null>(null)\n const player = shallowRef<Player | null>(null)\n\n onMounted(() => {\n if (!mountEl.value) return\n const instance = new Player(mountEl.value, {\n ...props.options,\n source: props.source,\n // The slot wins over whatever was passed in options.\n ...(slots.pauseScreen ? { pauseScreen: true } : {}),\n })\n const forward = emit as (event: string, payload?: unknown) => void\n for (const name of EVENT_NAMES) {\n instance.on(name, (payload) => forward(name, payload))\n }\n if (slots.pauseScreen && slotEl.value) {\n instance.setPauseScreenContent(slotEl.value)\n }\n player.value = instance\n })\n\n watch(\n () => props.source,\n (next) => {\n if (next && player.value) player.value.load(next)\n },\n { deep: true },\n )\n\n onBeforeUnmount(() => {\n player.value?.destroy()\n player.value = null\n })\n\n expose({ player })\n\n // \"Close & continue\" — resume playback; the player hides the pause screen\n // on the `play` event, so this both closes the custom block and plays.\n const close = () => {\n void player.value?.play()\n }\n\n return () =>\n h('div', { class: 'imp-vue-host' }, [\n h('div', { ref: mountEl }),\n // Scoped slot: `player` (reactive — null until mounted, then the\n // instance) and `close()` are available to the custom pause content.\n slots.pauseScreen\n ? h('div', { ref: slotEl, class: 'imp-vue-pause-slot' }, slots.pauseScreen({ player: player.value, close }))\n : null,\n ])\n },\n})\n\nexport default ITubePlayer\nexport { Player }\nexport type * from './types'\n"],"names":["EVENT_NAMES","ITubePlayer","defineComponent","props","emit","slots","expose","mountEl","ref","slotEl","player","shallowRef","onMounted","instance","Player","forward","name","payload","watch","next","onBeforeUnmount","close","h"],"mappings":"wLA0BMA,EAAc,CAClB,QAAS,OAAQ,QAAS,QAAS,aAAc,WAAY,eAC7D,aAAc,UAAW,SAAU,eAAgB,qBAAsB,iBACzE,gBAAiB,kBAAmB,mBAAoB,YAAa,gBACrE,iBAAkB,cAAe,eAAgB,SAAU,eAC3D,UAAW,QAAS,SAAU,UAAW,UAAW,WAAY,UAAW,QAAS,SACtF,EAEaC,EAAcC,EAAAA,gBAAgB,CACzC,KAAM,cACN,MAAO,CAEL,OAAQ,CACN,KAAM,CAAC,OAAQ,KAAK,EACpB,SAAU,GACV,QAAS,MAAA,EAGX,QAAS,CACP,KAAM,OACN,QAAS,KAAO,CAAA,EAAC,CACnB,EAEF,MAAO,CAAC,GAAGF,CAAW,EACtB,MAAMG,EAAO,CAAE,KAAAC,EAAM,MAAAC,EAAO,OAAAC,GAAU,CACpC,MAAMC,EAAUC,EAAAA,IAAwB,IAAI,EACtCC,EAASD,EAAAA,IAAwB,IAAI,EACrCE,EAASC,EAAAA,WAA0B,IAAI,EAE7CC,EAAAA,UAAU,IAAM,CACd,GAAI,CAACL,EAAQ,MAAO,OACpB,MAAMM,EAAW,IAAIC,SAAOP,EAAQ,MAAO,CACzC,GAAGJ,EAAM,QACT,OAAQA,EAAM,OAEd,GAAIE,EAAM,YAAc,CAAE,YAAa,EAAA,EAAS,CAAA,CAAC,CAClD,EACKU,EAAUX,EAChB,UAAWY,KAAQhB,EACjBa,EAAS,GAAGG,EAAOC,GAAYF,EAAQC,EAAMC,CAAO,CAAC,EAEnDZ,EAAM,aAAeI,EAAO,OAC9BI,EAAS,sBAAsBJ,EAAO,KAAK,EAE7CC,EAAO,MAAQG,CACjB,CAAC,EAEDK,EAAAA,MACE,IAAMf,EAAM,OACXgB,GAAS,CACJA,GAAQT,EAAO,OAAOA,EAAO,MAAM,KAAKS,CAAI,CAClD,EACA,CAAE,KAAM,EAAA,CAAK,EAGfC,EAAAA,gBAAgB,IAAM,CACpBV,EAAO,OAAO,QAAA,EACdA,EAAO,MAAQ,IACjB,CAAC,EAEDJ,EAAO,CAAE,OAAAI,EAAQ,EAIjB,MAAMW,EAAQ,IAAM,CACbX,EAAO,OAAO,KAAA,CACrB,EAEA,MAAO,IACLY,EAAAA,EAAE,MAAO,CAAE,MAAO,gBAAkB,CAClCA,EAAAA,EAAE,MAAO,CAAE,IAAKf,EAAS,EAGzBF,EAAM,YACFiB,IAAE,MAAO,CAAE,IAAKb,EAAQ,MAAO,oBAAA,EAAwBJ,EAAM,YAAY,CAAE,OAAQK,EAAO,MAAO,MAAAW,CAAA,CAAO,CAAC,EACzG,IAAA,CACL,CACL,CACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"vue.cjs","sources":["../src/vue.ts"],"sourcesContent":["/**\n * Vue 3 wrapper — a separate entry (`itube-modern-player/vue`).\n *\n * The heavy player core is loaded with a DYNAMIC import — importing this\n * module costs a few KB, not the whole player. Two modes:\n *\n * - default: the core chunk is fetched on mount (imperceptible vs the old\n * static import, but it no longer bloats the consumer's entry bundle);\n * - `lazy`: poster-first, player-on-intent. The component renders a\n * declarative SSR-able placeholder (the poster ships in the first HTML and\n * is the LCP candidate — no JS needed), fetches the core chunk + CSS on the\n * `loadOn` trigger, and swaps via reactive state. No `<ClientOnly>` needed.\n *\n * ```vue\n * <ITubePlayer lazy :source=\"video\" :options=\"{ seekStep: 5 }\"\n * :ad-config=\"fetchVast\" @ended=\"onEnded\" />\n * ```\n */\nimport {\n computed,\n defineComponent,\n h,\n onBeforeUnmount,\n onMounted,\n ref,\n shallowRef,\n watch,\n type PropType,\n} from 'vue'\nimport { renderPlaceholder } from './placeholder'\nimport type { Player } from './player'\nimport type { AdsOptions, PlayerEventMap, PlayerOptions, VideoSource } from './types'\n\n/** `adConfig` may be an async factory — resolved in parallel with the core chunk. */\nexport type AdConfigInput = AdsOptions | (() => AdsOptions | undefined | Promise<AdsOptions | undefined>)\n\nconst EVENT_NAMES = [\n 'ready', 'play', 'pause', 'ended', 'timeupdate', 'progress', 'volumechange',\n 'ratechange', 'seeking', 'seeked', 'sourcechange', 'playlistitemchange', 'advancerequest',\n 'chapterchange', 'scenetypechange', 'fullscreenchange', 'pipchange', 'qualitychange',\n 'subtitlechange', 'relatedshow', 'relatedclick', 'action', 'customaction',\n 'adstart', 'adend', 'adskip', 'adclick', 'adpause', 'adresume', 'aderror', 'error', 'destroy',\n] as const satisfies ReadonlyArray<keyof PlayerEventMap>\n\n/**\n * Best-effort load of the player stylesheet next to the core chunk. Resolved\n * via the package's own exports (self-reference) — works in Vite/webpack/Nuxt.\n * If the consumer already imports `itube-modern-player/style.css` themselves,\n * bundlers dedupe it; if the resolution fails in an exotic setup, the manual\n * import keeps working as before.\n */\nfunction loadStyles(): Promise<unknown> {\n return import('itube-modern-player/style.css').catch(() => undefined)\n}\n\nexport const ITubePlayer = defineComponent({\n name: 'ITubePlayer',\n props: {\n /** Single video or a playlist array. */\n source: {\n type: [Object, Array] as PropType<VideoSource | VideoSource[]>,\n required: false,\n default: undefined,\n },\n /** Everything else from PlayerOptions (source/adConfig inside are superseded by the props). */\n options: {\n type: Object as PropType<Omit<PlayerOptions, 'source'>>,\n default: () => ({}),\n },\n /**\n * Poster-first, player-on-intent: SSR-able declarative placeholder, core\n * chunk + CSS fetched on the `loadOn` trigger, swap via reactive state.\n */\n lazy: {\n type: Boolean,\n default: false,\n },\n /**\n * When `lazy`, what fetches the core chunk:\n * `'placeholder'` (default) — any interaction with the placeholder area;\n * `'interaction'` — first interaction anywhere on the page;\n * `'visible'` — placeholder enters the viewport;\n * `'immediate'` — right away (still a split chunk).\n * A click on the placeholder always loads AND starts playback.\n */\n loadOn: {\n type: String as PropType<'placeholder' | 'interaction' | 'visible' | 'immediate'>,\n default: 'placeholder',\n },\n /**\n * Ad configuration, or an async factory (e.g. a VAST-tag lookup). A factory\n * runs in parallel with the core-chunk download; the player is constructed\n * with the resolved config. Takes precedence over `options.adConfig`.\n */\n adConfig: {\n type: [Object, Function] as PropType<AdConfigInput>,\n default: undefined,\n },\n },\n emits: [...EVENT_NAMES] as unknown as Record<(typeof EVENT_NAMES)[number], (payload: unknown) => boolean>,\n setup(props, { emit, slots, expose }) {\n const mountEl = ref<HTMLElement | null>(null)\n const phEl = ref<HTMLElement | null>(null)\n const slotEl = ref<HTMLElement | null>(null)\n const player = shallowRef<Player | null>(null)\n /** Player constructed → the placeholder unmounts (reactive swap, no DOM adoption). */\n const playerReady = ref(false)\n\n let destroyed = false\n let loading = false\n let wantAutoplay = false\n const disposers: Array<() => void> = []\n const cleanupTriggers = () => {\n for (const d of disposers.splice(0)) d()\n }\n\n const firstSource = computed(() => (Array.isArray(props.source) ? props.source[0] : props.source))\n /** Declarative placeholder markup — self-contained (inline styles, no style.css needed). */\n const placeholderHtml = computed(() =>\n renderPlaceholder(\n {\n poster: firstSource.value?.poster,\n icon: props.options.icons?.bigPlay,\n styling: props.options.styling,\n className: props.options.className,\n },\n { inlineStyles: true },\n ),\n )\n\n const load = (autoplay: boolean) => {\n wantAutoplay ||= autoplay\n if (loading || destroyed) return\n loading = true\n cleanupTriggers()\n const adConfigInput = props.adConfig ?? props.options.adConfig\n const adConfigPromise = Promise.resolve(\n typeof adConfigInput === 'function' ? adConfigInput() : adConfigInput,\n ).catch(() => undefined)\n void Promise.all([import('./index'), adConfigPromise, loadStyles()]).then(\n ([{ Player: PlayerCtor }, adCfg]) => {\n if (destroyed || !mountEl.value) return\n const opts: PlayerOptions = {\n ...props.options,\n source: props.source,\n ...(adCfg ? { adConfig: adCfg } : {}),\n // The slot wins over whatever was passed in options.\n ...(slots.pauseScreen ? { pauseScreen: true } : {}),\n }\n if (props.lazy) {\n // Lazy semantics: only an explicit placeholder click autoplays —\n // background triggers must stay invisible (same as createLazyPlayer).\n opts.playOnInit = wantAutoplay\n opts.autoplay = wantAutoplay\n }\n const instance = new PlayerCtor(mountEl.value, opts)\n const forward = emit as (event: string, payload?: unknown) => void\n for (const name of EVENT_NAMES) {\n instance.on(name, (payload) => forward(name, payload))\n }\n if (slots.pauseScreen && slotEl.value) {\n instance.setPauseScreenContent(slotEl.value)\n }\n player.value = instance\n playerReady.value = true\n },\n (err) => {\n // Surfaced, not swallowed — a failed chunk load must be debuggable.\n loading = false\n console.error('[itube-player] failed to load the player chunk', err)\n },\n )\n }\n\n onMounted(() => {\n if (!props.lazy) {\n load(false)\n return\n }\n const ph = phEl.value\n if (props.loadOn === 'immediate' || !ph) {\n load(false)\n return\n }\n const bg = () => load(false)\n if (props.loadOn === 'interaction') {\n const events: Array<keyof DocumentEventMap> = ['pointerdown', 'keydown', 'touchstart', 'wheel']\n for (const name of events) {\n document.addEventListener(name, bg, { once: true, passive: true })\n disposers.push(() => document.removeEventListener(name, bg))\n }\n } else if (props.loadOn === 'visible' && typeof IntersectionObserver !== 'undefined') {\n const io = new IntersectionObserver((entries) => {\n if (entries.some((e) => e.isIntersecting)) bg()\n })\n io.observe(ph)\n disposers.push(() => io.disconnect())\n } else {\n // 'placeholder' (default): any interaction with the placeholder area.\n const events: Array<keyof HTMLElementEventMap> = ['pointerenter', 'pointerdown', 'touchstart', 'focusin']\n for (const name of events) {\n ph.addEventListener(name, bg, { once: true, passive: true })\n disposers.push(() => ph.removeEventListener(name, bg))\n }\n }\n })\n\n watch(\n () => props.source,\n (next) => {\n if (next && player.value) player.value.load(next)\n },\n { deep: true },\n )\n\n onBeforeUnmount(() => {\n destroyed = true\n cleanupTriggers()\n player.value?.destroy()\n player.value = null\n })\n\n expose({ player })\n\n // \"Close & continue\" — resume playback; the player hides the pause screen\n // on the `play` event, so this both closes the custom block and plays.\n const close = () => {\n void player.value?.play()\n }\n\n return () => {\n const children = []\n if (props.lazy && !playerReady.value) {\n // Declarative, SSR-rendered placeholder (innerHTML is serialized by\n // @vue/server-renderer, so the poster <img> ships in the first HTML).\n children.push(\n h('div', {\n key: 'imp-ph',\n ref: phEl,\n class: 'imp-vue-placeholder',\n innerHTML: placeholderHtml.value,\n onClick: () => load(true),\n }),\n )\n }\n children.push(h('div', { key: 'imp-mount', ref: mountEl }))\n if (slots.pauseScreen) {\n // Scoped slot: `player` (reactive — null until mounted, then the\n // instance) and `close()` are available to the custom pause content.\n children.push(h('div', { ref: slotEl, class: 'imp-vue-pause-slot' }, slots.pauseScreen({ player: player.value, close })))\n }\n return h('div', { class: 'imp-vue-host' }, children)\n }\n },\n})\n\nexport default ITubePlayer\nexport type { Player }\nexport type * from './types'\n"],"names":["EVENT_NAMES","loadStyles","ITubePlayer","defineComponent","props","emit","slots","expose","mountEl","ref","phEl","slotEl","player","shallowRef","playerReady","destroyed","loading","wantAutoplay","disposers","cleanupTriggers","d","firstSource","computed","placeholderHtml","renderPlaceholder","load","autoplay","adConfigInput","adConfigPromise","PlayerCtor","adCfg","opts","instance","forward","name","payload","err","onMounted","ph","bg","events","io","entries","e","watch","next","onBeforeUnmount","close","children","h"],"mappings":"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"}
|