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() + }) +})