mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 21:06:15 -08:00
refactor(thumbnail): [PKG-TOOL-THUMB-04] preserve approved published defaults
This commit is contained in:
1 parent
490e157717
commit
b5f8b5e28e
37 files changed
+11172
-136
No files matched your search
+10
-1
@@ -2,7 +2,7 @@
|
||||
/** Extract PNG thumbnail sheets from a locally selected video file. */
|
||||
declare class ArtplayerToolThumbnail {
|
||||
constructor(option?: ArtplayerToolThumbnail.Option)
|
||||
static readonly DEFAULTS: ArtplayerToolThumbnail.SheetOptions
|
||||
static readonly DEFAULTS: ArtplayerToolThumbnail.DefaultOptions
|
||||
static ondragover(event: DragEvent): void
|
||||
static creatVideo(): HTMLVideoElement
|
||||
processing: boolean
|
||||
@@ -39,13 +39,22 @@ declare namespace ArtplayerToolThumbnail {
|
||||
begin: number
|
||||
end: number
|
||||
}
|
||||
type Compatibility = 'published-3.5' | 'workspace-4.4'
|
||||
interface DefaultOptions extends SheetOptions {
|
||||
delay: number
|
||||
}
|
||||
/** fileInput must be a file input or an Element wrapper when constructing. */
|
||||
interface Option extends Partial<SheetOptions> {
|
||||
fileInput?: Element
|
||||
compatibility?: Compatibility
|
||||
/** Published mode delay in milliseconds, clamped to 10-1000. */
|
||||
delay?: number
|
||||
[name: string]: unknown
|
||||
}
|
||||
interface ResolvedOption extends SheetOptions {
|
||||
fileInput: HTMLInputElement
|
||||
compatibility?: Compatibility
|
||||
delay?: number
|
||||
[name: string]: unknown
|
||||
}
|
||||
interface ScreenshotPoint {
|
||||
|
||||
@@ -31,4 +31,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,r){const o=this.e||(this.e={});let i=e(o,t)?o[t]:void 0;return i||(i=[],Object.defineProperty(o,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:r}),this}once(e,t,n){const r=this,o=t;let i=!1;function s(...t){i||(i=!0,r.off(e,s),o.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const r=this.e||(this.e={}),o=(e(r,t)&&r[t]||[]).slice();for(let e=0;e<o.length;e+=1)o[e].fn.apply(o[e].ctx,n);return this}off(t,n){const r=this.e||(this.e={}),o=e(r,t)?r[t]:void 0,i=[];if(o&&n)for(let e=0,s=o.length;e<s;e+=1)o[e].fn!==n&&o[e].fn._!==n&&i.push(o[e]);return i.length?r[t]=i:delete r[t],this}}const n=new WeakMap;function r(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function o(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const o of e)try{o()}catch(r){n||(n=!0,t=r)}if(n)throw t}function c(e,t="destroyed"){const n=r(e);return!n.closed&&(n.closed=!0,n.epoch++,n.job?.cancel(t),!0)}function a(e){const t=r(e),n=new Set([...t.sourceUrls,e.videoUrl]),o=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const r of new Set([t.video,e.video]))r&&c.push(()=>r.pause?.(),()=>r.removeAttribute?.("src"),()=>r.load?.(),()=>r.parentNode?.removeChild(r));s([...t.sourceListeners.splice(0),...c,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...o].map(e=>()=>i(t.thumbnailUrls,e))])}function l(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e,t){let n=t.epoch;const c=e.video;let a,u,d,h,p=!1,f=!1,m=()=>{};const b=[],y=new Promise((e,t)=>{a=e,u=t});y.catch(()=>{});const g=()=>!p&&!t.closed&&t.epoch===n&&t.job===h;function v(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),s([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{v()}catch(r){t||(t=r)}if(n)try{e.emit("error",t?.message)}catch(o){t=o}u(t)}}function U(t){if(!g())return;f=!0,e.processing=!0;let n=0;const o=()=>{if(!g())return;if(n===t.points.length)return void function(){if(g())try{v(),e.emit("done"),a()}catch(t){let r=t;try{e.emit("error",t?.message)}catch(n){r=n}u(r)}}();const s=t.points[n];let l=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!g()||l||c.seeking||void 0!==c.readyState&&c.readyState<2)){l=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,n){const o=r(e),s=URL.createObjectURL(t);if(o.thumbnailUrls.add(s),!n())return i(o.thumbnailUrls,s),null;const c=new Set([...o.thumbnailUrls,e.thumbnailUrl]);o.thumbnailUrl=s,e.thumbnailUrl=s;for(const r of c)r&&r!==s&&i(o.thumbnailUrls,r);return n()?s:null}(e,s,g);if(!c)return;e.emit("update",c,(n+1)/t.number),n++,Promise.resolve().then(o)}catch(c){w(c)}}})}catch(a){w(a)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(o)}function E(n=!1){if(g()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,a=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=l(i,0,n.duration),e.option.end=l(s||n.duration,i,n.duration),a(e.option.end>e.option.begin,"End time must be greater than the start time"),a(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,a(e.file&&n,"Please select the video file first"),a(!e.processing,"There is currently a task in progress, please wait a moment..."),a(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),g()||clearTimeout(d)}h={video:c,promise:y,ready:E,fail:w,cancel(e){w(o(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(n=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{g()&&w(new Error(`Unable to load video: media error ${c.error?.code||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeEventLisLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,o,n){const r=this.e||(this.e={});let i=e(r,t)?r[t]:void 0;return i||(i=[],Object.defineProperty(r,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:o,ctx:n}),this}once(e,t,o){const n=this,r=t;let i=!1;function s(...t){i||(i=!0,n.off(e,s),r.apply(o,t))}return s._=t,this.on(e,s,o)}emit(t,...o){const n=this.e||(this.e={}),r=(e(n,t)&&n[t]||[]).slice();for(let e=0;e<r.length;e+=1)r[e].fn.apply(r[e].ctx,o);return this}off(t,o){const n=this.e||(this.e={}),r=e(n,t)?n[t]:void 0,i=[];if(r&&o)for(let e=0,s=r.length;e<s;e+=1)r[e].fn!==o&&r[e].fn._!==o&&i.push(r[e]);return i.length?n[t]=i:delete n[t],this}}const o=new WeakMap;function n(e){return o.has(e)||o.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),o.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,o=!1;for(const r of e)try{r()}catch(n){o||(o=!0,t=n)}if(o)throw t}function c(e,t="destroyed"){const o=n(e);return!o.closed&&(o.closed=!0,o.epoch++,o.job?.cancel(t),!0)}function a(e){const t=n(e),o=new Set([...t.sourceUrls,e.videoUrl]),r=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const n of new Set([t.video,e.video]))n&&c.push(()=>n.pause?.(),()=>n.removeAttribute?.("src"),()=>n.load?.(),()=>n.parentNode?.removeChild(n));s([...t.sourceListeners.splice(0),...c,...[...o].map(e=>()=>i(t.sourceUrls,e)),...[...r].map(e=>()=>i(t.thumbnailUrls,e))])}function l(e,t,o){return Math.max(Math.min(e,Math.max(t,o)),Math.min(t,o))}function u(e){const t="workspace-4.4"===e.compatibility;return{delay:t?null:e.delay,aspectHeight:t,resetInput:t}}function d(e,t){let o=t.epoch;const c=e.video;let a,d,h,p,f=!1,m=!1,b=()=>{};const y=[],g=new Promise((e,t)=>{a=e,d=t});g.catch(()=>{});const v=()=>!f&&!t.closed&&t.epoch===o&&t.job===p;function w(){return!f&&(f=!0,t.job===p&&(t.job=null,e.processing=!1),s([()=>clearTimeout(h),b,...y.splice(0)]),!0)}function U(t,o=!0){if(!f){try{w()}catch(n){t||(t=n)}if(o)try{e.emit("error",t?.message)}catch(r){t=r}d(t)}}function E(){if(v())try{w(),e.emit("done"),a()}catch(t){let n=t;try{e.emit("error",t?.message)}catch(o){n=o}d(n)}}function T(t){if(!v())return;m=!0,e.processing=!0;let o=0;const r=()=>{if(!v())return;if(o===t.points.length)return void(null!==t.delay?h=setTimeout(E,2*Number(t.delay)):E());const s=t.points[o];let a=!1,l=!1,u=null===t.delay;const d=c.oncanplay,p=()=>{if(!(!v()||!u||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(b(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!v())return;t.canvas.toBlob(s=>{if(v()&&!l){l=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,o){const r=n(e),s=URL.createObjectURL(t);if(r.thumbnailUrls.add(s),!o())return i(r.thumbnailUrls,s),null;const c=new Set([...r.thumbnailUrls,e.thumbnailUrl]);r.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&i(r.thumbnailUrls,n);return o()?s:null}(e,s,v);if(!c)return;e.emit("update",c,(o+1)/t.number),o++,Promise.resolve().then(r)}catch(c){U(c)}}})}catch(d){U(d)}}};b=()=>{c.oncanplay===p&&(c.oncanplay=d),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,null!==t.delay?(h=setTimeout(()=>{u=!0,p()},t.delay),v()||clearTimeout(h)):c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){U(f)}};Promise.resolve().then(r)}function j(o=!1){if(v()&&!m)if(clearTimeout(h),!t.loading&&c.duration){m=!0;try{const t=function(e,t){const{video:o}=t,{width:n,number:r,begin:i,end:s}=e.option,c=u(e.option),a=c.aspectHeight?o.videoHeight/o.videoWidth*n:e.option.height,d=(o,n)=>{try{e.errorHandle(o,n)}catch(r){throw t.reported=r,t.wasReported=!0,r}};e.option.height=a,e.option.begin=l(i,0,o.duration),e.option.end=l(s||o.duration,i,o.duration),d(e.option.end>e.option.begin,"End time must be greater than the start time"),d(Number.isFinite(o.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=r/e.duration,d(e.file&&o,"Please select the video file first"),d(!e.processing,"There is currently a task in progress, please wait a moment..."),d(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const h=e.creatScreenshotDate(),p=e.creatCanvas(),f=p.getContext("2d");return e.emit("canvas",p),{width:n,height:a,number:r,points:h,canvas:p,context:f,delay:c.delay}}(e,p);T(t)}catch(n){if(U(n,!o&&!(p.wasReported&&n===p.reported)),o)throw n}}else h=setTimeout(()=>j(),1e3),v()Line truncated
|
||||
@@ -31,4 +31,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,r){const o=this.e||(this.e={});let i=e(o,t)?o[t]:void 0;return i||(i=[],Object.defineProperty(o,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:r}),this}once(e,t,n){const r=this,o=t;let i=!1;function s(...t){i||(i=!0,r.off(e,s),o.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const r=this.e||(this.e={}),o=(e(r,t)&&r[t]||[]).slice();for(let e=0;e<o.length;e+=1)o[e].fn.apply(o[e].ctx,n);return this}off(t,n){const r=this.e||(this.e={}),o=e(r,t)?r[t]:void 0,i=[];if(o&&n)for(let e=0,s=o.length;e<s;e+=1)o[e].fn!==n&&o[e].fn._!==n&&i.push(o[e]);return i.length?r[t]=i:delete r[t],this}}const n=new WeakMap;function r(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function o(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const o of e)try{o()}catch(r){n||(n=!0,t=r)}if(n)throw t}function c(e,t="destroyed"){var n;const o=r(e);return!o.closed&&(o.closed=!0,o.epoch++,null==(n=o.job)||n.cancel(t),!0)}function l(e){const t=r(e),n=new Set([...t.sourceUrls,e.videoUrl]),o=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const r of new Set([t.video,e.video]))r&&c.push(()=>{var e;return null==(e=r.pause)?void 0:e.call(r)},()=>{var e;return null==(e=r.removeAttribute)?void 0:e.call(r,"src")},()=>{var e;return null==(e=r.load)?void 0:e.call(r)},()=>{var e;return null==(e=r.parentNode)?void 0:e.removeChild(r)});s([...t.sourceListeners.splice(0),...c,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...o].map(e=>()=>i(t.thumbnailUrls,e))])}function a(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e,t){let n=t.epoch;const c=e.video;let l,u,d,h,p=!1,f=!1,m=()=>{};const b=[],v=new Promise((e,t)=>{l=e,u=t});v.catch(()=>{});const y=()=>!p&&!t.closed&&t.epoch===n&&t.job===h;function g(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),s([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{g()}catch(r){t||(t=r)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(o){t=o}u(t)}}function U(t){if(!y())return;f=!0,e.processing=!0;let n=0;const o=()=>{if(!y())return;if(n===t.points.length)return void function(){if(y())try{g(),e.emit("done"),l()}catch(t){let r=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){r=n}u(r)}}();const s=t.points[n];let a=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!y()||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!y())return;t.canvas.toBlob(s=>{if(y()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,n){const o=r(e),s=URL.createObjectURL(t);if(o.thumbnailUrls.add(s),!n())return i(o.thumbnailUrls,s),null;const c=new Set([...o.thumbnailUrls,e.thumbnailUrl]);o.thumbnailUrl=s,e.thumbnailUrl=s;for(const r of c)r&&r!==s&&i(o.thumbnailUrls,r);return n()?s:null}(e,s,y);if(!c)return;e.emit("update",c,(n+1)/t.number),n++,Promise.resolve().then(o)}catch(c){w(c)}}})}catch(l){w(l)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(o)}function E(n=!1){if(y()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,l=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=a(i,0,n.duration),e.option.end=a(s||n.duration,i,n.duration),l(e.option.end>e.option.begin,"End time must be greater than the start time"),l(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,l(e.file&&n,"Please select the video file first"),l(!e.processing,"There is currently a task in progress, please wait a moment..."),l(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),y()||clearTimeout(d)}h={video:c,promise:v,ready:E,fail:w,cancel(e){w(o(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(n=t,!0)},t.job=h;tryLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,o){const r=this.e||(this.e={});let i=e(r,t)?r[t]:void 0;return i||(i=[],Object.defineProperty(r,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:o}),this}once(e,t,n){const o=this,r=t;let i=!1;function s(...t){i||(i=!0,o.off(e,s),r.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const o=this.e||(this.e={}),r=(e(o,t)&&o[t]||[]).slice();for(let e=0;e<r.length;e+=1)r[e].fn.apply(r[e].ctx,n);return this}off(t,n){const o=this.e||(this.e={}),r=e(o,t)?o[t]:void 0,i=[];if(r&&n)for(let e=0,s=r.length;e<s;e+=1)r[e].fn!==n&&r[e].fn._!==n&&i.push(r[e]);return i.length?o[t]=i:delete o[t],this}}const n=new WeakMap;function o(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const r of e)try{r()}catch(o){n||(n=!0,t=o)}if(n)throw t}function l(e,t="destroyed"){var n;const r=o(e);return!r.closed&&(r.closed=!0,r.epoch++,null==(n=r.job)||n.cancel(t),!0)}function a(e){const t=o(e),n=new Set([...t.sourceUrls,e.videoUrl]),r=new Set([...t.thumbnailUrls,e.thumbnailUrl]),l=[];for(const o of new Set([t.video,e.video]))o&&l.push(()=>{var e;return null==(e=o.pause)?void 0:e.call(o)},()=>{var e;return null==(e=o.removeAttribute)?void 0:e.call(o,"src")},()=>{var e;return null==(e=o.load)?void 0:e.call(o)},()=>{var e;return null==(e=o.parentNode)?void 0:e.removeChild(o)});s([...t.sourceListeners.splice(0),...l,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...r].map(e=>()=>i(t.thumbnailUrls,e))])}function c(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e){const t="workspace-4.4"===e.compatibility;return{delay:t?null:e.delay,aspectHeight:t,resetInput:t}}function d(e,t){let n=t.epoch;const l=e.video;let a,d,h,p,f=!1,m=!1,b=()=>{};const y=[],v=new Promise((e,t)=>{a=e,d=t});v.catch(()=>{});const g=()=>!f&&!t.closed&&t.epoch===n&&t.job===p;function w(){return!f&&(f=!0,t.job===p&&(t.job=null,e.processing=!1),s([()=>clearTimeout(h),b,...y.splice(0)]),!0)}function U(t,n=!0){if(!f){try{w()}catch(o){t||(t=o)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(r){t=r}d(t)}}function E(){if(g())try{w(),e.emit("done"),a()}catch(t){let o=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){o=n}d(o)}}function T(t){if(!g())return;m=!0,e.processing=!0;let n=0;const r=()=>{if(!g())return;if(n===t.points.length)return void(null!==t.delay?h=setTimeout(E,2*Number(t.delay)):E());const s=t.points[n];let a=!1,c=!1,u=null===t.delay;const d=l.oncanplay,p=()=>{if(!(!g()||!u||a||l.seeking||void 0!==l.readyState&&l.readyState<2)){a=!0;try{if(b(),t.context.drawImage(l,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!c){c=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const l=function(e,t,n){const r=o(e),s=URL.createObjectURL(t);if(r.thumbnailUrls.add(s),!n())return i(r.thumbnailUrls,s),null;const l=new Set([...r.thumbnailUrls,e.thumbnailUrl]);r.thumbnailUrl=s,e.thumbnailUrl=s;for(const o of l)o&&o!==s&&i(r.thumbnailUrls,o);return n()?s:null}(e,s,g);if(!l)return;e.emit("update",l,(n+1)/t.number),n++,Promise.resolve().then(r)}catch(l){U(l)}}})}catch(d){U(d)}}};b=()=>{l.oncanplay===p&&(l.oncanplay=d),l.removeEventListener("seeked",p)};try{l.oncanplay=p,l.addEventListener("seeked",p),l.currentTime=s.time,null!==t.delay?(h=setTimeout(()=>{u=!0,p()},t.delay),g()||clearTimeout(h)):l.readyState>=2&&!l.seeking&&Promise.resolve().then(p)}catch(f){U(f)}};Promise.resolve().then(r)}function j(n=!1){if(g()&&!m)if(clearTimeout(h),!t.loading&&l.duration){m=!0;try{const t=function(e,t){const{video:n}=t,{width:o,number:r,begin:i,end:s}=e.option,l=u(e.option),a=l.aspectHeight?n.videoHeight/n.videoWidth*o:e.option.height,d=(n,o)=>{try{e.errorHandle(n,o)}catch(r){throw t.reported=r,t.wasReported=!0,r}};e.option.height=a,e.option.begin=c(i,0,n.duration),e.option.end=c(s||n.duration,i,n.duration),d(e.option.end>e.option.begin,"End time must be greater than the start time"),d(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=r/e.duration,d(e.file&&n,"Please select the video file first"),d(!e.processing,"There is currently a task in progress, please wait a moment..."),d(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const h=e.creatScreenshotDate(),p=e.creatCanvas(),f=p.getContext("2d");return e.emiLine truncated
|
||||
@@ -160,12 +160,39 @@ function releaseMedia(tool) {
|
||||
...[...thumbnails].map((url) => () => revoke(state.thumbnailUrls, url))
|
||||
]);
|
||||
}
|
||||
function getFileName(name) {
|
||||
const nameArray = name.split(".");
|
||||
nameArray.pop();
|
||||
return nameArray.join(".");
|
||||
}
|
||||
function clamp(num, a, b) {
|
||||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
}
|
||||
function thumbnailPolicy(option) {
|
||||
const workspace = option.compatibility === "workspace-4.4";
|
||||
return { delay: workspace ? null : option.delay, aspectHeight: workspace, resetInput: workspace };
|
||||
}
|
||||
function normalizePolicy(option, validate) {
|
||||
validate(option.compatibility === void 0 || option.compatibility === "published-3.5" || option.compatibility === "workspace-4.4", "The compatibility option must be published-3.5 or workspace-4.4");
|
||||
const workspace = thumbnailPolicy(option).aspectHeight;
|
||||
const fields = workspace ? ["number", "width", "column", "begin", "end"] : ["delay", "number", "width", "height", "column", "begin", "end"];
|
||||
for (const name of fields)
|
||||
validate(typeof option[name] === "number", `The '${name}' is not a number`);
|
||||
option.number = clamp(option.number, 10, 1e3);
|
||||
option.width = clamp(option.width, 10, 1e3);
|
||||
option.column = clamp(option.column, 1, 1e3);
|
||||
if (!workspace) {
|
||||
option.delay = clamp(Number(option.delay), 10, 1e3);
|
||||
option.height = clamp(option.height, 10, 1e3);
|
||||
}
|
||||
}
|
||||
function loadSource(tool, file) {
|
||||
const state = stateFor(tool);
|
||||
if (!file || state.closed)
|
||||
return;
|
||||
const initialEpoch = state.epoch;
|
||||
const video = tool.video;
|
||||
const { delay } = thumbnailPolicy(tool.option);
|
||||
const support = video.canPlayType(file.type);
|
||||
tool.errorHandle(support === "maybe" || support === "probably", `Playback of this file format is not supported: ${file.type}`);
|
||||
if (state.closed || state.epoch !== initialEpoch)
|
||||
@@ -187,11 +214,15 @@ function loadSource(tool, file) {
|
||||
state.sourceListeners = listeners;
|
||||
let pendingCleanup = () => {
|
||||
};
|
||||
let notificationTimer;
|
||||
const cancelNotification = () => clearTimeout(notificationTimer);
|
||||
listeners.push(cancelNotification);
|
||||
let reported = false;
|
||||
const error = () => {
|
||||
if (!current() || reported || video.currentSrc && video.currentSrc !== url)
|
||||
return;
|
||||
reported = true;
|
||||
cancelNotification();
|
||||
state.loading = false;
|
||||
const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`);
|
||||
if (state.job)
|
||||
@@ -238,8 +269,19 @@ function loadSource(tool, file) {
|
||||
if (previous !== url)
|
||||
revoke(state.sourceUrls, previous);
|
||||
}
|
||||
if (current())
|
||||
tool.emit("video", video);
|
||||
if (current() && !reported) {
|
||||
if (delay !== null) {
|
||||
notificationTimer = setTimeout(() => {
|
||||
cancelNotification();
|
||||
if (current() && !reported)
|
||||
tool.emit("video", video);
|
||||
}, delay);
|
||||
if (!current() || reported)
|
||||
cancelNotification();
|
||||
} else {
|
||||
tool.emit("video", video);
|
||||
}
|
||||
}
|
||||
} catch (error2) {
|
||||
if (current()) {
|
||||
state.loading = false;
|
||||
@@ -270,18 +312,11 @@ function replaceThumbnail(tool, blob, live) {
|
||||
}
|
||||
return live() ? url : null;
|
||||
}
|
||||
function getFileName(name) {
|
||||
const nameArray = name.split(".");
|
||||
nameArray.pop();
|
||||
return nameArray.join(".");
|
||||
}
|
||||
function clamp(num, a, b) {
|
||||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
}
|
||||
function prepare(tool, job) {
|
||||
const { video } = job;
|
||||
const { width, number, begin, end } = tool.option;
|
||||
const height = video.videoHeight / video.videoWidth * width;
|
||||
const policy = thumbnailPolicy(tool.option);
|
||||
const height = policy.aspectHeight ? video.videoHeight / video.videoWidth * width : tool.option.height;
|
||||
const guard = (condition, message) => {
|
||||
try {
|
||||
tool.errorHandle(condition, message);
|
||||
@@ -305,7 +340,7 @@ function prepare(tool, job) {
|
||||
const canvas = tool.creatCanvas();
|
||||
const context = canvas.getContext("2d");
|
||||
tool.emit("canvas", canvas);
|
||||
return { width, height, number, points, canvas, context };
|
||||
return { width, height, number, points, canvas, context, delay: policy.delay };
|
||||
}
|
||||
function createJob(tool, state) {
|
||||
let epoch = state.epoch;
|
||||
@@ -381,15 +416,19 @@ function createJob(tool, state) {
|
||||
if (!live())
|
||||
return;
|
||||
if (index === plan.points.length) {
|
||||
complete();
|
||||
if (plan.delay !== null)
|
||||
timer = setTimeout(complete, Number(plan.delay) * 2);
|
||||
else
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
const point = plan.points[index];
|
||||
let drawing = false;
|
||||
let encoded = false;
|
||||
let delayElapsed = plan.delay === null;
|
||||
const previous = video.oncanplay;
|
||||
const draw = () => {
|
||||
if (!live() || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2)
|
||||
if (!live() || !delayElapsed || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2)
|
||||
return;
|
||||
drawing = true;
|
||||
try {
|
||||
@@ -427,8 +466,16 @@ function createJob(tool, state) {
|
||||
video.oncanplay = draw;
|
||||
video.addEventListener("seeked", draw);
|
||||
video.currentTime = point.time;
|
||||
if (video.readyState >= 2 && !video.seeking)
|
||||
if (plan.delay !== null) {
|
||||
timer = setTimeout(() => {
|
||||
delayElapsed = true;
|
||||
draw();
|
||||
}, plan.delay);
|
||||
if (!live())
|
||||
clearTimeout(timer);
|
||||
} else if (video.readyState >= 2 && !video.seeking) {
|
||||
Promise.resolve().then(draw);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
@@ -552,11 +599,7 @@ function setupInput(tool, patch) {
|
||||
const option = Object.assign({}, tool.option, patch);
|
||||
const target = option.fileInput;
|
||||
tool.errorHandle(target instanceof Element, "The 'fileInput' is not a Element");
|
||||
for (const name of ["number", "width", "column", "begin", "end"])
|
||||
tool.errorHandle(typeof option[name] === "number", `The '${name}' is not a number`);
|
||||
option.number = clamp(option.number, 10, 1e3);
|
||||
option.width = clamp(option.width, 10, 1e3);
|
||||
option.column = clamp(option.column, 1, 1e3);
|
||||
normalizePolicy(option, (condition, message) => tool.errorHandle(condition, message));
|
||||
if (!current())
|
||||
throw cancellation("input setup superseded");
|
||||
const previous = inputs.get(tool);
|
||||
@@ -681,6 +724,7 @@ class ArtplayerToolThumbnail extends Emitter {
|
||||
}
|
||||
static get DEFAULTS() {
|
||||
return {
|
||||
delay: 300,
|
||||
number: 60,
|
||||
width: 160,
|
||||
height: 90,
|
||||
@@ -723,9 +767,11 @@ class ArtplayerToolThumbnail extends Emitter {
|
||||
inputChange(event) {
|
||||
if (stateFor(this).closed)
|
||||
return;
|
||||
const { resetInput } = thumbnailPolicy(this.option);
|
||||
const file = this.option.fileInput.files[0];
|
||||
this.loadVideo(file);
|
||||
event.target.value = "";
|
||||
if (resetInput)
|
||||
event.target.value = "";
|
||||
}
|
||||
loadVideo(file) {
|
||||
loadSource(this, file);
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:vconsole && yarn build:console && yarn build:site-notices && yarn build:test && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-ass.test.js test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-metadata.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail-registration.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js 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-exports.test.js test/vast.test.js test/vast-lifecycle.test.js test/vast-compatibility.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/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js test/asr-media-error.test.js test/thumbnail-cleanup-errors.test.js test/dash-cleanup-errors.test.js test/hls-example.test.js test/dash-example.test.js",
|
||||
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-ass.test.js test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-metadata.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail-registration.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js 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-exports.test.js test/vast.test.js test/vast-lifecycle.test.js test/vast-compatibility.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/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js test/asr-media-error.test.js test/thumbnail-cleanup-errors.test.js test/dash-cleanup-errors.test.js test/hls-example.test.js test/dash-example.test.js test/thumbnail-policy.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",
|
||||
@@ -85,7 +85,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-emitter.test.js 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 refactor/scripts/thumbnail-types.test.mjs test/thumbnail-cleanup-errors.test.js",
|
||||
"test:thumbnail": "node --test test/thumbnail-emitter.test.js 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 refactor/scripts/thumbnail-types.test.mjs test/thumbnail-cleanup-errors.test.js test/thumbnail-policy.test.js",
|
||||
"test:thumbnail-types-package": "node refactor/scripts/thumbnail-package-types.mjs",
|
||||
"test:auto-thumbnail": "node --test test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-metadata.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail-registration.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs refactor/scripts/auto-thumbnail-types.test.mjs",
|
||||
"test:vtt-thumbnail": "node --test test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs refactor/scripts/vtt-thumbnail-types.test.mjs",
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
The runtime responsibilities were separated under PKG-TOOL-THUMB-03 and are now
|
||||
strict TypeScript under PKG-TOOL-THUMB-04. Public declarations and isolated installed
|
||||
entrypoints now have checks; the historical default-policy decision and final
|
||||
integration/release gates remain open.
|
||||
entrypoints now have checks; final integration/release gates remain open.
|
||||
|
||||
The pending choice is now specified in the
|
||||
[compatibility proposal](../../refactor/thumbnail-compatibility-decision.md):
|
||||
The user approved the
|
||||
[compatibility decision](../../refactor/thumbnail-compatibility-decision.md) on 2026-09-15:
|
||||
recovered 3.5.31 defaults versus opt-in workspace 4.4.0 behavior, including the
|
||||
class-level DEFAULTS boundary. It is not approved or implemented; do not infer
|
||||
permission from the approved VAST or other package decisions.
|
||||
class-level DEFAULTS boundary. The root constructor now defaults to the published
|
||||
policy; `compatibility: 'workspace-4.4'` selects the previous workspace behavior.
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
@@ -18,6 +17,7 @@ permission from the approved VAST or other package decisions.
|
||||
| src/source.ts | File loading, native metadata/error listeners and source/thumbnail Blob URLs |
|
||||
| src/extraction.ts | Owned metadata wait and serial frame job, callback/error completion and cancellation |
|
||||
| src/input.ts | Option validation/clamps, file-input wrapper creation, listener registration/replacement and release |
|
||||
| src/policy.ts | Approved mode selection and numeric normalization; no DOM or timer ownership |
|
||||
| src/sheet.ts | Midpoint grid, canvas/footer geometry, temporary download anchor |
|
||||
| src/emitter.ts | Typed local adaptation of tiny-emitter; preserves on/once/emit/off behavior |
|
||||
| src/utils.ts | Pure clamp and filename helpers; unused sleep/serial helpers removed |
|
||||
@@ -30,8 +30,8 @@ Input/source use lifecycle state, extraction uses lifecycle/source, and sheet us
|
||||
only filename calculation. types.ts contains no imports or executable state.
|
||||
Input records live in a private WeakMap, so callers replacing `option` cannot
|
||||
lose ownership of generated inputs/listeners. Normal construction preserves the
|
||||
existing instance-field order and bound inputChange/ondrop methods. DEFAULTS,
|
||||
file/video event order, public method spelling and return values stay unchanged.
|
||||
existing instance-field order and bound inputChange/ondrop methods. Public method
|
||||
spelling and return values stay unchanged. DEFAULTS includes published delay: 300.
|
||||
|
||||
Input setup validates options before changing DOM or committed options. A wrapper
|
||||
receives one owned input; repeated setup with that wrapper reuses it. Replacement
|
||||
@@ -62,8 +62,9 @@ behavior, checks first-error identity across nested cleanup and verifies resourc
|
||||
|
||||
## Extraction and event ordering
|
||||
|
||||
Loading emits file before assigning video.src, then video synchronously as in
|
||||
workspace 4.4.0. A source generation prevents an older file callback or URL creation
|
||||
Loading emits file before assigning video.src, then video after the configured
|
||||
delay in the default published policy, or synchronously in workspace mode.
|
||||
A source generation prevents an older file callback or URL creation
|
||||
hook from overwriting a nested newer load. Successful replacement revokes old
|
||||
source URLs. Native errors report once for their source; stale listeners cannot
|
||||
fail the latest job. Existing sheet URLs stay available until the next sheet frame
|
||||
@@ -76,7 +77,15 @@ Preflight stays synchronous when metadata is ready. Canvas precedes processing=t
|
||||
updates observe true, done observes false and may start another job without old
|
||||
completion clearing the new state.
|
||||
|
||||
Frames wait for readiness/seek, accept readiness/Blob callbacks once and restore
|
||||
Published mode keeps configured height (clamped to 10-1000), waits delay after each
|
||||
seek and delay * 2 after the last update before done. Delay defaults to 300 and is
|
||||
clamped to 10-1000. Workspace mode derives height from video aspect ratio, ignores
|
||||
delay and resets the selected input value; published mode retains that value.
|
||||
Source notifications and extraction waits own their timers separately, so replacement,
|
||||
error and destroy cancel both without late events. A null policy delay means no
|
||||
timer; NaN retains the historical asynchronous browser timer behavior.
|
||||
|
||||
Frames wait for readiness/seek as well as any policy delay, accept readiness/Blob callbacks once and restore
|
||||
oncanplay only while still owning it. Seek/draw/encoding failures, null Blob and
|
||||
throwing callbacks settle the promise and detach job resources. Source replacement
|
||||
or destruction rejects with AbortError without an error event. The public promise
|
||||
@@ -117,14 +126,16 @@ Safari/WebKit evidence in 05. The independent tool example is
|
||||
`test/thumbnail-lifecycle.test.js` covers jobs, source cancellation, native errors,
|
||||
callback failures, reentrancy and private resources. Browser tests hold real PNG
|
||||
callbacks and replace a selected file during encoding; only the latest job may
|
||||
complete. Continue published declarations and installed consumers in 04; preserve
|
||||
old tests and the default delay/height boundary. The [public type guide](types/README.md)
|
||||
complete. Preserve old tests and the approved delay/height boundary. The new
|
||||
`test/thumbnail-policy.test.js` covers both modes, owned delays and cancellation;
|
||||
browser extraction checks both policies against the frozen historical bundles.
|
||||
The [public type guide](types/README.md)
|
||||
documents declaration ownership, module entries, events and installed consumers.
|
||||
See [task plan](../../refactor/plan.md) and [risk ledger](../../refactor/risks.json).
|
||||
|
||||
## Type and provenance boundaries
|
||||
|
||||
All eight executable source modules and the shared type module are checked with
|
||||
All nine executable source modules and the shared type module are checked with
|
||||
strict, noUncheckedIndexedAccess, noImplicitOverride, skipLibCheck=false and no
|
||||
ambient Node/test globals. Public fields use declare so TypeScript does not add
|
||||
early undefined properties or change constructor property order. File/URL/density
|
||||
|
||||
@@ -4,9 +4,30 @@ Thumbnail tool for ArtPlayer
|
||||
|
||||
Implementation and migration notes: [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
|
||||
The default constructor preserves published 3.5.31 behavior: fixed `height: 90`,
|
||||
`delay: 300` before the video event and each frame, and twice that delay before
|
||||
completion. Numeric height/delay values are clamped to 10-1000. File selection
|
||||
retains the input value. Existing methods, including `creatCanvas` and
|
||||
`creatScreenshotDate`, retain their names.
|
||||
|
||||
Consumers of the unpublished 4.4 workspace behavior should select it explicitly:
|
||||
|
||||
```js
|
||||
const tool = new ArtplayerToolThumbnail({
|
||||
fileInput: document.querySelector('input[type=file]'),
|
||||
compatibility: 'workspace-4.4',
|
||||
})
|
||||
```
|
||||
|
||||
This mode calculates height from the video aspect ratio, emits `video` synchronously,
|
||||
has no fixed extraction delays and clears the file input value. Class-level
|
||||
`DEFAULTS` always describes the published policy, including `delay`; it does not
|
||||
vary by instance. Both modes wait for frame readiness and cancel owned work on
|
||||
source replacement or destruction.
|
||||
|
||||
## Demo
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-tool-thumbnail/index.js&example=thumbnail)
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-tool-thumbnail/index.js&example=tool.thumbnail)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -31,4 +31,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,r){const o=this.e||(this.e={});let i=e(o,t)?o[t]:void 0;return i||(i=[],Object.defineProperty(o,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:r}),this}once(e,t,n){const r=this,o=t;let i=!1;function s(...t){i||(i=!0,r.off(e,s),o.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const r=this.e||(this.e={}),o=(e(r,t)&&r[t]||[]).slice();for(let e=0;e<o.length;e+=1)o[e].fn.apply(o[e].ctx,n);return this}off(t,n){const r=this.e||(this.e={}),o=e(r,t)?r[t]:void 0,i=[];if(o&&n)for(let e=0,s=o.length;e<s;e+=1)o[e].fn!==n&&o[e].fn._!==n&&i.push(o[e]);return i.length?r[t]=i:delete r[t],this}}const n=new WeakMap;function r(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function o(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const o of e)try{o()}catch(r){n||(n=!0,t=r)}if(n)throw t}function c(e,t="destroyed"){const n=r(e);return!n.closed&&(n.closed=!0,n.epoch++,n.job?.cancel(t),!0)}function a(e){const t=r(e),n=new Set([...t.sourceUrls,e.videoUrl]),o=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const r of new Set([t.video,e.video]))r&&c.push(()=>r.pause?.(),()=>r.removeAttribute?.("src"),()=>r.load?.(),()=>r.parentNode?.removeChild(r));s([...t.sourceListeners.splice(0),...c,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...o].map(e=>()=>i(t.thumbnailUrls,e))])}function l(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e,t){let n=t.epoch;const c=e.video;let a,u,d,h,p=!1,f=!1,m=()=>{};const b=[],y=new Promise((e,t)=>{a=e,u=t});y.catch(()=>{});const g=()=>!p&&!t.closed&&t.epoch===n&&t.job===h;function v(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),s([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{v()}catch(r){t||(t=r)}if(n)try{e.emit("error",t?.message)}catch(o){t=o}u(t)}}function U(t){if(!g())return;f=!0,e.processing=!0;let n=0;const o=()=>{if(!g())return;if(n===t.points.length)return void function(){if(g())try{v(),e.emit("done"),a()}catch(t){let r=t;try{e.emit("error",t?.message)}catch(n){r=n}u(r)}}();const s=t.points[n];let l=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!g()||l||c.seeking||void 0!==c.readyState&&c.readyState<2)){l=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,n){const o=r(e),s=URL.createObjectURL(t);if(o.thumbnailUrls.add(s),!n())return i(o.thumbnailUrls,s),null;const c=new Set([...o.thumbnailUrls,e.thumbnailUrl]);o.thumbnailUrl=s,e.thumbnailUrl=s;for(const r of c)r&&r!==s&&i(o.thumbnailUrls,r);return n()?s:null}(e,s,g);if(!c)return;e.emit("update",c,(n+1)/t.number),n++,Promise.resolve().then(o)}catch(c){w(c)}}})}catch(a){w(a)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(o)}function E(n=!1){if(g()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,a=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=l(i,0,n.duration),e.option.end=l(s||n.duration,i,n.duration),a(e.option.end>e.option.begin,"End time must be greater than the start time"),a(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,a(e.file&&n,"Please select the video file first"),a(!e.processing,"There is currently a task in progress, please wait a moment..."),a(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),g()||clearTimeout(d)}h={video:c,promise:y,ready:E,fail:w,cancel(e){w(o(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(n=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{g()&&w(new Error(`Unable to load video: media error ${c.error?.code||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeEventLisLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,o,n){const r=this.e||(this.e={});let i=e(r,t)?r[t]:void 0;return i||(i=[],Object.defineProperty(r,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:o,ctx:n}),this}once(e,t,o){const n=this,r=t;let i=!1;function s(...t){i||(i=!0,n.off(e,s),r.apply(o,t))}return s._=t,this.on(e,s,o)}emit(t,...o){const n=this.e||(this.e={}),r=(e(n,t)&&n[t]||[]).slice();for(let e=0;e<r.length;e+=1)r[e].fn.apply(r[e].ctx,o);return this}off(t,o){const n=this.e||(this.e={}),r=e(n,t)?n[t]:void 0,i=[];if(r&&o)for(let e=0,s=r.length;e<s;e+=1)r[e].fn!==o&&r[e].fn._!==o&&i.push(r[e]);return i.length?n[t]=i:delete n[t],this}}const o=new WeakMap;function n(e){return o.has(e)||o.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),o.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,o=!1;for(const r of e)try{r()}catch(n){o||(o=!0,t=n)}if(o)throw t}function c(e,t="destroyed"){const o=n(e);return!o.closed&&(o.closed=!0,o.epoch++,o.job?.cancel(t),!0)}function a(e){const t=n(e),o=new Set([...t.sourceUrls,e.videoUrl]),r=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const n of new Set([t.video,e.video]))n&&c.push(()=>n.pause?.(),()=>n.removeAttribute?.("src"),()=>n.load?.(),()=>n.parentNode?.removeChild(n));s([...t.sourceListeners.splice(0),...c,...[...o].map(e=>()=>i(t.sourceUrls,e)),...[...r].map(e=>()=>i(t.thumbnailUrls,e))])}function l(e,t,o){return Math.max(Math.min(e,Math.max(t,o)),Math.min(t,o))}function u(e){const t="workspace-4.4"===e.compatibility;return{delay:t?null:e.delay,aspectHeight:t,resetInput:t}}function d(e,t){let o=t.epoch;const c=e.video;let a,d,h,p,f=!1,m=!1,b=()=>{};const y=[],g=new Promise((e,t)=>{a=e,d=t});g.catch(()=>{});const v=()=>!f&&!t.closed&&t.epoch===o&&t.job===p;function w(){return!f&&(f=!0,t.job===p&&(t.job=null,e.processing=!1),s([()=>clearTimeout(h),b,...y.splice(0)]),!0)}function U(t,o=!0){if(!f){try{w()}catch(n){t||(t=n)}if(o)try{e.emit("error",t?.message)}catch(r){t=r}d(t)}}function E(){if(v())try{w(),e.emit("done"),a()}catch(t){let n=t;try{e.emit("error",t?.message)}catch(o){n=o}d(n)}}function T(t){if(!v())return;m=!0,e.processing=!0;let o=0;const r=()=>{if(!v())return;if(o===t.points.length)return void(null!==t.delay?h=setTimeout(E,2*Number(t.delay)):E());const s=t.points[o];let a=!1,l=!1,u=null===t.delay;const d=c.oncanplay,p=()=>{if(!(!v()||!u||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(b(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!v())return;t.canvas.toBlob(s=>{if(v()&&!l){l=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,o){const r=n(e),s=URL.createObjectURL(t);if(r.thumbnailUrls.add(s),!o())return i(r.thumbnailUrls,s),null;const c=new Set([...r.thumbnailUrls,e.thumbnailUrl]);r.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&i(r.thumbnailUrls,n);return o()?s:null}(e,s,v);if(!c)return;e.emit("update",c,(o+1)/t.number),o++,Promise.resolve().then(r)}catch(c){U(c)}}})}catch(d){U(d)}}};b=()=>{c.oncanplay===p&&(c.oncanplay=d),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,null!==t.delay?(h=setTimeout(()=>{u=!0,p()},t.delay),v()||clearTimeout(h)):c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){U(f)}};Promise.resolve().then(r)}function j(o=!1){if(v()&&!m)if(clearTimeout(h),!t.loading&&c.duration){m=!0;try{const t=function(e,t){const{video:o}=t,{width:n,number:r,begin:i,end:s}=e.option,c=u(e.option),a=c.aspectHeight?o.videoHeight/o.videoWidth*n:e.option.height,d=(o,n)=>{try{e.errorHandle(o,n)}catch(r){throw t.reported=r,t.wasReported=!0,r}};e.option.height=a,e.option.begin=l(i,0,o.duration),e.option.end=l(s||o.duration,i,o.duration),d(e.option.end>e.option.begin,"End time must be greater than the start time"),d(Number.isFinite(o.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=r/e.duration,d(e.file&&o,"Please select the video file first"),d(!e.processing,"There is currently a task in progress, please wait a moment..."),d(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const h=e.creatScreenshotDate(),p=e.creatCanvas(),f=p.getContext("2d");return e.emit("canvas",p),{width:n,height:a,number:r,points:h,canvas:p,context:f,delay:c.delay}}(e,p);T(t)}catch(n){if(U(n,!o&&!(p.wasReported&&n===p.reported)),o)throw n}}else h=setTimeout(()=>j(),1e3),v()Line truncated
|
||||
@@ -31,4 +31,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,r){const o=this.e||(this.e={});let i=e(o,t)?o[t]:void 0;return i||(i=[],Object.defineProperty(o,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:r}),this}once(e,t,n){const r=this,o=t;let i=!1;function s(...t){i||(i=!0,r.off(e,s),o.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const r=this.e||(this.e={}),o=(e(r,t)&&r[t]||[]).slice();for(let e=0;e<o.length;e+=1)o[e].fn.apply(o[e].ctx,n);return this}off(t,n){const r=this.e||(this.e={}),o=e(r,t)?r[t]:void 0,i=[];if(o&&n)for(let e=0,s=o.length;e<s;e+=1)o[e].fn!==n&&o[e].fn._!==n&&i.push(o[e]);return i.length?r[t]=i:delete r[t],this}}const n=new WeakMap;function r(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function o(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const o of e)try{o()}catch(r){n||(n=!0,t=r)}if(n)throw t}function c(e,t="destroyed"){var n;const o=r(e);return!o.closed&&(o.closed=!0,o.epoch++,null==(n=o.job)||n.cancel(t),!0)}function l(e){const t=r(e),n=new Set([...t.sourceUrls,e.videoUrl]),o=new Set([...t.thumbnailUrls,e.thumbnailUrl]),c=[];for(const r of new Set([t.video,e.video]))r&&c.push(()=>{var e;return null==(e=r.pause)?void 0:e.call(r)},()=>{var e;return null==(e=r.removeAttribute)?void 0:e.call(r,"src")},()=>{var e;return null==(e=r.load)?void 0:e.call(r)},()=>{var e;return null==(e=r.parentNode)?void 0:e.removeChild(r)});s([...t.sourceListeners.splice(0),...c,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...o].map(e=>()=>i(t.thumbnailUrls,e))])}function a(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e,t){let n=t.epoch;const c=e.video;let l,u,d,h,p=!1,f=!1,m=()=>{};const b=[],v=new Promise((e,t)=>{l=e,u=t});v.catch(()=>{});const y=()=>!p&&!t.closed&&t.epoch===n&&t.job===h;function g(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),s([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{g()}catch(r){t||(t=r)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(o){t=o}u(t)}}function U(t){if(!y())return;f=!0,e.processing=!0;let n=0;const o=()=>{if(!y())return;if(n===t.points.length)return void function(){if(y())try{g(),e.emit("done"),l()}catch(t){let r=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){r=n}u(r)}}();const s=t.points[n];let a=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!y()||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!y())return;t.canvas.toBlob(s=>{if(y()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,n){const o=r(e),s=URL.createObjectURL(t);if(o.thumbnailUrls.add(s),!n())return i(o.thumbnailUrls,s),null;const c=new Set([...o.thumbnailUrls,e.thumbnailUrl]);o.thumbnailUrl=s,e.thumbnailUrl=s;for(const r of c)r&&r!==s&&i(o.thumbnailUrls,r);return n()?s:null}(e,s,y);if(!c)return;e.emit("update",c,(n+1)/t.number),n++,Promise.resolve().then(o)}catch(c){w(c)}}})}catch(l){w(l)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(o)}function E(n=!1){if(y()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,l=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=a(i,0,n.duration),e.option.end=a(s||n.duration,i,n.duration),l(e.option.end>e.option.begin,"End time must be greater than the start time"),l(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,l(e.file&&n,"Please select the video file first"),l(!e.processing,"There is currently a task in progress, please wait a moment..."),l(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),y()||clearTimeout(d)}h={video:c,promise:v,ready:E,fail:w,cancel(e){w(o(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(n=t,!0)},t.job=h;tryLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";const e=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);class t{on(t,n,o){const r=this.e||(this.e={});let i=e(r,t)?r[t]:void 0;return i||(i=[],Object.defineProperty(r,t,{value:i,enumerable:!0,configurable:!0,writable:!0})),i.push({fn:n,ctx:o}),this}once(e,t,n){const o=this,r=t;let i=!1;function s(...t){i||(i=!0,o.off(e,s),r.apply(n,t))}return s._=t,this.on(e,s,n)}emit(t,...n){const o=this.e||(this.e={}),r=(e(o,t)&&o[t]||[]).slice();for(let e=0;e<r.length;e+=1)r[e].fn.apply(r[e].ctx,n);return this}off(t,n){const o=this.e||(this.e={}),r=e(o,t)?o[t]:void 0,i=[];if(r&&n)for(let e=0,s=r.length;e<s;e+=1)r[e].fn!==n&&r[e].fn._!==n&&i.push(r[e]);return i.length?o[t]=i:delete o[t],this}}const n=new WeakMap;function o(e){return n.has(e)||n.set(e,{closed:!1,epoch:0,inputEpoch:0,job:null,video:null,sourceUrl:null,thumbnailUrl:null,sourceUrls:new Set,thumbnailUrls:new Set,sourceListeners:[],loading:!1}),n.get(e)}function r(e){const t=new Error(`Thumbnail task cancelled: ${e}`);return t.name="AbortError",t}function i(e,t){t&&(e.delete(t),URL.revokeObjectURL(t))}function s(e){let t,n=!1;for(const r of e)try{r()}catch(o){n||(n=!0,t=o)}if(n)throw t}function l(e,t="destroyed"){var n;const r=o(e);return!r.closed&&(r.closed=!0,r.epoch++,null==(n=r.job)||n.cancel(t),!0)}function a(e){const t=o(e),n=new Set([...t.sourceUrls,e.videoUrl]),r=new Set([...t.thumbnailUrls,e.thumbnailUrl]),l=[];for(const o of new Set([t.video,e.video]))o&&l.push(()=>{var e;return null==(e=o.pause)?void 0:e.call(o)},()=>{var e;return null==(e=o.removeAttribute)?void 0:e.call(o,"src")},()=>{var e;return null==(e=o.load)?void 0:e.call(o)},()=>{var e;return null==(e=o.parentNode)?void 0:e.removeChild(o)});s([...t.sourceListeners.splice(0),...l,...[...n].map(e=>()=>i(t.sourceUrls,e)),...[...r].map(e=>()=>i(t.thumbnailUrls,e))])}function c(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function u(e){const t="workspace-4.4"===e.compatibility;return{delay:t?null:e.delay,aspectHeight:t,resetInput:t}}function d(e,t){let n=t.epoch;const l=e.video;let a,d,h,p,f=!1,m=!1,b=()=>{};const y=[],v=new Promise((e,t)=>{a=e,d=t});v.catch(()=>{});const g=()=>!f&&!t.closed&&t.epoch===n&&t.job===p;function w(){return!f&&(f=!0,t.job===p&&(t.job=null,e.processing=!1),s([()=>clearTimeout(h),b,...y.splice(0)]),!0)}function U(t,n=!0){if(!f){try{w()}catch(o){t||(t=o)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(r){t=r}d(t)}}function E(){if(g())try{w(),e.emit("done"),a()}catch(t){let o=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){o=n}d(o)}}function T(t){if(!g())return;m=!0,e.processing=!0;let n=0;const r=()=>{if(!g())return;if(n===t.points.length)return void(null!==t.delay?h=setTimeout(E,2*Number(t.delay)):E());const s=t.points[n];let a=!1,c=!1,u=null===t.delay;const d=l.oncanplay,p=()=>{if(!(!g()||!u||a||l.seeking||void 0!==l.readyState&&l.readyState<2)){a=!0;try{if(b(),t.context.drawImage(l,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!c){c=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const l=function(e,t,n){const r=o(e),s=URL.createObjectURL(t);if(r.thumbnailUrls.add(s),!n())return i(r.thumbnailUrls,s),null;const l=new Set([...r.thumbnailUrls,e.thumbnailUrl]);r.thumbnailUrl=s,e.thumbnailUrl=s;for(const o of l)o&&o!==s&&i(r.thumbnailUrls,o);return n()?s:null}(e,s,g);if(!l)return;e.emit("update",l,(n+1)/t.number),n++,Promise.resolve().then(r)}catch(l){U(l)}}})}catch(d){U(d)}}};b=()=>{l.oncanplay===p&&(l.oncanplay=d),l.removeEventListener("seeked",p)};try{l.oncanplay=p,l.addEventListener("seeked",p),l.currentTime=s.time,null!==t.delay?(h=setTimeout(()=>{u=!0,p()},t.delay),g()||clearTimeout(h)):l.readyState>=2&&!l.seeking&&Promise.resolve().then(p)}catch(f){U(f)}};Promise.resolve().then(r)}function j(n=!1){if(g()&&!m)if(clearTimeout(h),!t.loading&&l.duration){m=!0;try{const t=function(e,t){const{video:n}=t,{width:o,number:r,begin:i,end:s}=e.option,l=u(e.option),a=l.aspectHeight?n.videoHeight/n.videoWidth*o:e.option.height,d=(n,o)=>{try{e.errorHandle(n,o)}catch(r){throw t.reported=r,t.wasReported=!0,r}};e.option.height=a,e.option.begin=c(i,0,n.duration),e.option.end=c(s||n.duration,i,n.duration),d(e.option.end>e.option.begin,"End time must be greater than the start time"),d(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=r/e.duration,d(e.file&&n,"Please select the video file first"),d(!e.processing,"There is currently a task in progress, please wait a moment..."),d(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const h=e.creatScreenshotDate(),p=e.creatCanvas(),f=p.getContext("2d");return e.emiLine truncated
|
||||
@@ -160,12 +160,39 @@ function releaseMedia(tool) {
|
||||
...[...thumbnails].map((url) => () => revoke(state.thumbnailUrls, url))
|
||||
]);
|
||||
}
|
||||
function getFileName(name) {
|
||||
const nameArray = name.split(".");
|
||||
nameArray.pop();
|
||||
return nameArray.join(".");
|
||||
}
|
||||
function clamp(num, a, b) {
|
||||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
}
|
||||
function thumbnailPolicy(option) {
|
||||
const workspace = option.compatibility === "workspace-4.4";
|
||||
return { delay: workspace ? null : option.delay, aspectHeight: workspace, resetInput: workspace };
|
||||
}
|
||||
function normalizePolicy(option, validate) {
|
||||
validate(option.compatibility === void 0 || option.compatibility === "published-3.5" || option.compatibility === "workspace-4.4", "The compatibility option must be published-3.5 or workspace-4.4");
|
||||
const workspace = thumbnailPolicy(option).aspectHeight;
|
||||
const fields = workspace ? ["number", "width", "column", "begin", "end"] : ["delay", "number", "width", "height", "column", "begin", "end"];
|
||||
for (const name of fields)
|
||||
validate(typeof option[name] === "number", `The '${name}' is not a number`);
|
||||
option.number = clamp(option.number, 10, 1e3);
|
||||
option.width = clamp(option.width, 10, 1e3);
|
||||
option.column = clamp(option.column, 1, 1e3);
|
||||
if (!workspace) {
|
||||
option.delay = clamp(Number(option.delay), 10, 1e3);
|
||||
option.height = clamp(option.height, 10, 1e3);
|
||||
}
|
||||
}
|
||||
function loadSource(tool, file) {
|
||||
const state = stateFor(tool);
|
||||
if (!file || state.closed)
|
||||
return;
|
||||
const initialEpoch = state.epoch;
|
||||
const video = tool.video;
|
||||
const { delay } = thumbnailPolicy(tool.option);
|
||||
const support = video.canPlayType(file.type);
|
||||
tool.errorHandle(support === "maybe" || support === "probably", `Playback of this file format is not supported: ${file.type}`);
|
||||
if (state.closed || state.epoch !== initialEpoch)
|
||||
@@ -187,11 +214,15 @@ function loadSource(tool, file) {
|
||||
state.sourceListeners = listeners;
|
||||
let pendingCleanup = () => {
|
||||
};
|
||||
let notificationTimer;
|
||||
const cancelNotification = () => clearTimeout(notificationTimer);
|
||||
listeners.push(cancelNotification);
|
||||
let reported = false;
|
||||
const error = () => {
|
||||
if (!current() || reported || video.currentSrc && video.currentSrc !== url)
|
||||
return;
|
||||
reported = true;
|
||||
cancelNotification();
|
||||
state.loading = false;
|
||||
const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`);
|
||||
if (state.job)
|
||||
@@ -238,8 +269,19 @@ function loadSource(tool, file) {
|
||||
if (previous !== url)
|
||||
revoke(state.sourceUrls, previous);
|
||||
}
|
||||
if (current())
|
||||
tool.emit("video", video);
|
||||
if (current() && !reported) {
|
||||
if (delay !== null) {
|
||||
notificationTimer = setTimeout(() => {
|
||||
cancelNotification();
|
||||
if (current() && !reported)
|
||||
tool.emit("video", video);
|
||||
}, delay);
|
||||
if (!current() || reported)
|
||||
cancelNotification();
|
||||
} else {
|
||||
tool.emit("video", video);
|
||||
}
|
||||
}
|
||||
} catch (error2) {
|
||||
if (current()) {
|
||||
state.loading = false;
|
||||
@@ -270,18 +312,11 @@ function replaceThumbnail(tool, blob, live) {
|
||||
}
|
||||
return live() ? url : null;
|
||||
}
|
||||
function getFileName(name) {
|
||||
const nameArray = name.split(".");
|
||||
nameArray.pop();
|
||||
return nameArray.join(".");
|
||||
}
|
||||
function clamp(num, a, b) {
|
||||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
}
|
||||
function prepare(tool, job) {
|
||||
const { video } = job;
|
||||
const { width, number, begin, end } = tool.option;
|
||||
const height = video.videoHeight / video.videoWidth * width;
|
||||
const policy = thumbnailPolicy(tool.option);
|
||||
const height = policy.aspectHeight ? video.videoHeight / video.videoWidth * width : tool.option.height;
|
||||
const guard = (condition, message) => {
|
||||
try {
|
||||
tool.errorHandle(condition, message);
|
||||
@@ -305,7 +340,7 @@ function prepare(tool, job) {
|
||||
const canvas = tool.creatCanvas();
|
||||
const context = canvas.getContext("2d");
|
||||
tool.emit("canvas", canvas);
|
||||
return { width, height, number, points, canvas, context };
|
||||
return { width, height, number, points, canvas, context, delay: policy.delay };
|
||||
}
|
||||
function createJob(tool, state) {
|
||||
let epoch = state.epoch;
|
||||
@@ -381,15 +416,19 @@ function createJob(tool, state) {
|
||||
if (!live())
|
||||
return;
|
||||
if (index === plan.points.length) {
|
||||
complete();
|
||||
if (plan.delay !== null)
|
||||
timer = setTimeout(complete, Number(plan.delay) * 2);
|
||||
else
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
const point = plan.points[index];
|
||||
let drawing = false;
|
||||
let encoded = false;
|
||||
let delayElapsed = plan.delay === null;
|
||||
const previous = video.oncanplay;
|
||||
const draw = () => {
|
||||
if (!live() || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2)
|
||||
if (!live() || !delayElapsed || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2)
|
||||
return;
|
||||
drawing = true;
|
||||
try {
|
||||
@@ -427,8 +466,16 @@ function createJob(tool, state) {
|
||||
video.oncanplay = draw;
|
||||
video.addEventListener("seeked", draw);
|
||||
video.currentTime = point.time;
|
||||
if (video.readyState >= 2 && !video.seeking)
|
||||
if (plan.delay !== null) {
|
||||
timer = setTimeout(() => {
|
||||
delayElapsed = true;
|
||||
draw();
|
||||
}, plan.delay);
|
||||
if (!live())
|
||||
clearTimeout(timer);
|
||||
} else if (video.readyState >= 2 && !video.seeking) {
|
||||
Promise.resolve().then(draw);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
@@ -552,11 +599,7 @@ function setupInput(tool, patch) {
|
||||
const option = Object.assign({}, tool.option, patch);
|
||||
const target = option.fileInput;
|
||||
tool.errorHandle(target instanceof Element, "The 'fileInput' is not a Element");
|
||||
for (const name of ["number", "width", "column", "begin", "end"])
|
||||
tool.errorHandle(typeof option[name] === "number", `The '${name}' is not a number`);
|
||||
option.number = clamp(option.number, 10, 1e3);
|
||||
option.width = clamp(option.width, 10, 1e3);
|
||||
option.column = clamp(option.column, 1, 1e3);
|
||||
normalizePolicy(option, (condition, message) => tool.errorHandle(condition, message));
|
||||
if (!current())
|
||||
throw cancellation("input setup superseded");
|
||||
const previous = inputs.get(tool);
|
||||
@@ -681,6 +724,7 @@ class ArtplayerToolThumbnail extends Emitter {
|
||||
}
|
||||
static get DEFAULTS() {
|
||||
return {
|
||||
delay: 300,
|
||||
number: 60,
|
||||
width: 160,
|
||||
height: 90,
|
||||
@@ -723,9 +767,11 @@ class ArtplayerToolThumbnail extends Emitter {
|
||||
inputChange(event) {
|
||||
if (stateFor(this).closed)
|
||||
return;
|
||||
const { resetInput } = thumbnailPolicy(this.option);
|
||||
const file = this.option.fileInput.files[0];
|
||||
this.loadVideo(file);
|
||||
event.target.value = "";
|
||||
if (resetInput)
|
||||
event.target.value = "";
|
||||
}
|
||||
loadVideo(file) {
|
||||
loadSource(this, file);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { Cleanup, ExtractionJob, LifecycleState } from './types'
|
||||
import { cancellation, cleanupAll, stateFor } from './lifecycle'
|
||||
import { thumbnailPolicy } from './policy'
|
||||
import { replaceThumbnail } from './source'
|
||||
import { clamp } from './utils'
|
||||
|
||||
function prepare(tool: ArtplayerToolThumbnail, job: ExtractionJob) {
|
||||
const { video } = job
|
||||
const { width, number, begin, end } = tool.option
|
||||
const height = (video.videoHeight / video.videoWidth) * width
|
||||
const policy = thumbnailPolicy(tool.option)
|
||||
const height = policy.aspectHeight ? (video.videoHeight / video.videoWidth) * width : tool.option.height
|
||||
const guard = (condition: unknown, message: string) => {
|
||||
try {
|
||||
tool.errorHandle(condition, message)
|
||||
@@ -32,7 +34,7 @@ function prepare(tool: ArtplayerToolThumbnail, job: ExtractionJob) {
|
||||
const canvas = tool.creatCanvas()
|
||||
const context = canvas.getContext('2d')!
|
||||
tool.emit('canvas', canvas)
|
||||
return { width, height, number, points, canvas, context }
|
||||
return { width, height, number, points, canvas, context, delay: policy.delay }
|
||||
}
|
||||
|
||||
function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
|
||||
@@ -112,15 +114,19 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
|
||||
if (!live())
|
||||
return
|
||||
if (index === plan.points.length) {
|
||||
complete()
|
||||
if (plan.delay !== null)
|
||||
timer = setTimeout(complete, Number(plan.delay) * 2)
|
||||
else
|
||||
complete()
|
||||
return
|
||||
}
|
||||
const point = plan.points[index]!
|
||||
let drawing = false
|
||||
let encoded = false
|
||||
let delayElapsed = plan.delay === null
|
||||
const previous = video.oncanplay
|
||||
const draw = () => {
|
||||
if (!live() || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2))
|
||||
if (!live() || !delayElapsed || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2))
|
||||
return
|
||||
drawing = true
|
||||
try {
|
||||
@@ -160,8 +166,17 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
|
||||
video.oncanplay = draw
|
||||
video.addEventListener('seeked', draw)
|
||||
video.currentTime = point.time
|
||||
if (video.readyState >= 2 && !video.seeking)
|
||||
if (plan.delay !== null) {
|
||||
timer = setTimeout(() => {
|
||||
delayElapsed = true
|
||||
draw()
|
||||
}, plan.delay)
|
||||
if (!live())
|
||||
clearTimeout(timer)
|
||||
}
|
||||
else if (video.readyState >= 2 && !video.seeking) {
|
||||
Promise.resolve().then(draw)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
|
||||
@@ -3,6 +3,7 @@ import Emitter from './emitter'
|
||||
import { startExtraction } from './extraction'
|
||||
import { connectInput, releaseInput, setupInput } from './input'
|
||||
import { cleanupAll, closeState, releaseMedia, stateFor } from './lifecycle'
|
||||
import { thumbnailPolicy } from './policy'
|
||||
import { createSheet, downloadSheet, screenshotPoints } from './sheet'
|
||||
import { loadSource } from './source'
|
||||
|
||||
@@ -46,6 +47,7 @@ export default class ArtplayerToolThumbnail extends Emitter<ThumbnailEvents> {
|
||||
|
||||
static get DEFAULTS() {
|
||||
return {
|
||||
delay: 300,
|
||||
number: 60,
|
||||
width: 160,
|
||||
height: 90,
|
||||
@@ -94,9 +96,11 @@ export default class ArtplayerToolThumbnail extends Emitter<ThumbnailEvents> {
|
||||
inputChange(event: Event) {
|
||||
if (stateFor(this).closed)
|
||||
return
|
||||
const { resetInput } = thumbnailPolicy(this.option)
|
||||
const file = this.option.fileInput.files![0]
|
||||
this.loadVideo(file)
|
||||
;(event.target as HTMLInputElement).value = ''
|
||||
if (resetInput)
|
||||
(event.target as HTMLInputElement).value = ''
|
||||
}
|
||||
|
||||
loadVideo(file?: File | null) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { InputOptions } from './types'
|
||||
import { cancellation, stateFor } from './lifecycle'
|
||||
import { clamp } from './utils'
|
||||
import { normalizePolicy } from './policy'
|
||||
|
||||
type Registration = readonly [name: string, callback: EventListener]
|
||||
interface InputRecord {
|
||||
@@ -62,11 +62,7 @@ export function setupInput(tool: ArtplayerToolThumbnail, patch: InputOptions) {
|
||||
const option = Object.assign({}, tool.option, patch)
|
||||
const target = option.fileInput!
|
||||
tool.errorHandle(target instanceof Element, 'The \'fileInput\' is not a Element')
|
||||
for (const name of ['number', 'width', 'column', 'begin', 'end'])
|
||||
tool.errorHandle(typeof option[name] === 'number', `The '${name}' is not a number`)
|
||||
option.number = clamp(option.number, 10, 1000)
|
||||
option.width = clamp(option.width, 10, 1000)
|
||||
option.column = clamp(option.column, 1, 1000)
|
||||
normalizePolicy(option, (condition, message) => tool.errorHandle(condition, message))
|
||||
if (!current())
|
||||
throw cancellation('input setup superseded')
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { RuntimeOptions } from './types'
|
||||
import { clamp } from './utils'
|
||||
|
||||
export function thumbnailPolicy(option: RuntimeOptions) {
|
||||
const workspace = option.compatibility === 'workspace-4.4'
|
||||
return { delay: workspace ? null : option.delay, aspectHeight: workspace, resetInput: workspace }
|
||||
}
|
||||
|
||||
export function normalizePolicy(option: RuntimeOptions, validate: (condition: unknown, message: string) => void) {
|
||||
validate(option.compatibility === undefined || option.compatibility === 'published-3.5' || option.compatibility === 'workspace-4.4', 'The compatibility option must be published-3.5 or workspace-4.4')
|
||||
const workspace = thumbnailPolicy(option).aspectHeight
|
||||
const fields = workspace ? ['number', 'width', 'column', 'begin', 'end'] : ['delay', 'number', 'width', 'height', 'column', 'begin', 'end']
|
||||
for (const name of fields)
|
||||
validate(typeof option[name] === 'number', `The '${name}' is not a number`)
|
||||
option.number = clamp(option.number, 10, 1000)
|
||||
option.width = clamp(option.width, 10, 1000)
|
||||
option.column = clamp(option.column, 1, 1000)
|
||||
if (!workspace) {
|
||||
option.delay = clamp(Number(option.delay), 10, 1000)
|
||||
option.height = clamp(option.height, 10, 1000)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { Cleanup } from './types'
|
||||
import { cleanupAll, revoke, stateFor } from './lifecycle'
|
||||
import { thumbnailPolicy } from './policy'
|
||||
|
||||
export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
|
||||
const state = stateFor(tool)
|
||||
@@ -8,6 +9,7 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
|
||||
return
|
||||
const initialEpoch = state.epoch
|
||||
const video = tool.video
|
||||
const { delay } = thumbnailPolicy(tool.option)
|
||||
const support = video.canPlayType(file.type)
|
||||
tool.errorHandle(support === 'maybe' || support === 'probably', `Playback of this file format is not supported: ${file.type}`)
|
||||
if (state.closed || state.epoch !== initialEpoch)
|
||||
@@ -28,11 +30,15 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
|
||||
const listeners: Cleanup[] = []
|
||||
state.sourceListeners = listeners
|
||||
let pendingCleanup = () => {}
|
||||
let notificationTimer: ReturnType<typeof setTimeout> | undefined
|
||||
const cancelNotification = () => clearTimeout(notificationTimer)
|
||||
listeners.push(cancelNotification)
|
||||
let reported = false
|
||||
const error = () => {
|
||||
if (!current() || reported || (video.currentSrc && video.currentSrc !== url))
|
||||
return
|
||||
reported = true
|
||||
cancelNotification()
|
||||
state.loading = false
|
||||
const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`)
|
||||
if (state.job)
|
||||
@@ -79,8 +85,20 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
|
||||
if (previous !== url)
|
||||
revoke(state.sourceUrls, previous)
|
||||
}
|
||||
if (current())
|
||||
tool.emit('video', video)
|
||||
if (current() && !reported) {
|
||||
if (delay !== null) {
|
||||
notificationTimer = setTimeout(() => {
|
||||
cancelNotification()
|
||||
if (current() && !reported)
|
||||
tool.emit('video', video)
|
||||
}, delay)
|
||||
if (!current() || reported)
|
||||
cancelNotification()
|
||||
}
|
||||
else {
|
||||
tool.emit('video', video)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (current()) {
|
||||
|
||||
@@ -9,11 +9,15 @@ export interface SheetOptions {
|
||||
|
||||
export interface InputOptions extends Partial<SheetOptions> {
|
||||
fileInput?: Element
|
||||
delay?: number
|
||||
compatibility?: 'published-3.5' | 'workspace-4.4'
|
||||
[name: string]: unknown
|
||||
}
|
||||
|
||||
export interface RuntimeOptions extends SheetOptions {
|
||||
fileInput: HTMLInputElement
|
||||
delay?: number
|
||||
compatibility?: 'published-3.5' | 'workspace-4.4'
|
||||
[name: string]: unknown
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ The declaration describes the existing tool class, not an ArtPlayer plugin facto
|
||||
The previous workspace manifest pointed to a missing declaration. There is no
|
||||
recovered legacy declaration to claim as an exact TypeScript baseline.
|
||||
|
||||
| Entry | Declaration | Runtime |
|
||||
| --- | --- | --- |
|
||||
| Root ESM import | artplayer-tool-thumbnail.d.mts | Default constructor in dist/artplayer-tool-thumbnail.mjs |
|
||||
| Root require | artplayer-tool-thumbnail.d.cts | Direct constructor in dist/artplayer-tool-thumbnail.js |
|
||||
| Older Node resolution / TS 4.3 | artplayer-tool-thumbnail.d.ts | main points to the direct CommonJS constructor |
|
||||
| artplayer-tool-thumbnail/legacy | d.cts or typesVersions fallback | Direct legacy constructor; native ESM gets its default |
|
||||
| Browser script / editor | Generated docs/assets/ts declaration | ArtplayerToolThumbnail global |
|
||||
| Entry | Declaration | Runtime |
|
||||
| ------------------------------- | ------------------------------------ | -------------------------------------------------------- |
|
||||
| Root ESM import | artplayer-tool-thumbnail.d.mts | Default constructor in dist/artplayer-tool-thumbnail.mjs |
|
||||
| Root require | artplayer-tool-thumbnail.d.cts | Direct constructor in dist/artplayer-tool-thumbnail.js |
|
||||
| Older Node resolution / TS 4.3 | artplayer-tool-thumbnail.d.ts | main points to the direct CommonJS constructor |
|
||||
| artplayer-tool-thumbnail/legacy | d.cts or typesVersions fallback | Direct legacy constructor; native ESM gets its default |
|
||||
| Browser script / editor | Generated docs/assets/ts declaration | ArtplayerToolThumbnail global |
|
||||
|
||||
The d.ts file is the public source of truth. d.cts forwards its export assignment;
|
||||
d.mts exposes the default constructor and type-only named exports. No runtime
|
||||
@@ -44,7 +44,7 @@ and source replacement/destroy reject with AbortError. Error events usually cont
|
||||
strings, but callback throws can supply any message value, so narrow unknown first.
|
||||
|
||||
Source/declaration constructors are mutually checked in test/types/thumbnail-source.ts.
|
||||
Current and TS 4.3.5 consumers include ten invalid uses. Installed checks pack/install
|
||||
Current and TS 4.3.5 consumers include twelve invalid uses. Installed checks pack/install
|
||||
outside the workspace, offline with an unchanged frozen lock, verify every file
|
||||
and exercise native Node require/import without a DOM.
|
||||
|
||||
@@ -57,4 +57,9 @@ yarn test:browser test/browser/thumbnail-editor-types.spec.js
|
||||
|
||||
build:ts selects package declaration entries rather than auxiliary types and emits
|
||||
the uppercase global. Actual file extraction, old-core composition and the full
|
||||
demo remain separate checks. The 3.5.31 versus 4.4.0 default policy remains pending.
|
||||
demo remain separate checks. The approved default is published 3.5.31;
|
||||
`compatibility: 'workspace-4.4'` opts into the previous workspace behavior.
|
||||
`Compatibility` names these two modes. `DefaultOptions` extends `SheetOptions`
|
||||
with the numeric static delay; existing `SheetOptions` literals need no new field.
|
||||
`Option.delay` is optional and numeric; workspace mode ignores it, including
|
||||
explicit undefined. The public resolved option therefore keeps delay optional.
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default } from './artplayer-tool-thumbnail.cjs'
|
||||
export type { EventArgs, EventRegistry, Events, Listener, Option, ResolvedOption, ScreenshotPoint, SheetOptions } from './artplayer-tool-thumbnail.js'
|
||||
export type { Compatibility, DefaultOptions, EventArgs, EventRegistry, Events, Listener, Option, ResolvedOption, ScreenshotPoint, SheetOptions } from './artplayer-tool-thumbnail.js'
|
||||
@@ -2,7 +2,7 @@
|
||||
declare class ArtplayerToolThumbnail {
|
||||
constructor(option?: ArtplayerToolThumbnail.Option)
|
||||
|
||||
static readonly DEFAULTS: ArtplayerToolThumbnail.SheetOptions
|
||||
static readonly DEFAULTS: ArtplayerToolThumbnail.DefaultOptions
|
||||
static ondragover(event: DragEvent): void
|
||||
static creatVideo(): HTMLVideoElement
|
||||
|
||||
@@ -44,14 +44,25 @@ declare namespace ArtplayerToolThumbnail {
|
||||
end: number
|
||||
}
|
||||
|
||||
type Compatibility = 'published-3.5' | 'workspace-4.4'
|
||||
|
||||
interface DefaultOptions extends SheetOptions {
|
||||
delay: number
|
||||
}
|
||||
|
||||
/** fileInput must be a file input or an Element wrapper when constructing. */
|
||||
interface Option extends Partial<SheetOptions> {
|
||||
fileInput?: Element
|
||||
compatibility?: Compatibility
|
||||
/** Published mode delay in milliseconds, clamped to 10-1000. */
|
||||
delay?: number
|
||||
[name: string]: unknown
|
||||
}
|
||||
|
||||
interface ResolvedOption extends SheetOptions {
|
||||
fileInput: HTMLInputElement
|
||||
compatibility?: Compatibility
|
||||
delay?: number
|
||||
[name: string]: unknown
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
||||
# PKG-TOOL-THUMB-04 approved Thumbnail policy and migration completion
|
||||
|
||||
The user approved published 3.5.31 behavior as the default, with the unpublished
|
||||
workspace behavior explicitly selected through `compatibility: 'workspace-4.4'`.
|
||||
This completes the source/type migration task, including its earlier TypeScript,
|
||||
declaration, installed-entry and emitter checkpoints. It does not complete 05/06,
|
||||
physical-device evidence, formal reviews or publication readiness.
|
||||
|
||||
## Implementation
|
||||
|
||||
`src/policy.ts` selects and validates the policy. The public constructor restores
|
||||
`DEFAULTS.delay: 300` and published key order. Published mode validates/clamps
|
||||
height and delay to 10-1000, retains configured height and the selected input
|
||||
value, delays the video event and each frame, and waits twice the delay before
|
||||
done. Workspace mode keeps aspect-ratio height, synchronous video notification,
|
||||
no fixed extraction delays and input reset. The class DEFAULTS always represents
|
||||
the published policy; this boundary was explicitly approved.
|
||||
|
||||
Source and extraction continue owning their resources separately. Source changes,
|
||||
native errors and destroy cancel pending notification, frame and completion
|
||||
timers. Frame readiness and seek completion are required in addition to the
|
||||
published delay. Cancellation settles the Promise without late update/done or
|
||||
new URLs. NaN retains the old asynchronous timer boundary rather than being
|
||||
mistaken for the workspace no-timer mode. Existing public spellings, descriptors,
|
||||
custom fields, event payloads, synchronous validation and return values remain.
|
||||
|
||||
The authored declarations add optional compatibility/delay and exported
|
||||
Compatibility/DefaultOptions types. SheetOptions still accepts old literals
|
||||
without a delay field. Source and public constructors remain mutually assignable.
|
||||
The generated local Monaco declarations are regenerated; its real worker compiles
|
||||
the new mode/default types and executes constructor/event/cleanup code.
|
||||
|
||||
README, architecture and type ownership guides describe both policies. No new
|
||||
dependency, lockfile or version change is needed. `test:thumbnail` and `test:unit`
|
||||
now include the new policy regression suite. Bundles and docs copies were built
|
||||
through the repository build script; source remains authoritative.
|
||||
|
||||
## Verification
|
||||
|
||||
Node 24.21.0, Yarn Classic 1.22.22, Windows. Evidence:
|
||||
[thumbnail-policy-validation.json](../baselines/thumbnail-policy-validation.json).
|
||||
|
||||
- The initial new policy tests failed 6/7 against the previous implementation.
|
||||
Final `yarn test:thumbnail`: 136 passed, no failure/skip/cancellation, 14.968 s.
|
||||
This includes frozen historical tests and current/old TypeScript consumers.
|
||||
- Strict source tsc and scoped source/type/test/script lint pass. Five public
|
||||
compiler/resolution modes reject all 12 invalid calls. Native CommonJS/no-interop
|
||||
fixtures and both-direction source/declaration assignment pass.
|
||||
- Main/legacy/ESM build succeeds. Final external Yarn pack/install evidence is
|
||||
`.cache/thumbnail-package-types-IH4Ouy`: offline frozen reinstall, installed-file
|
||||
hashes, native require/import and 12 compiler cases pass. Five cases characterize
|
||||
the frozen workspace missing-entry defect; seven exercise candidate consumers.
|
||||
Installed legacy runtime/policy/lifecycle subset: 63/63, 2.770 s; the final
|
||||
legacy bytes are identical to that tested snapshot. The final packed files are
|
||||
checked against the working tree in the evidence recorder.
|
||||
- Three-engine source browser run: 90 passed, 0 failed/skipped/flaky, 66.423 s.
|
||||
Installed main browser run: 30 passed, 0 failed/skipped/flaky, 33.414 s.
|
||||
Chromium 153.0.8010.12 and Firefox 155.0 verify actual file selection, configured
|
||||
versus aspect height, PNG pixels, repeated extraction, URL release, held native
|
||||
encoding cancellation and latest-source-only updates. Published video/frame/done
|
||||
delays have lower-bound assertions; owned-timer tests verify exact scheduling.
|
||||
- Windows WebKit 26.6 cannot decode the tested native Blob URLs (error 4), while
|
||||
HTTP controls load. There are 23 annotated capability-control source cases and
|
||||
10 installed cases. These are not successful extraction or physical Safari
|
||||
evidence. Other passing historical tests also include exact old-defect controls.
|
||||
|
||||
The first broad runtime attempt exposed fixtures that assumed no fixed delays;
|
||||
those now explicitly select workspace mode. A waiting worker was stopped and its
|
||||
unfinished run is not counted as passing. An initial strict error on optional
|
||||
delay was fixed through numeric normalization after runtime validation.
|
||||
|
||||
The first browser run had 89 passes and one Chromium failure: selecting an
|
||||
identical file while published mode retains input.value did not trigger change,
|
||||
so starting a second job correctly rejected it. The replacement test now clears
|
||||
selection before choosing the file again. Both modes retain the original strict
|
||||
AbortError/latest-source assertions. Final reports and the initial trace are
|
||||
archived separately; no retry, weaker assertion or production input reset hides
|
||||
that failure.
|
||||
|
||||
## Status and continuation
|
||||
|
||||
PKG-TOOL-THUMB-04 is done; THUMB-COMPAT-01 is resolved for the approved default
|
||||
policy and migration boundary. No complete original npm archive or historical
|
||||
declaration has been recovered, and 05/06 retain integration/distribution work.
|
||||
Next implementation work includes the recorded installed Danmuku CPU-gap failure
|
||||
and remaining package/release preparation. Finish implementation and required
|
||||
tests, then await user direction for reviews; do not push or publish.
|
||||
|
||||
Rollback requires reverting this task's source, declarations, generated outputs,
|
||||
tests and documentation together. Returning to the prior workspace default would
|
||||
also reverse the approved compatibility decision and must not be done silently.
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 282 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 43 / doing 21 / blocked 0 / done 218 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 43 / doing 20 / blocked 0 / done 219 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -396,7 +396,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, PKG-TOOL-THUMB-07 | 补齐真实 API 声明,自有 emitter/utils TS 化 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证 | H | doing |
|
||||
| PKG-TOOL-THUMB-04 | artplayer-tool-thumbnail<br>迁移自有源码和公开类型 | PKG-TOOL-THUMB-03, ENG-04, ENG-06, CORE-07, PKG-TOOL-THUMB-07 | 补齐真实 API 声明,自有 emitter/utils TS 化 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证 | H | done |
|
||||
| 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 |
|
||||
| PKG-TOOL-THUMB-07 | artplayer-tool-thumbnail<br>恢复清理和销毁中的原始异常传播 | PKG-TOOL-THUMB-03 | 清理记录与异常值分离,保留首个 falsy 抛出值并继续释放资源;历史对照、真实DOM、分发验证及维护记录 | 恢复旧 destroy 监听器同步异常语义,0/false/undefined/NaN等不被吞掉或覆盖;后续清理、重复销毁与正常抽帧回归通过,默认兼容决策不受影响 | M | done |
|
||||
@@ -673,7 +673,7 @@
|
||||
- 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) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md) [记录](baselines/thumbnail-emitter-validation.json) [记录](thumbnail-compatibility-decision.md) [记录](changes/2026-09-15-PKG-TOOL-THUMB-04-policy-proposal.md)
|
||||
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md) [记录](baselines/thumbnail-emitter-validation.json) [记录](thumbnail-compatibility-decision.md) [记录](changes/2026-09-15-PKG-TOOL-THUMB-04-policy-proposal.md) [记录](changes/2026-09-15-PKG-TOOL-THUMB-04-approved-policy.md) [记录](baselines/thumbnail-policy-validation.json)
|
||||
- SITE-01: [记录](site-inventory.md) [记录](baselines/site-inventory.json) [记录](baselines/site-provenance.json) [记录](baselines/demo-additions.json) [记录](changes/2026-09-14-SITE-01-site-inventory.md) [记录](baselines/site-inventory-validation.json)
|
||||
- SITE-SMOKE-01: [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md) [记录](baselines/docs-smoke-validation.json) [记录](scripts/docs-smoke.test.mjs)
|
||||
- SITE-02: [记录](changes/2026-09-14-SITE-02-editor-declarations.md) [记录](baselines/editor-declarations-validation.json) [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md)
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-TOOL-THUMB-04 Thumbnail 策略与源码迁移完成
|
||||
|
||||
用户批准已落实:默认保留 npm 3.5.31,显式 workspace-4.4 保留未发布工作区行为。
|
||||
策略集中到 policy.ts,延迟由 source/job 各自管理;类型、迁移说明与构建产物同步。
|
||||
136 项 Node 测试、严格类型、隔离安装 12 编译器案例通过;安装 legacy 63 项通过。
|
||||
三引擎源码浏览器 90 项、安装 main 30 项通过,其中分别 23/10 项为 WebKit
|
||||
能力对照,不能算截图提取或真实 Safari 通过。首次同文件重选夹具失败已修正并归档。
|
||||
见[完成记录](changes/2026-09-15-PKG-TOOL-THUMB-04-approved-policy.md)、
|
||||
[证据](baselines/thumbnail-policy-validation.json)。04 done,THUMB-COMPAT-01 resolved;
|
||||
05/06、设备与整体发布门槛保留。当前 219 done、20 doing、43 todo,共282项。
|
||||
下一步修复已捕获的 DANMUKU-INSTALLED-GAP-01,并继续剩余实施/版本准备。
|
||||
实施完成后等待用户指导复盘;未启动复盘、推送或发布。
|
||||
|
||||
## CI-01 完整 Windows WebKit 安装包回归
|
||||
|
||||
固定bb2c12df3,复用5e0447fc5打包的20包run-cRduve;77文件/845项:694通过、
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
| IFRAME-LIFE-01 | open / 已复现 | Iframe timestamp IDs and unowned pending requests/timers lack terminal cleanup | PKG-IFRAME-02, PKG-IFRAME-03, PKG-IFRAME-05 |
|
||||
| IFRAME-TRUST-01 | open / 已复现 | Iframe executable commit protocol accepts messages without source/origin validation | PKG-IFRAME-03, PKG-IFRAME-05 |
|
||||
| IFRAME-DIST-01 | open / 已复现 | Iframe tool name is unavailable at registry observation and old archive includes distinct helper exports | PKG-IFRAME-04, PKG-IFRAME-06 |
|
||||
| THUMB-COMPAT-01 | open / 已复现 | Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-05 |
|
||||
| THUMB-COMPAT-01 | resolved / 已复现 | Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-05 |
|
||||
| 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 | resolved / 源码/产物事实 | 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 |
|
||||
|
||||
+12
-4
@@ -4125,9 +4125,9 @@
|
||||
"PKG-TOOL-THUMB-04",
|
||||
"PKG-TOOL-THUMB-05"
|
||||
],
|
||||
"compatibleResolution": "Freeze both implementations and derive a concrete compatibility policy before changing defaults; no silent claim that identical calls have identical historical semantics.",
|
||||
"compatibleResolution": "User approved published 3.5.31 default behavior and explicit workspace-4.4 mode, including class DEFAULTS. Implemented as an owned policy without restoring historical resource defects.",
|
||||
"closureCriteria": "Explicit documented compatibility decision, old/candidate tests and migration examples covering delay, height, input reset and readiness.",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"evidence": [
|
||||
"refactor/baselines/thumbnail-contract.md",
|
||||
"refactor/baselines/thumbnail-release.json",
|
||||
@@ -4139,9 +4139,17 @@
|
||||
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md",
|
||||
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md",
|
||||
"refactor/thumbnail-compatibility-decision.md",
|
||||
"refactor/changes/2026-09-15-PKG-TOOL-THUMB-04-policy-proposal.md"
|
||||
"refactor/changes/2026-09-15-PKG-TOOL-THUMB-04-policy-proposal.md",
|
||||
"refactor/changes/2026-09-15-PKG-TOOL-THUMB-04-approved-policy.md",
|
||||
"refactor/baselines/thumbnail-policy-validation.json"
|
||||
],
|
||||
"workspaceState": "Concrete proposal prepared: recovered 3.5.31 runtime defaults and optional workspace-4.4 mode, explicitly including the class-level DEFAULTS difference, delay/height validation, event/frame timing and input reset. Frozen contract/behavior tests rerun 62/62. User choice remains pending; no mode/default change implemented or inferred from other package approvals."
|
||||
"workspaceState": "Default fixed/clamped height and delay, delayed video/frame/done and retained input value implemented; explicit workspace mode keeps aspect height, no policy delays and input reset. Source and installed browser checks plus strict current/old types pass within documented Windows WebKit Blob capability limits. Package integration/distribution and physical devices remain separate gates.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-15-PKG-TOOL-THUMB-04-approved-policy.md",
|
||||
"refactor/baselines/thumbnail-policy-validation.json",
|
||||
"refactor/thumbnail-compatibility-decision.md"
|
||||
],
|
||||
"resolutionRationale": "Explicit user approval and migration examples, frozen/default/workspace tests, source and installed real PNG checks on Chromium/Firefox, owned cancellation timing and declaration consumers close the conflicting-default policy. Windows WebKit native Blob limitation is recorded, not treated as successful extraction."
|
||||
},
|
||||
{
|
||||
"id": "THUMB-LIFE-01",
|
||||
|
||||
@@ -6,8 +6,8 @@ import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
|
||||
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
|
||||
import { verifyThumbnailContract } from './thumbnail-contract.mjs'
|
||||
import { hash, readMember } from './releases.mjs'
|
||||
import { verifyThumbnailContract } from './thumbnail-contract.mjs'
|
||||
|
||||
async function main() {
|
||||
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:thumbnail-types-package')
|
||||
@@ -20,7 +20,8 @@ async function main() {
|
||||
fs.mkdirSync(frozen)
|
||||
const prefix = 'packages/artplayer-tool-thumbnail/'
|
||||
for (const [file, text] of contract.source) {
|
||||
if (!file.startsWith(prefix)) continue
|
||||
if (!file.startsWith(prefix))
|
||||
continue
|
||||
const target = path.join(frozen, file.slice(prefix.length))
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
fs.writeFileSync(target, text)
|
||||
@@ -74,7 +75,8 @@ ${label === 'candidate' ? `const legacy = require('${pkg.name}/legacy'); assert.
|
||||
fs.writeFileSync(path.join(output, `${label}-runtime.log`), observation)
|
||||
runtime.push({ package: label, ...JSON.parse(observation.trim()) })
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
if (label === 'candidate') modes.push([ts, 'nodenext-cjs-no-interop'], [compat, 'node10-commonjs-no-interop'])
|
||||
if (label === 'candidate')
|
||||
modes.push([ts, 'nodenext-cjs-no-interop'], [compat, 'node10-commonjs-no-interop'])
|
||||
for (const [compiler, mode] of modes) {
|
||||
const next = mode.startsWith('nodenext')
|
||||
const noInterop = mode.endsWith('no-interop')
|
||||
@@ -92,15 +94,19 @@ ${label === 'candidate' ? `const legacy = require('${pkg.name}/legacy'); assert.
|
||||
}
|
||||
const diagnostics = compile(source)
|
||||
writeJson(path.join(output, `${label}-${compiler.version}-${mode}.json`), diagnostics)
|
||||
if (label === 'candidate') assert.deepEqual(diagnostics, [], `${compiler.version} ${mode}`)
|
||||
if (label === 'candidate') {
|
||||
assert.deepEqual(diagnostics, [], `${compiler.version} ${mode}`)
|
||||
}
|
||||
else {
|
||||
assert.deepEqual(diagnostics.map(item => item.code), [mode === 'node10-commonjs' ? 2307 : 7016], 'Only the exact frozen missing entry/declaration diagnostic is expected')
|
||||
}
|
||||
const negative = label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
|
||||
if (label === 'candidate') assert.equal(negative.length, noInterop ? 1 : 10)
|
||||
if (label === 'candidate')
|
||||
assert.equal(negative.length, noInterop ? 1 : 12)
|
||||
matrix.push({ package: label, compiler: compiler.version, mode, diagnostics, negative })
|
||||
}
|
||||
if (label === 'candidate') fs.cpSync(installed, path.join(output, 'installed-artifacts'), { recursive: true })
|
||||
if (label === 'candidate')
|
||||
fs.cpSync(installed, path.join(output, 'installed-artifacts'), { recursive: true })
|
||||
}
|
||||
finally { removeConsumer(consumer) }
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Public type and editor contract runner.
|
||||
import test from 'node:test'
|
||||
import { ESLint } from 'eslint'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { ESLint } from 'eslint'
|
||||
import { checkConsumer } from '../../scripts/typecheck.mjs'
|
||||
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
|
||||
import { checkConsumer } from '../../scripts/typecheck.mjs'
|
||||
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
const invalid = source => source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')
|
||||
@@ -15,7 +15,7 @@ test('Thumbnail public declarations check actual callbacks, custom events and in
|
||||
const source = fs.readFileSync('test/types/thumbnail.ts', 'utf8')
|
||||
for (const [compiler, mode] of modes) {
|
||||
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
|
||||
assert.equal(checkConsumer(compiler, mode, invalid(source)).length, 10, `${compiler.version} ${mode} negatives`)
|
||||
assert.equal(checkConsumer(compiler, mode, invalid(source)).length, 12, `${compiler.version} ${mode} negatives`)
|
||||
}
|
||||
const commonjs = fs.readFileSync('test/types/thumbnail-commonjs.cts', 'utf8')
|
||||
for (const [compiler, mode] of [[ts, 'nodenext-cjs'], [compat, 'node10-commonjs']]) {
|
||||
@@ -67,7 +67,7 @@ const bad = player.mediabunny?.levels;`
|
||||
assert.equal(checkPluginEditorDeclaration(code, core, invalid(consumer), compiler).length, 1)
|
||||
}
|
||||
assert.throws(() => generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny'), /Unsupported editor type re-export/)
|
||||
assert.throws(() => generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny', { './media': media + '\nexport interface Extra {}' }), /explicitly cover/)
|
||||
assert.throws(() => generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny', { './media': `${media}\nexport interface Extra {}` }), /explicitly cover/)
|
||||
const common = fs.readFileSync('docs/assets/js/common.js', 'utf8')
|
||||
assert(common.includes('./assets/ts/artplayer-tool-thumbnail.d.ts'))
|
||||
assert(!common.includes('./assets/ts/media.d.ts'))
|
||||
|
||||
+4
-2
@@ -4238,7 +4238,7 @@
|
||||
"CORE-07",
|
||||
"PKG-TOOL-THUMB-07"
|
||||
],
|
||||
"status": "doing",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "补齐真实 API 声明,自有 emitter/utils TS 化",
|
||||
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证",
|
||||
@@ -4250,7 +4250,9 @@
|
||||
"changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md",
|
||||
"baselines/thumbnail-emitter-validation.json",
|
||||
"thumbnail-compatibility-decision.md",
|
||||
"changes/2026-09-15-PKG-TOOL-THUMB-04-policy-proposal.md"
|
||||
"changes/2026-09-15-PKG-TOOL-THUMB-04-policy-proposal.md",
|
||||
"changes/2026-09-15-PKG-TOOL-THUMB-04-approved-policy.md",
|
||||
"baselines/thumbnail-policy-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Thumbnail 默认行为兼容决策(待用户确认)
|
||||
# Thumbnail 默认行为兼容决策(已批准,已实施)
|
||||
|
||||
状态:proposal,尚未批准或实施。责任任务 PKG-TOOL-THUMB-04,风险 THUMB-COMPAT-01。
|
||||
本文件把已有待答问题具体化;不把其他包的兼容授权推广到本工具。
|
||||
状态:approved,源码、类型及本机验证完成。责任任务 PKG-TOOL-THUMB-04,风险 THUMB-COMPAT-01。
|
||||
2026-09-15 用户明确批准:“接受:npm 行为默认,工作区行为显式选择(推荐)”。
|
||||
下列方案现在可直接实施,无需再次询问;批准不代替源码、类型和浏览器验证。
|
||||
实施结果见 [完成记录](changes/2026-09-15-PKG-TOOL-THUMB-04-approved-policy.md);
|
||||
05/06、设备和发布门槛仍独立保留,Windows WebKit 能力对照不计截图提取通过。
|
||||
|
||||
## 已复现的冲突
|
||||
|
||||
@@ -23,7 +26,7 @@
|
||||
变成 90 高;loadVideo 后同步注册 video 监听器在两版也会得到不同结果。
|
||||
不能通过 TS 重命名或内部拆文件同时满足这两种默认值和时序。
|
||||
|
||||
## 推荐方案:恢复已发布行为为默认,显式选择工作区模式
|
||||
## 已批准方案:恢复已发布行为为默认,显式选择工作区模式
|
||||
|
||||
1. 根构造器默认选择恢复的 3.5.31 行为,支持旧 delay 参数,DEFAULTS 恢复
|
||||
delay: 300;配置 height 保留原钳制,video 延迟、每帧/完成延迟及 input
|
||||
@@ -33,7 +36,7 @@
|
||||
显式 compatibility: 'published-3.5' 可表达默认选择。
|
||||
3. 类级 DEFAULTS 没有实例上下文,统一表示根默认的 3.5.31 配置。依赖工作区
|
||||
DEFAULTS 精确字段集合的代码需要明确选择/保存自己的配置;不能承诺它同时
|
||||
返回两套对象。这个差异也属于本次待批准范围。
|
||||
返回两套对象。这个差异也属于本次已批准范围。
|
||||
4. 保留已实现的 URL/监听器/定时器所有权、销毁与切源取消、错误结算、输入
|
||||
清理和 emitter 修复。不会恢复旧版错绑 this、陈旧回调、资源泄漏等缺陷。
|
||||
定时等待不取代真实帧就绪;修复慢解码导致错误截图的路径单独记录和验证。
|
||||
@@ -51,8 +54,8 @@ const workspaceTool = new ArtplayerToolThumbnail({
|
||||
})
|
||||
```
|
||||
|
||||
另一种可选决策是保留工作区 4.4.0 为默认,并接受已发布 3.5.31 用户需要选择
|
||||
兼容模式。这会把迁移成本放到已发布旧用户,不作为当前推荐。
|
||||
未选择的替代方案是保留工作区 4.4.0 为默认,并要求已发布 3.5.31 用户选择
|
||||
兼容模式;用户已经选择上面的 npm 默认方案。
|
||||
|
||||
## 批准后实施与验收
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ test('Monaco consumes Thumbnail global declarations and runs the actual typed co
|
||||
}))
|
||||
const model = api.editor.createModel(`
|
||||
const input = document.createElement('input'); input.type = 'file'; document.body.appendChild(input);
|
||||
const options: ArtplayerToolThumbnail.Option = { fileInput: input, number: 10 };
|
||||
const mode: ArtplayerToolThumbnail.Compatibility = 'workspace-4.4';
|
||||
const defaults: ArtplayerToolThumbnail.DefaultOptions = ArtplayerToolThumbnail.DEFAULTS;
|
||||
const options: ArtplayerToolThumbnail.Option = { fileInput: input, number: 10, compatibility: mode, delay: defaults.delay };
|
||||
const tool: ArtplayerToolThumbnail = new ArtplayerToolThumbnail(options);
|
||||
let last = 0;
|
||||
tool.on('custom', (value: number) => { last = value; });
|
||||
@@ -32,7 +34,7 @@ const video: HTMLVideoElement = tool.video;
|
||||
tool.destroy(); tool.destroy(); input.remove();
|
||||
function mediaTypes(player: artplayerProxyMediabunny.MediaBunnyPlayer): number | undefined { return player.mediabunny?.currentTime; }
|
||||
`, 'typescript', api.Uri.parse('file:///thumbnail-consumer.ts'))
|
||||
const invalid = api.editor.createModel("new ArtplayerToolThumbnail({ fileInput: '#input' });", 'typescript', api.Uri.parse('file:///thumbnail-invalid.ts'))
|
||||
const invalid = api.editor.createModel('new ArtplayerToolThumbnail({ fileInput: \'#input\' });', 'typescript', api.Uri.parse('file:///thumbnail-invalid.ts'))
|
||||
try {
|
||||
const worker = await (await types.getTypeScriptWorker())(model.uri, invalid.uri)
|
||||
const syntax = await worker.getSyntacticDiagnostics(model.uri.toString())
|
||||
|
||||
@@ -12,7 +12,7 @@ test('Thumbnail input ownership replaces real DOM listeners and generated inputs
|
||||
await page.addScriptTag({ content: implementation.code })
|
||||
await page.evaluate(() => {
|
||||
window.inputEvents = []
|
||||
window.inputTool = new window.ArtplayerToolThumbnail({ fileInput: document.querySelector('#first') })
|
||||
window.inputTool = new window.ArtplayerToolThumbnail({ fileInput: document.querySelector('#first'), compatibility: 'workspace-4.4' })
|
||||
window.firstInput = window.inputTool.option.fileInput
|
||||
window.inputTool.on('file', file => window.inputEvents.push(file.name))
|
||||
window.inputTool.setup({ fileInput: document.querySelector('#first') })
|
||||
|
||||
@@ -27,7 +27,12 @@ async function capabilityControl(page, testInfo, lifecycle) {
|
||||
return true
|
||||
}
|
||||
|
||||
const implementations = [...thumbnailHistorical(), { ...await thumbnailCandidate(), inputOwnership: true, lifecycle: true }]
|
||||
const candidate = await thumbnailCandidate()
|
||||
const implementations = [
|
||||
...thumbnailHistorical(),
|
||||
{ ...candidate, inputOwnership: true, lifecycle: true, publishedPolicy: true },
|
||||
{ ...candidate, name: `${candidate.name}-workspace-policy`, inputOwnership: true, lifecycle: true, compatibility: 'workspace-4.4' },
|
||||
]
|
||||
for (const implementation of implementations) {
|
||||
test.describe(`Thumbnail tool ${implementation.name}`, () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
@@ -61,7 +66,7 @@ for (const implementation of implementations) {
|
||||
}
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
})
|
||||
await page.evaluate(() => {
|
||||
await page.evaluate((compatibility) => {
|
||||
window.created = []
|
||||
window.revoked = []
|
||||
const create = URL.createObjectURL.bind(URL)
|
||||
@@ -75,11 +80,11 @@ for (const implementation of implementations) {
|
||||
revoked.push(url)
|
||||
revoke(url)
|
||||
}
|
||||
window.tool = new ArtplayerToolThumbnail({ fileInput: document.querySelector('#input'), number: 10, width: 80, height: 30, column: 3, begin: 0, end: 20 })
|
||||
window.tool = new ArtplayerToolThumbnail({ fileInput: document.querySelector('#input'), number: 10, width: 80, height: 30, column: 3, begin: 0, end: 20, compatibility })
|
||||
window.observed = []
|
||||
for (const event of ['file', 'video', 'canvas', 'update', 'done', 'error', 'destroy'])
|
||||
tool.on(event, (...args) => observed.push({ event, progress: event === 'update' ? args[1] : null, processing: tool.processing }))
|
||||
})
|
||||
tool.on(event, (...args) => observed.push({ event, at: performance.now(), progress: event === 'update' ? args[1] : null, processing: tool.processing }))
|
||||
}, implementation.compatibility)
|
||||
await testInfo.attach('thumbnail-runtime', { contentType: 'application/json', body: JSON.stringify({ name: implementation.name, sha256: hash(implementation.code), recoveredIndividualFile: implementation.legacy, originalNpmArchive: false, media: path.relative(process.cwd(), sample) }) })
|
||||
})
|
||||
|
||||
@@ -117,12 +122,20 @@ for (const implementation of implementations) {
|
||||
return { width: image.width, height: image.height, sourceWidth, sourceHeight, cellHeight: tool.option.height, mime: blob.type, bytes: blob.size, nonBlack, initialEvents, events: observed, first, second, created, revoked, connected: tool.video.isConnected, retiredReadyState: tool.video.readyState, retiredSrc: tool.video.getAttribute('src') }
|
||||
})
|
||||
expect(result.width).toBe(240)
|
||||
expect(result.cellHeight).toBe(implementation.legacy ? 30 : result.sourceHeight / result.sourceWidth * 80)
|
||||
expect(result.cellHeight).toBe(implementation.legacy || implementation.publishedPolicy ? 30 : result.sourceHeight / result.sourceWidth * 80)
|
||||
expect(result.height).toBe(Math.trunc(result.cellHeight * 4 + 30))
|
||||
expect(result.mime).toBe('image/png')
|
||||
expect(result.bytes).toBeGreaterThan(100)
|
||||
expect(result.nonBlack).toBeGreaterThan(100)
|
||||
expect(result.initialEvents.filter(item => item.event === 'update').map(item => item.progress)).toEqual(Array.from({ length: 10 }, (_, i) => (i + 1) / 10))
|
||||
if (implementation.publishedPolicy) {
|
||||
const events = result.initialEvents
|
||||
expect(events.find(item => item.event === 'video').at - events.find(item => item.event === 'file').at).toBeGreaterThanOrEqual(298)
|
||||
const frames = events.filter(item => item.event === 'canvas' || item.event === 'update')
|
||||
for (let index = 1; index < frames.length; index++)
|
||||
expect(frames[index].at - frames[index - 1].at).toBeGreaterThanOrEqual(298)
|
||||
expect(events.find(item => item.event === 'done').at - frames.at(-1).at).toBeGreaterThanOrEqual(598)
|
||||
}
|
||||
expect(result.events.filter(item => item.event === 'done')).toHaveLength(2)
|
||||
expect(result.first).not.toBe(result.second)
|
||||
expect([...result.created].sort()).toEqual([...result.revoked].sort())
|
||||
@@ -177,7 +190,7 @@ for (const implementation of implementations) {
|
||||
expect(result.revoked).toEqual(implementation.lifecycle ? result.created : [result.created[1]])
|
||||
expect(result.revoked.includes(failed.first)).toBe(!!implementation.lifecycle)
|
||||
expect(result.repeated).toBe(implementation.inputOwnership ? undefined : 'NotFoundError')
|
||||
expect(result.value === '').toBe(!implementation.legacy)
|
||||
expect(result.value === '').toBe(!(implementation.legacy || implementation.publishedPolicy))
|
||||
await testInfo.attach('thumbnail-media-failure', { contentType: 'application/json', body: JSON.stringify({ failed, result }) })
|
||||
})
|
||||
|
||||
@@ -255,6 +268,8 @@ for (const implementation of implementations) {
|
||||
})
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => typeof window.releaseFirst)).toBe('function')
|
||||
// Published policy retains the value; selecting the same file need not fire change.
|
||||
await page.locator('#input').setInputFiles([])
|
||||
await page.locator('#input').setInputFiles(sample)
|
||||
const result = await page.evaluate(async () => {
|
||||
const source = tool.videoUrl
|
||||
|
||||
@@ -7,7 +7,7 @@ const implementation = await thumbnailCandidate()
|
||||
function setup(wrapper = false) {
|
||||
const env = thumbnailEnvironment(implementation)
|
||||
const target = new env.Element(wrapper ? 'div' : 'input')
|
||||
const instance = new env.Factory({ fileInput: target, number: 10, width: 20, height: 30, column: 3 })
|
||||
const instance = new env.Factory({ fileInput: target, number: 10, width: 20, height: 30, column: 3, compatibility: 'workspace-4.4' })
|
||||
return { ...env, target, instance }
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ test('Thumbnail input ownership preserves class methods, constructor fields and
|
||||
assert.equal(exported.default, undefined)
|
||||
assert.deepEqual(Object.keys(instance), ['processing', 'option', 'video', 'duration', 'inputChange', 'ondrop'])
|
||||
assert.deepEqual(Object.getOwnPropertyNames(Factory.prototype), ['constructor', 'ondrop', 'setup', 'inputChange', 'loadVideo', 'start', 'creatScreenshotDate', 'creatCanvas', 'download', 'errorHandle', 'destroy'])
|
||||
assert.deepEqual(Object.keys(Factory.DEFAULTS), ['number', 'width', 'height', 'column', 'begin', 'end'])
|
||||
assert.deepEqual(Object.keys(Factory.DEFAULTS), ['delay', 'number', 'width', 'height', 'column', 'begin', 'end'])
|
||||
assert.equal(instance.setup({ custom: 7 }), instance)
|
||||
assert.equal(instance.option.custom, 7)
|
||||
instance.destroy()
|
||||
|
||||
@@ -16,7 +16,8 @@ function observe(promise) {
|
||||
}
|
||||
function setup(ready = true) {
|
||||
const env = thumbnailEnvironment(implementation)
|
||||
env.instance = new env.Factory({ fileInput: new env.Element('input'), number: 10, width: 40, column: 3 })
|
||||
// These lifecycle fixtures retain the explicitly selected workspace timing.
|
||||
env.instance = new env.Factory({ fileInput: new env.Element('input'), number: 10, width: 40, column: 3, compatibility: 'workspace-4.4' })
|
||||
env.instance.loadVideo({ name: 'first.mp4', type: 'video/mp4' })
|
||||
if (ready) {
|
||||
env.instance.video.duration = 100
|
||||
@@ -409,7 +410,7 @@ test('Thumbnail URL creation reentrant load preserves the newer source and revok
|
||||
|
||||
test('Thumbnail start before the first file retains its legacy wait-for-selection behavior', async () => {
|
||||
const env = thumbnailEnvironment(implementation)
|
||||
env.instance = new env.Factory({ fileInput: new env.Element('input'), number: 10 })
|
||||
env.instance = new env.Factory({ fileInput: new env.Element('input'), number: 10, compatibility: 'workspace-4.4' })
|
||||
const state = observe(env.instance.start())
|
||||
env.instance.loadVideo({ name: 'first.mp4', type: 'video/mp4' })
|
||||
env.instance.video.duration = 100
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Approved runtime policy compatibility.
|
||||
import test from 'node:test'
|
||||
import { thumbnailCandidate, thumbnailEnvironment, thumbnailHistorical } from './helpers/thumbnail.js'
|
||||
|
||||
const candidate = await thumbnailCandidate()
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve))
|
||||
function create(option = {}) {
|
||||
const env = thumbnailEnvironment(candidate)
|
||||
env.input = new env.Element('input')
|
||||
env.tool = new env.Factory({ fileInput: env.input, number: 10, width: 40, height: 25, ...option })
|
||||
return env
|
||||
}
|
||||
function tick(env, delay) {
|
||||
const selected = [...env.timers].find(([, timer]) => Object.is(timer.delay, delay))
|
||||
assert(selected, `Expected owned timer for ${delay} ms`)
|
||||
env.timers.delete(selected[0])
|
||||
selected[1].callback()
|
||||
}
|
||||
function ready(env) {
|
||||
env.tool.video.duration = 100
|
||||
env.tool.video.readyState = 2
|
||||
env.tool.video.videoWidth = 160
|
||||
env.tool.video.videoHeight = 90
|
||||
env.tool.loadVideo({ name: 'sample.mp4', type: 'video/mp4' })
|
||||
for (const fn of env.tool.video.handlers.get('loadedmetadata'))
|
||||
fn()
|
||||
}
|
||||
|
||||
test('Thumbnail root defaults match the recovered published profile, including key order', () => {
|
||||
const actual = create()
|
||||
const old = thumbnailEnvironment(thumbnailHistorical().find(item => item.legacy))
|
||||
assert.deepEqual(Object.keys(actual.Factory.DEFAULTS), Object.keys(old.Factory.DEFAULTS))
|
||||
for (const key of Object.keys(old.Factory.DEFAULTS))
|
||||
assert.equal(actual.Factory.DEFAULTS[key], old.Factory.DEFAULTS[key])
|
||||
actual.tool.destroy()
|
||||
})
|
||||
|
||||
test('Thumbnail published policy validates and clamps delay/height while workspace preserves its inputs', () => {
|
||||
const published = create({ height: 2, delay: 4000 })
|
||||
assert.equal(published.tool.option.height, 10)
|
||||
assert.equal(published.tool.option.delay, 1000)
|
||||
assert.throws(() => published.tool.setup({ delay: 'bad' }), /delay.*number/)
|
||||
assert.throws(() => published.tool.setup({ height: 'bad' }), /height.*number/)
|
||||
assert.throws(() => published.tool.setup({ compatibility: 'unknown' }), /compatibility/)
|
||||
const workspace = create({ compatibility: 'workspace-4.4', height: 2, delay: 4000 })
|
||||
assert.equal(workspace.tool.option.height, 2)
|
||||
assert.equal(workspace.tool.option.delay, 4000)
|
||||
published.tool.destroy()
|
||||
workspace.tool.destroy()
|
||||
})
|
||||
|
||||
for (const compatibility of [undefined, 'published-3.5', 'workspace-4.4']) {
|
||||
test(`Thumbnail ${compatibility || 'default'} preserves file/video timing and input reset policy`, () => {
|
||||
const env = create({ compatibility, delay: 25 })
|
||||
const seen = []
|
||||
env.input.files = [{ name: 'sample.mp4', type: 'video/mp4' }]
|
||||
env.input.value = 'selected'
|
||||
env.tool.on('file', () => seen.push(['file', env.tool.video.src]))
|
||||
env.tool.on('video', video => seen.push(['video', video.src]))
|
||||
env.tool.inputChange({ target: env.input })
|
||||
const workspace = compatibility === 'workspace-4.4'
|
||||
assert.deepEqual(seen, workspace ? [['file', undefined], ['video', env.tool.videoUrl]] : [['file', undefined]])
|
||||
assert.equal(env.input.value, workspace ? '' : 'selected')
|
||||
if (!workspace) {
|
||||
tick(env, 25)
|
||||
assert.deepEqual(seen[1], ['video', env.tool.videoUrl])
|
||||
}
|
||||
env.tool.destroy()
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('Thumbnail default extraction keeps configured height and requires delay plus frame readiness', async () => {
|
||||
const env = create({ delay: 25 })
|
||||
ready(env)
|
||||
tick(env, 25)
|
||||
env.tool.video.seeking = true
|
||||
const work = env.tool.start()
|
||||
await flush()
|
||||
assert.equal(env.tool.option.height, 25)
|
||||
tick(env, 25)
|
||||
assert.equal(env.operations.filter(op => op.name === 'drawImage').length, 0)
|
||||
env.tool.video.seeking = false
|
||||
env.tool.video.oncanplay()
|
||||
await flush()
|
||||
assert.equal(env.operations.filter(op => op.name === 'drawImage').length, 1)
|
||||
env.tool.destroy()
|
||||
await assert.rejects(work, { name: 'AbortError' })
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
|
||||
test('Thumbnail delayed video notifications are cancelled by replacement, failure and destroy', () => {
|
||||
for (const action of ['replace', 'error', 'destroy']) {
|
||||
const env = create({ delay: 25 })
|
||||
const seen = []
|
||||
env.tool.on('video', () => seen.push(env.tool.file.name)).on('error', () => {})
|
||||
env.tool.loadVideo({ name: 'old.mp4', type: 'video/mp4' })
|
||||
const stale = [...env.timers.values()].find(timer => timer.delay === 25)
|
||||
assert(stale)
|
||||
if (action === 'replace') {
|
||||
env.tool.loadVideo({ name: 'new.mp4', type: 'video/mp4' })
|
||||
}
|
||||
else if (action === 'error') {
|
||||
for (const fn of env.tool.video.handlers.get('error'))
|
||||
fn()
|
||||
}
|
||||
else {
|
||||
env.tool.destroy()
|
||||
}
|
||||
stale.callback()
|
||||
assert.deepEqual(seen, [])
|
||||
if (action === 'replace') {
|
||||
tick(env, 25)
|
||||
assert.deepEqual(seen, ['new.mp4'])
|
||||
}
|
||||
env.tool.destroy()
|
||||
assert.equal(env.timers.size, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('Thumbnail numeric NaN delay keeps the historical asynchronous timer boundary', () => {
|
||||
const env = create({ delay: Number.NaN })
|
||||
let events = 0
|
||||
env.tool.on('video', () => events++)
|
||||
env.tool.loadVideo({ name: 'sample.mp4', type: 'video/mp4' })
|
||||
assert.equal(events, 0)
|
||||
tick(env, Number.NaN)
|
||||
assert.equal(events, 1)
|
||||
env.tool.destroy()
|
||||
})
|
||||
|
||||
for (const finish of ['complete', 'destroy', 'replace', 'error']) {
|
||||
test(`Thumbnail default final delay owns completion and ${finish} settlement`, async () => {
|
||||
const env = create({ delay: 25 })
|
||||
ready(env)
|
||||
tick(env, 25)
|
||||
let done = 0
|
||||
const updates = []
|
||||
env.tool.on('done', () => done++).on('update', (_, progress) => updates.push(progress))
|
||||
const work = env.tool.start()
|
||||
await flush()
|
||||
for (let frame = 0; frame < 10; frame++) {
|
||||
tick(env, 25)
|
||||
await flush()
|
||||
}
|
||||
assert.deepEqual(updates, Array.from({ length: 10 }, (_, index) => (index + 1) / 10))
|
||||
assert.equal(done, 0)
|
||||
assert.equal(env.tool.processing, true)
|
||||
const final = [...env.timers.values()].find(timer => timer.delay === 50)
|
||||
assert(final)
|
||||
if (finish === 'complete') {
|
||||
tick(env, 50)
|
||||
await work
|
||||
assert.equal(done, 1)
|
||||
}
|
||||
else {
|
||||
if (finish === 'destroy') {
|
||||
env.tool.destroy()
|
||||
}
|
||||
else if (finish === 'replace') {
|
||||
env.tool.loadVideo({ name: 'next.mp4', type: 'video/mp4' })
|
||||
}
|
||||
else {
|
||||
env.tool.on('error', () => {})
|
||||
for (const fn of [...env.tool.video.handlers.get('error')])
|
||||
fn()
|
||||
}
|
||||
await assert.rejects(work, finish === 'error' ? /Unable to load video/ : { name: 'AbortError' })
|
||||
final.callback()
|
||||
assert.equal(done, 0)
|
||||
}
|
||||
assert.equal(env.tool.processing, false)
|
||||
env.tool.destroy()
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('Thumbnail explicit workspace extraction retains aspect height with no policy timers', async () => {
|
||||
const env = create({ compatibility: 'workspace-4.4', delay: 25 })
|
||||
ready(env)
|
||||
const work = env.tool.start()
|
||||
await work
|
||||
assert.equal(env.tool.option.height, 22.5)
|
||||
assert.equal(env.operations.filter(op => op.name === 'drawImage').length, 10)
|
||||
assert.equal(env.timers.size, 0)
|
||||
env.tool.destroy()
|
||||
})
|
||||
@@ -6,9 +6,9 @@ import vm from 'node:vm'
|
||||
import { thumbnailCandidate, thumbnailEnvironment, thumbnailHistorical } from './helpers/thumbnail.js'
|
||||
|
||||
const candidate = await thumbnailCandidate()
|
||||
function create(implementation) {
|
||||
function create(implementation, option = {}) {
|
||||
const env = thumbnailEnvironment(implementation)
|
||||
return { ...env, tool: new env.Factory({ fileInput: new env.Element('input') }) }
|
||||
return { ...env, tool: new env.Factory({ fileInput: new env.Element('input'), ...option }) }
|
||||
}
|
||||
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 })
|
||||
@@ -39,7 +39,8 @@ test('Thumbnail typed facade preserves descriptors, lazy fields, default class a
|
||||
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))
|
||||
const published = thumbnailEnvironment(thumbnailHistorical().find(item => item.legacy))
|
||||
assert.deepEqual(Object.keys(Factory.DEFAULTS), Object.keys(published.Factory.DEFAULTS))
|
||||
assert(Number.isNaN(Factory.DEFAULTS.end))
|
||||
tool.destroy()
|
||||
})
|
||||
@@ -132,7 +133,7 @@ test('Thumbnail typed facade retains synchronous validation, option mutation and
|
||||
|
||||
test('Thumbnail error event preserves arbitrary thrown message values and Promise rejection identity', async () => {
|
||||
for (const failure of [{ message: 7 }, { message: { detail: 'failure' } }, 3, null]) {
|
||||
const { tool } = create(candidate)
|
||||
const { tool } = create(candidate, { compatibility: 'workspace-4.4' })
|
||||
tool.setup({ number: 10 })
|
||||
tool.file = { name: 'sample.mp4' }
|
||||
tool.video.duration = 100
|
||||
|
||||
+12
-2
@@ -1,4 +1,4 @@
|
||||
import type { EventArgs, Option, ScreenshotPoint } from 'artplayer-tool-thumbnail'
|
||||
import type { Compatibility, DefaultOptions, EventArgs, Option, ScreenshotPoint } from 'artplayer-tool-thumbnail'
|
||||
import Thumbnail from 'artplayer-tool-thumbnail'
|
||||
import Legacy from 'artplayer-tool-thumbnail/legacy'
|
||||
|
||||
@@ -32,8 +32,14 @@ tool.off('file').setup({ width: 100 }).download()
|
||||
tool.loadVideo(input.files?.[0])
|
||||
tool.loadVideo(null)
|
||||
const defaults: Thumbnail.SheetOptions = Thumbnail.DEFAULTS
|
||||
const publishedDefaults: DefaultOptions = Thumbnail.DEFAULTS
|
||||
const delay: number = publishedDefaults.delay
|
||||
const mode: Compatibility = 'workspace-4.4'
|
||||
tool.setup({ compatibility: mode, delay: undefined })
|
||||
legacy.setup({ compatibility: 'published-3.5', delay: 300, height: 25 })
|
||||
const sheet: Thumbnail.SheetOptions = { number: 10, width: 80, height: 25, column: 3, begin: 0, end: 20 }
|
||||
const end: void = tool.destroy()
|
||||
void [legacy, duration, file, url, pending, canvas, points, extension, defaults, end]
|
||||
void [legacy, duration, file, url, pending, canvas, points, extension, defaults, delay, sheet, end]
|
||||
|
||||
// @ts-expect-error Numeric width is required.
|
||||
tool.setup({ width: '80' })
|
||||
@@ -55,4 +61,8 @@ const earlyDensity: number = tool.density
|
||||
tool.on('error', (message: string) => message.length)
|
||||
// @ts-expect-error Built-in done does not carry a file.
|
||||
tool.emit('done', input.files?.[0])
|
||||
// @ts-expect-error Compatibility accepts only the two supported policies.
|
||||
tool.setup({ compatibility: 'latest' })
|
||||
// @ts-expect-error Published delay is a numeric option.
|
||||
tool.setup({ delay: '300' })
|
||||
void [wrongInput, wrongResult, WrongAlias, earlyDensity]
|
||||
Reference in new issue
Block a user