itube-modern-player 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -3
- package/dist/core.cjs +3 -3
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +362 -332
- package/dist/core.js.map +1 -1
- package/dist/gesture-BLWiPHlW.cjs +2 -0
- package/dist/gesture-BLWiPHlW.cjs.map +1 -0
- package/dist/gesture-BYJrYanO.js +11 -0
- package/dist/gesture-BYJrYanO.js.map +1 -0
- package/dist/gesture.d.ts +18 -0
- 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.js +81 -44
- package/dist/lazy.js.map +1 -1
- package/dist/player.d.ts +10 -0
- package/dist/types.d.ts +20 -3
- package/dist/vue.cjs +1 -1
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +101 -72
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
package/dist/lazy.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const F=require("./gesture-BLWiPHlW.cjs"),M=require("./placeholder.cjs");function N(u,j={}){const s=typeof u=="string"?document.querySelector(u):u;if(!s)throw new Error(`[itube-player] mount target not found: ${String(u)}`);const{loadOn:i="interaction",adConfig:y,...o}=j,z=Array.isArray(o.source)?o.source[0]:o.source,m=s.children.length>0;let n;if(m)n=s;else{const e=document.createElement("div");e.innerHTML=M.renderPlaceholder({poster:z?.poster,icon:o.icons?.bigPlay,styling:o.styling,className:o.className}),n=e.firstElementChild,s.append(n)}let P,g;const b=new Promise((e,r)=>{P=e,g=r});b.catch(()=>{});const d={ready:b,player:null,el:n,destroy(){h=!0,O();for(const e of C.splice(0))e();m||n.remove(),d.player?.destroy()}};let h=!1,w=!1;const f=[],C=[],O=()=>{for(const e of f.splice(0))e()};let I=!1,p=null;const l=e=>{if(I||(I=e),w||h)return;w=!0,O();const r=Promise.resolve(typeof y=="function"?y():y).catch(()=>{});Promise.all([Promise.resolve().then(()=>require("./index.cjs")),r]).then(([{Player:t},c])=>{const a=()=>{if(h)return;m?s.textContent="":n.remove();const L=c?{...o,adConfig:c}:o,A=I?{...L,playOnInit:!0,autoplay:!0}:i==="interaction"||i==="placeholder"?{...L,playOnInit:!1,autoplay:!1}:L;p&&(A.videoElement=p);const S=new t(s,A);d.player=S,P(S)};v?E=a:a()},t=>{w=!1,g(t)})};let v=!1,E=null;const k=e=>{if(!v)return;v=!1;const r=E;if(!r)return;if(E=null,e){r();return}let t=!1;const c=()=>{t||(t=!0,n.removeEventListener("click",a),r())},a=()=>setTimeout(c,0);n.addEventListener("click",a,{once:!0}),setTimeout(c,300)};n.addEventListener("pointerdown",()=>{v=!0}),window.addEventListener("pointerup",T),window.addEventListener("pointercancel",q);function T(){k(!1)}function q(){k(!0)}if(C.push(()=>{window.removeEventListener("pointerup",T),window.removeEventListener("pointercancel",q)}),n.addEventListener("click",()=>{!d.player&&!p&&(p=F.createGestureVideo()),l(!0)}),i==="immediate")l(!1);else if(i==="placeholder"){const e=()=>l(!1),r=["pointerenter","pointerdown","touchstart","focusin"];for(const t of r)n.addEventListener(t,e,{once:!0,passive:!0}),f.push(()=>n.removeEventListener(t,e))}else if(i==="visible"&&typeof IntersectionObserver<"u"){const e=new IntersectionObserver(r=>{r.some(t=>t.isIntersecting)&&l(!1)});e.observe(n),f.push(()=>e.disconnect())}else{const e=()=>l(!1),r=["pointerdown","keydown","touchstart","wheel"];for(const t of r)document.addEventListener(t,e,{once:!0,passive:!0}),f.push(()=>document.removeEventListener(t,e))}return d}exports.createLazyPlayer=N;
|
|
2
2
|
//# sourceMappingURL=lazy.cjs.map
|
package/dist/lazy.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy.cjs","sources":["../src/lazy.ts"],"sourcesContent":["/**\n * Micro entry (`itube-modern-player/lazy`) for bundle-deferred setups.\n *\n * Renders an instant poster placeholder (pure DOM, a couple of KB) and pulls\n * the real player chunk on the first user interaction — the pattern used to\n * be hand-rolled around fluid-player; here it is built in.\n *\n * ```ts\n * import { createLazyPlayer } from 'itube-modern-player/lazy'\n * import 'itube-modern-player/style.css'\n *\n * const lazy = createLazyPlayer('#mount', {\n * source: { src: 'stream.m3u8', poster: 'poster.jpg', title: '…' },\n * loadOn: 'interaction',\n * })\n * lazy.ready.then((player) => player.on('ended', …))\n * ```\n */\nimport { 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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"}
|
|
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 for (const dispose of disposersForever.splice(0)) dispose()\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 // `disposers` are cleared when a load starts; `disposersForever` (press\n // tracking) live until destroy — the press race spans the load itself.\n const disposers: Array<() => void> = []\n const disposersForever: 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 const construct = () => {\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 // A chunk that resolves MID-TAP (touchstart already preloads it) must\n // not swap the placeholder out from under the finger: the tap's click\n // would die on the detached node and the play intent (+ the gesture\n // unlock) would be lost — \"the first tap only inits\" bug. Defer the\n // swap until the press settles.\n if (pressActive) deferredConstruct = construct\n else construct()\n },\n (err) => {\n loading = false\n rejectReady(err)\n },\n )\n }\n\n // --- press tracking for the mid-tap construction race (see above) --------\n let pressActive = false\n let deferredConstruct: (() => void) | null = null\n const settlePress = (cancelled: boolean) => {\n if (!pressActive) return\n pressActive = false\n const run = deferredConstruct\n if (!run) return\n deferredConstruct = null\n if (cancelled) {\n // The press became a scroll — no click is coming, construct right away.\n run()\n return\n }\n // The tap's click lands right after pointerup. Let it register the play\n // intent first (our listener runs after the intent handler on ph), with a\n // timeout fallback for browsers that end up not firing click at all.\n let done = false\n const fire = () => {\n if (done) return\n done = true\n ph.removeEventListener('click', onTapClick)\n run()\n }\n const onTapClick = () => setTimeout(fire, 0)\n ph.addEventListener('click', onTapClick, { once: true })\n setTimeout(fire, 300)\n }\n ph.addEventListener('pointerdown', () => {\n pressActive = true\n })\n window.addEventListener('pointerup', onPressUp)\n window.addEventListener('pointercancel', onPressCancel)\n function onPressUp() {\n settlePress(false)\n }\n function onPressCancel() {\n settlePress(true)\n }\n disposersForever.push(() => {\n window.removeEventListener('pointerup', onPressUp)\n window.removeEventListener('pointercancel', onPressCancel)\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","dispose","disposersForever","loading","disposers","wantAutoplay","gestureVideo","load","autoplay","adConfigPromise","PlayerCtor","resolvedAdConfig","construct","withAds","effective","player","pressActive","deferredConstruct","err","settlePress","cancelled","run","done","fire","onTapClick","onPressUp","onPressCancel","createGestureVideo","onPh","events","name","io","entries","e","onFirstInteraction"],"mappings":"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"}
|
package/dist/lazy.js
CHANGED
|
@@ -1,75 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { c as S } from "./gesture-BYJrYanO.js";
|
|
2
|
+
import { renderPlaceholder as V } from "./placeholder.js";
|
|
3
|
+
function z(d, N = {}) {
|
|
4
|
+
const s = typeof d == "string" ? document.querySelector(d) : d;
|
|
5
|
+
if (!s) throw new Error(`[itube-player] mount target not found: ${String(d)}`);
|
|
6
|
+
const { loadOn: i = "interaction", adConfig: v, ...r } = N, R = Array.isArray(r.source) ? r.source[0] : r.source, y = s.children.length > 0;
|
|
6
7
|
let n;
|
|
7
|
-
if (
|
|
8
|
-
n =
|
|
8
|
+
if (y)
|
|
9
|
+
n = s;
|
|
9
10
|
else {
|
|
10
11
|
const e = document.createElement("div");
|
|
11
|
-
e.innerHTML =
|
|
12
|
-
poster:
|
|
13
|
-
icon:
|
|
14
|
-
styling:
|
|
15
|
-
className:
|
|
16
|
-
}), n = e.firstElementChild,
|
|
12
|
+
e.innerHTML = V({
|
|
13
|
+
poster: R?.poster,
|
|
14
|
+
icon: r.icons?.bigPlay,
|
|
15
|
+
styling: r.styling,
|
|
16
|
+
className: r.className
|
|
17
|
+
}), n = e.firstElementChild, s.append(n);
|
|
17
18
|
}
|
|
18
|
-
let
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
let P, g;
|
|
20
|
+
const C = new Promise((e, o) => {
|
|
21
|
+
P = e, g = o;
|
|
21
22
|
});
|
|
22
|
-
|
|
23
|
+
C.catch(() => {
|
|
23
24
|
});
|
|
24
|
-
const
|
|
25
|
-
ready:
|
|
25
|
+
const u = {
|
|
26
|
+
ready: C,
|
|
26
27
|
player: null,
|
|
27
28
|
el: n,
|
|
28
29
|
destroy() {
|
|
29
|
-
|
|
30
|
+
h = !0, I();
|
|
31
|
+
for (const e of b.splice(0)) e();
|
|
32
|
+
y || n.remove(), u.player?.destroy();
|
|
30
33
|
}
|
|
31
34
|
};
|
|
32
|
-
let
|
|
33
|
-
const
|
|
34
|
-
for (const e of
|
|
35
|
+
let h = !1, w = !1;
|
|
36
|
+
const f = [], b = [], I = () => {
|
|
37
|
+
for (const e of f.splice(0)) e();
|
|
35
38
|
};
|
|
36
|
-
let
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
const o = Promise.resolve(typeof
|
|
39
|
+
let O = !1, p = null;
|
|
40
|
+
const c = (e) => {
|
|
41
|
+
if (O || (O = e), w || h) return;
|
|
42
|
+
w = !0, I();
|
|
43
|
+
const o = Promise.resolve(typeof v == "function" ? v() : v).catch(() => {
|
|
41
44
|
});
|
|
42
45
|
Promise.all([import("./index.js"), o]).then(
|
|
43
|
-
([{ Player: t },
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
([{ Player: t }, l]) => {
|
|
47
|
+
const a = () => {
|
|
48
|
+
if (h) return;
|
|
49
|
+
y ? s.textContent = "" : n.remove();
|
|
50
|
+
const L = l ? { ...r, adConfig: l } : r, x = O ? { ...L, playOnInit: !0, autoplay: !0 } : i === "interaction" || i === "placeholder" ? { ...L, playOnInit: !1, autoplay: !1 } : L;
|
|
51
|
+
p && (x.videoElement = p);
|
|
52
|
+
const F = new t(s, x);
|
|
53
|
+
u.player = F, P(F);
|
|
54
|
+
};
|
|
55
|
+
m ? E = a : a();
|
|
48
56
|
},
|
|
49
57
|
(t) => {
|
|
50
|
-
|
|
58
|
+
w = !1, g(t);
|
|
51
59
|
}
|
|
52
60
|
);
|
|
53
61
|
};
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
let m = !1, E = null;
|
|
63
|
+
const k = (e) => {
|
|
64
|
+
if (!m) return;
|
|
65
|
+
m = !1;
|
|
66
|
+
const o = E;
|
|
67
|
+
if (!o) return;
|
|
68
|
+
if (E = null, e) {
|
|
69
|
+
o();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let t = !1;
|
|
73
|
+
const l = () => {
|
|
74
|
+
t || (t = !0, n.removeEventListener("click", a), o());
|
|
75
|
+
}, a = () => setTimeout(l, 0);
|
|
76
|
+
n.addEventListener("click", a, { once: !0 }), setTimeout(l, 300);
|
|
77
|
+
};
|
|
78
|
+
n.addEventListener("pointerdown", () => {
|
|
79
|
+
m = !0;
|
|
80
|
+
}), window.addEventListener("pointerup", A), window.addEventListener("pointercancel", T);
|
|
81
|
+
function A() {
|
|
82
|
+
k(!1);
|
|
83
|
+
}
|
|
84
|
+
function T() {
|
|
85
|
+
k(!0);
|
|
86
|
+
}
|
|
87
|
+
if (b.push(() => {
|
|
88
|
+
window.removeEventListener("pointerup", A), window.removeEventListener("pointercancel", T);
|
|
89
|
+
}), n.addEventListener("click", () => {
|
|
90
|
+
!u.player && !p && (p = S()), c(!0);
|
|
91
|
+
}), i === "immediate")
|
|
92
|
+
c(!1);
|
|
56
93
|
else if (i === "placeholder") {
|
|
57
|
-
const e = () =>
|
|
94
|
+
const e = () => c(!1), o = ["pointerenter", "pointerdown", "touchstart", "focusin"];
|
|
58
95
|
for (const t of o)
|
|
59
|
-
n.addEventListener(t, e, { once: !0, passive: !0 }),
|
|
96
|
+
n.addEventListener(t, e, { once: !0, passive: !0 }), f.push(() => n.removeEventListener(t, e));
|
|
60
97
|
} else if (i === "visible" && typeof IntersectionObserver < "u") {
|
|
61
98
|
const e = new IntersectionObserver((o) => {
|
|
62
|
-
o.some((t) => t.isIntersecting) &&
|
|
99
|
+
o.some((t) => t.isIntersecting) && c(!1);
|
|
63
100
|
});
|
|
64
|
-
e.observe(n),
|
|
101
|
+
e.observe(n), f.push(() => e.disconnect());
|
|
65
102
|
} else {
|
|
66
|
-
const e = () =>
|
|
103
|
+
const e = () => c(!1), o = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
67
104
|
for (const t of o)
|
|
68
|
-
document.addEventListener(t, e, { once: !0, passive: !0 }),
|
|
105
|
+
document.addEventListener(t, e, { once: !0, passive: !0 }), f.push(() => document.removeEventListener(t, e));
|
|
69
106
|
}
|
|
70
|
-
return
|
|
107
|
+
return u;
|
|
71
108
|
}
|
|
72
109
|
export {
|
|
73
|
-
|
|
110
|
+
z as createLazyPlayer
|
|
74
111
|
};
|
|
75
112
|
//# 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 { 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":";AAyDO,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;AACnB,QAAMC,IAAO,CAACC,MAAsB;AAElC,QADAF,UAAiBE,IACbL,KAAWF,EAAW;AAC1B,IAAAE,IAAU,IACVD,EAAA;AAGA,UAAMO,IAAkB,QAAQ,QAAQ,OAAOrB,KAAa,aAAaA,EAAA,IAAaA,CAAQ,EAC3F,MAAM,MAAA;AAAA,KAAe;AAExB,YAAQ,IAAI,CAAC,OAAO,YAAS,GAAGqB,CAAe,CAAC,EAAE;AAAA,MAChD,CAAC,CAAC,EAAE,QAAQC,EAAA,GAAcC,CAAgB,MAAM;AAC9C,YAAIV,EAAW;AAGf,QAAIV,MAAe,cAAc,OACzB,OAAA;AAOR,cAAMqB,IAAyBD,IAC3B,EAAE,GAAGtB,GAAe,UAAUsB,MAC9BtB,GACEwB,IAAYP,IACd,EAAE,GAAGM,GAAS,YAAY,IAAM,UAAU,OAC1CzB,MAAW,iBAAiBA,MAAW,gBACrC,EAAE,GAAGyB,GAAS,YAAY,IAAO,UAAU,GAAA,IAC3CA,GACAE,IAAS,IAAIJ,EAAWxB,GAAO2B,CAAS;AAC9C,QAAAb,EAAI,SAASc,GACbnB,EAAamB,CAAM;AAAA,MACrB;AAAA,MACA,CAACC,MAAQ;AACP,QAAAZ,IAAU,IACVP,EAAYmB,CAAG;AAAA,MACjB;AAAA,IAAA;AAAA,EAEJ;AAOA,MAFAvB,EAAG,iBAAiB,SAAS,MAAMe,EAAK,EAAI,CAAC,GAEzCpB,MAAW;AACb,IAAAoB,EAAK,EAAK;AAAA,WACDpB,MAAW,eAAe;AAInC,UAAM6B,IAAO,MAAMT,EAAK,EAAK,GACvBU,IAA2C,CAAC,gBAAgB,eAAe,cAAc,SAAS;AACxG,eAAWC,KAAQD;AACjB,MAAAzB,EAAG,iBAAiB0B,GAAMF,GAAM,EAAE,MAAM,IAAM,SAAS,IAAM,GAC7DZ,EAAU,KAAK,MAAMZ,EAAG,oBAAoB0B,GAAMF,CAAI,CAAC;AAAA,EAE3D,WAAW7B,MAAW,aAAa,OAAO,uBAAyB,KAAa;AAC9E,UAAMgC,IAAK,IAAI,qBAAqB,CAACC,MAAY;AAC/C,MAAIA,EAAQ,KAAK,CAACC,MAAMA,EAAE,cAAc,OAAQ,EAAK;AAAA,IACvD,CAAC;AACD,IAAAF,EAAG,QAAQ3B,CAAE,GACbY,EAAU,KAAK,MAAMe,EAAG,WAAA,CAAY;AAAA,EACtC,OAAO;AACL,UAAMG,IAAqB,MAAMf,EAAK,EAAK,GACrCU,IAAwC,CAAC,eAAe,WAAW,cAAc,OAAO;AAC9F,eAAWC,KAAQD;AACjB,eAAS,iBAAiBC,GAAMI,GAAoB,EAAE,MAAM,IAAM,SAAS,IAAM,GACjFlB,EAAU,KAAK,MAAM,SAAS,oBAAoBc,GAAMI,CAAkB,CAAC;AAAA,EAE/E;AAEA,SAAOtB;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 for (const dispose of disposersForever.splice(0)) dispose()\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 // `disposers` are cleared when a load starts; `disposersForever` (press\n // tracking) live until destroy — the press race spans the load itself.\n const disposers: Array<() => void> = []\n const disposersForever: 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 const construct = () => {\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 // A chunk that resolves MID-TAP (touchstart already preloads it) must\n // not swap the placeholder out from under the finger: the tap's click\n // would die on the detached node and the play intent (+ the gesture\n // unlock) would be lost — \"the first tap only inits\" bug. Defer the\n // swap until the press settles.\n if (pressActive) deferredConstruct = construct\n else construct()\n },\n (err) => {\n loading = false\n rejectReady(err)\n },\n )\n }\n\n // --- press tracking for the mid-tap construction race (see above) --------\n let pressActive = false\n let deferredConstruct: (() => void) | null = null\n const settlePress = (cancelled: boolean) => {\n if (!pressActive) return\n pressActive = false\n const run = deferredConstruct\n if (!run) return\n deferredConstruct = null\n if (cancelled) {\n // The press became a scroll — no click is coming, construct right away.\n run()\n return\n }\n // The tap's click lands right after pointerup. Let it register the play\n // intent first (our listener runs after the intent handler on ph), with a\n // timeout fallback for browsers that end up not firing click at all.\n let done = false\n const fire = () => {\n if (done) return\n done = true\n ph.removeEventListener('click', onTapClick)\n run()\n }\n const onTapClick = () => setTimeout(fire, 0)\n ph.addEventListener('click', onTapClick, { once: true })\n setTimeout(fire, 300)\n }\n ph.addEventListener('pointerdown', () => {\n pressActive = true\n })\n window.addEventListener('pointerup', onPressUp)\n window.addEventListener('pointercancel', onPressCancel)\n function onPressUp() {\n settlePress(false)\n }\n function onPressCancel() {\n settlePress(true)\n }\n disposersForever.push(() => {\n window.removeEventListener('pointerup', onPressUp)\n window.removeEventListener('pointercancel', onPressCancel)\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","dispose","disposersForever","loading","disposers","wantAutoplay","gestureVideo","load","autoplay","adConfigPromise","PlayerCtor","resolvedAdConfig","construct","withAds","effective","player","pressActive","deferredConstruct","err","settlePress","cancelled","run","done","fire","onTapClick","onPressUp","onPressCancel","createGestureVideo","onPh","events","name","io","entries","e","onFirstInteraction"],"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
|
@@ -333,6 +333,15 @@ export interface ControlsOptions {
|
|
|
333
333
|
* over the video on mobile). This is the pre-0.3 desktop behaviour.
|
|
334
334
|
*/
|
|
335
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';
|
|
336
345
|
/**
|
|
337
346
|
* Left-to-right order of the right-cluster controls (gear, pip, playlist, …).
|
|
338
347
|
* Omit / empty → built-in order. Listed keys go first (in this order); any not
|
|
@@ -343,11 +352,11 @@ export interface ControlsOptions {
|
|
|
343
352
|
playlist?: boolean;
|
|
344
353
|
/**
|
|
345
354
|
* Hide the "previous" button in the control bar (the bar keeps only "next").
|
|
346
|
-
* Default true.
|
|
347
|
-
* 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.
|
|
348
357
|
*/
|
|
349
358
|
hidePrev?: boolean;
|
|
350
|
-
/** Hide the bar "next" button (
|
|
359
|
+
/** Hide the bar "next" button (`mobileLayout: "center"` still shows next in the center cluster). Default false. */
|
|
351
360
|
hideNext?: boolean;
|
|
352
361
|
/** Show the playlist-panel (list) button. Default true — set false to hide it even when a playlist is present. */
|
|
353
362
|
playlistButton?: boolean;
|
|
@@ -536,6 +545,14 @@ export interface PlayerOptions {
|
|
|
536
545
|
crossOrigin?: '' | 'anonymous' | 'use-credentials';
|
|
537
546
|
/** Inline playback on iOS. Default true. */
|
|
538
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;
|
|
539
556
|
/**
|
|
540
557
|
* Persist user preferences (volume/mute, autoplay-next) to `localStorage`
|
|
541
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";var
|
|
1
|
+
"use strict";var H=Object.create;var A=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,G=Object.prototype.hasOwnProperty;var D=(e,a,l,f)=>{if(a&&typeof a=="object"||typeof a=="function")for(let i of U(a))!G.call(e,i)&&i!==l&&A(e,i,{get:()=>a[i],enumerable:!(f=R(a,i))||f.enumerable});return e};var J=(e,a,l)=>(l=e!=null?H(F(e)):{},D(a||!e||!e.__esModule?A(l,"default",{value:e,enumerable:!0}):l,e));Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const n=require("vue"),K=require("./gesture-BLWiPHlW.cjs"),Q=require("./placeholder.cjs"),M=["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 W(){return import("itube-modern-player/style.css").catch(()=>{})}const j=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:[...M],setup(e,{emit:a,slots:l,expose:f}){const i=n.ref(null),g=n.ref(null),w=n.ref(null),u=n.shallowRef(null),L=n.ref(!1);let E=!1,b=!1,P=!1,v=null;const p=[],C=()=>{for(const t of p.splice(0))t()},z=n.computed(()=>Array.isArray(e.source)?e.source[0]:e.source),N=n.computed(()=>Q.renderPlaceholder({poster:z.value?.poster,icon:e.options.icons?.bigPlay,styling:e.options.styling,className:e.options.className},{inlineStyles:!0})),y=t=>{if(P||(P=t),b||E)return;b=!0,C();const r=e.adConfig??e.options.adConfig,s=Promise.resolve(typeof r=="function"?r():r).catch(()=>{});Promise.all([Promise.resolve().then(()=>require("./index.cjs")),s,W()]).then(([{Player:o},c])=>{const d=()=>{if(E||!i.value)return;const m={...e.options,source:e.source,...c?{adConfig:c}:{},...l.pauseScreen?{pauseScreen:!0}:{}};e.lazy&&(m.playOnInit=P,m.autoplay=P,v&&(m.videoElement=v));const S=new o(i.value,m),_=a;for(const q of M)S.on(q,B=>_(q,B));l.pauseScreen&&w.value&&S.setPauseScreenContent(w.value),u.value=S,L.value=!0};h?k=d:d()},o=>{b=!1,console.error("[itube-player] failed to load the player chunk",o)})};let h=!1,k=null;const O=t=>{if(!h)return;h=!1;const r=k;if(!r)return;if(k=null,t){r();return}const s=g.value;let o=!1;const c=()=>{o||(o=!0,s?.removeEventListener("click",d),r())},d=()=>setTimeout(c,0);s?.addEventListener("click",d,{once:!0}),setTimeout(c,300)},T=()=>O(!1),I=()=>O(!0);n.onMounted(()=>{if(!e.lazy){y(!1);return}const t=g.value;if(e.loadOn==="immediate"||!t){y(!1);return}t.addEventListener("pointerdown",()=>{h=!0}),window.addEventListener("pointerup",T),window.addEventListener("pointercancel",I);const r=()=>y(!1);if(e.loadOn==="interaction"){const s=["pointerdown","keydown","touchstart","wheel"];for(const o of s)document.addEventListener(o,r,{once:!0,passive:!0}),p.push(()=>document.removeEventListener(o,r))}else if(e.loadOn==="visible"&&typeof IntersectionObserver<"u"){const s=new IntersectionObserver(o=>{o.some(c=>c.isIntersecting)&&r()});s.observe(t),p.push(()=>s.disconnect())}else{const s=["pointerenter","pointerdown","touchstart","focusin"];for(const o of s)t.addEventListener(o,r,{once:!0,passive:!0}),p.push(()=>t.removeEventListener(o,r))}}),n.watch(()=>e.source,t=>{t&&u.value&&u.value.load(t)},{deep:!0}),n.onBeforeUnmount(()=>{E=!0,C(),window.removeEventListener("pointerup",T),window.removeEventListener("pointercancel",I),u.value?.destroy(),u.value=null}),f({player:u});const V=()=>{u.value?.play()};return()=>{const t=[];return e.lazy&&!L.value&&t.push(n.h("div",{key:"imp-ph",ref:g,class:"imp-vue-placeholder",innerHTML:N.value,onClick:()=>{!u.value&&!v&&(v=K.createGestureVideo()),y(!0)}})),t.push(n.h("div",{key:"imp-mount",ref:i})),l.pauseScreen&&t.push(n.h("div",{ref:w,class:"imp-vue-pause-slot"},l.pauseScreen({player:u.value,close:V}))),n.h("div",{class:"imp-vue-host"},t)}}});exports.ITubePlayer=j;exports.default=j;
|
|
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`).\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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"}
|
|
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 { createGestureVideo } from './gesture'\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 /** Media element unlocked inside the click gesture (see gesture.ts). */\n let gestureVideo: HTMLVideoElement | null = null\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 const construct = () => {\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 if (gestureVideo) opts.videoElement = gestureVideo\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 // A chunk that resolves MID-TAP (the touch already preloads it) must\n // not unmount the placeholder from under the finger: the tap's click\n // would die with it and the play intent (+ gesture unlock) would be\n // lost — \"the first tap only inits\" bug. Defer until the press settles.\n if (pressActive) deferredConstruct = construct\n else construct()\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 // --- press tracking for the mid-tap construction race (see above) ------\n let pressActive = false\n let deferredConstruct: (() => void) | null = null\n const settlePress = (cancelled: boolean) => {\n if (!pressActive) return\n pressActive = false\n const run = deferredConstruct\n if (!run) return\n deferredConstruct = null\n if (cancelled) {\n // The press became a scroll — no click is coming.\n run()\n return\n }\n // The tap's click lands right after pointerup: let it register the play\n // intent first, with a timeout fallback if no click arrives.\n const ph = phEl.value\n let done = false\n const fire = () => {\n if (done) return\n done = true\n ph?.removeEventListener('click', onTapClick)\n run()\n }\n const onTapClick = () => setTimeout(fire, 0)\n ph?.addEventListener('click', onTapClick, { once: true })\n setTimeout(fire, 300)\n }\n const onPressUp = () => settlePress(false)\n const onPressCancel = () => settlePress(true)\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 // Press tracking (survives the trigger cleanup at load start — the\n // mid-tap race spans the load itself; removed on unmount).\n ph.addEventListener('pointerdown', () => {\n pressActive = true\n })\n window.addEventListener('pointerup', onPressUp)\n window.addEventListener('pointercancel', onPressCancel)\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 window.removeEventListener('pointerup', onPressUp)\n window.removeEventListener('pointercancel', onPressCancel)\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 // The gesture video MUST be created synchronously in the click\n // handler — it carries the user-activation flag across the async\n // chunk load so the autostart can play with sound on mobile.\n onClick: () => {\n if (!player.value && !gestureVideo) gestureVideo = createGestureVideo()\n load(true)\n },\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","gestureVideo","disposers","cleanupTriggers","d","firstSource","computed","placeholderHtml","renderPlaceholder","load","autoplay","adConfigInput","adConfigPromise","PlayerCtor","adCfg","construct","opts","instance","forward","name","payload","pressActive","deferredConstruct","err","settlePress","cancelled","run","ph","done","fire","onTapClick","onPressUp","onPressCancel","onMounted","bg","events","io","entries","e","watch","next","onBeforeUnmount","close","children","h","createGestureVideo"],"mappings":"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"}
|