fix(ambilight): [PKG-AMBILIGHT-PROXY-01] sample canvas output pixels after resizing

This commit is contained in:
Harvey Zhao committed 2026-09-12 12:04:41 +08:00
1 parent 959cccf515
commit 20f062bc90
19 files changed
+508 -20

No files matched your search

+1 -1
View File
@@ -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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){try{n.parentNode?.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){try{n.parentNode?.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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})});
@@ -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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){var t;try{null==(t=n.parentNode)||t.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){var t;try{null==(t=n.parentNode)||t.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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})});
+6 -2
View File
@@ -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;
+2 -2
View File
@@ -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",
@@ -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.
@@ -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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){try{n.parentNode?.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){try{n.parentNode?.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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})});
@@ -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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){var t;try{null==(t=n.parentNode)||t.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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("<div></div>").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<t.length;r++){if(!n())return;const i=e[r],o=t[r];i&&void 0!==o&&(i.style.backgroundColor=o)}},destroy:function(){var t;try{null==(t=n.parentNode)||t.removeChild(n)}finally{e.length=0}}}}(h),i||o.mount(c,{blur:y,opacity:p,duration:m},()=>!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})});
@@ -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;
@@ -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
+22
View File
@@ -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;下一步先核对代理发布契约。
@@ -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."
]
}
@@ -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
修正仅将对象字段换行,未修改已经通过的浏览器测试含义。
+8 -5
View File
@@ -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<br>建立特有行为与错误测试 | PKG-AMBILIGHT-01, ENG-03, ENG-05 | 跨域 canvas 失败、零尺寸、暂停、重复 start/stop 和 destroy | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | done |
| PKG-AMBILIGHT-03 | artplayer-plugin-ambilight<br>整理内部职责与资源 | PKG-AMBILIGHT-02, CORE-03, CORE-12 | 取色计算与绘帧分离,统一 RAF 和 DOM 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | done |
| PKG-AMBILIGHT-04 | artplayer-plugin-ambilight<br>迁移自有源码和公开类型 | PKG-AMBILIGHT-03, ENG-04, ENG-06, CORE-07 | canvas 上下文、参数及 start/stop 的明确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | done |
| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight<br>验证新旧核心和组合 | PKG-AMBILIGHT-04, CORE-22 | 原生 video、canvas proxy 的能力边界及销毁无帧循环 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo |
| PKG-AMBILIGHT-PROXY-01 | artplayer-plugin-ambilight, artplayer-proxy-canvas<br>修复Canvas代理输出尺寸与取色区域 | PKG-AMBILIGHT-04, CORE-22 | 按Canvas实际输出buffer取色,原生video保留intrinsic尺寸,明确5.1.7没有proxy配置 | 旧错误复现、三引擎实际代理九色区域对照及原生视频回归通过;不把本子项当作最终代理/设备验收 | M | done |
| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight<br>验证新旧核心和组合 | PKG-AMBILIGHT-04, CORE-22, PKG-AMBILIGHT-PROXY-01, PKG-CANVAS-04 | 原生 video、canvas proxy 的能力边界及销毁无帧循环 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo |
| PKG-AMBILIGHT-06 | artplayer-plugin-ambilight<br>验证分发并同步文档 | 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)
+11
View File
@@ -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自别名
+1
View File
@@ -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 |
+20
View File
@@ -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."
}
]
}
+29 -2
View File
@@ -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",
+53
View File
@@ -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)
})
+78
View File
@@ -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()
})
})