mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 08:42:21 -08:00
refactor(dpip): [PKG-DPIP-03] own windows and DOM resources in strict TypeScript
This commit is contained in:
1 parent
202351934b
commit
80d37d66f8
31 files changed
+2335
-597
No files matched your search
@@ -2,4 +2,5 @@ node_modules
|
||||
.DS_Store
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
src
|
||||
src
|
||||
tsconfig.json
|
||||
@@ -0,0 +1,72 @@
|
||||
# Implementation and maintenance
|
||||
|
||||
The public factory still returns a synchronous plugin initializer. Its result has
|
||||
`name`, readonly runtime getters `isSupported`/`isActive`, async `open`/`close`
|
||||
returning `Promise<void>`, and synchronous `toggle` returning `undefined`.
|
||||
The published declarations currently describe void actions and writable flags;
|
||||
public declaration compatibility is tracked separately in PKG-DPIP-04.
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.ts` | Factory option snapshot, capability snapshot, core bridge, terminal plugin cleanup |
|
||||
| `src/window-session.ts` | One pending request/active window, revisions, cancelled requests, window listeners and transition ordering |
|
||||
| `src/projection.ts` | Original parent/sibling/document, placeholder, root, atomic restoration and rollback |
|
||||
| `src/styles.ts` | Module style installation and best-effort copying from the player's actual document |
|
||||
| `src/control.ts` | Existing control name/position/index, click and tooltip subscriptions, partial setup cleanup |
|
||||
| `src/resources.ts` | Attempt-all cleanup and cancellable 100ms transition delay |
|
||||
|
||||
The session receives narrow callbacks and does not import Artplayer. Projection
|
||||
and style modules only handle DOM ownership. Control and entry modules are the
|
||||
core integration boundary. No shared runtime dependency on the new core is added.
|
||||
|
||||
`open()` calls the browser request synchronously to retain user activation. While
|
||||
that request is pending, another open coalesces and toggle cancels it. `close()`
|
||||
settles cancelled callers immediately; when the browser eventually returns its
|
||||
window, the stale continuation closes it without adopting any player nodes.
|
||||
An active window owns its listeners, projection and delay. Successful activation
|
||||
adds the existing class, rebinds core document events and emits `document-pip(true)`;
|
||||
resize follows after 100ms only if that transition is still current. Normal close
|
||||
restores the same node, closes the window, removes the class, rebinds, emits false
|
||||
and delays resize. Destroy is terminal and suppresses new events/timers/UI effects.
|
||||
|
||||
Projection snapshots the original document, parent and sibling before moving the
|
||||
player. Restoration inserts directly into a validated original anchor; it does
|
||||
not detach first. If the placeholder has moved, the saved sibling is used instead.
|
||||
If the original parent rejects insertion, a best-effort fallback keeps the node
|
||||
in the original document body and reports a close failure. This cannot guarantee
|
||||
recovery if the original document also rejects insertion. Application mutation of
|
||||
the placeholder does not transfer ownership of the player to another parent.
|
||||
|
||||
All acquired resources are registered before operations that may throw. Cleanup
|
||||
attempts remaining resources after an error. Reentrant destruction during native
|
||||
adoption or insertion also cleans nodes returned after the initial disposal.
|
||||
The public module style stays shared for the document's lifetime; deferred
|
||||
installation rechecks its existing id and removes its own loading listener.
|
||||
|
||||
Existing defaults, optional runtime options, names, CSS ids/classes, tooltip,
|
||||
index40 and best-effort stylesheet attributes are retained. A throwing video PiP
|
||||
fallback setter still rejects `open()`; normal window errors retain the existing
|
||||
notice/warning behavior. Fixes to pending requests, partial rollback and stale
|
||||
effects are intentional corrections to reproduced defects, not new public APIs.
|
||||
|
||||
Run from the repository root with the pinned Yarn toolchain:
|
||||
|
||||
```sh
|
||||
yarn test:dpip
|
||||
yarn typecheck
|
||||
yarn build artplayer-plugin-document-pip
|
||||
yarn test:browser test/browser/dpip.spec.js test/browser/dpip-lifecycle.spec.js
|
||||
yarn dev artplayer-plugin-document-pip --no-open
|
||||
```
|
||||
|
||||
The development example is `http://localhost:8082/?libs=./uncompiled/artplayer-plugin-document-pip/index.js&example=document.pip`.
|
||||
Historical tests use actual archives and frozen Git source. Candidate lifecycle
|
||||
tests can run against the frozen source with `ARTPLAYER_DPIP_BASELINE=1`; the known
|
||||
failures must remain visible. `ARTPLAYER_DPIP_ARTIFACT` selects an explicit built
|
||||
file for browser verification and never silently falls back to source.
|
||||
|
||||
The iframe matrix uses real DOM with controlled window APIs. It does not prove
|
||||
native Document PiP activation, window focus/keyboard, continuous playback or
|
||||
device support. Native video/Canvas/Mediabunny combinations and the observed
|
||||
WebKit dimension reports remain PKG-DPIP-05. Full archive/demo/type acceptance is
|
||||
PKG-DPIP-04/06. See the repository `refactor/dpip-validation.md` for exact evidence.
|
||||
@@ -6,6 +6,12 @@ Document PIP plugin for ArtPlayer
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-document-pip/index.js&example=document.pip)
|
||||
|
||||
## Maintenance
|
||||
|
||||
Source is split into strict TypeScript modules for window ownership, DOM migration,
|
||||
styles and controls. See [ARCHITECTURE.md](ARCHITECTURE.md) for lifecycle behavior,
|
||||
compatibility boundaries, tests and remaining native browser validation.
|
||||
|
||||
## License
|
||||
|
||||
MIT © Harvey Zhao
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginDocumentPip=t(),define(function(){return t.artplayerPluginDocumentPip})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDocumentPip=t()}(this,function(){"use strict";if("undefined"!==typeof document){const e="artplayer-plugin-document-pip";let t=document.getElementById(e);t||(t=document.createElement("style"),t.id=e,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(t)}):(document.head||document.documentElement).appendChild(t)),t.textContent=".artplayer-document-pip.art-video-player{width:100%!important;height:100%!important}"}return function(e={}){const t={width:480,height:270,fallbackToVideoPiP:!0,placeholder:"Playing in Document Picture-in-Picture",...e};return e=>{const n="documentPictureInPicture"in window&&"function"===typeof window.documentPictureInPicture?.requestWindow,{proxy:i,icons:o,i18n:r,template:{$player:d},constructor:{utils:{append:l,tooltip:c,addClass:a,removeClass:p,sleep:u}}}=e,s={win:null,originalParent:null,originalNext:null,currentDoc:null,placeholder:null};async function m(){if(!n&&t.fallbackToVideoPiP)return e.pip=!0,void console.warn("[artplayer-plugin-document-pip] Document Picture-in-Picture is not supported, falling back to Video Picture-in-Picture");if(!s.win)try{const n=await window.documentPictureInPicture.requestWindow({width:t.width,height:t.height});s.win=n,s.currentDoc=n.document,s.placeholder=document.createElement("div"),s.placeholder.className="artplayer-document-pip-placeholder",s.placeholder.style.cssText="display:flex;justify-content:center;align-items:center;width:100%;height:100%;",s.placeholder.textContent=t.placeholder,s.originalParent=d.parentNode,s.originalNext=d.nextSibling,s.originalParent.insertBefore(s.placeholder,s.originalNext);const i=n.document.createElement("div");i.id="__art_dpip_root",n.document.body.appendChild(i),function(e){const t=e.createElement("style");if(t.textContent="\n html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }\n #__art_dpip_root { position:absolute; inset:0; display:flex; }\n #__art_dpip_root > * { width:100% !important; height:100% !important; }\n ",e.head.appendChild(t),!e.querySelector('meta[name="viewport"]')){const t=e.createElement("meta");t.name="viewport",t.content="width=device-width,initial-scale=1",e.head.appendChild(t)}try{document.querySelectorAll('link[rel="stylesheet"]').forEach(t=>{const n=e.createElement("link");n.rel="stylesheet",n.href=t.href,t.media&&(n.media=t.media),t.crossOrigin&&(n.crossOrigin=t.crossOrigin),t.referrerPolicy&&(n.referrerPolicy=t.referrerPolicy),e.head.appendChild(n)})}catch{}try{document.querySelectorAll("style").forEach(t=>{const n=e.createElement("style");n.textContent=t.textContent,e.head.appendChild(n)})}catch{}}(n.document);const o=n.document.adoptNode(d);i.appendChild(o),s.currentDoc=n.document;const r=()=>e.resize?.(),l=()=>h();n.addEventListener("resize",r),n.addEventListener("pagehide",l),n.addEventListener("unload",l),s.cleanup=()=>{n.removeEventListener("resize",r),n.removeEventListener("pagehide",l),n.removeEventListener("unload",l)},a(d,"artplayer-document-pip"),e.events.bindGlobalEvents?.(),e.emit("document-pip",!0),await u(100),e.emit("resize")}catch(i){e.notice.show="Document Picture-in-Picture open failed",console.warn("[artplayer-plugin-document-pip] open failed:",i)}}async function h(){if(s.win)try{s.cleanup?.(),function(){if(!s.placeholder)return;const e=s.placeholder.ownerDocument||document,t=e.adoptNode(d);s.originalParent.insertBefore(t,s.placeholder),s.placeholder.remove(),s.placeholder=null,s.currentDoc=e}(),s.win.close(),s.win=null,p(d,"artplayer-document-pip"),e.events.bindGlobalEvents?.(),e.emit("document-pip",!1),await u(100),e.emit("resize")}catch(t){e.notice.show="Document Picture-in-Picture close failed",console.warn("[artplayer-plugin-document-pip] close failed:",t)}}function f(){s.win?h():m()}return e.controls.add({name:"document-pip",position:"right",index:40,tooltip:e.i18n.get("PIP Mode"),mounted:t=>{l(t,o.pip),i(t,"click",f),e.on("document-pip",e=>{c(t,r.get(e?"Exit PIP Mode":"PIP Mode"))})}}),e.on("destroy",h),{name:"artplayerPluginDocumentPip",get isSupported(){return n},get isActive(){return!!s.win},open:m,close:h,toggle:f}}}});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginDocumentPip=t(),define(function(){return e.artplayerPluginDocumentPip})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDocumentPip=t()}(this,function(){"use strict";function e(e){const t=[];for(const o of e)try{o()}catch(n){t.push(n)}return t}const t=".artplayer-document-pip.art-video-player{width:100%!important;height:100%!important}";function n(t){let n=!1,o=0,i=null,r=null,c=null;const l=()=>!n&&t.alive(),s=e=>e===o&&l();function u(){o++,r?.finish(),r=null,c?.cancel(),c=null}function a(t){const n=e(t.releases.splice(0));return n.push(...e([()=>t.window.close()])),n}async function d(e){if(!s(e))return;const n=function(){let e;const t=new Promise(t=>{e=t});let n=setTimeout(()=>{n=null,e()},100);return{promise:t,cancel(){null!==n&&clearTimeout(n),n=null,e()}}}();c=n,await n.promise,c===n&&(c=null),s(e)&&t.resize()}async function p(){u();const e=o,n=i;if(i=null,!n)return;const r=a(n);if(n.announced)try{t.activate(!1)}catch(c){r.push(c)}r.length&&l()&&t.report("close",r[0]),s(e)&&await d(e)}return{get active(){return null!==i},get opening(){return null!==r},open:async function(){if(!l()||i)return;if(r)return r.promise;u();const n=o;let c,f;const m=new Promise((e,t)=>{c=e,f=t}),h={promise:m,finish:c};r=h;const y=()=>{r===h&&(r=null),c()},g=o=>{if(s(n)){const n=i;i=null,n&&(a(n),n.announced&&e([()=>t.activate(!1)]));try{t.report("open",o)}catch(r){f(r)}}},v=async o=>{if(!s(n))return void(i?.window!==o&&e([()=>o.close()]));const r={window:o,releases:[],announced:!1};i=r;try{const e=t.project(o.document);if(r.releases.push(e.destroy),e.mount(),!s(n))return void a(r);for(const[i,c]of[["resize",()=>{s(n)&&t.nativeResize()}],["pagehide",()=>{p()}],["unload",()=>{p()}]])if(r.releases.unshift(()=>o.removeEventListener(i,c)),o.addEventListener(i,c),!s(n))return void a(r);r.announced=!0,t.activate(!0),await d(n)}catch(c){g(c)}};try{const e=t.request();Promise.resolve(e).then(v,g).then(y,e=>{s(n)&&f(e),y()})}catch(w){try{g(w)}finally{y()}}return m},close:p,destroy(){n||(n=!0,p().catch(()=>{}))}}}return"undefined"!==typeof document&&function(e){const n="artplayer-plugin-document-pip",o=e.getElementById(n);if(o)return void(o.textContent=t);const i=e.createElement("style");i.id=n,i.textContent=t;const r=()=>{e.removeEventListener("DOMContentLoaded",r);const o=e.getElementById(n);o?o.textContent=t:(e.head||e.documentElement).appendChild(i)};"loading"===e.readyState?e.addEventListener("DOMContentLoaded",r):r()}(document),function(t={}){const o={width:480,height:270,fallbackToVideoPiP:!0,placeholder:"Playing in Document Picture-in-Picture",...t};return t=>{const i=window,r="documentPictureInPicture"in i&&"function"===typeof i.documentPictureInPicture?.requestWindow;let c=t,l=t.template.$player,s=t.isDestroy;const u=t.constructor.utils,a=()=>!s&&!!c&&!c.isDestroy,d=n({alive:a,request:()=>i.documentPictureInPicture.requestWindow({width:o.width,height:o.height}),project:t=>function(t,n,o){const i=t.ownerDocument,r=t.parentNode,c=t.nextSibling;if(!r)throw new Error("Document Picture-in-Picture requires a mounted player");const l=i.createElement("div");l.className="artplayer-document-pip-placeholder",l.style.cssText="display:flex;justify-content:center;align-items:center;width:100%;height:100%;",l.textContent=o??"";const s=n.createElement("div");s.id="__art_dpip_root";const u=[];let a=!1;const d=()=>{const e=l.parentNode===r?l:c?.parentNode===r?c:null;if(t.parentNode!==r||t.nextSibling!==e)try{r.insertBefore(t,e)}catch(n){throw i.body.appendChild(t),n}},p=()=>{const t=e([d,()=>l.remove(),...u.splice(0).map(e=>()=>e.remove()),()=>s.remove()]);if(t.length)throw t[0]};return{mount(){if(!a){if(r.insertBefore(l,c?.parentNode===r?c:null),a||n.body.appendChild(s),a||function(e,t,n,o){const i=e=>{o()&&(n(e),t.head.appendChild(e),o()||e.remove())},r=t.createElement("style");if(r.textContent="\n html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }\n #__art_dpip_root { position:absolute; inset:0; display:flex; }\n #__art_dpip_root > * { width:100% !important; height:100% !important; }\n ",i(r),!t.querySelector('meta[name="viewport"]')){const e=t.createElement("meta");e.name="viewport",e.content="width=device-width,initial-scale=1",i(e)}try{e.querySelectorAll('link[rel="stylesheet"]').forEach(e=>{const n=t.createElement("link");n.rel="stylesheet",n.href=e.href,e.media&&(n.media=e.media),e.crossOrigin&&(n.crossOrigin=e.crossOrigin),e.referrerPolicy&&(n.referrerPolicy=e.referrerPolicy),i(n)})}catch{}try{e.querySelectorAll("style").forEach(e=>{const n=t.createElement("style");n.textContent=e.textContent,i(n)})}catch{}}(i,n,e=>u.push(e),()=>!a),!a){const e=n.adoptNode(t);a||s.appendChild(e)}a&&p()}},destroy(){a||(a=!0,p())}}}(l,t,o.placeholder),activate(e){c&&l&&(e?u.addClass(l,"artplayer-document-pip"):u.removeClass(l,"artplayerLine truncated
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginDocumentPip=t(),define(function(){return t.artplayerPluginDocumentPip})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDocumentPip=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,o=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,r=(e,t,n)=>new Promise((i,o)=>{var r=e=>{try{c(n.next(e))}catch(t){o(t)}},l=e=>{try{c(n.throw(e))}catch(t){o(t)}},c=e=>e.done?i(e.value):Promise.resolve(e.value).then(r,l);c((n=n.apply(e,t)).next())});if("undefined"!==typeof document){const e="artplayer-plugin-document-pip";let t=document.getElementById(e);t||(t=document.createElement("style"),t.id=e,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(t)}):(document.head||document.documentElement).appendChild(t)),t.textContent=".artplayer-document-pip.art-video-player{width:100%!important;height:100%!important}"}return function(e={}){const l=((e,r)=>{for(var l in r||(r={}))n.call(r,l)&&o(e,l,r[l]);if(t)for(var l of t(r))i.call(r,l)&&o(e,l,r[l]);return e})({width:480,height:270,fallbackToVideoPiP:!0,placeholder:"Playing in Document Picture-in-Picture"},e);return e=>{var t;const n="documentPictureInPicture"in window&&"function"===typeof(null==(t=window.documentPictureInPicture)?void 0:t.requestWindow),{proxy:i,icons:o,i18n:c,template:{$player:a},constructor:{utils:{append:d,tooltip:p,addClass:u,removeClass:s,sleep:m}}}=e,h={win:null,originalParent:null,originalNext:null,currentDoc:null,placeholder:null};function y(){return r(this,null,function*(){var t,i;if(!n&&l.fallbackToVideoPiP)return e.pip=!0,void console.warn("[artplayer-plugin-document-pip] Document Picture-in-Picture is not supported, falling back to Video Picture-in-Picture");if(!h.win)try{const n=yield window.documentPictureInPicture.requestWindow({width:l.width,height:l.height});h.win=n,h.currentDoc=n.document,h.placeholder=document.createElement("div"),h.placeholder.className="artplayer-document-pip-placeholder",h.placeholder.style.cssText="display:flex;justify-content:center;align-items:center;width:100%;height:100%;",h.placeholder.textContent=l.placeholder,h.originalParent=a.parentNode,h.originalNext=a.nextSibling,h.originalParent.insertBefore(h.placeholder,h.originalNext);const o=n.document.createElement("div");o.id="__art_dpip_root",n.document.body.appendChild(o),function(e){const t=e.createElement("style");if(t.textContent="\n html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }\n #__art_dpip_root { position:absolute; inset:0; display:flex; }\n #__art_dpip_root > * { width:100% !important; height:100% !important; }\n ",e.head.appendChild(t),!e.querySelector('meta[name="viewport"]')){const t=e.createElement("meta");t.name="viewport",t.content="width=device-width,initial-scale=1",e.head.appendChild(t)}try{document.querySelectorAll('link[rel="stylesheet"]').forEach(t=>{const n=e.createElement("link");n.rel="stylesheet",n.href=t.href,t.media&&(n.media=t.media),t.crossOrigin&&(n.crossOrigin=t.crossOrigin),t.referrerPolicy&&(n.referrerPolicy=t.referrerPolicy),e.head.appendChild(n)})}catch(n){}try{document.querySelectorAll("style").forEach(t=>{const n=e.createElement("style");n.textContent=t.textContent,e.head.appendChild(n)})}catch(n){}}(n.document);const r=n.document.adoptNode(a);o.appendChild(r),h.currentDoc=n.document;const c=()=>{var t;return null==(t=e.resize)?void 0:t.call(e)},d=()=>f();n.addEventListener("resize",c),n.addEventListener("pagehide",d),n.addEventListener("unload",d),h.cleanup=()=>{n.removeEventListener("resize",c),n.removeEventListener("pagehide",d),n.removeEventListener("unload",d)},u(a,"artplayer-document-pip"),null==(i=(t=e.events).bindGlobalEvents)||i.call(t),e.emit("document-pip",!0),yield m(100),e.emit("resize")}catch(o){e.notice.show="Document Picture-in-Picture open failed",console.warn("[artplayer-plugin-document-pip] open failed:",o)}})}function f(){return r(this,null,function*(){var t,n,i;if(h.win)try{null==(t=h.cleanup)||t.call(h),function(){if(!h.placeholder)return;const e=h.placeholder.ownerDocument||document,t=e.adoptNode(a);h.originalParent.insertBefore(t,h.placeholder),h.placeholder.remove(),h.placeholder=null,h.currentDoc=e}(),h.win.close(),h.win=null,s(a,"artplayer-document-pip"),null==(i=(n=e.events).bindGlobalEvents)||i.call(n),e.emit("document-pip",!1),yield m(100),e.emit("resize")}catch(o){e.notice.show="Document Picture-in-Picture close failed",console.warn("[artplayer-plugin-document-pip] close failed:",o)}})}function g(){h.win?f():y()}return e.controls.add({name:"document-pip",position:"right",index:40,tooltip:e.i18n.get("PIP Mode"),mounted:t=>{d(t,o.pip),i(t,"click",g),e.on("document-pip",Line truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginDocumentPip=t(),define(function(){return e.artplayerPluginDocumentPip})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDocumentPip=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(t,n,o)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[n]=o,i=(e,t,n)=>new Promise((o,r)=>{var i=e=>{try{c(n.next(e))}catch(t){r(t)}},l=e=>{try{c(n.throw(e))}catch(t){r(t)}},c=e=>e.done?o(e.value):Promise.resolve(e.value).then(i,l);c((n=n.apply(e,t)).next())});function l(e){const t=[];for(const o of e)try{o()}catch(n){t.push(n)}return t}const c=".artplayer-document-pip.art-video-player{width:100%!important;height:100%!important}";function u(e){let t=!1,n=0,o=null,r=null,c=null;const u=()=>!t&&e.alive(),s=e=>e===n&&u();function a(){n++,null==r||r.finish(),r=null,null==c||c.cancel(),c=null}function d(e){const t=l(e.releases.splice(0));return t.push(...l([()=>e.window.close()])),t}function p(t){return i(this,null,function*(){if(!s(t))return;const n=function(){let e;const t=new Promise(t=>{e=t});let n=setTimeout(()=>{n=null,e()},100);return{promise:t,cancel(){null!==n&&clearTimeout(n),n=null,e()}}}();c=n,yield n.promise,c===n&&(c=null),s(t)&&e.resize()})}function f(){return i(this,null,function*(){a();const t=n,r=o;if(o=null,!r)return;const i=d(r);if(r.announced)try{e.activate(!1)}catch(l){i.push(l)}i.length&&u()&&e.report("close",i[0]),s(t)&&(yield p(t))})}return{get active(){return null!==o},get opening(){return null!==r},open:function(){return i(this,null,function*(){if(!u()||o)return;if(r)return r.promise;a();const t=n;let c,m;const h=new Promise((e,t)=>{c=e,m=t}),y={promise:h,finish:c};r=y;const v=()=>{r===y&&(r=null),c()},g=n=>{if(s(t)){const t=o;o=null,t&&(d(t),t.announced&&l([()=>e.activate(!1)]));try{e.report("open",n)}catch(r){m(r)}}},P=n=>i(null,null,function*(){if(!s(t))return void((null==o?void 0:o.window)!==n&&l([()=>n.close()]));const r={window:n,releases:[],announced:!1};o=r;try{const o=e.project(n.document);if(r.releases.push(o.destroy),o.mount(),!s(t))return void d(r);for(const[i,l]of[["resize",()=>{s(t)&&e.nativeResize()}],["pagehide",()=>{f()}],["unload",()=>{f()}]])if(r.releases.unshift(()=>n.removeEventListener(i,l)),n.addEventListener(i,l),!s(t))return void d(r);r.announced=!0,e.activate(!0),yield p(t)}catch(i){g(i)}});try{const n=e.request();Promise.resolve(n).then(P,g).then(v,e=>{s(t)&&m(e),v()})}catch(w){try{g(w)}finally{v()}}return h})},close:f,destroy(){t||(t=!0,f().catch(()=>{}))}}}return"undefined"!==typeof document&&function(e){const t="artplayer-plugin-document-pip",n=e.getElementById(t);if(n)return void(n.textContent=c);const o=e.createElement("style");o.id=t,o.textContent=c;const r=()=>{e.removeEventListener("DOMContentLoaded",r);const n=e.getElementById(t);n?n.textContent=c:(e.head||e.documentElement).appendChild(o)};"loading"===e.readyState?e.addEventListener("DOMContentLoaded",r):r()}(document),function(e={}){const c=((e,i)=>{for(var l in i||(i={}))n.call(i,l)&&r(e,l,i[l]);if(t)for(var l of t(i))o.call(i,l)&&r(e,l,i[l]);return e})({width:480,height:270,fallbackToVideoPiP:!0,placeholder:"Playing in Document Picture-in-Picture"},e);return e=>{var t;const n=window,o="documentPictureInPicture"in n&&"function"===typeof(null==(t=n.documentPictureInPicture)?void 0:t.requestWindow);let r=e,s=e.template.$player,a=e.isDestroy;const d=e.constructor.utils,p=()=>!a&&!!r&&!r.isDestroy,f=u({alive:p,request:()=>n.documentPictureInPicture.requestWindow({width:c.width,height:c.height}),project:e=>function(e,t,n){const o=e.ownerDocument,r=e.parentNode,i=e.nextSibling;if(!r)throw new Error("Document Picture-in-Picture requires a mounted player");const c=o.createElement("div");c.className="artplayer-document-pip-placeholder",c.style.cssText="display:flex;justify-content:center;align-items:center;width:100%;height:100%;",c.textContent=null!=n?n:"";const u=t.createElement("div");u.id="__art_dpip_root";const s=[];let a=!1;const d=()=>{const t=c.parentNode===r?c:(null==i?void 0:i.parentNode)===r?i:null;if(e.parentNode!==r||e.nextSibling!==t)try{r.insertBefore(e,t)}catch(n){throw o.body.appendChild(e),n}},p=()=>{const e=l([d,()=>c.remove(),...s.splice(0).map(e=>()=>e.remove()),()=>u.remove()]);if(e.length)throw e[0]};return{mount(){if(!a){if(r.insertBefore(c,(null==i?void 0:i.parentNode)===r?i:null),a||t.body.appendChild(u),a||function(e,t,n,o){const r=e=>{o()&&(n(e),t.head.appendChild(e),o()||e.remove())},i=t.createElement("style");if(i.textContent="\n html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }\n #__art_dpip_root { position:absolute; inset:0; display:flex; }\n #__art_dpip_root > * { width:100% !important; height:100% !important; }\n ",r(i),!t.querySelectorLine truncated
|
||||
+448
-162
@@ -4,191 +4,477 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
const style = ".artplayer-document-pip.art-video-player {\n width: 100% !important;\n height: 100% !important;\n}\n";
|
||||
function artplayerPluginDocumentPip(userOptions = {}) {
|
||||
const options = {
|
||||
width: 480,
|
||||
height: 270,
|
||||
fallbackToVideoPiP: true,
|
||||
placeholder: `Playing in Document Picture-in-Picture`,
|
||||
...userOptions
|
||||
function releaseAll(releases) {
|
||||
const errors = [];
|
||||
for (const release of releases) {
|
||||
try {
|
||||
release();
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
function createDelay() {
|
||||
let finish;
|
||||
const promise = new Promise((resolve) => {
|
||||
finish = resolve;
|
||||
});
|
||||
let timer = setTimeout(() => {
|
||||
timer = null;
|
||||
finish();
|
||||
}, 100);
|
||||
return {
|
||||
promise,
|
||||
cancel() {
|
||||
if (timer !== null)
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
finish();
|
||||
}
|
||||
};
|
||||
return (art) => {
|
||||
const isSupported = "documentPictureInPicture" in window && typeof window.documentPictureInPicture?.requestWindow === "function";
|
||||
const { proxy, icons, i18n, template: { $player }, constructor: { utils: { append, tooltip, addClass, removeClass, sleep } } } = art;
|
||||
const state = {
|
||||
win: null,
|
||||
originalParent: null,
|
||||
originalNext: null,
|
||||
currentDoc: null,
|
||||
placeholder: null
|
||||
};
|
||||
function copyStylesTo(targetDoc) {
|
||||
const base = targetDoc.createElement("style");
|
||||
base.textContent = `
|
||||
html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }
|
||||
#__art_dpip_root { position:absolute; inset:0; display:flex; }
|
||||
#__art_dpip_root > * { width:100% !important; height:100% !important; }
|
||||
`;
|
||||
targetDoc.head.appendChild(base);
|
||||
if (!targetDoc.querySelector('meta[name="viewport"]')) {
|
||||
const meta = targetDoc.createElement("meta");
|
||||
meta.name = "viewport";
|
||||
meta.content = "width=device-width,initial-scale=1";
|
||||
targetDoc.head.appendChild(meta);
|
||||
}
|
||||
}
|
||||
function createControl(art, toggle, alive) {
|
||||
const utils = art.constructor.utils;
|
||||
const releases = [];
|
||||
let closed = false;
|
||||
return {
|
||||
mount() {
|
||||
if (closed || !alive())
|
||||
return;
|
||||
releases.push(() => art.controls.remove("document-pip"));
|
||||
art.controls.add({
|
||||
name: "document-pip",
|
||||
position: "right",
|
||||
index: 40,
|
||||
tooltip: art.i18n.get("PIP Mode"),
|
||||
mounted: (control) => {
|
||||
if (closed || !alive())
|
||||
return;
|
||||
utils.append(control, art.icons.pip);
|
||||
const click = () => {
|
||||
if (alive())
|
||||
toggle();
|
||||
};
|
||||
releases.push(() => control.removeEventListener("click", click));
|
||||
const unbind = art.proxy(control, "click", click);
|
||||
if (closed)
|
||||
unbind();
|
||||
else
|
||||
releases.push(unbind);
|
||||
if (closed || !alive())
|
||||
return;
|
||||
const update = (active) => {
|
||||
if (alive())
|
||||
utils.tooltip(control, art.i18n.get(active ? "Exit PIP Mode" : "PIP Mode"));
|
||||
};
|
||||
releases.push(() => art.off("document-pip", update));
|
||||
art.on("document-pip", update);
|
||||
}
|
||||
});
|
||||
if (closed)
|
||||
art.controls.remove("document-pip");
|
||||
},
|
||||
destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
const errors = releaseAll(releases.splice(0).reverse());
|
||||
if (errors.length)
|
||||
throw errors[0];
|
||||
}
|
||||
};
|
||||
}
|
||||
const style = ".artplayer-document-pip.art-video-player {\n width: 100% !important;\n height: 100% !important;\n}\n";
|
||||
function installStyle(document2) {
|
||||
const id = "artplayer-plugin-document-pip";
|
||||
const existing = document2.getElementById(id);
|
||||
if (existing) {
|
||||
existing.textContent = style;
|
||||
return;
|
||||
}
|
||||
const element = document2.createElement("style");
|
||||
element.id = id;
|
||||
element.textContent = style;
|
||||
const insert = () => {
|
||||
document2.removeEventListener("DOMContentLoaded", insert);
|
||||
const current = document2.getElementById(id);
|
||||
if (current)
|
||||
current.textContent = style;
|
||||
else
|
||||
(document2.head || document2.documentElement).appendChild(element);
|
||||
};
|
||||
if (document2.readyState === "loading")
|
||||
document2.addEventListener("DOMContentLoaded", insert);
|
||||
else
|
||||
insert();
|
||||
}
|
||||
function copyStyles(source, target, own, alive) {
|
||||
const append = (node) => {
|
||||
if (!alive())
|
||||
return;
|
||||
own(node);
|
||||
target.head.appendChild(node);
|
||||
if (!alive())
|
||||
node.remove();
|
||||
};
|
||||
const base = target.createElement("style");
|
||||
base.textContent = `
|
||||
html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }
|
||||
#__art_dpip_root { position:absolute; inset:0; display:flex; }
|
||||
#__art_dpip_root > * { width:100% !important; height:100% !important; }
|
||||
`;
|
||||
append(base);
|
||||
if (!target.querySelector('meta[name="viewport"]')) {
|
||||
const meta = target.createElement("meta");
|
||||
meta.name = "viewport";
|
||||
meta.content = "width=device-width,initial-scale=1";
|
||||
append(meta);
|
||||
}
|
||||
try {
|
||||
source.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||
const clone = target.createElement("link");
|
||||
clone.rel = "stylesheet";
|
||||
clone.href = link.href;
|
||||
if (link.media)
|
||||
clone.media = link.media;
|
||||
if (link.crossOrigin)
|
||||
clone.crossOrigin = link.crossOrigin;
|
||||
if (link.referrerPolicy)
|
||||
clone.referrerPolicy = link.referrerPolicy;
|
||||
append(clone);
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
source.querySelectorAll("style").forEach((element) => {
|
||||
const clone = target.createElement("style");
|
||||
clone.textContent = element.textContent;
|
||||
append(clone);
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
function createProjection(player, target, text) {
|
||||
const source = player.ownerDocument;
|
||||
const parent = player.parentNode;
|
||||
const next = player.nextSibling;
|
||||
if (!parent)
|
||||
throw new Error("Document Picture-in-Picture requires a mounted player");
|
||||
const placeholder = source.createElement("div");
|
||||
placeholder.className = "artplayer-document-pip-placeholder";
|
||||
placeholder.style.cssText = "display:flex;justify-content:center;align-items:center;width:100%;height:100%;";
|
||||
placeholder.textContent = text ?? "";
|
||||
const root = target.createElement("div");
|
||||
root.id = "__art_dpip_root";
|
||||
const nodes = [];
|
||||
let closed = false;
|
||||
const restore = () => {
|
||||
const anchor = placeholder.parentNode === parent ? placeholder : next?.parentNode === parent ? next : null;
|
||||
if (player.parentNode !== parent || player.nextSibling !== anchor) {
|
||||
try {
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||
const clone = targetDoc.createElement("link");
|
||||
clone.rel = "stylesheet";
|
||||
clone.href = link.href;
|
||||
if (link.media)
|
||||
clone.media = link.media;
|
||||
if (link.crossOrigin)
|
||||
clone.crossOrigin = link.crossOrigin;
|
||||
if (link.referrerPolicy)
|
||||
clone.referrerPolicy = link.referrerPolicy;
|
||||
targetDoc.head.appendChild(clone);
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
document.querySelectorAll("style").forEach((style2) => {
|
||||
const clone = targetDoc.createElement("style");
|
||||
clone.textContent = style2.textContent;
|
||||
targetDoc.head.appendChild(clone);
|
||||
});
|
||||
} catch {
|
||||
parent.insertBefore(player, anchor);
|
||||
} catch (error) {
|
||||
source.body.appendChild(player);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
const cleanup = () => {
|
||||
const errors = releaseAll([restore, () => placeholder.remove(), ...nodes.splice(0).map((node) => () => node.remove()), () => root.remove()]);
|
||||
if (errors.length)
|
||||
throw errors[0];
|
||||
};
|
||||
return {
|
||||
mount() {
|
||||
if (closed)
|
||||
return;
|
||||
parent.insertBefore(placeholder, next?.parentNode === parent ? next : null);
|
||||
if (!closed)
|
||||
target.body.appendChild(root);
|
||||
if (!closed)
|
||||
copyStyles(source, target, (node) => nodes.push(node), () => !closed);
|
||||
if (!closed) {
|
||||
const adopted = target.adoptNode(player);
|
||||
if (!closed)
|
||||
root.appendChild(adopted);
|
||||
}
|
||||
if (closed)
|
||||
cleanup();
|
||||
},
|
||||
destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
}
|
||||
function createWindowSession(hooks) {
|
||||
let destroyed = false;
|
||||
let revision = 0;
|
||||
let session = null;
|
||||
let pending = null;
|
||||
let delay = null;
|
||||
const alive = () => !destroyed && hooks.alive();
|
||||
const valid = (value) => value === revision && alive();
|
||||
function invalidate() {
|
||||
revision++;
|
||||
pending?.finish();
|
||||
pending = null;
|
||||
delay?.cancel();
|
||||
delay = null;
|
||||
}
|
||||
function dispose(current) {
|
||||
const errors = releaseAll(current.releases.splice(0));
|
||||
errors.push(...releaseAll([() => current.window.close()]));
|
||||
return errors;
|
||||
}
|
||||
async function resizeLater(value) {
|
||||
if (!valid(value))
|
||||
return;
|
||||
const current = createDelay();
|
||||
delay = current;
|
||||
await current.promise;
|
||||
if (delay === current)
|
||||
delay = null;
|
||||
if (valid(value))
|
||||
hooks.resize();
|
||||
}
|
||||
async function close() {
|
||||
invalidate();
|
||||
const value = revision;
|
||||
const current = session;
|
||||
session = null;
|
||||
if (!current)
|
||||
return;
|
||||
const errors = dispose(current);
|
||||
if (current.announced) {
|
||||
try {
|
||||
hooks.activate(false);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
if (errors.length && alive())
|
||||
hooks.report("close", errors[0]);
|
||||
if (valid(value))
|
||||
await resizeLater(value);
|
||||
}
|
||||
async function open() {
|
||||
if (!alive() || session)
|
||||
return;
|
||||
if (pending)
|
||||
return pending.promise;
|
||||
invalidate();
|
||||
const value = revision;
|
||||
let finish;
|
||||
let reject;
|
||||
const promise = new Promise((resolve, fail) => {
|
||||
finish = resolve;
|
||||
reject = fail;
|
||||
});
|
||||
const ticket = { promise, finish };
|
||||
pending = ticket;
|
||||
const settle = () => {
|
||||
if (pending === ticket)
|
||||
pending = null;
|
||||
finish();
|
||||
};
|
||||
const failed = (error) => {
|
||||
if (valid(value)) {
|
||||
const current = session;
|
||||
session = null;
|
||||
if (current) {
|
||||
dispose(current);
|
||||
if (current.announced)
|
||||
releaseAll([() => hooks.activate(false)]);
|
||||
}
|
||||
try {
|
||||
hooks.report("open", error);
|
||||
} catch (noticeError) {
|
||||
reject(noticeError);
|
||||
}
|
||||
}
|
||||
};
|
||||
const receive = async (window2) => {
|
||||
if (!valid(value)) {
|
||||
if (session?.window !== window2)
|
||||
releaseAll([() => window2.close()]);
|
||||
return;
|
||||
}
|
||||
const current = { window: window2, releases: [], announced: false };
|
||||
session = current;
|
||||
try {
|
||||
const projection = hooks.project(window2.document);
|
||||
current.releases.push(projection.destroy);
|
||||
projection.mount();
|
||||
if (!valid(value)) {
|
||||
dispose(current);
|
||||
return;
|
||||
}
|
||||
for (const [event, callback] of [["resize", () => {
|
||||
if (valid(value))
|
||||
hooks.nativeResize();
|
||||
}], ["pagehide", () => {
|
||||
void close();
|
||||
}], ["unload", () => {
|
||||
void close();
|
||||
}]]) {
|
||||
current.releases.unshift(() => window2.removeEventListener(event, callback));
|
||||
window2.addEventListener(event, callback);
|
||||
if (!valid(value)) {
|
||||
dispose(current);
|
||||
return;
|
||||
}
|
||||
}
|
||||
current.announced = true;
|
||||
hooks.activate(true);
|
||||
await resizeLater(value);
|
||||
} catch (error) {
|
||||
failed(error);
|
||||
}
|
||||
};
|
||||
try {
|
||||
const request = hooks.request();
|
||||
void Promise.resolve(request).then(receive, failed).then(settle, (error) => {
|
||||
if (valid(value))
|
||||
reject(error);
|
||||
settle();
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
failed(error);
|
||||
} finally {
|
||||
settle();
|
||||
}
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
return {
|
||||
get active() {
|
||||
return session !== null;
|
||||
},
|
||||
get opening() {
|
||||
return pending !== null;
|
||||
},
|
||||
open,
|
||||
close,
|
||||
destroy() {
|
||||
if (destroyed)
|
||||
return;
|
||||
destroyed = true;
|
||||
void close().catch(() => {
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
function artplayerPluginDocumentPip(userOptions = {}) {
|
||||
const options = { width: 480, height: 270, fallbackToVideoPiP: true, placeholder: "Playing in Document Picture-in-Picture", ...userOptions };
|
||||
return (art) => {
|
||||
const browser = window;
|
||||
const isSupported = "documentPictureInPicture" in browser && typeof browser.documentPictureInPicture?.requestWindow === "function";
|
||||
let host = art;
|
||||
let player = art.template.$player;
|
||||
let closed = art.isDestroy;
|
||||
const utils = art.constructor.utils;
|
||||
const alive = () => !closed && !!host && !host.isDestroy;
|
||||
const session = createWindowSession({
|
||||
alive,
|
||||
request: () => browser.documentPictureInPicture.requestWindow({ width: options.width, height: options.height }),
|
||||
project: (document2) => createProjection(player, document2, options.placeholder),
|
||||
activate(active) {
|
||||
if (!host || !player)
|
||||
return;
|
||||
if (active)
|
||||
utils.addClass(player, "artplayer-document-pip");
|
||||
else
|
||||
utils.removeClass(player, "artplayer-document-pip");
|
||||
if (!alive())
|
||||
return;
|
||||
host.events.bindGlobalEvents?.();
|
||||
if (alive())
|
||||
host.emit("document-pip", active);
|
||||
},
|
||||
resize: () => {
|
||||
if (alive())
|
||||
host.emit("resize");
|
||||
},
|
||||
nativeResize: () => {
|
||||
if (alive())
|
||||
host.resize?.();
|
||||
},
|
||||
report(action, error) {
|
||||
if (!alive())
|
||||
return;
|
||||
host.notice.show = `Document Picture-in-Picture ${action} failed`;
|
||||
console.warn(`[artplayer-plugin-document-pip] ${action} failed:`, error);
|
||||
}
|
||||
});
|
||||
async function open() {
|
||||
if (!alive())
|
||||
return;
|
||||
if (!isSupported && options.fallbackToVideoPiP) {
|
||||
art.pip = true;
|
||||
host.pip = true;
|
||||
console.warn("[artplayer-plugin-document-pip] Document Picture-in-Picture is not supported, falling back to Video Picture-in-Picture");
|
||||
return;
|
||||
}
|
||||
if (state.win)
|
||||
return;
|
||||
try {
|
||||
const pipWin = await window.documentPictureInPicture.requestWindow({
|
||||
width: options.width,
|
||||
height: options.height
|
||||
});
|
||||
state.win = pipWin;
|
||||
state.currentDoc = pipWin.document;
|
||||
state.placeholder = document.createElement("div");
|
||||
state.placeholder.className = "artplayer-document-pip-placeholder";
|
||||
state.placeholder.style.cssText = `display:flex;justify-content:center;align-items:center;width:100%;height:100%;`;
|
||||
state.placeholder.textContent = options.placeholder;
|
||||
state.originalParent = $player.parentNode;
|
||||
state.originalNext = $player.nextSibling;
|
||||
state.originalParent.insertBefore(state.placeholder, state.originalNext);
|
||||
const root = pipWin.document.createElement("div");
|
||||
root.id = "__art_dpip_root";
|
||||
pipWin.document.body.appendChild(root);
|
||||
copyStylesTo(pipWin.document);
|
||||
const adopted = pipWin.document.adoptNode($player);
|
||||
root.appendChild(adopted);
|
||||
state.currentDoc = pipWin.document;
|
||||
const onResize = () => art.resize?.();
|
||||
const onClose = () => close();
|
||||
pipWin.addEventListener("resize", onResize);
|
||||
pipWin.addEventListener("pagehide", onClose);
|
||||
pipWin.addEventListener("unload", onClose);
|
||||
state.cleanup = () => {
|
||||
pipWin.removeEventListener("resize", onResize);
|
||||
pipWin.removeEventListener("pagehide", onClose);
|
||||
pipWin.removeEventListener("unload", onClose);
|
||||
};
|
||||
addClass($player, "artplayer-document-pip");
|
||||
art.events.bindGlobalEvents?.();
|
||||
art.emit("document-pip", true);
|
||||
await sleep(100);
|
||||
art.emit("resize");
|
||||
} catch (err) {
|
||||
art.notice.show = "Document Picture-in-Picture open failed";
|
||||
console.warn("[artplayer-plugin-document-pip] open failed:", err);
|
||||
}
|
||||
}
|
||||
function restoreToOriginalDocument() {
|
||||
if (!state.placeholder)
|
||||
return;
|
||||
const backDoc = state.placeholder.ownerDocument || document;
|
||||
const adoptedBack = backDoc.adoptNode($player);
|
||||
state.originalParent.insertBefore(adoptedBack, state.placeholder);
|
||||
state.placeholder.remove();
|
||||
state.placeholder = null;
|
||||
state.currentDoc = backDoc;
|
||||
}
|
||||
async function close() {
|
||||
if (!state.win)
|
||||
return;
|
||||
try {
|
||||
state.cleanup?.();
|
||||
restoreToOriginalDocument();
|
||||
state.win.close();
|
||||
state.win = null;
|
||||
removeClass($player, "artplayer-document-pip");
|
||||
art.events.bindGlobalEvents?.();
|
||||
art.emit("document-pip", false);
|
||||
await sleep(100);
|
||||
art.emit("resize");
|
||||
} catch (err) {
|
||||
art.notice.show = "Document Picture-in-Picture close failed";
|
||||
console.warn("[artplayer-plugin-document-pip] close failed:", err);
|
||||
}
|
||||
return session.open();
|
||||
}
|
||||
function toggle() {
|
||||
if (state.win) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
if (!alive())
|
||||
return;
|
||||
if (session.active || session.opening)
|
||||
void session.close();
|
||||
else
|
||||
void open();
|
||||
}
|
||||
art.controls.add({
|
||||
name: "document-pip",
|
||||
position: "right",
|
||||
index: 40,
|
||||
tooltip: art.i18n.get("PIP Mode"),
|
||||
mounted: ($control) => {
|
||||
append($control, icons.pip);
|
||||
proxy($control, "click", toggle);
|
||||
art.on("document-pip", (value) => {
|
||||
tooltip($control, i18n.get(value ? "Exit PIP Mode" : "PIP Mode"));
|
||||
});
|
||||
}
|
||||
});
|
||||
art.on("destroy", close);
|
||||
return {
|
||||
let control = createControl(art, toggle, alive);
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
const current = host;
|
||||
const previousControl = control;
|
||||
control = null;
|
||||
const errors = releaseAll([session.destroy, () => previousControl?.destroy(), () => current?.off("destroy", destroy)]);
|
||||
host = null;
|
||||
player = null;
|
||||
if (errors.length)
|
||||
console.warn("[artplayer-plugin-document-pip] cleanup failed:", errors[0]);
|
||||
}
|
||||
const result = {
|
||||
name: "artplayerPluginDocumentPip",
|
||||
get isSupported() {
|
||||
return isSupported;
|
||||
},
|
||||
get isActive() {
|
||||
return !!state.win;
|
||||
return session.active;
|
||||
},
|
||||
open,
|
||||
close,
|
||||
close: session.close,
|
||||
toggle
|
||||
};
|
||||
if (closed) {
|
||||
host = null;
|
||||
player = null;
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
art.on("destroy", destroy);
|
||||
if (alive())
|
||||
control?.mount();
|
||||
else
|
||||
art.off("destroy", destroy);
|
||||
} catch (error) {
|
||||
destroy();
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
const id = "artplayer-plugin-document-pip";
|
||||
let $style = document.getElementById(id);
|
||||
if (!$style) {
|
||||
$style = document.createElement("style");
|
||||
$style.id = id;
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.head.appendChild($style);
|
||||
});
|
||||
} else {
|
||||
(document.head || document.documentElement).appendChild($style);
|
||||
}
|
||||
}
|
||||
$style.textContent = style;
|
||||
}
|
||||
if (typeof document !== "undefined")
|
||||
installStyle(document);
|
||||
export {
|
||||
artplayerPluginDocumentPip as default
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Disposer } from './resources'
|
||||
import { releaseAll } from './resources'
|
||||
|
||||
export function createControl(art: Artplayer, toggle: Disposer, alive: () => boolean) {
|
||||
const utils = (art.constructor as typeof Artplayer).utils
|
||||
const releases: Disposer[] = []
|
||||
let closed = false
|
||||
return {
|
||||
mount() {
|
||||
if (closed || !alive())
|
||||
return
|
||||
releases.push(() => art.controls.remove('document-pip'))
|
||||
art.controls.add({
|
||||
name: 'document-pip',
|
||||
position: 'right',
|
||||
index: 40,
|
||||
tooltip: art.i18n.get('PIP Mode'),
|
||||
mounted: (control) => {
|
||||
if (closed || !alive())
|
||||
return
|
||||
utils.append(control, art.icons.pip)
|
||||
const click = () => {
|
||||
if (alive())
|
||||
toggle()
|
||||
}
|
||||
// Explicit removal also covers legacy proxy registration that throws after binding.
|
||||
releases.push(() => control.removeEventListener('click', click))
|
||||
const unbind = art.proxy(control, 'click', click)
|
||||
if (closed)
|
||||
unbind()
|
||||
else
|
||||
releases.push(unbind)
|
||||
if (closed || !alive())
|
||||
return
|
||||
const update = (active: unknown) => {
|
||||
if (alive())
|
||||
utils.tooltip(control, art.i18n.get(active ? 'Exit PIP Mode' : 'PIP Mode'))
|
||||
}
|
||||
releases.push(() => art.off('document-pip', update))
|
||||
art.on('document-pip', update)
|
||||
},
|
||||
})
|
||||
if (closed)
|
||||
art.controls.remove('document-pip')
|
||||
},
|
||||
destroy() {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
const errors = releaseAll(releases.splice(0).reverse())
|
||||
if (errors.length)
|
||||
throw errors[0]
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import style from './style.less?inline'
|
||||
|
||||
export default function artplayerPluginDocumentPip(userOptions = {}) {
|
||||
const options = {
|
||||
width: 480,
|
||||
height: 270,
|
||||
fallbackToVideoPiP: true,
|
||||
placeholder: `Playing in Document Picture-in-Picture`,
|
||||
...userOptions,
|
||||
}
|
||||
|
||||
return (art) => {
|
||||
const isSupported = 'documentPictureInPicture' in window
|
||||
&& typeof window.documentPictureInPicture?.requestWindow === 'function'
|
||||
|
||||
const { proxy, icons, i18n, template: { $player }, constructor: { utils: { append, tooltip, addClass, removeClass, sleep } } } = art
|
||||
|
||||
const state = {
|
||||
win: null,
|
||||
originalParent: null,
|
||||
originalNext: null,
|
||||
currentDoc: null,
|
||||
placeholder: null,
|
||||
}
|
||||
|
||||
function copyStylesTo(targetDoc) {
|
||||
const base = targetDoc.createElement('style')
|
||||
base.textContent = `
|
||||
html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }
|
||||
#__art_dpip_root { position:absolute; inset:0; display:flex; }
|
||||
#__art_dpip_root > * { width:100% !important; height:100% !important; }
|
||||
`
|
||||
targetDoc.head.appendChild(base)
|
||||
if (!targetDoc.querySelector('meta[name="viewport"]')) {
|
||||
const meta = targetDoc.createElement('meta')
|
||||
meta.name = 'viewport'
|
||||
meta.content = 'width=device-width,initial-scale=1'
|
||||
targetDoc.head.appendChild(meta)
|
||||
}
|
||||
|
||||
try {
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||
const clone = targetDoc.createElement('link')
|
||||
clone.rel = 'stylesheet'
|
||||
clone.href = link.href
|
||||
if (link.media)
|
||||
clone.media = link.media
|
||||
if (link.crossOrigin)
|
||||
clone.crossOrigin = link.crossOrigin
|
||||
if (link.referrerPolicy)
|
||||
clone.referrerPolicy = link.referrerPolicy
|
||||
targetDoc.head.appendChild(clone)
|
||||
})
|
||||
}
|
||||
catch { }
|
||||
|
||||
try {
|
||||
document.querySelectorAll('style').forEach((style) => {
|
||||
const clone = targetDoc.createElement('style')
|
||||
clone.textContent = style.textContent
|
||||
targetDoc.head.appendChild(clone)
|
||||
})
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
async function open() {
|
||||
if (!isSupported && options.fallbackToVideoPiP) {
|
||||
art.pip = true
|
||||
console.warn('[artplayer-plugin-document-pip] Document Picture-in-Picture is not supported, falling back to Video Picture-in-Picture')
|
||||
return
|
||||
}
|
||||
|
||||
if (state.win)
|
||||
return
|
||||
|
||||
try {
|
||||
const pipWin = await window.documentPictureInPicture.requestWindow({
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
})
|
||||
|
||||
state.win = pipWin
|
||||
state.currentDoc = pipWin.document
|
||||
|
||||
state.placeholder = document.createElement('div')
|
||||
state.placeholder.className = 'artplayer-document-pip-placeholder'
|
||||
state.placeholder.style.cssText = `display:flex;justify-content:center;align-items:center;width:100%;height:100%;`
|
||||
state.placeholder.textContent = options.placeholder
|
||||
state.originalParent = $player.parentNode
|
||||
state.originalNext = $player.nextSibling
|
||||
state.originalParent.insertBefore(state.placeholder, state.originalNext)
|
||||
|
||||
const root = pipWin.document.createElement('div')
|
||||
root.id = '__art_dpip_root'
|
||||
pipWin.document.body.appendChild(root)
|
||||
|
||||
copyStylesTo(pipWin.document)
|
||||
|
||||
const adopted = pipWin.document.adoptNode($player)
|
||||
root.appendChild(adopted)
|
||||
state.currentDoc = pipWin.document
|
||||
|
||||
const onResize = () => art.resize?.()
|
||||
const onClose = () => close()
|
||||
|
||||
pipWin.addEventListener('resize', onResize)
|
||||
pipWin.addEventListener('pagehide', onClose)
|
||||
pipWin.addEventListener('unload', onClose)
|
||||
|
||||
state.cleanup = () => {
|
||||
pipWin.removeEventListener('resize', onResize)
|
||||
pipWin.removeEventListener('pagehide', onClose)
|
||||
pipWin.removeEventListener('unload', onClose)
|
||||
}
|
||||
|
||||
addClass($player, 'artplayer-document-pip')
|
||||
art.events.bindGlobalEvents?.()
|
||||
art.emit('document-pip', true)
|
||||
await sleep(100)
|
||||
art.emit('resize')
|
||||
}
|
||||
catch (err) {
|
||||
art.notice.show = 'Document Picture-in-Picture open failed'
|
||||
console.warn('[artplayer-plugin-document-pip] open failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function restoreToOriginalDocument() {
|
||||
if (!state.placeholder)
|
||||
return
|
||||
const backDoc = state.placeholder.ownerDocument || document
|
||||
const adoptedBack = backDoc.adoptNode($player)
|
||||
state.originalParent.insertBefore(adoptedBack, state.placeholder)
|
||||
state.placeholder.remove()
|
||||
state.placeholder = null
|
||||
state.currentDoc = backDoc
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (!state.win)
|
||||
return
|
||||
try {
|
||||
state.cleanup?.()
|
||||
restoreToOriginalDocument()
|
||||
state.win.close()
|
||||
state.win = null
|
||||
removeClass($player, 'artplayer-document-pip')
|
||||
art.events.bindGlobalEvents?.()
|
||||
art.emit('document-pip', false)
|
||||
await sleep(100)
|
||||
art.emit('resize')
|
||||
}
|
||||
catch (err) {
|
||||
art.notice.show = 'Document Picture-in-Picture close failed'
|
||||
console.warn('[artplayer-plugin-document-pip] close failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (state.win) {
|
||||
close()
|
||||
}
|
||||
else {
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
art.controls.add({
|
||||
name: 'document-pip',
|
||||
position: 'right',
|
||||
index: 40,
|
||||
tooltip: art.i18n.get('PIP Mode'),
|
||||
mounted: ($control) => {
|
||||
append($control, icons.pip)
|
||||
proxy($control, 'click', toggle)
|
||||
art.on('document-pip', (value) => {
|
||||
tooltip($control, i18n.get(value ? 'Exit PIP Mode' : 'PIP Mode'))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
art.on('destroy', close)
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginDocumentPip',
|
||||
get isSupported() { return isSupported },
|
||||
get isActive() { return !!state.win },
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-document-pip'
|
||||
let $style = document.getElementById(id)
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.head.appendChild($style)
|
||||
})
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
}
|
||||
}
|
||||
$style.textContent = style
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type PublicPlugin from '../types/artplayer-plugin-document-pip'
|
||||
import type { WindowApi } from './window-session'
|
||||
import { createControl } from './control'
|
||||
import { createProjection } from './projection'
|
||||
import { releaseAll } from './resources'
|
||||
import { installStyle } from './styles'
|
||||
import { createWindowSession } from './window-session'
|
||||
|
||||
type Option = Parameters<typeof PublicPlugin>[0]
|
||||
|
||||
export default function artplayerPluginDocumentPip(userOptions: Option = {}) {
|
||||
const options = { width: 480, height: 270, fallbackToVideoPiP: true, placeholder: 'Playing in Document Picture-in-Picture', ...userOptions }
|
||||
return (art: Artplayer) => {
|
||||
const browser = window as Window & { documentPictureInPicture?: WindowApi }
|
||||
const isSupported = 'documentPictureInPicture' in browser && typeof browser.documentPictureInPicture?.requestWindow === 'function'
|
||||
let host: (Artplayer & { resize?: () => void }) | null = art
|
||||
let player: HTMLElement | null = art.template.$player
|
||||
let closed = art.isDestroy
|
||||
const utils = (art.constructor as typeof Artplayer).utils
|
||||
const alive = () => !closed && !!host && !host.isDestroy
|
||||
const session = createWindowSession({
|
||||
alive,
|
||||
request: () => browser.documentPictureInPicture!.requestWindow({ width: options.width, height: options.height }),
|
||||
project: document => createProjection(player!, document, options.placeholder),
|
||||
activate(active) {
|
||||
if (!host || !player)
|
||||
return
|
||||
if (active)
|
||||
utils.addClass(player, 'artplayer-document-pip')
|
||||
else
|
||||
utils.removeClass(player, 'artplayer-document-pip')
|
||||
if (!alive())
|
||||
return
|
||||
host.events.bindGlobalEvents?.()
|
||||
if (alive())
|
||||
host.emit('document-pip', active)
|
||||
},
|
||||
resize: () => {
|
||||
if (alive())
|
||||
host!.emit('resize')
|
||||
},
|
||||
nativeResize: () => {
|
||||
if (alive())
|
||||
host!.resize?.()
|
||||
},
|
||||
report(action, error) {
|
||||
if (!alive())
|
||||
return
|
||||
host!.notice.show = `Document Picture-in-Picture ${action} failed`
|
||||
console.warn(`[artplayer-plugin-document-pip] ${action} failed:`, error)
|
||||
},
|
||||
})
|
||||
async function open() {
|
||||
if (!alive())
|
||||
return
|
||||
if (!isSupported && options.fallbackToVideoPiP) {
|
||||
host!.pip = true
|
||||
console.warn('[artplayer-plugin-document-pip] Document Picture-in-Picture is not supported, falling back to Video Picture-in-Picture')
|
||||
return
|
||||
}
|
||||
return session.open()
|
||||
}
|
||||
function toggle() {
|
||||
if (!alive())
|
||||
return
|
||||
if (session.active || session.opening)
|
||||
void session.close()
|
||||
else
|
||||
void open()
|
||||
}
|
||||
let control: ReturnType<typeof createControl> | null = createControl(art, toggle, alive)
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
const current = host
|
||||
const previousControl = control
|
||||
control = null
|
||||
const errors = releaseAll([session.destroy, () => previousControl?.destroy(), () => current?.off('destroy', destroy)])
|
||||
host = null
|
||||
player = null
|
||||
if (errors.length)
|
||||
console.warn('[artplayer-plugin-document-pip] cleanup failed:', errors[0])
|
||||
}
|
||||
const result = {
|
||||
name: 'artplayerPluginDocumentPip' as const,
|
||||
get isSupported() { return isSupported },
|
||||
get isActive() { return session.active },
|
||||
open,
|
||||
close: session.close,
|
||||
toggle,
|
||||
}
|
||||
if (closed) {
|
||||
host = null
|
||||
player = null
|
||||
return result
|
||||
}
|
||||
try {
|
||||
art.on('destroy', destroy)
|
||||
if (alive())
|
||||
control?.mount()
|
||||
else
|
||||
art.off('destroy', destroy)
|
||||
}
|
||||
catch (error) {
|
||||
destroy()
|
||||
throw error
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined')
|
||||
installStyle(document)
|
||||
@@ -0,0 +1,61 @@
|
||||
import { releaseAll } from './resources'
|
||||
import { copyStyles } from './styles'
|
||||
|
||||
export function createProjection(player: HTMLElement, target: Document, text: string | undefined) {
|
||||
const source = player.ownerDocument
|
||||
const parent = player.parentNode
|
||||
const next = player.nextSibling
|
||||
if (!parent)
|
||||
throw new Error('Document Picture-in-Picture requires a mounted player')
|
||||
const placeholder = source.createElement('div')
|
||||
placeholder.className = 'artplayer-document-pip-placeholder'
|
||||
placeholder.style.cssText = 'display:flex;justify-content:center;align-items:center;width:100%;height:100%;'
|
||||
placeholder.textContent = text ?? ''
|
||||
const root = target.createElement('div')
|
||||
root.id = '__art_dpip_root'
|
||||
const nodes: HTMLElement[] = []
|
||||
let closed = false
|
||||
const restore = () => {
|
||||
// Insertion adopts atomically; never detach the player before validating the anchor.
|
||||
const anchor = placeholder.parentNode === parent ? placeholder : next?.parentNode === parent ? next : null
|
||||
if (player.parentNode !== parent || player.nextSibling !== anchor) {
|
||||
try {
|
||||
parent.insertBefore(player, anchor)
|
||||
}
|
||||
catch (error) {
|
||||
// Keep a recoverable node in the original document if its parent rejects insertion.
|
||||
source.body.appendChild(player)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
const cleanup = () => {
|
||||
const errors = releaseAll([restore, () => placeholder.remove(), ...nodes.splice(0).map(node => () => node.remove()), () => root.remove()])
|
||||
if (errors.length)
|
||||
throw errors[0]
|
||||
}
|
||||
return {
|
||||
mount() {
|
||||
if (closed)
|
||||
return
|
||||
parent.insertBefore(placeholder, next?.parentNode === parent ? next : null)
|
||||
if (!closed)
|
||||
target.body.appendChild(root)
|
||||
if (!closed)
|
||||
copyStyles(source, target, node => nodes.push(node), () => !closed)
|
||||
if (!closed) {
|
||||
const adopted = target.adoptNode(player)
|
||||
if (!closed)
|
||||
root.appendChild(adopted)
|
||||
}
|
||||
if (closed)
|
||||
cleanup()
|
||||
},
|
||||
destroy() {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
cleanup()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export type Disposer = () => void
|
||||
|
||||
export function releaseAll(releases: Disposer[]): unknown[] {
|
||||
const errors: unknown[] = []
|
||||
for (const release of releases) {
|
||||
try {
|
||||
release()
|
||||
}
|
||||
catch (error) { errors.push(error) }
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
export function createDelay() {
|
||||
let finish!: () => void
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
finish = resolve
|
||||
})
|
||||
let timer: ReturnType<typeof setTimeout> | null = setTimeout(() => {
|
||||
timer = null
|
||||
finish()
|
||||
}, 100)
|
||||
return {
|
||||
promise,
|
||||
cancel() {
|
||||
if (timer !== null)
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
finish()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import style from './style.less?inline'
|
||||
|
||||
export function installStyle(document: Document): void {
|
||||
const id = 'artplayer-plugin-document-pip'
|
||||
const existing = document.getElementById(id)
|
||||
if (existing) {
|
||||
existing.textContent = style
|
||||
return
|
||||
}
|
||||
const element = document.createElement('style')
|
||||
element.id = id
|
||||
element.textContent = style
|
||||
const insert = () => {
|
||||
document.removeEventListener('DOMContentLoaded', insert)
|
||||
const current = document.getElementById(id)
|
||||
if (current)
|
||||
current.textContent = style
|
||||
else
|
||||
(document.head || document.documentElement).appendChild(element)
|
||||
}
|
||||
if (document.readyState === 'loading')
|
||||
document.addEventListener('DOMContentLoaded', insert)
|
||||
else
|
||||
insert()
|
||||
}
|
||||
|
||||
export function copyStyles(source: Document, target: Document, own: (node: HTMLElement) => void, alive: () => boolean): void {
|
||||
const append = (node: HTMLElement) => {
|
||||
if (!alive())
|
||||
return
|
||||
own(node)
|
||||
target.head.appendChild(node)
|
||||
if (!alive())
|
||||
node.remove()
|
||||
}
|
||||
const base = target.createElement('style')
|
||||
base.textContent = `
|
||||
html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }
|
||||
#__art_dpip_root { position:absolute; inset:0; display:flex; }
|
||||
#__art_dpip_root > * { width:100% !important; height:100% !important; }
|
||||
`
|
||||
append(base)
|
||||
if (!target.querySelector('meta[name="viewport"]')) {
|
||||
const meta = target.createElement('meta')
|
||||
meta.name = 'viewport'
|
||||
meta.content = 'width=device-width,initial-scale=1'
|
||||
append(meta)
|
||||
}
|
||||
try {
|
||||
source.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]').forEach((link) => {
|
||||
const clone = target.createElement('link')
|
||||
clone.rel = 'stylesheet'
|
||||
clone.href = link.href
|
||||
if (link.media)
|
||||
clone.media = link.media
|
||||
if (link.crossOrigin)
|
||||
clone.crossOrigin = link.crossOrigin
|
||||
if (link.referrerPolicy)
|
||||
clone.referrerPolicy = link.referrerPolicy
|
||||
append(clone)
|
||||
})
|
||||
}
|
||||
catch { /* Preserve best-effort copying when stylesheets are inaccessible. */ }
|
||||
try {
|
||||
source.querySelectorAll('style').forEach((element) => {
|
||||
const clone = target.createElement('style')
|
||||
clone.textContent = element.textContent
|
||||
append(clone)
|
||||
})
|
||||
}
|
||||
catch { /* A page may restrict access to its style nodes. */ }
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import type { Disposer } from './resources'
|
||||
import { createDelay, releaseAll } from './resources'
|
||||
|
||||
export interface WindowApi {
|
||||
requestWindow: (option: { width?: number, height?: number }) => Promise<Window>
|
||||
}
|
||||
|
||||
interface Hooks {
|
||||
alive: () => boolean
|
||||
request: () => Promise<Window>
|
||||
project: (document: Document) => { mount: () => void, destroy: Disposer }
|
||||
activate: (active: boolean) => void
|
||||
resize: Disposer
|
||||
nativeResize: Disposer
|
||||
report: (action: 'open' | 'close', error: unknown) => void
|
||||
}
|
||||
|
||||
interface Session {
|
||||
window: Window
|
||||
releases: Disposer[]
|
||||
announced: boolean
|
||||
}
|
||||
|
||||
export function createWindowSession(hooks: Hooks) {
|
||||
let destroyed = false
|
||||
let revision = 0
|
||||
let session: Session | null = null
|
||||
let pending: { promise: Promise<void>, finish: Disposer } | null = null
|
||||
let delay: ReturnType<typeof createDelay> | null = null
|
||||
const alive = () => !destroyed && hooks.alive()
|
||||
const valid = (value: number) => value === revision && alive()
|
||||
function invalidate() {
|
||||
revision++
|
||||
pending?.finish()
|
||||
pending = null
|
||||
delay?.cancel()
|
||||
delay = null
|
||||
}
|
||||
function dispose(current: Session): unknown[] {
|
||||
const errors = releaseAll(current.releases.splice(0))
|
||||
errors.push(...releaseAll([() => current.window.close()]))
|
||||
return errors
|
||||
}
|
||||
async function resizeLater(value: number) {
|
||||
if (!valid(value))
|
||||
return
|
||||
const current = createDelay()
|
||||
delay = current
|
||||
await current.promise
|
||||
if (delay === current)
|
||||
delay = null
|
||||
if (valid(value))
|
||||
hooks.resize()
|
||||
}
|
||||
async function close() {
|
||||
invalidate()
|
||||
const value = revision
|
||||
const current = session
|
||||
session = null
|
||||
if (!current)
|
||||
return
|
||||
const errors = dispose(current)
|
||||
if (current.announced) {
|
||||
try {
|
||||
hooks.activate(false)
|
||||
}
|
||||
catch (error) { errors.push(error) }
|
||||
}
|
||||
if (errors.length && alive())
|
||||
hooks.report('close', errors[0])
|
||||
if (valid(value))
|
||||
await resizeLater(value)
|
||||
}
|
||||
async function open() {
|
||||
if (!alive() || session)
|
||||
return
|
||||
if (pending)
|
||||
return pending.promise
|
||||
invalidate()
|
||||
const value = revision
|
||||
let finish!: Disposer
|
||||
let reject!: (error: unknown) => void
|
||||
const promise = new Promise<void>((resolve, fail) => {
|
||||
finish = resolve
|
||||
reject = fail
|
||||
})
|
||||
const ticket = { promise, finish }
|
||||
pending = ticket
|
||||
const settle = () => {
|
||||
if (pending === ticket)
|
||||
pending = null
|
||||
finish()
|
||||
}
|
||||
const failed = (error: unknown) => {
|
||||
if (valid(value)) {
|
||||
const current = session
|
||||
session = null
|
||||
if (current) {
|
||||
dispose(current)
|
||||
if (current.announced)
|
||||
releaseAll([() => hooks.activate(false)])
|
||||
}
|
||||
try {
|
||||
hooks.report('open', error)
|
||||
}
|
||||
catch (noticeError) { reject(noticeError) }
|
||||
}
|
||||
}
|
||||
const receive = async (window: Window) => {
|
||||
if (!valid(value)) {
|
||||
// Only the window is retained by a stale request, never a live projection.
|
||||
if (session?.window !== window)
|
||||
releaseAll([() => window.close()])
|
||||
return
|
||||
}
|
||||
const current: Session = { window, releases: [], announced: false }
|
||||
session = current
|
||||
try {
|
||||
const projection = hooks.project(window.document)
|
||||
current.releases.push(projection.destroy)
|
||||
projection.mount()
|
||||
if (!valid(value)) {
|
||||
dispose(current)
|
||||
return
|
||||
}
|
||||
for (const [event, callback] of [['resize', () => {
|
||||
if (valid(value))
|
||||
hooks.nativeResize()
|
||||
}], ['pagehide', () => {
|
||||
void close()
|
||||
}], ['unload', () => {
|
||||
void close()
|
||||
}]] as const) {
|
||||
current.releases.unshift(() => window.removeEventListener(event, callback))
|
||||
window.addEventListener(event, callback)
|
||||
if (!valid(value)) {
|
||||
dispose(current)
|
||||
return
|
||||
}
|
||||
}
|
||||
current.announced = true
|
||||
hooks.activate(true)
|
||||
await resizeLater(value)
|
||||
}
|
||||
catch (error) { failed(error) }
|
||||
}
|
||||
try {
|
||||
// Called directly while the caller's user activation is still available.
|
||||
const request = hooks.request()
|
||||
void Promise.resolve(request).then(receive, failed).then(settle, (error) => {
|
||||
if (valid(value))
|
||||
reject(error)
|
||||
settle()
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
failed(error)
|
||||
}
|
||||
finally {
|
||||
settle()
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
return {
|
||||
get active() { return session !== null },
|
||||
get opening() { return pending !== null },
|
||||
open,
|
||||
close,
|
||||
destroy() {
|
||||
if (destroyed)
|
||||
return
|
||||
destroyed = true
|
||||
// close performs synchronous restoration; its delayed effects are now disabled.
|
||||
void close().catch(() => {})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": { "allowJs": false },
|
||||
"include": ["src/**/*.ts", "types/**/*.d.ts", "../../types/assets.d.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user