mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-17 01:56:16 -08:00
refactor(vtt-thumbnail): [PKG-VTT-THUMB-03] own pending requests and preview resources
This commit is contained in:
1 parent
05244d1104
commit
524ddf784f
23 files changed
+3045
-262
No files matched your search
@@ -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.
|
||||
+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(":")||[],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<n.length;l+=2){const o=n[l],s=n[l+1];if(!s.trim())continue;const r=/((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\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<m.length;t++)h[m[t]]=g[t];i.push(h)}return i}(e.vtt);return n.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:e.style||{},mounted(t){o(t,"art-control-thumbnails"),n.on("setBar",async(e,o,u)=>{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&&e<s.clientWidth?function(t,e,n){i(t,"backgroundImage",`url(${e.url})`),i(t,"height",`${e.h}px`),i(t,"width",`${e.w}px`),i(t,"backgroundPosition",`-${e.x}px -${e.y}px`),n<=e.w/2?i(t,"left",0):n>s.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&&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}}}});
|
||||
+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((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<t.length;i+=2){const o=t[i],r=t[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=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;t<m.length;t++)f[m[t]]=g[t];l.push(f)}return l})}(n.vtt);return l.controls.add({name:"vtt-thumbnail",position:"top",index:20,style:n.style||{},mounted(e){r(e,"art-control-thumbnails"),l.on("setBar",(n,r,c)=>t(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&&n<s.clientWidth?function(t,e,n){i(t,"backgroundImage",`url(${e.url})`),i(t,"height",`${e.h}px`),i(t,"width",`${e.w}px`),i(t,"backgroundPosition",`-${e.x}px -${e.y}px`),n<=e.w/2?i(t,"left",0):n>s.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&&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}})}});
|
||||
+173
-58
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user