itube-modern-player 0.7.2 → 0.8.1
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 +131 -5
- package/dist/core.cjs +4 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +696 -583
- package/dist/core.js.map +1 -1
- package/dist/gesture-4dfbCdW5.cjs +2 -0
- package/dist/gesture-4dfbCdW5.cjs.map +1 -0
- package/dist/gesture-SgY5sB4t.js +11 -0
- package/dist/gesture-SgY5sB4t.js.map +1 -0
- package/dist/gesture.d.ts +18 -0
- 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 -58
- 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/player.d.ts +10 -0
- package/dist/types.d.ts +39 -5
- package/dist/vue.cjs +1 -1
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.d.ts +68 -5
- package/dist/vue.js +137 -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("./gesture-4dfbCdW5.cjs"),S=require("./placeholder.cjs");function A(a,I={}){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:f,...r}=I,q=Array.isArray(r.source)?r.source[0]:r.source,p=s.children.length>0;let n;if(p)n=s;else{const e=document.createElement("div");e.innerHTML=S.renderPlaceholder({poster:q?.poster,icon:r.icons?.bigPlay,styling:r.styling,className:r.className}),n=e.firstElementChild,s.append(n)}let h,g;const P=new Promise((e,o)=>{h=e,g=o});P.catch(()=>{});const c={ready:P,player:null,el:n,destroy(){y=!0,w(),p||n.remove(),c.player?.destroy()}};let y=!1,m=!1;const d=[],w=()=>{for(const e of d.splice(0))e()};let E=!1,u=null;const l=e=>{if(E||(E=e),m||y)return;m=!0,w();const o=Promise.resolve(typeof f=="function"?f():f).catch(()=>{});Promise.all([Promise.resolve().then(()=>require("./index.cjs")),o]).then(([{Player:t},b])=>{if(y)return;p?s.textContent="":n.remove();const v=b?{...r,adConfig:b}:r,L=E?{...v,playOnInit:!0,autoplay:!0}:i==="interaction"||i==="placeholder"?{...v,playOnInit:!1,autoplay:!1}:v;u&&(L.videoElement=u);const O=new t(s,L);c.player=O,h(O)},t=>{m=!1,g(t)})};if(n.addEventListener("click",()=>{!c.player&&!u&&(u=C.createGestureVideo()),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}),d.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),d.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}),d.push(()=>document.removeEventListener(t,e))}return c}exports.createLazyPlayer=A;
|
|
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 { createGestureVideo } from './gesture'\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 // Media element \"blessed\" inside the click gesture (see gesture.ts). Read at\n // construction time, not load-start time: a click during an in-flight\n // background load must still hand its unlocked element to the player.\n let gestureVideo: HTMLVideoElement | null = null\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 if (gestureVideo) effective.videoElement = gestureVideo\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 // The gesture video MUST be created synchronously here, before any await:\n // mobile browsers only honor play()-with-sound issued inside the gesture.\n ph.addEventListener('click', () => {\n if (!api.player && !gestureVideo) gestureVideo = createGestureVideo()\n load(true)\n })\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","gestureVideo","load","autoplay","adConfigPromise","PlayerCtor","resolvedAdConfig","withAds","effective","player","err","createGestureVideo","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,80 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
function
|
|
4
|
-
const
|
|
5
|
-
if (!
|
|
6
|
-
const { loadOn:
|
|
7
|
-
let
|
|
1
|
+
import { c as A } from "./gesture-SgY5sB4t.js";
|
|
2
|
+
import { renderPlaceholder as k } from "./placeholder.js";
|
|
3
|
+
function R(a, O = {}) {
|
|
4
|
+
const s = typeof a == "string" ? document.querySelector(a) : a;
|
|
5
|
+
if (!s) throw new Error(`[itube-player] mount target not found: ${String(a)}`);
|
|
6
|
+
const { loadOn: i = "interaction", adConfig: p, ...r } = O, C = Array.isArray(r.source) ? r.source[0] : r.source, u = s.children.length > 0;
|
|
7
|
+
let n;
|
|
8
8
|
if (u)
|
|
9
|
-
|
|
9
|
+
n = s;
|
|
10
10
|
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);
|
|
11
|
+
const e = document.createElement("div");
|
|
12
|
+
e.innerHTML = k({
|
|
13
|
+
poster: C?.poster,
|
|
14
|
+
icon: r.icons?.bigPlay,
|
|
15
|
+
styling: r.styling,
|
|
16
|
+
className: r.className
|
|
17
|
+
}), n = e.firstElementChild, s.append(n);
|
|
28
18
|
}
|
|
29
|
-
let h,
|
|
30
|
-
const g = new Promise((e,
|
|
31
|
-
h = e,
|
|
19
|
+
let h, w;
|
|
20
|
+
const g = new Promise((e, o) => {
|
|
21
|
+
h = e, w = o;
|
|
32
22
|
});
|
|
33
23
|
g.catch(() => {
|
|
34
24
|
});
|
|
35
|
-
const
|
|
25
|
+
const c = {
|
|
36
26
|
ready: g,
|
|
37
27
|
player: null,
|
|
38
|
-
el:
|
|
28
|
+
el: n,
|
|
39
29
|
destroy() {
|
|
40
|
-
y = !0,
|
|
30
|
+
y = !0, E(), u || n.remove(), c.player?.destroy();
|
|
41
31
|
}
|
|
42
32
|
};
|
|
43
|
-
let y = !1,
|
|
44
|
-
const d = [],
|
|
33
|
+
let y = !1, m = !1;
|
|
34
|
+
const d = [], E = () => {
|
|
45
35
|
for (const e of d.splice(0)) e();
|
|
46
36
|
};
|
|
47
|
-
let
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
let P = !1, f = null;
|
|
38
|
+
const l = (e) => {
|
|
39
|
+
if (P || (P = e), m || y) return;
|
|
40
|
+
m = !0, E();
|
|
41
|
+
const o = Promise.resolve(typeof p == "function" ? p() : p).catch(() => {
|
|
42
|
+
});
|
|
43
|
+
Promise.all([import("./index.js"), o]).then(
|
|
44
|
+
([{ Player: t }, L]) => {
|
|
51
45
|
if (y) return;
|
|
52
|
-
u ?
|
|
53
|
-
const
|
|
54
|
-
|
|
46
|
+
u ? s.textContent = "" : n.remove();
|
|
47
|
+
const v = L ? { ...r, adConfig: L } : r, b = P ? { ...v, playOnInit: !0, autoplay: !0 } : i === "interaction" || i === "placeholder" ? { ...v, playOnInit: !1, autoplay: !1 } : v;
|
|
48
|
+
f && (b.videoElement = f);
|
|
49
|
+
const I = new t(s, b);
|
|
50
|
+
c.player = I, h(I);
|
|
55
51
|
},
|
|
56
|
-
(
|
|
57
|
-
|
|
52
|
+
(t) => {
|
|
53
|
+
m = !1, w(t);
|
|
58
54
|
}
|
|
59
|
-
)
|
|
55
|
+
);
|
|
60
56
|
};
|
|
61
|
-
if (
|
|
62
|
-
c(!
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
if (n.addEventListener("click", () => {
|
|
58
|
+
!c.player && !f && (f = A()), l(!0);
|
|
59
|
+
}), i === "immediate")
|
|
60
|
+
l(!1);
|
|
61
|
+
else if (i === "placeholder") {
|
|
62
|
+
const e = () => l(!1), o = ["pointerenter", "pointerdown", "touchstart", "focusin"];
|
|
63
|
+
for (const t of o)
|
|
64
|
+
n.addEventListener(t, e, { once: !0, passive: !0 }), d.push(() => n.removeEventListener(t, e));
|
|
65
|
+
} else if (i === "visible" && typeof IntersectionObserver < "u") {
|
|
66
|
+
const e = new IntersectionObserver((o) => {
|
|
67
|
+
o.some((t) => t.isIntersecting) && l(!1);
|
|
70
68
|
});
|
|
71
|
-
e.observe(
|
|
69
|
+
e.observe(n), d.push(() => e.disconnect());
|
|
72
70
|
} else {
|
|
73
|
-
const e = () =>
|
|
74
|
-
for (const
|
|
75
|
-
document.addEventListener(
|
|
71
|
+
const e = () => l(!1), o = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
72
|
+
for (const t of o)
|
|
73
|
+
document.addEventListener(t, e, { once: !0, passive: !0 }), d.push(() => document.removeEventListener(t, e));
|
|
76
74
|
}
|
|
77
|
-
return
|
|
75
|
+
return c;
|
|
78
76
|
}
|
|
79
77
|
export {
|
|
80
|
-
|
|
78
|
+
R as createLazyPlayer
|
|
81
79
|
};
|
|
82
80
|
//# 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":";AAgDA,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,QAOtFE,IAAUJ,EAAM,SAAS,SAAS;AACxC,MAAIK;AACJ,MAAID;AACF,IAAAC,IAAKL;AAAA,OACA;AAGL,IAAAK,IAAK,SAAS,cAAc,KAAK,GACjCA,EAAG,YAAY;AAEf,UAAMC,IAAUJ,EAAc;AAE9B,QADII,GAAS,cAAYD,EAAG,MAAM,YAAY,gBAAgBC,EAAQ,UAAU,GAC5EA,GAAS,iBAAiB,QAAW;AACvC,YAAMC,IAAI,OAAOD,EAAQ,gBAAiB,WAAW,GAAGA,EAAQ,YAAY,OAAOA,EAAQ;AAC3F,MAAAD,EAAG,MAAM,YAAY,gBAAgBE,CAAC;AAAA,IACxC;AACA,IAAID,GAAS,oBAAoB,cAAYD,EAAG,UAAU,IAAI,2BAA2B,GACrFH,EAAc,aAAWG,EAAG,UAAU,IAAIH,EAAc,SAAS;AACrE,UAAMM,IAAS,SAAS,cAAc,KAAK;AAC3C,IAAAA,EAAO,YAAY;AAGnB,UAAMC,IAAQ,SAAS,cAAc,KAAK;AAE1C,QADAA,EAAM,YAAY,qBACdN,GAAO,QAAQ;AACjB,YAAMO,IAAM,SAAS,cAAc,KAAK;AACxC,MAAAA,EAAI,YAAY,iBAChBA,EAAI,MAAM,IACVA,EAAI,WAAW,SACfA,EAAI,gBAAgB,QACpBA,EAAI,MAAMP,EAAM,QAChBM,EAAM,OAAOC,CAAG;AAAA,IAClB;AACE,MAAAD,EAAM,SAAS;AAEjB,UAAME,IAAM,SAAS,cAAc,QAAQ;AAC3C,IAAAA,EAAI,OAAO,UACXA,EAAI,YAAY,4BAChBA,EAAI,aAAa,cAAc,MAAM,GAIrCC,EAAUD,GAAKT,EAAc,OAAO,WAAWN,CAAQ,GACvDY,EAAO,OAAOC,GAAOE,CAAG,GACxBN,EAAG,OAAOG,CAAM,GAChBR,EAAM,OAAOK,CAAE;AAAA,EACjB;AAEA,MAAIQ,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,IAAIV;AAAA,IACJ,UAAU;AACR,MAAAc,IAAY,IACZC,EAAA,GAGKhB,KAASC,EAAG,OAAA,GACjBa,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;AAGf,QAAIf,MAAe,cAAc,OACzB,OAAA;AAOR,cAAMwB,IAAYJ,IACd,EAAE,GAAGtB,GAAe,YAAY,IAAM,UAAU,OAChDD,MAAW,iBAAiBA,MAAW,gBACrC,EAAE,GAAGC,GAAe,YAAY,IAAO,UAAU,GAAA,IACjDA,GACA2B,IAAS,IAAIF,EAAW3B,GAAO4B,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,MAFAzB,EAAG,iBAAiB,SAAS,MAAMoB,EAAK,EAAI,CAAC,GAEzCxB,MAAW;AACb,IAAAwB,EAAK,EAAK;AAAA,WACDxB,MAAW,eAAe;AAInC,UAAM8B,IAAO,MAAMN,EAAK,EAAK,GACvBO,IAA2C,CAAC,gBAAgB,eAAe,cAAc,SAAS;AACxG,eAAWC,KAAQD;AACjB,MAAA3B,EAAG,iBAAiB4B,GAAMF,GAAM,EAAE,MAAM,IAAM,SAAS,IAAM,GAC7DT,EAAU,KAAK,MAAMjB,EAAG,oBAAoB4B,GAAMF,CAAI,CAAC;AAAA,EAE3D,WAAW9B,MAAW,aAAa,OAAO,uBAAyB,KAAa;AAC9E,UAAMiC,IAAK,IAAI,qBAAqB,CAACC,MAAY;AAC/C,MAAIA,EAAQ,KAAK,CAACC,MAAMA,EAAE,cAAc,OAAQ,EAAK;AAAA,IACvD,CAAC;AACD,IAAAF,EAAG,QAAQ7B,CAAE,GACbiB,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 { createGestureVideo } from './gesture'\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 // Media element \"blessed\" inside the click gesture (see gesture.ts). Read at\n // construction time, not load-start time: a click during an in-flight\n // background load must still hand its unlocked element to the player.\n let gestureVideo: HTMLVideoElement | null = null\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 if (gestureVideo) effective.videoElement = gestureVideo\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 // The gesture video MUST be created synchronously here, before any await:\n // mobile browsers only honor play()-with-sound issued inside the gesture.\n ph.addEventListener('click', () => {\n if (!api.player && !gestureVideo) gestureVideo = createGestureVideo()\n load(true)\n })\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","gestureVideo","load","autoplay","adConfigPromise","PlayerCtor","resolvedAdConfig","withAds","effective","player","err","createGestureVideo","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":";;AA0DO,SAASA,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,UAAAC,GAAU,GAAGC,MAAkBJ,GACzDK,IAAQ,MAAM,QAAQD,EAAc,MAAM,IAAIA,EAAc,OAAO,CAAC,IAAIA,EAAc,QAOtFE,IAAUL,EAAM,SAAS,SAAS;AACxC,MAAIM;AACJ,MAAID;AACF,IAAAC,IAAKN;AAAA,OACA;AAKL,UAAMO,IAAM,SAAS,cAAc,KAAK;AACxC,IAAAA,EAAI,YAAYC,EAAkB;AAAA,MAChC,QAAQJ,GAAO;AAAA,MACf,MAAMD,EAAc,OAAO;AAAA,MAC3B,SAASA,EAAc;AAAA,MACvB,WAAWA,EAAc;AAAA,IAAA,CAC1B,GACDG,IAAKC,EAAI,mBACTP,EAAM,OAAOM,CAAE;AAAA,EACjB;AAEA,MAAIG,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,IAAIL;AAAA,IACJ,UAAU;AACR,MAAAS,IAAY,IACZC,EAAA,GAGKX,KAASC,EAAG,OAAA,GACjBQ,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,IAIfC,IAAwC;AAC5C,QAAMC,IAAO,CAACC,MAAsB;AAElC,QADAH,UAAiBG,IACbN,KAAWF,EAAW;AAC1B,IAAAE,IAAU,IACVD,EAAA;AAGA,UAAMQ,IAAkB,QAAQ,QAAQ,OAAOtB,KAAa,aAAaA,EAAA,IAAaA,CAAQ,EAC3F,MAAM,MAAA;AAAA,KAAe;AAExB,YAAQ,IAAI,CAAC,OAAO,YAAS,GAAGsB,CAAe,CAAC,EAAE;AAAA,MAChD,CAAC,CAAC,EAAE,QAAQC,EAAA,GAAcC,CAAgB,MAAM;AAC9C,YAAIX,EAAW;AAGf,QAAIV,MAAe,cAAc,OACzB,OAAA;AAOR,cAAMsB,IAAyBD,IAC3B,EAAE,GAAGvB,GAAe,UAAUuB,MAC9BvB,GACEyB,IAAYR,IACd,EAAE,GAAGO,GAAS,YAAY,IAAM,UAAU,OAC1C1B,MAAW,iBAAiBA,MAAW,gBACrC,EAAE,GAAG0B,GAAS,YAAY,IAAO,UAAU,GAAA,IAC3CA;AACN,QAAIN,QAAwB,eAAeA;AAC3C,cAAMQ,IAAS,IAAIJ,EAAWzB,GAAO4B,CAAS;AAC9C,QAAAd,EAAI,SAASe,GACbpB,EAAaoB,CAAM;AAAA,MACrB;AAAA,MACA,CAACC,MAAQ;AACP,QAAAb,IAAU,IACVP,EAAYoB,CAAG;AAAA,MACjB;AAAA,IAAA;AAAA,EAEJ;AAYA,MALAxB,EAAG,iBAAiB,SAAS,MAAM;AACjC,IAAI,CAACQ,EAAI,UAAU,CAACO,UAA6BU,EAAA,IACjDT,EAAK,EAAI;AAAA,EACX,CAAC,GAEGrB,MAAW;AACb,IAAAqB,EAAK,EAAK;AAAA,WACDrB,MAAW,eAAe;AAInC,UAAM+B,IAAO,MAAMV,EAAK,EAAK,GACvBW,IAA2C,CAAC,gBAAgB,eAAe,cAAc,SAAS;AACxG,eAAWC,KAAQD;AACjB,MAAA3B,EAAG,iBAAiB4B,GAAMF,GAAM,EAAE,MAAM,IAAM,SAAS,IAAM,GAC7Dd,EAAU,KAAK,MAAMZ,EAAG,oBAAoB4B,GAAMF,CAAI,CAAC;AAAA,EAE3D,WAAW/B,MAAW,aAAa,OAAO,uBAAyB,KAAa;AAC9E,UAAMkC,IAAK,IAAI,qBAAqB,CAACC,MAAY;AAC/C,MAAIA,EAAQ,KAAK,CAACC,MAAMA,EAAE,cAAc,OAAQ,EAAK;AAAA,IACvD,CAAC;AACD,IAAAF,EAAG,QAAQ7B,CAAE,GACbY,EAAU,KAAK,MAAMiB,EAAG,WAAA,CAAY;AAAA,EACtC,OAAO;AACL,UAAMG,IAAqB,MAAMhB,EAAK,EAAK,GACrCW,IAAwC,CAAC,eAAe,WAAW,cAAc,OAAO;AAC9F,eAAWC,KAAQD;AACjB,eAAS,iBAAiBC,GAAMI,GAAoB,EAAE,MAAM,IAAM,SAAS,IAAM,GACjFpB,EAAU,KAAK,MAAM,SAAS,oBAAoBgB,GAAMI,CAAkB,CAAC;AAAA,EAE/E;AAEA,SAAOxB;AACT;"}
|
|
@@ -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":"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"}
|
package/dist/player.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ export declare class Player {
|
|
|
81
81
|
private errorResumeTime;
|
|
82
82
|
/** Last known good playhead time (for the retry-resume above). */
|
|
83
83
|
private lastPlayheadTime;
|
|
84
|
+
/** Seek requested before metadata (pre-start scrub) — applied on loadedmetadata. */
|
|
85
|
+
private pendingSeek;
|
|
84
86
|
private destroyed;
|
|
85
87
|
constructor(target: string | HTMLElement, options?: PlayerOptions);
|
|
86
88
|
on<K extends keyof PlayerEventMap>(event: K, fn: (payload: PlayerEventMap[K]) => void): () => void;
|
|
@@ -105,6 +107,8 @@ export declare class Player {
|
|
|
105
107
|
*/
|
|
106
108
|
retry(): void;
|
|
107
109
|
private clearError;
|
|
110
|
+
/** True once the current source has started playing at least once. */
|
|
111
|
+
get started(): boolean;
|
|
108
112
|
seek(time: number): void;
|
|
109
113
|
/** Relative seek, e.g. `skip(-10)`. */
|
|
110
114
|
skip(delta: number): void;
|
|
@@ -183,6 +187,12 @@ export declare class Player {
|
|
|
183
187
|
get activeSceneType(): SceneGroup | null;
|
|
184
188
|
/** Switch the active scene type — rebuilds the timeline segments + scenes panel. */
|
|
185
189
|
setSceneGroup(id: string): void;
|
|
190
|
+
/**
|
|
191
|
+
* Best duration currently known: the media's real one once metadata is in,
|
|
192
|
+
* else the declared `source.duration` (lets scene groups and the seek bar
|
|
193
|
+
* render before the first play), else NaN.
|
|
194
|
+
*/
|
|
195
|
+
private get knownDuration();
|
|
186
196
|
/** Normalize the active group's scenes into `chapters` and refresh the UI. */
|
|
187
197
|
private applyActiveSceneGroup;
|
|
188
198
|
/**
|
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=`). */
|
|
@@ -316,6 +333,15 @@ export interface ControlsOptions {
|
|
|
316
333
|
* over the video on mobile). This is the pre-0.3 desktop behaviour.
|
|
317
334
|
*/
|
|
318
335
|
seekPlacement?: 'overlay' | 'bar';
|
|
336
|
+
/**
|
|
337
|
+
* Mobile (≤767px) control layout:
|
|
338
|
+
* - `"bar"` (default): only the ±N seek buttons stay over the video; play
|
|
339
|
+
* sits bottom-left in the control bar with the next-video button beside
|
|
340
|
+
* it — same as desktop;
|
|
341
|
+
* - `"center"`: the pre-0.8.1 layout — prev · −Ns · play · +Ns · next as a
|
|
342
|
+
* big centered cluster over the video, no play/next in the bar.
|
|
343
|
+
*/
|
|
344
|
+
mobileLayout?: 'bar' | 'center';
|
|
319
345
|
/**
|
|
320
346
|
* Left-to-right order of the right-cluster controls (gear, pip, playlist, …).
|
|
321
347
|
* Omit / empty → built-in order. Listed keys go first (in this order); any not
|
|
@@ -326,11 +352,11 @@ export interface ControlsOptions {
|
|
|
326
352
|
playlist?: boolean;
|
|
327
353
|
/**
|
|
328
354
|
* Hide the "previous" button in the control bar (the bar keeps only "next").
|
|
329
|
-
* Default true.
|
|
330
|
-
* the bar prev button back.
|
|
355
|
+
* Default true. With `mobileLayout: "center"` the mobile center cluster
|
|
356
|
+
* still shows prev. Set false to bring the bar prev button back.
|
|
331
357
|
*/
|
|
332
358
|
hidePrev?: boolean;
|
|
333
|
-
/** Hide the bar "next" button (
|
|
359
|
+
/** Hide the bar "next" button (`mobileLayout: "center"` still shows next in the center cluster). Default false. */
|
|
334
360
|
hideNext?: boolean;
|
|
335
361
|
/** Show the playlist-panel (list) button. Default true — set false to hide it even when a playlist is present. */
|
|
336
362
|
playlistButton?: boolean;
|
|
@@ -519,6 +545,14 @@ export interface PlayerOptions {
|
|
|
519
545
|
crossOrigin?: '' | 'anonymous' | 'use-credentials';
|
|
520
546
|
/** Inline playback on iOS. Default true. */
|
|
521
547
|
playsInline?: boolean;
|
|
548
|
+
/**
|
|
549
|
+
* Use this element as the player's `<video>` instead of creating one.
|
|
550
|
+
* The lazy entries pass a media element "blessed" synchronously inside the
|
|
551
|
+
* click gesture (see `createGestureVideo`) so that when the player chunk
|
|
552
|
+
* lands across the async boundary, `play()` with sound is still allowed on
|
|
553
|
+
* mobile — without it the first tap only loads and the second one plays.
|
|
554
|
+
*/
|
|
555
|
+
videoElement?: HTMLVideoElement;
|
|
522
556
|
/**
|
|
523
557
|
* Persist user preferences (volume/mute, autoplay-next) to `localStorage`
|
|
524
558
|
* and restore them on the next load. `true` enables all with defaults; pass
|
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 j=Object.create;var k=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var V=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var B=(e,a,r,c)=>{if(a&&typeof a=="object"||typeof a=="function")for(let s of N(a))!_.call(e,s)&&s!==r&&k(e,s,{get:()=>a[s],enumerable:!(c=z(a,s))||c.enumerable});return e};var H=(e,a,r)=>(r=e!=null?j(V(e)):{},B(a||!e||!e.__esModule?k(r,"default",{value:e,enumerable:!0}):r,e));Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const n=require("vue"),R=require("./gesture-4dfbCdW5.cjs"),F=require("./placeholder.cjs"),C=["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 G(){return import("itube-modern-player/style.css").catch(()=>{})}const I=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:[...C],setup(e,{emit:a,slots:r,expose:c}){const s=n.ref(null),P=n.ref(null),h=n.ref(null),l=n.shallowRef(null),w=n.ref(!1);let m=!1,g=!1,b=!1,d=null;const f=[],E=()=>{for(const t of f.splice(0))t()},q=n.computed(()=>Array.isArray(e.source)?e.source[0]:e.source),T=n.computed(()=>F.renderPlaceholder({poster:q.value?.poster,icon:e.options.icons?.bigPlay,styling:e.options.styling,className:e.options.className},{inlineStyles:!0})),v=t=>{if(b||(b=t),g||m)return;g=!0,E();const u=e.adConfig??e.options.adConfig,i=Promise.resolve(typeof u=="function"?u():u).catch(()=>{});Promise.all([Promise.resolve().then(()=>require("./index.cjs")),i,G()]).then(([{Player:o},p])=>{if(m||!s.value)return;const y={...e.options,source:e.source,...p?{adConfig:p}:{},...r.pauseScreen?{pauseScreen:!0}:{}};e.lazy&&(y.playOnInit=b,y.autoplay=b,d&&(y.videoElement=d));const S=new o(s.value,y),L=a;for(const O of C)S.on(O,M=>L(O,M));r.pauseScreen&&h.value&&S.setPauseScreenContent(h.value),l.value=S,w.value=!0},o=>{g=!1,console.error("[itube-player] failed to load the player chunk",o)})};n.onMounted(()=>{if(!e.lazy){v(!1);return}const t=P.value;if(e.loadOn==="immediate"||!t){v(!1);return}const u=()=>v(!1);if(e.loadOn==="interaction"){const i=["pointerdown","keydown","touchstart","wheel"];for(const o of i)document.addEventListener(o,u,{once:!0,passive:!0}),f.push(()=>document.removeEventListener(o,u))}else if(e.loadOn==="visible"&&typeof IntersectionObserver<"u"){const i=new IntersectionObserver(o=>{o.some(p=>p.isIntersecting)&&u()});i.observe(t),f.push(()=>i.disconnect())}else{const i=["pointerenter","pointerdown","touchstart","focusin"];for(const o of i)t.addEventListener(o,u,{once:!0,passive:!0}),f.push(()=>t.removeEventListener(o,u))}}),n.watch(()=>e.source,t=>{t&&l.value&&l.value.load(t)},{deep:!0}),n.onBeforeUnmount(()=>{m=!0,E(),l.value?.destroy(),l.value=null}),c({player:l});const A=()=>{l.value?.play()};return()=>{const t=[];return e.lazy&&!w.value&&t.push(n.h("div",{key:"imp-ph",ref:P,class:"imp-vue-placeholder",innerHTML:T.value,onClick:()=>{!l.value&&!d&&(d=R.createGestureVideo()),v(!0)}})),t.push(n.h("div",{key:"imp-mount",ref:s})),r.pauseScreen&&t.push(n.h("div",{ref:h,class:"imp-vue-pause-slot"},r.pauseScreen({player:l.value,close:A}))),n.h("div",{class:"imp-vue-host"},t)}}});exports.ITubePlayer=I;exports.default=I;
|
|
2
2
|
//# sourceMappingURL=vue.cjs.map
|