mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-21 21:01:20 -08:00
refactor(thumbnail): [PKG-TOOL-THUMB-04] type runtime modules and retain emitter attribution
This commit is contained in:
1 parent
efd9f42fb9
commit
15485b6420
37 files changed
+8446
-152
No files matched your search
@@ -1,23 +1,25 @@
|
||||
# Thumbnail tool maintenance map
|
||||
|
||||
The runtime responsibilities are separated under PKG-TOOL-THUMB-03. Strict TS and
|
||||
published declaration work belongs to PKG-TOOL-THUMB-04. Runtime restructuring
|
||||
does not imply complete migration or release readiness.
|
||||
The runtime responsibilities were separated under PKG-TOOL-THUMB-03 and are now
|
||||
strict TypeScript under PKG-TOOL-THUMB-04. Published declarations and installed
|
||||
entrypoint checks are still in progress; runtime migration is not release readiness.
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| src/index.js | Public class, historical method names, construction and destruction entry |
|
||||
| src/lifecycle.js | Private state, source/input generations, cancellation and resource release |
|
||||
| src/source.js | File loading, native metadata/error listeners and source/thumbnail Blob URLs |
|
||||
| src/extraction.js | Owned metadata wait and serial frame job, callback/error completion and cancellation |
|
||||
| src/input.js | Option validation/clamps, file-input wrapper creation, listener registration/replacement and release |
|
||||
| src/sheet.js | Midpoint grid, canvas/footer geometry, temporary download anchor |
|
||||
| src/emitter.js | Existing on/once/emit/off semantics; provenance review remains in 04 |
|
||||
| src/utils.js | Existing clamp, filename, sleep and serial-promise helpers |
|
||||
| src/index.ts | Public class, historical method names, construction and destruction entry |
|
||||
| src/lifecycle.ts | Private state, source/input generations, cancellation and resource release |
|
||||
| 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/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 |
|
||||
| src/types.ts | Internal option, frame, event tuple, job and lifecycle contracts; no runtime output |
|
||||
|
||||
The entry delegates to input/source/extraction/sheet; none imports the entry.
|
||||
The entry delegates to input/source/extraction/sheet. Helpers import its type only;
|
||||
the imports are erased and do not create a runtime dependency on the entry.
|
||||
Input/source use lifecycle state, extraction uses lifecycle/source, and sheet uses
|
||||
only filename calculation. There is no dependency back from helpers to the class.
|
||||
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,
|
||||
@@ -76,6 +78,7 @@ Use Yarn and the repository scripts:
|
||||
|
||||
```sh
|
||||
yarn test:thumbnail
|
||||
yarn typecheck
|
||||
yarn build artplayer-tool-thumbnail
|
||||
yarn test:browser test/browser/thumbnail-tool.spec.js test/browser/thumbnail-native.spec.js test/browser/thumbnail-input.spec.js
|
||||
```
|
||||
@@ -96,7 +99,42 @@ 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 strict TS and installed declarations in 04; preserve old tests
|
||||
and the default delay/height boundary. The old sleep/serial helpers in utils.js are
|
||||
no longer used by extraction and can be removed during that migration.
|
||||
complete. Continue published declarations and installed consumers in 04; preserve
|
||||
old tests and the default delay/height boundary.
|
||||
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
|
||||
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
|
||||
fields retain their absence until the corresponding operation publishes them.
|
||||
Unknown option fields remain unknown, and custom string/number/symbol events keep
|
||||
an open payload boundary; built-in events have precise tuples. This source typing
|
||||
is not yet the missing public npm declaration entrypoint.
|
||||
|
||||
Assertions are limited to existing runtime boundaries: a temporary empty option
|
||||
object before setup validation; the input/wrapper conversion; event target and
|
||||
dataTransfer/files supplied by native input/drop events; private records inserted
|
||||
before lookup; nonempty/dense frame arrays; a successful 2D canvas context; and
|
||||
the old property read of message on arbitrary thrown values. These assertions
|
||||
preserve existing runtime validation/errors rather than adding new coercions.
|
||||
The constructor and destroy rollback paths also intentionally tolerate fields
|
||||
that have not yet been assigned. DOM listener casts connect known event names to
|
||||
the matching bound callbacks. Keep new assertions equally local and documented.
|
||||
|
||||
The emitter structurally corresponds to tiny-emitter 2.1.0, which the recovered
|
||||
3.5.31 manifest declared as ^2.1.0. The exact original copied revision is unknown.
|
||||
The pinned [reference files](../../refactor/baselines/thumbnail-vendor/sources.json)
|
||||
and [MIT notice](THIRD_PARTY_NOTICES) retain upstream attribution. The repository
|
||||
build includes the complete notice in main, legacy and ESM bundle headers.
|
||||
Final packed contents remain a distribution gate. Do not replace this local
|
||||
emitter with the core emitter as part of a type-only change: dispatch semantics
|
||||
and historical prototype-key handling need their own behavior review.
|
||||
|
||||
`test/thumbnail-runtime.test.js` compares upstream, recovered/historical bundles
|
||||
and the selected candidate for dispatch behavior, and checks candidate public
|
||||
descriptors and validation. `test/thumbnail-vendor.test.js` verifies fixed source
|
||||
bytes, complete notices and dist/docs equality. `test/types/thumbnail-runtime.ts`
|
||||
checks source consumers with positive cases and six rejected invalid uses.
|
||||
@@ -0,0 +1,26 @@
|
||||
ArtPlayer thumbnail tool bundled third-party notices
|
||||
|
||||
tiny-emitter - reference 2.1.0, locally adapted to a TypeScript class
|
||||
https://github.com/scottcorgan/tiny-emitter/tree/2.1.0
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Scott Corgan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -3,5 +3,32 @@
|
||||
* Github: https://github.com/zhw2590582/ArtPlayer
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*
|
||||
* ArtPlayer thumbnail tool bundled third-party notices
|
||||
*
|
||||
* tiny-emitter - reference 2.1.0, locally adapted to a TypeScript class
|
||||
* https://github.com/scottcorgan/tiny-emitter/tree/2.1.0
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2017 Scott Corgan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* 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";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this;function o(...i){r.off(e,o),t.apply(n,i)}return o._=t,this.on(e,o,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;r<n.length;r+=1)n[r].fn.apply(n[r].ctx,t);return this}off(e,t){const n=this.e||(this.e={}),r=n[e],o=[];if(r&&t)for(let i=0,s=r.length;i<s;i+=1)r[i].fn!==t&&r[i].fn._!==t&&o.push(r[i]);return o.length?n[e]=o:delete n[e],this}}const t=new WeakMap;function n(e){return t.has(e)||t.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}),t.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function o(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function i(e){let t;for(const r of e)try{r()}catch(n){t||(t=n)}if(t)throw t}function s(e,t="destroyed"){const r=n(e);return!r.closed&&(r.closed=!0,r.epoch++,r.job?.cancel(t),!0)}function c(e){const t=n(e),r=new Set([...t.sourceUrls,e.videoUrl]),s=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));i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function a(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function l(e,t){let s=t.epoch;const c=e.video;let l,u,d,h,p=!1,f=!1,m=()=>{};const b=[],y=new Promise((e,t)=>{l=e,u=t});y.catch(()=>{});const g=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function v(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>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 r=0;const i=()=>{if(!g())return;if(r===t.points.length)return void function(){if(g())try{v(),e.emit("done"),l()}catch(t){let r=t;try{e.emit("error",t?.message)}catch(n){r=n}u(r)}}();const s=t.points[r];let a=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!g()||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),!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,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,g);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}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(i)}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,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),g()||clearTimeout(d)}h={video:c,promise:y,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=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.removeEventListener(e,t)),c.addEventListener(e,t),!g())return i([()=>c.removeEventListener(e,t),...b.splice(0)]),y;c.error?w(new Error(`Unable to load video: media error ${c.error.code}`)):E(!0)}catch(j){throw w(j,!1),j}return
Line 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";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this,o=t;function i(...t){r.off(e,i),o.apply(n,t)}return i._=t,this.on(e,i,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;r<n.length;r+=1)n[r].fn.apply(n[r].ctx,t);return this}off(e,t){const n=this.e||(this.e={}),r=n[e],o=[];if(r&&t)for(let i=0,s=r.length;i<s;i+=1)r[i].fn!==t&&r[i].fn._!==t&&o.push(r[i]);return o.length?n[e]=o:delete n[e],this}}const t=new WeakMap;function n(e){return t.has(e)||t.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}),t.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function o(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function i(e){let t;for(const r of e)try{r()}catch(n){t||(t=n)}if(t)throw t}function s(e,t="destroyed"){const r=n(e);return!r.closed&&(r.closed=!0,r.epoch++,r.job?.cancel(t),!0)}function c(e){const t=n(e),r=new Set([...t.sourceUrls,e.videoUrl]),s=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));i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function a(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function l(e,t){let s=t.epoch;const c=e.video;let l,u,d,h,p=!1,f=!1,m=()=>{};const b=[],y=new Promise((e,t)=>{l=e,u=t});y.catch(()=>{});const g=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function v(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>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 r=0;const i=()=>{if(!g())return;if(r===t.points.length)return void function(){if(g())try{v(),e.emit("done"),l()}catch(t){let r=t;try{e.emit("error",t?.message)}catch(n){r=n}u(r)}}();const s=t.points[r];let a=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!g()||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),!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,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,g);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}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(i)}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,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),g()||clearTimeout(d)}h={video:c,promise:y,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=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.removeEventListener(e,t)),c.addEventListener(e,t),!g())return i([()=>c.removeEventListener(e,t),...b.splice(0)]),y;c.error?w(new Error(`Unable to load video: media error ${c.error.code}`)):E(!0)}catch(j){throw w(j,!1),j}retLine truncated
|
||||
@@ -3,5 +3,32 @@
|
||||
* Github: https://github.com/zhw2590582/ArtPlayer
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*
|
||||
* ArtPlayer thumbnail tool bundled third-party notices
|
||||
*
|
||||
* tiny-emitter - reference 2.1.0, locally adapted to a TypeScript class
|
||||
* https://github.com/scottcorgan/tiny-emitter/tree/2.1.0
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2017 Scott Corgan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* 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";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this;function o(...i){r.off(e,o),t.apply(n,i)}return o._=t,this.on(e,o,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;r<n.length;r+=1)n[r].fn.apply(n[r].ctx,t);return this}off(e,t){const n=this.e||(this.e={}),r=n[e],o=[];if(r&&t)for(let i=0,s=r.length;i<s;i+=1)r[i].fn!==t&&r[i].fn._!==t&&o.push(r[i]);return o.length?n[e]=o:delete n[e],this}}const t=new WeakMap;function n(e){return t.has(e)||t.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}),t.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function o(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function i(e){let t;for(const r of e)try{r()}catch(n){t||(t=n)}if(t)throw t}function s(e,t="destroyed"){var r;const o=n(e);return!o.closed&&(o.closed=!0,o.epoch++,null==(r=o.job)||r.cancel(t),!0)}function c(e){const t=n(e),r=new Set([...t.sourceUrls,e.videoUrl]),s=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const n of new Set([t.video,e.video]))n&&c.push(()=>{var e;return null==(e=n.pause)?void 0:e.call(n)},()=>{var e;return null==(e=n.removeAttribute)?void 0:e.call(n,"src")},()=>{var e;return null==(e=n.load)?void 0:e.call(n)},()=>{var e;return null==(e=n.parentNode)?void 0:e.removeChild(n)});i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function l(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function a(e,t){let s=t.epoch;const c=e.video;let a,u,d,h,p=!1,f=!1,m=()=>{};const b=[],v=new Promise((e,t)=>{a=e,u=t});v.catch(()=>{});const y=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function g(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>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 r=0;const i=()=>{if(!y())return;if(r===t.points.length)return void function(){if(y())try{g(),e.emit("done"),a()}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[r];let l=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!y()||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),!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,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,y);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}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(i)}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,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),y()||clearTimeout(d)}h={video:c,promise:v,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{var e;y()&&w(new Error(`Unable to load video: media error ${(null==(e=c.error)?void 0:e.code)||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeELine 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";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this,o=t;function i(...t){r.off(e,i),o.apply(n,t)}return i._=t,this.on(e,i,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;r<n.length;r+=1)n[r].fn.apply(n[r].ctx,t);return this}off(e,t){const n=this.e||(this.e={}),r=n[e],o=[];if(r&&t)for(let i=0,s=r.length;i<s;i+=1)r[i].fn!==t&&r[i].fn._!==t&&o.push(r[i]);return o.length?n[e]=o:delete n[e],this}}const t=new WeakMap;function n(e){return t.has(e)||t.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}),t.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function o(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function i(e){let t;for(const r of e)try{r()}catch(n){t||(t=n)}if(t)throw t}function s(e,t="destroyed"){var r;const o=n(e);return!o.closed&&(o.closed=!0,o.epoch++,null==(r=o.job)||r.cancel(t),!0)}function c(e){const t=n(e),r=new Set([...t.sourceUrls,e.videoUrl]),s=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const n of new Set([t.video,e.video]))n&&c.push(()=>{var e;return null==(e=n.pause)?void 0:e.call(n)},()=>{var e;return null==(e=n.removeAttribute)?void 0:e.call(n,"src")},()=>{var e;return null==(e=n.load)?void 0:e.call(n)},()=>{var e;return null==(e=n.parentNode)?void 0:e.removeChild(n)});i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function l(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function a(e,t){let s=t.epoch;const c=e.video;let a,u,d,h,p=!1,f=!1,m=()=>{};const b=[],v=new Promise((e,t)=>{a=e,u=t});v.catch(()=>{});const y=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function g(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>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 r=0;const i=()=>{if(!y())return;if(r===t.points.length)return void function(){if(y())try{g(),e.emit("done"),a()}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[r];let l=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!y()||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),!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,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,y);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}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(i)}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,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),y()||clearTimeout(d)}h={video:c,promise:v,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{var e;y()&&w(new Error(`Unable to load video: media error ${(null==(e=c.error)?void 0:e.code)||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.remLine truncated
|
||||
@@ -3,6 +3,33 @@
|
||||
* Github: https://github.com/zhw2590582/ArtPlayer
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*
|
||||
* ArtPlayer thumbnail tool bundled third-party notices
|
||||
*
|
||||
* tiny-emitter - reference 2.1.0, locally adapted to a TypeScript class
|
||||
* https://github.com/scottcorgan/tiny-emitter/tree/2.1.0
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2017 Scott Corgan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
class Emitter {
|
||||
on(name, fn, ctx) {
|
||||
@@ -12,15 +39,17 @@ class Emitter {
|
||||
}
|
||||
once(name, fn, ctx) {
|
||||
const self = this;
|
||||
const callback = fn;
|
||||
function listener(...args) {
|
||||
self.off(name, listener);
|
||||
fn.apply(ctx, args);
|
||||
callback.apply(ctx, args);
|
||||
}
|
||||
listener._ = fn;
|
||||
return this.on(name, listener, ctx);
|
||||
}
|
||||
emit(name, ...data) {
|
||||
const evtArr = ((this.e || (this.e = {}))[name] || []).slice();
|
||||
const e = this.e || (this.e = {});
|
||||
const evtArr = (e[name] || []).slice();
|
||||
for (let i = 0; i < evtArr.length; i += 1) {
|
||||
evtArr[i].fn.apply(evtArr[i].ctx, data);
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
export default class Emitter {
|
||||
on(name, fn, ctx) {
|
||||
const e = this.e || (this.e = {});
|
||||
(e[name] || (e[name] = [])).push({ fn, ctx })
|
||||
return this
|
||||
}
|
||||
|
||||
once(name, fn, ctx) {
|
||||
const self = this
|
||||
function listener(...args) {
|
||||
self.off(name, listener)
|
||||
fn.apply(ctx, args)
|
||||
}
|
||||
listener._ = fn
|
||||
return this.on(name, listener, ctx)
|
||||
}
|
||||
|
||||
emit(name, ...data) {
|
||||
const evtArr = ((this.e || (this.e = {}))[name] || []).slice()
|
||||
for (let i = 0; i < evtArr.length; i += 1) {
|
||||
evtArr[i].fn.apply(evtArr[i].ctx, data)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
off(name, callback) {
|
||||
const e = this.e || (this.e = {})
|
||||
const evts = e[name]
|
||||
const liveEvents = []
|
||||
if (evts && callback) {
|
||||
for (let i = 0, len = evts.length; i < len; i += 1) {
|
||||
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
|
||||
liveEvents.push(evts[i])
|
||||
}
|
||||
}
|
||||
if (liveEvents.length) {
|
||||
e[name] = liveEvents
|
||||
}
|
||||
else {
|
||||
delete e[name]
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Locally adapted from tiny-emitter; upstream reference 2.1.0. See THIRD_PARTY_NOTICES.
|
||||
type EventMap<Events> = { [Name in keyof Events]: readonly unknown[] }
|
||||
type Listener<Args extends unknown[]> = ((...args: Args) => unknown) & { _?: (...args: Args) => unknown }
|
||||
interface Registration<Args extends unknown[]> {
|
||||
fn: Listener<Args>
|
||||
ctx: unknown
|
||||
}
|
||||
type Registry<Events extends EventMap<Events>> = { [Name in keyof Events]?: Registration<[...Events[Name]]>[] }
|
||||
|
||||
export default class Emitter<Events extends EventMap<Events> = Record<PropertyKey, unknown[]>> {
|
||||
declare e?: Registry<Events>
|
||||
|
||||
on<Name extends keyof Events, Context>(name: Name, fn: (this: Context, ...args: [...Events[Name]]) => unknown, ctx?: Context): this {
|
||||
const e: Registry<Events> = this.e || (this.e = {});
|
||||
(e[name] || (e[name] = [])).push({ fn, ctx })
|
||||
return this
|
||||
}
|
||||
|
||||
once<Name extends keyof Events, Context>(name: Name, fn: (this: Context, ...args: [...Events[Name]]) => unknown, ctx?: Context): this {
|
||||
// eslint-disable-next-line ts/no-this-alias -- Preserve the owning emitter independently of callback ctx.
|
||||
const self = this
|
||||
const callback: Listener<[...Events[Name]]> = fn
|
||||
function listener(...args: [...Events[Name]]) {
|
||||
self.off(name, listener)
|
||||
callback.apply(ctx, args)
|
||||
}
|
||||
listener._ = fn
|
||||
return this.on(name, listener, ctx)
|
||||
}
|
||||
|
||||
emit<Name extends keyof Events>(name: Name, ...data: [...Events[Name]]): this {
|
||||
const e: Registry<Events> = this.e || (this.e = {})
|
||||
const evtArr = (e[name] || []).slice()
|
||||
for (let i = 0; i < evtArr.length; i += 1) {
|
||||
evtArr[i]!.fn.apply(evtArr[i]!.ctx, data)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
off<Name extends keyof Events>(name: Name, callback?: Listener<[...Events[Name]]>): this {
|
||||
const e: Registry<Events> = this.e || (this.e = {})
|
||||
const evts = e[name]
|
||||
const liveEvents: Registration<[...Events[Name]]>[] = []
|
||||
if (evts && callback) {
|
||||
for (let i = 0, len = evts.length; i < len; i += 1) {
|
||||
if (evts[i]!.fn !== callback && evts[i]!.fn._ !== callback)
|
||||
liveEvents.push(evts[i]!)
|
||||
}
|
||||
}
|
||||
if (liveEvents.length) {
|
||||
e[name] = liveEvents
|
||||
}
|
||||
else {
|
||||
delete e[name]
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
+19
-17
@@ -1,12 +1,14 @@
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { Cleanup, ExtractionJob, LifecycleState } from './types'
|
||||
import { cancellation, cleanupAll, stateFor } from './lifecycle'
|
||||
import { replaceThumbnail } from './source'
|
||||
import { clamp } from './utils'
|
||||
|
||||
function prepare(tool, job) {
|
||||
function prepare(tool: ArtplayerToolThumbnail, job: ExtractionJob) {
|
||||
const { video } = job
|
||||
const { width, number, begin, end } = tool.option
|
||||
const height = (video.videoHeight / video.videoWidth) * width
|
||||
const guard = (condition, message) => {
|
||||
const guard = (condition: unknown, message: string) => {
|
||||
try {
|
||||
tool.errorHandle(condition, message)
|
||||
}
|
||||
@@ -28,23 +30,23 @@ function prepare(tool, job) {
|
||||
guard(tool.density <= 1, `The preview density cannot be greater than 1, but got ${tool.density}`)
|
||||
const points = tool.creatScreenshotDate()
|
||||
const canvas = tool.creatCanvas()
|
||||
const context = canvas.getContext('2d')
|
||||
const context = canvas.getContext('2d')!
|
||||
tool.emit('canvas', canvas)
|
||||
return { width, height, number, points, canvas, context }
|
||||
}
|
||||
|
||||
function createJob(tool, state) {
|
||||
function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
|
||||
let epoch = state.epoch
|
||||
const video = tool.video
|
||||
let resolve
|
||||
let reject
|
||||
let timer
|
||||
let resolve!: () => void
|
||||
let reject!: (error: unknown) => void
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let settled = false
|
||||
let running = false
|
||||
let job
|
||||
let job: ExtractionJob
|
||||
let frameCleanup = () => {}
|
||||
const listeners = []
|
||||
const promise = new Promise((yes, no) => {
|
||||
const listeners: Cleanup[] = []
|
||||
const promise = new Promise<void>((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
@@ -62,7 +64,7 @@ function createJob(tool, state) {
|
||||
cleanupAll([() => clearTimeout(timer), frameCleanup, ...listeners.splice(0)])
|
||||
return true
|
||||
}
|
||||
function fail(error, report = true) {
|
||||
function fail(error: unknown, report = true) {
|
||||
if (settled)
|
||||
return
|
||||
try {
|
||||
@@ -73,7 +75,7 @@ function createJob(tool, state) {
|
||||
}
|
||||
if (report) {
|
||||
try {
|
||||
tool.emit('error', error?.message)
|
||||
tool.emit('error', (error as { message?: string } | null | undefined)?.message)
|
||||
}
|
||||
catch (listenerError) {
|
||||
error = listenerError
|
||||
@@ -92,7 +94,7 @@ function createJob(tool, state) {
|
||||
catch (error) {
|
||||
let failure = error
|
||||
try {
|
||||
tool.emit('error', error?.message)
|
||||
tool.emit('error', (error as { message?: string } | null | undefined)?.message)
|
||||
}
|
||||
catch (listenerError) {
|
||||
failure = listenerError
|
||||
@@ -100,7 +102,7 @@ function createJob(tool, state) {
|
||||
reject(failure)
|
||||
}
|
||||
}
|
||||
function extract(plan) {
|
||||
function extract(plan: ReturnType<typeof prepare>) {
|
||||
if (!live())
|
||||
return
|
||||
running = true
|
||||
@@ -113,7 +115,7 @@ function createJob(tool, state) {
|
||||
complete()
|
||||
return
|
||||
}
|
||||
const point = plan.points[index]
|
||||
const point = plan.points[index]!
|
||||
let drawing = false
|
||||
let encoded = false
|
||||
const previous = video.oncanplay
|
||||
@@ -211,7 +213,7 @@ function createJob(tool, state) {
|
||||
}],
|
||||
['loadedmetadata', () => ready()],
|
||||
['durationchange', () => ready()],
|
||||
]) {
|
||||
] as const) {
|
||||
listeners.push(() => video.removeEventListener(name, callback))
|
||||
video.addEventListener(name, callback)
|
||||
if (!live()) {
|
||||
@@ -231,7 +233,7 @@ function createJob(tool, state) {
|
||||
return promise
|
||||
}
|
||||
|
||||
export function startExtraction(tool) {
|
||||
export function startExtraction(tool: ArtplayerToolThumbnail): Promise<void> {
|
||||
const state = stateFor(tool)
|
||||
if (state.closed) {
|
||||
const promise = Promise.reject(cancellation('destroyed'))
|
||||
+24
-13
@@ -1,3 +1,4 @@
|
||||
import type { InputOptions, RuntimeOptions, ThumbnailEvents } from './types'
|
||||
import Emitter from './emitter'
|
||||
import { startExtraction } from './extraction'
|
||||
import { connectInput, releaseInput, setupInput } from './input'
|
||||
@@ -5,11 +6,21 @@ import { cleanupAll, closeState, releaseMedia, stateFor } from './lifecycle'
|
||||
import { createSheet, downloadSheet, screenshotPoints } from './sheet'
|
||||
import { loadSource } from './source'
|
||||
|
||||
export default class ArtplayerToolThumbnail extends Emitter {
|
||||
constructor(option = {}) {
|
||||
export default class ArtplayerToolThumbnail extends Emitter<ThumbnailEvents> {
|
||||
declare processing: boolean
|
||||
declare option: RuntimeOptions
|
||||
declare video: HTMLVideoElement
|
||||
declare duration: number
|
||||
declare density: number | undefined
|
||||
declare file: File | undefined
|
||||
declare videoUrl: string | undefined
|
||||
declare thumbnailUrl: string | undefined
|
||||
|
||||
constructor(option: InputOptions = {}) {
|
||||
super()
|
||||
this.processing = false
|
||||
this.option = {}
|
||||
// setup validates the temporary empty options before construction can succeed.
|
||||
this.option = {} as RuntimeOptions
|
||||
try {
|
||||
this.setup(Object.assign({}, ArtplayerToolThumbnail.DEFAULTS, option))
|
||||
this.video = ArtplayerToolThumbnail.creatVideo()
|
||||
@@ -44,19 +55,19 @@ export default class ArtplayerToolThumbnail extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
static ondragover(event) {
|
||||
static ondragover(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
ondrop(event) {
|
||||
ondrop(event: DragEvent) {
|
||||
if (stateFor(this).closed)
|
||||
return
|
||||
event.preventDefault()
|
||||
const file = event.dataTransfer.files[0]
|
||||
const file = event.dataTransfer!.files[0]
|
||||
this.loadVideo(file)
|
||||
}
|
||||
|
||||
setup(option = {}) {
|
||||
setup(option: InputOptions = {}) {
|
||||
if (!stateFor(this).closed)
|
||||
setupInput(this, option)
|
||||
return this
|
||||
@@ -80,15 +91,15 @@ export default class ArtplayerToolThumbnail extends Emitter {
|
||||
return video
|
||||
}
|
||||
|
||||
inputChange(event) {
|
||||
inputChange(event: Event) {
|
||||
if (stateFor(this).closed)
|
||||
return
|
||||
const file = this.option.fileInput.files[0]
|
||||
const file = this.option.fileInput.files![0]
|
||||
this.loadVideo(file)
|
||||
event.target.value = ''
|
||||
;(event.target as HTMLInputElement).value = ''
|
||||
}
|
||||
|
||||
loadVideo(file) {
|
||||
loadVideo(file?: File | null) {
|
||||
loadSource(this, file)
|
||||
}
|
||||
|
||||
@@ -112,12 +123,12 @@ export default class ArtplayerToolThumbnail extends Emitter {
|
||||
'Download does not seem to be ready, please create preview first',
|
||||
)
|
||||
this.errorHandle(!this.processing, 'There is currently a task in progress, please wait a moment...')
|
||||
const name = downloadSheet(this.file, this.thumbnailUrl)
|
||||
const name = downloadSheet(this.file!, this.thumbnailUrl!)
|
||||
this.emit('download', name)
|
||||
return this
|
||||
}
|
||||
|
||||
errorHandle(condition, msg) {
|
||||
errorHandle(condition: unknown, msg: string) {
|
||||
if (!condition) {
|
||||
this.emit('error', msg)
|
||||
throw new Error(msg)
|
||||
+25
-12
@@ -1,10 +1,21 @@
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { InputOptions } from './types'
|
||||
import { cancellation, stateFor } from './lifecycle'
|
||||
import { clamp } from './utils'
|
||||
|
||||
const inputs = new WeakMap()
|
||||
type Registration = readonly [name: string, callback: EventListener]
|
||||
interface InputRecord {
|
||||
input: HTMLInputElement
|
||||
wrapper: HTMLElement | null
|
||||
position: string
|
||||
listeners: Registration[]
|
||||
callbacks: Registration[] | null
|
||||
}
|
||||
|
||||
function release(record) {
|
||||
let failure
|
||||
const inputs = new WeakMap<ArtplayerToolThumbnail, InputRecord>()
|
||||
|
||||
function release(record: InputRecord) {
|
||||
let failure: unknown
|
||||
for (const [name, callback] of record.listeners.splice(0)) {
|
||||
try {
|
||||
record.input.removeEventListener(name, callback)
|
||||
@@ -28,7 +39,7 @@ function release(record) {
|
||||
throw failure
|
||||
}
|
||||
|
||||
function connect(record, callbacks) {
|
||||
function connect(record: InputRecord, callbacks: Registration[]) {
|
||||
try {
|
||||
for (const [name, callback] of callbacks) {
|
||||
record.listeners.push([name, callback])
|
||||
@@ -44,12 +55,12 @@ function connect(record, callbacks) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setupInput(tool, patch) {
|
||||
export function setupInput(tool: ArtplayerToolThumbnail, patch: InputOptions) {
|
||||
const state = stateFor(tool)
|
||||
const epoch = ++state.inputEpoch
|
||||
const current = () => !state.closed && state.inputEpoch === epoch
|
||||
const option = Object.assign({}, tool.option, patch)
|
||||
const target = option.fileInput
|
||||
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`)
|
||||
@@ -65,9 +76,10 @@ export function setupInput(tool, patch) {
|
||||
tool.option = option
|
||||
return option
|
||||
}
|
||||
const record = { input: target, wrapper: null, position: '', listeners: [], callbacks: previous?.callbacks || null }
|
||||
// A wrapper is replaced by its owned file input before the record is committed.
|
||||
const record: InputRecord = { input: target as HTMLInputElement, wrapper: null, position: '', listeners: [], callbacks: previous?.callbacks || null }
|
||||
try {
|
||||
if (!(target.tagName === 'INPUT' && target.type === 'file')) {
|
||||
if (!(target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'file')) {
|
||||
record.wrapper = target
|
||||
record.position = target.style.position
|
||||
record.input = document.createElement('input')
|
||||
@@ -98,13 +110,14 @@ export function setupInput(tool, patch) {
|
||||
return option
|
||||
}
|
||||
|
||||
export function connectInput(tool, dragover) {
|
||||
const record = inputs.get(tool)
|
||||
record.callbacks = [['change', tool.inputChange], ['dragover', dragover], ['drop', tool.ondrop]]
|
||||
export function connectInput(tool: ArtplayerToolThumbnail, dragover: (event: DragEvent) => void) {
|
||||
const record = inputs.get(tool)!
|
||||
// These callbacks are registered only for their matching DOM event names.
|
||||
record.callbacks = [['change', tool.inputChange], ['dragover', dragover as EventListener], ['drop', tool.ondrop as EventListener]]
|
||||
connect(record, record.callbacks)
|
||||
}
|
||||
|
||||
export function releaseInput(tool) {
|
||||
export function releaseInput(tool: ArtplayerToolThumbnail) {
|
||||
const record = inputs.get(tool)
|
||||
if (!record)
|
||||
return
|
||||
+13
-10
@@ -1,6 +1,9 @@
|
||||
const states = new WeakMap()
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { Cleanup, LifecycleState } from './types'
|
||||
|
||||
export function stateFor(tool) {
|
||||
const states = new WeakMap<ArtplayerToolThumbnail, LifecycleState>()
|
||||
|
||||
export function stateFor(tool: ArtplayerToolThumbnail): LifecycleState {
|
||||
if (!states.has(tool)) {
|
||||
states.set(tool, {
|
||||
closed: false,
|
||||
@@ -16,24 +19,24 @@ export function stateFor(tool) {
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
return states.get(tool)
|
||||
return states.get(tool)!
|
||||
}
|
||||
|
||||
export function cancellation(reason) {
|
||||
export function cancellation(reason: string) {
|
||||
const error = new Error(`Thumbnail task cancelled: ${reason}`)
|
||||
error.name = 'AbortError'
|
||||
return error
|
||||
}
|
||||
|
||||
export function revoke(urls, url) {
|
||||
export function revoke(urls: Set<string>, url: string | undefined) {
|
||||
if (!url)
|
||||
return
|
||||
urls.delete(url)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export function cleanupAll(callbacks) {
|
||||
let failure
|
||||
export function cleanupAll(callbacks: Cleanup[]) {
|
||||
let failure: unknown
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
callback()
|
||||
@@ -46,7 +49,7 @@ export function cleanupAll(callbacks) {
|
||||
throw failure
|
||||
}
|
||||
|
||||
export function closeState(tool, reason = 'destroyed') {
|
||||
export function closeState(tool: ArtplayerToolThumbnail, reason = 'destroyed') {
|
||||
const state = stateFor(tool)
|
||||
if (state.closed)
|
||||
return false
|
||||
@@ -56,11 +59,11 @@ export function closeState(tool, reason = 'destroyed') {
|
||||
return true
|
||||
}
|
||||
|
||||
export function releaseMedia(tool) {
|
||||
export function releaseMedia(tool: ArtplayerToolThumbnail) {
|
||||
const state = stateFor(tool)
|
||||
const sources = new Set([...state.sourceUrls, tool.videoUrl])
|
||||
const thumbnails = new Set([...state.thumbnailUrls, tool.thumbnailUrl])
|
||||
const media = []
|
||||
const media: Cleanup[] = []
|
||||
for (const video of new Set([state.video, tool.video])) {
|
||||
if (video) {
|
||||
media.push(
|
||||
+6
-5
@@ -1,11 +1,12 @@
|
||||
import type { ScreenshotPoint, SheetOptions } from './types'
|
||||
import { getFileName } from './utils'
|
||||
|
||||
export function screenshotPoints(option, duration) {
|
||||
export function screenshotPoints(option: SheetOptions, duration: number): ScreenshotPoint[] {
|
||||
const { number, width, height, column, begin } = option
|
||||
const timeGap = duration / number
|
||||
const timePoints = [begin + timeGap]
|
||||
while (timePoints.length < number) {
|
||||
const last = timePoints[timePoints.length - 1]
|
||||
const last = timePoints[timePoints.length - 1]!
|
||||
timePoints.push(last + timeGap)
|
||||
}
|
||||
return timePoints.map((item, index) => ({
|
||||
@@ -15,10 +16,10 @@ export function screenshotPoints(option, duration) {
|
||||
}))
|
||||
}
|
||||
|
||||
export function createSheet(option) {
|
||||
export function createSheet(option: SheetOptions) {
|
||||
const { number, width, height, column } = option
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
const context = canvas.getContext('2d')!
|
||||
canvas.width = width * column
|
||||
canvas.height = Math.ceil(number / column) * height + 30
|
||||
context.fillStyle = 'black'
|
||||
@@ -33,7 +34,7 @@ export function createSheet(option) {
|
||||
return canvas
|
||||
}
|
||||
|
||||
export function downloadSheet(file, url) {
|
||||
export function downloadSheet(file: File, url: string) {
|
||||
const link = document.createElement('a')
|
||||
const name = `${getFileName(file.name)}.png`
|
||||
link.download = name
|
||||
+6
-4
@@ -1,6 +1,8 @@
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { Cleanup } from './types'
|
||||
import { cleanupAll, revoke, stateFor } from './lifecycle'
|
||||
|
||||
export function loadSource(tool, file) {
|
||||
export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
|
||||
const state = stateFor(tool)
|
||||
if (!file || state.closed)
|
||||
return
|
||||
@@ -23,7 +25,7 @@ export function loadSource(tool, file) {
|
||||
state.loading = true
|
||||
const current = () => !state.closed && state.epoch === epoch
|
||||
const previousListeners = state.sourceListeners
|
||||
const listeners = []
|
||||
const listeners: Cleanup[] = []
|
||||
state.sourceListeners = listeners
|
||||
let pendingCleanup = () => {}
|
||||
let reported = false
|
||||
@@ -50,7 +52,7 @@ export function loadSource(tool, file) {
|
||||
revoke(state.sourceUrls, url)
|
||||
return
|
||||
}
|
||||
for (const [name, callback] of [['error', error], ['loadedmetadata', ready]]) {
|
||||
for (const [name, callback] of [['error', error], ['loadedmetadata', ready]] as const) {
|
||||
pendingCleanup = () => video.removeEventListener(name, callback)
|
||||
listeners.push(pendingCleanup)
|
||||
video.addEventListener(name, callback)
|
||||
@@ -94,7 +96,7 @@ export function loadSource(tool, file) {
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceThumbnail(tool, blob, live) {
|
||||
export function replaceThumbnail(tool: ArtplayerToolThumbnail, blob: Blob, live: () => boolean) {
|
||||
const state = stateFor(tool)
|
||||
const url = URL.createObjectURL(blob)
|
||||
state.thumbnailUrls.add(url)
|
||||
@@ -0,0 +1,62 @@
|
||||
export interface SheetOptions {
|
||||
number: number
|
||||
width: number
|
||||
height: number
|
||||
column: number
|
||||
begin: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface InputOptions extends Partial<SheetOptions> {
|
||||
fileInput?: Element
|
||||
[name: string]: unknown
|
||||
}
|
||||
|
||||
export interface RuntimeOptions extends SheetOptions {
|
||||
fileInput: HTMLInputElement
|
||||
[name: string]: unknown
|
||||
}
|
||||
|
||||
export interface ScreenshotPoint {
|
||||
time: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface ThumbnailEvents extends Record<PropertyKey, unknown[]> {
|
||||
file: [file: File]
|
||||
video: [video: HTMLVideoElement]
|
||||
canvas: [canvas: HTMLCanvasElement]
|
||||
update: [url: string, progress: number]
|
||||
done: []
|
||||
download: [name: string]
|
||||
error: [message: string | undefined]
|
||||
destroy: []
|
||||
}
|
||||
|
||||
export type Cleanup = () => void
|
||||
|
||||
export interface ExtractionJob {
|
||||
video: HTMLVideoElement
|
||||
promise: Promise<void>
|
||||
ready: (synchronous?: boolean) => void
|
||||
fail: (error: unknown, report?: boolean) => void
|
||||
cancel: (reason: string) => void
|
||||
adopt: (epoch: number) => boolean
|
||||
reported?: unknown
|
||||
wasReported?: boolean
|
||||
}
|
||||
|
||||
export interface LifecycleState {
|
||||
closed: boolean
|
||||
epoch: number
|
||||
inputEpoch: number
|
||||
job: ExtractionJob | null
|
||||
video: HTMLVideoElement | null
|
||||
sourceUrl: string | null
|
||||
thumbnailUrl: string | null
|
||||
sourceUrls: Set<string>
|
||||
thumbnailUrls: Set<string>
|
||||
sourceListeners: Cleanup[]
|
||||
loading: boolean
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export function runPromisesInSeries(ps) {
|
||||
return ps.reduce((p, next) => p.then(next), Promise.resolve())
|
||||
}
|
||||
|
||||
export function getFileName(name) {
|
||||
const nameArray = name.split('.')
|
||||
nameArray.pop()
|
||||
return nameArray.join('.')
|
||||
}
|
||||
|
||||
export function clamp(num, a, b) {
|
||||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function getFileName(name: string) {
|
||||
const nameArray = name.split('.')
|
||||
nameArray.pop()
|
||||
return nameArray.join('.')
|
||||
}
|
||||
|
||||
export function clamp(num: number, a: number, b: number) {
|
||||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": { "allowJs": false },
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user