refactor(thumbnail): [PKG-TOOL-THUMB-04] preserve approved published defaults

This commit is contained in:
Harvey Zhao committed 2026-09-15 23:54:24 +08:00
1 parent 490e157717
commit b5f8b5e28e
37 files changed
+11172 -136

No files matched your search

@@ -2,14 +2,13 @@
The runtime responsibilities were separated under PKG-TOOL-THUMB-03 and are now
strict TypeScript under PKG-TOOL-THUMB-04. Public declarations and isolated installed
entrypoints now have checks; the historical default-policy decision and final
integration/release gates remain open.
entrypoints now have checks; final integration/release gates remain open.
The pending choice is now specified in the
[compatibility proposal](../../refactor/thumbnail-compatibility-decision.md):
The user approved the
[compatibility decision](../../refactor/thumbnail-compatibility-decision.md) on 2026-09-15:
recovered 3.5.31 defaults versus opt-in workspace 4.4.0 behavior, including the
class-level DEFAULTS boundary. It is not approved or implemented; do not infer
permission from the approved VAST or other package decisions.
class-level DEFAULTS boundary. The root constructor now defaults to the published
policy; `compatibility: 'workspace-4.4'` selects the previous workspace behavior.
| File | Responsibility |
| --- | --- |
@@ -18,6 +17,7 @@ permission from the approved VAST or other package decisions.
| src/source.ts | File loading, native metadata/error listeners and source/thumbnail Blob URLs |
| src/extraction.ts | Owned metadata wait and serial frame job, callback/error completion and cancellation |
| src/input.ts | Option validation/clamps, file-input wrapper creation, listener registration/replacement and release |
| src/policy.ts | Approved mode selection and numeric normalization; no DOM or timer ownership |
| src/sheet.ts | Midpoint grid, canvas/footer geometry, temporary download anchor |
| src/emitter.ts | Typed local adaptation of tiny-emitter; preserves on/once/emit/off behavior |
| src/utils.ts | Pure clamp and filename helpers; unused sleep/serial helpers removed |
@@ -30,8 +30,8 @@ Input/source use lifecycle state, extraction uses lifecycle/source, and sheet us
only filename calculation. types.ts contains no imports or executable state.
Input records live in a private WeakMap, so callers replacing `option` cannot
lose ownership of generated inputs/listeners. Normal construction preserves the
existing instance-field order and bound inputChange/ondrop methods. DEFAULTS,
file/video event order, public method spelling and return values stay unchanged.
existing instance-field order and bound inputChange/ondrop methods. Public method
spelling and return values stay unchanged. DEFAULTS includes published delay: 300.
Input setup validates options before changing DOM or committed options. A wrapper
receives one owned input; repeated setup with that wrapper reuses it. Replacement
@@ -62,8 +62,9 @@ behavior, checks first-error identity across nested cleanup and verifies resourc
## Extraction and event ordering
Loading emits file before assigning video.src, then video synchronously as in
workspace 4.4.0. A source generation prevents an older file callback or URL creation
Loading emits file before assigning video.src, then video after the configured
delay in the default published policy, or synchronously in workspace mode.
A source generation prevents an older file callback or URL creation
hook from overwriting a nested newer load. Successful replacement revokes old
source URLs. Native errors report once for their source; stale listeners cannot
fail the latest job. Existing sheet URLs stay available until the next sheet frame
@@ -76,7 +77,15 @@ Preflight stays synchronous when metadata is ready. Canvas precedes processing=t
updates observe true, done observes false and may start another job without old
completion clearing the new state.
Frames wait for readiness/seek, accept readiness/Blob callbacks once and restore
Published mode keeps configured height (clamped to 10-1000), waits delay after each
seek and delay * 2 after the last update before done. Delay defaults to 300 and is
clamped to 10-1000. Workspace mode derives height from video aspect ratio, ignores
delay and resets the selected input value; published mode retains that value.
Source notifications and extraction waits own their timers separately, so replacement,
error and destroy cancel both without late events. A null policy delay means no
timer; NaN retains the historical asynchronous browser timer behavior.
Frames wait for readiness/seek as well as any policy delay, accept readiness/Blob callbacks once and restore
oncanplay only while still owning it. Seek/draw/encoding failures, null Blob and
throwing callbacks settle the promise and detach job resources. Source replacement
or destruction rejects with AbortError without an error event. The public promise
@@ -117,14 +126,16 @@ Safari/WebKit evidence in 05. The independent tool example is
`test/thumbnail-lifecycle.test.js` covers jobs, source cancellation, native errors,
callback failures, reentrancy and private resources. Browser tests hold real PNG
callbacks and replace a selected file during encoding; only the latest job may
complete. Continue published declarations and installed consumers in 04; preserve
old tests and the default delay/height boundary. The [public type guide](types/README.md)
complete. Preserve old tests and the approved delay/height boundary. The new
`test/thumbnail-policy.test.js` covers both modes, owned delays and cancellation;
browser extraction checks both policies against the frozen historical bundles.
The [public type guide](types/README.md)
documents declaration ownership, module entries, events and installed consumers.
See [task plan](../../refactor/plan.md) and [risk ledger](../../refactor/risks.json).
## Type and provenance boundaries
All eight executable source modules and the shared type module are checked with
All nine executable source modules and the shared type module are checked with
strict, noUncheckedIndexedAccess, noImplicitOverride, skipLibCheck=false and no
ambient Node/test globals. Public fields use declare so TypeScript does not add
early undefined properties or change constructor property order. File/URL/density
+22 -1
View File
@@ -4,9 +4,30 @@ Thumbnail tool for ArtPlayer
Implementation and migration notes: [ARCHITECTURE.md](./ARCHITECTURE.md).
The default constructor preserves published 3.5.31 behavior: fixed `height: 90`,
`delay: 300` before the video event and each frame, and twice that delay before
completion. Numeric height/delay values are clamped to 10-1000. File selection
retains the input value. Existing methods, including `creatCanvas` and
`creatScreenshotDate`, retain their names.
Consumers of the unpublished 4.4 workspace behavior should select it explicitly:
```js
const tool = new ArtplayerToolThumbnail({
fileInput: document.querySelector('input[type=file]'),
compatibility: 'workspace-4.4',
})
```
This mode calculates height from the video aspect ratio, emits `video` synchronously,
has no fixed extraction delays and clears the file input value. Class-level
`DEFAULTS` always describes the published policy, including `delay`; it does not
vary by instance. Both modes wait for frame readiness and cancel owned work on
source replacement or destruction.
## Demo
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-tool-thumbnail/index.js&example=thumbnail)
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-tool-thumbnail/index.js&example=tool.thumbnail)
## License
@@ -31,4 +31,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,r){const o=this.e||(this.e={});let i=e(o,t)?o[t]:void 0;return i||(i=[],Object.defineProperty(o,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:r}),this}once(e,t,n){const r=this,o=t;let i=!1;function s(...t){i||(i=!0,r.off(e,s),o.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const r=this.e||(this.e={}),o=(e(r,t)&&r[t]||[]).slice();for(let e=0;e<o.length;e+=1)o[e].fn.apply(o[e].ctx,n);return this}off(t,n){const r=this.e||(this.e={}),o=e(r,t)?r[t]:void 0,i=[];if(o&&n)for(let e=0,s=o.length;e<s;e+=1)o[e].fn!==n&&o[e].fn._!==n&&i.push(o[e]);return i.length?r[t]=i:delete r[t],this}}const n=new WeakMap;function r(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function o(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const o of e)try{o()}catch(r){n||(n=!0,t=r)}if(n)throw t}function c(e,t="destroyed"){const n=r(e);return!n.closed&&(n.closed=!0,n.epoch++,n.job?.cancel(t),!0)}function a(e){const t=r(e),n=new Set([...t.sourceUrls,e.videoUrl]),o=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const r of new Set([t.video,e.video]))r&&c.push(()=>r.pause?.(),()=>r.removeAttribute?.("src"),()=>r.load?.(),()=>r.parentNode?.removeChild(r));s([...t.sourceListeners.splice(0),...c,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...o].map(e=>()=>i(t.thumbnailUrls,e))])}function l(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e,t){let n=t.epoch;const c=e.video;let a,u,d,h,p=!1,f=!1,m=()=>{};const b=[],y=new Promise((e,t)=>{a=e,u=t});y.catch(()=>{});const g=()=>!p&&!t.closed&&t.epoch===n&&t.job===h;function v(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),s([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{v()}catch(r){t||(t=r)}if(n)try{e.emit("error",t?.message)}catch(o){t=o}u(t)}}function U(t){if(!g())return;f=!0,e.processing=!0;let n=0;const o=()=>{if(!g())return;if(n===t.points.length)return void function(){if(g())try{v(),e.emit("done"),a()}catch(t){let r=t;try{e.emit("error",t?.message)}catch(n){r=n}u(r)}}();const s=t.points[n];let l=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!g()||l||c.seeking||void 0!==c.readyState&&c.readyState<2)){l=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,n){const o=r(e),s=URL.createObjectURL(t);if(o.thumbnailUrls.add(s),!n())return i(o.thumbnailUrls,s),null;const c=new Set([...o.thumbnailUrls,e.thumbnailUrl]);o.thumbnailUrl=s,e.thumbnailUrl=s;for(const r of c)r&&r!==s&&i(o.thumbnailUrls,r);return n()?s:null}(e,s,g);if(!c)return;e.emit("update",c,(n+1)/t.number),n++,Promise.resolve().then(o)}catch(c){w(c)}}})}catch(a){w(a)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(o)}function E(n=!1){if(g()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,a=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=l(i,0,n.duration),e.option.end=l(s||n.duration,i,n.duration),a(e.option.end>e.option.begin,"End time must be greater than the start time"),a(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,a(e.file&&n,"Please select the video file first"),a(!e.processing,"There is currently a task in progress, please wait a moment..."),a(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),g()||clearTimeout(d)}h={video:c,promise:y,ready:E,fail:w,cancel(e){w(o(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(n=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{g()&&w(new Error(`Unable to load video: media error ${c.error?.code||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeEventLisLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,o,n){const r=this.e||(this.e={});let i=e(r,t)?r[t]:void 0;return i||(i=[],Object.defineProperty(r,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:o,ctx:n}),this}once(e,t,o){const n=this,r=t;let i=!1;function s(...t){i||(i=!0,n.off(e,s),r.apply(o,t))}return s._=t,this.on(e,s,o)}emit(t,...o){const n=this.e||(this.e={}),r=(e(n,t)&&n[t]||[]).slice();for(let e=0;e<r.length;e+=1)r[e].fn.apply(r[e].ctx,o);return this}off(t,o){const n=this.e||(this.e={}),r=e(n,t)?n[t]:void 0,i=[];if(r&&o)for(let e=0,s=r.length;e<s;e+=1)r[e].fn!==o&&r[e].fn._!==o&&i.push(r[e]);return i.length?n[t]=i:delete n[t],this}}const o=new WeakMap;function n(e){return o.has(e)||o.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),o.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,o=!1;for(const r of e)try{r()}catch(n){o||(o=!0,t=n)}if(o)throw t}function c(e,t="destroyed"){const o=n(e);return!o.closed&&(o.closed=!0,o.epoch++,o.job?.cancel(t),!0)}function a(e){const t=n(e),o=new Set([...t.sourceUrls,e.videoUrl]),r=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const n of new Set([t.video,e.video]))n&&c.push(()=>n.pause?.(),()=>n.removeAttribute?.("src"),()=>n.load?.(),()=>n.parentNode?.removeChild(n));s([...t.sourceListeners.splice(0),...c,...[...o].map(e=>()=>i(t.sourceUrls,e)),...[...r].map(e=>()=>i(t.thumbnailUrls,e))])}function l(e,t,o){return Math.max(Math.min(e,Math.max(t,o)),Math.min(t,o))}function u(e){const t="workspace-4.4"===e.compatibility;return{delay:t?null:e.delay,aspectHeight:t,resetInput:t}}function d(e,t){let o=t.epoch;const c=e.video;let a,d,h,p,f=!1,m=!1,b=()=>{};const y=[],g=new Promise((e,t)=>{a=e,d=t});g.catch(()=>{});const v=()=>!f&&!t.closed&&t.epoch===o&&t.job===p;function w(){return!f&&(f=!0,t.job===p&&(t.job=null,e.processing=!1),s([()=>clearTimeout(h),b,...y.splice(0)]),!0)}function U(t,o=!0){if(!f){try{w()}catch(n){t||(t=n)}if(o)try{e.emit("error",t?.message)}catch(r){t=r}d(t)}}function E(){if(v())try{w(),e.emit("done"),a()}catch(t){let n=t;try{e.emit("error",t?.message)}catch(o){n=o}d(n)}}function T(t){if(!v())return;m=!0,e.processing=!0;let o=0;const r=()=>{if(!v())return;if(o===t.points.length)return void(null!==t.delay?h=setTimeout(E,2*Number(t.delay)):E());const s=t.points[o];let a=!1,l=!1,u=null===t.delay;const d=c.oncanplay,p=()=>{if(!(!v()||!u||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(b(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!v())return;t.canvas.toBlob(s=>{if(v()&&!l){l=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,o){const r=n(e),s=URL.createObjectURL(t);if(r.thumbnailUrls.add(s),!o())return i(r.thumbnailUrls,s),null;const c=new Set([...r.thumbnailUrls,e.thumbnailUrl]);r.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&i(r.thumbnailUrls,n);return o()?s:null}(e,s,v);if(!c)return;e.emit("update",c,(o+1)/t.number),o++,Promise.resolve().then(r)}catch(c){U(c)}}})}catch(d){U(d)}}};b=()=>{c.oncanplay===p&&(c.oncanplay=d),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,null!==t.delay?(h=setTimeout(()=>{u=!0,p()},t.delay),v()||clearTimeout(h)):c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){U(f)}};Promise.resolve().then(r)}function j(o=!1){if(v()&&!m)if(clearTimeout(h),!t.loading&&c.duration){m=!0;try{const t=function(e,t){const{video:o}=t,{width:n,number:r,begin:i,end:s}=e.option,c=u(e.option),a=c.aspectHeight?o.videoHeight/o.videoWidth*n:e.option.height,d=(o,n)=>{try{e.errorHandle(o,n)}catch(r){throw t.reported=r,t.wasReported=!0,r}};e.option.height=a,e.option.begin=l(i,0,o.duration),e.option.end=l(s||o.duration,i,o.duration),d(e.option.end>e.option.begin,"End time must be greater than the start time"),d(Number.isFinite(o.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=r/e.duration,d(e.file&&o,"Please select the video file first"),d(!e.processing,"There is currently a task in progress, please wait a moment..."),d(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const h=e.creatScreenshotDate(),p=e.creatCanvas(),f=p.getContext("2d");return e.emit("canvas",p),{width:n,height:a,number:r,points:h,canvas:p,context:f,delay:c.delay}}(e,p);T(t)}catch(n){if(U(n,!o&&!(p.wasReported&&n===p.reported)),o)throw n}}else h=setTimeout(()=>j(),1e3),v()Line truncated
@@ -31,4 +31,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,r){const o=this.e||(this.e={});let i=e(o,t)?o[t]:void 0;return i||(i=[],Object.defineProperty(o,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:r}),this}once(e,t,n){const r=this,o=t;let i=!1;function s(...t){i||(i=!0,r.off(e,s),o.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const r=this.e||(this.e={}),o=(e(r,t)&&r[t]||[]).slice();for(let e=0;e<o.length;e+=1)o[e].fn.apply(o[e].ctx,n);return this}off(t,n){const r=this.e||(this.e={}),o=e(r,t)?r[t]:void 0,i=[];if(o&&n)for(let e=0,s=o.length;e<s;e+=1)o[e].fn!==n&&o[e].fn._!==n&&i.push(o[e]);return i.length?r[t]=i:delete r[t],this}}const n=new WeakMap;function r(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function o(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const o of e)try{o()}catch(r){n||(n=!0,t=r)}if(n)throw t}function c(e,t="destroyed"){var n;const o=r(e);return!o.closed&&(o.closed=!0,o.epoch++,null==(n=o.job)||n.cancel(t),!0)}function l(e){const t=r(e),n=new Set([...t.sourceUrls,e.videoUrl]),o=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const r of new Set([t.video,e.video]))r&&c.push(()=>{var e;return null==(e=r.pause)?void 0:e.call(r)},()=>{var e;return null==(e=r.removeAttribute)?void 0:e.call(r,"src")},()=>{var e;return null==(e=r.load)?void 0:e.call(r)},()=>{var e;return null==(e=r.parentNode)?void 0:e.removeChild(r)});s([...t.sourceListeners.splice(0),...c,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...o].map(e=>()=>i(t.thumbnailUrls,e))])}function a(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e,t){let n=t.epoch;const c=e.video;let l,u,d,h,p=!1,f=!1,m=()=>{};const b=[],v=new Promise((e,t)=>{l=e,u=t});v.catch(()=>{});const y=()=>!p&&!t.closed&&t.epoch===n&&t.job===h;function g(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),s([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{g()}catch(r){t||(t=r)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(o){t=o}u(t)}}function U(t){if(!y())return;f=!0,e.processing=!0;let n=0;const o=()=>{if(!y())return;if(n===t.points.length)return void function(){if(y())try{g(),e.emit("done"),l()}catch(t){let r=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){r=n}u(r)}}();const s=t.points[n];let a=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!y()||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!y())return;t.canvas.toBlob(s=>{if(y()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,n){const o=r(e),s=URL.createObjectURL(t);if(o.thumbnailUrls.add(s),!n())return i(o.thumbnailUrls,s),null;const c=new Set([...o.thumbnailUrls,e.thumbnailUrl]);o.thumbnailUrl=s,e.thumbnailUrl=s;for(const r of c)r&&r!==s&&i(o.thumbnailUrls,r);return n()?s:null}(e,s,y);if(!c)return;e.emit("update",c,(n+1)/t.number),n++,Promise.resolve().then(o)}catch(c){w(c)}}})}catch(l){w(l)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(o)}function E(n=!1){if(y()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,l=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=a(i,0,n.duration),e.option.end=a(s||n.duration,i,n.duration),l(e.option.end>e.option.begin,"End time must be greater than the start time"),l(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,l(e.file&&n,"Please select the video file first"),l(!e.processing,"There is currently a task in progress, please wait a moment..."),l(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),y()||clearTimeout(d)}h={video:c,promise:v,ready:E,fail:w,cancel(e){w(o(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(n=t,!0)},t.job=h;tryLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,o){const r=this.e||(this.e={});let i=e(r,t)?r[t]:void 0;return i||(i=[],Object.defineProperty(r,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:o}),this}once(e,t,n){const o=this,r=t;let i=!1;function s(...t){i||(i=!0,o.off(e,s),r.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const o=this.e||(this.e={}),r=(e(o,t)&&o[t]||[]).slice();for(let e=0;e<r.length;e+=1)r[e].fn.apply(r[e].ctx,n);return this}off(t,n){const o=this.e||(this.e={}),r=e(o,t)?o[t]:void 0,i=[];if(r&&n)for(let e=0,s=r.length;e<s;e+=1)r[e].fn!==n&&r[e].fn._!==n&&i.push(r[e]);return i.length?o[t]=i:delete o[t],this}}const n=new WeakMap;function o(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const r of e)try{r()}catch(o){n||(n=!0,t=o)}if(n)throw t}function l(e,t="destroyed"){var n;const r=o(e);return!r.closed&&(r.closed=!0,r.epoch++,null==(n=r.job)||n.cancel(t),!0)}function a(e){const t=o(e),n=new Set([...t.sourceUrls,e.videoUrl]),r=new Set([...t.thumbnailUrls,e.thumbnailUrl]),l=[];for(const o of new Set([t.video,e.video]))o&&l.push(()=>{var e;return null==(e=o.pause)?void 0:e.call(o)},()=>{var e;return null==(e=o.removeAttribute)?void 0:e.call(o,"src")},()=>{var e;return null==(e=o.load)?void 0:e.call(o)},()=>{var e;return null==(e=o.parentNode)?void 0:e.removeChild(o)});s([...t.sourceListeners.splice(0),...l,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...r].map(e=>()=>i(t.thumbnailUrls,e))])}function c(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e){const t="workspace-4.4"===e.compatibility;return{delay:t?null:e.delay,aspectHeight:t,resetInput:t}}function d(e,t){let n=t.epoch;const l=e.video;let a,d,h,p,f=!1,m=!1,b=()=>{};const y=[],v=new Promise((e,t)=>{a=e,d=t});v.catch(()=>{});const g=()=>!f&&!t.closed&&t.epoch===n&&t.job===p;function w(){return!f&&(f=!0,t.job===p&&(t.job=null,e.processing=!1),s([()=>clearTimeout(h),b,...y.splice(0)]),!0)}function U(t,n=!0){if(!f){try{w()}catch(o){t||(t=o)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(r){t=r}d(t)}}function E(){if(g())try{w(),e.emit("done"),a()}catch(t){let o=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){o=n}d(o)}}function T(t){if(!g())return;m=!0,e.processing=!0;let n=0;const r=()=>{if(!g())return;if(n===t.points.length)return void(null!==t.delay?h=setTimeout(E,2*Number(t.delay)):E());const s=t.points[n];let a=!1,c=!1,u=null===t.delay;const d=l.oncanplay,p=()=>{if(!(!g()||!u||a||l.seeking||void 0!==l.readyState&&l.readyState<2)){a=!0;try{if(b(),t.context.drawImage(l,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!c){c=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const l=function(e,t,n){const r=o(e),s=URL.createObjectURL(t);if(r.thumbnailUrls.add(s),!n())return i(r.thumbnailUrls,s),null;const l=new Set([...r.thumbnailUrls,e.thumbnailUrl]);r.thumbnailUrl=s,e.thumbnailUrl=s;for(const o of l)o&&o!==s&&i(r.thumbnailUrls,o);return n()?s:null}(e,s,g);if(!l)return;e.emit("update",l,(n+1)/t.number),n++,Promise.resolve().then(r)}catch(l){U(l)}}})}catch(d){U(d)}}};b=()=>{l.oncanplay===p&&(l.oncanplay=d),l.removeEventListener("seeked",p)};try{l.oncanplay=p,l.addEventListener("seeked",p),l.currentTime=s.time,null!==t.delay?(h=setTimeout(()=>{u=!0,p()},t.delay),g()||clearTimeout(h)):l.readyState>=2&&!l.seeking&&Promise.resolve().then(p)}catch(f){U(f)}};Promise.resolve().then(r)}function j(n=!1){if(g()&&!m)if(clearTimeout(h),!t.loading&&l.duration){m=!0;try{const t=function(e,t){const{video:n}=t,{width:o,number:r,begin:i,end:s}=e.option,l=u(e.option),a=l.aspectHeight?n.videoHeight/n.videoWidth*o:e.option.height,d=(n,o)=>{try{e.errorHandle(n,o)}catch(r){throw t.reported=r,t.wasReported=!0,r}};e.option.height=a,e.option.begin=c(i,0,n.duration),e.option.end=c(s||n.duration,i,n.duration),d(e.option.end>e.option.begin,"End time must be greater than the start time"),d(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=r/e.duration,d(e.file&&n,"Please select the video file first"),d(!e.processing,"There is currently a task in progress, please wait a moment..."),d(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const h=e.creatScreenshotDate(),p=e.creatCanvas(),f=p.getContext("2d");return e.emiLine truncated
@@ -160,12 +160,39 @@ function releaseMedia(tool) {
...[...thumbnails].map((url) => () => revoke(state.thumbnailUrls, url))
]);
}
function getFileName(name) {
const nameArray = name.split(".");
nameArray.pop();
return nameArray.join(".");
}
function clamp(num, a, b) {
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
}
function thumbnailPolicy(option) {
const workspace = option.compatibility === "workspace-4.4";
return { delay: workspace ? null : option.delay, aspectHeight: workspace, resetInput: workspace };
}
function normalizePolicy(option, validate) {
validate(option.compatibility === void 0 || option.compatibility === "published-3.5" || option.compatibility === "workspace-4.4", "The compatibility option must be published-3.5 or workspace-4.4");
const workspace = thumbnailPolicy(option).aspectHeight;
const fields = workspace ? ["number", "width", "column", "begin", "end"] : ["delay", "number", "width", "height", "column", "begin", "end"];
for (const name of fields)
validate(typeof option[name] === "number", `The '${name}' is not a number`);
option.number = clamp(option.number, 10, 1e3);
option.width = clamp(option.width, 10, 1e3);
option.column = clamp(option.column, 1, 1e3);
if (!workspace) {
option.delay = clamp(Number(option.delay), 10, 1e3);
option.height = clamp(option.height, 10, 1e3);
}
}
function loadSource(tool, file) {
const state = stateFor(tool);
if (!file || state.closed)
return;
const initialEpoch = state.epoch;
const video = tool.video;
const { delay } = thumbnailPolicy(tool.option);
const support = video.canPlayType(file.type);
tool.errorHandle(support === "maybe" || support === "probably", `Playback of this file format is not supported: ${file.type}`);
if (state.closed || state.epoch !== initialEpoch)
@@ -187,11 +214,15 @@ function loadSource(tool, file) {
state.sourceListeners = listeners;
let pendingCleanup = () => {
};
let notificationTimer;
const cancelNotification = () => clearTimeout(notificationTimer);
listeners.push(cancelNotification);
let reported = false;
const error = () => {
if (!current() || reported || video.currentSrc && video.currentSrc !== url)
return;
reported = true;
cancelNotification();
state.loading = false;
const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`);
if (state.job)
@@ -238,8 +269,19 @@ function loadSource(tool, file) {
if (previous !== url)
revoke(state.sourceUrls, previous);
}
if (current())
tool.emit("video", video);
if (current() && !reported) {
if (delay !== null) {
notificationTimer = setTimeout(() => {
cancelNotification();
if (current() && !reported)
tool.emit("video", video);
}, delay);
if (!current() || reported)
cancelNotification();
} else {
tool.emit("video", video);
}
}
} catch (error2) {
if (current()) {
state.loading = false;
@@ -270,18 +312,11 @@ function replaceThumbnail(tool, blob, live) {
}
return live() ? url : null;
}
function getFileName(name) {
const nameArray = name.split(".");
nameArray.pop();
return nameArray.join(".");
}
function clamp(num, a, b) {
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
}
function prepare(tool, job) {
const { video } = job;
const { width, number, begin, end } = tool.option;
const height = video.videoHeight / video.videoWidth * width;
const policy = thumbnailPolicy(tool.option);
const height = policy.aspectHeight ? video.videoHeight / video.videoWidth * width : tool.option.height;
const guard = (condition, message) => {
try {
tool.errorHandle(condition, message);
@@ -305,7 +340,7 @@ function prepare(tool, job) {
const canvas = tool.creatCanvas();
const context = canvas.getContext("2d");
tool.emit("canvas", canvas);
return { width, height, number, points, canvas, context };
return { width, height, number, points, canvas, context, delay: policy.delay };
}
function createJob(tool, state) {
let epoch = state.epoch;
@@ -381,15 +416,19 @@ function createJob(tool, state) {
if (!live())
return;
if (index === plan.points.length) {
complete();
if (plan.delay !== null)
timer = setTimeout(complete, Number(plan.delay) * 2);
else
complete();
return;
}
const point = plan.points[index];
let drawing = false;
let encoded = false;
let delayElapsed = plan.delay === null;
const previous = video.oncanplay;
const draw = () => {
if (!live() || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2)
if (!live() || !delayElapsed || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2)
return;
drawing = true;
try {
@@ -427,8 +466,16 @@ function createJob(tool, state) {
video.oncanplay = draw;
video.addEventListener("seeked", draw);
video.currentTime = point.time;
if (video.readyState >= 2 && !video.seeking)
if (plan.delay !== null) {
timer = setTimeout(() => {
delayElapsed = true;
draw();
}, plan.delay);
if (!live())
clearTimeout(timer);
} else if (video.readyState >= 2 && !video.seeking) {
Promise.resolve().then(draw);
}
} catch (error) {
fail(error);
}
@@ -552,11 +599,7 @@ function setupInput(tool, patch) {
const option = Object.assign({}, tool.option, patch);
const target = option.fileInput;
tool.errorHandle(target instanceof Element, "The 'fileInput' is not a Element");
for (const name of ["number", "width", "column", "begin", "end"])
tool.errorHandle(typeof option[name] === "number", `The '${name}' is not a number`);
option.number = clamp(option.number, 10, 1e3);
option.width = clamp(option.width, 10, 1e3);
option.column = clamp(option.column, 1, 1e3);
normalizePolicy(option, (condition, message) => tool.errorHandle(condition, message));
if (!current())
throw cancellation("input setup superseded");
const previous = inputs.get(tool);
@@ -681,6 +724,7 @@ class ArtplayerToolThumbnail extends Emitter {
}
static get DEFAULTS() {
return {
delay: 300,
number: 60,
width: 160,
height: 90,
@@ -723,9 +767,11 @@ class ArtplayerToolThumbnail extends Emitter {
inputChange(event) {
if (stateFor(this).closed)
return;
const { resetInput } = thumbnailPolicy(this.option);
const file = this.option.fileInput.files[0];
this.loadVideo(file);
event.target.value = "";
if (resetInput)
event.target.value = "";
}
loadVideo(file) {
loadSource(this, file);
@@ -1,13 +1,15 @@
import type ArtplayerToolThumbnail from './index'
import type { Cleanup, ExtractionJob, LifecycleState } from './types'
import { cancellation, cleanupAll, stateFor } from './lifecycle'
import { thumbnailPolicy } from './policy'
import { replaceThumbnail } from './source'
import { clamp } from './utils'
function prepare(tool: ArtplayerToolThumbnail, job: ExtractionJob) {
const { video } = job
const { width, number, begin, end } = tool.option
const height = (video.videoHeight / video.videoWidth) * width
const policy = thumbnailPolicy(tool.option)
const height = policy.aspectHeight ? (video.videoHeight / video.videoWidth) * width : tool.option.height
const guard = (condition: unknown, message: string) => {
try {
tool.errorHandle(condition, message)
@@ -32,7 +34,7 @@ function prepare(tool: ArtplayerToolThumbnail, job: ExtractionJob) {
const canvas = tool.creatCanvas()
const context = canvas.getContext('2d')!
tool.emit('canvas', canvas)
return { width, height, number, points, canvas, context }
return { width, height, number, points, canvas, context, delay: policy.delay }
}
function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
@@ -112,15 +114,19 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
if (!live())
return
if (index === plan.points.length) {
complete()
if (plan.delay !== null)
timer = setTimeout(complete, Number(plan.delay) * 2)
else
complete()
return
}
const point = plan.points[index]!
let drawing = false
let encoded = false
let delayElapsed = plan.delay === null
const previous = video.oncanplay
const draw = () => {
if (!live() || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2))
if (!live() || !delayElapsed || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2))
return
drawing = true
try {
@@ -160,8 +166,17 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
video.oncanplay = draw
video.addEventListener('seeked', draw)
video.currentTime = point.time
if (video.readyState >= 2 && !video.seeking)
if (plan.delay !== null) {
timer = setTimeout(() => {
delayElapsed = true
draw()
}, plan.delay)
if (!live())
clearTimeout(timer)
}
else if (video.readyState >= 2 && !video.seeking) {
Promise.resolve().then(draw)
}
}
catch (error) {
fail(error)
@@ -3,6 +3,7 @@ import Emitter from './emitter'
import { startExtraction } from './extraction'
import { connectInput, releaseInput, setupInput } from './input'
import { cleanupAll, closeState, releaseMedia, stateFor } from './lifecycle'
import { thumbnailPolicy } from './policy'
import { createSheet, downloadSheet, screenshotPoints } from './sheet'
import { loadSource } from './source'
@@ -46,6 +47,7 @@ export default class ArtplayerToolThumbnail extends Emitter<ThumbnailEvents> {
static get DEFAULTS() {
return {
delay: 300,
number: 60,
width: 160,
height: 90,
@@ -94,9 +96,11 @@ export default class ArtplayerToolThumbnail extends Emitter<ThumbnailEvents> {
inputChange(event: Event) {
if (stateFor(this).closed)
return
const { resetInput } = thumbnailPolicy(this.option)
const file = this.option.fileInput.files![0]
this.loadVideo(file)
;(event.target as HTMLInputElement).value = ''
if (resetInput)
(event.target as HTMLInputElement).value = ''
}
loadVideo(file?: File | null) {
@@ -1,7 +1,7 @@
import type ArtplayerToolThumbnail from './index'
import type { InputOptions } from './types'
import { cancellation, stateFor } from './lifecycle'
import { clamp } from './utils'
import { normalizePolicy } from './policy'
type Registration = readonly [name: string, callback: EventListener]
interface InputRecord {
@@ -62,11 +62,7 @@ export function setupInput(tool: ArtplayerToolThumbnail, patch: InputOptions) {
const option = Object.assign({}, tool.option, patch)
const target = option.fileInput!
tool.errorHandle(target instanceof Element, 'The \'fileInput\' is not a Element')
for (const name of ['number', 'width', 'column', 'begin', 'end'])
tool.errorHandle(typeof option[name] === 'number', `The '${name}' is not a number`)
option.number = clamp(option.number, 10, 1000)
option.width = clamp(option.width, 10, 1000)
option.column = clamp(option.column, 1, 1000)
normalizePolicy(option, (condition, message) => tool.errorHandle(condition, message))
if (!current())
throw cancellation('input setup superseded')
@@ -0,0 +1,22 @@
import type { RuntimeOptions } from './types'
import { clamp } from './utils'
export function thumbnailPolicy(option: RuntimeOptions) {
const workspace = option.compatibility === 'workspace-4.4'
return { delay: workspace ? null : option.delay, aspectHeight: workspace, resetInput: workspace }
}
export function normalizePolicy(option: RuntimeOptions, validate: (condition: unknown, message: string) => void) {
validate(option.compatibility === undefined || option.compatibility === 'published-3.5' || option.compatibility === 'workspace-4.4', 'The compatibility option must be published-3.5 or workspace-4.4')
const workspace = thumbnailPolicy(option).aspectHeight
const fields = workspace ? ['number', 'width', 'column', 'begin', 'end'] : ['delay', 'number', 'width', 'height', 'column', 'begin', 'end']
for (const name of fields)
validate(typeof option[name] === 'number', `The '${name}' is not a number`)
option.number = clamp(option.number, 10, 1000)
option.width = clamp(option.width, 10, 1000)
option.column = clamp(option.column, 1, 1000)
if (!workspace) {
option.delay = clamp(Number(option.delay), 10, 1000)
option.height = clamp(option.height, 10, 1000)
}
}
@@ -1,6 +1,7 @@
import type ArtplayerToolThumbnail from './index'
import type { Cleanup } from './types'
import { cleanupAll, revoke, stateFor } from './lifecycle'
import { thumbnailPolicy } from './policy'
export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
const state = stateFor(tool)
@@ -8,6 +9,7 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
return
const initialEpoch = state.epoch
const video = tool.video
const { delay } = thumbnailPolicy(tool.option)
const support = video.canPlayType(file.type)
tool.errorHandle(support === 'maybe' || support === 'probably', `Playback of this file format is not supported: ${file.type}`)
if (state.closed || state.epoch !== initialEpoch)
@@ -28,11 +30,15 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
const listeners: Cleanup[] = []
state.sourceListeners = listeners
let pendingCleanup = () => {}
let notificationTimer: ReturnType<typeof setTimeout> | undefined
const cancelNotification = () => clearTimeout(notificationTimer)
listeners.push(cancelNotification)
let reported = false
const error = () => {
if (!current() || reported || (video.currentSrc && video.currentSrc !== url))
return
reported = true
cancelNotification()
state.loading = false
const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`)
if (state.job)
@@ -79,8 +85,20 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
if (previous !== url)
revoke(state.sourceUrls, previous)
}
if (current())
tool.emit('video', video)
if (current() && !reported) {
if (delay !== null) {
notificationTimer = setTimeout(() => {
cancelNotification()
if (current() && !reported)
tool.emit('video', video)
}, delay)
if (!current() || reported)
cancelNotification()
}
else {
tool.emit('video', video)
}
}
}
catch (error) {
if (current()) {
@@ -9,11 +9,15 @@ export interface SheetOptions {
export interface InputOptions extends Partial<SheetOptions> {
fileInput?: Element
delay?: number
compatibility?: 'published-3.5' | 'workspace-4.4'
[name: string]: unknown
}
export interface RuntimeOptions extends SheetOptions {
fileInput: HTMLInputElement
delay?: number
compatibility?: 'published-3.5' | 'workspace-4.4'
[name: string]: unknown
}
@@ -4,13 +4,13 @@ The declaration describes the existing tool class, not an ArtPlayer plugin facto
The previous workspace manifest pointed to a missing declaration. There is no
recovered legacy declaration to claim as an exact TypeScript baseline.
| Entry | Declaration | Runtime |
| --- | --- | --- |
| Root ESM import | artplayer-tool-thumbnail.d.mts | Default constructor in dist/artplayer-tool-thumbnail.mjs |
| Root require | artplayer-tool-thumbnail.d.cts | Direct constructor in dist/artplayer-tool-thumbnail.js |
| Older Node resolution / TS 4.3 | artplayer-tool-thumbnail.d.ts | main points to the direct CommonJS constructor |
| artplayer-tool-thumbnail/legacy | d.cts or typesVersions fallback | Direct legacy constructor; native ESM gets its default |
| Browser script / editor | Generated docs/assets/ts declaration | ArtplayerToolThumbnail global |
| Entry | Declaration | Runtime |
| ------------------------------- | ------------------------------------ | -------------------------------------------------------- |
| Root ESM import | artplayer-tool-thumbnail.d.mts | Default constructor in dist/artplayer-tool-thumbnail.mjs |
| Root require | artplayer-tool-thumbnail.d.cts | Direct constructor in dist/artplayer-tool-thumbnail.js |
| Older Node resolution / TS 4.3 | artplayer-tool-thumbnail.d.ts | main points to the direct CommonJS constructor |
| artplayer-tool-thumbnail/legacy | d.cts or typesVersions fallback | Direct legacy constructor; native ESM gets its default |
| Browser script / editor | Generated docs/assets/ts declaration | ArtplayerToolThumbnail global |
The d.ts file is the public source of truth. d.cts forwards its export assignment;
d.mts exposes the default constructor and type-only named exports. No runtime
@@ -44,7 +44,7 @@ and source replacement/destroy reject with AbortError. Error events usually cont
strings, but callback throws can supply any message value, so narrow unknown first.
Source/declaration constructors are mutually checked in test/types/thumbnail-source.ts.
Current and TS 4.3.5 consumers include ten invalid uses. Installed checks pack/install
Current and TS 4.3.5 consumers include twelve invalid uses. Installed checks pack/install
outside the workspace, offline with an unchanged frozen lock, verify every file
and exercise native Node require/import without a DOM.
@@ -57,4 +57,9 @@ yarn test:browser test/browser/thumbnail-editor-types.spec.js
build:ts selects package declaration entries rather than auxiliary types and emits
the uppercase global. Actual file extraction, old-core composition and the full
demo remain separate checks. The 3.5.31 versus 4.4.0 default policy remains pending.
demo remain separate checks. The approved default is published 3.5.31;
`compatibility: 'workspace-4.4'` opts into the previous workspace behavior.
`Compatibility` names these two modes. `DefaultOptions` extends `SheetOptions`
with the numeric static delay; existing `SheetOptions` literals need no new field.
`Option.delay` is optional and numeric; workspace mode ignores it, including
explicit undefined. The public resolved option therefore keeps delay optional.
@@ -1,2 +1,2 @@
export { default } from './artplayer-tool-thumbnail.cjs'
export type { EventArgs, EventRegistry, Events, Listener, Option, ResolvedOption, ScreenshotPoint, SheetOptions } from './artplayer-tool-thumbnail.js'
export type { Compatibility, DefaultOptions, EventArgs, EventRegistry, Events, Listener, Option, ResolvedOption, ScreenshotPoint, SheetOptions } from './artplayer-tool-thumbnail.js'
@@ -2,7 +2,7 @@
declare class ArtplayerToolThumbnail {
constructor(option?: ArtplayerToolThumbnail.Option)
static readonly DEFAULTS: ArtplayerToolThumbnail.SheetOptions
static readonly DEFAULTS: ArtplayerToolThumbnail.DefaultOptions
static ondragover(event: DragEvent): void
static creatVideo(): HTMLVideoElement
@@ -44,14 +44,25 @@ declare namespace ArtplayerToolThumbnail {
end: number
}
type Compatibility = 'published-3.5' | 'workspace-4.4'
interface DefaultOptions extends SheetOptions {
delay: number
}
/** fileInput must be a file input or an Element wrapper when constructing. */
interface Option extends Partial<SheetOptions> {
fileInput?: Element
compatibility?: Compatibility
/** Published mode delay in milliseconds, clamped to 10-1000. */
delay?: number
[name: string]: unknown
}
interface ResolvedOption extends SheetOptions {
fileInput: HTMLInputElement
compatibility?: Compatibility
delay?: number
[name: string]: unknown
}