mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 19:01:20 -08:00
refactor(thumbnail): [PKG-TOOL-THUMB-04] preserve approved published defaults
This commit is contained in:
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user