mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(vtt-thumbnail): [PKG-VTT-THUMB-03] validate cues and separate pure lookup
This commit is contained in:
1 parent
524ddf784f
commit
80588a3046
19 files changed
+3049
-169
No files matched your search
@@ -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(":")||[],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&&h<n.clientWidth?function(t,e,o){c(t,"backgroundImage",`url(${e.url})`),c(t,"height",`${e.h}px`),c(t,"width",`${e.w}px`),c(t,"backgroundPosition",`-${e.x}px -${e.y}px`),o<=e.w/2?c(t,"left",0):o>n.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<o.length;i+=2){const e=o[i],r=o[i+1];if(!r.trim())continue;const s=/((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\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;t<m.length;t++)p[m[t]]=y[t];l.push(p)}return l}(d,a),h=e({lifetime:c,thumbnails:f,progress:s,duration:()=>o.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}}}});
|
||||
!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,e){throw new TypeError(`Invalid VTT thumbnail at line ${t}: ${e}`)}function e(e,n){/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(e)||t(n,"invalid timestamp");const[i,o="0"]=e.split("."),r=i.split(":").reduce((t,e)=>60*t+Number(e),0)+Number(o)/1e3;return Number.isSafeInteger(Math.floor(r))||t(n,"timestamp exceeds the supported numeric range"),r}function n(e,n,i){const o=e.match(/^(.+)#([xywh]{4})=(.*)$/);o&&4===new Set(o[2]).size||t(i,"expected an image URL and four distinct xywh keys");const r=o[3].split(",").map(t=>t.trim());4!==r.length&&t(i,"expected four rectangle coordinates");const s={};for(let c=0;c<4;c++){const e=o[2][c],n=r[c];/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(n)&&Number.isFinite(Number(n))||t(i,"rectangle coordinates must be finite non-negative decimals"),"w"!==e&&"h"!==e||0!==Number(n)||t(i,"rectangle width and height must be positive"),s[e]=n}let l=o[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(l)){const t=n.split("/");t.pop(),t.push(l),l=t.join("/")}return{url:l,...s}}function i({lifetime:t,thumbnails:e,progress:n,duration:i,setStyle:o,isMobile:r}){let s=null,l=0;function c(e,n,i){t.closed||o(e,n,i)}return t.own(()=>{if(l++,null!==s){const t=s;s=null,clearTimeout(t)}}),o=>async(u,a,d)=>{if(t.closed)return;const f="played"===u&&d&&r;if("hover"!==u&&!f)return;const m=n.clientWidth*a,h=a*i();if(c(o,"display","flex"),t.closed)return;const p=function(t,e){return t.find(t=>e>=t.start&&e<=t.end)}(e,h);if(!p)return c(o,"display","none");if(m>0&&m<n.clientWidth?function(t,e,i){c(t,"backgroundImage",`url(${e.url})`),c(t,"height",`${e.h}px`),c(t,"width",`${e.w}px`),c(t,"backgroundPosition",`-${e.x}px -${e.y}px`),i<=e.w/2?c(t,"left",0):i>n.clientWidth-e.w/2?c(t,"left",n.clientWidth-e.w+"px"):c(t,"left",i-e.w/2+"px")}(o,p,m):r||c(o,"display","none"),f&&!t.closed){const e=++l;if(null!==s&&clearTimeout(s),t.closed)return;const n=setTimeout(()=>{e!==l||t.closed||(s=null,c(o,"display","none"))},500);t.closed||e!==l?clearTimeout(n):s=n}}}return function(o){return async r=>{const{constructor:{utils:{setStyle:s,isMobile:l,addClass:c}},template:{$progress:u}}=r,a=function(t){let e=Boolean(t.isDestroy);const n=new Set;let i;const o=new Promise(t=>{i=t}),r={get closed(){return e},own:t=>(e?s(t):n.add(t),()=>n.delete(t)),listen(n,i){e||(r.own(()=>t.off(n,i)),t.on(n,i))},wait:t=>Promise.race([t,o]),dispose(){if(e)return;e=!0,i();const t=[...n].reverse();n.clear();for(const e of t)s(e)}};function s(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{r.listen("destroy",r.dispose)}catch(l){throw r.dispose(),l}return r}(r),d={name:"artplayerPluginVttThumbnail"};try{if(a.closed)return d;const f=o.vtt,m=await async function(t="",e){if(e.closed)return;const n="function"===typeof AbortController?new AbortController:null,i=e.own(()=>n?.abort());try{if(e.closed)return;const i=await e.wait(n?fetch(t,{signal:n.signal}):fetch(t));if(e.closed)return;if(!1===i.ok)throw new Error(`Failed to fetch VTT thumbnails: HTTP ${i.status}`);if(e.closed)return;return await e.wait(i.text())}finally{i()}}(f,a);if(a.closed)return d;const h=function(i,o=""){"string"!==typeof i&&t(1,"expected text");const r=i.replace(/^\uFEFF/,"").split(/\r\n|\r|\n/);let s=0;function l(){for(;s<r.length&&!r[s].trim();)s++}if(l(),s===r.length)return[];/^WEBVTT(?:[\t ].*)?$/.test(r[s].trim())&&!r[s].includes("--\x3e")||t(s+1,"expected WEBVTT header"),s++;const c=[];for(;s<r.length&&(l(),s!==r.length);){const i=r[s].trim();if(/^NOTE(?:[\t ]|$)/.test(i)||"STYLE"===i||"REGION"===i){for(;s<r.length&&r[s].trim();)s++;continue}const u=s+1;i.includes("--\x3e")||(s++,s!==r.length&&r[s].trim()||t(u,"cue identifier must be followed by timing"));const a=s+1,d=r[s].trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/);d||t(a,"invalid cue timing");const f=e(d[1],a),m=e(d[2],a);m<f&&t(a,"cue end precedes its start"),s++,l(),s===r.length&&t(a,"missing sprite image");const h=n(r[s].trim(),o,s+1);c.push({start:Math.floor(f),end:Math.floor(m),...h}),s++}return c}(m,f),p=i({lifetime:a,thumbnails:h,progress:u,duration:()=>r.duration,setStyle:s,isMobile:l}),g=o.style||{};return a.closed||r.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:g,mounted(t){a.own(()=>{r.controls["vtt-thumbnail"]===t&&r.controls.remove("vtt-thumbnail")}),a.closed||(c(t,"art-control-thumbnails"),a.listen("setBar",p(t)))}}),d}catch(f){throw a.dispose(),f}}}});
|
||||
@@ -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((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&&h<o.clientWidth?function(t,e,n){c(t,"backgroundImage",`url(${e.url})`),c(t,"height",`${e.h}px`),c(t,"width",`${e.w}px`),c(t,"backgroundPosition",`-${e.x}px -${e.y}px`),n<=e.w/2?c(t,"left",0):n>o.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<o.length;i+=2){const t=o[i],r=o[i+1];if(!r.trim())continue;const s=/((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\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;e<m.length;e++)p[m[e]]=y[e];l.push(p)}return l}(f,d),p=n({lifetime:c,thumbnails:h,progress:u,duration:()=>l.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}})}});
|
||||
!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=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,i=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,r=(t,r)=>{for(var l in r||(r={}))n.call(r,l)&&i(t,l,r[l]);if(e)for(var l of e(r))o.call(r,l)&&i(t,l,r[l]);return t},l=(t,e,n)=>new Promise((o,i)=>{var r=t=>{try{s(n.next(t))}catch(e){i(e)}},l=t=>{try{s(n.throw(t))}catch(e){i(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(r,l);s((n=n.apply(t,e)).next())});function s(t,e){throw new TypeError(`Invalid VTT thumbnail at line ${t}: ${e}`)}function c(t,e){/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(t)||s(e,"invalid timestamp");const[n,o="0"]=t.split("."),i=n.split(":").reduce((t,e)=>60*t+Number(e),0)+Number(o)/1e3;return Number.isSafeInteger(Math.floor(i))||s(e,"timestamp exceeds the supported numeric range"),i}function u(t,e,n){const o=t.match(/^(.+)#([xywh]{4})=(.*)$/);o&&4===new Set(o[2]).size||s(n,"expected an image URL and four distinct xywh keys");const i=o[3].split(",").map(t=>t.trim());4!==i.length&&s(n,"expected four rectangle coordinates");const l={};for(let r=0;r<4;r++){const t=o[2][r],e=i[r];/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(e)&&Number.isFinite(Number(e))||s(n,"rectangle coordinates must be finite non-negative decimals"),"w"!==t&&"h"!==t||0!==Number(e)||s(n,"rectangle width and height must be positive"),l[t]=e}let c=o[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(c)){const t=e.split("/");t.pop(),t.push(c),c=t.join("/")}return r({url:c},l)}function a({lifetime:t,thumbnails:e,progress:n,duration:o,setStyle:i,isMobile:r}){let s=null,c=0;function u(e,n,o){t.closed||i(e,n,o)}return t.own(()=>{if(c++,null!==s){const t=s;s=null,clearTimeout(t)}}),i=>(a,d,f)=>l(null,null,function*(){if(t.closed)return;const l="played"===a&&f&&r;if("hover"!==a&&!l)return;const p=n.clientWidth*d,m=d*o();if(u(i,"display","flex"),t.closed)return;const h=function(t,e){return t.find(t=>e>=t.start&&e<=t.end)}(e,m);if(!h)return u(i,"display","none");if(p>0&&p<n.clientWidth?function(t,e,o){u(t,"backgroundImage",`url(${e.url})`),u(t,"height",`${e.h}px`),u(t,"width",`${e.w}px`),u(t,"backgroundPosition",`-${e.x}px -${e.y}px`),o<=e.w/2?u(t,"left",0):o>n.clientWidth-e.w/2?u(t,"left",n.clientWidth-e.w+"px"):u(t,"left",o-e.w/2+"px")}(i,h,p):r||u(i,"display","none"),l&&!t.closed){const e=++c;if(null!==s&&clearTimeout(s),t.closed)return;const n=setTimeout(()=>{e!==c||t.closed||(s=null,u(i,"display","none"))},500);t.closed||e!==c?clearTimeout(n):s=n}})}return function(t){return e=>l(null,null,function*(){const{constructor:{utils:{setStyle:n,isMobile:o,addClass:i}},template:{$progress:d}}=e,f=function(t){let e=Boolean(t.isDestroy);const n=new Set;let o;const i=new Promise(t=>{o=t}),r={get closed(){return e},own:t=>(e?l(t):n.add(t),()=>n.delete(t)),listen(n,o){e||(r.own(()=>t.off(n,o)),t.on(n,o))},wait:t=>Promise.race([t,i]),dispose(){if(e)return;e=!0,o();const t=[...n].reverse();n.clear();for(const e of t)l(e)}};function l(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{r.listen("destroy",r.dispose)}catch(s){throw r.dispose(),s}return r}(e),p={name:"artplayerPluginVttThumbnail"};try{if(f.closed)return p;const m=t.vtt,h=yield function(t="",e){return l(this,null,function*(){if(e.closed)return;const n="function"===typeof AbortController?new AbortController:null,o=e.own(()=>null==n?void 0:n.abort());try{if(e.closed)return;const o=yield 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 yield e.wait(o.text())}finally{o()}})}(m,f);if(f.closed)return p;const y=function(t,e=""){"string"!==typeof t&&s(1,"expected text");const n=t.replace(/^\uFEFF/,"").split(/\r\n|\r|\n/);let o=0;function i(){for(;o<n.length&&!n[o].trim();)o++}if(i(),o===n.length)return[];/^WEBVTT(?:[\t ].*)?$/.test(n[o].trim())&&!n[o].includes("--\x3e")||s(o+1,"expected WEBVTT header"),o++;const l=[];for(;o<n.length&&(i(),o!==n.length);){const t=n[o].trim();if(/^NOTE(?:[\t ]|$)/.test(t)||"STYLE"===t||"REGION"===t){for(;o<n.length&&n[o].trim();)o++;continue}const a=o+1;t.includes("--\x3e")||(o++,o!==n.length&&n[o].trim()||s(a,"cue identifier must be followed by timing"));const d=o+1,f=n[o].trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/);f||s(d,"invalid cue timing");const p=c(f[1],d),m=c(f[2],d);m<p&&s(d,"cue end precedes its start"),o++,i(),o===n.length&&s(d,"missing sprite image");const h=u(n[o].trim(),e,o+1);l.push(r({start:Math.floor(p),end:Math.floor(m)},h)),o++}return l}(h,m),b=a({lifetime:f,thumbnails:y,progress:d,duraLine truncated
|
||||
@@ -57,58 +57,96 @@ function createLifetime(art) {
|
||||
}
|
||||
return lifetime;
|
||||
}
|
||||
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 invalid(line, message) {
|
||||
throw new TypeError(`Invalid VTT thumbnail at line ${line}: ${message}`);
|
||||
}
|
||||
function timestamp(value, line) {
|
||||
if (!/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(value))
|
||||
invalid(line, "invalid timestamp");
|
||||
const [whole, fraction = "0"] = value.split(".");
|
||||
const seconds = whole.split(":").reduce((total, part) => total * 60 + Number(part), 0) + Number(fraction) / 1e3;
|
||||
if (!Number.isSafeInteger(Math.floor(seconds)))
|
||||
invalid(line, "timestamp exceeds the supported numeric range");
|
||||
return seconds;
|
||||
}
|
||||
function rectangle(text, vttUrl, line) {
|
||||
const match = text.match(/^(.+)#([xywh]{4})=(.*)$/);
|
||||
if (!match || new Set(match[2]).size !== 4)
|
||||
invalid(line, "expected an image URL and four distinct xywh keys");
|
||||
const values = match[3].split(",").map((value) => value.trim());
|
||||
if (values.length !== 4)
|
||||
invalid(line, "expected four rectangle coordinates");
|
||||
const rect = {};
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const key = match[2][index];
|
||||
const value = values[index];
|
||||
if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(value) || !Number.isFinite(Number(value)))
|
||||
invalid(line, "rectangle coordinates must be finite non-negative decimals");
|
||||
if ((key === "w" || key === "h") && Number(value) === 0)
|
||||
invalid(line, "rectangle width and height must be positive");
|
||||
rect[key] = value;
|
||||
}
|
||||
let url = match[1];
|
||||
if (!/^\/|(?:https?|ftp|file):\/\//i.test(url)) {
|
||||
const segments = vttUrl.split("/");
|
||||
segments.pop();
|
||||
segments.push(url);
|
||||
url = segments.join("/");
|
||||
}
|
||||
return { url, ...rect };
|
||||
}
|
||||
function t2d(time) {
|
||||
const arr = time.split(".");
|
||||
const left = arr[0].split(":") || [];
|
||||
const right = padEnd(arr[1] || "0", 3, "0");
|
||||
const ms = Number(right) / 1e3;
|
||||
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;
|
||||
function findThumbnail(thumbnails, second) {
|
||||
return thumbnails.find((item) => second >= item.start && second <= item.end);
|
||||
}
|
||||
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())
|
||||
function parseVtt(text, vttUrl = "") {
|
||||
if (typeof text !== "string")
|
||||
invalid(1, "expected text");
|
||||
const lines = text.replace(/^\uFEFF/, "").split(/\r\n|\r|\n/);
|
||||
let index = 0;
|
||||
function skipEmpty() {
|
||||
while (index < lines.length && !lines[index].trim())
|
||||
index++;
|
||||
}
|
||||
skipEmpty();
|
||||
if (index === lines.length)
|
||||
return [];
|
||||
if (!/^WEBVTT(?:[\t ].*)?$/.test(lines[index].trim()) || lines[index].includes("-->"))
|
||||
invalid(index + 1, "expected WEBVTT header");
|
||||
index++;
|
||||
const thumbnails = [];
|
||||
while (index < lines.length) {
|
||||
skipEmpty();
|
||||
if (index === lines.length)
|
||||
break;
|
||||
const first = lines[index].trim();
|
||||
if (/^NOTE(?:[\t ]|$)/.test(first) || first === "STYLE" || first === "REGION") {
|
||||
while (index < lines.length && lines[index].trim())
|
||||
index++;
|
||||
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];
|
||||
const identifierLine = index + 1;
|
||||
if (!first.includes("-->")) {
|
||||
index++;
|
||||
if (index === lines.length || !lines[index].trim())
|
||||
invalid(identifierLine, "cue identifier must be followed by timing");
|
||||
}
|
||||
vttArray.push(result);
|
||||
const timingLine = index + 1;
|
||||
const timing = lines[index].trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/);
|
||||
if (!timing)
|
||||
invalid(timingLine, "invalid cue timing");
|
||||
const start = timestamp(timing[1], timingLine);
|
||||
const end = timestamp(timing[2], timingLine);
|
||||
if (end < start)
|
||||
invalid(timingLine, "cue end precedes its start");
|
||||
index++;
|
||||
skipEmpty();
|
||||
if (index === lines.length)
|
||||
invalid(timingLine, "missing sprite image");
|
||||
const image = rectangle(lines[index].trim(), vttUrl, index + 1);
|
||||
thumbnails.push({ start: Math.floor(start), end: Math.floor(end), ...image });
|
||||
index++;
|
||||
}
|
||||
return vttArray;
|
||||
return thumbnails;
|
||||
}
|
||||
function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isMobile }) {
|
||||
let timer = null;
|
||||
@@ -148,7 +186,7 @@ function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isM
|
||||
style(control, "display", "flex");
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const cue = thumbnails.find((item) => second >= item.start && second <= item.end);
|
||||
const cue = findThumbnail(thumbnails, second);
|
||||
if (!cue)
|
||||
return style(control, "display", "none");
|
||||
if (width > 0 && width < progress.clientWidth)
|
||||
|
||||
+2
-2
@@ -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-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:unit": "node --test test/vtt-thumbnail-parser.test.js 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-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs"
|
||||
"test:vtt-thumbnail": "node --test test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -15,9 +15,10 @@ and the strict TypeScript migration. Do not infer the return shape from those de
|
||||
- `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/parseVtt.js` parses thumbnail metadata and exports the pure `findThumbnail` lookup.
|
||||
It preserves timestamp flooring, first inclusive matches, decimal xywh strings and lexical
|
||||
URL joining. It handles cue IDs, NOTE/STYLE/REGION blocks, BOM/line endings and timing
|
||||
settings; invalid input raises TypeError with the original line before any UI is installed.
|
||||
- `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
|
||||
@@ -43,6 +44,20 @@ later option.vtt changes. Empty cues remain valid and hide the preview. HTTP fai
|
||||
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.
|
||||
|
||||
This is a thumbnail metadata profile, not a complete subtitle renderer or WebVTT validator.
|
||||
Caption style/region blocks and timing settings do not style the player's DOM. Preserve
|
||||
legacy compact arrows, seconds-only timestamps, numeric overflow between minute/second
|
||||
fields, zero-length cues, unsorted/overlapping cues and dense pairs. Lookup does not sort:
|
||||
the first inclusive match in input order wins. Fractions still floor only after conversion.
|
||||
Three-or-more-digit hours now use the entire field rather than silently truncating it.
|
||||
|
||||
Malformed headers/timing, reversed ranges and missing/duplicate/unknown rectangle keys
|
||||
reject. Coordinates must be finite non-negative decimal strings, and width/height positive;
|
||||
coordinate whitespace is trimmed before forming CSS. Broken input no longer creates
|
||||
invalid CSS or partially mounted controls. Each cue has one image/rectangle payload line;
|
||||
multiline subtitle text, timestamp-map offsets and full caption semantics are outside this
|
||||
plugin. Empty or comment-only input remains a valid empty preview.
|
||||
|
||||
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.
|
||||
@@ -65,5 +80,9 @@ 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.
|
||||
Pure parser comparison can use ARTPLAYER_VTT_PARSER_BASELINE=1, which loads the exact
|
||||
parser from the 524ddf78 resource checkpoint. Bundle-mode parser tests also run malformed
|
||||
payloads and extended registration through the actual factory; pure parsing assertions
|
||||
still target source and must not be described as tests of an installed tarball.
|
||||
See ../../refactor/baselines/vtt-thumbnail-contract.md and the task 03 change record for
|
||||
historical evidence and outstanding acceptance work.
|
||||
+1
-1
@@ -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(":")||[],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&&h<n.clientWidth?function(t,e,o){c(t,"backgroundImage",`url(${e.url})`),c(t,"height",`${e.h}px`),c(t,"width",`${e.w}px`),c(t,"backgroundPosition",`-${e.x}px -${e.y}px`),o<=e.w/2?c(t,"left",0):o>n.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<o.length;i+=2){const e=o[i],r=o[i+1];if(!r.trim())continue;const s=/((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\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;t<m.length;t++)p[m[t]]=y[t];l.push(p)}return l}(d,a),h=e({lifetime:c,thumbnails:f,progress:s,duration:()=>o.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}}}});
|
||||
!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,e){throw new TypeError(`Invalid VTT thumbnail at line ${t}: ${e}`)}function e(e,n){/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(e)||t(n,"invalid timestamp");const[i,o="0"]=e.split("."),r=i.split(":").reduce((t,e)=>60*t+Number(e),0)+Number(o)/1e3;return Number.isSafeInteger(Math.floor(r))||t(n,"timestamp exceeds the supported numeric range"),r}function n(e,n,i){const o=e.match(/^(.+)#([xywh]{4})=(.*)$/);o&&4===new Set(o[2]).size||t(i,"expected an image URL and four distinct xywh keys");const r=o[3].split(",").map(t=>t.trim());4!==r.length&&t(i,"expected four rectangle coordinates");const s={};for(let c=0;c<4;c++){const e=o[2][c],n=r[c];/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(n)&&Number.isFinite(Number(n))||t(i,"rectangle coordinates must be finite non-negative decimals"),"w"!==e&&"h"!==e||0!==Number(n)||t(i,"rectangle width and height must be positive"),s[e]=n}let l=o[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(l)){const t=n.split("/");t.pop(),t.push(l),l=t.join("/")}return{url:l,...s}}function i({lifetime:t,thumbnails:e,progress:n,duration:i,setStyle:o,isMobile:r}){let s=null,l=0;function c(e,n,i){t.closed||o(e,n,i)}return t.own(()=>{if(l++,null!==s){const t=s;s=null,clearTimeout(t)}}),o=>async(u,a,d)=>{if(t.closed)return;const f="played"===u&&d&&r;if("hover"!==u&&!f)return;const m=n.clientWidth*a,h=a*i();if(c(o,"display","flex"),t.closed)return;const p=function(t,e){return t.find(t=>e>=t.start&&e<=t.end)}(e,h);if(!p)return c(o,"display","none");if(m>0&&m<n.clientWidth?function(t,e,i){c(t,"backgroundImage",`url(${e.url})`),c(t,"height",`${e.h}px`),c(t,"width",`${e.w}px`),c(t,"backgroundPosition",`-${e.x}px -${e.y}px`),i<=e.w/2?c(t,"left",0):i>n.clientWidth-e.w/2?c(t,"left",n.clientWidth-e.w+"px"):c(t,"left",i-e.w/2+"px")}(o,p,m):r||c(o,"display","none"),f&&!t.closed){const e=++l;if(null!==s&&clearTimeout(s),t.closed)return;const n=setTimeout(()=>{e!==l||t.closed||(s=null,c(o,"display","none"))},500);t.closed||e!==l?clearTimeout(n):s=n}}}return function(o){return async r=>{const{constructor:{utils:{setStyle:s,isMobile:l,addClass:c}},template:{$progress:u}}=r,a=function(t){let e=Boolean(t.isDestroy);const n=new Set;let i;const o=new Promise(t=>{i=t}),r={get closed(){return e},own:t=>(e?s(t):n.add(t),()=>n.delete(t)),listen(n,i){e||(r.own(()=>t.off(n,i)),t.on(n,i))},wait:t=>Promise.race([t,o]),dispose(){if(e)return;e=!0,i();const t=[...n].reverse();n.clear();for(const e of t)s(e)}};function s(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{r.listen("destroy",r.dispose)}catch(l){throw r.dispose(),l}return r}(r),d={name:"artplayerPluginVttThumbnail"};try{if(a.closed)return d;const f=o.vtt,m=await async function(t="",e){if(e.closed)return;const n="function"===typeof AbortController?new AbortController:null,i=e.own(()=>n?.abort());try{if(e.closed)return;const i=await e.wait(n?fetch(t,{signal:n.signal}):fetch(t));if(e.closed)return;if(!1===i.ok)throw new Error(`Failed to fetch VTT thumbnails: HTTP ${i.status}`);if(e.closed)return;return await e.wait(i.text())}finally{i()}}(f,a);if(a.closed)return d;const h=function(i,o=""){"string"!==typeof i&&t(1,"expected text");const r=i.replace(/^\uFEFF/,"").split(/\r\n|\r|\n/);let s=0;function l(){for(;s<r.length&&!r[s].trim();)s++}if(l(),s===r.length)return[];/^WEBVTT(?:[\t ].*)?$/.test(r[s].trim())&&!r[s].includes("--\x3e")||t(s+1,"expected WEBVTT header"),s++;const c=[];for(;s<r.length&&(l(),s!==r.length);){const i=r[s].trim();if(/^NOTE(?:[\t ]|$)/.test(i)||"STYLE"===i||"REGION"===i){for(;s<r.length&&r[s].trim();)s++;continue}const u=s+1;i.includes("--\x3e")||(s++,s!==r.length&&r[s].trim()||t(u,"cue identifier must be followed by timing"));const a=s+1,d=r[s].trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/);d||t(a,"invalid cue timing");const f=e(d[1],a),m=e(d[2],a);m<f&&t(a,"cue end precedes its start"),s++,l(),s===r.length&&t(a,"missing sprite image");const h=n(r[s].trim(),o,s+1);c.push({start:Math.floor(f),end:Math.floor(m),...h}),s++}return c}(m,f),p=i({lifetime:a,thumbnails:h,progress:u,duration:()=>r.duration,setStyle:s,isMobile:l}),g=o.style||{};return a.closed||r.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:g,mounted(t){a.own(()=>{r.controls["vtt-thumbnail"]===t&&r.controls.remove("vtt-thumbnail")}),a.closed||(c(t,"art-control-thumbnails"),a.listen("setBar",p(t)))}}),d}catch(f){throw a.dispose(),f}}}});
|
||||
+1
-1
@@ -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((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&&h<o.clientWidth?function(t,e,n){c(t,"backgroundImage",`url(${e.url})`),c(t,"height",`${e.h}px`),c(t,"width",`${e.w}px`),c(t,"backgroundPosition",`-${e.x}px -${e.y}px`),n<=e.w/2?c(t,"left",0):n>o.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<o.length;i+=2){const t=o[i],r=o[i+1];if(!r.trim())continue;const s=/((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\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;e<m.length;e++)p[m[e]]=y[e];l.push(p)}return l}(f,d),p=n({lifetime:c,thumbnails:h,progress:u,duration:()=>l.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}})}});
|
||||
!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=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,i=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,r=(t,r)=>{for(var l in r||(r={}))n.call(r,l)&&i(t,l,r[l]);if(e)for(var l of e(r))o.call(r,l)&&i(t,l,r[l]);return t},l=(t,e,n)=>new Promise((o,i)=>{var r=t=>{try{s(n.next(t))}catch(e){i(e)}},l=t=>{try{s(n.throw(t))}catch(e){i(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(r,l);s((n=n.apply(t,e)).next())});function s(t,e){throw new TypeError(`Invalid VTT thumbnail at line ${t}: ${e}`)}function c(t,e){/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(t)||s(e,"invalid timestamp");const[n,o="0"]=t.split("."),i=n.split(":").reduce((t,e)=>60*t+Number(e),0)+Number(o)/1e3;return Number.isSafeInteger(Math.floor(i))||s(e,"timestamp exceeds the supported numeric range"),i}function u(t,e,n){const o=t.match(/^(.+)#([xywh]{4})=(.*)$/);o&&4===new Set(o[2]).size||s(n,"expected an image URL and four distinct xywh keys");const i=o[3].split(",").map(t=>t.trim());4!==i.length&&s(n,"expected four rectangle coordinates");const l={};for(let r=0;r<4;r++){const t=o[2][r],e=i[r];/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(e)&&Number.isFinite(Number(e))||s(n,"rectangle coordinates must be finite non-negative decimals"),"w"!==t&&"h"!==t||0!==Number(e)||s(n,"rectangle width and height must be positive"),l[t]=e}let c=o[1];if(!/^\/|(?:https?|ftp|file):\/\//i.test(c)){const t=e.split("/");t.pop(),t.push(c),c=t.join("/")}return r({url:c},l)}function a({lifetime:t,thumbnails:e,progress:n,duration:o,setStyle:i,isMobile:r}){let s=null,c=0;function u(e,n,o){t.closed||i(e,n,o)}return t.own(()=>{if(c++,null!==s){const t=s;s=null,clearTimeout(t)}}),i=>(a,d,f)=>l(null,null,function*(){if(t.closed)return;const l="played"===a&&f&&r;if("hover"!==a&&!l)return;const p=n.clientWidth*d,m=d*o();if(u(i,"display","flex"),t.closed)return;const h=function(t,e){return t.find(t=>e>=t.start&&e<=t.end)}(e,m);if(!h)return u(i,"display","none");if(p>0&&p<n.clientWidth?function(t,e,o){u(t,"backgroundImage",`url(${e.url})`),u(t,"height",`${e.h}px`),u(t,"width",`${e.w}px`),u(t,"backgroundPosition",`-${e.x}px -${e.y}px`),o<=e.w/2?u(t,"left",0):o>n.clientWidth-e.w/2?u(t,"left",n.clientWidth-e.w+"px"):u(t,"left",o-e.w/2+"px")}(i,h,p):r||u(i,"display","none"),l&&!t.closed){const e=++c;if(null!==s&&clearTimeout(s),t.closed)return;const n=setTimeout(()=>{e!==c||t.closed||(s=null,u(i,"display","none"))},500);t.closed||e!==c?clearTimeout(n):s=n}})}return function(t){return e=>l(null,null,function*(){const{constructor:{utils:{setStyle:n,isMobile:o,addClass:i}},template:{$progress:d}}=e,f=function(t){let e=Boolean(t.isDestroy);const n=new Set;let o;const i=new Promise(t=>{o=t}),r={get closed(){return e},own:t=>(e?l(t):n.add(t),()=>n.delete(t)),listen(n,o){e||(r.own(()=>t.off(n,o)),t.on(n,o))},wait:t=>Promise.race([t,i]),dispose(){if(e)return;e=!0,o();const t=[...n].reverse();n.clear();for(const e of t)l(e)}};function l(t){try{t()}catch(e){console.warn("Failed to clean up VTT thumbnails:",e)}}try{r.listen("destroy",r.dispose)}catch(s){throw r.dispose(),s}return r}(e),p={name:"artplayerPluginVttThumbnail"};try{if(f.closed)return p;const m=t.vtt,h=yield function(t="",e){return l(this,null,function*(){if(e.closed)return;const n="function"===typeof AbortController?new AbortController:null,o=e.own(()=>null==n?void 0:n.abort());try{if(e.closed)return;const o=yield 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 yield e.wait(o.text())}finally{o()}})}(m,f);if(f.closed)return p;const y=function(t,e=""){"string"!==typeof t&&s(1,"expected text");const n=t.replace(/^\uFEFF/,"").split(/\r\n|\r|\n/);let o=0;function i(){for(;o<n.length&&!n[o].trim();)o++}if(i(),o===n.length)return[];/^WEBVTT(?:[\t ].*)?$/.test(n[o].trim())&&!n[o].includes("--\x3e")||s(o+1,"expected WEBVTT header"),o++;const l=[];for(;o<n.length&&(i(),o!==n.length);){const t=n[o].trim();if(/^NOTE(?:[\t ]|$)/.test(t)||"STYLE"===t||"REGION"===t){for(;o<n.length&&n[o].trim();)o++;continue}const a=o+1;t.includes("--\x3e")||(o++,o!==n.length&&n[o].trim()||s(a,"cue identifier must be followed by timing"));const d=o+1,f=n[o].trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/);f||s(d,"invalid cue timing");const p=c(f[1],d),m=c(f[2],d);m<p&&s(d,"cue end precedes its start"),o++,i(),o===n.length&&s(d,"missing sprite image");const h=u(n[o].trim(),e,o+1);l.push(r({start:Math.floor(p),end:Math.floor(m)},h)),o++}return l}(h,m),b=a({lifetime:f,thumbnails:y,progress:d,duraLine truncated
|
||||
+84
-46
@@ -57,58 +57,96 @@ function createLifetime(art) {
|
||||
}
|
||||
return lifetime;
|
||||
}
|
||||
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 invalid(line, message) {
|
||||
throw new TypeError(`Invalid VTT thumbnail at line ${line}: ${message}`);
|
||||
}
|
||||
function timestamp(value, line) {
|
||||
if (!/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(value))
|
||||
invalid(line, "invalid timestamp");
|
||||
const [whole, fraction = "0"] = value.split(".");
|
||||
const seconds = whole.split(":").reduce((total, part) => total * 60 + Number(part), 0) + Number(fraction) / 1e3;
|
||||
if (!Number.isSafeInteger(Math.floor(seconds)))
|
||||
invalid(line, "timestamp exceeds the supported numeric range");
|
||||
return seconds;
|
||||
}
|
||||
function rectangle(text, vttUrl, line) {
|
||||
const match = text.match(/^(.+)#([xywh]{4})=(.*)$/);
|
||||
if (!match || new Set(match[2]).size !== 4)
|
||||
invalid(line, "expected an image URL and four distinct xywh keys");
|
||||
const values = match[3].split(",").map((value) => value.trim());
|
||||
if (values.length !== 4)
|
||||
invalid(line, "expected four rectangle coordinates");
|
||||
const rect = {};
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const key = match[2][index];
|
||||
const value = values[index];
|
||||
if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(value) || !Number.isFinite(Number(value)))
|
||||
invalid(line, "rectangle coordinates must be finite non-negative decimals");
|
||||
if ((key === "w" || key === "h") && Number(value) === 0)
|
||||
invalid(line, "rectangle width and height must be positive");
|
||||
rect[key] = value;
|
||||
}
|
||||
let url = match[1];
|
||||
if (!/^\/|(?:https?|ftp|file):\/\//i.test(url)) {
|
||||
const segments = vttUrl.split("/");
|
||||
segments.pop();
|
||||
segments.push(url);
|
||||
url = segments.join("/");
|
||||
}
|
||||
return { url, ...rect };
|
||||
}
|
||||
function t2d(time) {
|
||||
const arr = time.split(".");
|
||||
const left = arr[0].split(":") || [];
|
||||
const right = padEnd(arr[1] || "0", 3, "0");
|
||||
const ms = Number(right) / 1e3;
|
||||
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;
|
||||
function findThumbnail(thumbnails, second) {
|
||||
return thumbnails.find((item) => second >= item.start && second <= item.end);
|
||||
}
|
||||
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())
|
||||
function parseVtt(text, vttUrl = "") {
|
||||
if (typeof text !== "string")
|
||||
invalid(1, "expected text");
|
||||
const lines = text.replace(/^\uFEFF/, "").split(/\r\n|\r|\n/);
|
||||
let index = 0;
|
||||
function skipEmpty() {
|
||||
while (index < lines.length && !lines[index].trim())
|
||||
index++;
|
||||
}
|
||||
skipEmpty();
|
||||
if (index === lines.length)
|
||||
return [];
|
||||
if (!/^WEBVTT(?:[\t ].*)?$/.test(lines[index].trim()) || lines[index].includes("-->"))
|
||||
invalid(index + 1, "expected WEBVTT header");
|
||||
index++;
|
||||
const thumbnails = [];
|
||||
while (index < lines.length) {
|
||||
skipEmpty();
|
||||
if (index === lines.length)
|
||||
break;
|
||||
const first = lines[index].trim();
|
||||
if (/^NOTE(?:[\t ]|$)/.test(first) || first === "STYLE" || first === "REGION") {
|
||||
while (index < lines.length && lines[index].trim())
|
||||
index++;
|
||||
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];
|
||||
const identifierLine = index + 1;
|
||||
if (!first.includes("-->")) {
|
||||
index++;
|
||||
if (index === lines.length || !lines[index].trim())
|
||||
invalid(identifierLine, "cue identifier must be followed by timing");
|
||||
}
|
||||
vttArray.push(result);
|
||||
const timingLine = index + 1;
|
||||
const timing = lines[index].trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/);
|
||||
if (!timing)
|
||||
invalid(timingLine, "invalid cue timing");
|
||||
const start = timestamp(timing[1], timingLine);
|
||||
const end = timestamp(timing[2], timingLine);
|
||||
if (end < start)
|
||||
invalid(timingLine, "cue end precedes its start");
|
||||
index++;
|
||||
skipEmpty();
|
||||
if (index === lines.length)
|
||||
invalid(timingLine, "missing sprite image");
|
||||
const image = rectangle(lines[index].trim(), vttUrl, index + 1);
|
||||
thumbnails.push({ start: Math.floor(start), end: Math.floor(end), ...image });
|
||||
index++;
|
||||
}
|
||||
return vttArray;
|
||||
return thumbnails;
|
||||
}
|
||||
function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isMobile }) {
|
||||
let timer = null;
|
||||
@@ -148,7 +186,7 @@ function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isM
|
||||
style(control, "display", "flex");
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const cue = thumbnails.find((item) => second >= item.start && second <= item.end);
|
||||
const cue = findThumbnail(thumbnails, second);
|
||||
if (!cue)
|
||||
return style(control, "display", "none");
|
||||
if (width > 0 && width < progress.clientWidth)
|
||||
|
||||
@@ -1,69 +1,95 @@
|
||||
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 invalid(line, message) {
|
||||
throw new TypeError(`Invalid VTT thumbnail at line ${line}: ${message}`)
|
||||
}
|
||||
|
||||
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
|
||||
function timestamp(value, line) {
|
||||
if (!/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(value))
|
||||
invalid(line, 'invalid timestamp')
|
||||
const [whole, fraction = '0'] = value.split('.')
|
||||
const seconds = whole.split(':').reduce((total, part) => total * 60 + Number(part), 0) + Number(fraction) / 1000
|
||||
if (!Number.isSafeInteger(Math.floor(seconds)))
|
||||
invalid(line, 'timestamp exceeds the supported numeric range')
|
||||
return seconds
|
||||
}
|
||||
|
||||
export default function parseVtt(vttString, vttUrl = '') {
|
||||
const lines = vttString.split(/[\n\r]/g).filter(item => item.trim())
|
||||
const vttArray = []
|
||||
function rectangle(text, vttUrl, line) {
|
||||
const match = text.match(/^(.+)#([xywh]{4})=(.*)$/)
|
||||
if (!match || new Set(match[2]).size !== 4)
|
||||
invalid(line, 'expected an image URL and four distinct xywh keys')
|
||||
const values = match[3].split(',').map(value => value.trim())
|
||||
if (values.length !== 4)
|
||||
invalid(line, 'expected four rectangle coordinates')
|
||||
const rect = {}
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const key = match[2][index]
|
||||
const value = values[index]
|
||||
if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(value) || !Number.isFinite(Number(value)))
|
||||
invalid(line, 'rectangle coordinates must be finite non-negative decimals')
|
||||
if ((key === 'w' || key === 'h') && Number(value) === 0)
|
||||
invalid(line, 'rectangle width and height must be positive')
|
||||
rect[key] = value
|
||||
}
|
||||
let url = match[1]
|
||||
if (!/^\/|(?:https?|ftp|file):\/\//i.test(url)) {
|
||||
const segments = vttUrl.split('/')
|
||||
segments.pop()
|
||||
segments.push(url)
|
||||
url = segments.join('/')
|
||||
}
|
||||
return { url, ...rect }
|
||||
}
|
||||
|
||||
for (let i = 1; i < lines.length; i += 2) {
|
||||
const time = lines[i]
|
||||
const text = lines[i + 1]
|
||||
export function findThumbnail(thumbnails, second) {
|
||||
return thumbnails.find(item => second >= item.start && second <= item.end)
|
||||
}
|
||||
|
||||
if (!text.trim())
|
||||
export default function parseVtt(text, vttUrl = '') {
|
||||
if (typeof text !== 'string')
|
||||
invalid(1, 'expected text')
|
||||
const lines = text.replace(/^\uFEFF/, '').split(/\r\n|\r|\n/)
|
||||
let index = 0
|
||||
function skipEmpty() {
|
||||
while (index < lines.length && !lines[index].trim())
|
||||
index++
|
||||
}
|
||||
skipEmpty()
|
||||
if (index === lines.length)
|
||||
return []
|
||||
if (!/^WEBVTT(?:[\t ].*)?$/.test(lines[index].trim()) || lines[index].includes('-->'))
|
||||
invalid(index + 1, 'expected WEBVTT header')
|
||||
index++
|
||||
const thumbnails = []
|
||||
while (index < lines.length) {
|
||||
skipEmpty()
|
||||
if (index === lines.length)
|
||||
break
|
||||
const first = lines[index].trim()
|
||||
if (/^NOTE(?:[\t ]|$)/.test(first) || first === 'STYLE' || first === 'REGION') {
|
||||
while (index < lines.length && lines[index].trim())
|
||||
index++
|
||||
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]
|
||||
const identifierLine = index + 1
|
||||
if (!first.includes('-->')) {
|
||||
index++
|
||||
if (index === lines.length || !lines[index].trim())
|
||||
invalid(identifierLine, 'cue identifier must be followed by timing')
|
||||
}
|
||||
|
||||
vttArray.push(result)
|
||||
const timingLine = index + 1
|
||||
const timing = lines[index].trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/)
|
||||
if (!timing)
|
||||
invalid(timingLine, 'invalid cue timing')
|
||||
const start = timestamp(timing[1], timingLine)
|
||||
const end = timestamp(timing[2], timingLine)
|
||||
if (end < start)
|
||||
invalid(timingLine, 'cue end precedes its start')
|
||||
index++
|
||||
skipEmpty()
|
||||
if (index === lines.length)
|
||||
invalid(timingLine, 'missing sprite image')
|
||||
const image = rectangle(lines[index].trim(), vttUrl, index + 1)
|
||||
thumbnails.push({ start: Math.floor(start), end: Math.floor(end), ...image })
|
||||
index++
|
||||
}
|
||||
|
||||
return vttArray
|
||||
return thumbnails
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { findThumbnail } from './parseVtt'
|
||||
|
||||
export default function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isMobile }) {
|
||||
let timer = null
|
||||
let generation = 0
|
||||
@@ -36,7 +38,7 @@ export default function createPreview({ lifetime, thumbnails, progress, duration
|
||||
style(control, 'display', 'flex')
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const cue = thumbnails.find(item => second >= item.start && second <= item.end)
|
||||
const cue = findThumbnail(thumbnails, second)
|
||||
if (!cue)
|
||||
return style(control, 'display', 'none')
|
||||
if (width > 0 && width < progress.clientWidth)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
# PKG-VTT-THUMB-03 解析与模块整理完成
|
||||
|
||||
在 524ddf78 的请求/资源检查点基础上,完成纯解析与区间查找边界。
|
||||
parseVtt 导出纯 findThumbnail,preview 只负责消费 cue、定位和计时器。
|
||||
注册、请求、生命周期、解析、视图及旧源码 helper 的职责详见包内 ARCHITECTURE.md。
|
||||
|
||||
## 格式支持和修复
|
||||
|
||||
新解析器识别 BOM/CRLF、header 注释、cue ID、NOTE 注释、STYLE/REGION 块与 timing settings。
|
||||
这些块不用于修改播放器样式。三位及更长小时数不再被截断。无效头、时间戳、逆向区间、
|
||||
缺失图片、重复/未知 xywh 键、非有限/负数坐标、零尺寸等给出带原始行号的 TypeError,
|
||||
在安装 UI 之前整体拒绝。坐标前后空白现在规范为可用 CSS 数字。
|
||||
|
||||
格式结构参考 [W3C WebVTT](https://www.w3.org/TR/webvtt1/)(2026-09-13 核对),
|
||||
采用自有实现,没有引入第三方解析器或复制其实现。这里仅解析 thumbnail metadata,
|
||||
不是完整字幕解析/渲染器;不处理多行字幕 payload、timestamp-map 偏移或全部字幕设置。
|
||||
|
||||
## 兼容边界
|
||||
|
||||
保留 floor 时间边界、首个包含端点的命中、原始输入顺序、重叠/未排序 cue、正常小数矩形、
|
||||
四键排列与字面 URL 拼接。还保留历史紧凑箭头、纯秒时间、分钟/秒字段的进位算术、
|
||||
零时长 cue、紧密 cue 对及 time/payload 间空行;不因标准校验重新收紧已可用的旧输入。
|
||||
空文件/仅头/仅注释均为空预览。真实 demo 120 个 cue 与旧 parser 深度比较完全相同。
|
||||
坏格式曾生成错误 CSS 或无法定位的异常,不视为要求继续保持的有效 API。
|
||||
|
||||
## 验证
|
||||
|
||||
26 项解析测试在固定旧 parser/运行产物上 5 通过、21 失败;新源码通过。
|
||||
坏输入还通过候选工厂验证 Promise 拒绝及监听器/控件清理;扩展 cue 验证重叠首个命中。
|
||||
浏览器用真实 HTTP 文本、sprite、旧/新核心验证扩展 VTT 悬停与坏格式拒绝后的重试恢复,
|
||||
继续运行原生请求取消和 destroy(false) 清理。各入口和最终 CI 以
|
||||
[验证记录](../baselines/vtt-thumbnail-parser.json)为准,不把纯 parser 测试称为安装包证据。
|
||||
|
||||
本任务完成的是 03 的内部结构和已复现输入/生命周期修复。04 继续严格 TS、公开类型和
|
||||
旧消费形式;05/06 继续旧核心范围、移动/全屏/chapter 组合、安装/demo/编辑器。
|
||||
类型/导出/完整生命周期风险继续由后续任务承接,不能据此声称包已可发布。
|
||||
无新依赖或版本变化;正常构建生成 main/legacy/ESM 及 docs 副本。本任务独立提交,
|
||||
回退只恢复解析检查点,前次资源修复仍可保留;不推送或发布。
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 93 / doing 13 / blocked 0 / done 116 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 93 / doing 12 / blocked 0 / done 117 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-VTT-THUMB-01 | artplayer-plugin-vtt-thumbnail<br>核对包契约与历史用法 | BASE-05 | vtt/style、异步返回、预览控件名与样式契约 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done |
|
||||
| PKG-VTT-THUMB-02 | artplayer-plugin-vtt-thumbnail<br>建立特有行为与错误测试 | PKG-VTT-THUMB-01, ENG-03, ENG-05 | VTT 解析、xywh、相对 URL、时间边界、请求失败和定位 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | done |
|
||||
| PKG-VTT-THUMB-03 | artplayer-plugin-vtt-thumbnail<br>整理内部职责与资源 | PKG-VTT-THUMB-02, CORE-09, CORE-19 | 纯解析/区间查找与 DOM、请求/定时器生命周期分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | doing |
|
||||
| PKG-VTT-THUMB-03 | artplayer-plugin-vtt-thumbnail<br>整理内部职责与资源 | PKG-VTT-THUMB-02, CORE-09, CORE-19 | 纯解析/区间查找与 DOM、请求/定时器生命周期分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | done |
|
||||
| PKG-VTT-THUMB-04 | artplayer-plugin-vtt-thumbnail<br>迁移自有源码和公开类型 | PKG-VTT-THUMB-03, ENG-04, ENG-06, CORE-07 | cue/rect/result 和参数可选性保持兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | todo |
|
||||
| PKG-VTT-THUMB-05 | artplayer-plugin-vtt-thumbnail<br>验证新旧核心和组合 | PKG-VTT-THUMB-04, CORE-22 | 鼠标和移动进度、全屏、切源、chapter 组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo |
|
||||
| PKG-VTT-THUMB-06 | artplayer-plugin-vtt-thumbnail<br>验证分发并同步文档 | PKG-VTT-THUMB-05, ENG-07 | vtt.thumbnail.js、VTT/图片资源解析和产物验证 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | M | todo |
|
||||
@@ -499,7 +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-VTT-THUMB-03: [记录](changes/2026-09-13-PKG-VTT-THUMB-03-resources.md) [记录](baselines/vtt-thumbnail-resources.json) [记录](changes/2026-09-13-PKG-VTT-THUMB-03-parser.md) [记录](baselines/vtt-thumbnail-parser.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)
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-VTT-THUMB-03 内部模块与解析修复完成
|
||||
|
||||
在资源检查点上完成纯解析/首个区间查找,支持 cue ID、注释/元数据块和 timing settings,
|
||||
修复长小时截断及非法输入无定位/坏 CSS。26 项解析在旧版 5 通过/21 失败;正常 demo 120 cue
|
||||
完全相同。源码/main/legacy 的解析+生命周期各 47 通过,原生三入口各 18 通过,共 54 项。
|
||||
实际 ESM 与完整 CI 2064 项(1727 单元、14 工程、323 基线)通过;无依赖/声明/版本变更。
|
||||
见[变更](changes/2026-09-13-PKG-VTT-THUMB-03-parser.md)与[验证](baselines/vtt-thumbnail-parser.json)。
|
||||
解析风险已关闭,类型/导出/完整设备和组合仍待 04–06。下一步严格 TS 及旧消费者。
|
||||
222 项:117 done、12 doing、93 todo;独立本地任务提交,不推送、不发布。
|
||||
|
||||
|
||||
## PKG-VTT-THUMB-03 请求与预览资源检查点(doing)
|
||||
|
||||
生产入口拆为注册、生命周期、请求、预览和纯解析,保留旧源码 helper。已修复销毁后
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
| AUTO-THUMB-ENCODE-01 | open / 已复现 | Unserialized encodes include blank output and lack media/canvas/input failure handling | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 |
|
||||
| AUTO-THUMB-PIXEL-01 | open / 已复现 | Historical auto-thumbnail yields black sampled JPEG cells on Windows WebKit while played native video and static JPEG controls yield color | PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 |
|
||||
| VTT-THUMB-DIST-01 | open / 已复现 | Historical 1.0.0 main/legacy contain invalid regular expressions and cannot load | PKG-VTT-THUMB-06 |
|
||||
| VTT-THUMB-PARSE-01 | open / 已复现 | Historical compiled arrows differ from shipped source; parsing floors bounds and assumes alternating lines | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03 |
|
||||
| VTT-THUMB-PARSE-01 | resolved / 已复现 | Historical compiled arrows differ from shipped source; parsing floors bounds and assumes alternating lines | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03 |
|
||||
| VTT-THUMB-LIFE-01 | open / 已复现 | Fetch completion, setBar listeners and mobile timers have no destroy ownership | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-05 |
|
||||
| VTT-THUMB-TYPE-01 | open / 已复现 | Published declarations falsely describe async registration as a synchronous result | PKG-VTT-THUMB-04 |
|
||||
| VTT-THUMB-EXPORT-01 | open / 已复现 | Older CommonJS default objects and thumbnails control names differ from latest direct export and vtt-thumbnail name | PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
|
||||
+9
-2
@@ -4141,7 +4141,7 @@
|
||||
"id": "VTT-THUMB-PARSE-01",
|
||||
"title": "Historical compiled arrows differ from shipped source; parsing floors bounds and assumes alternating lines",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-VTT-THUMB-02",
|
||||
"PKG-VTT-THUMB-03"
|
||||
@@ -4157,7 +4157,14 @@
|
||||
"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."
|
||||
"closureCriteria": "Parser fixtures include malformed, empty, real demo VTT, fractional and shared bounds, relative URLs and historical accepted formats.",
|
||||
"resolutionEvidence": [
|
||||
"test/vtt-thumbnail-parser.test.js",
|
||||
"test/browser/vtt-thumbnail-lifecycle.spec.js",
|
||||
"refactor/baselines/vtt-thumbnail-parser.json",
|
||||
"refactor/changes/2026-09-13-PKG-VTT-THUMB-03-parser.md"
|
||||
],
|
||||
"resolutionRationale": "Historical compiled/source arrow differences stay frozen. Pure parser validates malformed input with original lines, handles metadata blocks/IDs/settings and long hours, retains legacy boundaries/URL/coordinate semantics and compares all 120 demo cues. Candidate factories and native old/new core cases demonstrate rejection before mounting and recovery. This resolves the thumbnail parsing scope; declaration, full device/core and installed-package risks remain separate."
|
||||
},
|
||||
{
|
||||
"id": "VTT-THUMB-LIFE-01",
|
||||
|
||||
+4
-2
@@ -1897,13 +1897,15 @@
|
||||
"CORE-09",
|
||||
"CORE-19"
|
||||
],
|
||||
"status": "doing",
|
||||
"status": "done",
|
||||
"risk": "M",
|
||||
"deliverable": "纯解析/区间查找与 DOM、请求/定时器生命周期分离",
|
||||
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
|
||||
"evidence": [
|
||||
"changes/2026-09-13-PKG-VTT-THUMB-03-resources.md",
|
||||
"baselines/vtt-thumbnail-resources.json"
|
||||
"baselines/vtt-thumbnail-resources.json",
|
||||
"changes/2026-09-13-PKG-VTT-THUMB-03-parser.md",
|
||||
"baselines/vtt-thumbnail-parser.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,10 +5,11 @@ 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 = '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="45"><path fill="red" d="M0 0h80v45H0z"/><path fill="blue" d="M80 0h80v45H80z"/></svg>'
|
||||
const extendedCues = '\uFEFFWEBVTT thumbnails\r\n\r\nNOTE source metadata\r\nignored\r\n\r\nSTYLE\r\n::cue { color: red }\r\n\r\nREGION\r\nid:unused\r\n\r\nfirst\r\n00:00.000\t-->\t00:04.000 align:start\r\nvtt-sprite.svg#xywh=0,0,80,45\r\n\r\nsecond\r\n00:04.000 --> 00:08.000\r\nvtt-sprite.svg#xywh=80,0,80,45\r\n'
|
||||
|
||||
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-cues.vtt', route => route.fulfill({ contentType: 'text/vtt', body: extendedCues }))
|
||||
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 })
|
||||
@@ -45,6 +46,32 @@ for (const core of ['published', 'candidate']) {
|
||||
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), pixels }), contentType: 'application/json' })
|
||||
})
|
||||
|
||||
test(`VTT native ${core} core: malformed sprite rejects before mounting and a corrected registration recovers`, async ({ page }, testInfo) => {
|
||||
await page.route('**/test/vtt-invalid.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:00.000 --> 00:08.000\nvtt-sprite.svg#xywh=0,0,bad,45\n' }))
|
||||
await page.route('**/test/vtt-cues.vtt', route => route.fulfill({ contentType: 'text/vtt', body: extendedCues }))
|
||||
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 })
|
||||
const error = await page.evaluate(async () => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
try {
|
||||
await window.art.plugins.add(window.artplayerPluginVttThumbnail({ vtt: '/test/vtt-invalid.vtt' }))
|
||||
return null
|
||||
}
|
||||
catch (error) {
|
||||
return { name: error.name, message: error.message }
|
||||
}
|
||||
})
|
||||
expect(error.name).toBe('TypeError')
|
||||
expect(error.message).toContain('line 4:')
|
||||
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(0)
|
||||
await page.evaluate(() => window.art.plugins.add(window.artplayerPluginVttThumbnail({ vtt: '/test/vtt-cues.vtt' })))
|
||||
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(1)
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(0)
|
||||
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), recoveredAfter: error }), contentType: 'application/json' })
|
||||
})
|
||||
|
||||
test(`VTT native ${core} core: destroy aborts a pending native fetch and settles registration`, async ({ page }, testInfo) => {
|
||||
let release
|
||||
let requested
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Pure parser regression runner.
|
||||
import test from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
import { vttThumbnailCandidate, vttThumbnailEnvironment } from './helpers/vtt-thumbnail.js'
|
||||
|
||||
const baseline = process.env.ARTPLAYER_VTT_PARSER_BASELINE === '1'
|
||||
const module = baseline
|
||||
? await import(`data:text/javascript,${encodeURIComponent(execFileSync('git', ['show', '524ddf784fdc143d3020040e4de1a9e0921698ce:packages/artplayer-plugin-vtt-thumbnail/src/parseVtt.js'], { encoding: 'utf8' }))}`)
|
||||
: await loadModules({ defaultParser: 'packages/artplayer-plugin-vtt-thumbnail/src/parseVtt' })
|
||||
const parse = baseline ? module.default : module.defaultParser
|
||||
const implementation = await vttThumbnailCandidate()
|
||||
const cue = (time = '00:00.900 --> 00:05.900', image = 'sprite.jpg#xywh=10,20,80,45') => `${time}\n${image}`
|
||||
|
||||
test('VTT parser retains floored boundaries, raw xywh fields and lexical relative URL joining', () => {
|
||||
assert.deepEqual(parse(`WEBVTT\n\n${cue()}`, 'https://cdn.test/path/cues.vtt'), [{ start: 0, end: 5, url: 'https://cdn.test/path/sprite.jpg', x: '10', y: '20', w: '80', h: '45' }])
|
||||
assert.equal(parse(`WEBVTT\n${cue(undefined, '../sprite.jpg#yxhw=20,10,45,80')}`, '/folder/cues.vtt')[0].url, '/folder/../sprite.jpg')
|
||||
})
|
||||
|
||||
test('VTT parser accepts BOM, CRLF, header annotations, cue IDs, timing settings and NOTE blocks', () => {
|
||||
const text = `\uFEFFWEBVTT thumbnails\r\n\r\nNOTE author\r\nignored comment\r\n\r\nfirst-id\r\n${cue('00:00.900\t-->\t00:05.900 align:start')}`
|
||||
assert.equal(parse(text)[0].w, '80')
|
||||
assert.equal(parse(text)[0].end, 5)
|
||||
})
|
||||
|
||||
test('VTT parser ignores STYLE and REGION blocks without applying caption CSS', () => {
|
||||
const text = `WEBVTT\n\nSTYLE\n::cue { color: red }\n\nREGION\nid:region1\nwidth:50%\n\n${cue()}`
|
||||
assert.equal(parse(text).length, 1)
|
||||
})
|
||||
|
||||
test('VTT parser preserves compact arrows, seconds-only legacy timestamps and dense cue pairs', () => {
|
||||
assert.deepEqual(parse(`WEBVTT\n${cue('00-->05')}\n${cue('05-->10')}`).map(item => [item.start, item.end]), [[0, 5], [5, 10]])
|
||||
assert.equal(parse(`WEBVTT\n${cue('00:70-->01:20')}`)[0].start, 70, 'Keep previously accepted numeric time arithmetic outside strict subtitle syntax')
|
||||
})
|
||||
|
||||
test('VTT parser reads complete long-hour timestamps instead of truncating to their last two digits', () => {
|
||||
const parsed = parse(`WEBVTT\n${cue('123:00:00.100 --> 123:00:05.900')}`)
|
||||
assert.equal(parsed[0].start, 442800)
|
||||
assert.equal(parsed[0].end, 442805)
|
||||
})
|
||||
|
||||
test('VTT parser keeps empty documents, empty header and comment-only documents empty', () => {
|
||||
for (const text of ['', ' \n\t', '\uFEFFWEBVTT', 'WEBVTT\n\nNOTE empty\ncomment\n'])
|
||||
assert.deepEqual(parse(text), [])
|
||||
})
|
||||
|
||||
test('VTT parser retains valid decimal coordinates and all four-key permutations', () => {
|
||||
for (const keys of ['xywh', 'yxhw', 'whxy', 'hyxw']) {
|
||||
const values = { x: '0.5', y: '2.25', w: '80.5', h: '45.25' }
|
||||
const parsed = parse(`WEBVTT\n${cue(undefined, `sprite.jpg#${keys}=${[...keys].map(key => values[key]).join(',')}`)}`)[0]
|
||||
for (const key of keys)
|
||||
assert.equal(parsed[key], values[key])
|
||||
}
|
||||
})
|
||||
|
||||
test('VTT parser preserves legacy zero-length cues and blank separators inside dense pairs', () => {
|
||||
const parsed = parse('WEBVTT\n00:05.000-->00:05.000\n\nsprite.jpg#xywh=0,0,80,45')
|
||||
assert.equal(parsed[0].start, 5)
|
||||
assert.equal(parsed[0].end, 5)
|
||||
})
|
||||
|
||||
test('VTT parser refuses non-text and numeric overflow without returning partial cues', () => {
|
||||
for (const input of [null, undefined, 12, {}])
|
||||
assert.throws(() => parse(input), error => error instanceof TypeError && /line 1:/.test(error.message))
|
||||
const overflow = '9'.repeat(400)
|
||||
assert.throws(() => parse(`WEBVTT\n${cue(`${overflow}:00:00.000 --> ${overflow}:00:05.000`)}`), /numeric range/)
|
||||
assert.throws(() => parse(`WEBVTT\n${cue()}\n\n${cue(undefined, `sprite.jpg#xywh=0,0,${overflow},45`)}`), /finite/)
|
||||
})
|
||||
|
||||
for (const [label, text, line] of [
|
||||
['invalid header', `WRONG\n\n${cue()}`, 1],
|
||||
['missing timing after cue id', 'WEBVTT\n\nfirst-id', 3],
|
||||
['missing image', 'WEBVTT\n\n00:00.000 --> 00:05.000', 3],
|
||||
['bad timestamp separator', `WEBVTT\n\n${cue('00:00x100 --> 00:05.000')}`, 3],
|
||||
['bad fractional timestamp', `WEBVTT\n\n${cue('00:00.1 --> 00:05.000')}`, 3],
|
||||
['reverse interval', `WEBVTT\n\n${cue('00:05.000 --> 00:00.000')}`, 3],
|
||||
['missing rectangle', `WEBVTT\n\n${cue(undefined, 'sprite.jpg')}`, 4],
|
||||
['duplicate rectangle keys', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#xxwh=0,0,80,45')}`, 4],
|
||||
['unknown rectangle keys', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#abcd=0,0,80,45')}`, 4],
|
||||
['missing coordinate', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#xywh=0,0,80')}`, 4],
|
||||
['extra coordinate', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#xywh=0,0,80,45,5')}`, 4],
|
||||
['negative position', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#xywh=-1,0,80,45')}`, 4],
|
||||
['non-finite width', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#xywh=0,0,Infinity,45')}`, 4],
|
||||
['zero width', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#xywh=0,0,0,45')}`, 4],
|
||||
['non-numeric width', `WEBVTT\n\n${cue(undefined, 'sprite.jpg#xywh=0,0,bad,45')}`, 4],
|
||||
]) {
|
||||
test(`VTT parser reports ${label} as a TypeError with the original line`, async () => {
|
||||
assert.throws(() => parse(text), error => error instanceof TypeError && error.message.includes(`line ${line}:`))
|
||||
const env = vttThumbnailEnvironment(implementation, { text })
|
||||
await assert.rejects(env.factory({})(env.art), error => error.name === 'TypeError' && error.message.includes(`line ${line}:`))
|
||||
assert.equal(env.controls.length, 0)
|
||||
assert([...env.listeners.values()].every(items => items.length === 0))
|
||||
})
|
||||
}
|
||||
|
||||
test('VTT parser handles every real demo cue identically to the frozen parser', async () => {
|
||||
const old = await import(`data:text/javascript,${encodeURIComponent(execFileSync('git', ['show', '524ddf784fdc143d3020040e4de1a9e0921698ce:packages/artplayer-plugin-vtt-thumbnail/src/parseVtt.js'], { encoding: 'utf8' }))}`)
|
||||
const text = fs.readFileSync('docs/assets/sample/bbb-thumbnails.vtt', 'utf8')
|
||||
const actual = parse(text, '/assets/sample/bbb-thumbnails.vtt')
|
||||
assert.equal(actual.length, 120)
|
||||
assert.deepEqual(actual, old.default(text, '/assets/sample/bbb-thumbnails.vtt'))
|
||||
})
|
||||
|
||||
test('VTT candidate registers extended VTT and retains first-match selection for overlapping unsorted cues', async () => {
|
||||
const env = vttThumbnailEnvironment(implementation, { text: `WEBVTT\n\nNOTE comment\n\nlate\n${cue('00:04.100 --> 00:08.900', 'first.jpg#xywh=0,0,80,45')}\n\nearly\n${cue('00:00.900 --> 00:05.900', 'second.jpg#xywh=80,0,80,45')}` })
|
||||
await env.factory({})(env.art)
|
||||
await env.hover(0.4)
|
||||
assert.equal(env.styles.backgroundImage, 'url(first.jpg)')
|
||||
await env.hover(0.3)
|
||||
assert.equal(env.styles.backgroundImage, 'url(second.jpg)')
|
||||
await env.emit('destroy')
|
||||
})
|
||||
Reference in new issue
Block a user