diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.js b/docs/compiled/artplayer-plugin-auto-thumbnail.js
index 6d6107829..371c7579c 100644
--- a/docs/compiled/artplayer-plugin-auto-thumbnail.js
+++ b/docs/compiled/artplayer-plugin-auto-thumbnail.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e){let t,n=!1;for(const r of e)try{r()}catch(i){n||(n=!0,t=i)}return{failed:n,failure:t}}function t(t,n){const i="function"===typeof n.requestVideoFrameCallback&&"function"===typeof n.cancelVideoFrameCallback;let r;function o(){const t=r;if(r=void 0,n.onloadeddata=null,n.onseeked=null,!t)return;const i=e([()=>{null!==t.timer&&clearTimeout(t.timer)},()=>{null!==t.frame&&n.cancelVideoFrameCallback(t.frame)}]);if(i.failed)throw i.failure}return t.own(o),(e,a)=>{if(o(),!t.active())return;const u={frame:null,timer:null,sought:!1,presented:!i,retries:0,sequence:0,started:!1};r=u;const d=()=>t.active()&&r===u,s=t.guard(()=>{d()&&u.sought&&u.presented&&(o(),t.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!i)return;const e=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&n.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=n.requestVideoFrameCallback(t.guard(()=>{d()&&e===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,s())}));d()&&e===u.sequence&&!o?u.frame=a:n.cancelVideoFrameCallback(a)})(),d()&&(n.currentTime=e)},l=t.guard(()=>{d()&&!u.started&&(u.started=!0,n.onloadeddata=null,n.onseeked=t.guard(()=>{if(!d()||n.seeking)return;const t=n.currentTime;if(d())if(!Number.isFinite(t)||Math.abs(t-e)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,s()}),c())});if(u.timer=setTimeout(t.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!i||n.readyState>=2;d()&&(f?l():n.onloadeddata=l)}}function n(e,n){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const r=i.duration,o=i.videoHeight,a=i.videoWidth,{height:u,canvasWidth:d,canvasHeight:s}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),i=Math.trunc(10*e.width),r=Math.trunc(n*Math.ceil(e.number/10));if(![n,i,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:i,canvasHeight:r}}(n,{duration:r,videoHeight:o,videoWidth:a});if(!e.active())return;i.width=a,i.height=o,i.style.width=`${a}px`,i.style.height=`${o}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=s;const l=c.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,i);let h=0;const m=e.guard(()=>{h>=n.number?e.dispose():f(r*h/n.number,e.guard(()=>{if(l.drawImage(i,h%10*n.width,Math.floor(h/10)*u,n.width,u),!e.active())return;let t=!1;c.toBlob(e.guard(i=>{t||(t=!0,e.publish(i,{height:u,column:10,number:n.number,width:n.width,scale:n.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(i.src=n.url))}return function(t){return async i=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(t,n){let i,r,o=!1;const a=new Set;function u(e){void 0!==e&&a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=i;i=void 0,e?.dispose()}return{start:function(){if(o)return;const d=i,s=[];let c=!1;const l={active:()=>!o&&!c&&i===l,own(t){if(c){const i=e([t]);i.failed&&n(i.failure)}else s.push(t)},dispose(){if(c)return;c=!0,i===l&&(i=void 0);const t=e(s.splice(0).reverse());t.failed&&n(t.failure)},fail(e){l.active()&&(l.dispose(),n(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(n){l.fail(n)}},publish(i,o){if(!l.active())return;if(!i)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(i);if(a.add(d),!l.active())return void u(d);const s=r;r=d;try{t({url:d,...o})}catch(c){r===d&&(r=void 0!==s&&a.has(s)?s:void 0);const t=e([()=>u(d)]);throw t.failed&&n(t.failure),c}u(s)}};return i=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const t=e([d,...[...a].map(e=>()=>u(e))]);r=void 0,t.failed&&n(t.failure)},get closed(){return o}}}(e=>{i.thumbnails=e},r),a=[],u=()=>{const e=o.start();e&&e.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(t,()=>i.option.url);e.active()&&n(e,r)})()},d=()=>{const t=a.splice(0),n=e([o.destroy,...t]);n.failed&&r(n.failure)};try{const e=[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]];for(const[t,n]of e){if(o.closed)break;a.push(()=>i.off(t,n)),i.on(t,n),o.closed&&i.off(t,n)}}catch(s){throw d(),s}return{name:"artplayerPluginAutoThumbnail"}}}});
+!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e){let t,i=!1;for(const r of e)try{r()}catch(n){i||(i=!0,t=n)}return{failed:i,failure:t}}function t(t,i){const n="function"===typeof i.requestVideoFrameCallback&&"function"===typeof i.cancelVideoFrameCallback;let r;function o(){const t=r;if(r=void 0,i.onloadeddata=null,i.onseeked=null,!t)return;const n=e([()=>{null!==t.timer&&clearTimeout(t.timer)},()=>{null!==t.frame&&i.cancelVideoFrameCallback(t.frame)}]);if(n.failed)throw n.failure}return t.own(o),(e,a)=>{if(o(),!t.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>t.active()&&r===u,c=t.guard(()=>{d()&&u.sought&&u.presented&&(o(),t.active()&&a())}),s=()=>{u.sought=!1,(()=>{if(!n)return;const e=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&i.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=i.requestVideoFrameCallback(t.guard(()=>{d()&&e===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,c())}));d()&&e===u.sequence&&!o?u.frame=a:i.cancelVideoFrameCallback(a)})(),d()&&(i.currentTime=e)},l=t.guard(()=>{d()&&!u.started&&(u.started=!0,i.onloadeddata=null,i.onseeked=t.guard(()=>{if(!d()||i.seeking)return;const t=i.currentTime;if(d())if(!Number.isFinite(t)||Math.abs(t-e)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");s()}else u.sought=!0,c()}),s())});if(u.timer=setTimeout(t.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||i.readyState>=2;d()&&(f?l():i.onloadeddata=l)}}function i(e,i){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const i of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[i]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,o=n.videoHeight,a=n.videoWidth,{height:u,canvasWidth:d,canvasHeight:c}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const i=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),r=Math.trunc(i*Math.ceil(e.number/10));if(![i,n,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:i,canvasWidth:n,canvasHeight:r}}(i,{duration:r,videoHeight:o,videoWidth:a});if(!e.active())return;n.width=a,n.height=o,n.style.width=`${a}px`,n.style.height=`${o}px`;const s=document.createElement("canvas");if(e.own(()=>{s.height=0}),e.own(()=>{s.width=0}),!e.active())return;if(s.width=d,!e.active())return;if(s.height=c,!e.active())return;const l=s.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,n);let h=0;const m=e.guard(()=>{h>=i.number?e.dispose():f(r*h/i.number,e.guard(()=>{if(l.drawImage(n,h%10*i.width,Math.floor(h/10)*u,i.width,u),!e.active())return;let t=!1;s.toBlob(e.guard(n=>{t||(t=!0,e.publish(n,{height:u,column:10,number:i.number,width:i.width,scale:i.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=i.url))}return function(t){return async n=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(t,i){let n,r,o=!1;const a=new Set;function u(e){void 0!==e&&a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=n;n=void 0,e?.dispose()}return{start:function(){if(o)return;const d=n,c=[];let s=!1;const l={active:()=>!o&&!s&&n===l,own(t){if(s){const n=e([t]);n.failed&&i(n.failure)}else c.push(t)},dispose(){if(s)return;s=!0,n===l&&(n=void 0);const t=e(c.splice(0).reverse());t.failed&&i(t.failure)},fail(e){l.active()&&(l.dispose(),i(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(i){l.fail(i)}},publish(n,o){if(!l.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!l.active())return void u(d);const c=r;r=d;try{t({url:d,...o})}catch(s){r===d&&(r=void 0!==c&&a.has(c)?c:void 0);const t=e([()=>u(d)]);throw t.failed&&i(t.failure),s}u(c)}};return n=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const t=e([d,...[...a].map(e=>()=>u(e))]);r=void 0,t.failed&&i(t.failure)},get closed(){return o}}}(e=>{n.thumbnails=e},r),a=[],u=()=>{const e=o.start();e&&e.guard(()=>{const r=function(e,t){const i={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(i.width))||i.width<=0||!Number.isFinite(Number(i.number))||i.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return i}(t,()=>n.option.url);e.active()&&i(e,r)})()},d=()=>{const t=a.splice(0),i=e([o.destroy,...t]);i.failed&&r(i.failure)};try{const e=[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]];for(const[t,i]of e){if(o.closed)break;a.push(()=>n.off(t,i)),n.on(t,i),o.closed&&n.off(t,i)}}catch(c){throw d(),c}return{name:"artplayerPluginAutoThumbnail"}}}});
diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js b/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js
index e024473a3..8373ad2aa 100644
--- a/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js
+++ b/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable,i=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;function o(e){let t,n=!1;for(const i of e)try{i()}catch(r){n||(n=!0,t=r)}return{failed:n,failure:t}}function a(e,a){let u,d,l=!1;const c=new Set;function s(e){void 0!==e&&c.delete(e)&&URL.revokeObjectURL(e)}function f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(l)return;const f=u,h=[];let m=!1;const v={active:()=>!l&&!m&&u===v,own(e){if(m){const t=o([e]);t.failed&&a(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=o(h.splice(0).reverse());e.failed&&a(e.failure)},fail(e){v.active()&&(v.dispose(),a(e))},guard:e=>(...t)=>{if(v.active())try{return e(...t)}catch(n){v.fail(n)}},publish(u,l){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(c.add(f),!v.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))n.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))r.call(o,a)&&i(e,a,o[a]);return e})({url:f},l))}catch(m){d===f&&(d=void 0!==h&&c.has(h)?h:void 0);const e=o([()=>s(f)]);throw e.failed&&a(e.failure),m}s(h)}};return u=v,null==f||f.dispose(),v},cancel:f,destroy:function(){if(l)return;l=!0;const e=o([f,...[...c].map(e=>()=>s(e))]);d=void 0,e.failed&&a(e.failure)},get closed(){return l}}}function u(e,t){const n="function"===typeof t.requestVideoFrameCallback&&"function"===typeof t.cancelVideoFrameCallback;let r;function i(){const e=r;if(r=void 0,t.onloadeddata=null,t.onseeked=null,!e)return;const n=o([()=>{null!==e.timer&&clearTimeout(e.timer)},()=>{null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(i),(o,a)=>{if(i(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>e.active()&&r===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(i(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const r=++u.sequence;u.presented=!1;const i=u.frame;if(u.frame=null,null!==i&&t.cancelVideoFrameCallback(i),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&r===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&r===u.sequence&&!o?u.frame=a:t.cancelVideoFrameCallback(a)})(),d()&&(t.currentTime=o)},s=e.guard(()=>{d()&&!u.started&&(u.started=!0,t.onloadeddata=null,t.onseeked=e.guard(()=>{if(!d()||t.seeking)return;const e=t.currentTime;if(d())if(!Number.isFinite(e)||Math.abs(e-o)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,l()}),c())});if(u.timer=setTimeout(e.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||t.readyState>=2;d()&&(f?s():t.onloadeddata=s)}}function d(e,t){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,i=n.videoHeight,o=n.videoWidth,{height:a,canvasWidth:d,canvasHeight:l}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),r=Math.trunc(10*e.width),i=Math.trunc(n*Math.ceil(e.number/10));if(![n,r,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:r,canvasHeight:i}}(t,{duration:r,videoHeight:i,videoWidth:o});if(!e.active())return;n.width=o,n.height=i,n.style.width=`${o}px`,n.style.height=`${i}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=l;const s=c.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=u(e,n);let h=0;const m=e.guard(()=>{h>=t.number?e.dispose():f(r*h/t.number,e.guard(()=>{if(s.drawImage(n,h%10*t.width,Math.floor(h/10)*a,t.width,a),!e.active())return;let r=!1;c.toBlob(e.guard(n=>{r||(r=!0,e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=t.url))}return function(e){return t=>{return n=null,r=null,i=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),r=a(e=>{t.thumbnails=e},n),i=[],u=()=>{const n=r.start();n&&n.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(e,()=>t.option.url);n.active()&&d(n,r)})()},l=()=>{const e=i.splice(0),t=o([r.destroy,...e]);t.failed&&n(t.failure)};try{const e=[["destroy",l],["restart",r.cancel],["video:loadedmetadata",u]];for(const[n,o]of e){if(r.closed)break;i.push(()=>t.off(n,o)),t.on(n,o),r.closed&&t.off(n,o)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(n){t(n)}},a=e=>{try{u(i.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(n,r)).next())});var n,r,i}}});
+!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable,i=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;function o(e){let t,n=!1;for(const i of e)try{i()}catch(r){n||(n=!0,t=r)}return{failed:n,failure:t}}function a(e,a){let u,d,l=!1;const c=new Set;function s(e){void 0!==e&&c.delete(e)&&URL.revokeObjectURL(e)}function f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(l)return;const f=u,h=[];let m=!1;const v={active:()=>!l&&!m&&u===v,own(e){if(m){const t=o([e]);t.failed&&a(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=o(h.splice(0).reverse());e.failed&&a(e.failure)},fail(e){v.active()&&(v.dispose(),a(e))},guard:e=>(...t)=>{if(v.active())try{return e(...t)}catch(n){v.fail(n)}},publish(u,l){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(c.add(f),!v.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))n.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))r.call(o,a)&&i(e,a,o[a]);return e})({url:f},l))}catch(m){d===f&&(d=void 0!==h&&c.has(h)?h:void 0);const e=o([()=>s(f)]);throw e.failed&&a(e.failure),m}s(h)}};return u=v,null==f||f.dispose(),v},cancel:f,destroy:function(){if(l)return;l=!0;const e=o([f,...[...c].map(e=>()=>s(e))]);d=void 0,e.failed&&a(e.failure)},get closed(){return l}}}function u(e,t){const n="function"===typeof t.requestVideoFrameCallback&&"function"===typeof t.cancelVideoFrameCallback;let r;function i(){const e=r;if(r=void 0,t.onloadeddata=null,t.onseeked=null,!e)return;const n=o([()=>{null!==e.timer&&clearTimeout(e.timer)},()=>{null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(i),(o,a)=>{if(i(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>e.active()&&r===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(i(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const r=++u.sequence;u.presented=!1;const i=u.frame;if(u.frame=null,null!==i&&t.cancelVideoFrameCallback(i),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&r===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&r===u.sequence&&!o?u.frame=a:t.cancelVideoFrameCallback(a)})(),d()&&(t.currentTime=o)},s=e.guard(()=>{d()&&!u.started&&(u.started=!0,t.onloadeddata=null,t.onseeked=e.guard(()=>{if(!d()||t.seeking)return;const e=t.currentTime;if(d())if(!Number.isFinite(e)||Math.abs(e-o)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,l()}),c())});if(u.timer=setTimeout(e.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||t.readyState>=2;d()&&(f?s():t.onloadeddata=s)}}function d(e,t){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,i=n.videoHeight,o=n.videoWidth,{height:a,canvasWidth:d,canvasHeight:l}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),r=Math.trunc(10*e.width),i=Math.trunc(n*Math.ceil(e.number/10));if(![n,r,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:r,canvasHeight:i}}(t,{duration:r,videoHeight:i,videoWidth:o});if(!e.active())return;n.width=o,n.height=i,n.style.width=`${o}px`,n.style.height=`${i}px`;const c=document.createElement("canvas");if(e.own(()=>{c.height=0}),e.own(()=>{c.width=0}),!e.active())return;if(c.width=d,!e.active())return;if(c.height=l,!e.active())return;const s=c.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=u(e,n);let h=0;const m=e.guard(()=>{h>=t.number?e.dispose():f(r*h/t.number,e.guard(()=>{if(s.drawImage(n,h%10*t.width,Math.floor(h/10)*a,t.width,a),!e.active())return;let r=!1;c.toBlob(e.guard(n=>{r||(r=!0,e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=t.url))}return function(e){return t=>{return n=null,r=null,i=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),r=a(e=>{t.thumbnails=e},n),i=[],u=()=>{const n=r.start();n&&n.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(e,()=>t.option.url);n.active()&&d(n,r)})()},l=()=>{const e=i.splice(0),t=o([r.destroy,...e]);t.failed&&n(t.failure)};try{const e=[["destroy",l],["restart",r.cancel],["video:loadedmetadata",u]];for(const[n,o]of e){if(r.closed)break;i.push(()=>t.off(n,o)),t.on(n,o),r.closed&&t.off(n,o)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(n){t(n)}},a=e=>{try{u(i.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(n,r)).next())});var n,r,i}}});
diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.mjs b/docs/compiled/artplayer-plugin-auto-thumbnail.mjs
index 948ba1870..1e4756b4f 100644
--- a/docs/compiled/artplayer-plugin-auto-thumbnail.mjs
+++ b/docs/compiled/artplayer-plugin-auto-thumbnail.mjs
@@ -301,10 +301,20 @@ function extract(job, config) {
video.style.width = `${videoWidth}px`;
video.style.height = `${videoHeight}px`;
const canvas = document.createElement("canvas");
+ job.own(() => {
+ canvas.height = 0;
+ });
+ job.own(() => {
+ canvas.width = 0;
+ });
if (!job.active())
return;
canvas.width = canvasWidth;
+ if (!job.active())
+ return;
canvas.height = canvasHeight;
+ if (!job.active())
+ return;
const ctx = canvas.getContext("2d");
if (!ctx)
throw new Error("Auto-thumbnail canvas context is unavailable");
diff --git a/package.json b/package.json
index 781ef1f63..50cd373a7 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js 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-merge.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-frames.test.js test/auto-thumbnail-lifecycle.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.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/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: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-merge.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-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.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.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/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: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 @@
"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-types-package": "node refactor/scripts/thumbnail-package-types.mjs",
- "test:auto-thumbnail": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs",
+ "test:auto-thumbnail": "node --test test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.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",
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
"test:multiple-subtitles": "node --test test/multiple-subtitles-merge.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 refactor/scripts/multiple-subtitles-contract.test.mjs refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
diff --git a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md
index 787a233b5..b8398255f 100644
--- a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md
+++ b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md
@@ -24,6 +24,13 @@ for video processing.
handlers, and the frame/encode sequence. Each valid draw is encoded before the
next seek. The final decoder is paused, cleared and reset; the final sheet URL
remains owned by the session until another usable sheet replaces it or destroy.
+ The private canvas is also job-owned: cancellation, failure and normal completion
+ reset both dimensions to zero, even if an encoding callback still retains it.
+ Width and height resets are independent cleanup actions, so one throwing setter
+ cannot prevent the other reset or decoder cleanup. Allocation checks job identity
+ after each dimension write; synchronous destruction cannot reallocate the sheet.
+ Previously encoded JPEG Blobs/URLs retain their independent bytes. This controls
+ canvas dimensions, not the browser's precise GPU/encoder reclamation time.
- `src/video.ts` creates and owns the hidden media element. It is attached to the
document root because detached media loses drawable pixels on Windows WebKit.
`visibility:hidden` preserves its rendered box; `display:none` and a 1px box do
diff --git a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js
index 6d6107829..371c7579c 100644
--- a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js
+++ b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e){let t,n=!1;for(const r of e)try{r()}catch(i){n||(n=!0,t=i)}return{failed:n,failure:t}}function t(t,n){const i="function"===typeof n.requestVideoFrameCallback&&"function"===typeof n.cancelVideoFrameCallback;let r;function o(){const t=r;if(r=void 0,n.onloadeddata=null,n.onseeked=null,!t)return;const i=e([()=>{null!==t.timer&&clearTimeout(t.timer)},()=>{null!==t.frame&&n.cancelVideoFrameCallback(t.frame)}]);if(i.failed)throw i.failure}return t.own(o),(e,a)=>{if(o(),!t.active())return;const u={frame:null,timer:null,sought:!1,presented:!i,retries:0,sequence:0,started:!1};r=u;const d=()=>t.active()&&r===u,s=t.guard(()=>{d()&&u.sought&&u.presented&&(o(),t.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!i)return;const e=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&n.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=n.requestVideoFrameCallback(t.guard(()=>{d()&&e===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,s())}));d()&&e===u.sequence&&!o?u.frame=a:n.cancelVideoFrameCallback(a)})(),d()&&(n.currentTime=e)},l=t.guard(()=>{d()&&!u.started&&(u.started=!0,n.onloadeddata=null,n.onseeked=t.guard(()=>{if(!d()||n.seeking)return;const t=n.currentTime;if(d())if(!Number.isFinite(t)||Math.abs(t-e)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,s()}),c())});if(u.timer=setTimeout(t.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!i||n.readyState>=2;d()&&(f?l():n.onloadeddata=l)}}function n(e,n){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const r=i.duration,o=i.videoHeight,a=i.videoWidth,{height:u,canvasWidth:d,canvasHeight:s}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),i=Math.trunc(10*e.width),r=Math.trunc(n*Math.ceil(e.number/10));if(![n,i,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:i,canvasHeight:r}}(n,{duration:r,videoHeight:o,videoWidth:a});if(!e.active())return;i.width=a,i.height=o,i.style.width=`${a}px`,i.style.height=`${o}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=s;const l=c.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,i);let h=0;const m=e.guard(()=>{h>=n.number?e.dispose():f(r*h/n.number,e.guard(()=>{if(l.drawImage(i,h%10*n.width,Math.floor(h/10)*u,n.width,u),!e.active())return;let t=!1;c.toBlob(e.guard(i=>{t||(t=!0,e.publish(i,{height:u,column:10,number:n.number,width:n.width,scale:n.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(i.src=n.url))}return function(t){return async i=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(t,n){let i,r,o=!1;const a=new Set;function u(e){void 0!==e&&a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=i;i=void 0,e?.dispose()}return{start:function(){if(o)return;const d=i,s=[];let c=!1;const l={active:()=>!o&&!c&&i===l,own(t){if(c){const i=e([t]);i.failed&&n(i.failure)}else s.push(t)},dispose(){if(c)return;c=!0,i===l&&(i=void 0);const t=e(s.splice(0).reverse());t.failed&&n(t.failure)},fail(e){l.active()&&(l.dispose(),n(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(n){l.fail(n)}},publish(i,o){if(!l.active())return;if(!i)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(i);if(a.add(d),!l.active())return void u(d);const s=r;r=d;try{t({url:d,...o})}catch(c){r===d&&(r=void 0!==s&&a.has(s)?s:void 0);const t=e([()=>u(d)]);throw t.failed&&n(t.failure),c}u(s)}};return i=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const t=e([d,...[...a].map(e=>()=>u(e))]);r=void 0,t.failed&&n(t.failure)},get closed(){return o}}}(e=>{i.thumbnails=e},r),a=[],u=()=>{const e=o.start();e&&e.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(t,()=>i.option.url);e.active()&&n(e,r)})()},d=()=>{const t=a.splice(0),n=e([o.destroy,...t]);n.failed&&r(n.failure)};try{const e=[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]];for(const[t,n]of e){if(o.closed)break;a.push(()=>i.off(t,n)),i.on(t,n),o.closed&&i.off(t,n)}}catch(s){throw d(),s}return{name:"artplayerPluginAutoThumbnail"}}}});
+!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e){let t,i=!1;for(const r of e)try{r()}catch(n){i||(i=!0,t=n)}return{failed:i,failure:t}}function t(t,i){const n="function"===typeof i.requestVideoFrameCallback&&"function"===typeof i.cancelVideoFrameCallback;let r;function o(){const t=r;if(r=void 0,i.onloadeddata=null,i.onseeked=null,!t)return;const n=e([()=>{null!==t.timer&&clearTimeout(t.timer)},()=>{null!==t.frame&&i.cancelVideoFrameCallback(t.frame)}]);if(n.failed)throw n.failure}return t.own(o),(e,a)=>{if(o(),!t.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>t.active()&&r===u,c=t.guard(()=>{d()&&u.sought&&u.presented&&(o(),t.active()&&a())}),s=()=>{u.sought=!1,(()=>{if(!n)return;const e=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&i.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=i.requestVideoFrameCallback(t.guard(()=>{d()&&e===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,c())}));d()&&e===u.sequence&&!o?u.frame=a:i.cancelVideoFrameCallback(a)})(),d()&&(i.currentTime=e)},l=t.guard(()=>{d()&&!u.started&&(u.started=!0,i.onloadeddata=null,i.onseeked=t.guard(()=>{if(!d()||i.seeking)return;const t=i.currentTime;if(d())if(!Number.isFinite(t)||Math.abs(t-e)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");s()}else u.sought=!0,c()}),s())});if(u.timer=setTimeout(t.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||i.readyState>=2;d()&&(f?l():i.onloadeddata=l)}}function i(e,i){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const i of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[i]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,o=n.videoHeight,a=n.videoWidth,{height:u,canvasWidth:d,canvasHeight:c}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const i=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),r=Math.trunc(i*Math.ceil(e.number/10));if(![i,n,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:i,canvasWidth:n,canvasHeight:r}}(i,{duration:r,videoHeight:o,videoWidth:a});if(!e.active())return;n.width=a,n.height=o,n.style.width=`${a}px`,n.style.height=`${o}px`;const s=document.createElement("canvas");if(e.own(()=>{s.height=0}),e.own(()=>{s.width=0}),!e.active())return;if(s.width=d,!e.active())return;if(s.height=c,!e.active())return;const l=s.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,n);let h=0;const m=e.guard(()=>{h>=i.number?e.dispose():f(r*h/i.number,e.guard(()=>{if(l.drawImage(n,h%10*i.width,Math.floor(h/10)*u,i.width,u),!e.active())return;let t=!1;s.toBlob(e.guard(n=>{t||(t=!0,e.publish(n,{height:u,column:10,number:i.number,width:i.width,scale:i.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=i.url))}return function(t){return async n=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(t,i){let n,r,o=!1;const a=new Set;function u(e){void 0!==e&&a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=n;n=void 0,e?.dispose()}return{start:function(){if(o)return;const d=n,c=[];let s=!1;const l={active:()=>!o&&!s&&n===l,own(t){if(s){const n=e([t]);n.failed&&i(n.failure)}else c.push(t)},dispose(){if(s)return;s=!0,n===l&&(n=void 0);const t=e(c.splice(0).reverse());t.failed&&i(t.failure)},fail(e){l.active()&&(l.dispose(),i(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(i){l.fail(i)}},publish(n,o){if(!l.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!l.active())return void u(d);const c=r;r=d;try{t({url:d,...o})}catch(s){r===d&&(r=void 0!==c&&a.has(c)?c:void 0);const t=e([()=>u(d)]);throw t.failed&&i(t.failure),s}u(c)}};return n=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const t=e([d,...[...a].map(e=>()=>u(e))]);r=void 0,t.failed&&i(t.failure)},get closed(){return o}}}(e=>{n.thumbnails=e},r),a=[],u=()=>{const e=o.start();e&&e.guard(()=>{const r=function(e,t){const i={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(i.width))||i.width<=0||!Number.isFinite(Number(i.number))||i.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return i}(t,()=>n.option.url);e.active()&&i(e,r)})()},d=()=>{const t=a.splice(0),i=e([o.destroy,...t]);i.failed&&r(i.failure)};try{const e=[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]];for(const[t,i]of e){if(o.closed)break;a.push(()=>n.off(t,i)),n.on(t,i),o.closed&&n.off(t,i)}}catch(c){throw d(),c}return{name:"artplayerPluginAutoThumbnail"}}}});
diff --git a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js
index e024473a3..8373ad2aa 100644
--- a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js
+++ b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable,i=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;function o(e){let t,n=!1;for(const i of e)try{i()}catch(r){n||(n=!0,t=r)}return{failed:n,failure:t}}function a(e,a){let u,d,l=!1;const c=new Set;function s(e){void 0!==e&&c.delete(e)&&URL.revokeObjectURL(e)}function f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(l)return;const f=u,h=[];let m=!1;const v={active:()=>!l&&!m&&u===v,own(e){if(m){const t=o([e]);t.failed&&a(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=o(h.splice(0).reverse());e.failed&&a(e.failure)},fail(e){v.active()&&(v.dispose(),a(e))},guard:e=>(...t)=>{if(v.active())try{return e(...t)}catch(n){v.fail(n)}},publish(u,l){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(c.add(f),!v.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))n.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))r.call(o,a)&&i(e,a,o[a]);return e})({url:f},l))}catch(m){d===f&&(d=void 0!==h&&c.has(h)?h:void 0);const e=o([()=>s(f)]);throw e.failed&&a(e.failure),m}s(h)}};return u=v,null==f||f.dispose(),v},cancel:f,destroy:function(){if(l)return;l=!0;const e=o([f,...[...c].map(e=>()=>s(e))]);d=void 0,e.failed&&a(e.failure)},get closed(){return l}}}function u(e,t){const n="function"===typeof t.requestVideoFrameCallback&&"function"===typeof t.cancelVideoFrameCallback;let r;function i(){const e=r;if(r=void 0,t.onloadeddata=null,t.onseeked=null,!e)return;const n=o([()=>{null!==e.timer&&clearTimeout(e.timer)},()=>{null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(i),(o,a)=>{if(i(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>e.active()&&r===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(i(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const r=++u.sequence;u.presented=!1;const i=u.frame;if(u.frame=null,null!==i&&t.cancelVideoFrameCallback(i),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&r===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&r===u.sequence&&!o?u.frame=a:t.cancelVideoFrameCallback(a)})(),d()&&(t.currentTime=o)},s=e.guard(()=>{d()&&!u.started&&(u.started=!0,t.onloadeddata=null,t.onseeked=e.guard(()=>{if(!d()||t.seeking)return;const e=t.currentTime;if(d())if(!Number.isFinite(e)||Math.abs(e-o)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,l()}),c())});if(u.timer=setTimeout(e.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||t.readyState>=2;d()&&(f?s():t.onloadeddata=s)}}function d(e,t){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,i=n.videoHeight,o=n.videoWidth,{height:a,canvasWidth:d,canvasHeight:l}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),r=Math.trunc(10*e.width),i=Math.trunc(n*Math.ceil(e.number/10));if(![n,r,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:r,canvasHeight:i}}(t,{duration:r,videoHeight:i,videoWidth:o});if(!e.active())return;n.width=o,n.height=i,n.style.width=`${o}px`,n.style.height=`${i}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=l;const s=c.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=u(e,n);let h=0;const m=e.guard(()=>{h>=t.number?e.dispose():f(r*h/t.number,e.guard(()=>{if(s.drawImage(n,h%10*t.width,Math.floor(h/10)*a,t.width,a),!e.active())return;let r=!1;c.toBlob(e.guard(n=>{r||(r=!0,e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=t.url))}return function(e){return t=>{return n=null,r=null,i=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),r=a(e=>{t.thumbnails=e},n),i=[],u=()=>{const n=r.start();n&&n.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(e,()=>t.option.url);n.active()&&d(n,r)})()},l=()=>{const e=i.splice(0),t=o([r.destroy,...e]);t.failed&&n(t.failure)};try{const e=[["destroy",l],["restart",r.cancel],["video:loadedmetadata",u]];for(const[n,o]of e){if(r.closed)break;i.push(()=>t.off(n,o)),t.on(n,o),r.closed&&t.off(n,o)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(n){t(n)}},a=e=>{try{u(i.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(n,r)).next())});var n,r,i}}});
+!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable,i=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;function o(e){let t,n=!1;for(const i of e)try{i()}catch(r){n||(n=!0,t=r)}return{failed:n,failure:t}}function a(e,a){let u,d,l=!1;const c=new Set;function s(e){void 0!==e&&c.delete(e)&&URL.revokeObjectURL(e)}function f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(l)return;const f=u,h=[];let m=!1;const v={active:()=>!l&&!m&&u===v,own(e){if(m){const t=o([e]);t.failed&&a(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=o(h.splice(0).reverse());e.failed&&a(e.failure)},fail(e){v.active()&&(v.dispose(),a(e))},guard:e=>(...t)=>{if(v.active())try{return e(...t)}catch(n){v.fail(n)}},publish(u,l){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(c.add(f),!v.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))n.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))r.call(o,a)&&i(e,a,o[a]);return e})({url:f},l))}catch(m){d===f&&(d=void 0!==h&&c.has(h)?h:void 0);const e=o([()=>s(f)]);throw e.failed&&a(e.failure),m}s(h)}};return u=v,null==f||f.dispose(),v},cancel:f,destroy:function(){if(l)return;l=!0;const e=o([f,...[...c].map(e=>()=>s(e))]);d=void 0,e.failed&&a(e.failure)},get closed(){return l}}}function u(e,t){const n="function"===typeof t.requestVideoFrameCallback&&"function"===typeof t.cancelVideoFrameCallback;let r;function i(){const e=r;if(r=void 0,t.onloadeddata=null,t.onseeked=null,!e)return;const n=o([()=>{null!==e.timer&&clearTimeout(e.timer)},()=>{null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(i),(o,a)=>{if(i(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>e.active()&&r===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(i(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const r=++u.sequence;u.presented=!1;const i=u.frame;if(u.frame=null,null!==i&&t.cancelVideoFrameCallback(i),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&r===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&r===u.sequence&&!o?u.frame=a:t.cancelVideoFrameCallback(a)})(),d()&&(t.currentTime=o)},s=e.guard(()=>{d()&&!u.started&&(u.started=!0,t.onloadeddata=null,t.onseeked=e.guard(()=>{if(!d()||t.seeking)return;const e=t.currentTime;if(d())if(!Number.isFinite(e)||Math.abs(e-o)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,l()}),c())});if(u.timer=setTimeout(e.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||t.readyState>=2;d()&&(f?s():t.onloadeddata=s)}}function d(e,t){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,i=n.videoHeight,o=n.videoWidth,{height:a,canvasWidth:d,canvasHeight:l}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),r=Math.trunc(10*e.width),i=Math.trunc(n*Math.ceil(e.number/10));if(![n,r,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:r,canvasHeight:i}}(t,{duration:r,videoHeight:i,videoWidth:o});if(!e.active())return;n.width=o,n.height=i,n.style.width=`${o}px`,n.style.height=`${i}px`;const c=document.createElement("canvas");if(e.own(()=>{c.height=0}),e.own(()=>{c.width=0}),!e.active())return;if(c.width=d,!e.active())return;if(c.height=l,!e.active())return;const s=c.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=u(e,n);let h=0;const m=e.guard(()=>{h>=t.number?e.dispose():f(r*h/t.number,e.guard(()=>{if(s.drawImage(n,h%10*t.width,Math.floor(h/10)*a,t.width,a),!e.active())return;let r=!1;c.toBlob(e.guard(n=>{r||(r=!0,e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=t.url))}return function(e){return t=>{return n=null,r=null,i=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),r=a(e=>{t.thumbnails=e},n),i=[],u=()=>{const n=r.start();n&&n.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(e,()=>t.option.url);n.active()&&d(n,r)})()},l=()=>{const e=i.splice(0),t=o([r.destroy,...e]);t.failed&&n(t.failure)};try{const e=[["destroy",l],["restart",r.cancel],["video:loadedmetadata",u]];for(const[n,o]of e){if(r.closed)break;i.push(()=>t.off(n,o)),t.on(n,o),r.closed&&t.off(n,o)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(n){t(n)}},a=e=>{try{u(i.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(n,r)).next())});var n,r,i}}});
diff --git a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs
index 948ba1870..1e4756b4f 100644
--- a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs
+++ b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs
@@ -301,10 +301,20 @@ function extract(job, config) {
video.style.width = `${videoWidth}px`;
video.style.height = `${videoHeight}px`;
const canvas = document.createElement("canvas");
+ job.own(() => {
+ canvas.height = 0;
+ });
+ job.own(() => {
+ canvas.width = 0;
+ });
if (!job.active())
return;
canvas.width = canvasWidth;
+ if (!job.active())
+ return;
canvas.height = canvasHeight;
+ if (!job.active())
+ return;
const ctx = canvas.getContext("2d");
if (!ctx)
throw new Error("Auto-thumbnail canvas context is unavailable");
diff --git a/packages/artplayer-plugin-auto-thumbnail/src/extraction.ts b/packages/artplayer-plugin-auto-thumbnail/src/extraction.ts
index f837cbf6b..708ace515 100644
--- a/packages/artplayer-plugin-auto-thumbnail/src/extraction.ts
+++ b/packages/artplayer-plugin-auto-thumbnail/src/extraction.ts
@@ -27,10 +27,20 @@ export default function extract(job: ExtractionJob, config: ExtractionConfig) {
video.style.width = `${videoWidth}px`
video.style.height = `${videoHeight}px`
const canvas = document.createElement('canvas')
+ job.own(() => {
+ canvas.height = 0
+ })
+ job.own(() => {
+ canvas.width = 0
+ })
if (!job.active())
return
canvas.width = canvasWidth
+ if (!job.active())
+ return
canvas.height = canvasHeight
+ if (!job.active())
+ return
const ctx = canvas.getContext('2d')
if (!ctx)
throw new Error('Auto-thumbnail canvas context is unavailable')
diff --git a/refactor/baselines/auto-thumbnail-canvas-validation.json b/refactor/baselines/auto-thumbnail-canvas-validation.json
new file mode 100644
index 000000000..15160804c
--- /dev/null
+++ b/refactor/baselines/auto-thumbnail-canvas-validation.json
@@ -0,0 +1,4248 @@
+{
+ "task": "PKG-AUTO-THUMB-07",
+ "status": "complete",
+ "baselineCommit": "e93d3cbb67a937a94e3979f36efe16ee39cadb33",
+ "node": "24.21.0",
+ "yarn": "1.22.22",
+ "source": {
+ "path": "packages/artplayer-plugin-auto-thumbnail/src/extraction.ts",
+ "sha256": "028772ae962cb9d02e719435b799c6e0e193b15bc8448baa411a94bb7556fd5d",
+ "bytes": 2296
+ },
+ "architecture": {
+ "path": "packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md",
+ "sha256": "d25be2e728d2c8cab00426a84abba2b5afffe407fc2d92b0caf939baf47a904b",
+ "bytes": 8933
+ },
+ "beforeArtifact": {
+ "path": "refactor/.cache/auto-thumb-canvas-before.js",
+ "sha256": "806d3c4601099bc67210c500f6b556072a90c7de0269d45821cb19d5e3b0c69a",
+ "bytes": 6044
+ },
+ "artifacts": [
+ {
+ "path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "bytes": 6135,
+ "docs": {
+ "path": "docs/compiled/artplayer-plugin-auto-thumbnail.js",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "bytes": 6135
+ }
+ },
+ {
+ "path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "bytes": 6715,
+ "docs": {
+ "path": "docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "bytes": 6715
+ }
+ },
+ {
+ "path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs",
+ "sha256": "cf726c7e6c5c08a1afc9b2c080e15150f3b16c0b321779257db662fe83336bf0",
+ "bytes": 11677,
+ "docs": {
+ "path": "docs/compiled/artplayer-plugin-auto-thumbnail.mjs",
+ "sha256": "cf726c7e6c5c08a1afc9b2c080e15150f3b16c0b321779257db662fe83336bf0",
+ "bytes": 11677
+ }
+ }
+ ],
+ "controlled": [
+ {
+ "path": "refactor/.cache/auto-thumb-canvas-before.log",
+ "sha256": "3d40653069bcfe515426ecc8bb0595ac7191d7ebb74f88dd3bb974bc87929ff4",
+ "bytes": 5476,
+ "counts": {
+ "tests": 6,
+ "pass": 0,
+ "fail": 6
+ }
+ },
+ {
+ "path": "refactor/.cache/auto-thumb-canvas-after.log",
+ "sha256": "68fcd7a895737219444afc172b83114fd48661c840085de65396958244c96283",
+ "bytes": 664,
+ "counts": {
+ "tests": 6,
+ "pass": 6,
+ "fail": 0
+ }
+ },
+ {
+ "path": "refactor/.cache/auto-thumb-canvas-tests.log",
+ "sha256": "12e0e8b06291be00082f18f74d27bba1bfe4812c9da1c85b4edb843082060a77",
+ "bytes": 20711,
+ "counts": {
+ "tests": 160,
+ "pass": 159,
+ "fail": 1
+ }
+ },
+ {
+ "path": "refactor/.cache/auto-thumb-canvas-tests-final.log",
+ "sha256": "69157d61e43a1a99e3f17d760a4043ca6c0c9757305c2672e899fe58b2d90c83",
+ "bytes": 19819,
+ "counts": {
+ "tests": 160,
+ "pass": 160,
+ "fail": 0
+ }
+ },
+ {
+ "path": "refactor/.cache/auto-thumb-canvas-unit-js.log",
+ "sha256": "dd24231c16af3a8c8bdd70b61aea9204ba7d84648f339ff8e8bb9525dab8833d",
+ "bytes": 5808,
+ "counts": {
+ "tests": 58,
+ "pass": 58,
+ "fail": 0
+ }
+ },
+ {
+ "path": "refactor/.cache/auto-thumb-canvas-unit-legacy.js.log",
+ "sha256": "ffe539b33fb110ce4f5997000cb4f9d2f0981defd9b15e1b08be7dd3950f71f2",
+ "bytes": 5810,
+ "counts": {
+ "tests": 58,
+ "pass": 58,
+ "fail": 0
+ }
+ }
+ ],
+ "reports": [
+ {
+ "mode": "before",
+ "report": "refactor/.cache/auto-thumb-canvas-native-before.json",
+ "sha256": "62b56c29d2f1c8d391f534dc4d51b4c336b75e48503c9bc2f5e882b0e777f063",
+ "results": "refactor/.cache/auto-thumb-canvas-native-before-results",
+ "stats": {
+ "startTime": "2026-09-13T19:40:36.042Z",
+ "duration": 3486.4629999999997,
+ "expected": 0,
+ "skipped": 0,
+ "unexpected": 1,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "failed",
+ "errors": [
+ {
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\auto-thumbnail-lifecycle.spec.js",
+ "column": 36,
+ "line": 116
+ },
+ "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // deep equality\u001b[22m\n\n\u001b[32m- Expected - 2\u001b[39m\n\u001b[31m+ Received + 2\u001b[39m\n\n\u001b[2m Array [\u001b[22m\n\u001b[2m Array [\u001b[22m\n\u001b[32m- 0,\u001b[39m\n\u001b[32m- 0,\u001b[39m\n\u001b[31m+ 800,\u001b[39m\n\u001b[31m+ 45,\u001b[39m\n\u001b[2m ],\u001b[22m\n\u001b[2m ]\u001b[22m\n\n 114 | expect(state.after).toEqual(state.before)\n 115 | expect(state.video).toEqual({ connected: false, src: null, readyState: 0, paused: true, handlers: [true, true, true, true] })\n> 116 | expect(state.canvasDimensions).toEqual([[0, 0]])\n | ^\n 117 | expect(state.publishedImage).toEqual(scenario === 'complete' ? { width: 800, height: 45 } : null)\n 118 | expect(state.final).toEqual({ urls: 0, listeners: 0, updates: scenario === 'complete' ? 2 : 0 })\n 119 | expect(state.blobs.every(blob => blob.type === 'image/jpeg' && blob.bytes > 0)).toBe(true)\n at D:\\github\\ArtPlayer\\test\\browser\\auto-thumbnail-lifecycle.spec.js:116:36"
+ }
+ ],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "complete",
+ "sha256": "806d3c4601099bc67210c500f6b556072a90c7de0269d45821cb19d5e3b0c69a",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 1,
+ "updates": 2
+ },
+ "after": {
+ "urls": 1,
+ "updates": 2
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 800,
+ 45
+ ]
+ ],
+ "publishedImage": {
+ "width": 800,
+ "height": 45
+ },
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 2
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 2635
+ },
+ {
+ "type": "image/jpeg",
+ "bytes": 4226
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "mode": "main",
+ "report": "refactor/.cache/auto-thumb-canvas-native-main.json",
+ "sha256": "dae64fb488c400b5a8e9eba20fa92c593a669a6eaab7f2120816617ab2a80650",
+ "results": "refactor/.cache/auto-thumb-canvas-native-main-results",
+ "stats": {
+ "startTime": "2026-09-13T19:42:49.544Z",
+ "duration": 25004.026,
+ "expected": 21,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "destroy",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 2635
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "restart",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 2635
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "complete",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 1,
+ "updates": 2
+ },
+ "after": {
+ "urls": 1,
+ "updates": 2
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": {
+ "width": 800,
+ "height": 45
+ },
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 2
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 2635
+ },
+ {
+ "type": "image/jpeg",
+ "bytes": 4226
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-destroy",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-restart",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "destroy",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4076
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "restart",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4076
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "complete",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 1,
+ "updates": 2
+ },
+ "after": {
+ "urls": 1,
+ "updates": 2
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": {
+ "width": 800,
+ "height": 45
+ },
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 2
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4076
+ },
+ {
+ "type": "image/jpeg",
+ "bytes": 6752
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-destroy",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-restart",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "destroy",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "restart",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4125
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "complete",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 1,
+ "updates": 2
+ },
+ "after": {
+ "urls": 1,
+ "updates": 2
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": {
+ "width": 800,
+ "height": 45
+ },
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 2
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ },
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-destroy",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-restart",
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "media": "auto-thumbnail-timeline",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 126,
+ 0,
+ 126,
+ 255
+ ],
+ [
+ 254,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 1,
+ 252,
+ 255
+ ],
+ [
+ 255,
+ 255,
+ 1,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 127,
+ 0,
+ 126,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 254,
+ 0,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 255,
+ 2,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.6,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.2,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.8,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.4,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "media": "pattern",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 15,
+ 13,
+ 138,
+ 255
+ ],
+ [
+ 15,
+ 15,
+ 139,
+ 255
+ ],
+ [
+ 15,
+ 15,
+ 139,
+ 255
+ ],
+ [
+ 14,
+ 15,
+ 141,
+ 255
+ ],
+ [
+ 213,
+ 61,
+ 47,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 244,
+ 1,
+ 0,
+ 255
+ ],
+ [
+ 5,
+ 251,
+ 2,
+ 255
+ ],
+ [
+ 15,
+ 15,
+ 139,
+ 255
+ ],
+ [
+ 255,
+ 0,
+ 253,
+ 255
+ ],
+ [
+ 2,
+ 255,
+ 253,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 253,
+ 52,
+ 0,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.583333,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.166667,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.791667,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.375,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "media": "auto-thumbnail-timeline",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 136,
+ 19,
+ 126,
+ 255
+ ],
+ [
+ 254,
+ 24,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 254,
+ 241,
+ 2,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 137,
+ 19,
+ 126,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 24,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 241,
+ 2,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.6,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.2,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.8,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.4,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "media": "pattern",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 5,
+ 15,
+ 250,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 247,
+ 64,
+ 0,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 255,
+ 22,
+ 9,
+ 255
+ ],
+ [
+ 0,
+ 216,
+ 3,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 255,
+ 36,
+ 254,
+ 255
+ ],
+ [
+ 1,
+ 229,
+ 254,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 69,
+ 1,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.6,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.2,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.8,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.4,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "media": "auto-thumbnail-timeline",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 238,
+ 14,
+ 14,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 15,
+ 239,
+ 255
+ ],
+ [
+ 235,
+ 236,
+ 12,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "time": 1.6016053,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 238,
+ 14,
+ 14,
+ 255
+ ]
+ },
+ {
+ "time": 3.200928,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 16,
+ 16,
+ 255
+ ]
+ },
+ {
+ "time": 4.8015866,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 6.4018795,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 235,
+ 236,
+ 12,
+ 255
+ ]
+ }
+ ],
+ "presented": [],
+ "supportsPresentation": false,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "23c8a1405077c315a7ebd4700a16d8ac05716464a6601c32a702a30e599727c3",
+ "media": "pattern",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 201,
+ 71,
+ 57,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 238,
+ 14,
+ 15,
+ 255
+ ],
+ [
+ 14,
+ 238,
+ 14,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 238,
+ 13,
+ 237,
+ 255
+ ],
+ [
+ 14,
+ 237,
+ 237,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "time": 1.6003974,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 3.2008015,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 4.8012444,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 6.4034658,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 238,
+ 61,
+ 10,
+ 255
+ ]
+ }
+ ],
+ "presented": [],
+ "supportsPresentation": false,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "mode": "legacy",
+ "report": "refactor/.cache/auto-thumb-canvas-native-legacy.json",
+ "sha256": "87505fd4eb9cd4dab04ed7a284a60d118682214ccea88320630a297a1935a6a0",
+ "results": "refactor/.cache/auto-thumb-canvas-native-legacy-results",
+ "stats": {
+ "startTime": "2026-09-13T19:43:49.558Z",
+ "duration": 25423.173,
+ "expected": 21,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "destroy",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 2635
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "restart",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 2635
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "complete",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 1,
+ "updates": 2
+ },
+ "after": {
+ "urls": 1,
+ "updates": 2
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": {
+ "width": 800,
+ "height": 45
+ },
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 2
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 2635
+ },
+ {
+ "type": "image/jpeg",
+ "bytes": 4226
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-destroy",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-restart",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "destroy",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4076
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "restart",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4076
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "complete",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 1,
+ "updates": 2
+ },
+ "after": {
+ "urls": 1,
+ "updates": 2
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": {
+ "width": 800,
+ "height": 45
+ },
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 2
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4076
+ },
+ {
+ "type": "image/jpeg",
+ "bytes": 6752
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-destroy",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-restart",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [],
+ "heldFrames": 1,
+ "frameSupport": true,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "destroy",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "restart",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "complete",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 1,
+ "updates": 2
+ },
+ "after": {
+ "urls": 1,
+ "updates": 2
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": {
+ "width": 800,
+ "height": 45
+ },
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 2
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ },
+ {
+ "type": "image/jpeg",
+ "bytes": 4215
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-destroy",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 983
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-native-lifecycle",
+ "value": {
+ "scenario": "frame-restart",
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.",
+ "before": {
+ "urls": 0,
+ "updates": 0
+ },
+ "after": {
+ "urls": 0,
+ "updates": 0
+ },
+ "video": {
+ "connected": false,
+ "src": null,
+ "readyState": 0,
+ "paused": true,
+ "handlers": [
+ true,
+ true,
+ true,
+ true
+ ]
+ },
+ "canvasDimensions": [
+ [
+ 0,
+ 0
+ ]
+ ],
+ "publishedImage": null,
+ "final": {
+ "urls": 0,
+ "listeners": 0,
+ "updates": 0
+ },
+ "blobs": [
+ {
+ "type": "image/jpeg",
+ "bytes": 4125
+ }
+ ],
+ "heldFrames": 0,
+ "frameSupport": false,
+ "result": {
+ "name": "artplayerPluginAutoThumbnail"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "media": "auto-thumbnail-timeline",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 126,
+ 0,
+ 126,
+ 255
+ ],
+ [
+ 254,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 1,
+ 252,
+ 255
+ ],
+ [
+ 255,
+ 255,
+ 1,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 127,
+ 0,
+ 126,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 254,
+ 0,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 255,
+ 2,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.6,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.2,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.8,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.4,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits",
+ "browser": "153.0.8010.12",
+ "project": "chromium",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "media": "pattern",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 15,
+ 13,
+ 138,
+ 255
+ ],
+ [
+ 15,
+ 15,
+ 139,
+ 255
+ ],
+ [
+ 15,
+ 15,
+ 139,
+ 255
+ ],
+ [
+ 14,
+ 15,
+ 141,
+ 255
+ ],
+ [
+ 213,
+ 61,
+ 47,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 244,
+ 1,
+ 0,
+ 255
+ ],
+ [
+ 5,
+ 251,
+ 2,
+ 255
+ ],
+ [
+ 15,
+ 15,
+ 139,
+ 255
+ ],
+ [
+ 255,
+ 0,
+ 253,
+ 255
+ ],
+ [
+ 2,
+ 255,
+ 253,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 253,
+ 52,
+ 0,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.583333,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.166667,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.791667,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.375,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "media": "auto-thumbnail-timeline",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 136,
+ 19,
+ 126,
+ 255
+ ],
+ [
+ 254,
+ 24,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 254,
+ 241,
+ 2,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 137,
+ 19,
+ 126,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 24,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 241,
+ 2,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.6,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.2,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.8,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.4,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits",
+ "browser": "155.0",
+ "project": "firefox",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "media": "pattern",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 5,
+ 15,
+ 250,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 247,
+ 64,
+ 0,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 255,
+ 22,
+ 9,
+ 255
+ ],
+ [
+ 0,
+ 216,
+ 3,
+ 255
+ ],
+ [
+ 1,
+ 14,
+ 253,
+ 255
+ ],
+ [
+ 255,
+ 36,
+ 254,
+ 255
+ ],
+ [
+ 1,
+ 229,
+ 254,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 1.6,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 3.2,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 4.8,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 14,
+ 253,
+ 255
+ ]
+ },
+ {
+ "time": 6.4,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 255,
+ 69,
+ 1,
+ 255
+ ]
+ }
+ ],
+ "presented": [
+ {
+ "mediaTime": 0,
+ "currentTime": 0
+ },
+ {
+ "mediaTime": 1.6,
+ "currentTime": 1.6
+ },
+ {
+ "mediaTime": 3.2,
+ "currentTime": 3.2
+ },
+ {
+ "mediaTime": 4.8,
+ "currentTime": 4.8
+ },
+ {
+ "mediaTime": 6.4,
+ "currentTime": 6.4
+ }
+ ],
+ "supportsPresentation": true,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "media": "auto-thumbnail-timeline",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 238,
+ 14,
+ 14,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 15,
+ 239,
+ 255
+ ],
+ [
+ 235,
+ 236,
+ 12,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ],
+ [
+ 16,
+ 16,
+ 16,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0.0002962,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "time": 1.6019117,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 238,
+ 14,
+ 14,
+ 255
+ ]
+ },
+ {
+ "time": 3.2013872,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 16,
+ 16,
+ 255
+ ]
+ },
+ {
+ "time": 4.8018211,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 6.4017066,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 235,
+ 236,
+ 12,
+ 255
+ ]
+ }
+ ],
+ "presented": [],
+ "supportsPresentation": false,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits",
+ "browser": "26.6",
+ "project": "webkit",
+ "status": "passed",
+ "errors": [],
+ "attachments": [
+ {
+ "name": "auto-thumbnail-candidate-hidden-pixels",
+ "value": {
+ "sha256": "32a620523aaccbfa3910232f88f255ebbadc89e20a0cb21dc09c17dff611627d",
+ "media": "pattern",
+ "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.",
+ "cells": [
+ [
+ 0,
+ 0,
+ 0,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 217,
+ 65,
+ 51,
+ 255
+ ]
+ ],
+ "spatial": [
+ [
+ 238,
+ 14,
+ 15,
+ 255
+ ],
+ [
+ 14,
+ 238,
+ 14,
+ 255
+ ],
+ [
+ 28,
+ 28,
+ 140,
+ 255
+ ],
+ [
+ 238,
+ 13,
+ 237,
+ 255
+ ],
+ [
+ 14,
+ 237,
+ 237,
+ 255
+ ]
+ ],
+ "draws": [
+ {
+ "time": 0.0011342,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "time": 1.6006964,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 3.2010741,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 4.8014946,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 16,
+ 15,
+ 239,
+ 255
+ ]
+ },
+ {
+ "time": 6.4062879,
+ "duration": 8,
+ "connected": true,
+ "visibility": "hidden",
+ "display": "block",
+ "width": 320,
+ "height": 180,
+ "raw": [
+ 239,
+ 62,
+ 14,
+ 255
+ ]
+ }
+ ],
+ "presented": [],
+ "supportsPresentation": false,
+ "final": [
+ {
+ "connected": false,
+ "src": null,
+ "paused": true
+ }
+ ],
+ "playing": 0,
+ "warnings": []
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "checks": [
+ "Package strict TypeScript passed",
+ "Targeted source and test ESLint passed",
+ "Yarn strict toolchain passed: 28 tools / 1394 selectors",
+ "Normal three-format repository build passed; all docs copies byte-identical",
+ "Native Node ESM import exposes the existing default function"
+ ],
+ "limitation": "Only private canvas dimensions/lifecycle and retained JPEG ownership are closed. No exact RAM/VRAM reclamation measurement, native encoder cancellation, whole-package installed type matrix, full core combinations or device acceptance. Pixel specs retain explicit callback-less WebKit first-two-cell gaps. Full source suite includes historical defect observations. Intermediate 159/1 failure was the legacy test reading dimensions after completion; the same dimension assertion now runs before extraction and final zero dimensions are checked separately."
+}
diff --git a/refactor/changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md
new file mode 100644
index 000000000..e1adc223c
--- /dev/null
+++ b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md
@@ -0,0 +1,67 @@
+# PKG-AUTO-THUMB-07 私有抽帧画布清理
+
+状态:该独立任务完成;PKG-AUTO-THUMB-03 的剩余工作继续。
+
+从 PKG-AUTO-THUMB-03 的资源审查拆出独立修复;基线为
+`e93d3cbb67a937a94e3979f36efe16ee39cadb33`。03 增加对 07 的完成依赖,原来的
+WebKit 首两格像素、公共类型和后续完整包验收范围保持不变。
+
+## 已复现问题与实现
+
+抽帧任务只清理隐藏 video、监听和定时器,没有主动清空已分配的私有 canvas。
+编码回调或宿主观测保留该 canvas 时,任务完成/取消后其整张表的尺寸仍然存在。
+六项受控回归在基线全部失败;冻结正常 main 产物的原生完成用例也保持 800×45,
+未达到清理预期。这里确认的是可观察的画布尺寸/像素存储生命周期,不是测量泄漏
+了多少系统 RAM/VRAM,也不声称浏览器永远不会回收不可达对象。
+
+`src/extraction.ts` 在创建 canvas 后立即将宽、高复位加入 job 的既有资源清理。
+两个动作独立执行,任意一个抛错仍继续另一个以及 decoder 清理;正常完成、
+destroy、restart、提取失败复用同一条清理路径。尺寸赋值后重新核对 job 身份,
+避免用户覆写的 DOM setter 同步销毁任务后又分配下一维度。没有引入新抽象、
+共享全局状态、依赖或公开配置。
+
+编码后的 JPEG 是独立 Blob,保留既有最终预览 URL,不因清空 canvas 撤销它。
+后续任务拥有自己的新 canvas,旧回调不能发布或重新分配旧任务的画布。
+宽高归零不保证异步编码器或 GPU 驱动已经归还全部底层内存;它不代替整体预算
+或浏览器耐久测试。
+
+## 兼容边界与测试
+
+工厂/异步注册结果、原始配置及可强制转换数字、number 的分数行为、目标时间公式、
+更新次数、JPEG MIME、URL 所有权、错误通道和公开声明均不变。仅更改不可通过
+插件 API 取得的内部画布终态。ARCHITECTURE.md 随源码同步资源归属。
+
+新增 `test/auto-thumbnail-canvas.test.js` 覆盖保留编码回调时 destroy/restart、
+正常完成后仍持有预览、context 失败、单个 reset 抛错,以及分配中的销毁重入。
+已加入 test:unit/test:auto-thumbnail。首次联合 160 项为 159 通过/1 失败:旧
+数字兼容测试在完成后检查 canvas 宽度。现在在抽帧开始时检查原尺寸计算,并
+额外要求完成后归零;未删除原配置透传和次数断言。修正后联合 160 项通过。
+初次定向 lint 的单行语句格式错误也已修正。
+
+真实浏览器生命周期用例保留 canvas 引用和原生 Blob/帧回调,验证三引擎中各
+终止路径尺寸为零;完成时通过最终 URL 解码 JPEG,确认其仍为 800×45。使用
+受控播放器宿主和保留的原生回调,不能称为全部核心组合/未经拦截的生产时序。
+现有像素用例同时回归:有原生帧回调时仍要求五格准确,Windows WebKit 的前两格
+仍为诊断、只验收后面三格,AUTO-THUMB-PIXEL-01 不因本修复关闭。
+
+正常 main/legacy/ESM 经仓库 build 再生,docs 副本来自构建;公共声明与包版本
+未变。具体命令、基线/候选 hash、源码和产物回归、原生报告及范围见
+[验证记录](../baselines/auto-thumbnail-canvas-validation.json)。原失败及第一次
+159/1 中间结果保留在缓存日志;机器摘要保存关键结论与指纹。
+
+最终联合 160 项、main/legacy 各 58 项受控检查通过;main/legacy 各 21 项三
+浏览器检查通过(每组 15 生命周期、6 像素,后者保留既有 WebKit 局部范围)。
+严格包 TS、定向 lint、固定 Yarn 工具链和 ESM 原生导入通过;三种正式构建与
+docs 副本逐字节一致。只关闭 AUTO-THUMB-CANVAS-01 的可观察画布终态缺陷。
+
+```sh
+yarn test:auto-thumbnail
+node node_modules/typescript/bin/tsc -p packages/artplayer-plugin-auto-thumbnail/tsconfig.json --noEmit
+yarn build artplayer-plugin-auto-thumbnail
+yarn test:browser test/browser/auto-thumbnail-lifecycle.spec.js test/browser/auto-thumbnail-pixels.spec.js --workers=1
+```
+
+通过 ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT 分别选择 main/legacy,归档共享报告后再
+运行下一组;未设置该变量则测试源码编译结果。没有重复执行全部无关包测试或
+全仓安装类型矩阵。07 只负责该独立资源缺陷,不等于 03-06、包大版本升级、
+远程 CI 或发布验收完成。回退本提交并正常重建即可恢复原画布终态;不 push/publish。
diff --git a/refactor/plan.md b/refactor/plan.md
index 23e5a36f6..f5acbc744 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
-基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 234 项,范围 22 个包及工作区/示例。
+基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 235 项,范围 22 个包及工作区/示例。
-状态:todo 62 / doing 15 / blocked 0 / done 157 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
+状态:todo 62 / doing 15 / blocked 0 / done 158 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -19,7 +19,7 @@
| artplayer-plugin-ambilight | 1.1.0 | PKG-AMBILIGHT-01, PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-04, PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05, PKG-AMBILIGHT-06, PKG-FACTORY-01 |
| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06, PKG-ASR-07, PKG-ASR-08, PKG-ASR-09 |
| artplayer-plugin-audio-track | 1.1.0 | PKG-AUDIO-01, PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-04, PKG-AUDIO-05, PKG-AUDIO-06 |
-| artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06 |
+| artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06, PKG-AUTO-THUMB-07 |
| artplayer-plugin-chapter | 1.1.0 | PILOT-01, PKG-CHAPTER-01, PKG-CHAPTER-02, PKG-CHAPTER-03, PKG-CHAPTER-04, PKG-CHAPTER-05, PKG-CHAPTER-06 |
| artplayer-plugin-chromecast | 1.1.0 | PKG-CAST-01, PKG-CAST-02, PKG-CAST-03, PKG-CAST-04, PKG-CAST-05, PKG-CAST-06 |
| artplayer-plugin-danmuku | 5.3.0 | PKG-DANMUKU-01, PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-06, PKG-DANMUKU-07, PKG-DANMUKU-08, PKG-DANMUKU-09, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12 |
@@ -174,10 +174,11 @@
| --- | --- | --- | --- | --- | --- | --- |
| PKG-AUTO-THUMB-01 | artplayer-plugin-auto-thumbnail
核对包契约与历史用法 | BASE-05 | url/width/number/scale、异步工厂和渐进缩略图更新 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-AUTO-THUMB-02 | artplayer-plugin-auto-thumbnail
建立特有行为与错误测试 | PKG-AUTO-THUMB-01, ENG-03, ENG-05 | 抽帧边界、短视频、失败、连续来源和销毁中的抽帧 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
-| PKG-AUTO-THUMB-03 | artplayer-plugin-auto-thumbnail
整理内部职责与资源 | PKG-AUTO-THUMB-02, CORE-09, CORE-19 | 隐藏 video、seek 队列、canvas 编码与 Blob URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | doing |
+| PKG-AUTO-THUMB-03 | artplayer-plugin-auto-thumbnail
整理内部职责与资源 | PKG-AUTO-THUMB-02, CORE-09, CORE-19, PKG-AUTO-THUMB-07 | 隐藏 video、seek 队列、canvas 编码与 Blob URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | doing |
| PKG-AUTO-THUMB-04 | artplayer-plugin-auto-thumbnail
迁移自有源码和公开类型 | PKG-AUTO-THUMB-03, ENG-04, ENG-06, CORE-07 | 抽帧结果和真实异步插件返回类型兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-AUTO-THUMB-05 | artplayer-plugin-auto-thumbnail
验证新旧核心和组合 | PKG-AUTO-THUMB-04, CORE-22 | 旧核心/候选核心预览、多次加载无过期缩略图覆盖 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-AUTO-THUMB-06 | artplayer-plugin-auto-thumbnail
验证分发并同步文档 | PKG-AUTO-THUMB-05, ENG-07 | auto.thumbnail.js、三种产物和内存/URL 证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
+| PKG-AUTO-THUMB-07 | artplayer-plugin-auto-thumbnail
终止抽帧时清空私有画布像素尺寸 | PKG-AUTO-THUMB-02, CORE-09, CORE-19 | 将私有 canvas 尺寸归入 job 清理;完成、取消、失败及分配重入均有回归,保留已编码 JPEG | 旧红新绿证明保留回调时画布归零,单项清理失败不阻断其他资源;原生取消和完成后 JPEG 仍可解码,原 API/时间公式/类型不变 | M | done |
## 5 包迁移:artplayer-plugin-vtt-thumbnail
@@ -606,3 +607,4 @@
- PKG-DANMUKU-12: [记录](changes/2026-09-13-PKG-DANMUKU-12-frame-sampling.md) [记录](baselines/danmuku-frame-sampling-validation.json)
- PKG-JASSUB-08: [记录](changes/2026-09-14-PKG-JASSUB-08-offscreen.md) [记录](baselines/jassub-offscreen-validation.json) [记录](baselines/jassub-offscreen-patch.json)
- PKG-JASSUB-09: [记录](changes/2026-09-14-PKG-JASSUB-09-hybrid.md) [记录](baselines/jassub-hybrid-validation.json) [记录](baselines/jassub-hybrid-patch.json) [记录](changes/2026-09-14-PKG-JASSUB-09-firefox-diagnostics.md) [记录](changes/2026-09-14-PKG-JASSUB-09-display.md) [记录](baselines/jassub-display-validation.json)
+- PKG-AUTO-THUMB-07: [记录](changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md) [记录](baselines/auto-thumbnail-canvas-validation.json)
diff --git a/refactor/progress.md b/refactor/progress.md
index c83563117..8136234b5 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -1,5 +1,18 @@
# 进度与证据
+## PKG-AUTO-THUMB-07 私有画布清理完成
+
+抽帧 canvas 现由 job 清理宽高;取消、失败和正常完成后归零,单个 reset 抛错
+仍继续其他清理,分配期间销毁不会继续分配另一维。已编码 JPEG 和最终预览 URL
+保留。六项受控回归旧实现全部失败、新实现全部通过;冻结 main 的原生完成检查
+同样先失败。联合 160 项通过,main/legacy 各 58 项受控检查和各 21 项原生检查
+通过,完成后的 JPEG 仍可解码为 800×45。严格包 TS、lint、正常三格式构建及
+docs 副本一致性通过。关闭 AUTO-THUMB-CANVAS-01;不声称精确 GPU/内存回收量。
+[变更](changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md)和
+[红绿记录](baselines/auto-thumbnail-canvas-validation.json)含中间测试调整。
+03 依赖该独立修复,仍继续前两格 WebKit 像素和其他迁移工作;04-06、版本及
+发布验收未完成。VAST 的初始化决定仍待答复。下一步继续未完成源码/类型迁移。
+
## PKG-CHAPTER-05 时序取证继续(doing)
复核旧失败 ZIP:一次 restart 数组读取耗时 9956.244 ms,返回时已含目标 URL;
diff --git a/refactor/risk-table.md b/refactor/risk-table.md
index f32830aa0..05a6a9a81 100644
--- a/refactor/risk-table.md
+++ b/refactor/risk-table.md
@@ -252,3 +252,4 @@
| JASSUB-OFFSCREEN-01 | resolved / 已复现 | Default offscreen initialization lets queued resize render before Worker canvas ownership, raising clearRect on false context | PKG-JASSUB-08, PKG-JASSUB-05 |
| JASSUB-HYBRID-01 | open / 已复现 | Late hybrid frames mutate the new offscreen canvas or throw; terminal track calls recreate transferred canvases | PKG-JASSUB-09, PKG-JASSUB-05 |
| JASSUB-FIREFOX-OFFSCREEN-01 | open / 已复现 | Windows Firefox offscreen readback and asynchronous ImageBitmap creation can stall through seek/layout, including a minimal Worker control | PKG-JASSUB-09, PKG-JASSUB-05 |
+| AUTO-THUMB-CANVAS-01 | resolved / 已复现 | Private thumbnail canvas retains sheet dimensions after completion or cancellation while callbacks retain the element | PKG-AUTO-THUMB-07 |
diff --git a/refactor/risks.json b/refactor/risks.json
index b259af997..e10ddd535 100644
--- a/refactor/risks.json
+++ b/refactor/risks.json
@@ -5662,6 +5662,30 @@
],
"compatibleResolution": "Isolate native media/compositor/readback and sequence effects without globally disabling default offscreen or deleting assertions. The pre-09 artifact also fails in the same sequence; do not claim the new hybrid guard caused or fixed this separate failure.",
"closureCriteria": "Identify and verify a compatible fix or a reviewed precise platform boundary with native controls. Preserve known red sequence and successful standalone comparison; candidate playback/fullscreen pixels and lifetime must be justified, not merely rerun to green."
+ },
+ {
+ "id": "AUTO-THUMB-CANVAS-01",
+ "title": "Private thumbnail canvas retains sheet dimensions after completion or cancellation while callbacks retain the element",
+ "confirmation": "reproduced",
+ "status": "resolved",
+ "owners": [
+ "PKG-AUTO-THUMB-07"
+ ],
+ "evidence": [
+ "refactor/changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md",
+ "refactor/baselines/auto-thumbnail-canvas-validation.json",
+ "test/auto-thumbnail-canvas.test.js",
+ "test/browser/auto-thumbnail-lifecycle.spec.js"
+ ],
+ "compatibleResolution": "Own width/height reset in the extraction job and guard allocation reentry; retain completed JPEG URLs.",
+ "closureCriteria": "Controlled old/new and native retained-callback checks demonstrate terminal zero dimensions, independent cleanup and still-decodable completed JPEGs.",
+ "resolutionEvidence": [
+ "refactor/changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md",
+ "refactor/baselines/auto-thumbnail-canvas-validation.json",
+ "test/auto-thumbnail-canvas.test.js",
+ "test/browser/auto-thumbnail-lifecycle.spec.js"
+ ],
+ "resolutionRationale": "Six controlled regressions fail before and pass after. The frozen main native complete control fails dimensions; candidate main/legacy native lifecycle checks reset the retained canvas and decode final JPEGs after reset. No public options, types, sample positions or caller URLs changed; WebKit early-frame pixels remain separately open."
}
]
}
diff --git a/refactor/tasks.json b/refactor/tasks.json
index b41417fb1..8287eb64b 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -1783,7 +1783,8 @@
"dependsOn": [
"PKG-AUTO-THUMB-02",
"CORE-09",
- "CORE-19"
+ "CORE-19",
+ "PKG-AUTO-THUMB-07"
],
"status": "doing",
"risk": "H",
@@ -4816,6 +4817,27 @@
"changes/2026-09-14-PKG-JASSUB-09-display.md",
"baselines/jassub-display-validation.json"
]
+ },
+ {
+ "id": "PKG-AUTO-THUMB-07",
+ "phase": "5 包迁移:artplayer-plugin-auto-thumbnail",
+ "title": "终止抽帧时清空私有画布像素尺寸",
+ "scope": [
+ "artplayer-plugin-auto-thumbnail"
+ ],
+ "dependsOn": [
+ "PKG-AUTO-THUMB-02",
+ "CORE-09",
+ "CORE-19"
+ ],
+ "status": "done",
+ "risk": "M",
+ "deliverable": "将私有 canvas 尺寸归入 job 清理;完成、取消、失败及分配重入均有回归,保留已编码 JPEG",
+ "acceptance": "旧红新绿证明保留回调时画布归零,单项清理失败不阻断其他资源;原生取消和完成后 JPEG 仍可解码,原 API/时间公式/类型不变",
+ "evidence": [
+ "changes/2026-09-14-PKG-AUTO-THUMB-07-canvas.md",
+ "baselines/auto-thumbnail-canvas-validation.json"
+ ]
}
]
}
diff --git a/test/auto-thumbnail-canvas.test.js b/test/auto-thumbnail-canvas.test.js
new file mode 100644
index 000000000..326fcc058
--- /dev/null
+++ b/test/auto-thumbnail-canvas.test.js
@@ -0,0 +1,102 @@
+import assert from 'node:assert/strict'
+// eslint-disable-next-line test/no-import-node-test -- Repository canvas resource ownership regressions.
+import test from 'node:test'
+import { autoThumbnailCandidate, autoThumbnailEnvironment } from './helpers/auto-thumbnail.js'
+
+const implementation = await autoThumbnailCandidate()
+async function setup(number = 2) {
+ const env = autoThumbnailEnvironment(implementation)
+ await env.factory({ width: 80, number })(env.art)
+ env.art.emit('video:loadedmetadata')
+ return env
+}
+
+for (const event of ['destroy', 'restart']) {
+ test(`Auto-thumbnail clears a retained canvas when ${event} cancels an encoding`, async () => {
+ const env = await setup()
+ env.metadata()
+ env.seeked()
+ const canvas = env.canvases[0]
+ assert.equal(canvas.width * canvas.height, 36000)
+ env.art.emit(event)
+ assert.deepEqual([canvas.width, canvas.height], [0, 0])
+ env.finish()
+ assert.equal(env.updates.length, 0)
+ assert.equal(env.urls.size, 0)
+ assert.equal(env.attached.size, 0)
+ if (event === 'restart') {
+ env.art.emit('video:loadedmetadata')
+ env.metadata()
+ assert.deepEqual([env.canvases[1].width, env.canvases[1].height], [800, 45])
+ assert.deepEqual([canvas.width, canvas.height], [0, 0])
+ }
+ env.art.emit('destroy')
+ })
+}
+
+test('Auto-thumbnail clears completed canvas storage while retaining the encoded preview', async () => {
+ const env = await setup(1)
+ env.metadata()
+ env.seeked()
+ const blob = { type: 'image/jpeg', content: 'encoded image owns its bytes' }
+ env.finish(0, blob)
+ assert.deepEqual([env.canvases[0].width, env.canvases[0].height], [0, 0])
+ assert.equal(env.urls.get(env.art.thumbnails.url), blob)
+ assert.equal(env.attached.size, 0)
+ env.art.emit('destroy')
+ assert.equal(env.urls.size, 0)
+})
+
+test('Auto-thumbnail clears allocated dimensions if context creation fails', async () => {
+ const env = await setup()
+ env.controls.nullContext = true
+ env.metadata()
+ assert.deepEqual([env.canvases[0].width, env.canvases[0].height], [0, 0])
+ assert.equal(env.warnings.length, 1)
+ assert.equal(env.attached.size, 0)
+ env.art.emit('destroy')
+})
+
+test('Auto-thumbnail still clears height and decoder if the width reset throws', async () => {
+ const env = await setup()
+ env.metadata()
+ const canvas = env.canvases[0]
+ const failure = new Error('Width reset failed')
+ Object.defineProperty(canvas, 'width', {
+ get: () => 800,
+ set() {
+ throw failure
+ },
+ })
+ env.art.emit('destroy')
+ assert.equal(canvas.height, 0)
+ assert.equal(env.attached.size, 0)
+ assert.equal(env.videos[0].src, undefined)
+ assert.equal(env.timers.size, 0)
+ assert.equal(env.warnings.length, 1)
+ assert.equal(env.warnings[0][1], failure)
+})
+
+test('Auto-thumbnail does not allocate height after a width setter closes the job', async () => {
+ const env = await setup()
+ const createElement = env.box.document.createElement
+ env.box.document.createElement = function (name) {
+ const element = createElement.call(this, name)
+ if (name === 'canvas') {
+ let width = 0
+ Object.defineProperty(element, 'width', {
+ get: () => width,
+ set(value) {
+ width = value
+ if (value > 0)
+ env.art.emit('destroy')
+ },
+ })
+ }
+ return element
+ }
+ env.metadata()
+ assert.deepEqual([env.canvases[0].width, env.canvases[0].height], [0, 0])
+ assert.equal(env.attached.size, 0)
+ assert.equal(env.blobs.length, 0)
+})
diff --git a/test/auto-thumbnail-lifecycle.test.js b/test/auto-thumbnail-lifecycle.test.js
index e9a085bf4..753140a41 100644
--- a/test/auto-thumbnail-lifecycle.test.js
+++ b/test/auto-thumbnail-lifecycle.test.js
@@ -269,6 +269,7 @@ test('Auto-thumbnail candidate retains coercible numeric strings and fractional
for (const option of [{ width: '80', number: '2', scale: '0.5' }, { width: 20.5, number: 2.5, scale: 1 }]) {
const env = await setup(option)
env.metadata()
+ assert.equal(env.canvases[0].width, Math.trunc(Number(option.width) * 10))
for (let index = 0; index < Math.ceil(Number(option.number)); index++) {
env.seeked()
env.finish(index)
@@ -277,7 +278,7 @@ test('Auto-thumbnail candidate retains coercible numeric strings and fractional
assert.equal(env.updates[0].width, option.width)
assert.equal(env.updates[0].number, option.number)
assert.equal(env.updates[0].scale, option.scale)
- assert.equal(env.canvases[0].width, Math.trunc(Number(option.width) * 10))
+ assert.deepEqual([env.canvases[0].width, env.canvases[0].height], [0, 0])
assertClosed(env)
env.art.emit('destroy')
assert.equal(env.urls.size, 0)
diff --git a/test/browser/auto-thumbnail-lifecycle.spec.js b/test/browser/auto-thumbnail-lifecycle.spec.js
index 3470c1a06..278b892f2 100644
--- a/test/browser/auto-thumbnail-lifecycle.spec.js
+++ b/test/browser/auto-thumbnail-lifecycle.spec.js
@@ -9,13 +9,15 @@ for (const scenario of ['destroy', 'restart', 'complete', 'frame-destroy', 'fram
await page.setContent('