itube-modern-player 0.8.1 → 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 +6 -0
- package/dist/gesture-BLWiPHlW.cjs +2 -0
- package/dist/{gesture-4dfbCdW5.cjs.map → gesture-BLWiPHlW.cjs.map} +1 -1
- package/dist/{gesture-SgY5sB4t.js → gesture-BYJrYanO.js} +4 -4
- package/dist/{gesture-SgY5sB4t.js.map → gesture-BYJrYanO.js.map} +1 -1
- package/dist/lazy.cjs +1 -1
- package/dist/lazy.cjs.map +1 -1
- package/dist/lazy.js +75 -43
- package/dist/lazy.js.map +1 -1
- package/dist/vue.cjs +1 -1
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +97 -74
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
- package/dist/gesture-4dfbCdW5.cjs +0 -2
package/README.md
CHANGED
|
@@ -890,6 +890,12 @@ npm publish # prepublishOnly прогонит typecheck + build
|
|
|
890
890
|
|
|
891
891
|
Версионирование по [SemVer](https://semver.org/lang/ru/): `major.minor.patch`.
|
|
892
892
|
|
|
893
|
+
### 0.8.2
|
|
894
|
+
|
|
895
|
+
- **Фикс: тап по плейсхолдеру на мобиле всё ещё мог «только инициализировать» плеер** (довершение фикса двойного тапа из 0.8.1) — закрыты две гонки:
|
|
896
|
+
1. На быстром/закэшированном чанке `touchstart`-предзагрузка (`loadOn: 'placeholder'`) успевала **свопнуть плейсхолдер из-под пальца** посреди тапа — `click` умирал на отсоединённой ноде, намерение «играть» и жест-разблокировка терялись. Теперь при активном нажатии конструирование плеера откладывается до завершения тапа: клик успевает зарегистрировать намерение (в т.ч. блессинг видео), затем плеер конструируется и стартует; скролл-отмена (`pointercancel`) конструирует сразу, без автостарта. Исправлено и в `createLazyPlayer`, и во Vue-`lazy`.
|
|
897
|
+
2. Безсорсовый `play()` жест-разблокировки (`gesture.ts`) в некоторых движках не реджектится, а висит до появления источника — и резолвится в момент **реального** старта видео; его success-обработчик (`video.pause()`) ставил только что запущенное видео на паузу. Теперь success-ветка — no-op.
|
|
898
|
+
|
|
893
899
|
### 0.8.1
|
|
894
900
|
|
|
895
901
|
- **Новый мобильный лейаут по умолчанию** — `controls.mobileLayout: 'bar' | 'center'`. `'bar'` (default): поверх видео остаются только ±N-перемотка и большая play (как на десктопе), а кнопка play и «следующее видео» доступны в нижнем баре слева — по аналогии с десктопом. `'center'` — прежний вариант (prev · −N · play · +N · next кластером по центру, play/next в баре скрыты); вернуть старое поведение: `controls: { mobileLayout: 'center' }`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gesture-
|
|
1
|
+
{"version":3,"file":"gesture-BLWiPHlW.cjs","sources":["../src/gesture.ts"],"sourcesContent":["/**\n * User-gesture media unlock, shared by the lazy entries (`createLazyPlayer`\n * and the Vue wrapper's `lazy` mode).\n *\n * Mobile browsers (iOS Safari first of all) allow `play()` with sound only\n * synchronously inside a user gesture — and the permission is granted PER\n * ELEMENT. The lazy flow crosses two async boundaries between the tap and the\n * autostart (the `import()` of the player chunk + the HLS attach), so a video\n * element created after them has no gesture attached: the first tap only\n * loaded the player and playback started on the second tap.\n *\n * The fix: create the `<video>` synchronously in the click handler and call\n * `play()` on it right there. Without a src it rejects immediately (that's\n * fine and expected), but the element keeps its user-activation flag — when\n * the player later attaches the real source to THIS element, `play()` with\n * sound goes through.\n */\nexport function createGestureVideo(): HTMLVideoElement {\n const video = document.createElement('video')\n video.setAttribute('playsinline', '')\n const attempt = video.play()\n // No source yet → the promise usually rejects (NotSupportedError); the\n // play() CALL inside the gesture is what unlocks the element, not its\n // success. NB: in some engines this promise stays PENDING until a source\n // arrives and only resolves once REAL playback starts — so the success\n // branch must be a no-op. (A `video.pause()` here paused the just-started\n // video, killing the very autostart this helper exists for.)\n if (attempt) attempt.catch(() => {})\n return video\n}\n"],"names":["createGestureVideo","video","attempt"],"mappings":"aAiBO,SAASA,GAAuC,CACrD,MAAMC,EAAQ,SAAS,cAAc,OAAO,EAC5CA,EAAM,aAAa,cAAe,EAAE,EACpC,MAAMC,EAAUD,EAAM,KAAA,EAOtB,OAAIC,GAASA,EAAQ,MAAM,IAAM,CAAC,CAAC,EAC5BD,CACT"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
function
|
|
1
|
+
function c() {
|
|
2
2
|
const e = document.createElement("video");
|
|
3
3
|
e.setAttribute("playsinline", "");
|
|
4
4
|
const t = e.play();
|
|
5
|
-
return t && t.
|
|
5
|
+
return t && t.catch(() => {
|
|
6
6
|
}), e;
|
|
7
7
|
}
|
|
8
8
|
export {
|
|
9
|
-
|
|
9
|
+
c
|
|
10
10
|
};
|
|
11
|
-
//# sourceMappingURL=gesture-
|
|
11
|
+
//# sourceMappingURL=gesture-BYJrYanO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gesture-
|
|
1
|
+
{"version":3,"file":"gesture-BYJrYanO.js","sources":["../src/gesture.ts"],"sourcesContent":["/**\n * User-gesture media unlock, shared by the lazy entries (`createLazyPlayer`\n * and the Vue wrapper's `lazy` mode).\n *\n * Mobile browsers (iOS Safari first of all) allow `play()` with sound only\n * synchronously inside a user gesture — and the permission is granted PER\n * ELEMENT. The lazy flow crosses two async boundaries between the tap and the\n * autostart (the `import()` of the player chunk + the HLS attach), so a video\n * element created after them has no gesture attached: the first tap only\n * loaded the player and playback started on the second tap.\n *\n * The fix: create the `<video>` synchronously in the click handler and call\n * `play()` on it right there. Without a src it rejects immediately (that's\n * fine and expected), but the element keeps its user-activation flag — when\n * the player later attaches the real source to THIS element, `play()` with\n * sound goes through.\n */\nexport function createGestureVideo(): HTMLVideoElement {\n const video = document.createElement('video')\n video.setAttribute('playsinline', '')\n const attempt = video.play()\n // No source yet → the promise usually rejects (NotSupportedError); the\n // play() CALL inside the gesture is what unlocks the element, not its\n // success. NB: in some engines this promise stays PENDING until a source\n // arrives and only resolves once REAL playback starts — so the success\n // branch must be a no-op. (A `video.pause()` here paused the just-started\n // video, killing the very autostart this helper exists for.)\n if (attempt) attempt.catch(() => {})\n return video\n}\n"],"names":["createGestureVideo","video","attempt"],"mappings":"AAiBO,SAASA,IAAuC;AACrD,QAAMC,IAAQ,SAAS,cAAc,OAAO;AAC5C,EAAAA,EAAM,aAAa,eAAe,EAAE;AACpC,QAAMC,IAAUD,EAAM,KAAA;AAOtB,SAAIC,KAASA,EAAQ,MAAM,MAAM;AAAA,EAAC,CAAC,GAC5BD;AACT;"}
|
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 { 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":"yJA0DO,SAASA,EAAiBC,EAA8BC,EAA6B,GAAgB,CAC1G,MAAMC,EAAQ,OAAOF,GAAW,SAAW,SAAS,cAA2BA,CAAM,EAAIA,EACzF,GAAI,CAACE,EAAO,MAAM,IAAI,MAAM,0CAA0C,OAAOF,CAAM,CAAC,EAAE,EAEtF,KAAM,CAAE,OAAAG,EAAS,cAAe,SAAAC,EAAU,GAAGC,GAAkBJ,EACzDK,EAAQ,MAAM,QAAQD,EAAc,MAAM,EAAIA,EAAc,OAAO,CAAC,EAAIA,EAAc,OAOtFE,EAAUL,EAAM,SAAS,OAAS,EACxC,IAAIM,EACJ,GAAID,EACFC,EAAKN,MACA,CAKL,MAAMO,EAAM,SAAS,cAAc,KAAK,EACxCA,EAAI,UAAYC,oBAAkB,CAChC,OAAQJ,GAAO,OACf,KAAMD,EAAc,OAAO,QAC3B,QAASA,EAAc,QACvB,UAAWA,EAAc,SAAA,CAC1B,EACDG,EAAKC,EAAI,kBACTP,EAAM,OAAOM,CAAE,CACjB,CAEA,IAAIG,EACAC,EACJ,MAAMC,EAAQ,IAAI,QAAgB,CAACC,EAASC,IAAW,CACrDJ,EAAeG,EACfF,EAAcG,CAChB,CAAC,EAEDF,EAAM,MAAM,IAAM,CAAC,CAAC,EAEpB,MAAMG,EAAkB,CACtB,MAAAH,EACA,OAAQ,KACR,GAAIL,EACJ,SAAU,CACRS,EAAY,GACZC,EAAA,EAGKX,GAASC,EAAG,OAAA,EACjBQ,EAAI,QAAQ,QAAA,CACd,CAAA,EAGF,IAAIC,EAAY,GACZE,EAAU,GACd,MAAMC,EAA+B,CAAA,EAC/BF,EAAU,IAAM,CACpB,UAAWG,KAAWD,EAAU,OAAO,CAAC,EAAGC,EAAA,CAC7C,EAKA,IAAIC,EAAe,GAIfC,EAAwC,KAC5C,MAAMC,EAAQC,GAAsB,CAElC,GADAH,MAAiBG,GACbN,GAAWF,EAAW,OAC1BE,EAAU,GACVD,EAAA,EAGA,MAAMQ,EAAkB,QAAQ,QAAQ,OAAOtB,GAAa,WAAaA,EAAA,EAAaA,CAAQ,EAC3F,MAAM,IAAA,EAAe,EAExB,QAAQ,IAAI,CAAC,QAAA,QAAA,EAAA,KAAA,IAAA,QAAO,aAAS,CAAA,EAAGsB,CAAe,CAAC,EAAE,KAChD,CAAC,CAAC,CAAE,OAAQC,CAAA,EAAcC,CAAgB,IAAM,CAC9C,GAAIX,EAAW,OAGXV,IAAe,YAAc,KACzB,OAAA,EAOR,MAAMsB,EAAyBD,EAC3B,CAAE,GAAGvB,EAAe,SAAUuB,GAC9BvB,EACEyB,EAAYR,EACd,CAAE,GAAGO,EAAS,WAAY,GAAM,SAAU,IAC1C1B,IAAW,eAAiBA,IAAW,cACrC,CAAE,GAAG0B,EAAS,WAAY,GAAO,SAAU,EAAA,EAC3CA,EACFN,MAAwB,aAAeA,GAC3C,MAAMQ,EAAS,IAAIJ,EAAWzB,EAAO4B,CAAS,EAC9Cd,EAAI,OAASe,EACbpB,EAAaoB,CAAM,CACrB,EACCC,GAAQ,CACPb,EAAU,GACVP,EAAYoB,CAAG,CACjB,CAAA,CAEJ,EAYA,GALAxB,EAAG,iBAAiB,QAAS,IAAM,CAC7B,CAACQ,EAAI,QAAU,CAACO,MAA6BU,EAAAA,mBAAA,GACjDT,EAAK,EAAI,CACX,CAAC,EAEGrB,IAAW,YACbqB,EAAK,EAAK,UACDrB,IAAW,cAAe,CAInC,MAAM+B,EAAO,IAAMV,EAAK,EAAK,EACvBW,EAA2C,CAAC,eAAgB,cAAe,aAAc,SAAS,EACxG,UAAWC,KAAQD,EACjB3B,EAAG,iBAAiB4B,EAAMF,EAAM,CAAE,KAAM,GAAM,QAAS,GAAM,EAC7Dd,EAAU,KAAK,IAAMZ,EAAG,oBAAoB4B,EAAMF,CAAI,CAAC,CAE3D,SAAW/B,IAAW,WAAa,OAAO,qBAAyB,IAAa,CAC9E,MAAMkC,EAAK,IAAI,qBAAsBC,GAAY,CAC3CA,EAAQ,KAAMC,GAAMA,EAAE,cAAc,KAAQ,EAAK,CACvD,CAAC,EACDF,EAAG,QAAQ7B,CAAE,EACbY,EAAU,KAAK,IAAMiB,EAAG,WAAA,CAAY,CACtC,KAAO,CACL,MAAMG,EAAqB,IAAMhB,EAAK,EAAK,EACrCW,EAAwC,CAAC,cAAe,UAAW,aAAc,OAAO,EAC9F,UAAWC,KAAQD,EACjB,SAAS,iBAAiBC,EAAMI,EAAoB,CAAE,KAAM,GAAM,QAAS,GAAM,EACjFpB,EAAU,KAAK,IAAM,SAAS,oBAAoBgB,EAAMI,CAAkB,CAAC,CAE/E,CAEA,OAAOxB,CACT"}
|
|
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,80 +1,112 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import { renderPlaceholder as
|
|
3
|
-
function
|
|
4
|
-
const s = typeof
|
|
5
|
-
if (!s) throw new Error(`[itube-player] mount target not found: ${String(
|
|
6
|
-
const { loadOn: i = "interaction", adConfig:
|
|
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;
|
|
7
7
|
let n;
|
|
8
|
-
if (
|
|
8
|
+
if (y)
|
|
9
9
|
n = s;
|
|
10
10
|
else {
|
|
11
11
|
const e = document.createElement("div");
|
|
12
|
-
e.innerHTML =
|
|
13
|
-
poster:
|
|
12
|
+
e.innerHTML = V({
|
|
13
|
+
poster: R?.poster,
|
|
14
14
|
icon: r.icons?.bigPlay,
|
|
15
15
|
styling: r.styling,
|
|
16
16
|
className: r.className
|
|
17
17
|
}), n = e.firstElementChild, s.append(n);
|
|
18
18
|
}
|
|
19
|
-
let
|
|
20
|
-
const
|
|
21
|
-
|
|
19
|
+
let P, g;
|
|
20
|
+
const C = new Promise((e, o) => {
|
|
21
|
+
P = e, g = o;
|
|
22
22
|
});
|
|
23
|
-
|
|
23
|
+
C.catch(() => {
|
|
24
24
|
});
|
|
25
|
-
const
|
|
26
|
-
ready:
|
|
25
|
+
const u = {
|
|
26
|
+
ready: C,
|
|
27
27
|
player: null,
|
|
28
28
|
el: n,
|
|
29
29
|
destroy() {
|
|
30
|
-
|
|
30
|
+
h = !0, I();
|
|
31
|
+
for (const e of b.splice(0)) e();
|
|
32
|
+
y || n.remove(), u.player?.destroy();
|
|
31
33
|
}
|
|
32
34
|
};
|
|
33
|
-
let
|
|
34
|
-
const
|
|
35
|
-
for (const e of
|
|
35
|
+
let h = !1, w = !1;
|
|
36
|
+
const f = [], b = [], I = () => {
|
|
37
|
+
for (const e of f.splice(0)) e();
|
|
36
38
|
};
|
|
37
|
-
let
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
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(() => {
|
|
42
44
|
});
|
|
43
45
|
Promise.all([import("./index.js"), o]).then(
|
|
44
|
-
([{ Player: t },
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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();
|
|
51
56
|
},
|
|
52
57
|
(t) => {
|
|
53
|
-
|
|
58
|
+
w = !1, g(t);
|
|
54
59
|
}
|
|
55
60
|
);
|
|
56
61
|
};
|
|
57
|
-
|
|
58
|
-
|
|
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);
|
|
59
91
|
}), i === "immediate")
|
|
60
|
-
|
|
92
|
+
c(!1);
|
|
61
93
|
else if (i === "placeholder") {
|
|
62
|
-
const e = () =>
|
|
94
|
+
const e = () => c(!1), o = ["pointerenter", "pointerdown", "touchstart", "focusin"];
|
|
63
95
|
for (const t of o)
|
|
64
|
-
n.addEventListener(t, e, { once: !0, passive: !0 }),
|
|
96
|
+
n.addEventListener(t, e, { once: !0, passive: !0 }), f.push(() => n.removeEventListener(t, e));
|
|
65
97
|
} else if (i === "visible" && typeof IntersectionObserver < "u") {
|
|
66
98
|
const e = new IntersectionObserver((o) => {
|
|
67
|
-
o.some((t) => t.isIntersecting) &&
|
|
99
|
+
o.some((t) => t.isIntersecting) && c(!1);
|
|
68
100
|
});
|
|
69
|
-
e.observe(n),
|
|
101
|
+
e.observe(n), f.push(() => e.disconnect());
|
|
70
102
|
} else {
|
|
71
|
-
const e = () =>
|
|
103
|
+
const e = () => c(!1), o = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
72
104
|
for (const t of o)
|
|
73
|
-
document.addEventListener(t, e, { once: !0, passive: !0 }),
|
|
105
|
+
document.addEventListener(t, e, { once: !0, passive: !0 }), f.push(() => document.removeEventListener(t, e));
|
|
74
106
|
}
|
|
75
|
-
return
|
|
107
|
+
return u;
|
|
76
108
|
}
|
|
77
109
|
export {
|
|
78
|
-
|
|
110
|
+
z as createLazyPlayer
|
|
79
111
|
};
|
|
80
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 { 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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"}
|
|
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":";;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;AACA,iBAAWC,KAAWC,EAAiB,OAAO,CAAC,EAAG,CAAAD,EAAA;AAGlD,MAAKZ,KAASC,EAAG,OAAA,GACjBQ,EAAI,QAAQ,QAAA;AAAA,IACd;AAAA,EAAA;AAGF,MAAIC,IAAY,IACZI,IAAU;AAGd,QAAMC,IAA+B,CAAA,GAC/BF,IAAsC,CAAA,GACtCF,IAAU,MAAM;AACpB,eAAWC,KAAWG,EAAU,OAAO,CAAC,EAAG,CAAAH,EAAA;AAAA,EAC7C;AAKA,MAAII,IAAe,IAIfC,IAAwC;AAC5C,QAAMC,IAAO,CAACC,MAAsB;AAElC,QADAH,UAAiBG,IACbL,KAAWJ,EAAW;AAC1B,IAAAI,IAAU,IACVH,EAAA;AAGA,UAAMS,IAAkB,QAAQ,QAAQ,OAAOvB,KAAa,aAAaA,EAAA,IAAaA,CAAQ,EAC3F,MAAM,MAAA;AAAA,KAAe;AAExB,YAAQ,IAAI,CAAC,OAAO,YAAS,GAAGuB,CAAe,CAAC,EAAE;AAAA,MAChD,CAAC,CAAC,EAAE,QAAQC,EAAA,GAAcC,CAAgB,MAAM;AAC9C,cAAMC,IAAY,MAAM;AACtB,cAAIb,EAAW;AAGf,UAAIV,MAAe,cAAc,OACzB,OAAA;AAOR,gBAAMwB,IAAyBF,IAC3B,EAAE,GAAGxB,GAAe,UAAUwB,MAC9BxB,GACE2B,IAAYT,IACd,EAAE,GAAGQ,GAAS,YAAY,IAAM,UAAU,OAC1C5B,MAAW,iBAAiBA,MAAW,gBACrC,EAAE,GAAG4B,GAAS,YAAY,IAAO,UAAU,GAAA,IAC3CA;AACN,UAAIP,QAAwB,eAAeA;AAC3C,gBAAMS,IAAS,IAAIL,EAAW1B,GAAO8B,CAAS;AAC9C,UAAAhB,EAAI,SAASiB,GACbtB,EAAasB,CAAM;AAAA,QACrB;AAMA,QAAIC,IAAaC,IAAoBL,IAChCA,EAAA;AAAA,MACP;AAAA,MACA,CAACM,MAAQ;AACP,QAAAf,IAAU,IACVT,EAAYwB,CAAG;AAAA,MACjB;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAIF,IAAc,IACdC,IAAyC;AAC7C,QAAME,IAAc,CAACC,MAAuB;AAC1C,QAAI,CAACJ,EAAa;AAClB,IAAAA,IAAc;AACd,UAAMK,IAAMJ;AACZ,QAAI,CAACI,EAAK;AAEV,QADAJ,IAAoB,MAChBG,GAAW;AAEb,MAAAC,EAAA;AACA;AAAA,IACF;AAIA,QAAIC,IAAO;AACX,UAAMC,IAAO,MAAM;AACjB,MAAID,MACJA,IAAO,IACPhC,EAAG,oBAAoB,SAASkC,CAAU,GAC1CH,EAAA;AAAA,IACF,GACMG,IAAa,MAAM,WAAWD,GAAM,CAAC;AAC3C,IAAAjC,EAAG,iBAAiB,SAASkC,GAAY,EAAE,MAAM,IAAM,GACvD,WAAWD,GAAM,GAAG;AAAA,EACtB;AACA,EAAAjC,EAAG,iBAAiB,eAAe,MAAM;AACvC,IAAA0B,IAAc;AAAA,EAChB,CAAC,GACD,OAAO,iBAAiB,aAAaS,CAAS,GAC9C,OAAO,iBAAiB,iBAAiBC,CAAa;AACtD,WAASD,IAAY;AACnB,IAAAN,EAAY,EAAK;AAAA,EACnB;AACA,WAASO,IAAgB;AACvB,IAAAP,EAAY,EAAI;AAAA,EAClB;AAgBA,MAfAjB,EAAiB,KAAK,MAAM;AAC1B,WAAO,oBAAoB,aAAauB,CAAS,GACjD,OAAO,oBAAoB,iBAAiBC,CAAa;AAAA,EAC3D,CAAC,GAODpC,EAAG,iBAAiB,SAAS,MAAM;AACjC,IAAI,CAACQ,EAAI,UAAU,CAACQ,UAA6BqB,EAAA,IACjDpB,EAAK,EAAI;AAAA,EACX,CAAC,GAEGtB,MAAW;AACb,IAAAsB,EAAK,EAAK;AAAA,WACDtB,MAAW,eAAe;AAInC,UAAM2C,IAAO,MAAMrB,EAAK,EAAK,GACvBsB,IAA2C,CAAC,gBAAgB,eAAe,cAAc,SAAS;AACxG,eAAWC,KAAQD;AACjB,MAAAvC,EAAG,iBAAiBwC,GAAMF,GAAM,EAAE,MAAM,IAAM,SAAS,IAAM,GAC7DxB,EAAU,KAAK,MAAMd,EAAG,oBAAoBwC,GAAMF,CAAI,CAAC;AAAA,EAE3D,WAAW3C,MAAW,aAAa,OAAO,uBAAyB,KAAa;AAC9E,UAAM8C,IAAK,IAAI,qBAAqB,CAACC,MAAY;AAC/C,MAAIA,EAAQ,KAAK,CAACC,MAAMA,EAAE,cAAc,OAAQ,EAAK;AAAA,IACvD,CAAC;AACD,IAAAF,EAAG,QAAQzC,CAAE,GACbc,EAAU,KAAK,MAAM2B,EAAG,WAAA,CAAY;AAAA,EACtC,OAAO;AACL,UAAMG,IAAqB,MAAM3B,EAAK,EAAK,GACrCsB,IAAwC,CAAC,eAAe,WAAW,cAAc,OAAO;AAC9F,eAAWC,KAAQD;AACjB,eAAS,iBAAiBC,GAAMI,GAAoB,EAAE,MAAM,IAAM,SAAS,IAAM,GACjF9B,EAAU,KAAK,MAAM,SAAS,oBAAoB0B,GAAMI,CAAkB,CAAC;AAAA,EAE/E;AAEA,SAAOpC;AACT;"}
|
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 { 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 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 (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 // 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","opts","instance","forward","name","payload","err","onMounted","ph","bg","events","io","entries","e","watch","next","onBeforeUnmount","close","children","h","createGestureVideo"],"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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"}
|
package/dist/vue.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
import { c as
|
|
3
|
-
import { renderPlaceholder as
|
|
4
|
-
const
|
|
1
|
+
import { defineComponent as R, ref as p, shallowRef as U, computed as A, onMounted as F, watch as G, onBeforeUnmount as _, h as m } from "vue";
|
|
2
|
+
import { c as x } from "./gesture-BYJrYanO.js";
|
|
3
|
+
import { renderPlaceholder as D } from "./placeholder.js";
|
|
4
|
+
const z = [
|
|
5
5
|
"ready",
|
|
6
6
|
"play",
|
|
7
7
|
"pause",
|
|
@@ -35,11 +35,11 @@ const P = [
|
|
|
35
35
|
"error",
|
|
36
36
|
"destroy"
|
|
37
37
|
];
|
|
38
|
-
function
|
|
38
|
+
function J() {
|
|
39
39
|
return import("itube-modern-player/style.css").catch(() => {
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
const
|
|
42
|
+
const X = R({
|
|
43
43
|
name: "ITubePlayer",
|
|
44
44
|
props: {
|
|
45
45
|
/** Single video or a playlist array. */
|
|
@@ -83,108 +83,131 @@ const x = j({
|
|
|
83
83
|
default: void 0
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
-
emits: [...
|
|
87
|
-
setup(
|
|
88
|
-
const
|
|
89
|
-
let
|
|
90
|
-
const
|
|
91
|
-
for (const
|
|
92
|
-
},
|
|
93
|
-
() =>
|
|
86
|
+
emits: [...z],
|
|
87
|
+
setup(t, { emit: N, slots: i, expose: j }) {
|
|
88
|
+
const y = p(null), h = p(null), g = p(null), s = U(null), P = p(!1);
|
|
89
|
+
let w = !1, E = !1, b = !1, c = null;
|
|
90
|
+
const u = [], C = () => {
|
|
91
|
+
for (const e of u.splice(0)) e();
|
|
92
|
+
}, q = A(() => Array.isArray(t.source) ? t.source[0] : t.source), M = A(
|
|
93
|
+
() => D(
|
|
94
94
|
{
|
|
95
|
-
poster:
|
|
96
|
-
icon:
|
|
97
|
-
styling:
|
|
98
|
-
className:
|
|
95
|
+
poster: q.value?.poster,
|
|
96
|
+
icon: t.options.icons?.bigPlay,
|
|
97
|
+
styling: t.options.styling,
|
|
98
|
+
className: t.options.className
|
|
99
99
|
},
|
|
100
100
|
{ inlineStyles: !0 }
|
|
101
101
|
)
|
|
102
|
-
),
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
typeof
|
|
102
|
+
), d = (e) => {
|
|
103
|
+
if (b || (b = e), E || w) return;
|
|
104
|
+
E = !0, C();
|
|
105
|
+
const o = t.adConfig ?? t.options.adConfig, r = Promise.resolve(
|
|
106
|
+
typeof o == "function" ? o() : o
|
|
107
107
|
).catch(() => {
|
|
108
108
|
});
|
|
109
|
-
Promise.all([import("./index.js"),
|
|
110
|
-
([{ Player: n },
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
Promise.all([import("./index.js"), r, J()]).then(
|
|
110
|
+
([{ Player: n }, a]) => {
|
|
111
|
+
const l = () => {
|
|
112
|
+
if (w || !y.value) return;
|
|
113
|
+
const v = {
|
|
114
|
+
...t.options,
|
|
115
|
+
source: t.source,
|
|
116
|
+
...a ? { adConfig: a } : {},
|
|
117
|
+
// The slot wins over whatever was passed in options.
|
|
118
|
+
...i.pauseScreen ? { pauseScreen: !0 } : {}
|
|
119
|
+
};
|
|
120
|
+
t.lazy && (v.playOnInit = b, v.autoplay = b, c && (v.videoElement = c));
|
|
121
|
+
const L = new n(y.value, v), B = N;
|
|
122
|
+
for (const I of z)
|
|
123
|
+
L.on(I, (H) => B(I, H));
|
|
124
|
+
i.pauseScreen && g.value && L.setPauseScreenContent(g.value), s.value = L, P.value = !0;
|
|
118
125
|
};
|
|
119
|
-
|
|
120
|
-
const b = new n(v.value, d), z = C;
|
|
121
|
-
for (const k of P)
|
|
122
|
-
b.on(k, (N) => z(k, N));
|
|
123
|
-
r.pauseScreen && y.value && b.setPauseScreenContent(y.value), o.value = b, E.value = !0;
|
|
126
|
+
f ? k = l : l();
|
|
124
127
|
},
|
|
125
128
|
(n) => {
|
|
126
|
-
|
|
129
|
+
E = !1, console.error("[itube-player] failed to load the player chunk", n);
|
|
127
130
|
}
|
|
128
131
|
);
|
|
129
132
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
let f = !1, k = null;
|
|
134
|
+
const S = (e) => {
|
|
135
|
+
if (!f) return;
|
|
136
|
+
f = !1;
|
|
137
|
+
const o = k;
|
|
138
|
+
if (!o) return;
|
|
139
|
+
if (k = null, e) {
|
|
140
|
+
o();
|
|
133
141
|
return;
|
|
134
142
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
const r = h.value;
|
|
144
|
+
let n = !1;
|
|
145
|
+
const a = () => {
|
|
146
|
+
n || (n = !0, r?.removeEventListener("click", l), o());
|
|
147
|
+
}, l = () => setTimeout(a, 0);
|
|
148
|
+
r?.addEventListener("click", l, { once: !0 }), setTimeout(a, 300);
|
|
149
|
+
}, O = () => S(!1), T = () => S(!0);
|
|
150
|
+
F(() => {
|
|
151
|
+
if (!t.lazy) {
|
|
152
|
+
d(!1);
|
|
138
153
|
return;
|
|
139
154
|
}
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
const e = h.value;
|
|
156
|
+
if (t.loadOn === "immediate" || !e) {
|
|
157
|
+
d(!1);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
e.addEventListener("pointerdown", () => {
|
|
161
|
+
f = !0;
|
|
162
|
+
}), window.addEventListener("pointerup", O), window.addEventListener("pointercancel", T);
|
|
163
|
+
const o = () => d(!1);
|
|
164
|
+
if (t.loadOn === "interaction") {
|
|
165
|
+
const r = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
166
|
+
for (const n of r)
|
|
167
|
+
document.addEventListener(n, o, { once: !0, passive: !0 }), u.push(() => document.removeEventListener(n, o));
|
|
168
|
+
} else if (t.loadOn === "visible" && typeof IntersectionObserver < "u") {
|
|
169
|
+
const r = new IntersectionObserver((n) => {
|
|
170
|
+
n.some((a) => a.isIntersecting) && o();
|
|
148
171
|
});
|
|
149
|
-
|
|
172
|
+
r.observe(e), u.push(() => r.disconnect());
|
|
150
173
|
} else {
|
|
151
|
-
const
|
|
152
|
-
for (const n of
|
|
153
|
-
|
|
174
|
+
const r = ["pointerenter", "pointerdown", "touchstart", "focusin"];
|
|
175
|
+
for (const n of r)
|
|
176
|
+
e.addEventListener(n, o, { once: !0, passive: !0 }), u.push(() => e.removeEventListener(n, o));
|
|
154
177
|
}
|
|
155
|
-
}),
|
|
156
|
-
() =>
|
|
157
|
-
(
|
|
158
|
-
|
|
178
|
+
}), G(
|
|
179
|
+
() => t.source,
|
|
180
|
+
(e) => {
|
|
181
|
+
e && s.value && s.value.load(e);
|
|
159
182
|
},
|
|
160
183
|
{ deep: !0 }
|
|
161
|
-
),
|
|
162
|
-
|
|
163
|
-
}),
|
|
164
|
-
const
|
|
165
|
-
|
|
184
|
+
), _(() => {
|
|
185
|
+
w = !0, C(), window.removeEventListener("pointerup", O), window.removeEventListener("pointercancel", T), s.value?.destroy(), s.value = null;
|
|
186
|
+
}), j({ player: s });
|
|
187
|
+
const V = () => {
|
|
188
|
+
s.value?.play();
|
|
166
189
|
};
|
|
167
190
|
return () => {
|
|
168
|
-
const
|
|
169
|
-
return
|
|
170
|
-
|
|
191
|
+
const e = [];
|
|
192
|
+
return t.lazy && !P.value && e.push(
|
|
193
|
+
m("div", {
|
|
171
194
|
key: "imp-ph",
|
|
172
|
-
ref:
|
|
195
|
+
ref: h,
|
|
173
196
|
class: "imp-vue-placeholder",
|
|
174
|
-
innerHTML:
|
|
197
|
+
innerHTML: M.value,
|
|
175
198
|
// The gesture video MUST be created synchronously in the click
|
|
176
199
|
// handler — it carries the user-activation flag across the async
|
|
177
200
|
// chunk load so the autostart can play with sound on mobile.
|
|
178
201
|
onClick: () => {
|
|
179
|
-
!
|
|
202
|
+
!s.value && !c && (c = x()), d(!0);
|
|
180
203
|
}
|
|
181
204
|
})
|
|
182
|
-
),
|
|
205
|
+
), e.push(m("div", { key: "imp-mount", ref: y })), i.pauseScreen && e.push(m("div", { ref: g, class: "imp-vue-pause-slot" }, i.pauseScreen({ player: s.value, close: V }))), m("div", { class: "imp-vue-host" }, e);
|
|
183
206
|
};
|
|
184
207
|
}
|
|
185
208
|
});
|
|
186
209
|
export {
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
X as ITubePlayer,
|
|
211
|
+
X as default
|
|
189
212
|
};
|
|
190
213
|
//# sourceMappingURL=vue.js.map
|
package/dist/vue.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.js","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 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 (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 // 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","opts","instance","forward","name","payload","err","onMounted","ph","bg","events","io","entries","e","watch","next","onBeforeUnmount","close","children","h","createGestureVideo"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vue.js","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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "itube-modern-player",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Lightweight, framework-agnostic HTML5 video player: HLS streams, playlists, chapters, VTT subtitles, sprite previews, VAST ads. Ships with a Vue 3 wrapper.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|