diff --git a/docs/compiled/artplayer-plugin-vtt-thumbnail.js b/docs/compiled/artplayer-plugin-vtt-thumbnail.js index 3627b491f..502865ead 100644 --- a/docs/compiled/artplayer-plugin-vtt-thumbnail.js +++ b/docs/compiled/artplayer-plugin-vtt-thumbnail.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";function t(t){const e=t.split("."),n=e[0].split(":")||[],i=(l=e[1]||"0",o=3,s="0",l.length>o?String(l):((o-=l.length)>s.length&&(s+=s.repeat(o/s.length)),String(l)+s.slice(0,o)));var l,o,s;const r=Number(i)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+r}return function(e){return async n=>{const{constructor:{utils:{setStyle:i,isMobile:l,addClass:o}},template:{$progress:s}}=n;let r=null;const a=await async function(e=""){const n=(await(await fetch(e)).text()).split(/[\n\r]/g).filter(t=>t.trim()),i=[];for(let l=1;l ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,a=o.match(r),u=/(.*)#(\w{4})=(.*)/,c=s.match(u),d=Math.floor(t(a[1])),p=Math.floor(t(a[2]));let f=c[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(f)){const t=e.split("/");t.pop(),t.push(f),f=t.join("/")}const h={start:d,end:p,url:f},m=c[2].split(""),g=c[3].split(",");for(let t=0;t{const c="played"===e&&u&&l;if("hover"===e||c){const e=s.clientWidth*o,u=o*n.duration;i(t,"display","flex");const d=a.find(t=>u>=t.start&&u<=t.end);if(!d)return i(t,"display","none");e>0&&es.clientWidth-e.w/2?i(t,"left",s.clientWidth-e.w+"px"):i(t,"left",n-e.w/2+"px")}(t,d,e):l||i(t,"display","none"),c&&(clearTimeout(r),r=setTimeout(()=>{i(t,"display","none")},500))}})}}),{name:"artplayerPluginVttThumbnail"}}}}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";function t(t){const e=t.split("."),n=e[0].split(":")||[],o=(l=e[1]||"0",i=3,r="0",l.length>i?String(l):((i-=l.length)>r.length&&(r+=r.repeat(i/r.length)),String(l)+r.slice(0,i)));var l,i,r;const s=Number(o)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+s}function e({lifetime:t,thumbnails:e,progress:n,duration:o,setStyle:l,isMobile:i}){let r=null,s=0;function c(e,n,o){t.closed||l(e,n,o)}return t.own(()=>{if(s++,null!==r){const t=r;r=null,clearTimeout(t)}}),l=>async(u,a,d)=>{if(t.closed)return;const f="played"===u&&d&&i;if("hover"!==u&&!f)return;const h=n.clientWidth*a,p=a*o();if(c(l,"display","flex"),t.closed)return;const m=e.find(t=>p>=t.start&&p<=t.end);if(!m)return c(l,"display","none");if(h>0&&hn.clientWidth-e.w/2?c(t,"left",n.clientWidth-e.w+"px"):c(t,"left",o-e.w/2+"px")}(l,m,h):i||c(l,"display","none"),f&&!t.closed){const e=++s;if(null!==r&&clearTimeout(r),t.closed)return;const n=setTimeout(()=>{e!==s||t.closed||(r=null,c(l,"display","none"))},500);t.closed||e!==s?clearTimeout(n):r=n}}}return function(n){return async o=>{const{constructor:{utils:{setStyle:l,isMobile:i,addClass:r}},template:{$progress:s}}=o,c=function(t){let e=Boolean(t.isDestroy);const n=new Set;let o;const l=new Promise(t=>{o=t}),i={get closed(){return e},own:t=>(e?r(t):n.add(t),()=>n.delete(t)),listen(n,o){e||(i.own(()=>t.off(n,o)),t.on(n,o))},wait:t=>Promise.race([t,l]),dispose(){if(e)return;e=!0,o();const t=[...n].reverse();n.clear();for(const e of t)r(e)}};function r(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{i.listen("destroy",i.dispose)}catch(s){throw i.dispose(),s}return i}(o),u={name:"artplayerPluginVttThumbnail"};try{if(c.closed)return u;const a=n.vtt,d=await async function(t="",e){if(e.closed)return;const n="function"===typeof AbortController?new AbortController:null,o=e.own(()=>n?.abort());try{if(e.closed)return;const o=await e.wait(n?fetch(t,{signal:n.signal}):fetch(t));if(e.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch VTT thumbnails: HTTP ${o.status}`);if(e.closed)return;return await e.wait(o.text())}finally{o()}}(a,c);if(c.closed)return u;const f=function(e,n=""){const o=e.split(/[\n\r]/g).filter(t=>t.trim()),l=[];for(let i=1;i ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,c=e.match(s),u=/(.*)#(\w{4})=(.*)/,a=r.match(u),d=Math.floor(t(c[1])),f=Math.floor(t(c[2]));let h=a[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(h)){const t=n.split("/");t.pop(),t.push(h),h=t.join("/")}const p={start:d,end:f,url:h},m=a[2].split(""),y=a[3].split(",");for(let t=0;to.duration,setStyle:l,isMobile:i}),p=n.style||{};return c.closed||o.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:p,mounted(t){c.own(()=>{o.controls["vtt-thumbnail"]===t&&o.controls.remove("vtt-thumbnail")}),c.closed||(r(t,"art-control-thumbnails"),c.listen("setBar",h(t)))}}),u}catch(a){throw c.dispose(),a}}}}); diff --git a/docs/compiled/artplayer-plugin-vtt-thumbnail.legacy.js b/docs/compiled/artplayer-plugin-vtt-thumbnail.legacy.js index dead67bbf..53193cab0 100644 --- a/docs/compiled/artplayer-plugin-vtt-thumbnail.legacy.js +++ b/docs/compiled/artplayer-plugin-vtt-thumbnail.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";var t=(t,e,n)=>new Promise((l,i)=>{var o=t=>{try{s(n.next(t))}catch(e){i(e)}},r=t=>{try{s(n.throw(t))}catch(e){i(e)}},s=t=>t.done?l(t.value):Promise.resolve(t.value).then(o,r);s((n=n.apply(t,e)).next())});function e(t){const e=t.split("."),n=e[0].split(":")||[],l=(i=e[1]||"0",o=3,r="0",i.length>o?String(i):((o-=i.length)>r.length&&(r+=r.repeat(o/r.length)),String(i)+r.slice(0,o)));var i,o,r;const s=Number(l)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+s}return function(n){return l=>t(null,null,function*(){const{constructor:{utils:{setStyle:i,isMobile:o,addClass:r}},template:{$progress:s}}=l;let u=null;const a=yield function(n=""){return t(this,null,function*(){const t=(yield(yield fetch(n)).text()).split(/[\n\r]/g).filter(t=>t.trim()),l=[];for(let i=1;i ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,u=o.match(s),a=/(.*)#(\w{4})=(.*)/,c=r.match(a),d=Math.floor(e(u[1])),h=Math.floor(e(u[2]));let p=c[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(p)){const t=n.split("/");t.pop(),t.push(p),p=t.join("/")}const f={start:d,end:h,url:p},m=c[2].split(""),g=c[3].split(",");for(let t=0;tt(null,null,function*(){const t="played"===n&&c&&o;if("hover"===n||t){const n=s.clientWidth*r,c=r*l.duration;i(e,"display","flex");const d=a.find(t=>c>=t.start&&c<=t.end);if(!d)return i(e,"display","none");n>0&&ns.clientWidth-e.w/2?i(t,"left",s.clientWidth-e.w+"px"):i(t,"left",n-e.w/2+"px")}(e,d,n):o||i(e,"display","none"),t&&(clearTimeout(u),u=setTimeout(()=>{i(e,"display","none")},500))}}))}}),{name:"artplayerPluginVttThumbnail"}})}}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";var t=(t,e,n)=>new Promise((o,l)=>{var i=t=>{try{s(n.next(t))}catch(e){l(e)}},r=t=>{try{s(n.throw(t))}catch(e){l(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(i,r);s((n=n.apply(t,e)).next())});function e(t){const e=t.split("."),n=e[0].split(":")||[],o=(l=e[1]||"0",i=3,r="0",l.length>i?String(l):((i-=l.length)>r.length&&(r+=r.repeat(i/r.length)),String(l)+r.slice(0,i)));var l,i,r;const s=Number(o)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+s}function n({lifetime:e,thumbnails:n,progress:o,duration:l,setStyle:i,isMobile:r}){let s=null,u=0;function c(t,n,o){e.closed||i(t,n,o)}return e.own(()=>{if(u++,null!==s){const t=s;s=null,clearTimeout(t)}}),i=>(a,d,f)=>t(null,null,function*(){if(e.closed)return;const t="played"===a&&f&&r;if("hover"!==a&&!t)return;const h=o.clientWidth*d,p=d*l();if(c(i,"display","flex"),e.closed)return;const m=n.find(t=>p>=t.start&&p<=t.end);if(!m)return c(i,"display","none");if(h>0&&ho.clientWidth-e.w/2?c(t,"left",o.clientWidth-e.w+"px"):c(t,"left",n-e.w/2+"px")}(i,m,h):r||c(i,"display","none"),t&&!e.closed){const t=++u;if(null!==s&&clearTimeout(s),e.closed)return;const n=setTimeout(()=>{t!==u||e.closed||(s=null,c(i,"display","none"))},500);e.closed||t!==u?clearTimeout(n):s=n}})}return function(o){return l=>t(null,null,function*(){const{constructor:{utils:{setStyle:i,isMobile:r,addClass:s}},template:{$progress:u}}=l,c=function(t){let e=Boolean(t.isDestroy);const n=new Set;let o;const l=new Promise(t=>{o=t}),i={get closed(){return e},own:t=>(e?r(t):n.add(t),()=>n.delete(t)),listen(n,o){e||(i.own(()=>t.off(n,o)),t.on(n,o))},wait:t=>Promise.race([t,l]),dispose(){if(e)return;e=!0,o();const t=[...n].reverse();n.clear();for(const e of t)r(e)}};function r(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{i.listen("destroy",i.dispose)}catch(s){throw i.dispose(),s}return i}(l),a={name:"artplayerPluginVttThumbnail"};try{if(c.closed)return a;const d=o.vtt,f=yield function(e="",n){return t(this,null,function*(){if(n.closed)return;const t="function"===typeof AbortController?new AbortController:null,o=n.own(()=>null==t?void 0:t.abort());try{if(n.closed)return;const o=yield n.wait(t?fetch(e,{signal:t.signal}):fetch(e));if(n.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch VTT thumbnails: HTTP ${o.status}`);if(n.closed)return;return yield n.wait(o.text())}finally{o()}})}(d,c);if(c.closed)return a;const h=function(t,n=""){const o=t.split(/[\n\r]/g).filter(t=>t.trim()),l=[];for(let i=1;i ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,u=t.match(s),c=/(.*)#(\w{4})=(.*)/,a=r.match(c),d=Math.floor(e(u[1])),f=Math.floor(e(u[2]));let h=a[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(h)){const t=n.split("/");t.pop(),t.push(h),h=t.join("/")}const p={start:d,end:f,url:h},m=a[2].split(""),y=a[3].split(",");for(let e=0;el.duration,setStyle:i,isMobile:r}),m=o.style||{};return c.closed||l.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:m,mounted(t){c.own(()=>{l.controls["vtt-thumbnail"]===t&&l.controls.remove("vtt-thumbnail")}),c.closed||(s(t,"art-control-thumbnails"),c.listen("setBar",p(t)))}}),a}catch(d){throw c.dispose(),d}})}}); diff --git a/docs/compiled/artplayer-plugin-vtt-thumbnail.mjs b/docs/compiled/artplayer-plugin-vtt-thumbnail.mjs index ecffe74e3..27d351f66 100644 --- a/docs/compiled/artplayer-plugin-vtt-thumbnail.mjs +++ b/docs/compiled/artplayer-plugin-vtt-thumbnail.mjs @@ -4,6 +4,59 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ +function createLifetime(art) { + let closed = Boolean(art.isDestroy); + const cleanups = /* @__PURE__ */ new Set(); + let cancel; + const cancelled = new Promise((resolve) => { + cancel = resolve; + }); + const lifetime = { + get closed() { + return closed; + }, + own(cleanup) { + if (closed) + run(cleanup); + else + cleanups.add(cleanup); + return () => cleanups.delete(cleanup); + }, + listen(name, callback) { + if (closed) + return; + lifetime.own(() => art.off(name, callback)); + art.on(name, callback); + }, + wait(value) { + return Promise.race([value, cancelled]); + }, + dispose() { + if (closed) + return; + closed = true; + cancel(); + const pending = [...cleanups].reverse(); + cleanups.clear(); + for (const cleanup of pending) + run(cleanup); + } + }; + function run(cleanup) { + try { + cleanup(); + } catch (error) { + console.warn("Failed to clean up VTT thumbnails:", error); + } + } + try { + lifetime.listen("destroy", lifetime.dispose); + } catch (error) { + lifetime.dispose(); + throw error; + } + return lifetime; +} function padEnd(str, targetLength, padString) { if (str.length > targetLength) { return String(str); @@ -25,8 +78,7 @@ function t2d(time) { const s = Number(left[left.length - 1] || 0); return h + m + s + ms; } -async function getVttArray(vttUrl = "") { - const vttString = await (await fetch(vttUrl)).text(); +function parseVtt(vttString, vttUrl = "") { const lines = vttString.split(/[\n\r]/g).filter((item) => item.trim()); const vttArray = []; for (let i = 1; i < lines.length; i += 2) { @@ -58,65 +110,128 @@ async function getVttArray(vttUrl = "") { } return vttArray; } +function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isMobile }) { + let timer = null; + let generation = 0; + lifetime.own(() => { + generation++; + if (timer !== null) { + const previous = timer; + timer = null; + clearTimeout(previous); + } + }); + function style(control, key, value) { + if (!lifetime.closed) + setStyle(control, key, value); + } + function show(control, cue, width) { + style(control, "backgroundImage", `url(${cue.url})`); + style(control, "height", `${cue.h}px`); + style(control, "width", `${cue.w}px`); + style(control, "backgroundPosition", `-${cue.x}px -${cue.y}px`); + if (width <= cue.w / 2) + style(control, "left", 0); + else if (width > progress.clientWidth - cue.w / 2) + style(control, "left", `${progress.clientWidth - cue.w}px`); + else + style(control, "left", `${width - cue.w / 2}px`); + } + return (control) => async (type, percentage, event) => { + if (lifetime.closed) + return; + const dragging = type === "played" && event && isMobile; + if (type !== "hover" && !dragging) + return; + const width = progress.clientWidth * percentage; + const second = percentage * duration(); + style(control, "display", "flex"); + if (lifetime.closed) + return; + const cue = thumbnails.find((item) => second >= item.start && second <= item.end); + if (!cue) + return style(control, "display", "none"); + if (width > 0 && width < progress.clientWidth) + show(control, cue, width); + else if (!isMobile) + style(control, "display", "none"); + if (dragging && !lifetime.closed) { + const current = ++generation; + if (timer !== null) + clearTimeout(timer); + if (lifetime.closed) + return; + const id = setTimeout(() => { + if (current !== generation || lifetime.closed) + return; + timer = null; + style(control, "display", "none"); + }, 500); + if (lifetime.closed || current !== generation) + clearTimeout(id); + else + timer = id; + } + }; +} +async function requestVtt(url = "", lifetime) { + if (lifetime.closed) + return; + const controller = typeof AbortController === "function" ? new AbortController() : null; + const release = lifetime.own(() => controller?.abort()); + try { + if (lifetime.closed) + return; + const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url)); + if (lifetime.closed) + return; + if (response.ok === false) + throw new Error(`Failed to fetch VTT thumbnails: HTTP ${response.status}`); + if (lifetime.closed) + return; + return await lifetime.wait(response.text()); + } finally { + release(); + } +} function artplayerPluginVttThumbnail(option) { return async (art) => { - const { - constructor: { - utils: { setStyle, isMobile, addClass } - }, - template: { $progress } - } = art; - let timer = null; - const thumbnails = await getVttArray(option.vtt); - function showThumbnails($control, find, width) { - setStyle($control, "backgroundImage", `url(${find.url})`); - setStyle($control, "height", `${find.h}px`); - setStyle($control, "width", `${find.w}px`); - setStyle($control, "backgroundPosition", `-${find.x}px -${find.y}px`); - if (width <= find.w / 2) { - setStyle($control, "left", 0); - } else if (width > $progress.clientWidth - find.w / 2) { - setStyle($control, "left", `${$progress.clientWidth - find.w}px`); - } else { - setStyle($control, "left", `${width - find.w / 2}px`); - } + const { constructor: { utils: { setStyle, isMobile, addClass } }, template: { $progress } } = art; + const lifetime = createLifetime(art); + const result = { name: "artplayerPluginVttThumbnail" }; + try { + if (lifetime.closed) + return result; + const url = option.vtt; + const text = await requestVtt(url, lifetime); + if (lifetime.closed) + return result; + const thumbnails = parseVtt(text, url); + const preview = createPreview({ lifetime, thumbnails, progress: $progress, duration: () => art.duration, setStyle, isMobile }); + const style = option.style || {}; + if (lifetime.closed) + return result; + art.controls.add({ + name: "vtt-thumbnail", + position: "top", + index: 20, + style, + mounted(control) { + lifetime.own(() => { + if (art.controls["vtt-thumbnail"] === control) + art.controls.remove("vtt-thumbnail"); + }); + if (lifetime.closed) + return; + addClass(control, "art-control-thumbnails"); + lifetime.listen("setBar", preview(control)); + } + }); + return result; + } catch (error) { + lifetime.dispose(); + throw error; } - art.controls.add({ - name: "vtt-thumbnail", - position: "top", - index: 20, - style: option.style || {}, - mounted($control) { - addClass($control, "art-control-thumbnails"); - art.on("setBar", async (type, percentage, event) => { - const isMobileDroging = type === "played" && event && isMobile; - if (type === "hover" || isMobileDroging) { - const width = $progress.clientWidth * percentage; - const second = percentage * art.duration; - setStyle($control, "display", "flex"); - const find = thumbnails.find((item) => second >= item.start && second <= item.end); - if (!find) - return setStyle($control, "display", "none"); - if (width > 0 && width < $progress.clientWidth) { - showThumbnails($control, find, width); - } else { - if (!isMobile) { - setStyle($control, "display", "none"); - } - } - if (isMobileDroging) { - clearTimeout(timer); - timer = setTimeout(() => { - setStyle($control, "display", "none"); - }, 500); - } - } - }); - } - }); - return { - name: "artplayerPluginVttThumbnail" - }; }; } export { diff --git a/package.json b/package.json index 5bda74426..320e8b372 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js", "typecheck": "node scripts/typecheck.mjs", - "test:unit": "node --test test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", + "test:unit": "node --test test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", "test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs", "test": "yarn test:node && yarn test:baseline", "test:browser": "playwright test", @@ -86,7 +86,7 @@ "test:thumbnail": "node --test test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs refactor/scripts/thumbnail-types.test.mjs", "test:thumbnail-types-package": "node refactor/scripts/thumbnail-package-types.mjs", "test:auto-thumbnail": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs", - "test:vtt-thumbnail": "node --test test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs" + "test:vtt-thumbnail": "node --test test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-plugin-vtt-thumbnail/ARCHITECTURE.md b/packages/artplayer-plugin-vtt-thumbnail/ARCHITECTURE.md new file mode 100644 index 000000000..dce8ac4cc --- /dev/null +++ b/packages/artplayer-plugin-vtt-thumbnail/ARCHITECTURE.md @@ -0,0 +1,69 @@ +# VTT thumbnail maintenance + +The factory remains `artplayerPluginVttThumbnail(option)`. Its registration function is +asynchronous and resolves to `{ name: 'artplayerPluginVttThumbnail' }`. Existing public +declarations still describe a synchronous result; task PKG-VTT-THUMB-04 owns that conflict +and the strict TypeScript migration. Do not infer the return shape from those declarations. + +## Modules and ownership + +- `src/index.js` composes registration, captures the VTT URL before awaiting, reads style + afterwards, and installs the `vtt-thumbnail` control with its existing CSS class/index. +- `src/lifetime.js` owns one registration's destroy listener, event listeners and cleanup + callbacks. Close is idempotent. Cleanup continues after a callback fails. An independent + cancellation Promise settles pending work even if fetch does not honor AbortSignal. +- `src/request.js` owns a native AbortController when available and reads the response body. + Destroy cancels both phases; late rejections are observed. Failed HTTP responses reject + before parsing. The controller cleanup is released when the request ends. +- `src/parseVtt.js` is pure parsing with the historical timestamp floor, first inclusive + match data, raw xywh strings and lexical URL joining. Its old malformed-input limitations + remain under PKG-VTT-THUMB-03; extracting this function is not a parser rewrite. +- `src/preview.js` owns the mobile 500ms timer and creates the setBar callback. Timer + generations reject old callbacks. Every style write checks lifetime state, including + callbacks captured before destroy. Geometry, mobile/desktop edges and cue selection + retain the latest published behavior. +- `src/getVttArray.js` retains the old source helper signature as a fetch/parse wrapper. + The player registration uses the owned request module instead. Distribution/deep-import + review belongs to PKG-VTT-THUMB-06. + +The registration owns only the control element passed to its mounted callback. Before +removing it, cleanup compares the live `art.controls['vtt-thumbnail']` alias with that exact +element. It must not remove a later replacement. Partial listener/control installation is +rolled back if initialization throws, preserving the original rejection. + +## Compatibility and intentional fixes + +Option/style remain live at their historical read points. The return stays a Promise; +destroy during loading now settles it with the normal name result, without installing UI. +Existing core plugin managers can complete their own destroyed-instance handling. This +does not introduce a public destroy method or a new registration result field. + +Registration still downloads VTT once. A video restart does not reload it or reinterpret +later option.vtt changes. Empty cues remain valid and hide the preview. HTTP failures now +reject explicitly; before this fix, a 404 with a parseable body incorrectly displayed cues. +The historical failure tests retain the former outcome rather than claiming it was valid API. + +The old 1.0.x export objects, earlier `thumbnails` control name and incorrect declarations +are separately frozen and remain compatibility work for tasks 04-06. Current runtime work +does not claim those consumer migrations or the full package refactor are finished. + +## Verification and future changes + +Use Yarn 1.22.22 and the repository's pinned Node version: + +```sh +yarn test:vtt-thumbnail +yarn test:browser test/browser/vtt-thumbnail-lifecycle.spec.js +yarn build artplayer-plugin-vtt-thumbnail +``` + +The unit helper can load actual main/legacy files with ARTPLAYER_VTT_THUMBNAIL_ARTIFACT or +the frozen former implementation with ARTPLAYER_VTT_THUMBNAIL_BASELINE=1. Historical tests +are distinct from candidate expectations. Browser tests use real published/candidate cores, +native fetch/abort, mouse hover, sprite decoding and cleanup. Their scope is not full old +core/device coverage, installed tarballs, mobile Safari or the online editor. + +Modify parsing in parseVtt, requests in request, layout/timers in preview, and ownership in +lifetime. Preserve the normal/failure lifecycle tests alongside any parser improvements. +See ../../refactor/baselines/vtt-thumbnail-contract.md and the task 03 change record for +historical evidence and outstanding acceptance work. diff --git a/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js b/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js index 3627b491f..502865ead 100644 --- a/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js +++ b/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";function t(t){const e=t.split("."),n=e[0].split(":")||[],i=(l=e[1]||"0",o=3,s="0",l.length>o?String(l):((o-=l.length)>s.length&&(s+=s.repeat(o/s.length)),String(l)+s.slice(0,o)));var l,o,s;const r=Number(i)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+r}return function(e){return async n=>{const{constructor:{utils:{setStyle:i,isMobile:l,addClass:o}},template:{$progress:s}}=n;let r=null;const a=await async function(e=""){const n=(await(await fetch(e)).text()).split(/[\n\r]/g).filter(t=>t.trim()),i=[];for(let l=1;l ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,a=o.match(r),u=/(.*)#(\w{4})=(.*)/,c=s.match(u),d=Math.floor(t(a[1])),p=Math.floor(t(a[2]));let f=c[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(f)){const t=e.split("/");t.pop(),t.push(f),f=t.join("/")}const h={start:d,end:p,url:f},m=c[2].split(""),g=c[3].split(",");for(let t=0;t{const c="played"===e&&u&&l;if("hover"===e||c){const e=s.clientWidth*o,u=o*n.duration;i(t,"display","flex");const d=a.find(t=>u>=t.start&&u<=t.end);if(!d)return i(t,"display","none");e>0&&es.clientWidth-e.w/2?i(t,"left",s.clientWidth-e.w+"px"):i(t,"left",n-e.w/2+"px")}(t,d,e):l||i(t,"display","none"),c&&(clearTimeout(r),r=setTimeout(()=>{i(t,"display","none")},500))}})}}),{name:"artplayerPluginVttThumbnail"}}}}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";function t(t){const e=t.split("."),n=e[0].split(":")||[],o=(l=e[1]||"0",i=3,r="0",l.length>i?String(l):((i-=l.length)>r.length&&(r+=r.repeat(i/r.length)),String(l)+r.slice(0,i)));var l,i,r;const s=Number(o)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+s}function e({lifetime:t,thumbnails:e,progress:n,duration:o,setStyle:l,isMobile:i}){let r=null,s=0;function c(e,n,o){t.closed||l(e,n,o)}return t.own(()=>{if(s++,null!==r){const t=r;r=null,clearTimeout(t)}}),l=>async(u,a,d)=>{if(t.closed)return;const f="played"===u&&d&&i;if("hover"!==u&&!f)return;const h=n.clientWidth*a,p=a*o();if(c(l,"display","flex"),t.closed)return;const m=e.find(t=>p>=t.start&&p<=t.end);if(!m)return c(l,"display","none");if(h>0&&hn.clientWidth-e.w/2?c(t,"left",n.clientWidth-e.w+"px"):c(t,"left",o-e.w/2+"px")}(l,m,h):i||c(l,"display","none"),f&&!t.closed){const e=++s;if(null!==r&&clearTimeout(r),t.closed)return;const n=setTimeout(()=>{e!==s||t.closed||(r=null,c(l,"display","none"))},500);t.closed||e!==s?clearTimeout(n):r=n}}}return function(n){return async o=>{const{constructor:{utils:{setStyle:l,isMobile:i,addClass:r}},template:{$progress:s}}=o,c=function(t){let e=Boolean(t.isDestroy);const n=new Set;let o;const l=new Promise(t=>{o=t}),i={get closed(){return e},own:t=>(e?r(t):n.add(t),()=>n.delete(t)),listen(n,o){e||(i.own(()=>t.off(n,o)),t.on(n,o))},wait:t=>Promise.race([t,l]),dispose(){if(e)return;e=!0,o();const t=[...n].reverse();n.clear();for(const e of t)r(e)}};function r(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{i.listen("destroy",i.dispose)}catch(s){throw i.dispose(),s}return i}(o),u={name:"artplayerPluginVttThumbnail"};try{if(c.closed)return u;const a=n.vtt,d=await async function(t="",e){if(e.closed)return;const n="function"===typeof AbortController?new AbortController:null,o=e.own(()=>n?.abort());try{if(e.closed)return;const o=await e.wait(n?fetch(t,{signal:n.signal}):fetch(t));if(e.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch VTT thumbnails: HTTP ${o.status}`);if(e.closed)return;return await e.wait(o.text())}finally{o()}}(a,c);if(c.closed)return u;const f=function(e,n=""){const o=e.split(/[\n\r]/g).filter(t=>t.trim()),l=[];for(let i=1;i ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,c=e.match(s),u=/(.*)#(\w{4})=(.*)/,a=r.match(u),d=Math.floor(t(c[1])),f=Math.floor(t(c[2]));let h=a[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(h)){const t=n.split("/");t.pop(),t.push(h),h=t.join("/")}const p={start:d,end:f,url:h},m=a[2].split(""),y=a[3].split(",");for(let t=0;to.duration,setStyle:l,isMobile:i}),p=n.style||{};return c.closed||o.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:p,mounted(t){c.own(()=>{o.controls["vtt-thumbnail"]===t&&o.controls.remove("vtt-thumbnail")}),c.closed||(r(t,"art-control-thumbnails"),c.listen("setBar",h(t)))}}),u}catch(a){throw c.dispose(),a}}}}); diff --git a/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js b/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js index dead67bbf..53193cab0 100644 --- a/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js +++ b/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";var t=(t,e,n)=>new Promise((l,i)=>{var o=t=>{try{s(n.next(t))}catch(e){i(e)}},r=t=>{try{s(n.throw(t))}catch(e){i(e)}},s=t=>t.done?l(t.value):Promise.resolve(t.value).then(o,r);s((n=n.apply(t,e)).next())});function e(t){const e=t.split("."),n=e[0].split(":")||[],l=(i=e[1]||"0",o=3,r="0",i.length>o?String(i):((o-=i.length)>r.length&&(r+=r.repeat(o/r.length)),String(i)+r.slice(0,o)));var i,o,r;const s=Number(l)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+s}return function(n){return l=>t(null,null,function*(){const{constructor:{utils:{setStyle:i,isMobile:o,addClass:r}},template:{$progress:s}}=l;let u=null;const a=yield function(n=""){return t(this,null,function*(){const t=(yield(yield fetch(n)).text()).split(/[\n\r]/g).filter(t=>t.trim()),l=[];for(let i=1;i ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,u=o.match(s),a=/(.*)#(\w{4})=(.*)/,c=r.match(a),d=Math.floor(e(u[1])),h=Math.floor(e(u[2]));let p=c[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(p)){const t=n.split("/");t.pop(),t.push(p),p=t.join("/")}const f={start:d,end:h,url:p},m=c[2].split(""),g=c[3].split(",");for(let t=0;tt(null,null,function*(){const t="played"===n&&c&&o;if("hover"===n||t){const n=s.clientWidth*r,c=r*l.duration;i(e,"display","flex");const d=a.find(t=>c>=t.start&&c<=t.end);if(!d)return i(e,"display","none");n>0&&ns.clientWidth-e.w/2?i(t,"left",s.clientWidth-e.w+"px"):i(t,"left",n-e.w/2+"px")}(e,d,n):o||i(e,"display","none"),t&&(clearTimeout(u),u=setTimeout(()=>{i(e,"display","none")},500))}}))}}),{name:"artplayerPluginVttThumbnail"}})}}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginVttThumbnail=e(),define(function(){return t.artplayerPluginVttThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginVttThumbnail=e()}(this,function(){"use strict";var t=(t,e,n)=>new Promise((o,l)=>{var i=t=>{try{s(n.next(t))}catch(e){l(e)}},r=t=>{try{s(n.throw(t))}catch(e){l(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(i,r);s((n=n.apply(t,e)).next())});function e(t){const e=t.split("."),n=e[0].split(":")||[],o=(l=e[1]||"0",i=3,r="0",l.length>i?String(l):((i-=l.length)>r.length&&(r+=r.repeat(i/r.length)),String(l)+r.slice(0,i)));var l,i,r;const s=Number(o)/1e3;return 3600*Number(n[n.length-3]||0)+60*Number(n[n.length-2]||0)+Number(n[n.length-1]||0)+s}function n({lifetime:e,thumbnails:n,progress:o,duration:l,setStyle:i,isMobile:r}){let s=null,u=0;function c(t,n,o){e.closed||i(t,n,o)}return e.own(()=>{if(u++,null!==s){const t=s;s=null,clearTimeout(t)}}),i=>(a,d,f)=>t(null,null,function*(){if(e.closed)return;const t="played"===a&&f&&r;if("hover"!==a&&!t)return;const h=o.clientWidth*d,p=d*l();if(c(i,"display","flex"),e.closed)return;const m=n.find(t=>p>=t.start&&p<=t.end);if(!m)return c(i,"display","none");if(h>0&&ho.clientWidth-e.w/2?c(t,"left",o.clientWidth-e.w+"px"):c(t,"left",n-e.w/2+"px")}(i,m,h):r||c(i,"display","none"),t&&!e.closed){const t=++u;if(null!==s&&clearTimeout(s),e.closed)return;const n=setTimeout(()=>{t!==u||e.closed||(s=null,c(i,"display","none"))},500);e.closed||t!==u?clearTimeout(n):s=n}})}return function(o){return l=>t(null,null,function*(){const{constructor:{utils:{setStyle:i,isMobile:r,addClass:s}},template:{$progress:u}}=l,c=function(t){let e=Boolean(t.isDestroy);const n=new Set;let o;const l=new Promise(t=>{o=t}),i={get closed(){return e},own:t=>(e?r(t):n.add(t),()=>n.delete(t)),listen(n,o){e||(i.own(()=>t.off(n,o)),t.on(n,o))},wait:t=>Promise.race([t,l]),dispose(){if(e)return;e=!0,o();const t=[...n].reverse();n.clear();for(const e of t)r(e)}};function r(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{i.listen("destroy",i.dispose)}catch(s){throw i.dispose(),s}return i}(l),a={name:"artplayerPluginVttThumbnail"};try{if(c.closed)return a;const d=o.vtt,f=yield function(e="",n){return t(this,null,function*(){if(n.closed)return;const t="function"===typeof AbortController?new AbortController:null,o=n.own(()=>null==t?void 0:t.abort());try{if(n.closed)return;const o=yield n.wait(t?fetch(e,{signal:t.signal}):fetch(e));if(n.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch VTT thumbnails: HTTP ${o.status}`);if(n.closed)return;return yield n.wait(o.text())}finally{o()}})}(d,c);if(c.closed)return a;const h=function(t,n=""){const o=t.split(/[\n\r]/g).filter(t=>t.trim()),l=[];for(let i=1;i ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/,u=t.match(s),c=/(.*)#(\w{4})=(.*)/,a=r.match(c),d=Math.floor(e(u[1])),f=Math.floor(e(u[2]));let h=a[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(h)){const t=n.split("/");t.pop(),t.push(h),h=t.join("/")}const p={start:d,end:f,url:h},m=a[2].split(""),y=a[3].split(",");for(let e=0;el.duration,setStyle:i,isMobile:r}),m=o.style||{};return c.closed||l.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:m,mounted(t){c.own(()=>{l.controls["vtt-thumbnail"]===t&&l.controls.remove("vtt-thumbnail")}),c.closed||(s(t,"art-control-thumbnails"),c.listen("setBar",p(t)))}}),a}catch(d){throw c.dispose(),d}})}}); diff --git a/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs b/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs index ecffe74e3..27d351f66 100644 --- a/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs +++ b/packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs @@ -4,6 +4,59 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ +function createLifetime(art) { + let closed = Boolean(art.isDestroy); + const cleanups = /* @__PURE__ */ new Set(); + let cancel; + const cancelled = new Promise((resolve) => { + cancel = resolve; + }); + const lifetime = { + get closed() { + return closed; + }, + own(cleanup) { + if (closed) + run(cleanup); + else + cleanups.add(cleanup); + return () => cleanups.delete(cleanup); + }, + listen(name, callback) { + if (closed) + return; + lifetime.own(() => art.off(name, callback)); + art.on(name, callback); + }, + wait(value) { + return Promise.race([value, cancelled]); + }, + dispose() { + if (closed) + return; + closed = true; + cancel(); + const pending = [...cleanups].reverse(); + cleanups.clear(); + for (const cleanup of pending) + run(cleanup); + } + }; + function run(cleanup) { + try { + cleanup(); + } catch (error) { + console.warn("Failed to clean up VTT thumbnails:", error); + } + } + try { + lifetime.listen("destroy", lifetime.dispose); + } catch (error) { + lifetime.dispose(); + throw error; + } + return lifetime; +} function padEnd(str, targetLength, padString) { if (str.length > targetLength) { return String(str); @@ -25,8 +78,7 @@ function t2d(time) { const s = Number(left[left.length - 1] || 0); return h + m + s + ms; } -async function getVttArray(vttUrl = "") { - const vttString = await (await fetch(vttUrl)).text(); +function parseVtt(vttString, vttUrl = "") { const lines = vttString.split(/[\n\r]/g).filter((item) => item.trim()); const vttArray = []; for (let i = 1; i < lines.length; i += 2) { @@ -58,65 +110,128 @@ async function getVttArray(vttUrl = "") { } return vttArray; } +function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isMobile }) { + let timer = null; + let generation = 0; + lifetime.own(() => { + generation++; + if (timer !== null) { + const previous = timer; + timer = null; + clearTimeout(previous); + } + }); + function style(control, key, value) { + if (!lifetime.closed) + setStyle(control, key, value); + } + function show(control, cue, width) { + style(control, "backgroundImage", `url(${cue.url})`); + style(control, "height", `${cue.h}px`); + style(control, "width", `${cue.w}px`); + style(control, "backgroundPosition", `-${cue.x}px -${cue.y}px`); + if (width <= cue.w / 2) + style(control, "left", 0); + else if (width > progress.clientWidth - cue.w / 2) + style(control, "left", `${progress.clientWidth - cue.w}px`); + else + style(control, "left", `${width - cue.w / 2}px`); + } + return (control) => async (type, percentage, event) => { + if (lifetime.closed) + return; + const dragging = type === "played" && event && isMobile; + if (type !== "hover" && !dragging) + return; + const width = progress.clientWidth * percentage; + const second = percentage * duration(); + style(control, "display", "flex"); + if (lifetime.closed) + return; + const cue = thumbnails.find((item) => second >= item.start && second <= item.end); + if (!cue) + return style(control, "display", "none"); + if (width > 0 && width < progress.clientWidth) + show(control, cue, width); + else if (!isMobile) + style(control, "display", "none"); + if (dragging && !lifetime.closed) { + const current = ++generation; + if (timer !== null) + clearTimeout(timer); + if (lifetime.closed) + return; + const id = setTimeout(() => { + if (current !== generation || lifetime.closed) + return; + timer = null; + style(control, "display", "none"); + }, 500); + if (lifetime.closed || current !== generation) + clearTimeout(id); + else + timer = id; + } + }; +} +async function requestVtt(url = "", lifetime) { + if (lifetime.closed) + return; + const controller = typeof AbortController === "function" ? new AbortController() : null; + const release = lifetime.own(() => controller?.abort()); + try { + if (lifetime.closed) + return; + const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url)); + if (lifetime.closed) + return; + if (response.ok === false) + throw new Error(`Failed to fetch VTT thumbnails: HTTP ${response.status}`); + if (lifetime.closed) + return; + return await lifetime.wait(response.text()); + } finally { + release(); + } +} function artplayerPluginVttThumbnail(option) { return async (art) => { - const { - constructor: { - utils: { setStyle, isMobile, addClass } - }, - template: { $progress } - } = art; - let timer = null; - const thumbnails = await getVttArray(option.vtt); - function showThumbnails($control, find, width) { - setStyle($control, "backgroundImage", `url(${find.url})`); - setStyle($control, "height", `${find.h}px`); - setStyle($control, "width", `${find.w}px`); - setStyle($control, "backgroundPosition", `-${find.x}px -${find.y}px`); - if (width <= find.w / 2) { - setStyle($control, "left", 0); - } else if (width > $progress.clientWidth - find.w / 2) { - setStyle($control, "left", `${$progress.clientWidth - find.w}px`); - } else { - setStyle($control, "left", `${width - find.w / 2}px`); - } + const { constructor: { utils: { setStyle, isMobile, addClass } }, template: { $progress } } = art; + const lifetime = createLifetime(art); + const result = { name: "artplayerPluginVttThumbnail" }; + try { + if (lifetime.closed) + return result; + const url = option.vtt; + const text = await requestVtt(url, lifetime); + if (lifetime.closed) + return result; + const thumbnails = parseVtt(text, url); + const preview = createPreview({ lifetime, thumbnails, progress: $progress, duration: () => art.duration, setStyle, isMobile }); + const style = option.style || {}; + if (lifetime.closed) + return result; + art.controls.add({ + name: "vtt-thumbnail", + position: "top", + index: 20, + style, + mounted(control) { + lifetime.own(() => { + if (art.controls["vtt-thumbnail"] === control) + art.controls.remove("vtt-thumbnail"); + }); + if (lifetime.closed) + return; + addClass(control, "art-control-thumbnails"); + lifetime.listen("setBar", preview(control)); + } + }); + return result; + } catch (error) { + lifetime.dispose(); + throw error; } - art.controls.add({ - name: "vtt-thumbnail", - position: "top", - index: 20, - style: option.style || {}, - mounted($control) { - addClass($control, "art-control-thumbnails"); - art.on("setBar", async (type, percentage, event) => { - const isMobileDroging = type === "played" && event && isMobile; - if (type === "hover" || isMobileDroging) { - const width = $progress.clientWidth * percentage; - const second = percentage * art.duration; - setStyle($control, "display", "flex"); - const find = thumbnails.find((item) => second >= item.start && second <= item.end); - if (!find) - return setStyle($control, "display", "none"); - if (width > 0 && width < $progress.clientWidth) { - showThumbnails($control, find, width); - } else { - if (!isMobile) { - setStyle($control, "display", "none"); - } - } - if (isMobileDroging) { - clearTimeout(timer); - timer = setTimeout(() => { - setStyle($control, "display", "none"); - }, 500); - } - } - }); - } - }); - return { - name: "artplayerPluginVttThumbnail" - }; }; } export { diff --git a/packages/artplayer-plugin-vtt-thumbnail/src/getVttArray.js b/packages/artplayer-plugin-vtt-thumbnail/src/getVttArray.js index e0133e6e2..f452c60eb 100644 --- a/packages/artplayer-plugin-vtt-thumbnail/src/getVttArray.js +++ b/packages/artplayer-plugin-vtt-thumbnail/src/getVttArray.js @@ -1,70 +1,6 @@ -function padEnd(str, targetLength, padString) { - if (str.length > targetLength) { - return String(str) - } - else { - targetLength = targetLength - str.length - if (targetLength > padString.length) { - padString += padString.repeat(targetLength / padString.length) - } - return String(str) + padString.slice(0, targetLength) - } -} - -function t2d(time) { - const arr = time.split('.') - const left = arr[0].split(':') || [] - const right = padEnd(arr[1] || '0', 3, '0') - const ms = Number(right) / 1000 - - const h = Number(left[left.length - 3] || 0) * 3600 - const m = Number(left[left.length - 2] || 0) * 60 - const s = Number(left[left.length - 1] || 0) - return h + m + s + ms -} +import parseVtt from './parseVtt' +// Preserve the historical source helper while registration owns its requests. export default async function getVttArray(vttUrl = '') { - const vttString = await (await fetch(vttUrl)).text() - const lines = vttString.split(/[\n\r]/g).filter(item => item.trim()) - const vttArray = [] - - for (let i = 1; i < lines.length; i += 2) { - const time = lines[i] - const text = lines[i + 1] - - if (!text.trim()) - continue - - const timeReg - = /((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/ - const timeMatch = time.match(timeReg) - - const textReg = /(.*)#(\w{4})=(.*)/ - const textMatch = text.match(textReg) - - const start = Math.floor(t2d(timeMatch[1])) - const end = Math.floor(t2d(timeMatch[2])) - - let url = textMatch[1] - const isAbsoluteUrl = /^\/|(?:https?|ftp|file):\/\//i.test(url) - if (!isAbsoluteUrl) { - const urlArr = vttUrl.split('/') - urlArr.pop() - urlArr.push(url) - url = urlArr.join('/') - } - - const result = { start, end, url } - - const keys = textMatch[2].split('') - const values = textMatch[3].split(',') - - for (let j = 0; j < keys.length; j++) { - result[keys[j]] = values[j] - } - - vttArray.push(result) - } - - return vttArray + return parseVtt(await (await fetch(vttUrl)).text(), vttUrl) } diff --git a/packages/artplayer-plugin-vtt-thumbnail/src/index.js b/packages/artplayer-plugin-vtt-thumbnail/src/index.js index e002362f4..862434102 100644 --- a/packages/artplayer-plugin-vtt-thumbnail/src/index.js +++ b/packages/artplayer-plugin-vtt-thumbnail/src/index.js @@ -1,74 +1,46 @@ -import getVttArray from './getVttArray' +import createLifetime from './lifetime' +import parseVtt from './parseVtt' +import createPreview from './preview' +import requestVtt from './request' export default function artplayerPluginVttThumbnail(option) { return async (art) => { - const { - constructor: { - utils: { setStyle, isMobile, addClass }, - }, - template: { $progress }, - } = art - - let timer = null - const thumbnails = await getVttArray(option.vtt) - - function showThumbnails($control, find, width) { - setStyle($control, 'backgroundImage', `url(${find.url})`) - setStyle($control, 'height', `${find.h}px`) - setStyle($control, 'width', `${find.w}px`) - setStyle($control, 'backgroundPosition', `-${find.x}px -${find.y}px`) - if (width <= find.w / 2) { - setStyle($control, 'left', 0) - } - else if (width > $progress.clientWidth - find.w / 2) { - setStyle($control, 'left', `${$progress.clientWidth - find.w}px`) - } - else { - setStyle($control, 'left', `${width - find.w / 2}px`) - } + const { constructor: { utils: { setStyle, isMobile, addClass } }, template: { $progress } } = art + const lifetime = createLifetime(art) + const result = { name: 'artplayerPluginVttThumbnail' } + try { + if (lifetime.closed) + return result + const url = option.vtt + const text = await requestVtt(url, lifetime) + if (lifetime.closed) + return result + const thumbnails = parseVtt(text, url) + const preview = createPreview({ lifetime, thumbnails, progress: $progress, duration: () => art.duration, setStyle, isMobile }) + const style = option.style || {} + if (lifetime.closed) + return result + art.controls.add({ + name: 'vtt-thumbnail', + position: 'top', + index: 20, + style, + mounted(control) { + lifetime.own(() => { + if (art.controls['vtt-thumbnail'] === control) + art.controls.remove('vtt-thumbnail') + }) + if (lifetime.closed) + return + addClass(control, 'art-control-thumbnails') + lifetime.listen('setBar', preview(control)) + }, + }) + return result } - - art.controls.add({ - name: 'vtt-thumbnail', - position: 'top', - index: 20, - style: option.style || {}, - mounted($control) { - addClass($control, 'art-control-thumbnails') - art.on('setBar', async (type, percentage, event) => { - const isMobileDroging = type === 'played' && event && isMobile - - if (type === 'hover' || isMobileDroging) { - const width = $progress.clientWidth * percentage - const second = percentage * art.duration - setStyle($control, 'display', 'flex') - - const find = thumbnails.find(item => second >= item.start && second <= item.end) - if (!find) - return setStyle($control, 'display', 'none') - - if (width > 0 && width < $progress.clientWidth) { - showThumbnails($control, find, width) - } - else { - if (!isMobile) { - setStyle($control, 'display', 'none') - } - } - - if (isMobileDroging) { - clearTimeout(timer) - timer = setTimeout(() => { - setStyle($control, 'display', 'none') - }, 500) - } - } - }) - }, - }) - - return { - name: 'artplayerPluginVttThumbnail', + catch (error) { + lifetime.dispose() + throw error } } } diff --git a/packages/artplayer-plugin-vtt-thumbnail/src/lifetime.js b/packages/artplayer-plugin-vtt-thumbnail/src/lifetime.js new file mode 100644 index 000000000..7f96869d0 --- /dev/null +++ b/packages/artplayer-plugin-vtt-thumbnail/src/lifetime.js @@ -0,0 +1,51 @@ +export default function createLifetime(art) { + let closed = Boolean(art.isDestroy) + const cleanups = new Set() + let cancel + const cancelled = new Promise((resolve) => { + cancel = resolve + }) + const lifetime = { + get closed() { return closed }, + own(cleanup) { + if (closed) + run(cleanup) + else + cleanups.add(cleanup) + return () => cleanups.delete(cleanup) + }, + listen(name, callback) { + if (closed) + return + lifetime.own(() => art.off(name, callback)) + art.on(name, callback) + }, + wait(value) { return Promise.race([value, cancelled]) }, + dispose() { + if (closed) + return + closed = true + cancel() + const pending = [...cleanups].reverse() + cleanups.clear() + for (const cleanup of pending) + run(cleanup) + }, + } + function run(cleanup) { + try { + cleanup() + } + catch (error) { + console.warn('Failed to clean up VTT thumbnails:', error) + } + } + try { + lifetime.listen('destroy', lifetime.dispose) + } + catch (error) { + lifetime.dispose() + throw error + } + return lifetime +} diff --git a/packages/artplayer-plugin-vtt-thumbnail/src/parseVtt.js b/packages/artplayer-plugin-vtt-thumbnail/src/parseVtt.js new file mode 100644 index 000000000..b68af2428 --- /dev/null +++ b/packages/artplayer-plugin-vtt-thumbnail/src/parseVtt.js @@ -0,0 +1,69 @@ +function padEnd(str, targetLength, padString) { + if (str.length > targetLength) { + return String(str) + } + else { + targetLength = targetLength - str.length + if (targetLength > padString.length) { + padString += padString.repeat(targetLength / padString.length) + } + return String(str) + padString.slice(0, targetLength) + } +} + +function t2d(time) { + const arr = time.split('.') + const left = arr[0].split(':') || [] + const right = padEnd(arr[1] || '0', 3, '0') + const ms = Number(right) / 1000 + + const h = Number(left[left.length - 3] || 0) * 3600 + const m = Number(left[left.length - 2] || 0) * 60 + const s = Number(left[left.length - 1] || 0) + return h + m + s + ms +} + +export default function parseVtt(vttString, vttUrl = '') { + const lines = vttString.split(/[\n\r]/g).filter(item => item.trim()) + const vttArray = [] + + for (let i = 1; i < lines.length; i += 2) { + const time = lines[i] + const text = lines[i + 1] + + if (!text.trim()) + continue + + const timeReg + = /((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/ + const timeMatch = time.match(timeReg) + + const textReg = /(.*)#(\w{4})=(.*)/ + const textMatch = text.match(textReg) + + const start = Math.floor(t2d(timeMatch[1])) + const end = Math.floor(t2d(timeMatch[2])) + + let url = textMatch[1] + const isAbsoluteUrl = /^\/|(?:https?|ftp|file):\/\//i.test(url) + if (!isAbsoluteUrl) { + const urlArr = vttUrl.split('/') + urlArr.pop() + urlArr.push(url) + url = urlArr.join('/') + } + + const result = { start, end, url } + + const keys = textMatch[2].split('') + const values = textMatch[3].split(',') + + for (let j = 0; j < keys.length; j++) { + result[keys[j]] = values[j] + } + + vttArray.push(result) + } + + return vttArray +} diff --git a/packages/artplayer-plugin-vtt-thumbnail/src/preview.js b/packages/artplayer-plugin-vtt-thumbnail/src/preview.js new file mode 100644 index 000000000..62d84a3d3 --- /dev/null +++ b/packages/artplayer-plugin-vtt-thumbnail/src/preview.js @@ -0,0 +1,64 @@ +export default function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isMobile }) { + let timer = null + let generation = 0 + lifetime.own(() => { + generation++ + if (timer !== null) { + const previous = timer + timer = null + clearTimeout(previous) + } + }) + function style(control, key, value) { + if (!lifetime.closed) + setStyle(control, key, value) + } + function show(control, cue, width) { + style(control, 'backgroundImage', `url(${cue.url})`) + style(control, 'height', `${cue.h}px`) + style(control, 'width', `${cue.w}px`) + style(control, 'backgroundPosition', `-${cue.x}px -${cue.y}px`) + if (width <= cue.w / 2) + style(control, 'left', 0) + else if (width > progress.clientWidth - cue.w / 2) + style(control, 'left', `${progress.clientWidth - cue.w}px`) + else + style(control, 'left', `${width - cue.w / 2}px`) + } + return control => async (type, percentage, event) => { + if (lifetime.closed) + return + const dragging = type === 'played' && event && isMobile + if (type !== 'hover' && !dragging) + return + const width = progress.clientWidth * percentage + const second = percentage * duration() + style(control, 'display', 'flex') + if (lifetime.closed) + return + const cue = thumbnails.find(item => second >= item.start && second <= item.end) + if (!cue) + return style(control, 'display', 'none') + if (width > 0 && width < progress.clientWidth) + show(control, cue, width) + else if (!isMobile) + style(control, 'display', 'none') + if (dragging && !lifetime.closed) { + const current = ++generation + if (timer !== null) + clearTimeout(timer) + if (lifetime.closed) + return + const id = setTimeout(() => { + if (current !== generation || lifetime.closed) + return + timer = null + style(control, 'display', 'none') + }, 500) + if (lifetime.closed || current !== generation) + clearTimeout(id) + else + timer = id + } + } +} diff --git a/packages/artplayer-plugin-vtt-thumbnail/src/request.js b/packages/artplayer-plugin-vtt-thumbnail/src/request.js new file mode 100644 index 000000000..e52f67ac6 --- /dev/null +++ b/packages/artplayer-plugin-vtt-thumbnail/src/request.js @@ -0,0 +1,21 @@ +export default async function requestVtt(url = '', lifetime) { + if (lifetime.closed) + return + const controller = typeof AbortController === 'function' ? new AbortController() : null + const release = lifetime.own(() => controller?.abort()) + try { + if (lifetime.closed) + return + const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url)) + if (lifetime.closed) + return + if (response.ok === false) + throw new Error(`Failed to fetch VTT thumbnails: HTTP ${response.status}`) + if (lifetime.closed) + return + return await lifetime.wait(response.text()) + } + finally { + release() + } +} diff --git a/refactor/baselines/vtt-thumbnail-resources.json b/refactor/baselines/vtt-thumbnail-resources.json new file mode 100644 index 000000000..d3a7c0e21 --- /dev/null +++ b/refactor/baselines/vtt-thumbnail-resources.json @@ -0,0 +1,1938 @@ +{ + "schemaVersion": 1, + "task": "PKG-VTT-THUMB-03", + "status": "doing", + "parentCommit": "05244d1104997bf79148156fa5766dcbf8836293", + "checks": [ + { + "command": "old implementation candidate expectations", + "tests": 21, + "pass": 4, + "fail": 17, + "file": "refactor/.cache/vtt-thumbnail03-before-final.log", + "sha256": "da5aced97c5bb4d8516517c3c1452c94d5a0b8dd9dbc652eb2567ad58144d0d7" + }, + { + "command": "candidate lifecycle source", + "tests": 21, + "pass": 21, + "fail": 0, + "file": "refactor/.cache/vtt-thumbnail03-unit-source.log", + "sha256": "0b24862608e8d874db9192bbce6f07f82bd22a88831adc5bb1fb685029c74d7b" + }, + { + "command": "native browser source", + "file": "refactor/.cache/vtt-thumbnail03-browser-source.json", + "sha256": "8d1f89245545b877158bac4925856e395222eecf6d257b9f0d861d5209cac312", + "resultsDirectory": "refactor/.cache/vtt-thumbnail03-browser-source-results", + "stats": { + "startTime": "2026-09-13T00:32:57.534Z", + "duration": 12600.347, + "expected": 12, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": "31e53420af42118d4824811656010e9154f814c35afe6fa4afea145572e76cf1" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": "31e53420af42118d4824811656010e9154f814c35afe6fa4afea145572e76cf1" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "371f6856cc1c6b8b0dfe9adfc4ef32d5d6a7fcb7733a2714640a46428e4859e4" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "371f6856cc1c6b8b0dfe9adfc4ef32d5d6a7fcb7733a2714640a46428e4859e4" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "bf2fe015ba1b7dbea4bbdda08bc083841ec1e254d1248937826b4f93b4bb623f" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "1813f0288c493ea00e5cbdf5a869876f7fd281f6b300c21756bda468578b2fe1" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "0915d8582e9efac8c493c2af37681044a6a6d465c2404cd79b642ce3f5b04fab", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + } + ] + }, + { + "command": "candidate lifecycle main", + "tests": 21, + "pass": 21, + "fail": 0, + "file": "refactor/.cache/vtt-thumbnail03-unit-main.log", + "sha256": "edd8d6a85c7fd707374786ab738c361a30e9261bd679672109f510ce45c78897" + }, + { + "command": "native browser main", + "file": "refactor/.cache/vtt-thumbnail03-browser-main.json", + "sha256": "5de523cdab36c6b4e71318c352544f958b87299b05c58f1e0958810610f115e6", + "resultsDirectory": "refactor/.cache/vtt-thumbnail03-browser-main-results", + "stats": { + "startTime": "2026-09-13T00:33:11.258Z", + "duration": 11726.372, + "expected": 12, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": "31e53420af42118d4824811656010e9154f814c35afe6fa4afea145572e76cf1" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": "31e53420af42118d4824811656010e9154f814c35afe6fa4afea145572e76cf1" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "371f6856cc1c6b8b0dfe9adfc4ef32d5d6a7fcb7733a2714640a46428e4859e4" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "NS_ERROR_PARSED_DATA_CACHED" + } + }, + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "371f6856cc1c6b8b0dfe9adfc4ef32d5d6a7fcb7733a2714640a46428e4859e4" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "1813f0288c493ea00e5cbdf5a869876f7fd281f6b300c21756bda468578b2fe1" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "1813f0288c493ea00e5cbdf5a869876f7fd281f6b300c21756bda468578b2fe1" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + } + ] + }, + { + "command": "candidate lifecycle legacy", + "tests": 21, + "pass": 21, + "fail": 0, + "file": "refactor/.cache/vtt-thumbnail03-unit-legacy.log", + "sha256": "0ef659c5a5d20994e3ad23865f3efc591f6ef21965ed42d5d9dd15bd56ec1920" + }, + { + "command": "native browser legacy", + "file": "refactor/.cache/vtt-thumbnail03-browser-legacy.json", + "sha256": "ba1876fe4783652f6f9b203430fbfb14fe2465c133353cbf6696c9e2bae0a9a6", + "resultsDirectory": "refactor/.cache/vtt-thumbnail03-browser-legacy-results", + "stats": { + "startTime": "2026-09-13T00:33:24.061Z", + "duration": 12282.072, + "expected": 12, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": "31e53420af42118d4824811656010e9154f814c35afe6fa4afea145572e76cf1" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": "31e53420af42118d4824811656010e9154f814c35afe6fa4afea145572e76cf1" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "371f6856cc1c6b8b0dfe9adfc4ef32d5d6a7fcb7733a2714640a46428e4859e4" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "371f6856cc1c6b8b0dfe9adfc4ef32d5d6a7fcb7733a2714640a46428e4859e4" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "1813f0288c493ea00e5cbdf5a869876f7fd281f6b300c21756bda468578b2fe1" + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "published", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + }, + { + "title": "VTT native candidate core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "pixels": [ + [ + 255, + 0, + 0, + 255 + ], + [ + 0, + 0, + 255, + 255 + ] + ] + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [], + "consoleErrors": [], + "screenshotSha256": "1813f0288c493ea00e5cbdf5a869876f7fd281f6b300c21756bda468578b2fe1" + }, + { + "title": "VTT native candidate core: destroy aborts a pending native fetch and settles registration", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "platform": "win32", + "candidate": { + "core": "candidate", + "implementation": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "nativeAbort": true + }, + "core": { + "published": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "candidate": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + } + }, + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/vtt-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ], + "consoleErrors": [], + "screenshotSha256": null + } + ] + }, + { + "command": "native frozen old plugin with published core", + "file": "refactor/.cache/vtt-thumbnail03-browser-before.json", + "stats": { + "startTime": "2026-09-13T00:30:39.495Z", + "duration": 25840.971999999998, + "expected": 0, + "skipped": 0, + "unexpected": 6, + "flaky": 0 + }, + "failures": [ + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "chromium", + "messages": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveCount\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('.art-control-vtt-thumbnail')\nExpected: \u001b[32m0\u001b[39m\nReceived: \u001b[31m1\u001b[39m\nTimeout: 7000ms\n\nCall log:\n\u001b[2m - Expect \"toHaveCount\" locator('.art-control-vtt-thumbnail') with timeout 7000ms\u001b[22m\n\u001b[2m - waiting for locator('.art-control-vtt-thumbnail')\u001b[22m\n\u001b[2m 17 × locator resolved to 1 element\u001b[22m\n\u001b[2m - unexpected value \"1\"\u001b[22m\n\n\n 42 | await testInfo.attach('selected-sprite', { body: await preview.screenshot(), contentType: 'image/png' })\n 43 | await page.evaluate(() => window.art.destroy(false))\n> 44 | await expect(preview).toHaveCount(0)\n | ^\n 45 | await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), pixels }), contentType: 'application/json' })\n 46 | })\n 47 |\n at D:\\github\\ArtPlayer\\test\\browser\\vtt-thumbnail-lifecycle.spec.js:44:27" + ] + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "chromium", + "messages": [ + "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[32mtrue\u001b[39m\nReceived: \u001b[31mfalse\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 79 | await started\n 80 | await page.evaluate(() => window.art.destroy(false))\n> 81 | await expect.poll(() => page.evaluate(() => window.vttSettled)).toBe(true)\n | ^\n 82 | expect(await page.evaluate(() => window.vttSignal.aborted)).toBe(true)\n 83 | release()\n 84 | await page.evaluate(() => window.vttPending)\n at D:\\github\\ArtPlayer\\test\\browser\\vtt-thumbnail-lifecycle.spec.js:81:71" + ] + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "firefox", + "messages": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveCount\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('.art-control-vtt-thumbnail')\nExpected: \u001b[32m0\u001b[39m\nReceived: \u001b[31m1\u001b[39m\nTimeout: 7000ms\n\nCall log:\n\u001b[2m - Expect \"toHaveCount\" locator('.art-control-vtt-thumbnail') with timeout 7000ms\u001b[22m\n\u001b[2m - waiting for locator('.art-control-vtt-thumbnail')\u001b[22m\n\u001b[2m 18 × locator resolved to 1 element\u001b[22m\n\u001b[2m - unexpected value \"1\"\u001b[22m\n\n\n 42 | await testInfo.attach('selected-sprite', { body: await preview.screenshot(), contentType: 'image/png' })\n 43 | await page.evaluate(() => window.art.destroy(false))\n> 44 | await expect(preview).toHaveCount(0)\n | ^\n 45 | await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), pixels }), contentType: 'application/json' })\n 46 | })\n 47 |\n at D:\\github\\ArtPlayer\\test\\browser\\vtt-thumbnail-lifecycle.spec.js:44:27" + ] + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "firefox", + "messages": [ + "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[32mtrue\u001b[39m\nReceived: \u001b[31mfalse\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 79 | await started\n 80 | await page.evaluate(() => window.art.destroy(false))\n> 81 | await expect.poll(() => page.evaluate(() => window.vttSettled)).toBe(true)\n | ^\n 82 | expect(await page.evaluate(() => window.vttSignal.aborted)).toBe(true)\n 83 | release()\n 84 | await page.evaluate(() => window.vttPending)\n at D:\\github\\ArtPlayer\\test\\browser\\vtt-thumbnail-lifecycle.spec.js:81:71" + ] + }, + { + "title": "VTT native published core: real progress hover selects sprite regions and destroy removes owned UI", + "project": "webkit", + "messages": [ + "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveCount\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('.art-control-vtt-thumbnail')\nExpected: \u001b[32m0\u001b[39m\nReceived: \u001b[31m1\u001b[39m\nTimeout: 7000ms\n\nCall log:\n\u001b[2m - Expect \"toHaveCount\" locator('.art-control-vtt-thumbnail') with timeout 7000ms\u001b[22m\n\u001b[2m - waiting for locator('.art-control-vtt-thumbnail')\u001b[22m\n\u001b[2m 17 × locator resolved to 1 element\u001b[22m\n\u001b[2m - unexpected value \"1\"\u001b[22m\n\n\n 42 | await testInfo.attach('selected-sprite', { body: await preview.screenshot(), contentType: 'image/png' })\n 43 | await page.evaluate(() => window.art.destroy(false))\n> 44 | await expect(preview).toHaveCount(0)\n | ^\n 45 | await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), pixels }), contentType: 'application/json' })\n 46 | })\n 47 |\n at D:\\github\\ArtPlayer\\test\\browser\\vtt-thumbnail-lifecycle.spec.js:44:27" + ] + }, + { + "title": "VTT native published core: destroy aborts a pending native fetch and settles registration", + "project": "webkit", + "messages": [ + "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[32mtrue\u001b[39m\nReceived: \u001b[31mfalse\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 79 | await started\n 80 | await page.evaluate(() => window.art.destroy(false))\n> 81 | await expect.poll(() => page.evaluate(() => window.vttSettled)).toBe(true)\n | ^\n 82 | expect(await page.evaluate(() => window.vttSignal.aborted)).toBe(true)\n 83 | release()\n 84 | await page.evaluate(() => window.vttPending)\n at D:\\github\\ArtPlayer\\test\\browser\\vtt-thumbnail-lifecycle.spec.js:81:71" + ] + } + ] + }, + { + "command": "actual ESM import/registration/hover/destroy", + "file": "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs", + "sha256": "947baf39608a2674446638bf1e721987ce31b3a655139530ba62d5aa0a0a9a9c", + "status": "pass", + "scope": "Native import of actual .mjs; controlled host registration/hover/destroy, not installed tarball" + }, + { + "command": "yarn ci:check", + "tests": { + "unit": 1701, + "engineering": 14, + "baseline": 323, + "total": 2038 + }, + "file": "refactor/.cache/vtt-thumbnail03-ci.log", + "sha256": "1cf246d6f42bd92020bd07b9427a693e1972b655cc6b8e7f707cff4366237d3a" + }, + { + "command": "focused eslint --no-fix", + "result": "pass", + "file": "refactor/.cache/vtt-thumbnail03-lint-check.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ], + "limitations": [ + "Parser malformed-input behavior remains pending", + "Strict TS and public declarations remain task 04", + "Full core/device, installed tarball and demo/editor acceptance remain tasks 05/06" + ], + "changes": [ + "Owned fetch/body cancellation with fallback settlement", + "Owned control/listener/timer cleanup and reentry guards", + "HTTP failures now reject without parsing their body", + "Pure parser extracted without algorithm changes" + ], + "visualReview": "Inspected Chromium source selected-sprite screenshot: blue region with native 00:06 progress overlay; preserved inline in archived browser JSON.", + "source": { + "packages/artplayer-plugin-vtt-thumbnail/src/getVttArray.js": "3c5d78bec07628eafc5eb0b4afd86118cfc40242a1ae9597eef893c9533e61dc", + "packages/artplayer-plugin-vtt-thumbnail/src/index.js": "e6e5a51a8011e0cdc312c713f746bd9e229d4edb563f29c9d7b5eae3f39d371d", + "packages/artplayer-plugin-vtt-thumbnail/src/lifetime.js": "e13e98ca5f2f1de0f591437a7ca93addef0b0e2e19bcae9e30aa687a71a6a714", + "packages/artplayer-plugin-vtt-thumbnail/src/parseVtt.js": "bdd3bd293805700bb8e98df0e953d7186ad65f8554ef5b189813c654fcacac6c", + "packages/artplayer-plugin-vtt-thumbnail/src/preview.js": "9b6b6ad09b1af46b06c050aa488248b92e00f469eb3bc1367dc5fd51c15ccb36", + "packages/artplayer-plugin-vtt-thumbnail/src/request.js": "dd161a0780b926bb93de50779c961c7d2b1c66ceb8a5eea31b8fe7d9123c55f1" + }, + "artifacts": { + "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "docs/compiled/artplayer-plugin-vtt-thumbnail.js": "2b64d4f4e357692b4bf697dd226ca4938638078c58e302166947fbb2594bf95a", + "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "docs/compiled/artplayer-plugin-vtt-thumbnail.legacy.js": "bb4f3a0245d126e0cb3c55e1abf715619f5be3c4257d9ff52137e3d7d0704b87", + "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs": "947baf39608a2674446638bf1e721987ce31b3a655139530ba62d5aa0a0a9a9c", + "docs/compiled/artplayer-plugin-vtt-thumbnail.mjs": "947baf39608a2674446638bf1e721987ce31b3a655139530ba62d5aa0a0a9a9c" + } +} diff --git a/refactor/changes/2026-09-13-PKG-VTT-THUMB-03-resources.md b/refactor/changes/2026-09-13-PKG-VTT-THUMB-03-resources.md new file mode 100644 index 000000000..634297e7f --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-VTT-THUMB-03-resources.md @@ -0,0 +1,32 @@ +# PKG-VTT-THUMB-03 资源所有权检查点 + +将注册、请求、纯解析、预览/计时器和生命周期拆开;保留旧 getVttArray 源路径的包装函数。 +这是 03 的源码检查点,**任务继续 doing**:解析错误/输入规则及更多资源边界尚需完成, +严格 TS/公开声明归 04,完整核心设备/安装/demo/编辑器验收归 05/06。 + +## 已实现修复 + +- 请求前订阅 destroy;原生 AbortController 可用时取消 fetch/body。即使 fetch 忽略 signal + 或环境没有 AbortController,销毁也结算注册 Promise,迟到返回不安装控件,迟到拒绝被观察。 +- setBar、移动隐藏计时器和控件均有独立清理。计时器 ID 0 有效,旧定时回调有代次检查, + 在 setTimeout 返回之前重入销毁时仍清掉迟到的 handle。 +- 每次样式写入检查关闭状态;销毁中重入不会再排计时器或后续写入。 +- 安装失败保留原错误并回滚已安装监听器/控件。清理失败只记录警告,继续其它清理。 + 控件只在实时 alias 仍为本次 mounted 元素时移除,不误删别人替换的控件。 +- HTTP 失败现在明确拒绝,不再解析错误响应体。这是单独复现的缺陷修正。 + +## 保持的边界 + +工厂/结果名、异步注册、URL/style 读取顺序、style 原对象、控件名/CSS/index、向下取整 +时间区间及首个命中、xywh/URL 几何、500ms 移动隐藏均保留。restart 不触发新增下载。 +源码抽出的 parseVtt 保持原算法;非法 cue/坐标、NOTE/ID 等仍不能称已修复。 +旧版 CJS `.default` 与同步声明冲突仍待 04,不能以当前直接函数入口代替所有旧消费形式。 + +## 验证与交接 + +21 项候选回归在冻结旧版 4 通过/17 失败,新源码通过;保留 01/02 历史测试。 +真实浏览器检查覆盖 published/candidate 核心、鼠标悬停、实际 sprite 像素和原生请求取消。 +准确构建、源码/产物/浏览器与 CI 结果见 [验证记录](../baselines/vtt-thumbnail-resources.json)。 +包内 ARCHITECTURE.md 说明实际文件地图和资源数据流。无依赖、版本或公开声明变更。 +按仓库命令生成 dist/docs 编译产物,不手改。回退本检查点提交可回到 02 后生产实现; +历史对照仍可重跑。独立本地提交,不推送、不发布。 diff --git a/refactor/plan.md b/refactor/plan.md index fe5ac80b3..8f248f527 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。 -状态:todo 94 / doing 12 / blocked 0 / done 116 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 93 / doing 13 / blocked 0 / done 116 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -184,7 +184,7 @@ | --- | --- | --- | --- | --- | --- | --- | | PKG-VTT-THUMB-01 | artplayer-plugin-vtt-thumbnail
核对包契约与历史用法 | BASE-05 | vtt/style、异步返回、预览控件名与样式契约 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done | | PKG-VTT-THUMB-02 | artplayer-plugin-vtt-thumbnail
建立特有行为与错误测试 | PKG-VTT-THUMB-01, ENG-03, ENG-05 | VTT 解析、xywh、相对 URL、时间边界、请求失败和定位 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | done | -| PKG-VTT-THUMB-03 | artplayer-plugin-vtt-thumbnail
整理内部职责与资源 | PKG-VTT-THUMB-02, CORE-09, CORE-19 | 纯解析/区间查找与 DOM、请求/定时器生命周期分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | todo | +| PKG-VTT-THUMB-03 | artplayer-plugin-vtt-thumbnail
整理内部职责与资源 | PKG-VTT-THUMB-02, CORE-09, CORE-19 | 纯解析/区间查找与 DOM、请求/定时器生命周期分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | doing | | PKG-VTT-THUMB-04 | artplayer-plugin-vtt-thumbnail
迁移自有源码和公开类型 | PKG-VTT-THUMB-03, ENG-04, ENG-06, CORE-07 | cue/rect/result 和参数可选性保持兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | todo | | PKG-VTT-THUMB-05 | artplayer-plugin-vtt-thumbnail
验证新旧核心和组合 | PKG-VTT-THUMB-04, CORE-22 | 鼠标和移动进度、全屏、切源、chapter 组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo | | PKG-VTT-THUMB-06 | artplayer-plugin-vtt-thumbnail
验证分发并同步文档 | PKG-VTT-THUMB-05, ENG-07 | vtt.thumbnail.js、VTT/图片资源解析和产物验证 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | M | todo | @@ -499,6 +499,7 @@ - PKG-AUTO-THUMB-03: [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md) [记录](baselines/auto-thumbnail-lifecycle-checkpoint.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md) [记录](baselines/auto-thumbnail-hidden-renderer.json) [记录](baselines/auto-thumbnail-timeline-media.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md) [记录](baselines/auto-thumbnail-frame-presentation.json) - PKG-VTT-THUMB-01: [记录](baselines/vtt-thumbnail-release.json) [记录](baselines/vtt-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-VTT-THUMB-01-contract.md) - PKG-VTT-THUMB-02: [记录](baselines/vtt-thumbnail-failures.json) [记录](changes/2026-09-13-PKG-VTT-THUMB-02-failures.md) +- PKG-VTT-THUMB-03: [记录](changes/2026-09-13-PKG-VTT-THUMB-03-resources.md) [记录](baselines/vtt-thumbnail-resources.json) - PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md) - PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md) - PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md) diff --git a/refactor/progress.md b/refactor/progress.md index aedb2a6b9..18838bb27 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,17 @@ # 进度与证据 +## PKG-VTT-THUMB-03 请求与预览资源检查点(doing) + +生产入口拆为注册、生命周期、请求、预览和纯解析,保留旧源码 helper。已修复销毁后 +安装/样式写入、挂起请求取消、移动计时器和部分安装回滚;HTTP 错误明确拒绝。 +21 项候选测试在旧版 4 通过/17 失败,源码/main/legacy 各 21 通过;原生旧版 6 项均复现失败, +候选三种入口各 12 项通过(两种核心 × 三浏览器 × 两场景),实际 ESM 也通过。 +完整 CI 2038 项通过。构建产物与包内架构同步,未改变声明/版本;解析健壮性、严格 TS、 +完整设备/核心组合与安装/demo 验收仍待。见[变更](changes/2026-09-13-PKG-VTT-THUMB-03-resources.md) +和[验证](baselines/vtt-thumbnail-resources.json)。222 项:116 done、13 doing、93 todo。 +独立本地检查点提交,不推送、不发布。 + + ## PKG-VTT-THUMB-02 特有失败与生命周期复现完成 108 项新历史测试复现销毁后安装/计时器写入、请求拒绝、HTTP 状态忽略、非法 VTT/坐标、 diff --git a/refactor/risks.json b/refactor/risks.json index 85118ae04..8dcdb0128 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4151,7 +4151,10 @@ "refactor/baselines/vtt-thumbnail-contract.md", "refactor/scripts/vtt-thumbnail-contract.test.mjs", "test/vtt-thumbnail.test.js", - "refactor/baselines/vtt-thumbnail-failures.json" + "refactor/baselines/vtt-thumbnail-failures.json", + "test/vtt-thumbnail-lifecycle.test.js", + "test/browser/vtt-thumbnail-lifecycle.spec.js", + "refactor/baselines/vtt-thumbnail-resources.json" ], "compatibleResolution": "Preserve valid cue selection and geometry; test malformed data and standard VTT separately from old compiler defects.", "closureCriteria": "Parser fixtures include malformed, empty, real demo VTT, fractional and shared bounds, relative URLs and historical accepted formats." @@ -4171,7 +4174,10 @@ "refactor/baselines/vtt-thumbnail-contract.md", "refactor/scripts/vtt-thumbnail-contract.test.mjs", "test/vtt-thumbnail.test.js", - "refactor/baselines/vtt-thumbnail-failures.json" + "refactor/baselines/vtt-thumbnail-failures.json", + "test/vtt-thumbnail-lifecycle.test.js", + "test/browser/vtt-thumbnail-lifecycle.spec.js", + "refactor/baselines/vtt-thumbnail-resources.json" ], "compatibleResolution": "Own requests, listeners, timers and controls per registration without stale writes or cross-instance cleanup.", "closureCriteria": "Deferred request, destroy, failure, timer, repeated registration and native core scenarios demonstrate cleanup." diff --git a/refactor/tasks.json b/refactor/tasks.json index 4bfadafd7..e6e082fe9 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1897,11 +1897,14 @@ "CORE-09", "CORE-19" ], - "status": "todo", + "status": "doing", "risk": "M", "deliverable": "纯解析/区间查找与 DOM、请求/定时器生命周期分离", "acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过", - "evidence": [] + "evidence": [ + "changes/2026-09-13-PKG-VTT-THUMB-03-resources.md", + "baselines/vtt-thumbnail-resources.json" + ] }, { "id": "PKG-VTT-THUMB-04", diff --git a/test/browser/vtt-thumbnail-lifecycle.spec.js b/test/browser/vtt-thumbnail-lifecycle.spec.js new file mode 100644 index 000000000..c76081e9f --- /dev/null +++ b/test/browser/vtt-thumbnail-lifecycle.spec.js @@ -0,0 +1,91 @@ +import { hash } from '../../refactor/scripts/releases.mjs' +import { vttThumbnailCandidate } from '../helpers/vtt-thumbnail.js' +import { expect, test } from './fixtures.js' + +const implementation = await vttThumbnailCandidate() +const cues = 'WEBVTT\n\n00:00.000 --> 00:04.000\nvtt-sprite.svg#xywh=0,0,80,45\n\n00:04.000 --> 00:08.000\nvtt-sprite.svg#xywh=80,0,80,45\n' +const sprite = '' + +for (const core of ['published', 'candidate']) { + test(`VTT native ${core} core: real progress hover selects sprite regions and destroy removes owned UI`, async ({ page }, testInfo) => { + await page.route('**/test/vtt-cues.vtt', route => route.fulfill({ contentType: 'text/vtt', body: cues })) + await page.route('**/test/vtt-sprite.svg', route => route.fulfill({ contentType: 'image/svg+xml', body: sprite })) + await page.goto(`/test/player.html?core=${core}`) + await page.addScriptTag({ content: implementation.code }) + await page.evaluate(async () => { + window.createPlayer('/test/pattern.mp4') + await window.art.plugins.add(window.artplayerPluginVttThumbnail({ vtt: '/test/vtt-cues.vtt' })) + }) + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) + const progress = page.locator('.art-control-progress') + const preview = page.locator('.art-control-vtt-thumbnail') + const bounds = await progress.boundingBox() + await page.mouse.move(bounds.x + bounds.width * 0.25, bounds.y + bounds.height / 2) + await expect(preview).toHaveCSS('display', 'flex') + await expect(preview).toHaveCSS('width', '80px') + await expect(preview).toHaveCSS('height', '45px') + await expect(preview).toHaveCSS('background-position', '0px 0px') + await page.mouse.move(bounds.x + bounds.width * 0.75, bounds.y + bounds.height / 2) + await expect(preview).toHaveCSS('background-position', '-80px 0px') + const pixels = await page.evaluate(async () => { + const image = new Image() + image.src = '/test/vtt-sprite.svg' + await image.decode() + const canvas = document.createElement('canvas') + canvas.width = 160 + canvas.height = 45 + const context = canvas.getContext('2d') + context.drawImage(image, 0, 0) + return [Array.from(context.getImageData(40, 20, 1, 1).data), Array.from(context.getImageData(120, 20, 1, 1).data)] + }) + expect(pixels).toEqual([[255, 0, 0, 255], [0, 0, 255, 255]]) + await testInfo.attach('selected-sprite', { body: await preview.screenshot(), contentType: 'image/png' }) + await page.evaluate(() => window.art.destroy(false)) + await expect(preview).toHaveCount(0) + await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), pixels }), contentType: 'application/json' }) + }) + + test(`VTT native ${core} core: destroy aborts a pending native fetch and settles registration`, async ({ page }, testInfo) => { + let release + let requested + const started = new Promise((resolve) => { + requested = resolve + }) + const pending = new Promise((resolve) => { + release = resolve + }) + await page.route('**/test/vtt-held.vtt', async (route) => { + requested() + await pending + await route.fulfill({ contentType: 'text/vtt', body: cues }).catch(() => {}) + }) + try { + await page.goto(`/test/player.html?core=${core}`) + await page.addScriptTag({ content: implementation.code }) + await page.evaluate(() => { + window.createPlayer('/test/pattern.mp4') + const fetch = window.fetch.bind(window) + window.fetch = (url, options) => { + if (url === '/test/vtt-held.vtt') + window.vttSignal = options?.signal + return fetch(url, options) + } + window.vttSettled = false + window.vttPending = window.artplayerPluginVttThumbnail({ vtt: '/test/vtt-held.vtt' })(window.art).then((result) => { + window.vttSettled = true + window.vttResult = result + }) + }) + await started + await page.evaluate(() => window.art.destroy(false)) + await expect.poll(() => page.evaluate(() => window.vttSettled)).toBe(true) + expect(await page.evaluate(() => window.vttSignal.aborted)).toBe(true) + release() + await page.evaluate(() => window.vttPending) + await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(0) + expect(await page.evaluate(() => window.vttResult)).toEqual({ name: 'artplayerPluginVttThumbnail' }) + await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), nativeAbort: true }), contentType: 'application/json' }) + } + finally { release() } + }) +} diff --git a/test/helpers/vtt-thumbnail.js b/test/helpers/vtt-thumbnail.js index 0946ca723..26f1b1eb1 100644 --- a/test/helpers/vtt-thumbnail.js +++ b/test/helpers/vtt-thumbnail.js @@ -1,7 +1,16 @@ +import fs from 'node:fs' +import process from 'node:process' import vm from 'node:vm' import { transform } from 'esbuild' import { readMember } from '../../refactor/scripts/releases.mjs' import { verifyVttThumbnailContract } from '../../refactor/scripts/vtt-thumbnail-contract.mjs' +import { compilePackage } from './load.js' + +export async function vttThumbnailCandidate() { + if (process.env.ARTPLAYER_VTT_THUMBNAIL_BASELINE === '1') + return (await vttThumbnailHistorical()).find(item => item.name === 'frozen-workspace-js') + return { name: 'candidate', profile: 'current', code: process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-vtt-thumbnail', 'umd') } +} export async function vttThumbnailHistorical() { const { baseline, archives, sources } = await verifyVttThumbnailContract() @@ -41,6 +50,8 @@ export function vttThumbnailEnvironment(implementation, { script = false, mobile const timers = new Map() const classes = new Set() const styles = {} + const removed = [] + const warnings = [] let nextTimer = 0 let resolveFetch let rejectFetch @@ -69,7 +80,12 @@ export function vttThumbnailEnvironment(implementation, { script = false, mobile } }, controls: { add(option) { controls.push(option) + this[option.name] = control option.mounted(control) + return control + }, remove(name) { + removed.push(name) + delete this[name] } }, on(name, callback) { if (!listeners.has(name)) @@ -81,6 +97,8 @@ export function vttThumbnailEnvironment(implementation, { script = false, mobile } const box = { window: {}, + AbortController, + console: { warn(...args) { warnings.push(args) } }, fetch(...args) { requests.push(args) return pending || Promise.resolve(response(text)) @@ -111,6 +129,8 @@ export function vttThumbnailEnvironment(implementation, { script = false, mobile timers, requests, progress, + removed, + warnings, resolve(source = text) { resolveFetch(response(source)) }, reject(error) { rejectFetch(error) }, async emit(name, ...args) { return Promise.all((listeners.get(name) || []).map(callback => callback(...args))) }, diff --git a/test/vtt-thumbnail-lifecycle.test.js b/test/vtt-thumbnail-lifecycle.test.js new file mode 100644 index 000000000..155df18c1 --- /dev/null +++ b/test/vtt-thumbnail-lifecycle.test.js @@ -0,0 +1,268 @@ +import assert from 'node:assert/strict' +// eslint-disable-next-line test/no-import-node-test -- Candidate resource regressions use the Node runner. +import test from 'node:test' +import { vttText, vttThumbnailCandidate, vttThumbnailEnvironment } from './helpers/vtt-thumbnail.js' + +const implementation = await vttThumbnailCandidate() +const create = options => vttThumbnailEnvironment(implementation, options) +async function flush() { + for (let index = 0; index < 8; index++) + await Promise.resolve() +} +const count = env => [...env.listeners.values()].reduce((sum, items) => sum + items.length, 0) + +test('VTT candidate retains asynchronous registration, live style, floored bounds and literal image paths', async () => { + const env = create({ deferred: true }) + const option = { vtt: '/folder/cues.vtt', style: {} } + const pending = env.factory(option)(env.art) + assert.equal(env.requests[0][0], option.vtt) + assert.equal(env.controls.length, 0) + option.style = { opacity: '0.8' } + env.resolve() + assert.equal((await pending).name, 'artplayerPluginVttThumbnail') + assert.equal(env.controls[0].style, option.style) + await env.hover(0.5) + assert.equal(env.styles.backgroundImage, 'url(/folder/sheet.jpg)') + assert.equal(env.styles.backgroundPosition, '-10px -20px') + assert.equal(env.styles.left, '60px') + await env.hover(0.51) + assert.equal(env.styles.backgroundImage, 'url(/folder/second.jpg)') +}) + +for (const abort of [true, false]) { + test(`VTT candidate settles destroyed pending registration without late mounting; AbortController=${abort}`, async () => { + const env = create({ deferred: true }) + if (!abort) + env.box.AbortController = undefined + let settled = false + const pending = env.factory({ vtt: '/cues.vtt' })(env.art).then((result) => { + settled = true + return result + }) + await env.emit('destroy') + await flush() + assert.equal(settled, true, 'Destroy settles even a fetch that ignores cancellation') + if (abort) + assert.equal(env.requests[0][1].signal.aborted, true) + env.resolve() + await pending + await flush() + assert.equal(env.controls.length, 0) + assert.equal(count(env), 0) + }) +} + +test('VTT candidate observes late rejection after destroy without exposing an unhandled Promise', async () => { + const env = create({ deferred: true }) + const pending = env.factory({})(env.art) + await env.emit('destroy') + env.reject(new Error('late network error')) + await pending + await flush() + assert.equal(count(env), 0) + assert.equal(env.controls.length, 0) +}) + +test('VTT candidate cancels while response body is pending', async () => { + const env = create() + let finish + env.box.fetch = async () => ({ ok: true, text: () => new Promise((resolve) => { + finish = resolve + }) }) + const pending = env.factory({})(env.art) + await flush() + assert.equal(typeof finish, 'function') + await env.emit('destroy') + finish(vttText) + await pending + assert.equal(env.controls.length, 0) + assert.equal(count(env), 0) +}) + +test('VTT candidate clears timer zero, removes its control and ignores captured stale callbacks', async () => { + const env = create({ mobile: true }) + await env.factory({})(env.art) + const listener = env.listeners.get('setBar')[0] + await env.hover(0.3, 'played', {}) + assert.equal(env.timers.has(0), true) + const timer = env.timers.get(0) + await env.emit('destroy') + assert.equal(env.timers.size, 0) + assert.equal(count(env), 0) + assert.deepEqual(env.removed, ['vtt-thumbnail']) + env.styles.display = 'sentinel' + timer.callback() + await listener('hover', 0.7, {}) + assert.equal(env.styles.display, 'sentinel') + await env.emit('destroy') + assert.equal(env.removed.length, 1) +}) + +test('VTT candidate never removes a replacement control owned by another registration', async () => { + const env = create() + await env.factory({})(env.art) + const replacement = {} + env.art.controls['vtt-thumbnail'] = replacement + await env.emit('destroy') + assert.equal(env.art.controls['vtt-thumbnail'], replacement) + assert.deepEqual(env.removed, []) + assert.equal(count(env), 0) +}) + +for (const phase of ['fetch', 'text', 'parse', 'mount']) { + test(`VTT candidate releases registration resources after ${phase} failure`, async () => { + const env = create({ text: phase === 'parse' ? 'WEBVTT\ninvalid\npayload' : vttText }) + const failure = new Error(`${phase} failure`) + if (phase === 'fetch') + env.box.fetch = () => { throw failure } + if (phase === 'text') + env.box.fetch = async () => ({ text: async () => { throw failure } }) + if (phase === 'mount') + env.art.constructor.utils.addClass = () => { throw failure } + await assert.rejects(env.factory({})(env.art), error => phase === 'parse' ? error.name === 'TypeError' : error === failure) + assert.equal(count(env), 0) + assert.equal(env.timers.size, 0) + if (phase === 'mount') + assert.deepEqual(env.removed, ['vtt-thumbnail']) + }) +} + +test('VTT candidate rejects HTTP failure before parsing its body', async () => { + const env = create() + let read = false + env.box.fetch = async () => ({ ok: false, status: 404, text: async () => { + read = true + return vttText + } }) + await assert.rejects(env.factory({})(env.art), /404/) + assert.equal(read, false) + assert.equal(count(env), 0) +}) + +test('VTT candidate cleans up a listener installed by an on implementation that throws', async () => { + const env = create() + const original = env.art.on + const failure = new Error('partial listener registration') + env.art.on = function (name, callback) { + original.call(this, name, callback) + if (name === 'setBar') + throw failure + return this + } + await assert.rejects(env.factory({})(env.art), error => error === failure) + assert.equal(count(env), 0) + assert.deepEqual(env.removed, ['vtt-thumbnail']) +}) + +test('VTT candidate does not install when style access destroys the player', async () => { + const env = create() + await env.factory({ get style() { + env.emit('destroy') + return {} + } })(env.art) + assert.equal(env.controls.length, 0) + assert.equal(count(env), 0) +}) + +test('VTT candidate stops subsequent style writes and scheduling after reentrant destroy', async () => { + const env = create({ mobile: true }) + await env.factory({})(env.art) + const writes = [] + env.art.constructor.utils.setStyle = () => { + throw new Error('Captured utility should remain unchanged') + } + Object.defineProperty(env.styles, 'display', { + get() { return writes.at(-1) }, + set(value) { + writes.push(value) + env.emit('destroy') + }, + configurable: true, + }) + await env.hover(0.3, 'played', {}) + assert.deepEqual(Object.keys(env.styles), []) + assert.deepEqual(writes, ['flex']) + assert.equal(env.timers.size, 0) + assert.equal(count(env), 0) +}) + +test('VTT candidate continues other cleanup when a timer cleanup throws', async () => { + const env = create({ mobile: true }) + await env.factory({})(env.art) + await env.hover(0.3, 'played', {}) + env.box.clearTimeout = () => { + throw new Error('timer cleanup failed') + } + await env.emit('destroy') + assert.equal(count(env), 0) + assert.deepEqual(env.removed, ['vtt-thumbnail']) + assert.equal(env.warnings.length, 1) +}) + +test('VTT candidate starts no work on an already destroyed player', async () => { + const env = create() + env.art.isDestroy = true + assert.equal((await env.factory({})(env.art)).name, 'artplayerPluginVttThumbnail') + assert.equal(env.requests.length, 0) + assert.equal(count(env), 0) +}) + +test('VTT candidate removes a partially installed destroy listener before rejecting', async () => { + const env = create() + const original = env.art.on + const error = new Error('destroy registration failed') + env.art.on = function (name, callback) { + original.call(this, name, callback) + throw error + } + await assert.rejects(env.factory({})(env.art), value => value === error) + assert.equal(count(env), 0) + assert.equal(env.requests.length, 0) +}) + +test('VTT candidate invalidates replaced mobile callbacks and preserves the 500ms delay', async () => { + const env = create({ mobile: true }) + await env.factory({})(env.art) + await env.hover(0.3, 'played', {}) + const old = env.timers.get(0) + await env.hover(0.7, 'played', {}) + assert.equal(env.timers.size, 1) + old.callback() + assert.equal(env.styles.display, 'flex') + const latest = [...env.timers.values()][0] + assert.equal(latest.delay, 500) + latest.callback() + assert.equal(env.styles.display, 'none') +}) + +test('VTT candidate cleans up timer handles returned after reentrant destruction', async () => { + const env = create({ mobile: true }) + await env.factory({})(env.art) + const schedule = env.box.setTimeout + env.box.setTimeout = (...args) => { + env.emit('destroy') + return schedule(...args) + } + await env.hover(0.3, 'played', {}) + assert.equal(env.timers.size, 0) + assert.equal(count(env), 0) +}) + +test('VTT candidate keeps registrations isolated and does not reload VTT on restart', async () => { + const left = create({ mobile: true }) + const right = create({ mobile: true }) + await left.factory({ vtt: '/left/cues.vtt' })(left.art) + const option = { vtt: '/right/cues.vtt' } + await right.factory(option)(right.art) + await right.hover(0.3, 'played', {}) + await left.emit('destroy') + option.vtt = '/replacement.vtt' + await right.emit('restart') + await right.hover(0.7) + assert.equal(right.requests.length, 1) + assert.equal(right.styles.backgroundImage, 'url(/right/second.jpg)') + assert.equal(right.timers.size, 1) + assert.equal(right.removed.length, 0) + await right.emit('destroy') + assert.equal(right.timers.size, 0) +})