refactor(thumbnail): [PKG-TOOL-THUMB-04] type runtime modules and retain emitter attribution

This commit is contained in:
Harvey Zhao committed 2026-09-13 04:45:01 +08:00
1 parent efd9f42fb9
commit 15485b6420
37 files changed
+8446 -152

No files matched your search

+1
View File
@@ -8,3 +8,4 @@ test/browser/media/dash/*.mpd text eol=lf whitespace=-blank-at-eol
test/browser/media/dash/*.m4s binary
test/browser/media/*.m4a binary
refactor/baselines/core-vendor/*.txt text eol=lf
refactor/baselines/thumbnail-vendor/* text eol=lf whitespace=-blank-at-eof
+28 -1
View File
@@ -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
+31 -2
View File
@@ -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);
}
+2 -2
View File
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:unit": "node --test test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
@@ -83,7 +83,7 @@
"generate:media:hls": "node scripts/generate-hls-fixture.mjs",
"test:iframe": "node --test test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe.test.js test/iframe-lifecycle.test.js refactor/scripts/iframe-contract.test.mjs refactor/scripts/iframe-types.test.mjs",
"test:iframe-types-package": "node refactor/scripts/iframe-package-types.mjs",
"test:thumbnail": "node --test test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs"
"test:thumbnail": "node --test test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -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
}
}
@@ -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'))
@@ -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)
@@ -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
@@ -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(
@@ -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
@@ -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"]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,22 @@
{
"reviewedAt": "2026-09-12T20:36:43.265Z",
"referenceVersion": "2.1.0",
"sources": [
{
"file": "refactor/baselines/thumbnail-vendor/tiny-emitter-2.1.0.txt",
"url": "https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/index.js",
"sha256": "3e3679233f20177d63d253e7c2645024fef51731d05df15621c62a48ba98e0d3"
},
{
"file": "refactor/baselines/thumbnail-vendor/tiny-emitter-license.txt",
"url": "https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/LICENSE",
"sha256": "aada4c7da6a5be46beadf9d4f40d7dba9e222e57180d46289b2bb9112e65daae"
},
{
"file": "refactor/baselines/thumbnail-vendor/tiny-emitter-package.json",
"url": "https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/package.json",
"sha256": "f5328afd1e742ba9b107e316e5fc4c74ec81201bafad8eb8e6dc456ef6d628de"
}
],
"limitation": "Pinned upstream comparison reference. Structural correspondence and the recovered 3.5.31 dependency establish attribution, not the exact original copy revision or complete npm archive."
}
@@ -0,0 +1,67 @@
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
module.exports.TinyEmitter = E;
@@ -0,0 +1,22 @@
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.
@@ -0,0 +1,53 @@
{
"name": "tiny-emitter",
"version": "2.1.0",
"description": "A tiny (less than 1k) event emitter library",
"main": "index.js",
"scripts": {
"test-node": "tape test/index.js | tap-format-spec",
"test": "testling | tap-format-spec",
"bundle": "node_modules/.bin/browserify index.js > dist/tinyemitter.js -s TinyEmitter && echo 'Bundled'",
"minify": "node_modules/.bin/uglifyjs dist/tinyemitter.js -o dist/tinyemitter.min.js -m && echo 'Minified'",
"build": "npm test && npm run bundle && npm run minify",
"size": "node_modules/.bin/uglifyjs index.js -o minified.js -m && ls -l && rm minified.js"
},
"repository": {
"type": "git",
"url": "https://github.com/scottcorgan/tiny-emitter.git"
},
"keywords": [
"event",
"emitter",
"pubsub",
"tiny",
"events",
"bind"
],
"author": "Scott Corgan",
"license": "MIT",
"bugs": {
"url": "https://github.com/scottcorgan/tiny-emitter/issues"
},
"devDependencies": {
"@tap-format/spec": "0.2.0",
"browserify": "11.2.0",
"tape": "4.2.1",
"testling": "1.7.1",
"uglify-js": "2.5.0"
},
"testling": {
"files": [
"test/index.js"
],
"browsers": [
"iexplore/10.0",
"iexplore/9.0",
"firefox/16..latest",
"chrome/22..latest",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
}
}
@@ -0,0 +1,70 @@
# PKG-TOOL-THUMB-04 runtime TypeScript checkpoint
Status: doing. This checkpoint migrates executable source and records emitter
attribution. It does not complete public declarations, installed entrypoints or
the task's compatibility policy gate.
## Source and compatibility
Starts at efd9f42fb984302dd061d5cd9f96bd4de74fcccf. Eight runtime JS modules become
strict TS, with a ninth type-only module for configuration, events, frame points,
job controls and lifecycle state. The input and source WeakMaps remain private;
helpers import the facade only as an erased type. Existing runtime validation is
retained. There are no ts-ignore, ts-nocheck or broad any escape hatches. Local
assertions and initialization invariants are explained in the package architecture
map. Unused sleep and serial-promise helpers are removed; neither was reachable
from the public bundle or used by the owned extraction job.
declare fields preserve the old own-property order and lazy optional fields.
Normal class exports, historical creat* spellings, synchronous validation,
Promise<void> completion, cancellation and the workspace 4.4.0 defaults remain.
Source options accept Element wrappers and unknown extension fields. Known event
tuples are checked and unknown string/number/symbol events remain possible.
The source types are internal until the package's declaration entry is supplied.
## Emitter origin
Pinned upstream comparison: tiny-emitter 2.1.0 index.js, package.json and LICENSE,
fetched from its fixed Git tag and recorded with SHA-256. Its four methods and
listener registry structure correspond to the local class; the recovered 3.5.31
manifest also declared tiny-emitter ^2.1.0. This establishes the appropriate
attribution, not an exact original copied revision or a recovered complete npm
archive. Changes from that comparison reference are the existing class/ES module
conversion, rest arguments, local names and loop/style forms, plus this migration's
erased generic types and equivalent typed local aliases.
The complete upstream MIT notice is retained in package THIRD_PARTY_NOTICES and
the existing normal build banner includes it in all three standalone artifacts.
No new runtime dependency is introduced. VENDOR-09 records final packed-content
verification still outstanding; THUMB-PROVENANCE-01 retains the historical archive
limit. Source attribution must not be replaced with the repository author's name.
## Validation
The validation JSON for this checkpoint records final commands, file hashes and
browser outcomes. Six new runtime tests check public descriptors/defaults/unknown
options/validation and compare upstream, recovered 3.5.31, frozen workspace main
and legacy, and the selected candidate for emitter snapshot/removal/ctx/symbol/
exception behavior. Two provenance tests verify fixed upstream bytes and full
notice retention in package and dist/docs copies. Existing lifecycle/input tests
remain unchanged. Source type consumers include six negative uses.
Browser runs retain native media controls. Windows WebKit Blob decoding remains
an unavailable-capability control, not successful thumbnail extraction. No new
coverage percentage, physical Safari, remote GitHub CI or npm result is claimed.
## Continue / rollback
Continue 04 with the missing public declaration, legacy compiler and isolated
installed CommonJS/ESM consumers. Resolve or explicitly request a decision for
THUMB-COMPAT-01 (recovered 3.5.31 delay/fixed height versus workspace 4.4.0 defaults)
before claiming compatibility closure. Review custom emitter typing and dispatch
boundaries alongside those consumers. BASE-DIST-01 still covers the missing
promised .esm.js and types entries; task 06 owns final distribution acceptance.
05/06 retain supported WebKit/Safari file extraction and core integration/demo
verification. No manifest entry, version, lockfile or package manager changes in
this checkpoint. No push, tag or publication.
To roll back, revert this checkpoint commit and rebuild the tool using the normal
Yarn package build. Preserve upstream provenance/notice records for any retained
copy of the emitter. Do not revert the separate 03 resource fixes accidentally.
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
状态:todo 100 / doing 10 / blocked 0 / done 112 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 99 / doing 11 / blocked 0 / done 112 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -358,7 +358,7 @@
| PKG-TOOL-THUMB-01 | artplayer-tool-thumbnail<br>核对包契约与历史用法 | BASE-05 | 构造/defaults/事件/方法/历史拼写、.esm.js 入口和缺失 types | 源码/声明/README/demo/可恢复发布内容差异已登记;公开形状和版本范围冻结;区分旧 npm 的 main/CSS 与当前工作区无 main、.esm.js/类型缺失,不把工作区状态冒称旧 npm 发布事实 | H | done |
| PKG-TOOL-THUMB-02 | artplayer-tool-thumbnail<br>建立特有行为与错误测试 | PKG-TOOL-THUMB-01, ENG-03, ENG-05 | 文件输入/拖放、抽帧网格、begin/end、进度/失败/重复任务 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-TOOL-THUMB-03 | artplayer-tool-thumbnail<br>整理内部职责与资源 | PKG-TOOL-THUMB-02, CORE-01 | 输入、抽帧队列、网格导出、URL/监听清理分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-TOOL-THUMB-04 | artplayer-tool-thumbnail<br>迁移自有源码和公开类型 | PKG-TOOL-THUMB-03, ENG-04, ENG-06, CORE-07 | 补齐真实 API 声明,自有 emitter/utils TS 化 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证 | H | todo |
| PKG-TOOL-THUMB-04 | artplayer-tool-thumbnail<br>迁移自有源码和公开类型 | PKG-TOOL-THUMB-03, ENG-04, ENG-06, CORE-07 | 补齐真实 API 声明,自有 emitter/utils TS 化 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证 | H | doing |
| PKG-TOOL-THUMB-05 | artplayer-tool-thumbnail<br>验证新旧核心和组合 | PKG-TOOL-THUMB-04, CORE-22 | 工具独立浏览器使用,生成缩略图在核心中显示 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-TOOL-THUMB-06 | artplayer-tool-thumbnail<br>验证分发并同步文档 | PKG-TOOL-THUMB-05, ENG-07 | tool.thumbnail.js、历史 ESM 兼容文件、types 路径和 tarball | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -538,4 +538,5 @@
- PKG-TOOL-THUMB-01: [记录](baselines/thumbnail-release.json) [记录](baselines/thumbnail-contract.md) [记录](changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md) [记录](baselines/thumbnail-contract-validation.json)
- PKG-TOOL-THUMB-02: [记录](baselines/thumbnail-behavior-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md)
- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md) [记录](baselines/thumbnail-lifecycle-validation.json)
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
+14
View File
@@ -1,5 +1,19 @@
# 进度与证据
## PKG-TOOL-THUMB-04 运行时 TS 与 emitter 来源检查点(doing)
8个运行时模块已迁为严格TS,新增类型模块明确option/事件/帧/job/资源状态;declare保留
旧实例字段顺序和按需出现的字段。移除未使用的sleep/serial助手。生成ESM除许可和两个
等价emitter局部变量外与03运行时相同,默认值和同步/异步行为保持。固定tiny-emitter
2.1.0上游比较文件及完整MIT通知,登记VENDOR-09;不声称确定原始复制revision。
新增6项门面/emitter兼容与2项来源/通知检查,源码/main/legacy各110项通过。
三份浏览器各57通过(34真实工具、3输入、17Blob不可用、3原生媒体对照)。
完整CI1654通过,339生产TS和3导入通过;保留既有编辑器声明lint警告及Yarn警告记录。
公开声明/旧编译器/实际安装入口和默认历史差异仍待04继续,05/06保留真实设备/分发门槛。
见[变更](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md)和
[验证](baselines/thumbnail-runtime-types-validation.json)。222项:112 done、11 doing、99 todo。
## PKG-TOOL-THUMB-03 完成:抽帧任务与媒体资源所有权
增加lifecycle/source/extraction模块,保留公开类形状与工作区默认行为。元数据等待、
+1
View File
@@ -181,3 +181,4 @@
| THUMB-LIFE-01 | open / 已复现 | Thumbnail drop binding and repeated destroy fail; pending work and replacement resource ownership need repair | PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-05 |
| THUMB-PROVENANCE-01 | open / 源码/产物事实 | Thumbnail local emitter attribution and historical distribution completeness remain unverified | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
| THUMB-MEDIA-01 | open / 已复现 | Windows WebKit native Blob URLs cannot load tested MP4 media despite successful HTTP controls | PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 |
| VENDOR-09 | open / 源码/产物事实 | Thumbnail tiny-emitter 改写来源与最终分发许可 | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
+19
View File
@@ -3934,6 +3934,25 @@
],
"compatibleResolution": "Preserve independent HTTP/Blob capability controls; obtain actual supported WebKit/Safari file extraction evidence before release acceptance. Do not count the 12 unavailable controls as successful thumbnail generation or silently replace local files with HTTP media.",
"closureCriteria": "Actual generated PNG/extraction and lifecycle proof in supported Safari/WebKit environments, or explicit supported-environment decision with migration documentation; Windows limitation stays identifiable."
},
{
"id": "VENDOR-09",
"title": "Thumbnail tiny-emitter 改写来源与最终分发许可",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-TOOL-THUMB-04",
"PKG-TOOL-THUMB-06"
],
"evidence": [
"refactor/baselines/thumbnail-vendor/sources.json",
"packages/artplayer-tool-thumbnail/src/emitter.ts",
"packages/artplayer-tool-thumbnail/THIRD_PARTY_NOTICES",
"test/thumbnail-vendor.test.js"
],
"compatibleResolution": "Preserve original attribution, typed local dispatch compatibility and complete MIT notice in source/package/standalone bundles.",
"closureCriteria": "Pinned upstream reference and local adaptation reviewed; all bundle and actual packed distribution notices verified. Do not claim the exact original copied revision or a recovered complete historical archive.",
"workspaceState": "TS adaptation and pinned 2.1.0 source/license references recorded; normal build includes package notices. Final installed/packed distribution evidence remains with 04/06."
}
]
}
+5 -2
View File
@@ -3809,11 +3809,14 @@
"ENG-06",
"CORE-07"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "补齐真实 API 声明,自有 emitter/utils TS 化",
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证",
"evidence": []
"evidence": [
"changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md",
"baselines/thumbnail-runtime-types-validation.json"
]
},
{
"id": "PKG-TOOL-THUMB-05",
+33
View File
@@ -1025,6 +1025,39 @@
"sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03"
}
]
},
{
"id": "thumbnail-tiny-emitter",
"riskId": "VENDOR-09",
"inputs": [
"packages/artplayer-tool-thumbnail/src/emitter.ts"
],
"owners": [
"PKG-TOOL-THUMB-04",
"PKG-TOOL-THUMB-06"
],
"sourceStatus": "Local on/once/emit/off structurally correspond to the fixed tiny-emitter 2.1.0 reference; recovered 3.5.31 declared ^2.1.0. Exact original copied revision is unknown. This migration adds types while preserving the local class and dispatch behavior.",
"licenseStatus": "Pinned upstream MIT notice copied verbatim into package THIRD_PARTY_NOTICES and included by the normal standalone build banner. Final packed distribution verification remains 06.",
"upstreamSources": [
"https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/index.js",
"https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/LICENSE",
"https://raw.githubusercontent.com/scottcorgan/tiny-emitter/2.1.0/package.json"
],
"upstreamReviewedAt": "2026-09-13",
"updatePolicy": "Keep attribution and pinned upstream comparison. Review observable dispatch changes independently from type migration; run upstream/historical/candidate parity tests and validate all bundle and packed notices.",
"fingerprints": [
{
"path": "packages/artplayer-tool-thumbnail/src/emitter.ts",
"algorithm": "sha256-lf",
"sha256": "2844fa8d279d35d676f61eef4f8a3e173146548c64b8e49f2dabfc25aec56668"
}
],
"reviewEvidence": [
"refactor/baselines/thumbnail-vendor/sources.json",
"test/thumbnail-runtime.test.js",
"test/thumbnail-vendor.test.js",
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md"
]
}
],
"integrations": [
+131
View File
@@ -0,0 +1,131 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Runtime compatibility tests.
import test from 'node:test'
import vm from 'node:vm'
import { thumbnailCandidate, thumbnailEnvironment, thumbnailHistorical } from './helpers/thumbnail.js'
const candidate = await thumbnailCandidate()
function create(implementation) {
const env = thumbnailEnvironment(implementation)
return { ...env, tool: new env.Factory({ fileInput: new env.Element('input') }) }
}
const referenceModule = { exports: {} }
vm.runInNewContext(fs.readFileSync(new URL('../refactor/baselines/thumbnail-vendor/tiny-emitter-2.1.0.txt', import.meta.url), 'utf8'), { module: referenceModule })
const Upstream = referenceModule.exports
const upstream = () => new Upstream()
const emitters = [upstream, ...thumbnailHistorical().map(implementation => () => create(implementation).tool), () => create(candidate).tool]
test('Thumbnail typed facade preserves descriptors, lazy fields, default class and unknown options', () => {
const { tool, Factory, exported } = create(candidate)
const baseline = create(thumbnailHistorical().find(item => item.name === 'workspace.js'))
const describe = object => Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(object)).map(([name, value]) => [name, {
enumerable: value.enumerable,
configurable: value.configurable,
writable: value.writable,
getter: typeof value.get,
setter: typeof value.set,
type: typeof value.value,
arity: typeof value.value === 'function' ? value.value.length : undefined,
}]))
assert.deepEqual(describe(Factory.prototype), describe(baseline.Factory.prototype))
assert.deepEqual(describe(Factory), describe(baseline.Factory))
assert.deepEqual(describe(tool), describe(baseline.tool))
assert.deepEqual(Object.keys(tool), ['processing', 'option', 'video', 'duration', 'inputChange', 'ondrop'])
assert.equal(exported.default, undefined)
assert.equal(typeof thumbnailEnvironment(candidate, true).exported, 'function')
const metadata = { id: 123 }
assert.equal(tool.setup({ custom: metadata, width: 21.5 }), tool)
assert.equal(tool.option.custom, metadata)
assert.equal(tool.option.width, 21.5)
assert.notEqual(Factory.DEFAULTS, Factory.DEFAULTS)
assert.deepEqual(Object.keys(Factory.DEFAULTS), Object.keys(baseline.Factory.DEFAULTS))
assert(Number.isNaN(Factory.DEFAULTS.end))
tool.destroy()
})
test('Thumbnail emitter keeps upstream snapshot order, duplicate removal and original once callback identity', () => {
for (const create of emitters) {
const emitter = create()
const seen = []
const callback = value => seen.push(value)
assert.equal(emitter.on('value', () => emitter.off('value', callback)), emitter)
emitter.on('value', callback).once('value', callback).on('value', callback)
emitter.emit('value', 7).emit('value', 8)
assert.deepEqual(seen, [7, 7, 7])
assert.equal(emitter.off('value'), emitter)
assert.equal(emitter.e.value, undefined)
emitter.once('removed', callback).off('removed', callback).emit('removed', 9)
assert.deepEqual(seen, [7, 7, 7])
}
})
test('Thumbnail emitter preserves ctx, variadic arguments, symbol events and ignored callback returns', () => {
for (const create of emitters) {
const emitter = create()
const ctx = { calls: 0 }
const name = Symbol('event')
function callback(...args) {
assert.equal(this, ctx)
assert.deepEqual(args, [1, 'two', null])
this.calls++
return 123
}
assert.equal(emitter.on(name, callback, ctx), emitter)
assert.equal(emitter.once(name, callback, ctx), emitter)
assert.equal(emitter.emit(name, 1, 'two', null), emitter)
assert.equal(ctx.calls, 2)
emitter.off(name, callback).emit(name)
assert.equal(ctx.calls, 2)
}
})
test('Thumbnail emitter synchronous listener failures retain identity and stop the current snapshot', () => {
for (const create of emitters) {
const emitter = create()
const failure = { reason: 'listener failure' }
let later = 0
emitter.once('event', () => {
throw failure
})
emitter.on('event', () => later++)
assert.throws(() => emitter.emit('event'), error => error === failure)
assert.equal(later, 0)
emitter.emit('event')
assert.equal(later, 1)
}
})
test('Thumbnail emitter additions during dispatch wait until the next snapshot', () => {
for (const create of emitters) {
const emitter = create()
const seen = []
emitter.once('event', () => {
seen.push('first')
emitter.on('event', () => seen.push('new'))
})
emitter.on('event', () => seen.push('second'))
emitter.emit('event')
assert.deepEqual(seen, ['first', 'second'])
emitter.emit('event')
assert.deepEqual(seen, ['first', 'second', 'second', 'new'])
}
})
test('Thumbnail typed facade retains synchronous validation, option mutation and grid geometry', () => {
const { tool } = create(candidate)
tool.setup({ number: 10, width: 20, height: 30, column: 3, begin: 5 })
tool.duration = 100
assert.deepEqual(JSON.parse(JSON.stringify(tool.creatScreenshotDate())), Array.from({ length: 10 }, (_, i) => ({ time: 10 + i * 10, x: i % 3 * 20, y: Math.floor(i / 3) * 30 })))
const canvas = tool.creatCanvas()
assert.equal(canvas.width, 60)
assert.equal(canvas.height, 150)
const errors = []
tool.on('error', value => errors.push(value))
assert.throws(() => tool.setup({ width: '80' }), /not a number/)
assert.equal(tool.option.width, 20)
assert.throws(() => tool.download(), /does not seem to be ready/)
assert.equal(errors.length, 2)
assert.equal(tool.errorHandle(true, 'unused'), undefined)
tool.destroy()
})
+31
View File
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Pinned provenance and distributable notice checks.
import test from 'node:test'
const read = file => fs.readFileSync(new URL(`../${file}`, import.meta.url), 'utf8')
const base = 'refactor/baselines/thumbnail-vendor/'
const license = read(`${base}tiny-emitter-license.txt`).replaceAll('\r\n', '\n').trim()
test('Thumbnail emitter reference files retain pinned bytes, version and full upstream attribution', () => {
const manifest = JSON.parse(read(`${base}sources.json`))
for (const source of manifest.sources)
assert.equal(createHash('sha256').update(fs.readFileSync(new URL(`../${source.file}`, import.meta.url))).digest('hex'), source.sha256)
const pkg = JSON.parse(read(`${base}tiny-emitter-package.json`))
assert.equal(pkg.name, 'tiny-emitter')
assert.equal(pkg.version, '2.1.0')
assert.equal(pkg.license, 'MIT')
assert.match(license, /Copyright \(c\) 2017 Scott Corgan/)
assert(read('packages/artplayer-tool-thumbnail/THIRD_PARTY_NOTICES').replaceAll('\r\n', '\n').includes(license))
})
test('Thumbnail standalone distributions retain the complete notice and equal their docs copies', () => {
for (const suffix of ['.js', '.legacy.js', '.mjs']) {
const filename = `artplayer-tool-thumbnail${suffix}`
const bundle = read(`packages/artplayer-tool-thumbnail/dist/${filename}`)
const banner = bundle.match(/^\/\*![\s\S]*?\*\//)?.[0].replace(/^ \* ?/gm, '').replaceAll('\r\n', '\n')
assert(banner?.includes(license), `${filename} must carry the complete upstream notice`)
assert.equal(read(`docs/compiled/${filename}`), bundle)
}
})
+45
View File
@@ -0,0 +1,45 @@
import Thumbnail from '../../packages/artplayer-tool-thumbnail/src'
const input = document.createElement('input')
const tool = new Thumbnail({ fileInput: input, number: 10, tracking: { id: 'example' } })
const same: Thumbnail = tool.setup({ width: 80 }).on('update', (url, progress) => {
const value: string = url
const percent: number = progress
void [value, percent]
})
tool.once('file', function (file) {
this.name = file.name
}, { name: '' })
tool.on('error', (message) => {
const value: string | undefined = message
void value
})
tool.on('custom', (...payload) => {
const values: unknown[] = payload
void values
})
tool.emit(Symbol('extension'), 'value')
const pending: Promise<void> = tool.start()
const point: { time: number, x: number, y: number } | undefined = tool.creatScreenshotDate()[0]
const canvas: HTMLCanvasElement = tool.creatCanvas()
const defaults: number = Thumbnail.DEFAULTS.height
const wrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
tool.setup({ fileInput: wrapper })
tool.loadVideo(input.files?.[0])
tool.loadVideo(null)
const destroyed: void = tool.destroy()
void [same, pending, point, canvas, defaults, destroyed]
// @ts-expect-error Numeric options cannot be strings.
tool.setup({ width: '80' })
// @ts-expect-error A file-input selector string has never been supported.
const invalidInput = new Thumbnail({ fileInput: '#input' })
// @ts-expect-error Known update events carry a URL followed by numeric progress.
tool.emit('update', 123, '50%')
// @ts-expect-error Known callbacks must accept their actual payload.
tool.on('video', (file: File) => file.name)
// @ts-expect-error The public start result is completion, not the thumbnail URL.
const incorrect: Promise<string> = tool.start()
// @ts-expect-error Runtime ESM exposes only a default class, no .default self-alias.
const InvalidAlias = Thumbnail.default
void [incorrect, invalidInput, InvalidAlias]