From c3967bcbf41a6b812add887d9eb82f9ad281eff4 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Mon, 14 Sep 2026 18:51:57 +0800 Subject: [PATCH] fix(canvas): [PKG-CANVAS-SUBTITLE-01] attach native subtitle tracks --- docs/assets/example/canvas.js | 3 + docs/compiled/artplayer-proxy-canvas.js | 2 +- .../compiled/artplayer-proxy-canvas.legacy.js | 2 +- docs/compiled/artplayer-proxy-canvas.mjs | 16 + .../artplayer-proxy-canvas/ARCHITECTURE.md | 18 +- packages/artplayer-proxy-canvas/README.md | 6 + .../dist/artplayer-proxy-canvas.js | 2 +- .../dist/artplayer-proxy-canvas.legacy.js | 2 +- .../dist/artplayer-proxy-canvas.mjs | 16 + packages/artplayer-proxy-canvas/src/index.ts | 2 + .../artplayer-proxy-canvas/src/subtitles.ts | 15 + .../canvas-subtitles-validation.json | 1140 +++++++++++++++++ ...2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md | 60 + refactor/plan.md | 10 +- refactor/progress.md | 15 + refactor/risk-table.md | 1 + refactor/risks.json | 23 + refactor/tasks.json | 22 +- scripts/browser-validation/scope.ts | 1 + test/browser/README.md | 6 + test/browser/canvas-subtitles.spec.js | 68 + test/canvas-lifecycle.test.js | 16 + test/helpers/canvas.js | 24 +- 23 files changed, 1459 insertions(+), 11 deletions(-) create mode 100644 packages/artplayer-proxy-canvas/src/subtitles.ts create mode 100644 refactor/baselines/canvas-subtitles-validation.json create mode 100644 refactor/changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md create mode 100644 test/browser/canvas-subtitles.spec.js diff --git a/docs/assets/example/canvas.js b/docs/assets/example/canvas.js index dd32f6cf7..7bef93728 100644 --- a/docs/assets/example/canvas.js +++ b/docs/assets/example/canvas.js @@ -20,6 +20,9 @@ const art = new Artplayer({ miniProgressBar: true, autoPlayback: true, autoOrientation: true, + subtitle: { + url: '/assets/sample/subtitle.srt', + }, thumbnails: { url: '/assets/sample/thumbnails.png', number: 60, diff --git a/docs/compiled/artplayer-proxy-canvas.js b/docs/compiled/artplayer-proxy-canvas.js index 78314121d..87e8419c7 100644 --- a/docs/compiled/artplayer-proxy-canvas.js +++ b/docs/compiled/artplayer-proxy-canvas.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(e,t){let n=!1,i=!1,o=!1,r=!1,a=0,s=null;function l(){null!==s&&cancelAnimationFrame(s),s=null}function c(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=a,s=()=>{o=!1,c()};e(()=>!i&&t()&&a===n).then(s,s)}else n&&null===s&&(s=requestAnimationFrame(()=>{s=null,r=!0,c()}))}function u(){!i&&t()&&(a++,r=!0,l(),c())}function f(){n=!1,r=!1,a++,l()}return{request:u,start(){n||i||!t()||(n=!0,u())},stop:f,destroy(){i=!0,f()}}}function n(n){return i=>{const o=i.constructor,r=o.utils.createElement("canvas"),a=o.utils.createElement("video");let s=i,l=i.isDestroy,c=null;const u=[],f=()=>!l&&!s?.isDestroy,d=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(a,()=>s?.template?.$player,f),p=function(t,n,i,o,r){const a=t.getContext("2d");let s=!1;return{available:()=>!!a||(s||(s=!0,r(new Error("Canvas 2D context is unavailable"))),!1),async draw(s){const l=()=>s()&&n.readyState>=2&&!n.seeking;if(!a||!l()||!e(n)||!t.width||!t.height)return;let c=!1;try{if("undefined"!==typeof createImageBitmap){c=!0;const e=await createImageBitmap(n);c=!1;try{l()&&a.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else a.drawImage(n,0,0,t.width,t.height);if(!l())return;i&&i(a,n),l()&&o(a,n)}catch(u){const e=c&&"object"===typeof u&&null!==u&&"name"in u&&"InvalidStateError"===u.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;l()&&!e&&r(u)}}}}(r,a,n,(e,t)=>s?.emit("artplayerProxyCanvas:draw",e,t),e=>s?.emit("artplayerProxyCanvas:error",e)),h=t(p.draw,f);if(function(e,t,n,i,o){const r=new Map;for(const a in e){const t=Reflect.get(e,a);"function"===typeof t&&r.set(a,(...n)=>Reflect.apply(t,e,n))}for(const a in t)a in e||Object.defineProperty(e,a,{get(){const e=Reflect.get(t,a);return"function"!==typeof e?e:(...r)=>n()?("load"===a&&i(),"play"===a&&o(),Reflect.apply(e,t,r)):"play"===a?Promise.resolve():void 0},set(e){n()&&("src"!==a&&"srcObject"!==a||i(),Reflect.set(t,a,e))},configurable:!0,enumerable:!0});for(const[a,s]of r)Reflect.set(e,a,s)}(r,a,f,h.stop,d.mount),l)return s=null,r;function y(){if(l)return;l=!0,h.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...u.splice(0).reverse(),d.destroy])try{n()}catch(t){e.push(t)}if(s=null,r.width=0,r.height=0,e.length)throw e[0]}function m(e,t){if(!f())return;const n=()=>{("destroy"===e||f())&&t()},o=()=>i.off(e,n);u.push(o),i.on(e,n),l&&o()}function v(){p.available()&&h.request()}try{m("destroy",y),m("video:loadedmetadata",()=>{e(a)&&(r.width=a.videoWidth,r.height=a.videoHeight)}),m("video:play",()=>{d.mount(),p.available()&&h.start()}),m("video:pause",h.stop),m("video:emptied",h.stop),m("video:seeked",v),m("resize",()=>{!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,a=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(a)||r<=0||a<=0)return;const s=n.videoWidth/n.videoHeight,l=r/a>s?a*s:r,c=r/a>s?a:r/s;t.width=l,t.height=c,t.style.padding=`${(a-c)/2}px ${(r-l)/2}px`}(r,a,s?.template?.$player,s?.option.autoSize),v()}),f()&&(c=setTimeout(()=>{if(c=null,f())try{d.mount();for(const e of o.config.events){if(!f())break;const t=e=>{f()&&s?.emit(`video:${e.type}`,e)},n=()=>a.removeEventListener(e,t);u.push(n);const o=i.proxy(a,e,t);f()?u.push(o):o()}}catch(e){try{y()}catch{}i.emit("artplayerProxyCanvas:error",e)}}))}catch(g){try{y()}catch{}throw g}return r}}return Object.assign(n,{default:n})}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(e,t){let n=!1,i=!1,o=!1,r=!1,a=0,l=null;function s(){null!==l&&cancelAnimationFrame(l),l=null}function c(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=a,l=()=>{o=!1,c()};e(()=>!i&&t()&&a===n).then(l,l)}else n&&null===l&&(l=requestAnimationFrame(()=>{l=null,r=!0,c()}))}function u(){!i&&t()&&(a++,r=!0,s(),c())}function d(){n=!1,r=!1,a++,s()}return{request:u,start(){n||i||!t()||(n=!0,u())},stop:d,destroy(){i=!0,d()}}}function n(n){return i=>{const o=i.constructor,r=o.utils.createElement("canvas"),a=o.utils.createElement("video");let l=i,s=i.isDestroy,c=null;const u=[],d=()=>!s&&!l?.isDestroy,f=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(a,()=>l?.template?.$player,d),p=function(t,n,i,o,r){const a=t.getContext("2d");let l=!1;return{available:()=>!!a||(l||(l=!0,r(new Error("Canvas 2D context is unavailable"))),!1),async draw(l){const s=()=>l()&&n.readyState>=2&&!n.seeking;if(!a||!s()||!e(n)||!t.width||!t.height)return;let c=!1;try{if("undefined"!==typeof createImageBitmap){c=!0;const e=await createImageBitmap(n);c=!1;try{s()&&a.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else a.drawImage(n,0,0,t.width,t.height);if(!s())return;i&&i(a,n),s()&&o(a,n)}catch(u){const e=c&&"object"===typeof u&&null!==u&&"name"in u&&"InvalidStateError"===u.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;s()&&!e&&r(u)}}}}(r,a,n,(e,t)=>l?.emit("artplayerProxyCanvas:draw",e,t),e=>l?.emit("artplayerProxyCanvas:error",e)),h=t(p.draw,d);if(function(e,t,n,i,o){const r=new Map;for(const a in e){const t=Reflect.get(e,a);"function"===typeof t&&r.set(a,(...n)=>Reflect.apply(t,e,n))}for(const a in t)a in e||Object.defineProperty(e,a,{get(){const e=Reflect.get(t,a);return"function"!==typeof e?e:(...r)=>n()?("load"===a&&i(),"play"===a&&o(),Reflect.apply(e,t,r)):"play"===a?Promise.resolve():void 0},set(e){n()&&("src"!==a&&"srcObject"!==a||i(),Reflect.set(t,a,e))},configurable:!0,enumerable:!0});for(const[a,l]of r)Reflect.set(e,a,l)}(r,a,d,h.stop,f.mount),s)return l=null,r;function y(){if(s)return;s=!0,h.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...u.splice(0).reverse(),f.destroy])try{n()}catch(t){e.push(t)}if(l=null,r.width=0,r.height=0,e.length)throw e[0]}function m(e,t){if(!d())return;const n=()=>{("destroy"===e||d())&&t()},o=()=>i.off(e,n);u.push(o),i.on(e,n),s&&o()}function v(){p.available()&&h.request()}try{!function(e,t,n,i){if(!i())return;const o=e.appendChild;n.kind="metadata",n.default=!0,t.appendChild(n),e.appendChild=r=>i()&&1===r.nodeType&&"TRACK"===r.nodeName&&"namespaceURI"in r&&"http://www.w3.org/1999/xhtml"===r.namespaceURI?(n.remove(),t.appendChild(r)):o.call(e,r)}(r,a,o.utils.createElement("track"),d),m("destroy",y),m("video:loadedmetadata",()=>{e(a)&&(r.width=a.videoWidth,r.height=a.videoHeight)}),m("video:play",()=>{f.mount(),p.available()&&h.start()}),m("video:pause",h.stop),m("video:emptied",h.stop),m("video:seeked",v),m("resize",()=>{!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,a=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(a)||r<=0||a<=0)return;const l=n.videoWidth/n.videoHeight,s=r/a>l?a*l:r,c=r/a>l?a:r/l;t.width=s,t.height=c,t.style.padding=`${(a-c)/2}px ${(r-s)/2}px`}(r,a,l?.template?.$player,l?.option.autoSize),v()}),d()&&(c=setTimeout(()=>{if(c=null,d())try{f.mount();for(const e of o.config.events){if(!d())break;const t=e=>{d()&&l?.emit(`video:${e.type}`,e)},n=()=>a.removeEventListener(e,t);u.push(n);const o=i.proxy(a,e,t);d()?u.push(o):o()}}catch(e){try{y()}catch{}i.emit("artplayerProxyCanvas:error",e)}}))}catch(g){try{y()}catch{}throw g}return r}}return Object.assign(n,{default:n})}); diff --git a/docs/compiled/artplayer-proxy-canvas.legacy.js b/docs/compiled/artplayer-proxy-canvas.legacy.js index 39b4ddd53..e4374cd63 100644 --- a/docs/compiled/artplayer-proxy-canvas.legacy.js +++ b/docs/compiled/artplayer-proxy-canvas.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(t,n,i,o,r){const l=t.getContext("2d");let a=!1;return{available:()=>!!l||(a||(a=!0,r(new Error("Canvas 2D context is unavailable"))),!1),draw(a){return s=this,u=null,c=function*(){const s=()=>a()&&n.readyState>=2&&!n.seeking;if(!l||!s()||!e(n)||!t.width||!t.height)return;let u=!1;try{if("undefined"!==typeof createImageBitmap){u=!0;const e=yield createImageBitmap(n);u=!1;try{s()&&l.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else l.drawImage(n,0,0,t.width,t.height);if(!s())return;i&&i(l,n),s()&&o(l,n)}catch(c){const e=u&&"object"===typeof c&&null!==c&&"name"in c&&"InvalidStateError"===c.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;s()&&!e&&r(c)}},new Promise((e,t)=>{var n=e=>{try{o(c.next(e))}catch(n){t(n)}},i=e=>{try{o(c.throw(e))}catch(n){t(n)}},o=t=>t.done?e(t.value):Promise.resolve(t.value).then(n,i);o((c=c.apply(s,u)).next())});var s,u,c}}}function n(e,t){let n=!1,i=!1,o=!1,r=!1,l=0,a=null;function s(){null!==a&&cancelAnimationFrame(a),a=null}function u(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=l,a=()=>{o=!1,u()};e(()=>!i&&t()&&l===n).then(a,a)}else n&&null===a&&(a=requestAnimationFrame(()=>{a=null,r=!0,u()}))}function c(){!i&&t()&&(l++,r=!0,s(),u())}function d(){n=!1,r=!1,l++,s()}return{request:c,start(){n||i||!t()||(n=!0,c())},stop:d,destroy(){i=!0,d()}}}function i(i){return o=>{const r=o.constructor,l=r.utils.createElement("canvas"),a=r.utils.createElement("video");let s=o,u=o.isDestroy,c=null;const d=[],f=()=>!u&&!(null==s?void 0:s.isDestroy),h=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(a,()=>{var e;return null==(e=null==s?void 0:s.template)?void 0:e.$player},f),p=t(l,a,i,(e,t)=>null==s?void 0:s.emit("artplayerProxyCanvas:draw",e,t),e=>null==s?void 0:s.emit("artplayerProxyCanvas:error",e)),y=n(p.draw,f);if(function(e,t,n,i,o){const r=new Map;for(const l in e){const t=Reflect.get(e,l);"function"===typeof t&&r.set(l,(...n)=>Reflect.apply(t,e,n))}for(const l in t)l in e||Object.defineProperty(e,l,{get(){const e=Reflect.get(t,l);return"function"!==typeof e?e:(...r)=>n()?("load"===l&&i(),"play"===l&&o(),Reflect.apply(e,t,r)):"play"===l?Promise.resolve():void 0},set(e){n()&&("src"!==l&&"srcObject"!==l||i(),Reflect.set(t,l,e))},configurable:!0,enumerable:!0});for(const[l,a]of r)Reflect.set(e,l,a)}(l,a,f,y.stop,h.mount),u)return s=null,l;function v(){if(u)return;u=!0,y.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...d.splice(0).reverse(),h.destroy])try{n()}catch(t){e.push(t)}if(s=null,l.width=0,l.height=0,e.length)throw e[0]}function m(e,t){if(!f())return;const n=()=>{("destroy"===e||f())&&t()},i=()=>o.off(e,n);d.push(i),o.on(e,n),u&&i()}function g(){p.available()&&y.request()}try{m("destroy",v),m("video:loadedmetadata",()=>{e(a)&&(l.width=a.videoWidth,l.height=a.videoHeight)}),m("video:play",()=>{h.mount(),p.available()&&y.start()}),m("video:pause",y.stop),m("video:emptied",y.stop),m("video:seeked",g),m("resize",()=>{var t;!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,l=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(l)||r<=0||l<=0)return;const a=n.videoWidth/n.videoHeight,s=r/l>a?l*a:r,u=r/l>a?l:r/a;t.width=s,t.height=u,t.style.padding=`${(l-u)/2}px ${(r-s)/2}px`}(l,a,null==(t=null==s?void 0:s.template)?void 0:t.$player,null==s?void 0:s.option.autoSize),g()}),f()&&(c=setTimeout(()=>{if(c=null,f())try{h.mount();for(const e of r.config.events){if(!f())break;const t=e=>{f()&&(null==s||s.emit(`video:${e.type}`,e))},n=()=>a.removeEventListener(e,t);d.push(n);const i=o.proxy(a,e,t);f()?d.push(i):i()}}catch(e){try{v()}catch(t){}o.emit("artplayerProxyCanvas:error",e)}}))}catch(b){try{v()}catch(w){}throw b}return l}}return Object.assign(i,{default:i})}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(t,n,i,o,r){const a=t.getContext("2d");let l=!1;return{available:()=>!!a||(l||(l=!0,r(new Error("Canvas 2D context is unavailable"))),!1),draw(l){return s=this,u=null,c=function*(){const s=()=>l()&&n.readyState>=2&&!n.seeking;if(!a||!s()||!e(n)||!t.width||!t.height)return;let u=!1;try{if("undefined"!==typeof createImageBitmap){u=!0;const e=yield createImageBitmap(n);u=!1;try{s()&&a.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else a.drawImage(n,0,0,t.width,t.height);if(!s())return;i&&i(a,n),s()&&o(a,n)}catch(c){const e=u&&"object"===typeof c&&null!==c&&"name"in c&&"InvalidStateError"===c.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;s()&&!e&&r(c)}},new Promise((e,t)=>{var n=e=>{try{o(c.next(e))}catch(n){t(n)}},i=e=>{try{o(c.throw(e))}catch(n){t(n)}},o=t=>t.done?e(t.value):Promise.resolve(t.value).then(n,i);o((c=c.apply(s,u)).next())});var s,u,c}}}function n(e,t){let n=!1,i=!1,o=!1,r=!1,a=0,l=null;function s(){null!==l&&cancelAnimationFrame(l),l=null}function u(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=a,l=()=>{o=!1,u()};e(()=>!i&&t()&&a===n).then(l,l)}else n&&null===l&&(l=requestAnimationFrame(()=>{l=null,r=!0,u()}))}function c(){!i&&t()&&(a++,r=!0,s(),u())}function d(){n=!1,r=!1,a++,s()}return{request:c,start(){n||i||!t()||(n=!0,c())},stop:d,destroy(){i=!0,d()}}}function i(i){return o=>{const r=o.constructor,a=r.utils.createElement("canvas"),l=r.utils.createElement("video");let s=o,u=o.isDestroy,c=null;const d=[],f=()=>!u&&!(null==s?void 0:s.isDestroy),p=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(l,()=>{var e;return null==(e=null==s?void 0:s.template)?void 0:e.$player},f),h=t(a,l,i,(e,t)=>null==s?void 0:s.emit("artplayerProxyCanvas:draw",e,t),e=>null==s?void 0:s.emit("artplayerProxyCanvas:error",e)),y=n(h.draw,f);if(function(e,t,n,i,o){const r=new Map;for(const a in e){const t=Reflect.get(e,a);"function"===typeof t&&r.set(a,(...n)=>Reflect.apply(t,e,n))}for(const a in t)a in e||Object.defineProperty(e,a,{get(){const e=Reflect.get(t,a);return"function"!==typeof e?e:(...r)=>n()?("load"===a&&i(),"play"===a&&o(),Reflect.apply(e,t,r)):"play"===a?Promise.resolve():void 0},set(e){n()&&("src"!==a&&"srcObject"!==a||i(),Reflect.set(t,a,e))},configurable:!0,enumerable:!0});for(const[a,l]of r)Reflect.set(e,a,l)}(a,l,f,y.stop,p.mount),u)return s=null,a;function m(){if(u)return;u=!0,y.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...d.splice(0).reverse(),p.destroy])try{n()}catch(t){e.push(t)}if(s=null,a.width=0,a.height=0,e.length)throw e[0]}function v(e,t){if(!f())return;const n=()=>{("destroy"===e||f())&&t()},i=()=>o.off(e,n);d.push(i),o.on(e,n),u&&i()}function g(){h.available()&&y.request()}try{!function(e,t,n,i){if(!i())return;const o=e.appendChild;n.kind="metadata",n.default=!0,t.appendChild(n),e.appendChild=r=>i()&&1===r.nodeType&&"TRACK"===r.nodeName&&"namespaceURI"in r&&"http://www.w3.org/1999/xhtml"===r.namespaceURI?(n.remove(),t.appendChild(r)):o.call(e,r)}(a,l,r.utils.createElement("track"),f),v("destroy",m),v("video:loadedmetadata",()=>{e(l)&&(a.width=l.videoWidth,a.height=l.videoHeight)}),v("video:play",()=>{p.mount(),h.available()&&y.start()}),v("video:pause",y.stop),v("video:emptied",y.stop),v("video:seeked",g),v("resize",()=>{var t;!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,a=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(a)||r<=0||a<=0)return;const l=n.videoWidth/n.videoHeight,s=r/a>l?a*l:r,u=r/a>l?a:r/l;t.width=s,t.height=u,t.style.padding=`${(a-u)/2}px ${(r-s)/2}px`}(a,l,null==(t=null==s?void 0:s.template)?void 0:t.$player,null==s?void 0:s.option.autoSize),g()}),f()&&(c=setTimeout(()=>{if(c=null,f())try{p.mount();for(const e of r.config.events){if(!f())break;const t=e=>{f()&&(null==s||s.emit(`video:${e.type}`,e))},n=()=>l.removeEventListener(e,t);d.push(n);const i=o.proxy(l,e,t);f()?d.push(i):i()}}catch(e){try{m()}catch(t){}o.emit("artplayerProxyCanvas:error",e)}}))}catch(b){try{m()}catch(w){}throw b}return a}}return Object.assign(i,{default:i})}); diff --git a/docs/compiled/artplayer-proxy-canvas.mjs b/docs/compiled/artplayer-proxy-canvas.mjs index 2b6658e62..a87f39755 100644 --- a/docs/compiled/artplayer-proxy-canvas.mjs +++ b/docs/compiled/artplayer-proxy-canvas.mjs @@ -204,6 +204,21 @@ function createFrameScheduler(draw, active) { } }; } +function forwardSubtitleTrack(canvas, video, initialTrack, active) { + if (!active()) + return; + const append = canvas.appendChild; + initialTrack.kind = "metadata"; + initialTrack.default = true; + video.appendChild(initialTrack); + canvas.appendChild = (node) => { + if (active() && node.nodeType === 1 && node.nodeName === "TRACK" && "namespaceURI" in node && node.namespaceURI === "http://www.w3.org/1999/xhtml") { + initialTrack.remove(); + return video.appendChild(node); + } + return append.call(canvas, node); + }; +} function artplayerProxyCanvas(callback) { return (art) => { const constructor = art.constructor; @@ -262,6 +277,7 @@ function artplayerProxyCanvas(callback) { scheduler.request(); } try { + forwardSubtitleTrack(canvas, video, constructor.utils.createElement("track"), active); listen("destroy", destroy); listen("video:loadedmetadata", () => { if (hasDimensions(video)) { diff --git a/packages/artplayer-proxy-canvas/ARCHITECTURE.md b/packages/artplayer-proxy-canvas/ARCHITECTURE.md index 23a990415..4848d61a6 100644 --- a/packages/artplayer-proxy-canvas/ARCHITECTURE.md +++ b/packages/artplayer-proxy-canvas/ARCHITECTURE.md @@ -22,6 +22,7 @@ callback remains optional and function-valued. | --- | --- | | index.ts | Host event subscriptions, deferred media forwarding, initialization rollback and terminal disposal | | adapter.ts | Preserve native canvas methods; forward only video properties absent from canvas; guard escaped media actions after disposal | +| subtitles.ts | Supply the initial native metadata track and route the core's actual HTML track insertion into the backing video | | media.ts | Connect the backing video before playback and release its source, stream reference and DOM node | | geometry.ts | Finite intrinsic dimensions and aspect-fit canvas/padding calculations | | renderer.ts | Decoded-frame eligibility, bitmap acquisition/close, callback and draw/error notification order | @@ -33,7 +34,18 @@ private core implementation modules. ## State and event order -Canvas methods keep their receiver and win over same-named video methods. Media properties +Canvas methods keep their receiver and win over same-named video methods for +ordinary Canvas content. `appendChild` has one intentional +subtitle-specific adaptation: active HTML track elements attach to the backing video, +where native loading and cuechange work. Other nodes still attach to the Canvas. +The initial empty metadata track makes the old core's synchronous textTracks[0] +capability check succeed; it is removed before the first actual track is appended. +The core owns actual track replacement/removal and subtitle URLs. After destruction, +escaped appendChild cannot attach more tracks to the unloaded private video. +This fixes the formerly empty subtitle path; a working track's parent is now VIDEO. +The initialization is inside the same rollback boundary as host registration. + +Media properties remain live enumerable/configurable descriptors. Assigning src/srcObject or invoking load invalidates pending frames; the returned canvas and callback arguments keep their identities. The video is connected before play with absolute positioning, transparent opacity, no focus @@ -89,4 +101,8 @@ through the semantic generator; the editor root stays plain, with an explicit artplayerProxyCanvas.RuntimeFactory assertion for code needing the runtime self alias. Broader installed artifact contents/deep paths and 8082 demos remain 06. Real Safari/mobile/device scope remains 05; Windows Playwright WebKit is not a device claim. +`canvas-subtitles.spec.js` exercises native VTT load, paused seeking, replacement, +single-track ownership, ordinary Canvas child insertion/removal and escaped insertion +after destroy on old/new cores. It also belongs to the installed browser scope; +keep its bytes and native engine provenance with the lifecycle report. See `refactor/canvas-validation.md` and the task evidence for actual outcomes and limitations. diff --git a/packages/artplayer-proxy-canvas/README.md b/packages/artplayer-proxy-canvas/README.md index d264c5877..416de759c 100644 --- a/packages/artplayer-proxy-canvas/README.md +++ b/packages/artplayer-proxy-canvas/README.md @@ -53,6 +53,12 @@ Runtime falsy callback arguments retain historical behavior; declarations accept optional function, matching the published types. Implementation and test guidance is in [ARCHITECTURE.md](ARCHITECTURE.md). +The standard ArtPlayer `subtitle` option uses native subtitle tracks on the backing +video. HTML track elements passed to the active proxy's `appendChild` are attached +there so captions can load and follow playback; ordinary Canvas content keeps its +normal parent. The initial metadata track is replaced by the first actual subtitle. +This repairs the previously empty subtitle path without changing subtitle options. + ### Older TypeScript consumers The 1.0.0 declaration used `export =` and required a callback; 1.1.0 changed to diff --git a/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.js b/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.js index 78314121d..87e8419c7 100644 --- a/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.js +++ b/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(e,t){let n=!1,i=!1,o=!1,r=!1,a=0,s=null;function l(){null!==s&&cancelAnimationFrame(s),s=null}function c(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=a,s=()=>{o=!1,c()};e(()=>!i&&t()&&a===n).then(s,s)}else n&&null===s&&(s=requestAnimationFrame(()=>{s=null,r=!0,c()}))}function u(){!i&&t()&&(a++,r=!0,l(),c())}function f(){n=!1,r=!1,a++,l()}return{request:u,start(){n||i||!t()||(n=!0,u())},stop:f,destroy(){i=!0,f()}}}function n(n){return i=>{const o=i.constructor,r=o.utils.createElement("canvas"),a=o.utils.createElement("video");let s=i,l=i.isDestroy,c=null;const u=[],f=()=>!l&&!s?.isDestroy,d=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(a,()=>s?.template?.$player,f),p=function(t,n,i,o,r){const a=t.getContext("2d");let s=!1;return{available:()=>!!a||(s||(s=!0,r(new Error("Canvas 2D context is unavailable"))),!1),async draw(s){const l=()=>s()&&n.readyState>=2&&!n.seeking;if(!a||!l()||!e(n)||!t.width||!t.height)return;let c=!1;try{if("undefined"!==typeof createImageBitmap){c=!0;const e=await createImageBitmap(n);c=!1;try{l()&&a.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else a.drawImage(n,0,0,t.width,t.height);if(!l())return;i&&i(a,n),l()&&o(a,n)}catch(u){const e=c&&"object"===typeof u&&null!==u&&"name"in u&&"InvalidStateError"===u.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;l()&&!e&&r(u)}}}}(r,a,n,(e,t)=>s?.emit("artplayerProxyCanvas:draw",e,t),e=>s?.emit("artplayerProxyCanvas:error",e)),h=t(p.draw,f);if(function(e,t,n,i,o){const r=new Map;for(const a in e){const t=Reflect.get(e,a);"function"===typeof t&&r.set(a,(...n)=>Reflect.apply(t,e,n))}for(const a in t)a in e||Object.defineProperty(e,a,{get(){const e=Reflect.get(t,a);return"function"!==typeof e?e:(...r)=>n()?("load"===a&&i(),"play"===a&&o(),Reflect.apply(e,t,r)):"play"===a?Promise.resolve():void 0},set(e){n()&&("src"!==a&&"srcObject"!==a||i(),Reflect.set(t,a,e))},configurable:!0,enumerable:!0});for(const[a,s]of r)Reflect.set(e,a,s)}(r,a,f,h.stop,d.mount),l)return s=null,r;function y(){if(l)return;l=!0,h.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...u.splice(0).reverse(),d.destroy])try{n()}catch(t){e.push(t)}if(s=null,r.width=0,r.height=0,e.length)throw e[0]}function m(e,t){if(!f())return;const n=()=>{("destroy"===e||f())&&t()},o=()=>i.off(e,n);u.push(o),i.on(e,n),l&&o()}function v(){p.available()&&h.request()}try{m("destroy",y),m("video:loadedmetadata",()=>{e(a)&&(r.width=a.videoWidth,r.height=a.videoHeight)}),m("video:play",()=>{d.mount(),p.available()&&h.start()}),m("video:pause",h.stop),m("video:emptied",h.stop),m("video:seeked",v),m("resize",()=>{!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,a=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(a)||r<=0||a<=0)return;const s=n.videoWidth/n.videoHeight,l=r/a>s?a*s:r,c=r/a>s?a:r/s;t.width=l,t.height=c,t.style.padding=`${(a-c)/2}px ${(r-l)/2}px`}(r,a,s?.template?.$player,s?.option.autoSize),v()}),f()&&(c=setTimeout(()=>{if(c=null,f())try{d.mount();for(const e of o.config.events){if(!f())break;const t=e=>{f()&&s?.emit(`video:${e.type}`,e)},n=()=>a.removeEventListener(e,t);u.push(n);const o=i.proxy(a,e,t);f()?u.push(o):o()}}catch(e){try{y()}catch{}i.emit("artplayerProxyCanvas:error",e)}}))}catch(g){try{y()}catch{}throw g}return r}}return Object.assign(n,{default:n})}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(e,t){let n=!1,i=!1,o=!1,r=!1,a=0,l=null;function s(){null!==l&&cancelAnimationFrame(l),l=null}function c(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=a,l=()=>{o=!1,c()};e(()=>!i&&t()&&a===n).then(l,l)}else n&&null===l&&(l=requestAnimationFrame(()=>{l=null,r=!0,c()}))}function u(){!i&&t()&&(a++,r=!0,s(),c())}function d(){n=!1,r=!1,a++,s()}return{request:u,start(){n||i||!t()||(n=!0,u())},stop:d,destroy(){i=!0,d()}}}function n(n){return i=>{const o=i.constructor,r=o.utils.createElement("canvas"),a=o.utils.createElement("video");let l=i,s=i.isDestroy,c=null;const u=[],d=()=>!s&&!l?.isDestroy,f=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(a,()=>l?.template?.$player,d),p=function(t,n,i,o,r){const a=t.getContext("2d");let l=!1;return{available:()=>!!a||(l||(l=!0,r(new Error("Canvas 2D context is unavailable"))),!1),async draw(l){const s=()=>l()&&n.readyState>=2&&!n.seeking;if(!a||!s()||!e(n)||!t.width||!t.height)return;let c=!1;try{if("undefined"!==typeof createImageBitmap){c=!0;const e=await createImageBitmap(n);c=!1;try{s()&&a.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else a.drawImage(n,0,0,t.width,t.height);if(!s())return;i&&i(a,n),s()&&o(a,n)}catch(u){const e=c&&"object"===typeof u&&null!==u&&"name"in u&&"InvalidStateError"===u.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;s()&&!e&&r(u)}}}}(r,a,n,(e,t)=>l?.emit("artplayerProxyCanvas:draw",e,t),e=>l?.emit("artplayerProxyCanvas:error",e)),h=t(p.draw,d);if(function(e,t,n,i,o){const r=new Map;for(const a in e){const t=Reflect.get(e,a);"function"===typeof t&&r.set(a,(...n)=>Reflect.apply(t,e,n))}for(const a in t)a in e||Object.defineProperty(e,a,{get(){const e=Reflect.get(t,a);return"function"!==typeof e?e:(...r)=>n()?("load"===a&&i(),"play"===a&&o(),Reflect.apply(e,t,r)):"play"===a?Promise.resolve():void 0},set(e){n()&&("src"!==a&&"srcObject"!==a||i(),Reflect.set(t,a,e))},configurable:!0,enumerable:!0});for(const[a,l]of r)Reflect.set(e,a,l)}(r,a,d,h.stop,f.mount),s)return l=null,r;function y(){if(s)return;s=!0,h.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...u.splice(0).reverse(),f.destroy])try{n()}catch(t){e.push(t)}if(l=null,r.width=0,r.height=0,e.length)throw e[0]}function m(e,t){if(!d())return;const n=()=>{("destroy"===e||d())&&t()},o=()=>i.off(e,n);u.push(o),i.on(e,n),s&&o()}function v(){p.available()&&h.request()}try{!function(e,t,n,i){if(!i())return;const o=e.appendChild;n.kind="metadata",n.default=!0,t.appendChild(n),e.appendChild=r=>i()&&1===r.nodeType&&"TRACK"===r.nodeName&&"namespaceURI"in r&&"http://www.w3.org/1999/xhtml"===r.namespaceURI?(n.remove(),t.appendChild(r)):o.call(e,r)}(r,a,o.utils.createElement("track"),d),m("destroy",y),m("video:loadedmetadata",()=>{e(a)&&(r.width=a.videoWidth,r.height=a.videoHeight)}),m("video:play",()=>{f.mount(),p.available()&&h.start()}),m("video:pause",h.stop),m("video:emptied",h.stop),m("video:seeked",v),m("resize",()=>{!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,a=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(a)||r<=0||a<=0)return;const l=n.videoWidth/n.videoHeight,s=r/a>l?a*l:r,c=r/a>l?a:r/l;t.width=s,t.height=c,t.style.padding=`${(a-c)/2}px ${(r-s)/2}px`}(r,a,l?.template?.$player,l?.option.autoSize),v()}),d()&&(c=setTimeout(()=>{if(c=null,d())try{f.mount();for(const e of o.config.events){if(!d())break;const t=e=>{d()&&l?.emit(`video:${e.type}`,e)},n=()=>a.removeEventListener(e,t);u.push(n);const o=i.proxy(a,e,t);d()?u.push(o):o()}}catch(e){try{y()}catch{}i.emit("artplayerProxyCanvas:error",e)}}))}catch(g){try{y()}catch{}throw g}return r}}return Object.assign(n,{default:n})}); diff --git a/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.legacy.js b/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.legacy.js index 39b4ddd53..e4374cd63 100644 --- a/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.legacy.js +++ b/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(t,n,i,o,r){const l=t.getContext("2d");let a=!1;return{available:()=>!!l||(a||(a=!0,r(new Error("Canvas 2D context is unavailable"))),!1),draw(a){return s=this,u=null,c=function*(){const s=()=>a()&&n.readyState>=2&&!n.seeking;if(!l||!s()||!e(n)||!t.width||!t.height)return;let u=!1;try{if("undefined"!==typeof createImageBitmap){u=!0;const e=yield createImageBitmap(n);u=!1;try{s()&&l.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else l.drawImage(n,0,0,t.width,t.height);if(!s())return;i&&i(l,n),s()&&o(l,n)}catch(c){const e=u&&"object"===typeof c&&null!==c&&"name"in c&&"InvalidStateError"===c.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;s()&&!e&&r(c)}},new Promise((e,t)=>{var n=e=>{try{o(c.next(e))}catch(n){t(n)}},i=e=>{try{o(c.throw(e))}catch(n){t(n)}},o=t=>t.done?e(t.value):Promise.resolve(t.value).then(n,i);o((c=c.apply(s,u)).next())});var s,u,c}}}function n(e,t){let n=!1,i=!1,o=!1,r=!1,l=0,a=null;function s(){null!==a&&cancelAnimationFrame(a),a=null}function u(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=l,a=()=>{o=!1,u()};e(()=>!i&&t()&&l===n).then(a,a)}else n&&null===a&&(a=requestAnimationFrame(()=>{a=null,r=!0,u()}))}function c(){!i&&t()&&(l++,r=!0,s(),u())}function d(){n=!1,r=!1,l++,s()}return{request:c,start(){n||i||!t()||(n=!0,c())},stop:d,destroy(){i=!0,d()}}}function i(i){return o=>{const r=o.constructor,l=r.utils.createElement("canvas"),a=r.utils.createElement("video");let s=o,u=o.isDestroy,c=null;const d=[],f=()=>!u&&!(null==s?void 0:s.isDestroy),h=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(a,()=>{var e;return null==(e=null==s?void 0:s.template)?void 0:e.$player},f),p=t(l,a,i,(e,t)=>null==s?void 0:s.emit("artplayerProxyCanvas:draw",e,t),e=>null==s?void 0:s.emit("artplayerProxyCanvas:error",e)),y=n(p.draw,f);if(function(e,t,n,i,o){const r=new Map;for(const l in e){const t=Reflect.get(e,l);"function"===typeof t&&r.set(l,(...n)=>Reflect.apply(t,e,n))}for(const l in t)l in e||Object.defineProperty(e,l,{get(){const e=Reflect.get(t,l);return"function"!==typeof e?e:(...r)=>n()?("load"===l&&i(),"play"===l&&o(),Reflect.apply(e,t,r)):"play"===l?Promise.resolve():void 0},set(e){n()&&("src"!==l&&"srcObject"!==l||i(),Reflect.set(t,l,e))},configurable:!0,enumerable:!0});for(const[l,a]of r)Reflect.set(e,l,a)}(l,a,f,y.stop,h.mount),u)return s=null,l;function v(){if(u)return;u=!0,y.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...d.splice(0).reverse(),h.destroy])try{n()}catch(t){e.push(t)}if(s=null,l.width=0,l.height=0,e.length)throw e[0]}function m(e,t){if(!f())return;const n=()=>{("destroy"===e||f())&&t()},i=()=>o.off(e,n);d.push(i),o.on(e,n),u&&i()}function g(){p.available()&&y.request()}try{m("destroy",v),m("video:loadedmetadata",()=>{e(a)&&(l.width=a.videoWidth,l.height=a.videoHeight)}),m("video:play",()=>{h.mount(),p.available()&&y.start()}),m("video:pause",y.stop),m("video:emptied",y.stop),m("video:seeked",g),m("resize",()=>{var t;!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,l=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(l)||r<=0||l<=0)return;const a=n.videoWidth/n.videoHeight,s=r/l>a?l*a:r,u=r/l>a?l:r/a;t.width=s,t.height=u,t.style.padding=`${(l-u)/2}px ${(r-s)/2}px`}(l,a,null==(t=null==s?void 0:s.template)?void 0:t.$player,null==s?void 0:s.option.autoSize),g()}),f()&&(c=setTimeout(()=>{if(c=null,f())try{h.mount();for(const e of r.config.events){if(!f())break;const t=e=>{f()&&(null==s||s.emit(`video:${e.type}`,e))},n=()=>a.removeEventListener(e,t);d.push(n);const i=o.proxy(a,e,t);f()?d.push(i):i()}}catch(e){try{v()}catch(t){}o.emit("artplayerProxyCanvas:error",e)}}))}catch(b){try{v()}catch(w){}throw b}return l}}return Object.assign(i,{default:i})}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyCanvas=t(),define(function(){return e.artplayerProxyCanvas})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyCanvas=t()}(this,function(){"use strict";function e(e){return Number.isFinite(e.videoWidth)&&Number.isFinite(e.videoHeight)&&e.videoWidth>0&&e.videoHeight>0}function t(t,n,i,o,r){const a=t.getContext("2d");let l=!1;return{available:()=>!!a||(l||(l=!0,r(new Error("Canvas 2D context is unavailable"))),!1),draw(l){return s=this,u=null,c=function*(){const s=()=>l()&&n.readyState>=2&&!n.seeking;if(!a||!s()||!e(n)||!t.width||!t.height)return;let u=!1;try{if("undefined"!==typeof createImageBitmap){u=!0;const e=yield createImageBitmap(n);u=!1;try{s()&&a.drawImage(e,0,0,t.width,t.height)}finally{e.close()}}else a.drawImage(n,0,0,t.width,t.height);if(!s())return;i&&i(a,n),s()&&o(a,n)}catch(c){const e=u&&"object"===typeof c&&null!==c&&"name"in c&&"InvalidStateError"===c.name&&0===n.currentTime&&"function"===typeof n.getVideoPlaybackQuality&&0===n.getVideoPlaybackQuality().totalVideoFrames;s()&&!e&&r(c)}},new Promise((e,t)=>{var n=e=>{try{o(c.next(e))}catch(n){t(n)}},i=e=>{try{o(c.throw(e))}catch(n){t(n)}},o=t=>t.done?e(t.value):Promise.resolve(t.value).then(n,i);o((c=c.apply(s,u)).next())});var s,u,c}}}function n(e,t){let n=!1,i=!1,o=!1,r=!1,a=0,l=null;function s(){null!==l&&cancelAnimationFrame(l),l=null}function u(){if(!o&&!i&&t())if(r){r=!1,o=!0;const n=a,l=()=>{o=!1,u()};e(()=>!i&&t()&&a===n).then(l,l)}else n&&null===l&&(l=requestAnimationFrame(()=>{l=null,r=!0,u()}))}function c(){!i&&t()&&(a++,r=!0,s(),u())}function d(){n=!1,r=!1,a++,s()}return{request:c,start(){n||i||!t()||(n=!0,c())},stop:d,destroy(){i=!0,d()}}}function i(i){return o=>{const r=o.constructor,a=r.utils.createElement("canvas"),l=r.utils.createElement("video");let s=o,u=o.isDestroy,c=null;const d=[],f=()=>!u&&!(null==s?void 0:s.isDestroy),p=function(e,t,n){let i=!1;return{mount(){if(i||!n())return;const o=t();o&&(e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.playsInline=!0,Object.assign(e.style,{position:"absolute",width:"auto",height:"auto",opacity:"0",pointerEvents:"none",left:"0",top:"0"}),o.appendChild(e),i=!0,n()||e.remove())},destroy(){const t=[],n=()=>{e.srcObject=null};for(const i of[()=>e.pause(),()=>e.removeAttribute("src"),n,()=>e.load(),()=>e.remove()])try{i()}catch(o){t.push(o)}if(i=!1,t.length)throw t[0]}}}(l,()=>{var e;return null==(e=null==s?void 0:s.template)?void 0:e.$player},f),h=t(a,l,i,(e,t)=>null==s?void 0:s.emit("artplayerProxyCanvas:draw",e,t),e=>null==s?void 0:s.emit("artplayerProxyCanvas:error",e)),y=n(h.draw,f);if(function(e,t,n,i,o){const r=new Map;for(const a in e){const t=Reflect.get(e,a);"function"===typeof t&&r.set(a,(...n)=>Reflect.apply(t,e,n))}for(const a in t)a in e||Object.defineProperty(e,a,{get(){const e=Reflect.get(t,a);return"function"!==typeof e?e:(...r)=>n()?("load"===a&&i(),"play"===a&&o(),Reflect.apply(e,t,r)):"play"===a?Promise.resolve():void 0},set(e){n()&&("src"!==a&&"srcObject"!==a||i(),Reflect.set(t,a,e))},configurable:!0,enumerable:!0});for(const[a,l]of r)Reflect.set(e,a,l)}(a,l,f,y.stop,p.mount),u)return s=null,a;function m(){if(u)return;u=!0,y.destroy(),null!==c&&clearTimeout(c),c=null;const e=[];for(const n of[...d.splice(0).reverse(),p.destroy])try{n()}catch(t){e.push(t)}if(s=null,a.width=0,a.height=0,e.length)throw e[0]}function v(e,t){if(!f())return;const n=()=>{("destroy"===e||f())&&t()},i=()=>o.off(e,n);d.push(i),o.on(e,n),u&&i()}function g(){h.available()&&y.request()}try{!function(e,t,n,i){if(!i())return;const o=e.appendChild;n.kind="metadata",n.default=!0,t.appendChild(n),e.appendChild=r=>i()&&1===r.nodeType&&"TRACK"===r.nodeName&&"namespaceURI"in r&&"http://www.w3.org/1999/xhtml"===r.namespaceURI?(n.remove(),t.appendChild(r)):o.call(e,r)}(a,l,r.utils.createElement("track"),f),v("destroy",m),v("video:loadedmetadata",()=>{e(l)&&(a.width=l.videoWidth,a.height=l.videoHeight)}),v("video:play",()=>{p.mount(),h.available()&&y.start()}),v("video:pause",y.stop),v("video:emptied",y.stop),v("video:seeked",g),v("resize",()=>{var t;!function(t,n,i,o){if(!i||o||!e(n))return;const r=i.clientWidth,a=i.clientHeight;if(!Number.isFinite(r)||!Number.isFinite(a)||r<=0||a<=0)return;const l=n.videoWidth/n.videoHeight,s=r/a>l?a*l:r,u=r/a>l?a:r/l;t.width=s,t.height=u,t.style.padding=`${(a-u)/2}px ${(r-s)/2}px`}(a,l,null==(t=null==s?void 0:s.template)?void 0:t.$player,null==s?void 0:s.option.autoSize),g()}),f()&&(c=setTimeout(()=>{if(c=null,f())try{p.mount();for(const e of r.config.events){if(!f())break;const t=e=>{f()&&(null==s||s.emit(`video:${e.type}`,e))},n=()=>l.removeEventListener(e,t);d.push(n);const i=o.proxy(l,e,t);f()?d.push(i):i()}}catch(e){try{m()}catch(t){}o.emit("artplayerProxyCanvas:error",e)}}))}catch(b){try{m()}catch(w){}throw b}return a}}return Object.assign(i,{default:i})}); diff --git a/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs b/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs index 2b6658e62..a87f39755 100644 --- a/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs +++ b/packages/artplayer-proxy-canvas/dist/artplayer-proxy-canvas.mjs @@ -204,6 +204,21 @@ function createFrameScheduler(draw, active) { } }; } +function forwardSubtitleTrack(canvas, video, initialTrack, active) { + if (!active()) + return; + const append = canvas.appendChild; + initialTrack.kind = "metadata"; + initialTrack.default = true; + video.appendChild(initialTrack); + canvas.appendChild = (node) => { + if (active() && node.nodeType === 1 && node.nodeName === "TRACK" && "namespaceURI" in node && node.namespaceURI === "http://www.w3.org/1999/xhtml") { + initialTrack.remove(); + return video.appendChild(node); + } + return append.call(canvas, node); + }; +} function artplayerProxyCanvas(callback) { return (art) => { const constructor = art.constructor; @@ -262,6 +277,7 @@ function artplayerProxyCanvas(callback) { scheduler.request(); } try { + forwardSubtitleTrack(canvas, video, constructor.utils.createElement("track"), active); listen("destroy", destroy); listen("video:loadedmetadata", () => { if (hasDimensions(video)) { diff --git a/packages/artplayer-proxy-canvas/src/index.ts b/packages/artplayer-proxy-canvas/src/index.ts index 1fa2297e0..aaf27f244 100644 --- a/packages/artplayer-proxy-canvas/src/index.ts +++ b/packages/artplayer-proxy-canvas/src/index.ts @@ -5,6 +5,7 @@ import { hasDimensions, resizeCanvas } from './geometry' import { ownMedia } from './media' import { createRenderer } from './renderer' import { createFrameScheduler } from './scheduler' +import { forwardSubtitleTrack } from './subtitles' function artplayerProxyCanvas(callback?: DrawCallback) { return (art: Artplayer): HTMLCanvasElement => { @@ -67,6 +68,7 @@ function artplayerProxyCanvas(callback?: DrawCallback) { } try { + forwardSubtitleTrack(canvas, video, constructor.utils.createElement('track'), active) listen('destroy', destroy) listen('video:loadedmetadata', () => { if (hasDimensions(video)) { diff --git a/packages/artplayer-proxy-canvas/src/subtitles.ts b/packages/artplayer-proxy-canvas/src/subtitles.ts new file mode 100644 index 000000000..3e36dd8c0 --- /dev/null +++ b/packages/artplayer-proxy-canvas/src/subtitles.ts @@ -0,0 +1,15 @@ +export function forwardSubtitleTrack(canvas: HTMLCanvasElement, video: HTMLVideoElement, initialTrack: HTMLTrackElement, active: () => boolean): void { + if (!active()) + return + const append = canvas.appendChild + initialTrack.kind = 'metadata' + initialTrack.default = true + video.appendChild(initialTrack) + canvas.appendChild = (node: T): T => { + if (active() && node.nodeType === 1 && node.nodeName === 'TRACK' && 'namespaceURI' in node && node.namespaceURI === 'http://www.w3.org/1999/xhtml') { + initialTrack.remove() + return video.appendChild(node) + } + return append.call(canvas, node) as T + } +} diff --git a/refactor/baselines/canvas-subtitles-validation.json b/refactor/baselines/canvas-subtitles-validation.json new file mode 100644 index 000000000..2b987b08c --- /dev/null +++ b/refactor/baselines/canvas-subtitles-validation.json @@ -0,0 +1,1140 @@ +{ + "task": "PKG-CANVAS-SUBTITLE-01", + "date": "2026-09-14", + "node": "24.21.0", + "yarn": "1.22.22", + "conclusion": "The backing video lacks the initial track required by the core synchronous subtitle capability check. Adding only appendChild routing is insufficient. An empty native metadata track plus routing real HTML tracks enables native cues without changing core consumers.", + "scope": "Actual Canvas and native video/text tracks; old core 5.4.0 and current core, source and verified installed main/legacy, subtitle replacement/seek/cleanup and normal Canvas nodes. No mocked media or cues.", + "runs": [ + { + "directory": "refactor/.cache/canvas-subtitles-red-report", + "sha256": "62c48389ba2643bb405a18477e317bcf917b97420ba547d5612c49722dab08ab", + "stats": { + "startTime": "2026-09-14T10:32:01.024Z", + "duration": 30681.715999999997, + "expected": 0, + "skipped": 0, + "unexpected": 6, + "flaky": 0 + }, + "result": { + "scope": "source", + "exitCode": 1, + "signal": null, + "args": [ + "test", + "--config=playwright.source.config.js", + "test/browser/canvas-subtitles.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "candidate-source", + "sha256": "0d4ef6ac8df37aa33b6b1bf338f0b8135c709b9840533c9fa9b350deb707e1a1", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.164713 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "0d4ef6ac8df37aa33b6b1bf338f0b8135c709b9840533c9fa9b350deb707e1a1", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.163537 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "candidate-source", + "sha256": "0d4ef6ac8df37aa33b6b1bf338f0b8135c709b9840533c9fa9b350deb707e1a1", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.414475 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "0d4ef6ac8df37aa33b6b1bf338f0b8135c709b9840533c9fa9b350deb707e1a1", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.458332 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "candidate-source", + "sha256": "0d4ef6ac8df37aa33b6b1bf338f0b8135c709b9840533c9fa9b350deb707e1a1", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.2701148 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "0d4ef6ac8df37aa33b6b1bf338f0b8135c709b9840533c9fa9b350deb707e1a1", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.3426509 + } + ], + "failures": [ + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "chromium", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "chromium", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "firefox", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "firefox", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "webkit", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "webkit", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + } + ], + "inputs": [] + }, + { + "directory": "refactor/.cache/canvas-subtitles-route-only-report", + "sha256": "3dd69fb9040c626fa32eeeea2f0c897f0ef7dacd6cc615604c818752532c7edd", + "stats": { + "startTime": "2026-09-14T10:33:26.462Z", + "duration": 31060.009, + "expected": 0, + "skipped": 0, + "unexpected": 6, + "flaky": 0 + }, + "result": { + "scope": "source", + "exitCode": 1, + "signal": null, + "args": [ + "test", + "--config=playwright.source.config.js", + "test/browser/canvas-subtitles.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "candidate-source", + "sha256": "1a5d1c6ed13540b496b7489cfd79e48c2d8f5ab61c48f19494b2936ec7cef7c4", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.164371 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "1a5d1c6ed13540b496b7489cfd79e48c2d8f5ab61c48f19494b2936ec7cef7c4", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.164517 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "candidate-source", + "sha256": "1a5d1c6ed13540b496b7489cfd79e48c2d8f5ab61c48f19494b2936ec7cef7c4", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.169545 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "1a5d1c6ed13540b496b7489cfd79e48c2d8f5ab61c48f19494b2936ec7cef7c4", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.455228 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "candidate-source", + "sha256": "1a5d1c6ed13540b496b7489cfd79e48c2d8f5ab61c48f19494b2936ec7cef7c4", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.3036606 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "1a5d1c6ed13540b496b7489cfd79e48c2d8f5ab61c48f19494b2936ec7cef7c4", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "backingTracks": 0, + "text": "", + "time": 0.2724968 + } + ], + "failures": [ + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "chromium", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "chromium", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "firefox", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "firefox", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "webkit", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "webkit", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 20 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 21 | await page.click('#pause')\n> 22 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 23 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 24 | await page.evaluate(() => window.art.currentTime = 3)\n 25 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:22:85" + ] + } + ], + "inputs": [] + }, + { + "directory": "refactor/.cache/canvas-subtitles-bootstrap-report", + "sha256": "e8d4bf023079013683aac571558d96130894b3c383d1948713d46d83fdb3124d", + "stats": { + "startTime": "2026-09-14T10:35:20.365Z", + "duration": 10304.928, + "expected": 6, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "result": { + "scope": "source", + "exitCode": 0, + "signal": null, + "args": [ + "test", + "--config=playwright.source.config.js", + "test/browser/canvas-subtitles.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "candidate-source", + "sha256": "c7038f9e2317fbf24679cb5734f89141d167218352b6ea00981cd6b0a81f6b18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 2, + "backingTracks": 1, + "text": "\n Later caption\n ", + "time": 3 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "c7038f9e2317fbf24679cb5734f89141d167218352b6ea00981cd6b0a81f6b18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 2, + "backingTracks": 1, + "text": "\n Later caption\n ", + "time": 3 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "candidate-source", + "sha256": "c7038f9e2317fbf24679cb5734f89141d167218352b6ea00981cd6b0a81f6b18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 2, + "backingTracks": 1, + "text": "\n Later caption\n ", + "time": 3 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "c7038f9e2317fbf24679cb5734f89141d167218352b6ea00981cd6b0a81f6b18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 2, + "backingTracks": 1, + "text": "\n Later caption\n ", + "time": 3 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "candidate-source", + "sha256": "c7038f9e2317fbf24679cb5734f89141d167218352b6ea00981cd6b0a81f6b18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 2, + "backingTracks": 1, + "text": "\n Later caption\n ", + "time": 3.0013372 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "c7038f9e2317fbf24679cb5734f89141d167218352b6ea00981cd6b0a81f6b18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 2, + "backingTracks": 1, + "text": "\n Later caption\n ", + "time": 3.0014898 + } + ], + "failures": [], + "inputs": [] + }, + { + "directory": "refactor/.cache/canvas-subtitles-expanded-report", + "sha256": "400de980fdb957296f27f54042955bc77bcc7fb2d7c70401fd7854c427f94a6a", + "stats": { + "startTime": "2026-09-14T10:37:14.903Z", + "duration": 23013.584000000003, + "expected": 24, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "result": { + "scope": "source", + "exitCode": 0, + "signal": null, + "args": [ + "test", + "--config=playwright.source.config.js", + "test/browser/canvas-subtitles.spec.js", + "test/browser/canvas-lifecycle.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "candidate-source", + "sha256": "84cc55ac620ac5f240119e57cf473b09e197e8e74e012774645d18be9d06472c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "84cc55ac620ac5f240119e57cf473b09e197e8e74e012774645d18be9d06472c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "candidate-source", + "sha256": "84cc55ac620ac5f240119e57cf473b09e197e8e74e012774645d18be9d06472c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "84cc55ac620ac5f240119e57cf473b09e197e8e74e012774645d18be9d06472c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "candidate-source", + "sha256": "84cc55ac620ac5f240119e57cf473b09e197e8e74e012774645d18be9d06472c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.0022403 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "candidate-source", + "sha256": "84cc55ac620ac5f240119e57cf473b09e197e8e74e012774645d18be9d06472c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.0018386 + } + ], + "failures": [], + "inputs": [] + }, + { + "directory": "refactor/.cache/canvas-subtitles-installed-main-report", + "sha256": "43287d7984312bdecc2002db2d97d0e1b02f1968f547feb63c41afff26034164", + "stats": { + "startTime": "2026-09-14T10:40:33.147Z", + "duration": 34399.632999999994, + "expected": 33, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "result": { + "scope": "installed", + "exitCode": 0, + "signal": null, + "args": [ + "test", + "--config=playwright.installed.config.js", + "test/browser/canvas-subtitles.spec.js", + "test/browser/canvas-lifecycle.spec.js", + "test/browser/ambilight-proxy.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.0029913 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.001549 + } + ], + "failures": [], + "inputs": [ + { + "name": "artplayer", + "sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6", + "member": "package/dist/artplayer.js", + "archiveSha256": "b84d2bdfe1d6d625743d274923b8bd21e20d7d180c9c628ded6d61d37a894e2a" + }, + { + "name": "artplayer-plugin-chapter", + "sha256": "179c3ea90ddc85da27b46f000110e32d464dfa5c8c394e98316dc1b75d6f2a72", + "member": "package/dist/artplayer-plugin-chapter.js", + "archiveSha256": "1f6ea40e9a328e3014325e1d88c584a513213cdb1c7d446e5ca4f15efd4dd6a9" + }, + { + "name": "artplayer-plugin-ambilight", + "sha256": "21ee5227f26cfff6e82ee6932f6eafc9741fa3be0ec2f5252a47b53ceea14599", + "member": "package/dist/artplayer-plugin-ambilight.js", + "archiveSha256": "9d5950a513b8fc45090d6c80395622ddbfec2394b2863d788ddbdefce7699ebf" + }, + { + "name": "artplayer-proxy-canvas", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "member": "package/dist/artplayer-proxy-canvas.js", + "archiveSha256": "128ed15493791a351a5d37882b94e0db3fc444b58c9d194d70756c4f0c99727e" + } + ] + }, + { + "directory": "refactor/.cache/canvas-subtitles-installed-legacy-report", + "sha256": "b907e4e1f4968e0029ba4e5ee16b3e0a419bcf216211f4fdbfc304abc8f8378c", + "stats": { + "startTime": "2026-09-14T10:41:35.371Z", + "duration": 34319.648, + "expected": 33, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "result": { + "scope": "installed", + "exitCode": 0, + "signal": null, + "args": [ + "test", + "--config=playwright.installed.config.js", + "test/browser/canvas-subtitles.spec.js", + "test/browser/canvas-lifecycle.spec.js", + "test/browser/ambilight-proxy.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "candidate-installed", + "sha256": "069092ba27b48b7a54affc9c3095b6032c063b1f61c15fcb3249130ea18b9326", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "069092ba27b48b7a54affc9c3095b6032c063b1f61c15fcb3249130ea18b9326", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "candidate-installed", + "sha256": "069092ba27b48b7a54affc9c3095b6032c063b1f61c15fcb3249130ea18b9326", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "069092ba27b48b7a54affc9c3095b6032c063b1f61c15fcb3249130ea18b9326", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "candidate-installed", + "sha256": "069092ba27b48b7a54affc9c3095b6032c063b1f61c15fcb3249130ea18b9326", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.0017797 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "069092ba27b48b7a54affc9c3095b6032c063b1f61c15fcb3249130ea18b9326", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.0014418 + } + ], + "failures": [], + "inputs": [ + { + "name": "artplayer", + "sha256": "9bde35a6d64234cae15300e37b03204c075ef26126058f48bb3bf97d6e002449", + "member": "package/dist/artplayer.legacy.js", + "archiveSha256": "b84d2bdfe1d6d625743d274923b8bd21e20d7d180c9c628ded6d61d37a894e2a" + }, + { + "name": "artplayer-plugin-chapter", + "sha256": "dade919ed044cbf46ac16e505f538f86a86edccf177da2e14d79d111559d4bef", + "member": "package/dist/artplayer-plugin-chapter.legacy.js", + "archiveSha256": "1f6ea40e9a328e3014325e1d88c584a513213cdb1c7d446e5ca4f15efd4dd6a9" + }, + { + "name": "artplayer-plugin-ambilight", + "sha256": "c3ed1a54e44a0202811d70378cb19edf87b1ebd010d05fe96ebe64d0b4301d5d", + "member": "package/dist/artplayer-plugin-ambilight.legacy.js", + "archiveSha256": "9d5950a513b8fc45090d6c80395622ddbfec2394b2863d788ddbdefce7699ebf" + }, + { + "name": "artplayer-proxy-canvas", + "sha256": "069092ba27b48b7a54affc9c3095b6032c063b1f61c15fcb3249130ea18b9326", + "member": "package/dist/artplayer-proxy-canvas.legacy.js", + "archiveSha256": "128ed15493791a351a5d37882b94e0db3fc444b58c9d194d70756c4f0c99727e" + } + ] + }, + { + "directory": "refactor/.cache/canvas-subtitles-published-red-report", + "sha256": "b9fb5e8a92456e1c8b27f2a83d64c619a9755eddfc887c9eedd24afd21d2fe80", + "stats": { + "startTime": "2026-09-14T10:47:20.368Z", + "duration": 34959.439999999995, + "expected": 0, + "skipped": 0, + "unexpected": 6, + "flaky": 0 + }, + "result": { + "scope": "source", + "exitCode": 1, + "signal": null, + "args": [ + "test", + "--config=playwright.source.config.js", + "test/browser/canvas-subtitles.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "published-1.1.0", + "sha256": "ef489570e13f402f1a60eb256ce3900a0021a3d05699c2ba15adfaaeff184d4c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "text": "", + "time": 0.17707 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "published-1.1.0", + "sha256": "ef489570e13f402f1a60eb256ce3900a0021a3d05699c2ba15adfaaeff184d4c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "text": "", + "time": 0.177045 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "published-1.1.0", + "sha256": "ef489570e13f402f1a60eb256ce3900a0021a3d05699c2ba15adfaaeff184d4c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "text": "", + "time": 0.172173 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "published-1.1.0", + "sha256": "ef489570e13f402f1a60eb256ce3900a0021a3d05699c2ba15adfaaeff184d4c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "text": "", + "time": 0.174608 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "published-1.1.0", + "sha256": "ef489570e13f402f1a60eb256ce3900a0021a3d05699c2ba15adfaaeff184d4c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "text": "", + "time": 0.3123099 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "published-1.1.0", + "sha256": "ef489570e13f402f1a60eb256ce3900a0021a3d05699c2ba15adfaaeff184d4c", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 3, + "cues": 0, + "text": "", + "time": 0.3491449 + } + ], + "failures": [ + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "chromium", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 28 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 29 | await page.click('#pause')\n> 30 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 31 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 32 | await page.evaluate(() => window.art.currentTime = 3)\n 33 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:30:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "chromium", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 28 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 29 | await page.click('#pause')\n> 30 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 31 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 32 | await page.evaluate(() => window.art.currentTime = 3)\n 33 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:30:85" + ] + }, + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "firefox", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 28 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 29 | await page.click('#pause')\n> 30 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 31 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 32 | await page.evaluate(() => window.art.currentTime = 3)\n 33 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:30:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "firefox", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 28 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 29 | await page.click('#pause')\n> 30 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 31 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 32 | await page.evaluate(() => window.art.currentTime = 3)\n 33 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:30:85" + ] + }, + { + "title": "published: canvas proxy loads and seeks native subtitle cues", + "browser": "webkit", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 28 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 29 | await page.click('#pause')\n> 30 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 31 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 32 | await page.evaluate(() => window.art.currentTime = 3)\n 33 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:30:85" + ] + }, + { + "title": "candidate: canvas proxy loads and seeks native subtitle cues", + "browser": "webkit", + "errors": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m2\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 28 | await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)\n 29 | await page.click('#pause')\n> 30 | await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)\n | ^\n 31 | await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')\n 32 | await page.evaluate(() => window.art.currentTime = 3)\n 33 | await expect(page.locator('.art-subtitle')).toHaveText('Later caption')\n at D:\\github\\ArtPlayer\\test\\browser\\canvas-subtitles.spec.js:30:85" + ] + } + ], + "inputs": [] + }, + { + "directory": "refactor/.cache/canvas-subtitles-final-report", + "sha256": "2b586ee30c47f26c47af3a45dcbc97630c347639ce7ae58135aeff7300bbac81", + "stats": { + "startTime": "2026-09-14T10:48:32.636Z", + "duration": 10780.748, + "expected": 6, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "result": { + "scope": "installed", + "exitCode": 0, + "signal": null, + "args": [ + "test", + "--config=playwright.installed.config.js", + "test/browser/canvas-subtitles.spec.js", + "--workers=2" + ] + }, + "browsers": { + "chromium": "153.0.8010.12", + "firefox": "155.0", + "webkit": "26.6" + }, + "observations": [ + { + "browser": "chromium", + "core": "published", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "chromium", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "published", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "firefox", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4 + }, + { + "browser": "webkit", + "core": "published", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.0007267 + }, + { + "browser": "webkit", + "core": "candidate", + "implementation": "candidate-installed", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "renderer": "CANVAS", + "trackParent": "VIDEO", + "trackState": 2, + "cues": 1, + "backingTracks": 1, + "text": "\n Replacement caption\n ", + "time": 4.0020694 + } + ], + "failures": [], + "inputs": [ + { + "name": "artplayer", + "sha256": "9148a369636f814b03e6a1ed2a7db222a31b159f93e8a94d52c5a9dde2dfe6a6", + "member": "package/dist/artplayer.js", + "archiveSha256": "b84d2bdfe1d6d625743d274923b8bd21e20d7d180c9c628ded6d61d37a894e2a" + }, + { + "name": "artplayer-plugin-chapter", + "sha256": "179c3ea90ddc85da27b46f000110e32d464dfa5c8c394e98316dc1b75d6f2a72", + "member": "package/dist/artplayer-plugin-chapter.js", + "archiveSha256": "1f6ea40e9a328e3014325e1d88c584a513213cdb1c7d446e5ca4f15efd4dd6a9" + }, + { + "name": "artplayer-plugin-ambilight", + "sha256": "21ee5227f26cfff6e82ee6932f6eafc9741fa3be0ec2f5252a47b53ceea14599", + "member": "package/dist/artplayer-plugin-ambilight.js", + "archiveSha256": "9d5950a513b8fc45090d6c80395622ddbfec2394b2863d788ddbdefce7699ebf" + }, + { + "name": "artplayer-proxy-canvas", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18", + "member": "package/dist/artplayer-proxy-canvas.js", + "archiveSha256": "128ed15493791a351a5d37882b94e0db3fc444b58c9d194d70756c4f0c99727e" + } + ] + } + ], + "package": { + "directory": "refactor/.cache/packages/run-j8Po5Y", + "reportSha256": "aa9f90e4e73ed9aef60465fdab3e83db6b483979e0e644eed5abce403b283bb8", + "runtimeChecksScope": "36 core/chapter checks only; additional Canvas/Ambilight build/pack/install bytes verified separately" + }, + "types": { + "directory": "refactor/.cache/canvas-package-types-189k1I", + "reportSha256": "9b39556e9bb546349eda3f26214273363a2fff63c01a7692ae9cf07f8e333e31", + "cases": 17 + }, + "checks": { + "canvasUnit": 58, + "runnerAndRisks": 6, + "strictCanvasTypes": true, + "targetedLint": true, + "rootLint": "0 errors; 1 existing docs/assets/ts/artplayer.d.ts warning", + "demoSyntax": true, + "toolchain": true + }, + "demo": { + "url": "http://localhost:8082/?libs=./compiled/artplayer-proxy-canvas.js&example=canvas", + "browser": "Codex in-app browser", + "userAgentEngine": "Chrome/152.0.0.0", + "exactBrowserBuild": null, + "coreDisplayedVersion": "5.4.1", + "canvasCount": 1, + "nativeTrackCount": 1, + "cues": 34, + "playback": "Native currentTime 12.11811, paused false; canvas video image visually confirmed.", + "captions": "Fresh UI play showed 星星降落的日子 / 星が降った日 at 00:01; AX and screenshot confirmed both lines.", + "limits": [ + "Browser.getVersion and content export unsupported by in-app backend; screenshot is in the conversation, not a separately hashed file.", + "Initial text locator did not match the aria-named play control; AX click succeeded.", + "Direct diagnostic native seek returned time 0; it is not claimed as successful demo seeking. Native seek acceptance is in the committed installed tests.", + "Temporary tab closed after testing; no permanent injected page code." + ], + "proxyFile": { + "file": "docs/compiled/artplayer-proxy-canvas.js", + "sha256": "f572ea536107b7823452c5a99447b69e9226a3582c79dda95a4248cca819ec18" + } + }, + "remaining": [ + "Canvas-05 Document PiP and physical devices", + "Canvas-06 full distribution/release acceptance", + "Auto-thumbnail native first-frame problem unchanged", + "No remote CI, push, deployment or npm publication" + ] +} diff --git a/refactor/changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md b/refactor/changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md new file mode 100644 index 000000000..db74a5e90 --- /dev/null +++ b/refactor/changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md @@ -0,0 +1,60 @@ +# PKG-CANVAS-SUBTITLE-01 Canvas 原生字幕轨道 + +Canvas-05 的组合检查在三引擎、新旧核心均复现字幕 cues 为零。 +核心初始化会检查 textTracks[0];Canvas 底层 video 没有模板的初始 track,所以 +字幕初始化直接返回。仅将后续 appendChild(track) 转给 video 仍是六项失败, +已保留两轮报告,不能把插入路由单独视为修复。 + +最终方案:底层 video 先创建无 URL 的 metadata 占位轨道以提供原有能力探测; +核心实际 appendChild(HTML track) 时移除占位轨道,把真实字幕交给原生 video。 +普通 Canvas 节点操作保持原路径;销毁后不再向底层 video 插入轨道。 +新 subtitles.ts 只负责此适配,入口负责初始化失败回滚和整体资源释放。 + +这是修复原本不可工作的字幕路径。原生 Canvas 返回值、绘制方法、公开类型和 +媒体事件不变;实际字幕 track 的 parent 变成 video,已在 README/ARCHITECTURE +明确。仅 active 的 HTML track 插入进入媒体路径,普通节点继续使用绑定 Canvas +接收者的原方法。并未把所有 DOM 方法改为 video 方法,也没有修改消费者配置。 +核心负责真实轨道替换、cuechange 订阅和 Blob URL;代理负责底层 video 的释放。 +初始轨道创建/插入位于现有初始化 try/catch 内,失败回滚所有私有资源。 + +## 验证与历史对照 + +详情、浏览器版本、报告 SHA、加载包入口及摘要在 +[canvas-subtitles-validation.json](../baselines/canvas-subtitles-validation.json)。 + +- 最初源码和仅插入路由两轮各 6 失败;初始轨道修正后 6 通过。 +- 扩展原生字幕/Canvas 生命周期 24 通过,覆盖真实 VTT 加载、暂停寻址、字幕 + 替换、旧节点断开且只留一个原生轨道、普通 span 插入/移除、Canvas 方法和销毁。 +- 仓库外构建/打包/离线安装/冻结重装后,main/legacy 各 33 通过,另含 Ambilight + 组合播放/取色/双 RAF 清理。最后加入历史模式后再次定向 6 通过。 +- 真正 npm 1.1.0 同一字幕用例三引擎共 6 失败。设置 + `ARTPLAYER_CANVAS_SUBTITLE_BASELINE=1.1.0` 可重跑;它不允许与 installed map + 混用,且失败不被改写为通过。首次失败的 $track 仍在被替换掉的模板 video 内; + 不能误称它已经被 append 到 Canvas,根因首先是同步能力检查返回空。 +- 58 项 Canvas Node 回归,包括初始轨道插入异常后的回滚;17 项实际安装旧/新 + TypeScript 消费者;严格分包 TS、专项 ESLint、根 lint(0 错误/1 既有 warning)、 + 6 项风险/runner 测试及固定工具链通过。所有公开声明未变。 +- `yarn build artplayer-proxy-canvas` 生成三种 dist 并同步 docs/compiled。四包 + `test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas` 的 + 36 个运行时/5+8 类型模式仍只针对 core/chapter;不将它们虚报为 Canvas 验收。 + +本地 Canvas 示例增加现有 subtitle.srt 配置。内置浏览器显示一个 Canvas、底层 +video 的 34 条原生 cues,实际播放并显示“星星降落的日子 / 星が降った日”。 +UA 报 Chrome/152.0.0.0;该后端不支持完整版本查询或内容导出,截图在会话中。 +一次直接诊断 seek 返回 0,未计为 demo 寻址成功;正式寻址证据来自安装测试。 +临时标签页已关闭。对示例单独使用普通 Node ESLint 会报页面提供的 Artplayer/ +插件全局与编辑器消费的 art 未使用;没有为此改动全局 lint 规则。示例语法和 +实际页面验证通过,测试文件自身的 if 样式诊断已修复。 + +## 兼容、接续和回退 + +API-07/08 的修复限于原先无法工作的字幕能力与轨道父节点;原接口、样式类、 +Canvas 绘制、工厂同步返回和 main/module/legacy/types 入口均保留。没有新增依赖。 +后续字幕工作从 subtitles.ts 开始,调度仍归 scheduler/renderer,避免依赖核心私有 +实现。运行 `yarn test:browser:installed test/browser/canvas-subtitles.spec.js +test/browser/canvas-lifecycle.spec.js test/browser/ambilight-proxy.spec.js --workers=2` +前提供已验证的四包 map;普通源码模式用 test:browser:source。 + +Canvas-05/06、Document PiP、真机及完整发布门槛仍未完成;Auto Thumbnail 首帧 +问题也未改变。没有远端 CI、推送、部署或 npm 发布。本任务独立本地提交。 +回退需一起还原 source、测试、架构/示例、生成产物和任务/风险状态,保留历史报告。 diff --git a/refactor/plan.md b/refactor/plan.md index 85e4be106..9d3834a4e 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -2,9 +2,9 @@ > 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。 -基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 258 项,范围 22 个包及工作区/示例。 +基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 259 项,范围 22 个包及工作区/示例。 -状态:todo 50 / doing 19 / blocked 0 / done 189 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 50 / doing 19 / blocked 0 / done 190 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -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-07, PKG-MULTI-SUB-08, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06, PKG-MULTI-SUB-09, PKG-MULTI-SUB-10, PKG-MULTI-SUB-11 | | artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06, PKG-VAST-07 | | 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-AMBILIGHT-PROXY-01, PKG-CANVAS-01, PKG-CANVAS-02, PKG-CANVAS-03, PKG-CANVAS-04, PKG-CANVAS-05, PKG-CANVAS-06, PKG-FACTORY-01 | +| 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, PKG-FACTORY-01, PKG-CANVAS-SUBTITLE-01 | | 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 | @@ -347,8 +347,9 @@ | PKG-CANVAS-02 | artplayer-proxy-canvas
建立特有行为与错误测试 | PKG-CANVAS-01, ENG-03, ENG-05 | ready/loadeddata/canplay、play/seek、回调、resize 与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | | PKG-CANVAS-03 | artplayer-proxy-canvas
整理内部职责与资源 | PKG-CANVAS-02, CORE-06, CORE-11, CORE-16 | video adapter/原 canvas 方法/RAF 绘制/事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | | PKG-CANVAS-04 | artplayer-proxy-canvas
迁移自有源码和公开类型 | PKG-CANVAS-03, ENG-04, ENG-06, CORE-07 | canvas 与媒体能力的精确组合类型,保持 Result | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done | -| PKG-CANVAS-05 | artplayer-proxy-canvas
验证新旧核心和组合 | PKG-CANVAS-04, CORE-22, PKG-DPIP-04, PKG-FACTORY-01 | 真实 video/canvas 绘制、字幕和 document PiP 恢复 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | +| PKG-CANVAS-05 | artplayer-proxy-canvas
验证新旧核心和组合 | PKG-CANVAS-04, CORE-22, PKG-DPIP-04, PKG-FACTORY-01, PKG-CANVAS-SUBTITLE-01 | 真实 video/canvas 绘制、字幕和 document PiP 恢复 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | | PKG-CANVAS-06 | artplayer-proxy-canvas
验证分发并同步文档 | PKG-CANVAS-05, ENG-07 | canvas.js、三种产物、调用兼容与资源证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | +| PKG-CANVAS-SUBTITLE-01 | artplayer-proxy-canvas
修复 Canvas 代理原生字幕轨道挂载 | PKG-CANVAS-04 | 初始字幕能力、原生轨道挂载与资源生命周期适配及新旧核心回归 | 复现旧字幕 cues 为空;新旧核心加载、寻址、切换、销毁通过;普通 Canvas 节点行为和分发入口保持兼容 | M | done | ## 5 包迁移:artplayer-proxy-mediabunny @@ -670,3 +671,4 @@ - SITE-VCONSOLE-01: [记录](changes/2026-09-14-SITE-VCONSOLE-01-lifecycle.md) [记录](baselines/vconsole-lifecycle-validation.json) - CI-BROWSER-01: [记录](changes/2026-09-14-CI-BROWSER-01-scopes.md) [记录](baselines/browser-scope-validation.json) - PKG-CHAPTER-HOVER-01: [记录](changes/2026-09-14-PKG-CHAPTER-HOVER-01-geometry.md) [记录](baselines/chapter-hover-validation.json) +- PKG-CANVAS-SUBTITLE-01: [记录](changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md) [记录](baselines/canvas-subtitles-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index 00f858166..61167c732 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,20 @@ # 进度与证据 +## PKG-CANVAS-SUBTITLE-01 原生字幕轨道修复完成 + +新旧核心三引擎复现 Canvas 字幕为空。先前说明需修正:初始失败中的 $track +仍在旧模板的 detached video;底层 video 没有初始轨道,核心能力检查直接返回。 +仅路由 appendChild 的尝试仍六项失败;增加无 URL metadata 占位轨道并在首次 +真实轨道插入时替换后,原生字幕正常加载。模块、普通 Canvas 路径、失败回滚、 +销毁边界与包内文档一起更新。实际 npm 1.1.0 对照六项失败,保留旧红新绿。 +源码扩展24通过,安装main/legacy各33通过,最终定向6通过;Node58、安装类型17、 +风险/runner6、严格TS/lint/工具链通过。示例更新且内置浏览器实际显示双语字幕。 +见[变更](changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md)与 +[证据](baselines/canvas-subtitles-validation.json)。259项:190 done、19 doing、50 todo。 +Canvas-05/06及Document PiP/真机未关闭;Auto Thumbnail原生首帧仍待另一后端或 +准确解码路径证据。本机WSL无已安装发行版,未安装系统环境或更改原像素门槛。 +没有push/部署/发布;本任务独立提交后必须通过提交审计再开始下一任务。 + ## PKG-CHAPTER-HOVER-01 全屏悬停测试同步完成 原始 installed 224/1 失败保持归档。受控原生进度条移动实验确认立即读取坐标 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 5311959b9..de4824069 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -272,3 +272,4 @@ | ENG-PERF-OBS-01 | resolved / 已复现 | A nominal 350 ms resource observation can record only 349 ms and fail the required minimum | ENG-13 | | VCONSOLE-LIFECYCLE-01 | resolved / 已复现 | vConsole delayed log, panel and scroller work outlives its owner | SITE-07, SITE-05, SITE-VCONSOLE-01 | | CHAPTER-FULLSCREEN-GEOMETRY-01 | resolved / 已复现 | Chromium 原生全屏后章节 hover 标题曾出现后又隐藏 | PKG-CHAPTER-05, PKG-CHAPTER-HOVER-01 | +| CANVAS-SUBTITLE-01 | resolved / 已复现 | Canvas底层video缺初始字幕轨道且普通Canvas挂载不能加载原生字幕 | PKG-CANVAS-SUBTITLE-01, PKG-CANVAS-05 | diff --git a/refactor/risks.json b/refactor/risks.json index 205d52cec..2a7e61f70 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -6218,6 +6218,29 @@ "test/browser/chapter-hover.spec.js" ], "resolutionRationale": "The immediate bounding-box read permits stale pointer coordinates while progress transforms move. Initial hit-test control gives 4 failures/2 passes before and 6 passes after waiting for locator stability. WebKit hit tests disagree with the final rectangle, so additional explicit bounds assertions were added: baseline 6 failures, stable main/legacy 6 passes each. Complete main/legacy chapter combination suites retain title, opacity, thumbnails and fullscreen assertions (66 passes each). This closes the identified test synchronization race, not all possible production fullscreen defects or CHAPTER-TIMING-01. Original 224/1 failure remains archived; its exact scheduler cannot be replayed." + }, + { + "id": "CANVAS-SUBTITLE-01", + "title": "Canvas底层video缺初始字幕轨道且普通Canvas挂载不能加载原生字幕", + "confirmation": "reproduced", + "status": "resolved", + "owners": [ + "PKG-CANVAS-SUBTITLE-01", + "PKG-CANVAS-05" + ], + "evidence": [ + "refactor/changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md", + "test/browser/canvas-subtitles.spec.js" + ], + "compatibleResolution": "适配原生track能力和插入,同时保留普通Canvas操作;不得要求旧核心消费者更改调用。", + "closureCriteria": "新旧核心三引擎字幕加载/寻址/切换/清理、普通节点与分发回归,有旧红新绿证据。", + "workspaceState": "Native subtitle bootstrap and track insertion are isolated in subtitles.ts. Old/new core source and installed main/legacy pass real subtitle load/seek/replacement/cleanup and normal Canvas-node checks. Public declaration shape is unchanged.", + "resolutionEvidence": [ + "refactor/changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md", + "refactor/baselines/canvas-subtitles-validation.json", + "test/browser/canvas-subtitles.spec.js" + ], + "resolutionRationale": "Initial candidate and routing-only controls each fail all six subtitle cases. Bootstrap + insertion adaptation passes; npm 1.1.0 independently fails all six. Expanded native source tests pass 24; installed main and legacy pass 33 each; final six pass after adding baseline selection. Captions also visibly render in the local demo. This fixes subtitle capability/insertion only; broader Canvas integration remains open." } ] } diff --git a/refactor/tasks.json b/refactor/tasks.json index b667357c4..0f66e9744 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -3635,7 +3635,8 @@ "PKG-CANVAS-04", "CORE-22", "PKG-DPIP-04", - "PKG-FACTORY-01" + "PKG-FACTORY-01", + "PKG-CANVAS-SUBTITLE-01" ], "status": "todo", "risk": "H", @@ -5389,6 +5390,25 @@ "changes/2026-09-14-PKG-CHAPTER-HOVER-01-geometry.md", "baselines/chapter-hover-validation.json" ] + }, + { + "id": "PKG-CANVAS-SUBTITLE-01", + "phase": "5 包迁移:artplayer-proxy-canvas", + "title": "修复 Canvas 代理原生字幕轨道挂载", + "scope": [ + "artplayer-proxy-canvas" + ], + "dependsOn": [ + "PKG-CANVAS-04" + ], + "status": "done", + "risk": "M", + "deliverable": "初始字幕能力、原生轨道挂载与资源生命周期适配及新旧核心回归", + "acceptance": "复现旧字幕 cues 为空;新旧核心加载、寻址、切换、销毁通过;普通 Canvas 节点行为和分发入口保持兼容", + "evidence": [ + "changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md", + "baselines/canvas-subtitles-validation.json" + ] } ] } diff --git a/scripts/browser-validation/scope.ts b/scripts/browser-validation/scope.ts index d7958d874..ca66ce1b9 100644 --- a/scripts/browser-validation/scope.ts +++ b/scripts/browser-validation/scope.ts @@ -13,6 +13,7 @@ export const installedTests = [ 'ambilight-lifecycle.spec.js', 'ambilight-proxy.spec.js', 'canvas-lifecycle.spec.js', + 'canvas-subtitles.spec.js', ] export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) { diff --git a/test/browser/README.md b/test/browser/README.md index de332a768..1280dd40f 100644 --- a/test/browser/README.md +++ b/test/browser/README.md @@ -1,5 +1,11 @@ # Browser regression entry +`canvas-subtitles.spec.js` covers native track bootstrap/load/seek/replacement and +cleanup on old/new cores, including ordinary Canvas child operations. It is part +of both browser scopes. `ARTPLAYER_CANVAS_SUBTITLE_BASELINE=1.1.0` runs the exact +published proxy as a failing historical control; it cannot be combined with an +installed candidate map. This is a real media/track test, not a simulated cue list. + Chapter pointer geometry is shared by `chapter-hover.ts` and the combination suite. It awaits locator actionability with `hover({ trial: true })` before reading coordinates; `art.fullscreen` can become true while controls still move. diff --git a/test/browser/canvas-subtitles.spec.js b/test/browser/canvas-subtitles.spec.js new file mode 100644 index 000000000..67ed0f073 --- /dev/null +++ b/test/browser/canvas-subtitles.spec.js @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict' +import process from 'node:process' +import { hash } from '../../refactor/scripts/releases.mjs' +import { canvasCandidate, canvasHistorical } from '../helpers/canvas.js' +import { expect, test } from './fixtures.js' + +const baseline = process.env.ARTPLAYER_CANVAS_SUBTITLE_BASELINE +assert(!baseline || baseline === '1.1.0', 'Only the frozen published 1.1.0 subtitle baseline is supported') +assert(!baseline || !process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Historical subtitles cannot claim installed candidate provenance') +const candidate = baseline ? (await canvasHistorical()).find(item => item.name === 'published-1.1.0') : await canvasCandidate() +assert(candidate) + +for (const core of ['published', 'candidate']) { + test(`${core}: canvas proxy loads and seeks native subtitle cues`, async ({ page }, testInfo) => { + await page.route('**/canvas-captions.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nOpening caption\n\n00:00:02.000 --> 00:00:07.000\nLater caption\n' })) + await page.route('**/canvas-replacement.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:00:00.000 --> 00:00:07.000\nReplacement caption\n' })) + await page.goto(`/test/player.html?core=${core}`) + await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${candidate.source}; window.canvasFactory = module.exports.default || module.exports; })();` }) + await page.evaluate(() => { + window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, proxy: window.canvasFactory(), subtitle: { url: '/canvas-captions.vtt' } }) + document.querySelector('#play').onclick = () => window.art.play() + document.querySelector('#pause').onclick = () => window.art.pause() + }) + let completed = false + try { + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) + await page.click('#play') + await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1) + await page.click('#pause') + await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2) + await expect(page.locator('.art-subtitle')).toHaveText('Opening caption') + await page.evaluate(() => window.art.currentTime = 3) + await expect(page.locator('.art-subtitle')).toHaveText('Later caption') + await page.evaluate(async () => { + window.previousCanvasTrack = window.art.template.$track + await window.art.subtitle.switch('/canvas-replacement.vtt') + }) + await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1) + await page.evaluate(() => window.art.currentTime = 4) + await expect(page.locator('.art-subtitle')).toHaveText('Replacement caption') + expect(await page.evaluate(() => ({ previousConnected: window.previousCanvasTrack.isConnected, count: window.art.video.textTracks.length, parent: window.art.template.$track.parentNode === window.art.template.$player.querySelector('video') }))).toEqual({ previousConnected: false, count: 1, parent: true }) + expect(await page.evaluate(() => { + const canvas = window.art.video + const marker = document.createElement('span') + const append = canvas.appendChild + const returned = append(marker) + const same = returned === marker && marker.parentNode === canvas + canvas.removeChild(marker) + window.canvasAppend = append + window.backingCanvasVideo = window.art.template.$player.querySelector('video') + return same && !marker.parentNode && typeof canvas.toDataURL() === 'string' + })).toBe(true) + completed = true + } + finally { + await testInfo.attach('canvas-subtitle-state', { contentType: 'application/json', body: JSON.stringify({ core, implementation: candidate.name, sha256: hash(candidate.source), ...await page.evaluate(() => ({ renderer: window.art.video.nodeName, trackParent: window.art.template.$track.parentNode?.nodeName, trackState: window.art.template.$track.readyState, cues: window.art.subtitle.cues.length, backingTracks: window.art.template.$player.querySelector('video')?.textTracks.length, text: window.art.template.$subtitle.textContent, time: window.art.currentTime })) }) }) + await page.evaluate(() => window.art.destroy()) + if (completed) { + expect(await page.evaluate(() => { + const track = document.createElement('track') + window.canvasAppend?.(track) + const video = window.backingCanvasVideo + return video ? { connected: video.isConnected, paused: video.paused, source: video.getAttribute('src'), restarted: track.parentNode === video } : null + })).toEqual({ connected: false, paused: true, source: null, restarted: false }) + } + } + }) +} diff --git a/test/canvas-lifecycle.test.js b/test/canvas-lifecycle.test.js index fdfa8fcab..c334d03c1 100644 --- a/test/canvas-lifecycle.test.js +++ b/test/canvas-lifecycle.test.js @@ -5,6 +5,22 @@ import { canvasCandidate, canvasEnvironment, canvasHistorical } from './helpers/ const implementation = await canvasCandidate() const listenerCount = env => [...env.listeners.values()].reduce((count, set) => count + set.size, 0) + +test('Canvas rolls back private media when initial subtitle track insertion fails', async () => { + const env = await canvasEnvironment(implementation) + const failure = new Error('Initial track insertion failed') + env.video.appendChild = () => { + throw failure + } + assert.throws(() => env.factory()(env.art), error => error === failure) + assert.equal(listenerCount(env), 0) + assert.equal(env.timers.size, 0) + assert.equal(env.frames.size, 0) + assert.equal(env.video.parentNode, undefined) + assert.equal(env.video.src, undefined) + assert.equal(env.video.paused, true) + assert.deepEqual([env.canvas.width, env.canvas.height], [0, 0]) +}) function deferred() { let resolve, reject const promise = new Promise((yes, no) => { diff --git a/test/helpers/canvas.js b/test/helpers/canvas.js index 6e395b70d..2bc370f4b 100644 --- a/test/helpers/canvas.js +++ b/test/helpers/canvas.js @@ -75,6 +75,24 @@ export async function canvasEnvironment(implementation, settings = {}) { paused: true, muted: false, style: {}, + children: [], + textTracks: [], + appendChild(node) { + node.parentNode?.removeChild(node) + this.children.push(node) + node.parentNode = this + if (node.nodeName === 'TRACK') + this.textTracks.push(node.track) + return node + }, + removeChild(node) { + this.children.splice(this.children.indexOf(node), 1) + const index = this.textTracks.indexOf(node.track) + if (index >= 0) + this.textTracks.splice(index, 1) + node.parentNode = null + return node + }, play(...args) { calls.push({ name: 'play', receiver: this, args }) this.paused = false @@ -111,7 +129,11 @@ export async function canvasEnvironment(implementation, settings = {}) { }, } const art = { - constructor: { config: { events: ['loadedmetadata', 'play', 'pause', 'seeked', 'error'] }, utils: { createElement: tag => tag === 'canvas' ? canvas : video } }, + constructor: { config: { events: ['loadedmetadata', 'play', 'pause', 'seeked', 'error'] }, utils: { createElement: (tag) => { + if (tag === 'track') + return { nodeName: 'TRACK', nodeType: 1, namespaceURI: 'http://www.w3.org/1999/xhtml', track: { mode: 'disabled', cues: [], activeCues: [] }, remove() { this.parentNode?.removeChild(this) } } + return tag === 'canvas' ? canvas : video + } } }, option: { autoSize: false }, template: { $player: player }, isDestroy: false,