From eb6402fdca910fd39d329d0c26222c4359915150 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Mon, 14 Sep 2026 14:22:45 +0800 Subject: [PATCH] fix(PKG-AUTO-THUMB-11): isolate frame handler cleanup failures --- .../artplayer-plugin-auto-thumbnail.js | 2 +- .../artplayer-plugin-auto-thumbnail.legacy.js | 2 +- .../artplayer-plugin-auto-thumbnail.mjs | 14 +- .../ARCHITECTURE.md | 16 +- .../dist/artplayer-plugin-auto-thumbnail.js | 2 +- .../artplayer-plugin-auto-thumbnail.legacy.js | 2 +- .../dist/artplayer-plugin-auto-thumbnail.mjs | 14 +- .../src/frames.ts | 10 +- .../auto-thumbnail-handler-validation.json | 2700 +++++++++++++++++ ...09-14-PKG-AUTO-THUMB-11-handler-cleanup.md | 55 + refactor/plan.md | 10 +- refactor/progress.md | 11 + refactor/risk-table.md | 2 +- refactor/risks.json | 10 +- refactor/tasks.json | 24 +- test/auto-thumbnail-frames.test.js | 54 + 16 files changed, 2892 insertions(+), 36 deletions(-) create mode 100644 refactor/baselines/auto-thumbnail-handler-validation.json create mode 100644 refactor/changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.js b/docs/compiled/artplayer-plugin-auto-thumbnail.js index cd83d106e..838510cf5 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,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())}),l=()=>{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)},s=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");l()}else u.sought=!0,c()}),l())});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?s():i.onloadeddata=s)}}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 l=document.createElement("canvas");if(e.own(()=>{l.height=0}),e.own(()=>{l.width=0}),!e.active())return;if(l.width=d,!e.active())return;if(l.height=c,!e.active())return;const s=l.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,n),h=function(e,t){let i;function n(){const e=i;i=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(n),r=>{if(!e.active())return;if(i)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};i=o;const a=()=>e.active()&&i===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(n(),e.active()&&r(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,l);let m=0;const v=e.guard(()=>{m>=i.number?e.dispose():f(r*m/i.number,e.guard(()=>{s.drawImage(n,m%10*i.width,Math.floor(m/10)*u,i.width,u),e.active()&&h(t=>{e.publish(t,{height:u,column:10,number:i.number,width:i.width,scale:i.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=i.url))}function n(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 l=!1;const s={active:()=>!o&&!l&&n===s,own(t){if(l){const n=e([t]);n.failed&&i(n.failure)}else c.push(t)},dispose(){if(l)return;l=!0,n===s&&(n=void 0);const t=e(c.splice(0).reverse());t.failed&&i(t.failure)},fail(e){s.active()&&(s.dispose(),i(e))},guard:e=>(...t)=>{if(s.active())try{return e(...t)}catch(i){s.fail(i)}},publish(n,o){if(!s.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!s.active())return void u(d);const c=r;r=d;try{t({url:d,...o})}catch(l){r===d&&(r=void 0!==c&&a.has(c)?c:void 0);const t=e([()=>u(d)]);throw t.failed&&i(t.failure),l}u(c)}};return n=s,d?.dispose(),s},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"}}}return n.default=n,n}); +!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;r=void 0;const n=e([()=>{i.onloadeddata=null},()=>{i.onseeked=null},()=>{t&&null!==t.timer&&clearTimeout(t.timer)},()=>{t&&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())}),l=()=>{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)},s=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");l()}else u.sought=!0,c()}),l())});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?s():i.onloadeddata=s)}}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 l=document.createElement("canvas");if(e.own(()=>{l.height=0}),e.own(()=>{l.width=0}),!e.active())return;if(l.width=d,!e.active())return;if(l.height=c,!e.active())return;const s=l.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,n),h=function(e,t){let i;function n(){const e=i;i=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(n),r=>{if(!e.active())return;if(i)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};i=o;const a=()=>e.active()&&i===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(n(),e.active()&&r(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,l);let m=0;const v=e.guard(()=>{m>=i.number?e.dispose():f(r*m/i.number,e.guard(()=>{s.drawImage(n,m%10*i.width,Math.floor(m/10)*u,i.width,u),e.active()&&h(t=>{e.publish(t,{height:u,column:10,number:i.number,width:i.width,scale:i.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=i.url))}function n(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 l=!1;const s={active:()=>!o&&!l&&n===s,own(t){if(l){const n=e([t]);n.failed&&i(n.failure)}else c.push(t)},dispose(){if(l)return;l=!0,n===s&&(n=void 0);const t=e(c.splice(0).reverse());t.failed&&i(t.failure)},fail(e){s.active()&&(s.dispose(),i(e))},guard:e=>(...t)=>{if(s.active())try{return e(...t)}catch(i){s.fail(i)}},publish(n,o){if(!s.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!s.active())return void u(d);const c=r;r=d;try{t({url:d,...o})}catch(l){r===d&&(r=void 0!==c&&a.has(c)?c:void 0);const t=e([()=>u(d)]);throw t.failed&&i(t.failure),l}u(c)}};return n=s,d?.dispose(),s},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"}}}return n.default=n,n}); diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js b/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js index 3aad2ba09..ca9b20844 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.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),h=function(e,t){let n;function r(){const e=n;n=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(r),i=>{if(!e.active())return;if(n)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};n=o;const a=()=>e.active()&&n===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(r(),e.active()&&i(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,c);let m=0;const v=e.guard(()=>{m>=t.number?e.dispose():f(r*m/t.number,e.guard(()=>{s.drawImage(n,m%10*t.width,Math.floor(m/10)*a,t.width,a),e.active()&&h(n=>{e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=t.url))}function l(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}}return l.default=l,l}); +!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,i=Object.prototype.propertyIsEnumerable,r=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i;function o(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 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)&&r(e,a,o[a]);if(t)for(var a of t(o))i.call(o,a)&&r(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 i;function r(){const e=i;i=void 0;const n=o([()=>{t.onloadeddata=null},()=>{t.onseeked=null},()=>{e&&null!==e.timer&&clearTimeout(e.timer)},()=>{e&&null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(r),(o,a)=>{if(r(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};i=u;const d=()=>e.active()&&i===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(r(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const i=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&t.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&i===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&i===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 i=n.duration,r=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),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}}(t,{duration:i,videoHeight:r,videoWidth:o});if(!e.active())return;n.width=o,n.height=r,n.style.width=`${o}px`,n.style.height=`${r}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),h=function(e,t){let n;function i(){const e=n;n=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(i),r=>{if(!e.active())return;if(n)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};n=o;const a=()=>e.active()&&n===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(i(),e.active()&&r(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,c);let m=0;const v=e.guard(()=>{m>=t.number?e.dispose():f(i*m/t.number,e.guard(()=>{s.drawImage(n,m%10*t.width,Math.floor(m/10)*a,t.width,a),e.active()&&h(n=>{e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=t.url))}function l(e){return t=>{return n=null,i=null,r=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),i=a(e=>{t.thumbnails=e},n),r=[],u=()=>{const n=i.start();n&&n.guard(()=>{const i=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,i)})()},l=()=>{const e=r.splice(0),t=o([i.destroy,...e]);t.failed&&n(t.failure)};try{const e=[["destroy",l],["restart",i.cancel],["video:loadedmetadata",u]];for(const[n,o]of e){if(i.closed)break;r.push(()=>t.off(n,o)),t.on(n,o),i.closed&&t.off(n,o)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(r.next(e))}catch(n){t(n)}},a=e=>{try{u(r.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((r=r.apply(n,i)).next())});var n,i,r}}return l.default=l,l}); diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.mjs b/docs/compiled/artplayer-plugin-auto-thumbnail.mjs index 596b83d7f..820c93f06 100644 --- a/docs/compiled/artplayer-plugin-auto-thumbnail.mjs +++ b/docs/compiled/artplayer-plugin-auto-thumbnail.mjs @@ -165,17 +165,19 @@ function createFrameReader(job, video) { function clear() { const previous = pending; pending = void 0; - video.onloadeddata = null; - video.onseeked = null; - if (!previous) - return; const result = cleanupAll([ () => { - if (previous.timer !== null) + video.onloadeddata = null; + }, + () => { + video.onseeked = null; + }, + () => { + if (previous && previous.timer !== null) clearTimeout(previous.timer); }, () => { - if (previous.frame !== null) + if (previous && previous.frame !== null) video.cancelVideoFrameCallback(previous.frame); } ]); diff --git a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md index 23f669c99..1b24b7d63 100644 --- a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md +++ b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md @@ -52,6 +52,10 @@ for video processing. presentation before drawing. Callback identities are invalidated on retry and replacement; late/duplicate delivery cannot complete a newer sample. One job cleanup handles every frame without accumulating per-frame cleanup closures. + It invalidates the pending sample before independently clearing each handler, + deadline and frame callback. A throwing event-property setter cannot prevent + the remaining releases. The first cleanup error reaches the existing warning + path; even a handler that cannot be removed becomes inert after cancellation. - `src/types.ts` defines internal configuration, sheet data, the minimal host and guarded extraction-job callbacks. `guard` retains argument/result types and the inactive undefined result. It shares the opt-in `runtime-api.d.ts` option type; @@ -206,14 +210,16 @@ red, black, blue and yellow sections. Its command and fingerprint are in directory with `ARTPLAYER_FFMPEG` and `node scripts/generate-auto-thumbnail-fixture.mjs `; never silently replace the committed fixture. -Task03's internal-type checkpoint has converted the six runtime modules to strict -TypeScript. Numeric annotations describe the nominal options; no runtime casts or +The current implementation has eight strict TypeScript modules, including the +frame reader and encoder split after task03's initial checkpoint. Numeric +annotations describe the nominal options; no runtime casts or normalization were added, so historical coercible JS inputs retain the same validation/arithmetic and raw values. Missing options retain the existing behavior. -Task04 still resolves public declarations, implementation-to-consumer contracts and -historical CommonJS differences. Task05 verifies old/final cores and actual +Tasks08/09 supplied the compatible public declarations, implementation assignment +fixtures and historical CommonJS alias. Task04 still requires the completed task03 +implementation and final migration acceptance. Task05 verifies old/final cores and actual devices; task06 covers installed package entries and the real demo/editor. The -types and package version are unchanged at this checkpoint. Use +package version remains unchanged. Use `refactor/baselines/auto-thumbnail-contract.md` and `auto-thumbnail-failures.md` as the historical evidence map; do not regenerate those baselines from this source. 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 cd83d106e..838510cf5 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,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())}),l=()=>{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)},s=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");l()}else u.sought=!0,c()}),l())});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?s():i.onloadeddata=s)}}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 l=document.createElement("canvas");if(e.own(()=>{l.height=0}),e.own(()=>{l.width=0}),!e.active())return;if(l.width=d,!e.active())return;if(l.height=c,!e.active())return;const s=l.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,n),h=function(e,t){let i;function n(){const e=i;i=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(n),r=>{if(!e.active())return;if(i)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};i=o;const a=()=>e.active()&&i===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(n(),e.active()&&r(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,l);let m=0;const v=e.guard(()=>{m>=i.number?e.dispose():f(r*m/i.number,e.guard(()=>{s.drawImage(n,m%10*i.width,Math.floor(m/10)*u,i.width,u),e.active()&&h(t=>{e.publish(t,{height:u,column:10,number:i.number,width:i.width,scale:i.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=i.url))}function n(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 l=!1;const s={active:()=>!o&&!l&&n===s,own(t){if(l){const n=e([t]);n.failed&&i(n.failure)}else c.push(t)},dispose(){if(l)return;l=!0,n===s&&(n=void 0);const t=e(c.splice(0).reverse());t.failed&&i(t.failure)},fail(e){s.active()&&(s.dispose(),i(e))},guard:e=>(...t)=>{if(s.active())try{return e(...t)}catch(i){s.fail(i)}},publish(n,o){if(!s.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!s.active())return void u(d);const c=r;r=d;try{t({url:d,...o})}catch(l){r===d&&(r=void 0!==c&&a.has(c)?c:void 0);const t=e([()=>u(d)]);throw t.failed&&i(t.failure),l}u(c)}};return n=s,d?.dispose(),s},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"}}}return n.default=n,n}); +!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;r=void 0;const n=e([()=>{i.onloadeddata=null},()=>{i.onseeked=null},()=>{t&&null!==t.timer&&clearTimeout(t.timer)},()=>{t&&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())}),l=()=>{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)},s=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");l()}else u.sought=!0,c()}),l())});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?s():i.onloadeddata=s)}}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 l=document.createElement("canvas");if(e.own(()=>{l.height=0}),e.own(()=>{l.width=0}),!e.active())return;if(l.width=d,!e.active())return;if(l.height=c,!e.active())return;const s=l.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,n),h=function(e,t){let i;function n(){const e=i;i=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(n),r=>{if(!e.active())return;if(i)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};i=o;const a=()=>e.active()&&i===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(n(),e.active()&&r(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,l);let m=0;const v=e.guard(()=>{m>=i.number?e.dispose():f(r*m/i.number,e.guard(()=>{s.drawImage(n,m%10*i.width,Math.floor(m/10)*u,i.width,u),e.active()&&h(t=>{e.publish(t,{height:u,column:10,number:i.number,width:i.width,scale:i.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=i.url))}function n(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 l=!1;const s={active:()=>!o&&!l&&n===s,own(t){if(l){const n=e([t]);n.failed&&i(n.failure)}else c.push(t)},dispose(){if(l)return;l=!0,n===s&&(n=void 0);const t=e(c.splice(0).reverse());t.failed&&i(t.failure)},fail(e){s.active()&&(s.dispose(),i(e))},guard:e=>(...t)=>{if(s.active())try{return e(...t)}catch(i){s.fail(i)}},publish(n,o){if(!s.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!s.active())return void u(d);const c=r;r=d;try{t({url:d,...o})}catch(l){r===d&&(r=void 0!==c&&a.has(c)?c:void 0);const t=e([()=>u(d)]);throw t.failed&&i(t.failure),l}u(c)}};return n=s,d?.dispose(),s},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"}}}return n.default=n,n}); 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 3aad2ba09..ca9b20844 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.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),h=function(e,t){let n;function r(){const e=n;n=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(r),i=>{if(!e.active())return;if(n)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};n=o;const a=()=>e.active()&&n===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(r(),e.active()&&i(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,c);let m=0;const v=e.guard(()=>{m>=t.number?e.dispose():f(r*m/t.number,e.guard(()=>{s.drawImage(n,m%10*t.width,Math.floor(m/10)*a,t.width,a),e.active()&&h(n=>{e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=t.url))}function l(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}}return l.default=l,l}); +!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,i=Object.prototype.propertyIsEnumerable,r=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i;function o(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 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)&&r(e,a,o[a]);if(t)for(var a of t(o))i.call(o,a)&&r(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 i;function r(){const e=i;i=void 0;const n=o([()=>{t.onloadeddata=null},()=>{t.onseeked=null},()=>{e&&null!==e.timer&&clearTimeout(e.timer)},()=>{e&&null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(r),(o,a)=>{if(r(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};i=u;const d=()=>e.active()&&i===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(r(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const i=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&t.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&i===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&i===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 i=n.duration,r=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),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}}(t,{duration:i,videoHeight:r,videoWidth:o});if(!e.active())return;n.width=o,n.height=r,n.style.width=`${o}px`,n.style.height=`${r}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),h=function(e,t){let n;function i(){const e=n;n=void 0,e&&null!==e.timer&&clearTimeout(e.timer)}return e.own(i),r=>{if(!e.active())return;if(n)throw new Error("Auto-thumbnail encoding is already pending");const o={timer:null};n=o;const a=()=>e.active()&&n===o;o.timer=setTimeout(e.guard(()=>{if(a())throw new Error("Auto-thumbnail encoding timed out")}),3e4),a()?t.toBlob(e.guard(t=>{a()&&(i(),e.active()&&r(t))}),"image/jpeg"):clearTimeout(o.timer)}}(e,c);let m=0;const v=e.guard(()=>{m>=t.number?e.dispose():f(i*m/t.number,e.guard(()=>{s.drawImage(n,m%10*t.width,Math.floor(m/10)*a,t.width,a),e.active()&&h(n=>{e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(m+=1,v())})}))});v()}),e.active()&&(n.src=t.url))}function l(e){return t=>{return n=null,i=null,r=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),i=a(e=>{t.thumbnails=e},n),r=[],u=()=>{const n=i.start();n&&n.guard(()=>{const i=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,i)})()},l=()=>{const e=r.splice(0),t=o([i.destroy,...e]);t.failed&&n(t.failure)};try{const e=[["destroy",l],["restart",i.cancel],["video:loadedmetadata",u]];for(const[n,o]of e){if(i.closed)break;r.push(()=>t.off(n,o)),t.on(n,o),i.closed&&t.off(n,o)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(r.next(e))}catch(n){t(n)}},a=e=>{try{u(r.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((r=r.apply(n,i)).next())});var n,i,r}}return l.default=l,l}); 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 596b83d7f..820c93f06 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 @@ -165,17 +165,19 @@ function createFrameReader(job, video) { function clear() { const previous = pending; pending = void 0; - video.onloadeddata = null; - video.onseeked = null; - if (!previous) - return; const result = cleanupAll([ () => { - if (previous.timer !== null) + video.onloadeddata = null; + }, + () => { + video.onseeked = null; + }, + () => { + if (previous && previous.timer !== null) clearTimeout(previous.timer); }, () => { - if (previous.frame !== null) + if (previous && previous.frame !== null) video.cancelVideoFrameCallback(previous.frame); } ]); diff --git a/packages/artplayer-plugin-auto-thumbnail/src/frames.ts b/packages/artplayer-plugin-auto-thumbnail/src/frames.ts index 5e072bfc3..52bc1ff33 100644 --- a/packages/artplayer-plugin-auto-thumbnail/src/frames.ts +++ b/packages/artplayer-plugin-auto-thumbnail/src/frames.ts @@ -19,17 +19,15 @@ export default function createFrameReader(job: ExtractionJob, video: HTMLVideoEl function clear() { const previous = pending pending = undefined - video.onloadeddata = null - video.onseeked = null - if (!previous) - return const result = cleanupAll([ + () => { video.onloadeddata = null }, + () => { video.onseeked = null }, () => { - if (previous.timer !== null) + if (previous && previous.timer !== null) clearTimeout(previous.timer) }, () => { - if (previous.frame !== null) + if (previous && previous.frame !== null) video.cancelVideoFrameCallback(previous.frame) }, ]) diff --git a/refactor/baselines/auto-thumbnail-handler-validation.json b/refactor/baselines/auto-thumbnail-handler-validation.json new file mode 100644 index 000000000..97c911c5a --- /dev/null +++ b/refactor/baselines/auto-thumbnail-handler-validation.json @@ -0,0 +1,2700 @@ +{ + "task": "PKG-AUTO-THUMB-11", + "date": "2026-09-14", + "baselineCommit": "a8d19ce5ef8af8fe4c48424753742e8d78a77962", + "node": "24.21.0", + "yarn": "1.22.22", + "scope": "Independent cleanup under deliberately throwing event-property setters; native normal-lifecycle regression is separate from controlled faults and first-frame acceptance.", + "before": { + "artifact": { + "path": "refactor/.cache/auto-handler-before.js", + "sha256": "a9a1704f39ebd2ef6612cc146acf4ebd0d8eeb73764cb852320a85d31438ab01" + }, + "log": { + "path": "refactor/.cache/auto-handler-red-final.log", + "sha256": "e723e6966a08fb9b7e94becded05574f55ebd1315f817794db087b551db0aded" + }, + "exitCode": 1, + "pass": 19, + "fail": 8 + }, + "tests": [ + { + "log": { + "path": "refactor/.cache/auto-handler-green.log", + "sha256": "3036e82c097b5aed9afe4f595af22412d51b9301cb7fe15a19049ee0a553bfb8" + }, + "exitCode": 0, + "pass": 27, + "fail": 0 + }, + { + "log": { + "path": "refactor/.cache/auto-handler-suite.log", + "sha256": "e0a8c5bc18a0364c3d7a2920f38333fd4f215a73b6993129ed9632746df5cec7" + }, + "exitCode": 0, + "pass": 182, + "fail": 0 + }, + { + "log": { + "path": "refactor/.cache/auto-handler-js.log", + "sha256": "89d0e6ca56bdf6a072ffdf6164ccf22c9861239cd2dc8adb06a20a6f1e4096d4" + }, + "exitCode": 0, + "pass": 27, + "fail": 0 + }, + { + "log": { + "path": "refactor/.cache/auto-handler-legacy.js.log", + "sha256": "3d4ef5df38bf89dc5f9d6f5c3887c7242f3201a2fb3b38f3fbaed8b706255290" + }, + "exitCode": 0, + "pass": 27, + "fail": 0 + } + ], + "files": [ + { + "path": "packages/artplayer-plugin-auto-thumbnail/src/frames.ts", + "sha256": "b3e6a286cee6b365bf0a70abd2ce48f766d6e591f82fba24573ee3fcefe68021" + }, + { + "path": "test/auto-thumbnail-frames.test.js", + "sha256": "a7a987e37232a5dac8316a5ffdfc9f73ca6c8efd13a5b3b3f9099b4d43ea7eb8" + }, + { + "path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js", + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357" + }, + { + "path": "docs/compiled/artplayer-plugin-auto-thumbnail.js", + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357" + }, + { + "path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js", + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c" + }, + { + "path": "docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js", + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c" + }, + { + "path": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs", + "sha256": "7c4ea237f03eaae0cbc06b27782ba03ee8e3639ec78f0de03c11841467f22a17" + }, + { + "path": "docs/compiled/artplayer-plugin-auto-thumbnail.mjs", + "sha256": "7c4ea237f03eaae0cbc06b27782ba03ee8e3639ec78f0de03c11841467f22a17" + } + ], + "browser": { + "main": { + "report": { + "path": "refactor/.cache/auto-handler-main-browser/report.json", + "sha256": "8708e65a0215d9264cfd1e54dfbb8fa8462af0dd57da4d3c39c007a12b233649" + }, + "stats": { + "startTime": "2026-09-14T06:19:24.911Z", + "duration": 13697.495, + "expected": 21, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "destroy", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "restart", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "frame-destroy", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "frame-restart", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native alias-complete owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": true, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native encoding-timeout owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "encoding-timeout", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [ + [ + "ArtPlayer auto-thumbnail failed:", + "Error: Auto-thumbnail encoding timed out" + ] + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "destroy", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "restart", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "frame-destroy", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "frame-restart", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native alias-complete owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": true, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native encoding-timeout owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "encoding-timeout", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [ + [ + "ArtPlayer auto-thumbnail failed:", + "Error: Auto-thumbnail encoding timed out" + ] + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "destroy", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "restart", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "frame-destroy", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "frame-restart", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native alias-complete owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": true, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native encoding-timeout owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "encoding-timeout", + "useDefault": false, + "sha256": "4c8bdd75f49ad0514167e1b67c6941236c27837d09dd1d5d69d0345d69fa0357", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [ + [ + "ArtPlayer auto-thumbnail failed:", + "Error: Auto-thumbnail encoding timed out" + ] + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + } + ] + }, + "legacy": { + "report": { + "path": "refactor/.cache/auto-handler-legacy-browser/report.json", + "sha256": "6daaab48303cf3c51be5a39ceb0d816693155134037a908e14e628b47769b3fd" + }, + "stats": { + "startTime": "2026-09-14T06:20:19.573Z", + "duration": 12821.398, + "expected": 21, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "destroy", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "restart", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "frame-destroy", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "frame-restart", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native alias-complete owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": true, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native encoding-timeout owns decoder and encoded URLs", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "native": { + "scenario": "encoding-timeout", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [ + [ + "ArtPlayer auto-thumbnail failed:", + "Error: Auto-thumbnail encoding timed out" + ] + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "destroy", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "restart", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "frame-destroy", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "frame-restart", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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": [], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native alias-complete owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": true, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native encoding-timeout owns decoder and encoded URLs", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "native": { + "scenario": "encoding-timeout", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [ + [ + "ArtPlayer auto-thumbnail failed:", + "Error: Auto-thumbnail encoding timed out" + ] + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "destroy", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "restart", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "frame-destroy", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "frame-restart", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native alias-complete owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "complete", + "useDefault": true, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + }, + { + "title": "Auto-thumbnail candidate native encoding-timeout owns decoder and encoded URLs", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "native": { + "scenario": "encoding-timeout", + "useDefault": false, + "sha256": "389239c482d89639c70c796f2220a552ed8b1a87062fc5950cb03dc14c2b525c", + "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 + } + ], + "pendingDeadlines": 0, + "warnings": [ + [ + "ArtPlayer auto-thumbnail failed:", + "Error: Auto-thumbnail encoding timed out" + ] + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + } + } + ] + } + }, + "checks": { + "packageStrictTypes": "pass", + "targetedLint": "pass", + "strictToolchain": "pass", + "coreDeclarations": { + "files": 37, + "changed": 0 + }, + "build": { + "path": "refactor/.cache/auto-handler-build.log", + "sha256": "6ace060991a131daf66c3c13fc7a8a5c7581f20577b02ac74dc86c134a5be201" + } + }, + "remaining": [ + "AUTO-THUMB-PIXEL-01", + "AUTO-THUMB-LIFE-01", + "PKG-AUTO-THUMB-03", + "PKG-AUTO-THUMB-04", + "PKG-AUTO-THUMB-05", + "PKG-AUTO-THUMB-06" + ] +} diff --git a/refactor/changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md new file mode 100644 index 000000000..c4f353548 --- /dev/null +++ b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md @@ -0,0 +1,55 @@ +# PKG-AUTO-THUMB-11: independent frame cleanup + +Baseline: a8d19ce5ef8af8fe4c48424753742e8d78a77962. This task fixes one +resource boundary under the unfinished task03 implementation; task04 depends on +both. It does not close AUTO-THUMB-LIFE-01 or AUTO-THUMB-PIXEL-01. + +## Defect and implementation + +The frame reader invalidated its pending token, then cleared onloadeddata and +onseeked before entering cleanupAll. A throwing property setter skipped deadline +and native frame cancellation; outer job cleanup still removed the decoder and +canvas, leaving those handles registered. Controlled setters reproduce this in +both destroy and restart, while awaiting loaded data and when completing a frame. +These are deliberately injected exceptions, not evidence of spontaneous native +IDL setter failure in an ordinary browser. + +frames.ts now submits both handler clears and both handle releases to the same +cleanupAll operation. It still invalidates identity first, preserves handle zero, +and propagates the first error through the existing job warning path. A handler +that cannot be removed remains inert after disposal. A failure while finishing +prevents drawing/publication, and replacement work can still produce a preview. +No new abstraction, dependency, lockfile, script or public declaration is needed. + +API-04/05/06/09/11 retain event names, registration Promise, result, factory alias, +options, ten-column layout, sample times, progressive URLs and distribution paths. +Only the exceptional cleanup path changes; normal behavior has regression coverage. +The package architecture describes current eight-module ownership and remaining +acceptance work. This also corrects stale prose predating type/alias tasks08/09. + +## Validation + +See [fingerprints and native results](../baselines/auto-thumbnail-handler-validation.json). + +- Final 27-case frame suite against the exact pre-change main: 19 pass, all eight + new fault regressions fail. Current source, actual main and legacy: 27 pass each. +- Full package suite: 182 pass, including frozen contracts and type regressions. +- Strict package TypeScript passes; targeted read-only lint passes after fixing + a missing test brace. Pinned Node24.21.0/Yarn1.22.22 toolchain passes; core public + declarations remain 37 files with zero drift. +- Normal build regenerates main/legacy/ESM and the corresponding docs copies. +- Actual main and legacy each pass 21 native lifecycle cases across Windows + Chromium, Firefox and WebKit, with zero retries or skips. Native HTTP decoding, + seeking, JPEG, complete/restart/destroy, held frame/Blob delivery, encoding + timeout and URL cleanup use a stub ArtPlayer host. The throwing-setter cases + are controlled Node tests, not native browser fault-injection tests. +- No first-frame pixel rerun, physical-device acceptance, full old/new core + combinations, remote CI or npm publication is claimed. Existing pixel evidence + and failure remain unchanged; tasks03/04/05/06 remain unfinished. + +## Rollback and next work + +Revert this task's dedicated commit and rebuild the package to recover the +previous behavior; the new regression tests document the restored leak. Continue +task03 resource-boundary review and obtain a different native backend/device for +the outstanding first-frame issue rather than relaxing its assertions. diff --git a/refactor/plan.md b/refactor/plan.md index fb35374ca..a8b6e86cd 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -2,9 +2,9 @@ > 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。 -基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 251 项,范围 22 个包及工作区/示例。 +基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 252 项,范围 22 个包及工作区/示例。 -状态:todo 55 / doing 17 / blocked 0 / done 179 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 55 / doing 17 / blocked 0 / done 180 / 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, PKG-AUTO-THUMB-07, PKG-AUTO-THUMB-08, PKG-AUTO-THUMB-09, PKG-AUTO-THUMB-10 | +| 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, PKG-AUTO-THUMB-08, PKG-AUTO-THUMB-09, PKG-AUTO-THUMB-10, PKG-AUTO-THUMB-11 | | 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 | @@ -176,13 +176,14 @@ | 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, 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, PKG-AUTO-THUMB-08, PKG-AUTO-THUMB-09, PKG-AUTO-THUMB-10 | 抽帧结果和真实异步插件返回类型兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo | +| PKG-AUTO-THUMB-04 | artplayer-plugin-auto-thumbnail
迁移自有源码和公开类型 | PKG-AUTO-THUMB-03, ENG-04, ENG-06, CORE-07, PKG-AUTO-THUMB-08, PKG-AUTO-THUMB-09, PKG-AUTO-THUMB-10, PKG-AUTO-THUMB-11 | 抽帧结果和真实异步插件返回类型兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | 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 | | PKG-AUTO-THUMB-08 | artplayer-plugin-auto-thumbnail
保留旧声明并提供准确异步类型入口 | PKG-AUTO-THUMB-02, ENG-04, ENG-06, CORE-07 | 保留实际 npm 1.1.0 根声明;新增同实现 /runtime 类型、编辑器声明及隔离安装消费者 | 原 Parameters/ReturnType/替代函数不变;准确 Promise 类型正反例、CJS/ESM 条件入口和声明解析通过;不把像素与设备缺口计为通过 | M | done | | PKG-AUTO-THUMB-09 | artplayer-plugin-auto-thumbnail
兼容旧 CommonJS default 与直接工厂调用 | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-08 | 同一 callable 上的 default 自别名,保留最新根声明并补准确 runtime 别名类型;旧/新实际包入口证据 | 旧红新绿、直接/default/ESM/legacy/global 调用一致;旧根类型提取不变,公开准确类型与实际别名匹配;不关闭抽帧像素和设备缺口 | M | done | | PKG-AUTO-THUMB-10 | artplayer-plugin-auto-thumbnail
为渐进 JPEG 编码建立取消时限与独立资源归属 | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-07 | 拆分私有 canvas 编码模块,一次一帧的30秒编码时限,完成/失败/切源/销毁取消;迟到和重复回调无副作用 | 受控回调未返回的旧红新绿、正常JPEG和前一预览保持、同步重入/清理失败有回归;真实三浏览器编码/取消通过,公开时间公式与入口不变,首帧风险独立保留 | M | done | +| PKG-AUTO-THUMB-11 | artplayer-plugin-auto-thumbnail
保证抽帧事件属性清理异常不截断其他资源释放 | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-07, PKG-AUTO-THUMB-10 | 独立清除事件属性、抽帧时限和原生帧回调;首个异常保留,过期回调失效,替换任务可继续 | 八个故障注入用例旧红新绿,实际main/legacy抽帧与三浏览器生命周期回归通过;文档和构建同步,首帧及整体资源验收不冒充完成 | M | done | ## 5 包迁移:artplayer-plugin-vtt-thumbnail @@ -653,3 +654,4 @@ - PKG-MULTI-SUB-09: [记录](changes/2026-09-14-PKG-MULTI-SUB-09-switch-order.md) [记录](baselines/multiple-subtitles-switch-validation.json) - PKG-MULTI-SUB-10: [记录](changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md) [记录](baselines/multiple-subtitles-legacy-validation.json) - PKG-MULTI-SUB-11: [记录](changes/2026-09-14-PKG-MULTI-SUB-11-ass-conversion.md) [记录](baselines/multiple-subtitles-ass-validation.json) +- PKG-AUTO-THUMB-11: [记录](changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md) [记录](baselines/auto-thumbnail-handler-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index c6b8ff7d5..c77c22e8c 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,16 @@ # 进度与证据 +## PKG-AUTO-THUMB-11 抽帧清理异常修复完成 + +事件属性清除现在与定时器、原生帧回调取消分别执行,单个异常不截断剩余清理。 +八个受控故障用例在旧main全部失败,新源码/main/legacy各27项抽帧回归通过; +完整插件182项通过,实际main/legacy各21项三浏览器生命周期通过。异常属性由测试 +主动注入,不声称浏览器原生setter自行失败;原生验证使用真实解码和桩宿主。 +严格TS、定向lint、固定工具链、37声明零漂移通过,三格式和docs副本已重建。 +见[变更](changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md)与[证据](baselines/auto-thumbnail-handler-validation.json)。252项:180 done、17 doing、55 todo。 +任务03仍doing,04/05/06仍todo,首帧像素与完整资源验收风险保持open。下一步继续 +Auto Thumbnail资源边界与剩余插件工作;首帧需要另一原生后端/设备证据,不能靠延时放宽。 + ## PKG-MULTI-SUB-05 切源时序诊断检查点(仍未完成) 新增可选原生属性/事件跟踪和独立恢复事件等待对照。WebKit串行3项、重复9项及 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 86c2e8144..89f9a8983 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -186,7 +186,7 @@ | AUTO-THUMB-TYPE-01 | open / 已复现 | Published declarations misdescribe async registration and switch height/number option fields across generations | PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-08 | | AUTO-THUMB-EXPORT-01 | open / 已复现 | Published 1.0.1 CommonJS default namespace differs from the direct function exported by 1.1.0 | PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-06, PKG-AUTO-THUMB-09 | | AUTO-THUMB-DIST-01 | open / 已复现 | Published 1.0.0 archive lacks declared main and legacy runtimes | PKG-AUTO-THUMB-06 | -| AUTO-THUMB-LIFE-01 | open / 已复现 | Extraction tasks, decoder handlers and final object URLs lack source-switch and destroy ownership | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 | +| AUTO-THUMB-LIFE-01 | open / 已复现 | Extraction tasks, decoder handlers and final object URLs lack source-switch and destroy ownership | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-11 | | AUTO-THUMB-ENCODE-01 | open / 已复现 | Unserialized encodes include blank output and lack media/canvas/input failure handling | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 | | AUTO-THUMB-PIXEL-01 | open / 已复现 | Historical auto-thumbnail yields black sampled JPEG cells on Windows WebKit while played native video and static JPEG controls yield color | PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 | | VTT-THUMB-DIST-01 | open / 已复现 | Historical 1.0.0 main/legacy contain invalid regular expressions and cannot load | PKG-VTT-THUMB-06 | diff --git a/refactor/risks.json b/refactor/risks.json index 7b56faa05..963bdf245 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4124,7 +4124,8 @@ "owners": [ "PKG-AUTO-THUMB-02", "PKG-AUTO-THUMB-03", - "PKG-AUTO-THUMB-05" + "PKG-AUTO-THUMB-05", + "PKG-AUTO-THUMB-11" ], "evidence": [ "refactor/baselines/auto-thumbnail-release.json", @@ -4135,11 +4136,14 @@ "refactor/baselines/auto-thumbnail-failures-validation.json", "test/auto-thumbnail-lifecycle.test.js", "test/browser/auto-thumbnail-lifecycle.spec.js", - "refactor/baselines/auto-thumbnail-lifecycle-checkpoint.json" + "refactor/baselines/auto-thumbnail-lifecycle-checkpoint.json", + "refactor/changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md", + "refactor/baselines/auto-thumbnail-handler-validation.json", + "test/auto-thumbnail-frames.test.js" ], "compatibleResolution": "Cancel obsolete work and release owned resources while retaining live options and progressive usable sheet updates.", "closureCriteria": "Delayed callbacks, repeated metadata, switching, destroy, multiple instances and failure cleanup are reproduced then pass source/artifacts and native browser checks.", - "workspaceState": "Task03 ownership checkpoint: 22 candidate lifecycle regressions fail on frozen source and pass on source/main/legacy; two further explicit-URL short-circuit and duration-snapshot cases preserve historical successes. Final candidate runs have 24 passing cases each; 27 native lifecycle cases verify destroy/restart/completion and URL/decoder cleanup. New index/options/session/extraction modules fix serial encoding, stale callbacks, failure cleanup and selected reentry paths. Pixel/frame correctness, resource budgets and additional boundaries remain for task03; old/final core acceptance remains task05. Risk stays open rather than claiming complete extraction repair." + "workspaceState": "Task03 ownership checkpoint: 22 candidate lifecycle regressions fail on frozen source and pass on source/main/legacy; two further explicit-URL short-circuit and duration-snapshot cases preserve historical successes. Final candidate runs have 24 passing cases each; 27 native lifecycle cases verify destroy/restart/completion and URL/decoder cleanup. New index/options/session/extraction modules fix serial encoding, stale callbacks, failure cleanup and selected reentry paths. Pixel/frame correctness, resource budgets and additional boundaries remain for task03; old/final core acceptance remains task05. Risk stays open rather than claiming complete extraction repair. Task11 fixes throwing event-property cleanup skipping deadline/frame cancellation: eight controlled regressions fail on pre-change main and pass source/main/legacy; each actual artifact also passes21 native lifecycle checks. This does not prove spontaneous native setter failure or close remaining resource/pixel/combination gates." }, { "id": "AUTO-THUMB-ENCODE-01", diff --git a/refactor/tasks.json b/refactor/tasks.json index 62e012383..94e481c3c 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1820,7 +1820,8 @@ "CORE-07", "PKG-AUTO-THUMB-08", "PKG-AUTO-THUMB-09", - "PKG-AUTO-THUMB-10" + "PKG-AUTO-THUMB-10", + "PKG-AUTO-THUMB-11" ], "status": "todo", "risk": "H", @@ -5222,6 +5223,27 @@ "changes/2026-09-14-PKG-MULTI-SUB-11-ass-conversion.md", "baselines/multiple-subtitles-ass-validation.json" ] + }, + { + "id": "PKG-AUTO-THUMB-11", + "phase": "5 包迁移:artplayer-plugin-auto-thumbnail", + "title": "保证抽帧事件属性清理异常不截断其他资源释放", + "scope": [ + "artplayer-plugin-auto-thumbnail" + ], + "dependsOn": [ + "PKG-AUTO-THUMB-02", + "PKG-AUTO-THUMB-07", + "PKG-AUTO-THUMB-10" + ], + "status": "done", + "risk": "M", + "deliverable": "独立清除事件属性、抽帧时限和原生帧回调;首个异常保留,过期回调失效,替换任务可继续", + "acceptance": "八个故障注入用例旧红新绿,实际main/legacy抽帧与三浏览器生命周期回归通过;文档和构建同步,首帧及整体资源验收不冒充完成", + "evidence": [ + "changes/2026-09-14-PKG-AUTO-THUMB-11-handler-cleanup.md", + "baselines/auto-thumbnail-handler-validation.json" + ] } ] } diff --git a/test/auto-thumbnail-frames.test.js b/test/auto-thumbnail-frames.test.js index c9a74e9b9..4cabd59e4 100644 --- a/test/auto-thumbnail-frames.test.js +++ b/test/auto-thumbnail-frames.test.js @@ -41,6 +41,60 @@ function closed(env) { assert.equal(env.video.src, undefined) } +for (const property of ['onloadeddata', 'onseeked']) { + for (const scenario of ['destroy', 'restart', 'loadeddata-wait', 'finish']) { + test(`Auto-thumbnail frames release independent resources when ${property} cleanup throws during ${scenario}`, async () => { + const env = await setup({ readyState: scenario === 'loadeddata-wait' ? 1 : 4 }) + env.metadata() + const handler = env.video[property] + const lateSeek = env.video.onseeked + const lateLoaded = env.video.onloadeddata + const lateFrame = env.frames.get(0) + const lateTimer = [...env.timers.values()][0].callback + const failure = new Error(`${property} cleanup failed`) + Object.defineProperty(env.video, property, { + configurable: true, + get: () => handler, + set(value) { + assert.equal(value, null) + throw failure + }, + }) + if (scenario === 'finish') { + env.present() + env.seeked() + } + else { + env.art.emit(scenario === 'restart' ? 'restart' : 'destroy') + } + assert.equal(env.frames.size, 0, 'Native frame handles are canceled despite a handler setter failure') + assert.equal(env.timers.size, 0, 'The sample deadline is canceled despite a handler setter failure') + assert.equal(env.attached.size, 0) + assert.equal(env.video.src, undefined) + assert.deepEqual(env.canvases.map(canvas => [canvas.width, canvas.height]), [[0, 0]]) + assert.equal(env.video[property === 'onloadeddata' ? 'onseeked' : 'onloadeddata'], null) + assert.ok(env.warnings.some(warning => warning[1] === failure)) + lateLoaded?.() + lateSeek?.() + lateFrame?.() + lateTimer() + assert.equal(env.blobs.length, 0) + assert.equal(env.updates.length, 0) + assert.equal(env.timers.size, 0) + if (scenario === 'restart') { + env.art.emit('video:loadedmetadata') + env.metadata(env.videos[1]) + env.seeked(env.videos[1]) + env.finish() + assert.equal(env.updates.length, 1, 'A replacement job still publishes after predecessor cleanup fails') + } + env.art.emit('destroy') + assert.equal(env.urls.size, 0) + assert.equal(env.attached.size, 0) + }) + } +} + test('Auto-thumbnail frames wait for loadeddata before starting presentation-aware seeking', async () => { const env = await setup({ readyState: 1 }) env.metadata()