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