diff --git a/docs/compiled/artplayer-plugin-ambilight.js b/docs/compiled/artplayer-plugin-ambilight.js
index 3e58151f2..e7396874a 100644
--- a/docs/compiled/artplayer-plugin-ambilight.js
+++ b/docs/compiled/artplayer-plugin-ambilight.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||u?.start()},stop(){i||u?.stop()}};if(i)return r=null,s;function f(){const t=[u?.destroy,l?.destroy,o?.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{r?.()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{t?.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),r?.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("
").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u=n.videoWidth,c=n.videoHeight;if(!Number.isFinite(u)||!Number.isFinite(c)||u<=0||c<=0)return null;const s=u/3,f=c/3,a=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*s,t*f,s,f,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;a.push(`rgb(${o}, ${u}, ${c})`)}return a}catch{return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(r?.playing),t=>{const n=l?.read(t);n&&t()&&o?.render(n,t)}),d("ready",s.start),i&&f()}catch(h){try{a()}catch{}try{f()}catch{}throw h}return s}}return Object.assign(n,{default:n})});
+!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||u?.start()},stop(){i||u?.stop()}};if(i)return r=null,s;function f(){const t=[u?.destroy,l?.destroy,o?.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{r?.()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{t?.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),r?.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u="CANVAS"===n.nodeName,c=u?n.width:n.videoWidth,s=u?n.height:n.videoHeight;if(!Number.isFinite(c)||!Number.isFinite(s)||c<=0||s<=0)return null;const f=c/3,a=s/3,d=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*f,t*a,f,a,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;d.push(`rgb(${o}, ${u}, ${c})`)}return d}catch{return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(r?.playing),t=>{const n=l?.read(t);n&&t()&&o?.render(n,t)}),d("ready",s.start),i&&f()}catch(h){try{a()}catch{}try{f()}catch{}throw h}return s}}return Object.assign(n,{default:n})});
diff --git a/docs/compiled/artplayer-plugin-ambilight.legacy.js b/docs/compiled/artplayer-plugin-ambilight.legacy.js
index b37c2611b..6c52b4c6f 100644
--- a/docs/compiled/artplayer-plugin-ambilight.legacy.js
+++ b/docs/compiled/artplayer-plugin-ambilight.legacy.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||null==u||u.start()},stop(){i||null==u||u.stop()}};if(i)return r=null,s;function f(){const t=[null==u?void 0:u.destroy,null==l?void 0:l.destroy,null==o?void 0:o.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{null==r||r()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{null==t||t.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),null==r||r.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u=n.videoWidth,c=n.videoHeight;if(!Number.isFinite(u)||!Number.isFinite(c)||u<=0||c<=0)return null;const s=u/3,f=c/3,a=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*s,t*f,s,f,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;a.push(`rgb(${o}, ${u}, ${c})`)}return a}catch(d){return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(null==r?void 0:r.playing),t=>{const n=null==l?void 0:l.read(t);n&&t()&&(null==o||o.render(n,t))}),d("ready",s.start),i&&f()}catch(h){try{a()}catch(y){}try{f()}catch(y){}throw h}return s}}return Object.assign(n,{default:n})});
+!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||null==u||u.start()},stop(){i||null==u||u.stop()}};if(i)return r=null,s;function f(){const t=[null==u?void 0:u.destroy,null==l?void 0:l.destroy,null==o?void 0:o.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{null==r||r()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{null==t||t.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),null==r||r.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u="CANVAS"===n.nodeName,c=u?n.width:n.videoWidth,s=u?n.height:n.videoHeight;if(!Number.isFinite(c)||!Number.isFinite(s)||c<=0||s<=0)return null;const f=c/3,a=s/3,d=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*f,t*a,f,a,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;d.push(`rgb(${o}, ${u}, ${c})`)}return d}catch(h){return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(null==r?void 0:r.playing),t=>{const n=null==l?void 0:l.read(t);n&&t()&&(null==o||o.render(n,t))}),d("ready",s.start),i&&f()}catch(h){try{a()}catch(y){}try{f()}catch(y){}throw h}return s}}return Object.assign(n,{default:n})});
diff --git a/docs/compiled/artplayer-plugin-ambilight.mjs b/docs/compiled/artplayer-plugin-ambilight.mjs
index be39b2134..b6f4eadca 100644
--- a/docs/compiled/artplayer-plugin-ambilight.mjs
+++ b/docs/compiled/artplayer-plugin-ambilight.mjs
@@ -4,6 +4,9 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
+function isCanvas(source) {
+ return source.nodeName === "CANVAS";
+}
function createColorSampler(canvas, video, createCanvas) {
let context;
try {
@@ -20,8 +23,9 @@ function createColorSampler(canvas, video, createCanvas) {
function read(active) {
if (destroyed || !active())
return null;
- const width = video.videoWidth;
- const height = video.videoHeight;
+ const canvasOutput = isCanvas(video);
+ const width = canvasOutput ? video.width : video.videoWidth;
+ const height = canvasOutput ? video.height : video.videoHeight;
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
return null;
const w = width / 3;
diff --git a/package.json b/package.json
index fdebb9791..148dac69c 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",
"typecheck": "node scripts/typecheck.mjs",
- "test:unit": "node --test test/ambilight.test.js test/ambilight-lifecycle.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
+ "test:unit": "node --test test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
@@ -70,7 +70,7 @@
"check:impact": "node refactor/scripts/impact.mjs",
"check:contracts": "node refactor/scripts/contracts.mjs",
"test:contracts": "node refactor/scripts/run-contracts.mjs",
- "test:ambilight": "node --test test/ambilight.test.js test/ambilight-lifecycle.test.js",
+ "test:ambilight": "node --test test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js",
"test:ambilight-types-package": "node refactor/scripts/ambilight-package-types.mjs"
},
"browserslist": "last 1 Chrome version",
diff --git a/packages/artplayer-plugin-ambilight/ARCHITECTURE.md b/packages/artplayer-plugin-ambilight/ARCHITECTURE.md
index 4eb99a30b..d0e73d586 100644
--- a/packages/artplayer-plugin-ambilight/ARCHITECTURE.md
+++ b/packages/artplayer-plugin-ambilight/ARCHITECTURE.md
@@ -32,6 +32,13 @@ canvas could read the recovered same-origin video. Retries remain frequency-gate
with a permanently cross-origin source this allocates at most one replacement per
eligible sample. No bypass of canvas origin restrictions is attempted.
+Sampling uses the actual drawable pixels: native video's videoWidth/videoHeight,
+or a canvas proxy's width/height buffer. A proxy can forward the underlying media's
+dimensions while rendering a differently sized canvas, so never use its forwarded
+videoWidth/videoHeight to crop the output. The internal sampling source union and
+nodeName guard also avoid relying on same-window instanceof checks. Zero output
+dimensions skip sampling until the canvas has a valid buffer again.
+
Compatibility constraints: keep numeric style arguments, nine row-major samples,
blur/opacity/frequency/duration defaults, synchronous start/stop returns, option
reading at registration, and the old frequency comparison (including NaN and
@@ -62,3 +69,11 @@ candidate modes with interop disabled, and ten negative uses. Published 1.1.0 fa
NodeNext ESM with its old declarations; candidate must not inherit that failure.
Published 1.0.0 required all option fields; 1.1.0 already made them optional. Current
inference follows 1.1.0 while retaining valid calls from both generations.
+
+`test/ambilight-proxy.test.js` and `test/browser/ambilight-proxy.spec.js` protect
+resized output geometry. Browser tests play real video with the actual workspace
+proxy and paint a uniform nine-color palette through its public post-processing
+callback; getImageData is native. Actual core 5.1.7 has no proxy option and uses
+VIDEO, so its case checks that capability boundary and native playback. Only 5.4.0
+and candidate cases validate the canvas proxy combination. These tests do not
+establish that the unrefactored proxy's own async draw loop is terminally safe.
diff --git a/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.js b/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.js
index 3e58151f2..e7396874a 100644
--- a/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.js
+++ b/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||u?.start()},stop(){i||u?.stop()}};if(i)return r=null,s;function f(){const t=[u?.destroy,l?.destroy,o?.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{r?.()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{t?.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),r?.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u=n.videoWidth,c=n.videoHeight;if(!Number.isFinite(u)||!Number.isFinite(c)||u<=0||c<=0)return null;const s=u/3,f=c/3,a=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*s,t*f,s,f,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;a.push(`rgb(${o}, ${u}, ${c})`)}return a}catch{return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(r?.playing),t=>{const n=l?.read(t);n&&t()&&o?.render(n,t)}),d("ready",s.start),i&&f()}catch(h){try{a()}catch{}try{f()}catch{}throw h}return s}}return Object.assign(n,{default:n})});
+!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||u?.start()},stop(){i||u?.stop()}};if(i)return r=null,s;function f(){const t=[u?.destroy,l?.destroy,o?.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{r?.()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{t?.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),r?.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u="CANVAS"===n.nodeName,c=u?n.width:n.videoWidth,s=u?n.height:n.videoHeight;if(!Number.isFinite(c)||!Number.isFinite(s)||c<=0||s<=0)return null;const f=c/3,a=s/3,d=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*f,t*a,f,a,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;d.push(`rgb(${o}, ${u}, ${c})`)}return d}catch{return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(r?.playing),t=>{const n=l?.read(t);n&&t()&&o?.render(n,t)}),d("ready",s.start),i&&f()}catch(h){try{a()}catch{}try{f()}catch{}throw h}return s}}return Object.assign(n,{default:n})});
diff --git a/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.legacy.js b/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.legacy.js
index b37c2611b..6c52b4c6f 100644
--- a/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.legacy.js
+++ b/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.legacy.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||null==u||u.start()},stop(){i||null==u||u.stop()}};if(i)return r=null,s;function f(){const t=[null==u?void 0:u.destroy,null==l?void 0:l.destroy,null==o?void 0:o.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{null==r||r()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{null==t||t.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),null==r||r.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u=n.videoWidth,c=n.videoHeight;if(!Number.isFinite(u)||!Number.isFinite(c)||u<=0||c<=0)return null;const s=u/3,f=c/3,a=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*s,t*f,s,f,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;a.push(`rgb(${o}, ${u}, ${c})`)}return a}catch(d){return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(null==r?void 0:r.playing),t=>{const n=null==l?void 0:l.read(t);n&&t()&&(null==o||o.render(n,t))}),d("ready",s.start),i&&f()}catch(h){try{a()}catch(y){}try{f()}catch(y){}throw h}return s}}return Object.assign(n,{default:n})});
+!function(t,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(t.artplayerPluginAmbilight=n(),define(function(){return t.artplayerPluginAmbilight})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAmbilight=n()}(this,function(){"use strict";function t(t,n,e){let r="stopped",i=null,o=0,l=!1,u=0;function c(){const s=o,f=()=>"running"===r&&s===o;l=!0;try{const r=performance.now();if(r-u<1e3/t||!n()||!f())return;u=r,e(f)}finally{l=!1,"running"===r&&null===i&&function(){const t=o;try{i=requestAnimationFrame(()=>{"running"===r&&t===o&&(i=null,c())})}catch(n){throw r="stopped",o++,n}}()}}return{start:function(){"stopped"===r&&(r="running",o++,l||c())},stop:function(){if("destroyed"===r)return;r="stopped",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)},destroy:function(){if("destroyed"===r)return;r="destroyed",o++;const t=i;i=null,null!==t&&cancelAnimationFrame(t)}}}function n(n={}){return e=>{let r=e,i=e.isDestroy,o=null,l=null,u=null;const c=[],s={name:"artplayerPluginAmbilight",start(){i||null==u||u.start()},stop(){i||null==u||u.stop()}};if(i)return r=null,s;function f(){const t=[null==u?void 0:u.destroy,null==l?void 0:l.destroy,null==o?void 0:o.destroy];u=null,l=null,o=null;const n=[];for(const r of t)try{null==r||r()}catch(e){n.push(e)}if(n.length)throw n[0]}function a(){if(i)return;i=!0;const t=r;r=null;const n=[];for(const[r,i]of c.splice(0))try{null==t||t.off(r,i)}catch(e){n.push(e)}try{f()}catch(e){n.push(e)}if(n.length)throw n[0]}function d(t,n){c.push([t,n]),null==r||r.on(t,n)}try{if(d("destroy",a),i)return s;const{$video:c}=e.template,h=e.constructor.utils,{blur:y="50px",opacity:p=.5,frequency:g=10,duration:m=.3}=n;if(i)return s;if(o=function(t){const n=t.createElement("div");n.innerHTML=Array.from({length:9}).fill("").join("");const e=Array.from(n.children);return{mount:function(r,i,o){if(o()&&(t.addClass(n,"artplayer-plugin-ambilight"),o()&&(r.parentNode.insertBefore(n,r),o()))){t.setStyles(n,{position:"absolute",top:0,left:0,zIndex:9,inset:0,display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gridTemplateRows:"1fr 1fr 1fr"});for(const n of e){if(!o())return;t.setStyles(n,{opacity:i.opacity,filter:`blur(${i.blur})`,transition:`background-color ${i.duration}s ease`})}}},render:function(t,n){for(let r=0;r!i),i)return f(),s;const b=h.createElement("canvas");if(i)return b.width=0,b.height=0,s;if(l=function(t,n,e){let r;try{r=t.getContext("2d"),t.width=3,t.height=3}catch(l){throw t.width=0,t.height=0,l}let i=!1,o=!1;return{read:function(l){if(i||!l())return null;const u="CANVAS"===n.nodeName,c=u?n.width:n.videoWidth,s=u?n.height:n.videoHeight;if(!Number.isFinite(c)||!Number.isFinite(s)||c<=0||s<=0)return null;const f=c/3,a=s/3,d=[];try{if(o){const n=e();if(i||!l())return n.width=0,n.height=0,null;if(r=null,r=(t=n).getContext("2d"),i||!l())return t.width=0,t.height=0,null;t.width=3,t.height=3,o=!1}if(!r)return null;for(let t=0;t<3;t++)for(let e=0;e<3;e++){if(i||!l())return null;if(r.drawImage(n,e*f,t*a,f,a,0,0,1,1),i||!l())return null;const[o,u,c]=r.getImageData(0,0,1,1).data;d.push(`rgb(${o}, ${u}, ${c})`)}return d}catch(h){return i||(o=!0,t.width=0,t.height=0),null}},destroy:function(){i||(i=!0,t.width=0,t.height=0)}}}(b,c,()=>h.createElement("canvas")),i)return f(),s;u=t(g,()=>Boolean(null==r?void 0:r.playing),t=>{const n=null==l?void 0:l.read(t);n&&t()&&(null==o||o.render(n,t))}),d("ready",s.start),i&&f()}catch(h){try{a()}catch(y){}try{f()}catch(y){}throw h}return s}}return Object.assign(n,{default:n})});
diff --git a/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.mjs b/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.mjs
index be39b2134..b6f4eadca 100644
--- a/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.mjs
+++ b/packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.mjs
@@ -4,6 +4,9 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
+function isCanvas(source) {
+ return source.nodeName === "CANVAS";
+}
function createColorSampler(canvas, video, createCanvas) {
let context;
try {
@@ -20,8 +23,9 @@ function createColorSampler(canvas, video, createCanvas) {
function read(active) {
if (destroyed || !active())
return null;
- const width = video.videoWidth;
- const height = video.videoHeight;
+ const canvasOutput = isCanvas(video);
+ const width = canvasOutput ? video.width : video.videoWidth;
+ const height = canvasOutput ? video.height : video.videoHeight;
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
return null;
const w = width / 3;
diff --git a/packages/artplayer-plugin-ambilight/src/sampler.ts b/packages/artplayer-plugin-ambilight/src/sampler.ts
index 84cd0159f..07c857425 100644
--- a/packages/artplayer-plugin-ambilight/src/sampler.ts
+++ b/packages/artplayer-plugin-ambilight/src/sampler.ts
@@ -1,4 +1,10 @@
-export function createColorSampler(canvas: HTMLCanvasElement, video: HTMLVideoElement, createCanvas: () => HTMLCanvasElement) {
+type SamplingSource = HTMLVideoElement | HTMLCanvasElement
+
+function isCanvas(source: SamplingSource): source is HTMLCanvasElement {
+ return source.nodeName === 'CANVAS'
+}
+
+export function createColorSampler(canvas: HTMLCanvasElement, video: SamplingSource, createCanvas: () => HTMLCanvasElement) {
let context: CanvasRenderingContext2D | null
try {
context = canvas.getContext('2d')
@@ -16,8 +22,10 @@ export function createColorSampler(canvas: HTMLCanvasElement, video: HTMLVideoEl
function read(active: () => boolean): string[] | null {
if (destroyed || !active())
return null
- const width = video.videoWidth
- const height = video.videoHeight
+ // A canvas proxy's output buffer can differ from its forwarded video dimensions.
+ const canvasOutput = isCanvas(video)
+ const width = canvasOutput ? video.width : video.videoWidth
+ const height = canvasOutput ? video.height : video.videoHeight
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
return null
const w = width / 3
diff --git a/refactor/ambilight-validation.md b/refactor/ambilight-validation.md
index f53d2fa87..d8893949c 100644
--- a/refactor/ambilight-validation.md
+++ b/refactor/ambilight-validation.md
@@ -70,3 +70,25 @@ chapter,专项安装命令的存在不代表全部包已加入该门槛。
初始失败及明确限制见 [类型验证证据](baselines/ambilight-types-validation.json) 和
[实施记录](changes/2026-09-12-PKG-AMBILIGHT-04-types.md)。类型与两代导出具体风险已
关闭;当前待做范围为05/06,旧阶段记录保留其当时状态。
+
+## 05拆出:Canvas输出尺寸
+
+PKG-AMBILIGHT-PROXY-01从组合检查中独立修复采样边界:Canvas用实际width/height,
+原生video保留videoWidth/videoHeight。`yarn test:ambilight`加入3项输出resize/零尺寸
+与正常视频测试;修改前2失败/1通过,候选加历史/契约专项56项通过。
+
+`yarn test:browser test/browser/ambilight-proxy.spec.js test/browser/ambilight-lifecycle.spec.js`
+合计27项:6项真正Canvas组合(实际npm5.4.0/候选×三引擎)、3项实际npm5.1.7不支持
+proxy配置的原生播放能力对照、18项原生生命周期和跨域恢复。Canvas组合使用实际工作区
+代理源码和真实视频解码,通过公开callback绘制九色均匀区域,用原生getImageData
+检查九格颜色。不是下载媒体或模拟canvas返回值;这不是实际npm代理归档兼容证明。
+
+首次把5.1.7也当作代理是错误假设,诊断显示VIDEO节点、代理drawCount=0;旧option
+声明和真实Artplayer.option都不存在proxy,最终断言明确此能力边界。初始失败及中间
+诊断保留。本子项不关闭05最终设备/代理生命周期要求;代理自身仍需PKG-CANVAS系列
+完成后回归,不能从Ambilight视图清理推导代理已无残余绘帧。见
+[实施记录](changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md)。
+
+子项最终56项Node专项、27项浏览器及978项CI通过,无skip;证据见
+[代理采样验证](baselines/ambilight-proxy-validation.json)。05新增PKG-CANVAS-04依赖,
+等待最终代理实现后再执行整体组合验收,当前保留todo;下一步先核对代理发布契约。
diff --git a/refactor/baselines/ambilight-proxy-validation.json b/refactor/baselines/ambilight-proxy-validation.json
new file mode 100644
index 000000000..d4b0a2d2c
--- /dev/null
+++ b/refactor/baselines/ambilight-proxy-validation.json
@@ -0,0 +1,198 @@
+{
+ "schemaVersion": 1,
+ "task": "PKG-AMBILIGHT-PROXY-01",
+ "status": "done",
+ "parentCommit": "959cccf5151edafef16310b3ede38c3cd1614e50",
+ "change": "Internal sampling source accepts native video or canvas; canvas buffer width/height replaces forwarded video dimensions for canvas only.",
+ "checks": [
+ {
+ "command": "node --test test/ambilight-proxy.test.js (before source fix)",
+ "result": "expected-negative-control",
+ "tests": 3,
+ "failed": 2,
+ "passedNormalVideo": 1
+ },
+ {
+ "command": "node --test test/ambilight-proxy.test.js test/ambilight-lifecycle.test.js test/ambilight.test.js refactor/scripts/ambilight-contract.test.mjs",
+ "result": "pass",
+ "tests": 56
+ },
+ {
+ "command": "playwright test test/browser/ambilight-proxy.spec.js test/browser/ambilight-lifecycle.spec.js",
+ "result": "pass",
+ "tests": 27,
+ "actualCanvasCombinations": 6,
+ "oldCoreLacksProxyCapabilityObservations": 3,
+ "nativeLifecycleCases": 18,
+ "skipped": 0,
+ "stats": {
+ "startTime": "2026-09-12T03:57:46.543Z",
+ "duration": 33749.014,
+ "expected": 27,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "report": "refactor/.cache/ambilight-proxy01-final-results/report.json",
+ "archivedResults": "refactor/.cache/ambilight-proxy01-final-results/results",
+ "originalResultsPrefix": "refactor/.cache/browser/results",
+ "scope": "actual workspace canvas proxy with native video decode and native Canvas nine-color post-processing callback; 5.4.0/candidate proxy, 5.1.7 native capability boundary, three engines"
+ },
+ {
+ "command": "tsc -p packages/artplayer-plugin-ambilight/tsconfig.json and focused eslint --no-fix",
+ "result": "pass"
+ },
+ {
+ "command": "yarn build artplayer-plugin-ambilight",
+ "result": "pass",
+ "formats": [
+ "main",
+ "legacy",
+ "esm",
+ "docs/compiled copies"
+ ]
+ },
+ {
+ "command": "yarn ci:check",
+ "result": "pass",
+ "tests": {
+ "unit": 881,
+ "engineering": 14,
+ "baseline": 83,
+ "mainTotal": 978,
+ "repeatedContractExecutions": 44
+ },
+ "strictProductionTypeScriptFiles": 278
+ }
+ ],
+ "failedIterations": [
+ {
+ "report": "refactor/.cache/ambilight-proxy01-before-results/report.json",
+ "scope": "Initial Chromium: two actual proxy color failures, one unsupported 5.1.7 proxy assumption"
+ },
+ {
+ "report": "refactor/.cache/ambilight-proxy01-first-fixed-results/report.json",
+ "scope": "Six actual proxy combinations pass, three unsupported 5.1.7 cases fail before draw"
+ },
+ {
+ "report": "refactor/.cache/ambilight-proxy01-capability-probe-results/report.json",
+ "scope": "Ready wait does not change old core VIDEO host; 24 pass, 3 unsupported-proxy failures; state attachments identify native host"
+ },
+ {
+ "log": "refactor/.cache/ambilight-proxy01-ci.log",
+ "scope": "New prerequisite left parent incorrectly doing; plan guard rejected unfinished dependency. Parent now todo until Canvas migration and this child are complete."
+ }
+ ],
+ "inputs": [
+ {
+ "file": "packages/artplayer-plugin-ambilight/src/sampler.ts",
+ "sha256LF": "e6f4ea2795b500aa2ac8ffe4b65da4269e8b3457a9912808537bdacfbe9f5328"
+ },
+ {
+ "file": "packages/artplayer-plugin-ambilight/src/index.ts",
+ "sha256LF": "9db25144798ee667bc420b3e1f0b00ef6855185d26cefca45e05271354bf8591"
+ },
+ {
+ "file": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.js",
+ "sha256LF": "21ee5227f26cfff6e82ee6932f6eafc9741fa3be0ec2f5252a47b53ceea14599"
+ },
+ {
+ "file": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.legacy.js",
+ "sha256LF": "c3ed1a54e44a0202811d70378cb19edf87b1ebd010d05fe96ebe64d0b4301d5d"
+ },
+ {
+ "file": "packages/artplayer-plugin-ambilight/dist/artplayer-plugin-ambilight.mjs",
+ "sha256LF": "ef126959819c4973da27dbb1e60ccccc54386c23f8c7750ce1ba4d38f306aaaa"
+ },
+ {
+ "file": "packages/artplayer-proxy-canvas/src/index.js",
+ "sha256LF": "4b5922b05b46adf0d54ae92025b5c70456a5b8c87819c797ef91eb50e0c8f4d4"
+ },
+ {
+ "file": "packages/artplayer-proxy-canvas/package.json",
+ "sha256LF": "4df3a0098730cfc12232fc57066fd1f7159bbd41dce87904fa51f39b82252eed"
+ },
+ {
+ "file": "test/ambilight-proxy.test.js",
+ "sha256LF": "f284ea3069d66c36b62756cef468701c6fbc91cd17343843715d59b03779b928"
+ },
+ {
+ "file": "test/browser/ambilight-proxy.spec.js",
+ "sha256LF": "048d6b12dd26d174e978fcc68abed47df17ff232cb351583e4785ebbae6e5122"
+ },
+ {
+ "file": "test/browser/ambilight-lifecycle.spec.js",
+ "sha256LF": "321693a24d33f0c7aee02fd4137b55175386562a645ba138e15c3a51d6348d2c"
+ },
+ {
+ "file": "test/browser/server.mjs",
+ "sha256LF": "af513e1e64f875668b6a7cbd060276d85d930e0cc1715577e97907cc26c3b58d"
+ },
+ {
+ "file": "test/helpers/ambilight.js",
+ "sha256LF": "8cb80dba5270860afc121e3a02336a6cdb20b0a6a41e058cde11efe9174fba4f"
+ },
+ {
+ "file": "refactor/baselines/vast-core.json",
+ "sha256LF": "bb703cf37591211f78719665aed0e0cb2524ec5618dad48e6b8e2dbfef132b8e"
+ },
+ {
+ "file": "refactor/baselines/releases.json",
+ "sha256LF": "56054ecd0b861c91a14bf83fcb4243ab6f5ddb27004ad8eb98901c1f85f9b426"
+ },
+ {
+ "file": "package.json",
+ "sha256LF": "a94c6ded6fecef73e84932eb1f7c31de42e1ca02814fcd69c807f50c52fc8924"
+ }
+ ],
+ "logs": [
+ {
+ "file": "refactor/.cache/ambilight05-proxy-before.log",
+ "sha256": "02856164dc6566f05aa2336159f06352130356a86b046774acef09c8a4017090"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-node-before.log",
+ "sha256": "c43e7fd44d1227028443339300fe940c7d2e483aa6bd6740f4f084b40da9ca73"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-unit.log",
+ "sha256": "dbed1c54e564b2cdbfd587eb46961bc3288eef4c249943c94e5196f9d678d496"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-browser.log",
+ "sha256": "68ec5effee410003f046d78bd9fb22858a5fd999f4328c0bd26e92206dec312a"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-browser-final.log",
+ "sha256": "f5ef93ef517b1f1d66152699db77734d318d4a970d5d8f8259004c494e414bc0"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-browser-corrected.log",
+ "sha256": "b3702456e39ed6e8be3504a123065d4004b8e9f17ceaf6bbf53c124dd42e98da"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-build.log",
+ "sha256": "74a43735bbd36337dc218a3990b9f3571bdb8cf9b1115782ff325a0b4a6530e4"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-lint-final.log",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-ci.log",
+ "sha256": "312049cd0a59b220249a033eef5589d22d9755b9c06b6988e89d4828742eb49f"
+ },
+ {
+ "file": "refactor/.cache/ambilight-proxy01-ci-final.log",
+ "sha256": "306ccefd21e8d8d860e9b576f8b6aa00cb057dde86c21a0b583e417584aa2b16"
+ }
+ ],
+ "limitations": [
+ "The actual npm core 5.1.7 option declaration and runtime option object lack proxy. It is not counted as a canvas combination.",
+ "Proxy source is current workspace JS, not an immutable npm proxy release. Freeze proxy history in PKG-CANVAS-01.",
+ "Proxy-owned async bitmap/RAF/terminal semantics remain unrefactored and are not validated by Ambilight grid removal.",
+ "PKG-AMBILIGHT-05 depends on PKG-CANVAS-04 and remains todo for final integration/device acceptance; task 06 remains todo.",
+ "Only formatting line breaks changed in the browser test after its final successful run; focused lint passed afterward. Candidate and proxy executed bundle bytes are recorded in report attachments.",
+ "No public type or export changes, no dependency installation, push, version bump or publication."
+ ]
+}
diff --git a/refactor/changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md b/refactor/changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md
new file mode 100644
index 000000000..a04d79775
--- /dev/null
+++ b/refactor/changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md
@@ -0,0 +1,44 @@
+# PKG-AMBILIGHT-PROXY-01 Canvas输出取色边界
+
+从05组合检查中拆出可独立验证的源码修复;05仍需最终代理迁移后的组合和设备证据。
+当前实际proxy-canvas源码由本步输入哈希固定,未把工作区代理版本当作npm历史包。
+
+## 复现与修改
+
+原生canvas proxy在输出buffer改变尺寸后仍转发底层videoWidth/videoHeight。Ambilight
+原实现按转发媒体尺寸截取canvas,导致九宫格采样偏向左上角;浏览器真实视频解码后
+通过代理公开callback绘制九色native Canvas图案,以均匀颜色直接验证各区域取色,
+不mock像素读取、代理工厂或媒体播放。初始Chromium中5.4.0/候选核心均取错颜色。
+
+采样模块内部来源类型明确为HTMLVideoElement或HTMLCanvasElement;根据nodeName
+区分Canvas输出,使用其width/height。原生video仍使用videoWidth/videoHeight,显示
+宽高不影响取色。保留频率/生命周期/公共Option/Result及所有导出;Canvas零输出
+尺寸不采样,恢复尺寸后可继续。不使用跨window容易失效的instanceof判断。
+
+3项Node用例在修改前2失败/1正常视频通过,修改后与既有回归共56项通过。覆盖输出
+二次resize、原生视频显示尺寸与零Canvas输出恢复。完整三格式构建、专项lint/严格
+TS以及完整CI978项通过(881单元+14工程+83基线),另44项重复契约观察。
+
+## 纠正历史能力假设
+
+最初把5.1.7也当作canvas组合,读取不到代理draw回调;增加ready等待仍失败。
+诊断附件明确实际节点为VIDEO,而不是CANVAS。核对实际npm5.1.7的option声明和
+浏览器Artplayer.option均没有proxy配置,证明旧核心直接忽略该选项。这不是Canvas
+取色修复失败,也不能声称对该核心验证了代理。最终用单独能力断言记录该历史限制,
+同时确认原生Ambilight仍可播放取色;不会向旧核心注入虚构代理支持。
+
+最终浏览器矩阵包含6项真正canvas组合(5.4.0/候选×三引擎)、3项5.1.7无proxy配置
+但原生播放的能力对照、18项既有原生视频生命周期/跨域恢复。失败与通过报告分开归档。
+
+Canvas代理本身仍是未迁移代码,其异步绘帧/终止/事件归属继续由PKG-CANVAS-01至06
+核验;本步不能据“移除了Ambilight DOM”推断代理已无残余RAF。05增加PKG-CANVAS-04
+前置依赖,在依赖满足前保留todo并记录预检证据;06完整分发/demo也未完成。本子任务
+完成后独立本地commit,无推送/发布。下一步推进PKG-CANVAS-01发布契约,随后迁移代理。
+
+## 验证收尾
+
+最终27项浏览器全部通过,无skip;56项Node专项通过,278个生产TS文件严格检查。
+报告、输入哈希、初始颜色错误/中间能力诊断和完整CI日志见
+[验证证据](../baselines/ambilight-proxy-validation.json)。期间计划检查正确拒绝了新依赖
+未完成却将父任务标doing的状态;已改为依赖满足前todo,再完成本子任务。格式lint
+修正仅将对象字段换行,未修改已经通过的浏览器测试含义。
diff --git a/refactor/plan.md b/refactor/plan.md
index 376ddfa5f..c3c8423a1 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
-基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 220 项,范围 22 个包及工作区/示例。
+基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 221 项,范围 22 个包及工作区/示例。
-状态:todo 126 / doing 6 / blocked 0 / done 88 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
+状态:todo 126 / doing 6 / blocked 0 / done 89 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -16,7 +16,7 @@
| --- | --- | --- |
| artplayer | 5.4.1 | CORE-01, CORE-02, CORE-03, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, CORE-11, CORE-12, CORE-13, CORE-14, CORE-15, CORE-16, CORE-17, CORE-18, CORE-19, CORE-20, CORE-21, CORE-23, CORE-22, CORE-24 |
| artplayer-plugin-ads | 2.1.0 | PKG-ADS-01, PKG-ADS-02, PKG-ADS-03, PKG-ADS-04, PKG-ADS-05, PKG-ADS-06 |
-| artplayer-plugin-ambilight | 1.1.0 | PKG-AMBILIGHT-01, PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-04, PKG-AMBILIGHT-05, PKG-AMBILIGHT-06 |
+| 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 |
| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06 |
| 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 |
@@ -31,7 +31,7 @@
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 |
| artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 |
| artplayer-plugin-vtt-thumbnail | 1.1.0 | PKG-VTT-THUMB-01, PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
-| artplayer-proxy-canvas | 1.1.0 | PKG-CANVAS-01, PKG-CANVAS-02, PKG-CANVAS-03, PKG-CANVAS-04, PKG-CANVAS-05, PKG-CANVAS-06 |
+| artplayer-proxy-canvas | 1.1.0 | PKG-AMBILIGHT-PROXY-01, PKG-CANVAS-01, PKG-CANVAS-02, PKG-CANVAS-03, PKG-CANVAS-04, PKG-CANVAS-05, PKG-CANVAS-06 |
| artplayer-proxy-mediabunny | 1.2.0 | PKG-MB-01, PKG-MB-02, PKG-MB-03, PKG-MB-04, PKG-MB-05, PKG-MB-06, PKG-MB-07, PKG-MB-08, PKG-MB-09, PKG-MB-10 |
| artplayer-tool-iframe | 1.1.0 | PKG-IFRAME-01, PKG-IFRAME-02, PKG-IFRAME-03, PKG-IFRAME-04, PKG-IFRAME-05, PKG-IFRAME-06 |
| artplayer-tool-thumbnail | 4.4.0 | PKG-TOOL-THUMB-01, PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 |
@@ -152,7 +152,8 @@
| PKG-AMBILIGHT-02 | artplayer-plugin-ambilight
建立特有行为与错误测试 | PKG-AMBILIGHT-01, ENG-03, ENG-05 | 跨域 canvas 失败、零尺寸、暂停、重复 start/stop 和 destroy | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | done |
| PKG-AMBILIGHT-03 | artplayer-plugin-ambilight
整理内部职责与资源 | PKG-AMBILIGHT-02, CORE-03, CORE-12 | 取色计算与绘帧分离,统一 RAF 和 DOM 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | done |
| PKG-AMBILIGHT-04 | artplayer-plugin-ambilight
迁移自有源码和公开类型 | PKG-AMBILIGHT-03, ENG-04, ENG-06, CORE-07 | canvas 上下文、参数及 start/stop 的明确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | done |
-| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight
验证新旧核心和组合 | PKG-AMBILIGHT-04, CORE-22 | 原生 video、canvas proxy 的能力边界及销毁无帧循环 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo |
+| PKG-AMBILIGHT-PROXY-01 | artplayer-plugin-ambilight, artplayer-proxy-canvas
修复Canvas代理输出尺寸与取色区域 | PKG-AMBILIGHT-04, CORE-22 | 按Canvas实际输出buffer取色,原生video保留intrinsic尺寸,明确5.1.7没有proxy配置 | 旧错误复现、三引擎实际代理九色区域对照及原生视频回归通过;不把本子项当作最终代理/设备验收 | M | done |
+| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight
验证新旧核心和组合 | PKG-AMBILIGHT-04, CORE-22, PKG-AMBILIGHT-PROXY-01, PKG-CANVAS-04 | 原生 video、canvas proxy 的能力边界及销毁无帧循环 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo |
| PKG-AMBILIGHT-06 | artplayer-plugin-ambilight
验证分发并同步文档 | PKG-AMBILIGHT-05, ENG-07 | ambilight.js 示例及样式/分发验证 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | M | todo |
## 5 包迁移:artplayer-plugin-audio-track
@@ -480,6 +481,8 @@
- PKG-AMBILIGHT-02: [记录](changes/2026-09-12-PKG-AMBILIGHT-02-tests.md) [记录](baselines/ambilight-behavior-validation.json) [记录](ambilight-validation.md)
- PKG-AMBILIGHT-03: [记录](changes/2026-09-12-PKG-AMBILIGHT-03-lifecycle.md) [记录](ambilight-validation.md) [记录](baselines/ambilight-lifecycle-validation.json)
- PKG-AMBILIGHT-04: [记录](changes/2026-09-12-PKG-AMBILIGHT-04-types.md) [记录](ambilight-validation.md) [记录](baselines/ambilight-types-validation.json)
+- PKG-AMBILIGHT-PROXY-01: [记录](changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md) [记录](ambilight-validation.md) [记录](baselines/ambilight-proxy-validation.json)
+- PKG-AMBILIGHT-05: [记录](changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md) [记录](ambilight-validation.md)
- PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json)
- PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json)
- PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json)
diff --git a/refactor/progress.md b/refactor/progress.md
index 83264cce5..cc6b29f10 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -1,5 +1,16 @@
# 进度与证据
+## 最新完成:Ambilight-PROXY-01 Canvas输出区域修复
+
+组合测试复现Canvas缩放后仍按底层视频尺寸截取而取错九格颜色;内部明确video/canvas
+来源类型,Canvas采用输出buffer尺寸,原生video保留intrinsic尺寸。3项新Node测试
+修改前2失败/1正常通过,专项56项、最终27项浏览器及978项完整CI通过。浏览器实际
+覆盖6项Canvas组合、3项5.1.7没有proxy配置的原生能力对照、18项既有生命周期回归,
+没有把未启用代理算作组合通过。新增独立子任务,当前221项:89 done、6 doing、
+126 todo。05增加PKG-CANVAS-04依赖,代理自身仍未迁移;下一步PKG-CANVAS-01核对
+真实发布和历史契约,再推进TS迁移。见 [实施记录](changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md)
+和 [验证证据](baselines/ambilight-proxy-validation.json)。独立本地提交,无推送/发布。
+
## 最新完成:Ambilight-04 公开类型与两代导出
公共声明补齐可选调用,保留1.1.0的Parameters推导;CJS直接调用与历史.default自别名
diff --git a/refactor/risk-table.md b/refactor/risk-table.md
index adb53f81f..a55c5064d 100644
--- a/refactor/risk-table.md
+++ b/refactor/risk-table.md
@@ -154,3 +154,4 @@
| AMBILIGHT-LIFE-01 | open / 已复现 | Ambilight sampling and RAF/DOM ownership lack terminal/error handling | PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-05 |
| AMBILIGHT-TYPE-01 | resolved / 已复现 | Ambilight declarations disagree with optional factory argument and differ across published generations | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 |
| AMBILIGHT-DIST-01 | resolved / 已复现 | Ambilight 1.0.0 CommonJS default namespace differs from callable 1.1.0 | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 |
+| AMBILIGHT-PROXY-01 | resolved / 已复现 | Canvas proxy output pixels differ from forwarded video dimensions | PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05 |
diff --git a/refactor/risks.json b/refactor/risks.json
index 94dc0494d..a16f95ca3 100644
--- a/refactor/risks.json
+++ b/refactor/risks.json
@@ -3319,6 +3319,26 @@
"refactor/baselines/ambilight-types-validation.json"
],
"resolutionRationale": "Actual production and installed root/legacy callable factories have self default identity, native ESM default is callable and script globals match. Public default/import= consumers pass. Broader deep-import/demo/package-browser review remains task 06."
+ },
+ {
+ "id": "AMBILIGHT-PROXY-01",
+ "title": "Canvas proxy output pixels differ from forwarded video dimensions",
+ "confirmation": "reproduced",
+ "status": "resolved",
+ "owners": [
+ "PKG-AMBILIGHT-PROXY-01",
+ "PKG-AMBILIGHT-05"
+ ],
+ "compatibleResolution": "Sample Canvas width/height while native video retains intrinsic videoWidth/videoHeight; preserve public calls and lifecycle.",
+ "closureCriteria": "Node resize/zero-size/normal-video and actual supported-core canvas palette comparisons pass across three engines.",
+ "evidence": [
+ "refactor/changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md"
+ ],
+ "resolutionEvidence": [
+ "refactor/changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md",
+ "refactor/baselines/ambilight-proxy-validation.json"
+ ],
+ "resolutionRationale": "Node output resize/zero-size and normal video tests pass; actual 5.4.0/candidate canvas proxy nine-color regions pass across three engines. Actual 5.1.7 lacks proxy option and is recorded as a native capability observation, not a canvas pass. Proxy-owned lifecycle and final device acceptance remain tasks CANVAS and AMBILIGHT-05."
}
]
}
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 1202806a1..52fc203e0 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -1536,6 +1536,28 @@
"baselines/ambilight-types-validation.json"
]
},
+ {
+ "id": "PKG-AMBILIGHT-PROXY-01",
+ "phase": "5 包迁移:artplayer-plugin-ambilight",
+ "title": "修复Canvas代理输出尺寸与取色区域",
+ "scope": [
+ "artplayer-plugin-ambilight",
+ "artplayer-proxy-canvas"
+ ],
+ "dependsOn": [
+ "PKG-AMBILIGHT-04",
+ "CORE-22"
+ ],
+ "status": "done",
+ "risk": "M",
+ "deliverable": "按Canvas实际输出buffer取色,原生video保留intrinsic尺寸,明确5.1.7没有proxy配置",
+ "acceptance": "旧错误复现、三引擎实际代理九色区域对照及原生视频回归通过;不把本子项当作最终代理/设备验收",
+ "evidence": [
+ "changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md",
+ "ambilight-validation.md",
+ "baselines/ambilight-proxy-validation.json"
+ ]
+ },
{
"id": "PKG-AMBILIGHT-05",
"phase": "5 包迁移:artplayer-plugin-ambilight",
@@ -1545,13 +1567,18 @@
],
"dependsOn": [
"PKG-AMBILIGHT-04",
- "CORE-22"
+ "CORE-22",
+ "PKG-AMBILIGHT-PROXY-01",
+ "PKG-CANVAS-04"
],
"status": "todo",
"risk": "M",
"deliverable": "原生 video、canvas proxy 的能力边界及销毁无帧循环",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
- "evidence": []
+ "evidence": [
+ "changes/2026-09-12-PKG-AMBILIGHT-PROXY-01-sampling.md",
+ "ambilight-validation.md"
+ ]
},
{
"id": "PKG-AMBILIGHT-06",
diff --git a/test/ambilight-proxy.test.js b/test/ambilight-proxy.test.js
new file mode 100644
index 000000000..f9b5f7f09
--- /dev/null
+++ b/test/ambilight-proxy.test.js
@@ -0,0 +1,53 @@
+import assert from 'node:assert/strict'
+// eslint-disable-next-line test/no-import-node-test -- Node runtime contract runner.
+import test from 'node:test'
+import { ambilightCandidate, ambilightEnvironment } from './helpers/ambilight.js'
+
+const implementation = await ambilightCandidate()
+
+test('Ambilight canvas proxy sampling follows output pixels and subsequent output resize', async () => {
+ const env = await ambilightEnvironment(implementation)
+ env.video.nodeName = 'CANVAS'
+ env.video.width = 600
+ env.video.height = 300
+ env.art.playing = true
+ env.time(100)
+ const result = env.factory()(env.art)
+ result.start()
+ assert.equal(env.draws.length, 9)
+ assert.deepEqual(env.draws[0].slice(1), [0, 0, 200, 100, 0, 0, 1, 1])
+ assert.deepEqual(env.draws[8].slice(1), [400, 200, 200, 100, 0, 0, 1, 1])
+ env.video.width = 300
+ env.video.height = 600
+ env.frame(200)
+ assert.deepEqual(env.draws[17].slice(1), [200, 400, 100, 200, 0, 0, 1, 1])
+ env.art.destroy()
+})
+
+test('Ambilight native video uses intrinsic pixels regardless of its display width and height', async () => {
+ const env = await ambilightEnvironment(implementation)
+ env.video.nodeName = 'VIDEO'
+ env.video.width = 600
+ env.video.height = 300
+ env.art.playing = true
+ env.time(100)
+ env.factory()(env.art).start()
+ assert.deepEqual(env.draws[8].slice(1), [200, 100, 100, 50, 0, 0, 1, 1])
+ env.art.destroy()
+})
+
+test('Ambilight zero-sized canvas output skips sampling despite nonzero proxy media dimensions and recovers on resize', async () => {
+ const env = await ambilightEnvironment(implementation)
+ env.video.nodeName = 'CANVAS'
+ env.video.width = 0
+ env.video.height = 300
+ env.art.playing = true
+ env.time(100)
+ env.factory()(env.art).start()
+ assert.equal(env.draws.length, 0)
+ env.video.width = 600
+ env.frame(200)
+ assert.equal(env.draws.length, 9)
+ env.art.destroy()
+ assert.equal(env.frames.size, 0)
+})
diff --git a/test/browser/ambilight-proxy.spec.js b/test/browser/ambilight-proxy.spec.js
new file mode 100644
index 000000000..c1d0d57c5
--- /dev/null
+++ b/test/browser/ambilight-proxy.spec.js
@@ -0,0 +1,78 @@
+import fs from 'node:fs'
+import { transform } from 'esbuild'
+import { hash } from '../../refactor/scripts/releases.mjs'
+import { ambilightCandidate } from '../helpers/ambilight.js'
+import { expect, test } from './fixtures.js'
+
+const implementation = await ambilightCandidate()
+const proxySource = fs.readFileSync('packages/artplayer-proxy-canvas/src/index.js', 'utf8')
+const proxy = (await transform(proxySource, { format: 'cjs', target: 'es2020' })).code
+
+for (const core of ['published-5.1.7', 'published', 'candidate']) {
+ const scenario = core === 'published-5.1.7' ? 'historical core has no proxy option and retains native Ambilight playback' : 'Ambilight samples all nine canvas proxy regions after output resizing'
+ test(`${core}: ${scenario}`, async ({ page }, testInfo) => {
+ await page.goto(`/test/player.html?core=${core}`)
+ for (const [name, source] of [['ambilightFactory', implementation.source], ['canvasProxy', proxy]])
+ await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${source}; window.${name} = module.exports.default || module.exports; })();` })
+ await page.evaluate(() => {
+ window.palette = ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)', 'rgb(255, 255, 0)', 'rgb(0, 255, 255)', 'rgb(255, 0, 255)', 'rgb(120, 0, 0)', 'rgb(0, 120, 0)', 'rgb(0, 0, 120)']
+ window.drawCount = 0
+ window.proxyErrors = []
+ window.art = new window.Artplayer({
+ container: '.player',
+ url: '/test/pattern.mp4',
+ muted: true,
+ proxy: window.canvasProxy((context, video) => {
+ window.proxyVideo = video
+ // Exercise the proxy's public post-processing callback with native canvas.
+ const canvas = context.canvas
+ for (let index = 0; index < 9; index++) {
+ context.fillStyle = window.palette[index]
+ context.fillRect(index % 3 * canvas.width / 3, Math.floor(index / 3) * canvas.height / 3, canvas.width / 3, canvas.height / 3)
+ }
+ window.drawCount++
+ }),
+ plugins: [window.ambilightFactory({ frequency: Infinity })],
+ })
+ window.art.on('artplayerProxyCanvas:error', error => window.proxyErrors.push({ name: error.name, message: error.message }))
+ document.querySelector('#play').onclick = () => window.art.play()
+ })
+ await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
+ await page.click('#play')
+ if (core === 'published-5.1.7') {
+ expect(await page.evaluate(() => 'proxy' in window.Artplayer.option)).toBe(false)
+ expect(await page.evaluate(() => window.art.template.$video.nodeName)).toBe('VIDEO')
+ await expect.poll(() => page.evaluate(() => [...document.querySelectorAll('.artplayer-plugin-ambilight > div')].filter(node => /^rgb\(/.test(node.style.backgroundColor)).length)).toBe(9)
+ expect(await page.evaluate(() => window.drawCount)).toBe(0)
+ await testInfo.attach('ambilight-proxy-capability', { contentType: 'application/json', body: JSON.stringify({ core, proxyOption: false, actualHost: 'VIDEO', plugin: hash(implementation.source), proxy: hash(proxySource), scope: 'unsupported proxy option is ignored by this actual historical core; native plugin playback only' }) })
+ await page.evaluate(() => window.art.destroy())
+ return
+ }
+ expect(await page.evaluate(() => 'proxy' in window.Artplayer.option)).toBe(true)
+ await expect.poll(() => page.evaluate(() => window.drawCount)).toBeGreaterThan(0)
+ await expect.poll(() => page.evaluate(() => window.proxyVideo.currentTime)).toBeGreaterThan(0)
+ await page.evaluate(() => {
+ window.art.template.$video.width = 600
+ window.art.template.$video.height = 300
+ window.drawCount = 0
+ })
+ await expect.poll(() => page.evaluate(() => window.drawCount)).toBeGreaterThan(0)
+ const dimensions = await page.evaluate(() => ({ nodeName: window.art.template.$video.nodeName, canvas: [window.art.template.$video.width, window.art.template.$video.height], video: [window.proxyVideo.videoWidth, window.proxyVideo.videoHeight] }))
+ expect(dimensions.nodeName).toBe('CANVAS')
+ expect(dimensions.canvas).not.toEqual(dimensions.video)
+ await testInfo.attach('ambilight-proxy-inputs', { contentType: 'application/json', body: JSON.stringify({ core, dimensions, plugin: hash(implementation.source), proxy: hash(proxySource), media: '/test/pattern.mp4', scope: 'real video decode and proxy post-processing callback paints a nine-cell palette using native Canvas' }) })
+ await expect.poll(() => page.evaluate(() => [...document.querySelectorAll('.artplayer-plugin-ambilight > div')].map(node => node.style.backgroundColor))).toEqual(await page.evaluate(() => window.palette))
+ await page.evaluate(() => window.art.destroy())
+ expect(await page.locator('.artplayer-plugin-ambilight').count()).toBe(0)
+ })
+}
+
+test.afterEach(async ({ page }, testInfo) => {
+ await testInfo.attach('ambilight-proxy-state', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ ready: window.art?.isReady, nodeName: window.art?.template.$video.nodeName, readyState: window.art?.template.$video.readyState, paused: window.art?.template.$video.paused, currentTime: window.art?.template.$video.currentTime, drawCount: window.drawCount, errors: window.proxyErrors }))) })
+ await page.evaluate(() => {
+ window.art?.plugins.artplayerPluginAmbilight?.stop()
+ window.proxyVideo?.pause()
+ if (window.art && !window.art.isDestroy)
+ window.art.destroy()
+ })
+})