mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 22:06:17 -08:00
refactor(multiple-subtitles): [PKG-MULTI-SUB-03] own requests and subtitle resources
This commit is contained in:
1 parent
36cd151095
commit
beaa93d6cc
25 files changed
+7908
-250
No files matched your search
@@ -55,4 +55,4 @@
|
||||
*
|
||||
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=t,this.parse=function(a,s){a=a.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=a.split(/\r\n|\r|\n/),f=!1,c=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}for(;void 0!=l[o];){for(var x;!f&&""==l[o];)o++;if(!f&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||c.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}f=!1;var O=new n(l[o],p),L=0;if(u.length>0&&(L=u[u.length-1].startTime),!T||O.parse(x,L)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),f=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var I=new i(x.text,p,s,t);x.tree=I.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:c}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,a=(e=e,0),s=function(e){t(e,a+1)};function r(t){for(;void 0!=e[a]&&t.test(e[a]);)a++}function o(t){for(var n="";void 0!=e[a]&&t.test(e[a]);)n+=e[a],a++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[a])if(/\d/.test(e[a]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[a])&&s("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[a])if(a++,"-"==e[a])if(a++,">"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),f=i[r].slice(o+1);if(-1!=a.indexOf(l)&&s("Duplicate setting."),a.push(l),""==f)return void s("No value for setting defined.");if("vertical"==l){if("rl"!=f&&"lr"!=f){s("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=f}else if("line"==l){if(/,/.test(f)){var c=f.split(",");f=c[0];var u=c[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(f)){s("Line position takes a number or percentage.");continue}if(-1!=f.indexOf("-",1)){s("Line position can only have '-' at the start.");continue}if(-1!=f.indexOf("%")&&f.indexOf("%")!=f.Line truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=t,this.parse=function(s,a){s=s.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=s.split(/\r\n|\r|\n/),c=!1,f=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var O=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||O.parse(x,A)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var L=new i(x.text,p,a,t);x.tree=L.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:f}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,s=(e=e,0),a=function(e){t(e,s+1)};function r(t){for(;void 0!=e[s]&&t.test(e[s]);)s++}function o(t){for(var n="";void 0!=e[s]&&t.test(e[s]);)n+=e[s],s++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[s])if(/\d/.test(e[s]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[s])if(s++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[s]){if(":"!=e[s])return void a("No seconds found or minutes is greater than 59.");if(s++,2!=(i=o(/\d/)).length)return void a("Must be exactly two digits.")}else{if(2!=t.length)return void a("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[s])if(s++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)a("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;a("You cannot have more than 59 seconds.")}else a("Milliseconds must be given in three digits.");else a('No decimal separator (".") found.')}else a("Must be exactly two digits.");else a("No time unit separator found.");else a("Timestamp must start with a character in the range 0-9.");else a("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&a("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[s])&&a("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[s])if(s++,"-"==e[s])if(s++,">"==e[s]){if(s++,i.test(e[s])&&a("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&a("End timestamp is not greater than start timestamp."),i.test(e[s]),r(n),function(e,t){for(var i=e.split(n),s=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),c=i[r].slice(o+1);if(-1!=s.indexOf(l)&&a("Duplicate setting."),s.push(l),""==c)return void a("No value for setting defined.");if("vertical"==l){if("rl"!=c&&"lr"!=c){a("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=c}else if("line"==l){if(/,/.test(c)){var f=c.split(",");c=f[0];var u=f[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(c)){a("Line position takes a number or percentage.");continue}if(-1!=c.indexOf("-",1)){a("Line position can only have '-' at the start.");continue}if(-1!=c.indexOf("%")&&c.indexOf("%")!=c.Line truncated
|
||||
@@ -55,4 +55,4 @@
|
||||
*
|
||||
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))a.call(t,n)&&r(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&r(e,n,t[n]);return e},l=(e,t,n)=>new Promise((i,a)=>{var s=e=>{try{o(n.next(e))}catch(t){a(t)}},r=e=>{try{o(n.throw(e))}catch(t){a(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,r);o((n=n.apply(e,t)).next())}),f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},c=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),a=0,s=t.split(/\r\n|\r|\n/),r=!1,o=[],l=[],c=[];function p(e,t){c.push({message:e,line:a+1,col:t})}var m=s[a],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),a++;""!=s[a]&&void 0!=s[a];){if(p("No blank line after the signature."),-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}for(;void 0!=s[a];){for(var x;!r&&""==s[a];)a++;if(!r&&void 0==s[a])break;x=Object.assign({},f,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==s[a].indexOf("--\x3e")){if(x.id=s[a],/^NOTE($|[ \t])/.test(x.id)){for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),a++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(s[a]),a++;if(l.length){p("Style blocks cannot appear after the first cue.");continue}w||o.push(y.join("\n"));continue}if(""==s[++a]||void 0==s[a]){p("Cue identifier cannot be standalone.");continue}if(-1==s[a].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}r=!1;var O=new u(s[a],p),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!T||O.parse(x,P)){for(a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){p("Blank line missing before cue."),r=!0;break}""!=x.text&&(x.text+="\n"),x.text+=s[a],a++}var I=new d(x.text,p,n,e);x.tree=I.parse(x.startTime,x.endTime),l.push(x)}else for(x=null,a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:o}}},u=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,a=(e=e,0),s=function(e){t(e,a+1)};function r(t){for(;void 0!=e[a]&&t.test(e[a]);)a++}function o(t){for(var n="";void 0!=e[a]&&t.test(e[a]);)n+=e[a],a++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[a])if(/\d/.test(e[a]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[a])&&s("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[a])if(a++,"-"==e[a])if(a++,">"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r<i.length;r++)ifLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())});var f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),a=!1,o=[],l=[],c=[];function u(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&u('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(u("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}for(;void 0!=s[r];){for(var y;!a&&""==s[r];)r++;if(!a&&void 0==s[r])break;y=Object.assign({},f,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var x=!0;if(-1==s[r].indexOf("--\x3e")){if(y.id=s[r],/^NOTE($|[ \t])/.test(y.id)){for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&u("Cannot have timestamp in a comment."),r++;continue}if(/^STYLE($|[ \t])/.test(y.id)){var T=[],w=!1;for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&(u("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){u("Style blocks cannot appear after the first cue.");continue}w||o.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){u("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,u("Cue identifier needs to be followed by timestamp.");continue}}a=!1;var O=new d(s[r],u),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!x||O.parse(y,P)){for(r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){u("Blank line missing before cue."),a=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,u,n,e);y.tree=A.parse(y.startTime,y.endTime),l.push(y)}else for(y=null,r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:o}}},d=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,r=(e=e,0),s=function(e){t(e,r+1)};function a(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function o(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,a,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[r]){if(":"!=e[r])return void s("No seconds found or minutes is greater than 59.");if(r++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[r])if(r++,3==(a=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(a,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(a(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[r])&&s("Timestamp not separated from '--\x3e' by whitespace."),a(n),"-"==e[r])if(r++,"-"==e[r])if(r++,">"==e[r]){if(r++,i.test(e[r])&&s("'--\x3e' not separated from timestamp by whitespace."),a(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[r]),a(n),function(e,t){for(var i=e.split(n),r=[Line truncated
|
||||
@@ -55,6 +55,56 @@
|
||||
*
|
||||
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
|
||||
*/
|
||||
function createLifetime(art) {
|
||||
let closed = Boolean(art.isDestroy);
|
||||
const cleanups = /* @__PURE__ */ new Set();
|
||||
let cancel;
|
||||
const cancelled = new Promise((resolve) => {
|
||||
cancel = resolve;
|
||||
});
|
||||
function run(cleanup) {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (error) {
|
||||
console.warn("Failed to clean up multiple subtitles:", error);
|
||||
}
|
||||
}
|
||||
const lifetime = {
|
||||
get closed() {
|
||||
return closed;
|
||||
},
|
||||
own(cleanup) {
|
||||
if (closed)
|
||||
run(cleanup);
|
||||
else
|
||||
cleanups.add(cleanup);
|
||||
return () => cleanups.delete(cleanup);
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
if (!closed) {
|
||||
lifetime.own(() => art.off("destroy", lifetime.dispose));
|
||||
try {
|
||||
art.on("destroy", lifetime.dispose);
|
||||
} catch (error) {
|
||||
lifetime.dispose();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return lifetime;
|
||||
}
|
||||
var defaultCueSettings = {
|
||||
direction: "horizontal",
|
||||
snapToLines: true,
|
||||
@@ -799,85 +849,166 @@ var WebVTTSerializer = function() {
|
||||
return result;
|
||||
};
|
||||
};
|
||||
async function loadVtt(option, { getExt, srtToVtt, assToVtt }) {
|
||||
const response = await fetch(option.url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const decoder = new TextDecoder(option.encoding || "utf-8");
|
||||
const text = decoder.decode(buffer);
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case "srt": {
|
||||
return srtToVtt(text);
|
||||
}
|
||||
case "ass": {
|
||||
return assToVtt(text);
|
||||
}
|
||||
case "vtt": {
|
||||
return text;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function mergeTrees(trees) {
|
||||
function parseTracks(vtts, subtitles) {
|
||||
const parser = new WebVTTParser();
|
||||
const result = parser.parse("", "metadata");
|
||||
for (let i = 0; i < trees.length; i++) {
|
||||
const tree = trees[i];
|
||||
if (!tree.updated) {
|
||||
tree.updated = true;
|
||||
for (let j = 0; j < tree.cues.length; j++) {
|
||||
const cue = tree.cues[j];
|
||||
for (let k = 0; k < cue.tree.children.length; k++) {
|
||||
const children = cue.tree.children[k];
|
||||
children.value = `<div class="art-subtitle-${tree.name}">${children.value}</div>`;
|
||||
return vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, "metadata");
|
||||
tree.url = subtitles[index].url;
|
||||
tree.name = subtitles[index].name;
|
||||
return tree;
|
||||
});
|
||||
}
|
||||
function serializeTracks(trees) {
|
||||
const cues = [];
|
||||
for (const tree of trees) {
|
||||
for (const cue of tree.cues) {
|
||||
cues.push({
|
||||
...cue,
|
||||
tree: {
|
||||
...cue.tree,
|
||||
children: cue.tree.children.map((child) => ({
|
||||
...child,
|
||||
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
result.cues.push(...tree.cues);
|
||||
}
|
||||
return result;
|
||||
return new WebVTTSerializer().serialize(cues);
|
||||
}
|
||||
function createRenderer(art, lifetime, unescape) {
|
||||
let current = null;
|
||||
function own(url) {
|
||||
let freed = false;
|
||||
let release = () => {
|
||||
};
|
||||
const entry = {
|
||||
free() {
|
||||
if (freed)
|
||||
return;
|
||||
freed = true;
|
||||
release();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
release = lifetime.own(entry.free);
|
||||
return entry;
|
||||
}
|
||||
return (vtt) => {
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
|
||||
const entry = own(url);
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const previous = current;
|
||||
let option;
|
||||
let escape;
|
||||
let configured = false;
|
||||
current = entry;
|
||||
try {
|
||||
option = art.option.subtitle;
|
||||
escape = option.escape;
|
||||
if (lifetime.closed || current !== entry)
|
||||
return;
|
||||
option.escape = false;
|
||||
configured = true;
|
||||
const config = { ...option, url, type: "vtt", onVttLoad: unescape };
|
||||
if (lifetime.closed || current !== entry)
|
||||
return;
|
||||
const pending = art.subtitle.init(config);
|
||||
Promise.resolve(pending).catch((error) => {
|
||||
if (current === entry) {
|
||||
current = null;
|
||||
entry.free();
|
||||
}
|
||||
if (!lifetime.closed)
|
||||
console.warn("Failed to initialize multiple subtitles:", error);
|
||||
});
|
||||
} catch (error) {
|
||||
if (current === entry) {
|
||||
current = previous;
|
||||
if (configured && option.escape === false)
|
||||
option.escape = escape;
|
||||
}
|
||||
entry.free();
|
||||
throw error;
|
||||
} finally {
|
||||
if (previous !== current)
|
||||
previous?.free();
|
||||
if (entry !== current)
|
||||
entry.free();
|
||||
}
|
||||
};
|
||||
}
|
||||
async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
||||
const release = lifetime.own(() => controller?.abort());
|
||||
try {
|
||||
const url = option.url;
|
||||
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 multiple subtitles: HTTP ${response.status}`);
|
||||
const buffer = await lifetime.wait(response.arrayBuffer());
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const text = new TextDecoder(option.encoding || "utf-8").decode(buffer);
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case "srt":
|
||||
return srtToVtt(text);
|
||||
case "ass":
|
||||
return assToVtt(text);
|
||||
case "vtt":
|
||||
return text;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
} catch (error) {
|
||||
controller?.abort();
|
||||
throw error;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
function artplayerPluginMultipleSubtitles({ subtitles = [] }) {
|
||||
return async (art) => {
|
||||
const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils;
|
||||
const parser = new WebVTTParser();
|
||||
const seri = new WebVTTSerializer();
|
||||
const vtts = await Promise.all(
|
||||
subtitles.map((option) => {
|
||||
return loadVtt(option, { getExt, srtToVtt, assToVtt });
|
||||
})
|
||||
);
|
||||
const trees = vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, "metadata");
|
||||
tree.url = subtitles[index].url;
|
||||
tree.name = subtitles[index].name;
|
||||
return tree;
|
||||
});
|
||||
let lastUrl = "";
|
||||
function setTracks(trees2 = []) {
|
||||
const tree = mergeTrees(trees2);
|
||||
const vtt = seri.serialize(tree.cues);
|
||||
URL.revokeObjectURL(lastUrl);
|
||||
const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
|
||||
lastUrl = url;
|
||||
art.option.subtitle.escape = false;
|
||||
art.subtitle.init({
|
||||
...art.option.subtitle,
|
||||
url,
|
||||
type: "vtt",
|
||||
onVttLoad: unescape
|
||||
});
|
||||
const lifetime = createLifetime(art);
|
||||
const render = createRenderer(art, lifetime, unescape);
|
||||
let trees = [];
|
||||
function setTracks(selected) {
|
||||
if (!lifetime.closed)
|
||||
render(serializeTracks(selected));
|
||||
}
|
||||
setTracks(trees);
|
||||
return {
|
||||
const result = {
|
||||
name: "multipleSubtitles",
|
||||
tracks(names = []) {
|
||||
return setTracks(names.map((name) => trees.find((tree) => tree.name === name)));
|
||||
if (!lifetime.closed)
|
||||
setTracks(names.map((name) => trees.find((tree) => tree.name === name)));
|
||||
},
|
||||
reset() {
|
||||
return setTracks(trees);
|
||||
setTracks(trees);
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (lifetime.closed)
|
||||
return result;
|
||||
const vtts = await Promise.all(subtitles.map((option) => loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime)));
|
||||
if (lifetime.closed)
|
||||
return result;
|
||||
trees = parseTracks(vtts, subtitles);
|
||||
setTracks(trees);
|
||||
return result;
|
||||
} catch (error) {
|
||||
lifetime.dispose();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
export {
|
||||
|
||||
+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/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js 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:unit": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js 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",
|
||||
@@ -88,7 +88,7 @@
|
||||
"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-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs refactor/scripts/vtt-thumbnail-types.test.mjs",
|
||||
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,9 +8,15 @@ task PKG-MULTI-SUB-04 owns that compatibility work. Do not infer runtime behavio
|
||||
|
||||
## Current module boundaries
|
||||
|
||||
- `src/index.js` currently combines download/decode, conversion, parsing, merging, Blob URL
|
||||
replacement and public registration. Task 03 will separate pure merge, requests and owned
|
||||
resources while preserving the factory and CSS hooks.
|
||||
- `src/index.js` composes registration, named selection and the unchanged public result.
|
||||
- `src/request.js` owns each fetch through byte decoding and delegates SRT/ASS conversion
|
||||
to the existing core utils. Failure aborts the request; registration failure closes siblings.
|
||||
- `src/merge.js` parses metadata and serializes selected cues without mutating parsed trees.
|
||||
It depends only on the vendor parser, not the player or browser resource APIs.
|
||||
- `src/lifetime.js` owns the destroy listener, request cancellation and resource callbacks.
|
||||
Its cancellation Promise also settles pending operations without AbortController support.
|
||||
- `src/render.js` owns generated Blob URLs and synchronous/async host installation outcomes.
|
||||
Reentrant selections and stale host rejections cannot release the latest selected resource.
|
||||
- `src/parser.js` is vendored WebVTT parser/serializer code with a CC0 dedication. It is not
|
||||
owned plugin code to mechanically migrate to TypeScript. Keep it distinct from task 04.
|
||||
The pinned upstream reference and full adaptation check are in
|
||||
@@ -23,19 +29,22 @@ task PKG-MULTI-SUB-04 owns that compatibility work. Do not infer runtime behavio
|
||||
All tracks download concurrently and decode through TextDecoder. Explicit type wins over
|
||||
the URL extension; SRT/ASS conversion delegates to existing core utils. `onParser` appears
|
||||
in declarations but is not called. Parsing runs in metadata mode. Each track's top-level
|
||||
cue text receives a name-based div once; current merging concatenates cue arrays by track
|
||||
cue text receives a name-based div in a copied cue; merging concatenates cue arrays by track
|
||||
order, whereas 1.0.0 merged by cue index and retained the first track's timestamps.
|
||||
|
||||
Each selection serializes a new VTT, revokes the preceding URL, creates a text/vtt Blob,
|
||||
sets the live subtitle option's escape to false and calls subtitle.init with the live
|
||||
configuration plus URL/type/onVttLoad overrides. The latest URL and pending requests have
|
||||
no destroy owner yet. These are explicit future fixes, not behavior to silently call safe.
|
||||
Each selection serializes VTT and allocates a text/vtt Blob before replacing the preceding
|
||||
URL. It sets the live subtitle option's escape to false and calls subtitle.init with live
|
||||
configuration plus URL/type/onVttLoad overrides. Allocation failure preserves the prior URL;
|
||||
synchronous installation failure releases the new URL and restores the previous ownership.
|
||||
Async host failure is observed and releases only the failed active installation, with a warning
|
||||
while alive. The plugin cannot generically undo host side effects after asynchronous failure.
|
||||
Registration does not await host media loading, and selection methods remain void.
|
||||
|
||||
The failure baseline now reproduces late fetch/body installation, retained and newly allocated
|
||||
URLs after destroy, ignored HTTP failures, orphaned URLs after init failures, and revoking the
|
||||
old URL before a new allocation can fail. Async init rejection is currently unobserved by the
|
||||
plugin. Task 03 must handle these internal failures while retaining the void selection methods.
|
||||
Keep the current parser's best-effort behavior separate from these resource fixes: invalid-header
|
||||
Destroy cancels pending requests, settles registration with an inert result, releases owned
|
||||
URLs and removes its listener. Retained methods become no-ops; restart does not refetch tracks.
|
||||
Do not restore the shared escape option on normal destroy: another consumer may now own it.
|
||||
The historical failure suite deliberately retains the old outcomes; the candidate lifecycle
|
||||
suite verifies their fixes. Preserve parser best-effort behavior independently: invalid-header
|
||||
diagnostics can coexist with usable cues, and metadata is intentionally read after download.
|
||||
|
||||
## Vendor boundary and validation
|
||||
@@ -51,6 +60,7 @@ Use the pinned Node and Yarn 1.22.22:
|
||||
```sh
|
||||
yarn test:multiple-subtitles
|
||||
yarn test:browser test/browser/multiple-subtitles-history.spec.js
|
||||
yarn test:browser test/browser/multiple-subtitles-lifecycle.spec.js
|
||||
yarn build artplayer-plugin-multiple-subtitles
|
||||
```
|
||||
|
||||
@@ -63,5 +73,9 @@ uses actual 1.2.0 main with published/candidate cores: SRT display and selection
|
||||
fetch after destroy, and native Blob readability/late reset allocations. It covers Chromium,
|
||||
Firefox and Windows WebKit; it is not all historical cores, ASS display or physical Safari.
|
||||
The failure unit suite uses real current core converters and TextDecoder for SRT/ASS/encodings.
|
||||
Candidate lifecycle and merge suites cover cancellation, failures, resource ownership,
|
||||
reentrancy and immutable serialization. The candidate browser suite adds real video source
|
||||
switching, HTTP rejection and Blob revocation. For main/legacy validation, set the artifact
|
||||
environment variable above before running it; source builds are used when it is absent.
|
||||
Tasks 05/06 still own complete core/device combinations and demo acceptance.
|
||||
See `../../refactor/baselines/multiple-subtitles-contract.md` for exact historical differences.
|
||||
+1
-1
@@ -55,4 +55,4 @@
|
||||
*
|
||||
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=t,this.parse=function(a,s){a=a.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=a.split(/\r\n|\r|\n/),f=!1,c=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}for(;void 0!=l[o];){for(var x;!f&&""==l[o];)o++;if(!f&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||c.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}f=!1;var O=new n(l[o],p),L=0;if(u.length>0&&(L=u[u.length-1].startTime),!T||O.parse(x,L)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),f=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var I=new i(x.text,p,s,t);x.tree=I.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:c}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,a=(e=e,0),s=function(e){t(e,a+1)};function r(t){for(;void 0!=e[a]&&t.test(e[a]);)a++}function o(t){for(var n="";void 0!=e[a]&&t.test(e[a]);)n+=e[a],a++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[a])if(/\d/.test(e[a]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[a])&&s("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[a])if(a++,"-"==e[a])if(a++,">"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),f=i[r].slice(o+1);if(-1!=a.indexOf(l)&&s("Duplicate setting."),a.push(l),""==f)return void s("No value for setting defined.");if("vertical"==l){if("rl"!=f&&"lr"!=f){s("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=f}else if("line"==l){if(/,/.test(f)){var c=f.split(",");f=c[0];var u=c[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(f)){s("Line position takes a number or percentage.");continue}if(-1!=f.indexOf("-",1)){s("Line position can only have '-' at the start.");continue}if(-1!=f.indexOf("%")&&f.indexOf("%")!=f.Line truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=t,this.parse=function(s,a){s=s.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=s.split(/\r\n|\r|\n/),c=!1,f=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var O=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||O.parse(x,A)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var L=new i(x.text,p,a,t);x.tree=L.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:f}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,s=(e=e,0),a=function(e){t(e,s+1)};function r(t){for(;void 0!=e[s]&&t.test(e[s]);)s++}function o(t){for(var n="";void 0!=e[s]&&t.test(e[s]);)n+=e[s],s++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[s])if(/\d/.test(e[s]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[s])if(s++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[s]){if(":"!=e[s])return void a("No seconds found or minutes is greater than 59.");if(s++,2!=(i=o(/\d/)).length)return void a("Must be exactly two digits.")}else{if(2!=t.length)return void a("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[s])if(s++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)a("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;a("You cannot have more than 59 seconds.")}else a("Milliseconds must be given in three digits.");else a('No decimal separator (".") found.')}else a("Must be exactly two digits.");else a("No time unit separator found.");else a("Timestamp must start with a character in the range 0-9.");else a("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&a("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[s])&&a("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[s])if(s++,"-"==e[s])if(s++,">"==e[s]){if(s++,i.test(e[s])&&a("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&a("End timestamp is not greater than start timestamp."),i.test(e[s]),r(n),function(e,t){for(var i=e.split(n),s=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),c=i[r].slice(o+1);if(-1!=s.indexOf(l)&&a("Duplicate setting."),s.push(l),""==c)return void a("No value for setting defined.");if("vertical"==l){if("rl"!=c&&"lr"!=c){a("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=c}else if("line"==l){if(/,/.test(c)){var f=c.split(",");c=f[0];var u=f[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(c)){a("Line position takes a number or percentage.");continue}if(-1!=c.indexOf("-",1)){a("Line position can only have '-' at the start.");continue}if(-1!=c.indexOf("%")&&c.indexOf("%")!=c.Line truncated
|
||||
Vendored
+1
-1
@@ -55,4 +55,4 @@
|
||||
*
|
||||
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))a.call(t,n)&&r(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&r(e,n,t[n]);return e},l=(e,t,n)=>new Promise((i,a)=>{var s=e=>{try{o(n.next(e))}catch(t){a(t)}},r=e=>{try{o(n.throw(e))}catch(t){a(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,r);o((n=n.apply(e,t)).next())}),f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},c=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),a=0,s=t.split(/\r\n|\r|\n/),r=!1,o=[],l=[],c=[];function p(e,t){c.push({message:e,line:a+1,col:t})}var m=s[a],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),a++;""!=s[a]&&void 0!=s[a];){if(p("No blank line after the signature."),-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}for(;void 0!=s[a];){for(var x;!r&&""==s[a];)a++;if(!r&&void 0==s[a])break;x=Object.assign({},f,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==s[a].indexOf("--\x3e")){if(x.id=s[a],/^NOTE($|[ \t])/.test(x.id)){for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),a++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(s[a]),a++;if(l.length){p("Style blocks cannot appear after the first cue.");continue}w||o.push(y.join("\n"));continue}if(""==s[++a]||void 0==s[a]){p("Cue identifier cannot be standalone.");continue}if(-1==s[a].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}r=!1;var O=new u(s[a],p),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!T||O.parse(x,P)){for(a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){p("Blank line missing before cue."),r=!0;break}""!=x.text&&(x.text+="\n"),x.text+=s[a],a++}var I=new d(x.text,p,n,e);x.tree=I.parse(x.startTime,x.endTime),l.push(x)}else for(x=null,a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:o}}},u=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,a=(e=e,0),s=function(e){t(e,a+1)};function r(t){for(;void 0!=e[a]&&t.test(e[a]);)a++}function o(t){for(var n="";void 0!=e[a]&&t.test(e[a]);)n+=e[a],a++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[a])if(/\d/.test(e[a]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[a])&&s("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[a])if(a++,"-"==e[a])if(a++,">"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r<i.length;r++)ifLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())});var f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),a=!1,o=[],l=[],c=[];function u(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&u('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(u("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}for(;void 0!=s[r];){for(var y;!a&&""==s[r];)r++;if(!a&&void 0==s[r])break;y=Object.assign({},f,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var x=!0;if(-1==s[r].indexOf("--\x3e")){if(y.id=s[r],/^NOTE($|[ \t])/.test(y.id)){for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&u("Cannot have timestamp in a comment."),r++;continue}if(/^STYLE($|[ \t])/.test(y.id)){var T=[],w=!1;for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&(u("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){u("Style blocks cannot appear after the first cue.");continue}w||o.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){u("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,u("Cue identifier needs to be followed by timestamp.");continue}}a=!1;var O=new d(s[r],u),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!x||O.parse(y,P)){for(r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){u("Blank line missing before cue."),a=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,u,n,e);y.tree=A.parse(y.startTime,y.endTime),l.push(y)}else for(y=null,r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:o}}},d=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,r=(e=e,0),s=function(e){t(e,r+1)};function a(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function o(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,a,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[r]){if(":"!=e[r])return void s("No seconds found or minutes is greater than 59.");if(r++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[r])if(r++,3==(a=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(a,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(a(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[r])&&s("Timestamp not separated from '--\x3e' by whitespace."),a(n),"-"==e[r])if(r++,"-"==e[r])if(r++,">"==e[r]){if(r++,i.test(e[r])&&s("'--\x3e' not separated from timestamp by whitespace."),a(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[r]),a(n),function(e,t){for(var i=e.split(n),r=[Line truncated
|
||||
+195
-64
@@ -55,6 +55,56 @@
|
||||
*
|
||||
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
|
||||
*/
|
||||
function createLifetime(art) {
|
||||
let closed = Boolean(art.isDestroy);
|
||||
const cleanups = /* @__PURE__ */ new Set();
|
||||
let cancel;
|
||||
const cancelled = new Promise((resolve) => {
|
||||
cancel = resolve;
|
||||
});
|
||||
function run(cleanup) {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (error) {
|
||||
console.warn("Failed to clean up multiple subtitles:", error);
|
||||
}
|
||||
}
|
||||
const lifetime = {
|
||||
get closed() {
|
||||
return closed;
|
||||
},
|
||||
own(cleanup) {
|
||||
if (closed)
|
||||
run(cleanup);
|
||||
else
|
||||
cleanups.add(cleanup);
|
||||
return () => cleanups.delete(cleanup);
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
if (!closed) {
|
||||
lifetime.own(() => art.off("destroy", lifetime.dispose));
|
||||
try {
|
||||
art.on("destroy", lifetime.dispose);
|
||||
} catch (error) {
|
||||
lifetime.dispose();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return lifetime;
|
||||
}
|
||||
var defaultCueSettings = {
|
||||
direction: "horizontal",
|
||||
snapToLines: true,
|
||||
@@ -799,85 +849,166 @@ var WebVTTSerializer = function() {
|
||||
return result;
|
||||
};
|
||||
};
|
||||
async function loadVtt(option, { getExt, srtToVtt, assToVtt }) {
|
||||
const response = await fetch(option.url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const decoder = new TextDecoder(option.encoding || "utf-8");
|
||||
const text = decoder.decode(buffer);
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case "srt": {
|
||||
return srtToVtt(text);
|
||||
}
|
||||
case "ass": {
|
||||
return assToVtt(text);
|
||||
}
|
||||
case "vtt": {
|
||||
return text;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function mergeTrees(trees) {
|
||||
function parseTracks(vtts, subtitles) {
|
||||
const parser = new WebVTTParser();
|
||||
const result = parser.parse("", "metadata");
|
||||
for (let i = 0; i < trees.length; i++) {
|
||||
const tree = trees[i];
|
||||
if (!tree.updated) {
|
||||
tree.updated = true;
|
||||
for (let j = 0; j < tree.cues.length; j++) {
|
||||
const cue = tree.cues[j];
|
||||
for (let k = 0; k < cue.tree.children.length; k++) {
|
||||
const children = cue.tree.children[k];
|
||||
children.value = `<div class="art-subtitle-${tree.name}">${children.value}</div>`;
|
||||
return vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, "metadata");
|
||||
tree.url = subtitles[index].url;
|
||||
tree.name = subtitles[index].name;
|
||||
return tree;
|
||||
});
|
||||
}
|
||||
function serializeTracks(trees) {
|
||||
const cues = [];
|
||||
for (const tree of trees) {
|
||||
for (const cue of tree.cues) {
|
||||
cues.push({
|
||||
...cue,
|
||||
tree: {
|
||||
...cue.tree,
|
||||
children: cue.tree.children.map((child) => ({
|
||||
...child,
|
||||
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
result.cues.push(...tree.cues);
|
||||
}
|
||||
return result;
|
||||
return new WebVTTSerializer().serialize(cues);
|
||||
}
|
||||
function createRenderer(art, lifetime, unescape) {
|
||||
let current = null;
|
||||
function own(url) {
|
||||
let freed = false;
|
||||
let release = () => {
|
||||
};
|
||||
const entry = {
|
||||
free() {
|
||||
if (freed)
|
||||
return;
|
||||
freed = true;
|
||||
release();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
release = lifetime.own(entry.free);
|
||||
return entry;
|
||||
}
|
||||
return (vtt) => {
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
|
||||
const entry = own(url);
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const previous = current;
|
||||
let option;
|
||||
let escape;
|
||||
let configured = false;
|
||||
current = entry;
|
||||
try {
|
||||
option = art.option.subtitle;
|
||||
escape = option.escape;
|
||||
if (lifetime.closed || current !== entry)
|
||||
return;
|
||||
option.escape = false;
|
||||
configured = true;
|
||||
const config = { ...option, url, type: "vtt", onVttLoad: unescape };
|
||||
if (lifetime.closed || current !== entry)
|
||||
return;
|
||||
const pending = art.subtitle.init(config);
|
||||
Promise.resolve(pending).catch((error) => {
|
||||
if (current === entry) {
|
||||
current = null;
|
||||
entry.free();
|
||||
}
|
||||
if (!lifetime.closed)
|
||||
console.warn("Failed to initialize multiple subtitles:", error);
|
||||
});
|
||||
} catch (error) {
|
||||
if (current === entry) {
|
||||
current = previous;
|
||||
if (configured && option.escape === false)
|
||||
option.escape = escape;
|
||||
}
|
||||
entry.free();
|
||||
throw error;
|
||||
} finally {
|
||||
if (previous !== current)
|
||||
previous?.free();
|
||||
if (entry !== current)
|
||||
entry.free();
|
||||
}
|
||||
};
|
||||
}
|
||||
async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
||||
const release = lifetime.own(() => controller?.abort());
|
||||
try {
|
||||
const url = option.url;
|
||||
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 multiple subtitles: HTTP ${response.status}`);
|
||||
const buffer = await lifetime.wait(response.arrayBuffer());
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
const text = new TextDecoder(option.encoding || "utf-8").decode(buffer);
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case "srt":
|
||||
return srtToVtt(text);
|
||||
case "ass":
|
||||
return assToVtt(text);
|
||||
case "vtt":
|
||||
return text;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
} catch (error) {
|
||||
controller?.abort();
|
||||
throw error;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
function artplayerPluginMultipleSubtitles({ subtitles = [] }) {
|
||||
return async (art) => {
|
||||
const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils;
|
||||
const parser = new WebVTTParser();
|
||||
const seri = new WebVTTSerializer();
|
||||
const vtts = await Promise.all(
|
||||
subtitles.map((option) => {
|
||||
return loadVtt(option, { getExt, srtToVtt, assToVtt });
|
||||
})
|
||||
);
|
||||
const trees = vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, "metadata");
|
||||
tree.url = subtitles[index].url;
|
||||
tree.name = subtitles[index].name;
|
||||
return tree;
|
||||
});
|
||||
let lastUrl = "";
|
||||
function setTracks(trees2 = []) {
|
||||
const tree = mergeTrees(trees2);
|
||||
const vtt = seri.serialize(tree.cues);
|
||||
URL.revokeObjectURL(lastUrl);
|
||||
const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
|
||||
lastUrl = url;
|
||||
art.option.subtitle.escape = false;
|
||||
art.subtitle.init({
|
||||
...art.option.subtitle,
|
||||
url,
|
||||
type: "vtt",
|
||||
onVttLoad: unescape
|
||||
});
|
||||
const lifetime = createLifetime(art);
|
||||
const render = createRenderer(art, lifetime, unescape);
|
||||
let trees = [];
|
||||
function setTracks(selected) {
|
||||
if (!lifetime.closed)
|
||||
render(serializeTracks(selected));
|
||||
}
|
||||
setTracks(trees);
|
||||
return {
|
||||
const result = {
|
||||
name: "multipleSubtitles",
|
||||
tracks(names = []) {
|
||||
return setTracks(names.map((name) => trees.find((tree) => tree.name === name)));
|
||||
if (!lifetime.closed)
|
||||
setTracks(names.map((name) => trees.find((tree) => tree.name === name)));
|
||||
},
|
||||
reset() {
|
||||
return setTracks(trees);
|
||||
setTracks(trees);
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (lifetime.closed)
|
||||
return result;
|
||||
const vtts = await Promise.all(subtitles.map((option) => loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime)));
|
||||
if (lifetime.closed)
|
||||
return result;
|
||||
trees = parseTracks(vtts, subtitles);
|
||||
setTracks(trees);
|
||||
return result;
|
||||
} catch (error) {
|
||||
lifetime.dispose();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
export {
|
||||
|
||||
@@ -1,95 +1,41 @@
|
||||
import { WebVTTParser, WebVTTSerializer } from './parser'
|
||||
|
||||
async function loadVtt(option, { getExt, srtToVtt, assToVtt }) {
|
||||
const response = await fetch(option.url)
|
||||
const buffer = await response.arrayBuffer()
|
||||
const decoder = new TextDecoder(option.encoding || 'utf-8')
|
||||
const text = decoder.decode(buffer)
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case 'srt': {
|
||||
return srtToVtt(text)
|
||||
}
|
||||
case 'ass': {
|
||||
return assToVtt(text)
|
||||
}
|
||||
case 'vtt': {
|
||||
return text
|
||||
}
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function mergeTrees(trees) {
|
||||
const parser = new WebVTTParser()
|
||||
const result = parser.parse('', 'metadata')
|
||||
|
||||
for (let i = 0; i < trees.length; i++) {
|
||||
const tree = trees[i]
|
||||
|
||||
if (!tree.updated) {
|
||||
tree.updated = true
|
||||
for (let j = 0; j < tree.cues.length; j++) {
|
||||
const cue = tree.cues[j]
|
||||
for (let k = 0; k < cue.tree.children.length; k++) {
|
||||
const children = cue.tree.children[k]
|
||||
children.value = `<div class="art-subtitle-${tree.name}">${children.value}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.cues.push(...tree.cues)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
import createLifetime from './lifetime'
|
||||
import { parseTracks, serializeTracks } from './merge'
|
||||
import createRenderer from './render'
|
||||
import loadVtt from './request'
|
||||
|
||||
export default function artplayerPluginMultipleSubtitles({ subtitles = [] }) {
|
||||
return async (art) => {
|
||||
const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils
|
||||
|
||||
const parser = new WebVTTParser()
|
||||
const seri = new WebVTTSerializer()
|
||||
|
||||
const vtts = await Promise.all(
|
||||
subtitles.map((option) => {
|
||||
return loadVtt(option, { getExt, srtToVtt, assToVtt })
|
||||
}),
|
||||
)
|
||||
|
||||
const trees = vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, 'metadata')
|
||||
tree.url = subtitles[index].url
|
||||
tree.name = subtitles[index].name
|
||||
return tree
|
||||
})
|
||||
|
||||
let lastUrl = ''
|
||||
function setTracks(trees = []) {
|
||||
const tree = mergeTrees(trees)
|
||||
const vtt = seri.serialize(tree.cues)
|
||||
URL.revokeObjectURL(lastUrl)
|
||||
const url = URL.createObjectURL(new Blob([vtt], { type: 'text/vtt' }))
|
||||
lastUrl = url
|
||||
art.option.subtitle.escape = false
|
||||
art.subtitle.init({
|
||||
...art.option.subtitle,
|
||||
url,
|
||||
type: 'vtt',
|
||||
onVttLoad: unescape,
|
||||
})
|
||||
const lifetime = createLifetime(art)
|
||||
const render = createRenderer(art, lifetime, unescape)
|
||||
let trees = []
|
||||
function setTracks(selected) {
|
||||
if (!lifetime.closed)
|
||||
render(serializeTracks(selected))
|
||||
}
|
||||
|
||||
setTracks(trees)
|
||||
|
||||
return {
|
||||
const result = {
|
||||
name: 'multipleSubtitles',
|
||||
tracks(names = []) {
|
||||
return setTracks(names.map(name => trees.find(tree => tree.name === name)))
|
||||
if (!lifetime.closed)
|
||||
setTracks(names.map(name => trees.find(tree => tree.name === name)))
|
||||
},
|
||||
reset() {
|
||||
return setTracks(trees)
|
||||
setTracks(trees)
|
||||
},
|
||||
}
|
||||
try {
|
||||
if (lifetime.closed)
|
||||
return result
|
||||
const vtts = await Promise.all(subtitles.map(option => loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime)))
|
||||
if (lifetime.closed)
|
||||
return result
|
||||
trees = parseTracks(vtts, subtitles)
|
||||
setTracks(trees)
|
||||
return result
|
||||
}
|
||||
catch (error) {
|
||||
lifetime.dispose()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
export default function createLifetime(art) {
|
||||
let closed = Boolean(art.isDestroy)
|
||||
const cleanups = new Set()
|
||||
let cancel
|
||||
const cancelled = new Promise((resolve) => {
|
||||
cancel = resolve
|
||||
})
|
||||
function run(cleanup) {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to clean up multiple subtitles:', error)
|
||||
}
|
||||
}
|
||||
const lifetime = {
|
||||
get closed() { return closed },
|
||||
own(cleanup) {
|
||||
if (closed)
|
||||
run(cleanup)
|
||||
else
|
||||
cleanups.add(cleanup)
|
||||
return () => cleanups.delete(cleanup)
|
||||
},
|
||||
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)
|
||||
},
|
||||
}
|
||||
if (!closed) {
|
||||
lifetime.own(() => art.off('destroy', lifetime.dispose))
|
||||
try {
|
||||
art.on('destroy', lifetime.dispose)
|
||||
}
|
||||
catch (error) {
|
||||
lifetime.dispose()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return lifetime
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { WebVTTParser, WebVTTSerializer } from './parser'
|
||||
|
||||
export function parseTracks(vtts, subtitles) {
|
||||
const parser = new WebVTTParser()
|
||||
return vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, 'metadata')
|
||||
tree.url = subtitles[index].url
|
||||
tree.name = subtitles[index].name
|
||||
return tree
|
||||
})
|
||||
}
|
||||
|
||||
export function serializeTracks(trees) {
|
||||
const cues = []
|
||||
for (const tree of trees) {
|
||||
for (const cue of tree.cues) {
|
||||
cues.push({
|
||||
...cue,
|
||||
tree: {
|
||||
...cue.tree,
|
||||
children: cue.tree.children.map(child => ({
|
||||
...child,
|
||||
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`,
|
||||
})),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return new WebVTTSerializer().serialize(cues)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
export default function createRenderer(art, lifetime, unescape) {
|
||||
let current = null
|
||||
function own(url) {
|
||||
let freed = false
|
||||
let release = () => {}
|
||||
const entry = {
|
||||
free() {
|
||||
if (freed)
|
||||
return
|
||||
freed = true
|
||||
release()
|
||||
URL.revokeObjectURL(url)
|
||||
},
|
||||
}
|
||||
release = lifetime.own(entry.free)
|
||||
return entry
|
||||
}
|
||||
return (vtt) => {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
// Allocate before replacing the active URL so allocation failure preserves it.
|
||||
const url = URL.createObjectURL(new Blob([vtt], { type: 'text/vtt' }))
|
||||
const entry = own(url)
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const previous = current
|
||||
let option
|
||||
let escape
|
||||
let configured = false
|
||||
current = entry
|
||||
try {
|
||||
option = art.option.subtitle
|
||||
escape = option.escape
|
||||
if (lifetime.closed || current !== entry)
|
||||
return
|
||||
option.escape = false
|
||||
configured = true
|
||||
const config = { ...option, url, type: 'vtt', onVttLoad: unescape }
|
||||
if (lifetime.closed || current !== entry)
|
||||
return
|
||||
const pending = art.subtitle.init(config)
|
||||
// Selection stays void and registration does not await the host's media loading.
|
||||
Promise.resolve(pending).catch((error) => {
|
||||
if (current === entry) {
|
||||
current = null
|
||||
entry.free()
|
||||
}
|
||||
if (!lifetime.closed)
|
||||
console.warn('Failed to initialize multiple subtitles:', error)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (current === entry) {
|
||||
current = previous
|
||||
if (configured && option.escape === false)
|
||||
option.escape = escape
|
||||
}
|
||||
entry.free()
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
if (previous !== current)
|
||||
previous?.free()
|
||||
if (entry !== current)
|
||||
entry.free()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export default async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
||||
const release = lifetime.own(() => controller?.abort())
|
||||
try {
|
||||
const url = option.url
|
||||
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 multiple subtitles: HTTP ${response.status}`)
|
||||
const buffer = await lifetime.wait(response.arrayBuffer())
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const text = new TextDecoder(option.encoding || 'utf-8').decode(buffer)
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case 'srt': return srtToVtt(text)
|
||||
case 'ass': return assToVtt(text)
|
||||
case 'vtt': return text
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
controller?.abort()
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,73 @@
|
||||
# PKG-MULTI-SUB-03 模块边界与资源修复
|
||||
|
||||
本次修改生产源码,保持工厂名称/必需参数、async 注册结果、name/tracks/reset、同步 void
|
||||
选择方法和原分发入口。自有 JS 先按职责拆清,严格 TS 与公开声明仍由 04 完成。
|
||||
原始 parser、许可和公开类型未改,没有新增依赖或改变包版本。
|
||||
|
||||
## 结构变化
|
||||
|
||||
| 文件 | 实际职责 |
|
||||
| --- | --- |
|
||||
| index.js | 注册编排、名称选择、活跃状态检查和公开返回对象 |
|
||||
| request.js | 每轨请求、原生状态检查、字节解码与既有核心格式转换 |
|
||||
| merge.js | 解析元数据和按选择顺序序列化;通过复制 cue 包装文本,原树不变 |
|
||||
| lifetime.js | 本次注册的 destroy 监听、请求取消、Promise 提前结束与清理登记 |
|
||||
| render.js | Blob 所有权、新旧 URL 替换、重入保护及宿主 init 失败处理 |
|
||||
| parser.js | 保持原始 vendor 边界,不因本次拆分改写算法 |
|
||||
|
||||
模块依赖由入口向请求/纯解析/安装流动;仅入口和宿主安装/生命周期需要 art,纯 merge
|
||||
不依赖 DOM、fetch 或播放器。没有引入要求用户升级核心的共享运行时依赖。
|
||||
|
||||
## 独立缺陷修正
|
||||
|
||||
- 销毁中 fetch/body 通过取消信号和关闭 Promise 结束;无 AbortController 的旧环境仍
|
||||
能及时结束注册并忽略迟到结果。返回相同形状的失活结果,不新增 destroy 拒绝。
|
||||
- 单轨失败保留原传输/解码异常身份,关闭整个注册并取消兄弟请求;HTTP ok=false 明确
|
||||
拒绝且取消未消费的响应,不再把错误页面当作有效字幕。
|
||||
- 最后 Blob URL 在 destroy 时释放。保留的 tracks/reset 此后返回 undefined,不再
|
||||
读取名称、分配 URL 或触发宿主。重复 destroy 不重复释放。
|
||||
- 分配新 URL 成功后才尝试替换;分配失败保留旧 URL。同步 init 失败释放新 URL,
|
||||
保留先前资源并回滚该次 escape 写入;首次注册失败同时移除监听。
|
||||
- 观察宿主真实 Promise 的 rejection,释放失败安装对应的 URL,并在活跃时以 console.warn
|
||||
保留诊断。正常注册仍不等待宿主加载;tracks/reset 仍为 void。宿主核心可能自行显示
|
||||
notice,这里不引入新公开事件,也不假装异步错误会从旧 void 方法同步抛出。
|
||||
- 老 init 的迟到错误不释放后来成功替换的资源;同步重入选择、配置读取失败,以及
|
||||
用户 getter/init 中触发 destroy 都有明确归属检查。
|
||||
|
||||
异步 init 失败发生时,宿主可能已经更新自身状态;插件不能通用回滚第三方宿主的副作用。
|
||||
此时释放失败资源,保留旧异步接口时序并记录错误;调用方可在仍活跃时重新 reset。
|
||||
正常 destroy 不恢复共享 subtitle.escape,避免覆盖后续插件/用户的配置。
|
||||
|
||||
## 保留的兼容边界
|
||||
|
||||
1. 采用已发布 1.2.0 的逐轨连接语义;没有恢复 1.0.0 按下标丢 cue 的缺陷。
|
||||
2. 轨内 parser 排序、轨间选择顺序、同名取第一个、重复选择、未知名 TypeError、空轨、
|
||||
onParser 未执行,以及坏 header 仍可保留可用 cue 的容错均保留。
|
||||
3. option.type 优先、当前核心 SRT/ASS 转换、encoding 默认与原生解码行为保持。
|
||||
轨元数据在下载结束后读取;restart 不重新下载字幕,配置在每次安装时读取。
|
||||
4. 不再调用无实际资源的 revokeObjectURL('')。历史测试仍断言旧调用记录,候选正常
|
||||
测试仅对这一项无资源清理差异作明确区分,不放宽真实 URL 的释放断言。
|
||||
5. 合并不再原地标记 updated/改 cue;冻结树重复序列化测试和九种真实旧产物逐字对照
|
||||
证明输出保持,另对实际核心转换和 Windows-1252/UTF-16LE 字节逐字比较。
|
||||
|
||||
## 验证与限制
|
||||
|
||||
命令及准确运行结果、输入指纹、原生浏览器观察见
|
||||
[验证记录](../baselines/multiple-subtitles-resources.json)。新生命周期/merge 文件接入
|
||||
test:unit 与 test:multiple-subtitles,生产三入口由正常 yarn build 生成。
|
||||
浏览器矩阵使用发布/候选核心、Chromium/Firefox/Windows WebKit 和源码/现代/legacy
|
||||
插件,覆盖 SRT 显示/选择、挂起请求销毁、原生 Blob 不可访问、HTTP 失败及真实视频切源。
|
||||
其中资源/错误场景直接执行公开注册函数,显示与切源场景使用真实 plugins.add。
|
||||
|
||||
最终本包 241 项通过;源码/main/legacy 各 31 项通过(其中冻结树纯函数用例固定读取源码,
|
||||
其余正常/生命周期/转换对照通过选定入口)。三入口原生浏览器各 30 项,共 90 项通过。
|
||||
完整 CI 2311 项通过:1963 单元、14 工程、334 基线;严格生产 TS 仍 346 文件。
|
||||
状态更新后 9 项风险/契约元数据测试通过。显式 lint 无错误或警告;全量 lint 仍有既有
|
||||
生成声明 unused-disable 警告。实际 ESM 只验证导出形状和工厂,不冒充完整安装消费者。
|
||||
|
||||
这不是全部历史核心、实体 Safari/移动设备、ASS 原生显示、样式/编辑器或 npm 发布准入。
|
||||
HOST/HTTP 在本次边界回归后关闭;LIFE/MERGE 的完整支持范围组合仍由 05 补齐。
|
||||
每项状态以 tasks/risks 与验证记录为准;后续 04 迁移 TS 时重跑相同生产入口用例。
|
||||
|
||||
独立本地任务提交,不推送、不发布。回退本提交恢复旧单文件和生成产物,同时重新引入
|
||||
02 已复现的泄漏/失败行为;02 历史基线及浏览器证据保留。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 90 / doing 13 / blocked 0 / done 119 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 89 / doing 13 / blocked 0 / done 120 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-MULTI-SUB-01 | artplayer-plugin-multiple-subtitles<br>核对包契约与历史用法 | BASE-05 | subtitles/onParser、multipleSubtitles 名称及实际返回方法 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 VENDOR-03,定位 WebVTT parser 来源/修订及 CC0 头,保留解析/序列化行为 | H | done |
|
||||
| PKG-MULTI-SUB-02 | artplayer-plugin-multiple-subtitles<br>建立特有行为与错误测试 | PKG-MULTI-SUB-01, ENG-03, ENG-05 | VTT/SRT/ASS、编码/重叠 cue/排序/空轨/失败与切换 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-MULTI-SUB-03 | artplayer-plugin-multiple-subtitles<br>整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-MULTI-SUB-03 | artplayer-plugin-multiple-subtitles<br>整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
|
||||
| PKG-MULTI-SUB-04 | artplayer-plugin-multiple-subtitles<br>迁移自有源码和公开类型 | PKG-MULTI-SUB-03, ENG-04, ENG-06, CORE-07 | 准确 parser/cue/tree/result 类型与真实异步形状 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles<br>验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-MULTI-SUB-06 | artplayer-plugin-multiple-subtitles<br>验证分发并同步文档 | PKG-MULTI-SUB-05, ENG-07 | multiple.subtitles.js、parser 来源许可及打包边界 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
@@ -513,6 +513,7 @@
|
||||
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md) [记录](baselines/dash-seek-diagnosis.json)
|
||||
- PKG-MULTI-SUB-01: [记录](baselines/multiple-subtitles-release.json) [记录](baselines/multiple-subtitles-contract.md) [记录](baselines/multiple-subtitles-contract-validation.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-01-contract.md)
|
||||
- PKG-MULTI-SUB-02: [记录](baselines/multiple-subtitles-failures.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-02-failures.md)
|
||||
- PKG-MULTI-SUB-03: [记录](baselines/multiple-subtitles-resources.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-03-resources.md)
|
||||
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
|
||||
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
|
||||
- PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json)
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MULTI-SUB-03 模块与资源修复完成
|
||||
|
||||
生产源码拆为入口、请求、纯解析合并、生命周期和宿主安装五个模块,vendor parser 不变。
|
||||
修复 HTTP 失败、兄弟请求取消、fetch/body 销毁中结算、Blob 泄漏、分配失败替换顺序、
|
||||
同步/异步安装失败与重入归属;保留 async 注册与 void tracks/reset、轨序、元数据和容错。
|
||||
九种 markup/timing/空轨输入及真实转换/编码逐字对照已发布 1.2.0,冻结树重复序列化通过。
|
||||
源码/main/legacy 各 31 项通过;本包 241 项通过,两新增文件已接入主 test:unit。
|
||||
三入口配发布/候选核心,在 Chromium/Firefox/Windows WebKit 共 90 项通过:真实 SRT、
|
||||
选择/清空/reset、请求销毁、Blob 不可访问、HTTP 失败和真实视频切源无重复字幕下载。
|
||||
完整 CI 2311 项通过(1963 单元、14 工程、334 基线);严格生产 TS 仍 346,04 继续 TS。
|
||||
正常生产构建及实际 ESM 导入通过,三个 dist 与 docs/compiled 对应字节一致。
|
||||
HTTP/HOST 风险关闭;LIFE/MERGE 完整支持范围组合仍待 05。见
|
||||
[变更](changes/2026-09-13-PKG-MULTI-SUB-03-resources.md)和[验证](baselines/multiple-subtitles-resources.json)。
|
||||
222 项:120 done、13 doing、89 todo;196 项风险。独立本地提交,不推送、不发布。
|
||||
下一步 MULTI-SUB-04 自有模块严格 TS 与旧消费者公开类型;VTT 类型取舍继续待决。
|
||||
|
||||
|
||||
## PKG-MULTI-SUB-02 错误与生命周期复现完成
|
||||
|
||||
9 份历史实现各 18 类异常/边界,共 162 项;本包命令及主 test:unit 已接入,
|
||||
|
||||
@@ -198,5 +198,5 @@
|
||||
| MULTI-SUB-MERGE-01 | open / 已复现 | 1.0.0 merges cues by index while later versions concatenate independently timed track cues | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-LIFE-01 | open / 已复现 | Pending downloads and final object URL have no destroy owner | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-EXPORT-01 | open / 已复现 | Older CommonJS default objects conflict with export-assignment declarations; latest exports a direct factory | PKG-MULTI-SUB-04, PKG-MULTI-SUB-06 |
|
||||
| MULTI-SUB-HTTP-01 | open / 已复现 | HTTP error responses with parseable bodies are installed as subtitles | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-HOST-01 | open / 已复现 | Subtitle init rejections are ignored and failed installation or URL replacement leaks or invalidates resources | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-HTTP-01 | resolved / 已复现 | HTTP error responses with parseable bodies are installed as subtitles | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-HOST-01 | resolved / 已复现 | Subtitle init rejections are ignored and failed installation or URL replacement leaks or invalidates resources | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
+48
-10
@@ -4279,10 +4279,16 @@
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md",
|
||||
"refactor/baselines/multiple-subtitles-resources.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md",
|
||||
"test/multiple-subtitles-lifecycle.test.js",
|
||||
"test/multiple-subtitles-merge.test.js",
|
||||
"test/browser/multiple-subtitles-lifecycle.spec.js"
|
||||
],
|
||||
"compatibleResolution": "Keep historical merge profiles explicit and test real overlap/timing before choosing compatibility behavior.",
|
||||
"closureCriteria": "Unaligned lengths, overlap, sorting, names and real subtitle display across supported cores are verified."
|
||||
"closureCriteria": "Unaligned lengths, overlap, sorting, names and real subtitle display across supported cores are verified.",
|
||||
"workspaceState": "Candidate resource fixes and serialization parity are verified in task 03. This risk remains open for the complete supported-core/device coverage owned by task 05."
|
||||
},
|
||||
{
|
||||
"id": "MULTI-SUB-LIFE-01",
|
||||
@@ -4302,10 +4308,16 @@
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md",
|
||||
"refactor/baselines/multiple-subtitles-resources.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md",
|
||||
"test/multiple-subtitles-lifecycle.test.js",
|
||||
"test/multiple-subtitles-merge.test.js",
|
||||
"test/browser/multiple-subtitles-lifecycle.spec.js"
|
||||
],
|
||||
"compatibleResolution": "Reproduce stale completion and own requests and URLs without changing public selection methods.",
|
||||
"closureCriteria": "Cancellation, failures, source changes, repeated selection and destroy are verified with native core coverage."
|
||||
"closureCriteria": "Cancellation, failures, source changes, repeated selection and destroy are verified with native core coverage.",
|
||||
"workspaceState": "Candidate resource fixes and serialization parity are verified in task 03. This risk remains open for the complete supported-core/device coverage owned by task 05."
|
||||
},
|
||||
{
|
||||
"id": "MULTI-SUB-EXPORT-01",
|
||||
@@ -4331,7 +4343,7 @@
|
||||
"compatibleResolution": "Reject actual HTTP failures with explicit diagnostics while preserving successful conversion and original transport errors.",
|
||||
"closureCriteria": "Native HTTP errors, transport/body failures, sibling cancellation and successful requests have candidate regressions.",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-03",
|
||||
"PKG-MULTI-SUB-05"
|
||||
@@ -4340,8 +4352,21 @@
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
]
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md",
|
||||
"refactor/baselines/multiple-subtitles-resources.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md",
|
||||
"test/multiple-subtitles-lifecycle.test.js",
|
||||
"test/multiple-subtitles-merge.test.js",
|
||||
"test/browser/multiple-subtitles-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionEvidence": [
|
||||
"refactor/baselines/multiple-subtitles-resources.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md",
|
||||
"test/multiple-subtitles-lifecycle.test.js",
|
||||
"test/multiple-subtitles-merge.test.js",
|
||||
"test/browser/multiple-subtitles-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "HTTP failures reject before body consumption and abort their requests. Exact transport/body errors, sibling cancellation, conversion success and native 404 rejection pass candidate regressions; source/main/legacy run with both core fixtures."
|
||||
},
|
||||
{
|
||||
"id": "MULTI-SUB-HOST-01",
|
||||
@@ -4349,7 +4374,7 @@
|
||||
"compatibleResolution": "Own installation results and URL rollback while preserving void selection methods and normal registration timing.",
|
||||
"closureCriteria": "Synchronous and asynchronous host failures, stale rejection, allocation failure and destroy leave no plugin-owned live resources.",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-03",
|
||||
"PKG-MULTI-SUB-05"
|
||||
@@ -4358,8 +4383,21 @@
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
]
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md",
|
||||
"refactor/baselines/multiple-subtitles-resources.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md",
|
||||
"test/multiple-subtitles-lifecycle.test.js",
|
||||
"test/multiple-subtitles-merge.test.js",
|
||||
"test/browser/multiple-subtitles-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionEvidence": [
|
||||
"refactor/baselines/multiple-subtitles-resources.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md",
|
||||
"test/multiple-subtitles-lifecycle.test.js",
|
||||
"test/multiple-subtitles-merge.test.js",
|
||||
"test/browser/multiple-subtitles-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Owned installations retain prior URLs on synchronous failure, observe async rejections, release the failed active URL without affecting newer installs, and clean up on destroy. Controlled sync/async/reentrant/getter failures and native Blob revocation pass; void methods and registration timing are preserved."
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-2
@@ -2297,11 +2297,14 @@
|
||||
"PKG-MULTI-SUB-02",
|
||||
"CORE-15"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "解析与合并/获取/渲染分离,过期请求及对象 URL 清理",
|
||||
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"baselines/multiple-subtitles-resources.json",
|
||||
"changes/2026-09-13-PKG-MULTI-SUB-03-resources.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MULTI-SUB-04",
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { multipleSubtitlesCandidate } from '../helpers/multiple-subtitles.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await multipleSubtitlesCandidate()
|
||||
const vtt = 'WEBVTT\n\n00:00.000 --> 00:08.000\nCached caption\n'
|
||||
const srt = text => `1\n00:00:00,000 --> 00:00:08,000\n${text}\n`
|
||||
|
||||
async function prepare(page, core) {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: implementation.code })
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.multipleProbe = { initialized: [], revoked: [], signal: null, errors: [], requests: [] }
|
||||
const fetch = window.fetch.bind(window)
|
||||
window.multipleNativeFetch = fetch
|
||||
window.fetch = (url, options) => {
|
||||
if (typeof url === 'string' && url.startsWith('/test/multiple-'))
|
||||
window.multipleProbe.requests.push(url)
|
||||
if (url === '/test/multiple-held.vtt')
|
||||
window.multipleProbe.signal = options?.signal
|
||||
return fetch(url, options)
|
||||
}
|
||||
const revoke = URL.revokeObjectURL.bind(URL)
|
||||
URL.revokeObjectURL = (url) => {
|
||||
window.multipleProbe.revoked.push(url)
|
||||
revoke(url)
|
||||
}
|
||||
const init = window.art.subtitle.init.bind(window.art.subtitle)
|
||||
window.art.subtitle.init = (option) => {
|
||||
window.multipleProbe.initialized.push({ url: option.url, destroyed: window.art.isDestroy })
|
||||
const result = init(option)
|
||||
Promise.resolve(result).catch(error => window.multipleProbe.errors.push({ name: error.name, message: error.message }))
|
||||
return result
|
||||
}
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
async function evidence(page, testInfo, core, observation) {
|
||||
const probe = await page.evaluate(() => ({ ...window.multipleProbe, signal: window.multipleProbe.signal ? { aborted: window.multipleProbe.signal.aborted } : null }))
|
||||
expect(probe.errors).toEqual([])
|
||||
await testInfo.attach('multiple-subtitles-lifecycle', { body: JSON.stringify({ core, implementation: { name: implementation.name, sha256: hash(implementation.code) }, observation, probe }), contentType: 'application/json' })
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`Multiple subtitles candidate ${core} core: SRT display and selectors survive the resource refactor`, async ({ page }, testInfo) => {
|
||||
await page.route('**/test/multiple-en.srt', route => route.fulfill({ body: srt('English'), contentType: 'text/plain' }))
|
||||
await page.route('**/test/multiple-jp.srt', route => route.fulfill({ body: srt('日本語'), contentType: 'text/plain' }))
|
||||
await prepare(page, core)
|
||||
await page.evaluate(async () => {
|
||||
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'en', url: '/test/multiple-en.srt' }, { name: 'jp', url: '/test/multiple-jp.srt' }] }))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await expect(page.locator('.art-subtitle-en')).toHaveText('English')
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['jp']))
|
||||
await expect(page.locator('.art-subtitle-en')).toHaveCount(0)
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveCount(0)
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
||||
await expect(page.locator('.art-subtitle-en')).toHaveText('English')
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
||||
const released = await page.evaluate(() => {
|
||||
window.art.destroy(false)
|
||||
return window.multipleProbe.initialized.every(item => window.multipleProbe.revoked.includes(item.url))
|
||||
})
|
||||
expect(released).toBe(true)
|
||||
await evidence(page, testInfo, core, { nativeSrtDisplay: true, released })
|
||||
})
|
||||
|
||||
test(`Multiple subtitles candidate ${core} core: destroy settles a pending native request before it responds`, async ({ page }, testInfo) => {
|
||||
let requested
|
||||
let release
|
||||
const started = new Promise((resolve) => {
|
||||
requested = resolve
|
||||
})
|
||||
const held = new Promise((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
await page.route('**/test/multiple-held.vtt', async (route) => {
|
||||
requested()
|
||||
await held
|
||||
await route.fulfill({ body: vtt, contentType: 'text/vtt' })
|
||||
})
|
||||
try {
|
||||
await prepare(page, core)
|
||||
await page.evaluate(() => {
|
||||
window.multiplePending = window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-held.vtt', name: 'held' }] })(window.art)
|
||||
})
|
||||
await started
|
||||
const state = await page.evaluate(async () => {
|
||||
window.art.destroy(false)
|
||||
const result = await window.multiplePending
|
||||
return { name: result.name, voidReset: result.reset() === undefined, initialized: window.multipleProbe.initialized.length, aborted: window.multipleProbe.signal.aborted }
|
||||
})
|
||||
expect(state).toEqual({ name: 'multipleSubtitles', voidReset: true, initialized: 0, aborted: true })
|
||||
release()
|
||||
await evidence(page, testInfo, core, state)
|
||||
}
|
||||
finally { release() }
|
||||
})
|
||||
|
||||
test(`Multiple subtitles candidate ${core} core: destroy revokes the Blob and retained reset stays inert`, async ({ page }, testInfo) => {
|
||||
await page.route('**/test/multiple-ready.vtt', route => route.fulfill({ body: vtt, contentType: 'text/vtt' }))
|
||||
await prepare(page, core)
|
||||
await page.evaluate(async () => {
|
||||
window.multipleResult = await window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-ready.vtt', name: 'ready' }] })(window.art)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
||||
const state = await page.evaluate(async () => {
|
||||
const url = window.multipleProbe.initialized[0].url
|
||||
window.art.destroy(false)
|
||||
const voidReset = window.multipleResult.reset() === undefined
|
||||
let inaccessible = false
|
||||
try {
|
||||
await window.multipleNativeFetch(url)
|
||||
}
|
||||
catch (error) {
|
||||
inaccessible = error.name === 'TypeError'
|
||||
}
|
||||
return { voidReset, inaccessible, initialized: window.multipleProbe.initialized.length, revoked: window.multipleProbe.revoked.includes(url) }
|
||||
})
|
||||
expect(state).toEqual({ voidReset: true, inaccessible: true, initialized: 1, revoked: true })
|
||||
await evidence(page, testInfo, core, state)
|
||||
})
|
||||
|
||||
test(`Multiple subtitles candidate ${core} core: native HTTP error rejects without installing subtitles`, async ({ page }, testInfo) => {
|
||||
await page.route('**/test/multiple-error.vtt', route => route.fulfill({ status: 404, body: vtt, contentType: 'text/vtt' }))
|
||||
await prepare(page, core)
|
||||
const state = await page.evaluate(async () => {
|
||||
let message
|
||||
try {
|
||||
await window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-error.vtt', name: 'error' }] })(window.art)
|
||||
}
|
||||
catch (error) {
|
||||
message = error.message
|
||||
}
|
||||
window.art.destroy(false)
|
||||
return { message, initialized: window.multipleProbe.initialized.length }
|
||||
})
|
||||
expect(state).toEqual({ message: 'Failed to fetch multiple subtitles: HTTP 404', initialized: 0 })
|
||||
await evidence(page, testInfo, core, state)
|
||||
})
|
||||
|
||||
test(`Multiple subtitles candidate ${core} core: video source switching retains cached subtitle selection`, async ({ page }, testInfo) => {
|
||||
await page.route('**/test/multiple-restart.vtt', route => route.fulfill({ body: vtt, contentType: 'text/vtt' }))
|
||||
await prepare(page, core)
|
||||
await page.evaluate(async () => {
|
||||
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-restart.vtt', name: 'restart' }] }))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
||||
await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4?multiple-restart=1'))
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await expect(page.locator('.art-subtitle-restart')).toHaveText('Cached caption')
|
||||
const requests = await page.evaluate(() => window.multipleProbe.requests)
|
||||
expect(requests).toEqual(['/test/multiple-restart.vtt'])
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await evidence(page, testInfo, core, { requests, nativeSourceSwitch: true })
|
||||
})
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export async function multipleSubtitlesHistorical() {
|
||||
return implementations
|
||||
}
|
||||
|
||||
export function multipleSubtitlesEnvironment(implementation, { script = false, responses = {}, fetchResponse, coreUtils, onInit, onCreate } = {}) {
|
||||
export function multipleSubtitlesEnvironment(implementation, { script = false, responses = {}, fetchResponse, coreUtils, onInit, onCreate, abortController = true } = {}) {
|
||||
const requests = []
|
||||
const requestOptions = []
|
||||
const blobs = new Map()
|
||||
@@ -38,6 +38,7 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r
|
||||
const initStates = []
|
||||
const converted = []
|
||||
const listeners = new Map()
|
||||
const warnings = []
|
||||
const module = { exports: {} }
|
||||
const utils = {
|
||||
getExt: url => url.split('.').pop(),
|
||||
@@ -80,7 +81,8 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r
|
||||
const context = {
|
||||
Blob,
|
||||
TextDecoder,
|
||||
console: { log() {}, warn() {}, error() {} },
|
||||
AbortController: abortController ? AbortController : undefined,
|
||||
console: { log() {}, warn(...args) { warnings.push(args) }, error() {} },
|
||||
URL: {
|
||||
createObjectURL(blob) {
|
||||
onCreate?.(blob)
|
||||
@@ -115,5 +117,5 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r
|
||||
for (const callback of [...(listeners.get(name) || [])])
|
||||
callback(...args)
|
||||
}
|
||||
return { factory, exported, art, utils, requests, requestOptions, blobs, liveBlobs, revoked, initialized, initStates, converted, listeners, emit, responseFor, latestText }
|
||||
return { factory, exported, art, utils, requests, requestOptions, blobs, liveBlobs, revoked, initialized, initStates, converted, listeners, warnings, emit, responseFor, latestText }
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Lifecycle regressions use the repository Node runner.
|
||||
import test from 'node:test'
|
||||
import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, subtitleVtt } from './helpers/multiple-subtitles.js'
|
||||
|
||||
const candidate = await multipleSubtitlesCandidate()
|
||||
const create = options => multipleSubtitlesEnvironment(candidate, options)
|
||||
const options = { subtitles: [{ url: 'a.vtt', name: 'a' }] }
|
||||
function deferred() {
|
||||
let resolve
|
||||
let reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
async function flush() {
|
||||
for (let index = 0; index < 20; index++)
|
||||
await Promise.resolve()
|
||||
}
|
||||
function empty(env) {
|
||||
assert.equal(env.liveBlobs.size, 0)
|
||||
assert.equal(env.listeners.get('destroy')?.size || 0, 0)
|
||||
}
|
||||
|
||||
for (const abortController of [true, false]) {
|
||||
for (const phase of ['fetch', 'body']) {
|
||||
test(`Multiple subtitles candidate: destroy cancels ${phase}, AbortController=${abortController}`, async () => {
|
||||
const held = deferred()
|
||||
const started = deferred()
|
||||
const env = create({ abortController, fetchResponse: (_url, _options, responseFor) => {
|
||||
if (phase === 'fetch') {
|
||||
started.resolve()
|
||||
return held.promise
|
||||
}
|
||||
return { ...responseFor('a.vtt'), arrayBuffer() {
|
||||
started.resolve()
|
||||
return held.promise
|
||||
} }
|
||||
} })
|
||||
const pending = env.factory(options)(env.art)
|
||||
await started.promise
|
||||
env.emit('destroy')
|
||||
const result = await pending
|
||||
assert.equal(result.name, 'multipleSubtitles')
|
||||
assert.equal(result.reset(), undefined)
|
||||
assert.equal(result.tracks(['missing']), undefined)
|
||||
assert.equal(env.requestOptions[0]?.signal.aborted, abortController ? true : undefined)
|
||||
assert.equal(env.initialized.length, 0)
|
||||
empty(env)
|
||||
held.reject(new Error('late rejected operation'))
|
||||
await flush()
|
||||
assert.equal(env.initialized.length, 0)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test('Multiple subtitles candidate: already destroyed hosts do not request or subscribe', async () => {
|
||||
const env = create()
|
||||
env.emit('destroy')
|
||||
const result = await env.factory(options)(env.art)
|
||||
result.reset()
|
||||
assert.equal(env.requests.length, 0)
|
||||
assert.equal(env.initialized.length, 0)
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: failed request preserves error identity and cancels a sibling body', async () => {
|
||||
const failed = deferred()
|
||||
const body = deferred()
|
||||
const started = deferred()
|
||||
const error = new Error('network failed')
|
||||
const env = create({ fetchResponse: url => url === 'a.vtt'
|
||||
? failed.promise
|
||||
: { ok: true, arrayBuffer() {
|
||||
started.resolve()
|
||||
return body.promise
|
||||
} } })
|
||||
const pending = env.factory({ subtitles: [...options.subtitles, { url: 'b.vtt', name: 'b' }] })(env.art)
|
||||
await started.promise
|
||||
failed.reject(error)
|
||||
await assert.rejects(pending, caught => caught === error)
|
||||
assert.equal(env.requestOptions[1].signal.aborted, true)
|
||||
body.resolve(new TextEncoder().encode(subtitleVtt('late')).buffer)
|
||||
await flush()
|
||||
assert.equal(env.initialized.length, 0)
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: HTTP failure rejects without reading or installing its body', async () => {
|
||||
let read = false
|
||||
const env = create({ fetchResponse: () => ({ ok: false, status: 404, arrayBuffer() {
|
||||
read = true
|
||||
} }) })
|
||||
await assert.rejects(env.factory(options)(env.art), { message: 'Failed to fetch multiple subtitles: HTTP 404' })
|
||||
assert.equal(read, false)
|
||||
assert.equal(env.requestOptions[0].signal.aborted, true)
|
||||
assert.equal(env.initialized.length, 0)
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: body failure and bad encoding release registration resources', async () => {
|
||||
const error = new Error('body failed')
|
||||
const env = create({ fetchResponse: () => ({ ok: true, arrayBuffer: () => Promise.reject(error) }) })
|
||||
await assert.rejects(env.factory(options)(env.art), caught => caught === error)
|
||||
empty(env)
|
||||
const invalid = create()
|
||||
await assert.rejects(invalid.factory({ subtitles: [{ ...options.subtitles[0], encoding: 'invalid-label' }] })(invalid.art), { name: 'RangeError' })
|
||||
empty(invalid)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: destroy releases the last URL and makes retained methods inert', async () => {
|
||||
const env = create()
|
||||
const result = await env.factory(options)(env.art)
|
||||
env.emit('destroy')
|
||||
env.emit('destroy')
|
||||
assert.equal(result.reset(), undefined)
|
||||
assert.equal(result.tracks(), undefined)
|
||||
assert.deepEqual(env.revoked, ['blob:subtitle-1'])
|
||||
assert.equal(env.initialized.length, 1)
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: allocation failure preserves the old URL and reset recovers', async () => {
|
||||
let calls = 0
|
||||
const error = new Error('allocation failed')
|
||||
const env = create({ onCreate() {
|
||||
if (++calls === 2)
|
||||
throw error
|
||||
} })
|
||||
const result = await env.factory(options)(env.art)
|
||||
assert.throws(() => result.reset(), caught => caught === error)
|
||||
assert.equal(env.liveBlobs.has('blob:subtitle-1'), true)
|
||||
assert.equal(env.revoked.length, 0)
|
||||
result.reset()
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-2'])
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: synchronous initial host failure rolls back escape and frees the new URL', async () => {
|
||||
const error = new Error('init failed')
|
||||
const env = create({ onInit() {
|
||||
throw error
|
||||
} })
|
||||
await assert.rejects(env.factory(options)(env.art), caught => caught === error)
|
||||
assert.equal(env.art.option.subtitle.escape, true)
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: a failed synchronous replacement retains the previous resource', async () => {
|
||||
let calls = 0
|
||||
const error = new Error('replacement failed')
|
||||
const env = create({ onInit() {
|
||||
if (++calls === 2)
|
||||
throw error
|
||||
} })
|
||||
const result = await env.factory(options)(env.art)
|
||||
assert.throws(() => result.reset(), caught => caught === error)
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-1'])
|
||||
result.reset()
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-3'])
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: host media loading does not delay registration or change void selection', async () => {
|
||||
const held = deferred()
|
||||
const env = create({ onInit: () => held.promise })
|
||||
const result = await env.factory(options)(env.art)
|
||||
assert.equal(result.name, 'multipleSubtitles')
|
||||
assert.equal(result.reset(), undefined)
|
||||
assert.equal(env.initialized.length, 2)
|
||||
held.resolve()
|
||||
await flush()
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-2'])
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: asynchronous rejection is observed and only frees its own URL', async () => {
|
||||
const operations = [deferred(), deferred()]
|
||||
let calls = 0
|
||||
const env = create({ onInit: () => operations[calls++].promise })
|
||||
const result = await env.factory(options)(env.art)
|
||||
result.reset()
|
||||
operations[0].reject(new Error('stale failure'))
|
||||
await flush()
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-2'])
|
||||
operations[1].reject(new Error('current failure'))
|
||||
await flush()
|
||||
assert.equal(env.liveBlobs.size, 0)
|
||||
assert.equal(env.warnings.length, 2)
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: destroy in host init does not install resources again', async () => {
|
||||
const env = create({ onInit() {
|
||||
env.emit('destroy')
|
||||
} })
|
||||
const result = await env.factory(options)(env.art)
|
||||
result.reset()
|
||||
assert.equal(env.initialized.length, 1)
|
||||
assert.deepEqual(env.revoked, ['blob:subtitle-1'])
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: reentrant selection keeps only the newest resource', async () => {
|
||||
let result
|
||||
let reenter = false
|
||||
const env = create({ onInit() {
|
||||
if (reenter) {
|
||||
reenter = false
|
||||
result.tracks([])
|
||||
}
|
||||
} })
|
||||
result = await env.factory(options)(env.art)
|
||||
reenter = true
|
||||
result.reset()
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-3'])
|
||||
assert.equal(await env.latestText(), 'WEBVTT\n\n')
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: unknown names throw before allocation and do not corrupt later resets', async () => {
|
||||
const env = create()
|
||||
const result = await env.factory(options)(env.art)
|
||||
assert.throws(() => result.tracks(['missing']), { name: 'TypeError' })
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-1'])
|
||||
result.reset()
|
||||
assert.equal((env.utils.unescape(await env.latestText()).match(/<div/g) || []).length, 1)
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: destroy during allocation releases the returned URL immediately', async () => {
|
||||
const env = create({ onCreate() {
|
||||
env.emit('destroy')
|
||||
} })
|
||||
await env.factory(options)(env.art)
|
||||
assert.equal(env.initialized.length, 0)
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: throwing live configuration preserves the old resource', async () => {
|
||||
const env = create()
|
||||
const result = await env.factory(options)(env.art)
|
||||
const old = env.art.option.subtitle
|
||||
const error = new Error('configuration getter failed')
|
||||
Object.defineProperty(env.art.option, 'subtitle', { configurable: true, get() {
|
||||
throw error
|
||||
} })
|
||||
assert.throws(() => result.reset(), caught => caught === error)
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-1'])
|
||||
Object.defineProperty(env.art.option, 'subtitle', { value: old })
|
||||
result.reset()
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: destroy from live configuration stops the following init', async () => {
|
||||
const env = create()
|
||||
const result = await env.factory(options)(env.art)
|
||||
Object.defineProperty(env.art.option.subtitle, 'style', { enumerable: true, get() {
|
||||
env.emit('destroy')
|
||||
return {}
|
||||
} })
|
||||
result.reset()
|
||||
assert.equal(env.initialized.length, 1)
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: reentrant successful selection survives the outer host error', async () => {
|
||||
let result
|
||||
let reenter = false
|
||||
const error = new Error('outer init failed')
|
||||
const env = create({ onInit() {
|
||||
if (reenter) {
|
||||
reenter = false
|
||||
result.tracks([])
|
||||
throw error
|
||||
}
|
||||
} })
|
||||
result = await env.factory(options)(env.art)
|
||||
reenter = true
|
||||
assert.throws(() => result.reset(), caught => caught === error)
|
||||
assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-3'])
|
||||
env.emit('destroy')
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: late host rejection after destroy is handled without another write', async () => {
|
||||
const host = deferred()
|
||||
const env = create({ onInit: () => host.promise })
|
||||
await env.factory(options)(env.art)
|
||||
env.emit('destroy')
|
||||
host.reject(new Error('late host failure'))
|
||||
await flush()
|
||||
assert.equal(env.warnings.length, 0)
|
||||
assert.equal(env.initialized.length, 1)
|
||||
assert.deepEqual(env.revoked, ['blob:subtitle-1'])
|
||||
empty(env)
|
||||
})
|
||||
|
||||
test('Multiple subtitles candidate: destroy from a URL getter stops the request before dispatch', async () => {
|
||||
const env = create()
|
||||
const entry = { name: 'a', get url() {
|
||||
env.emit('destroy')
|
||||
return 'a.vtt'
|
||||
} }
|
||||
await env.factory({ subtitles: [entry] })(env.art)
|
||||
assert.equal(env.requests.length, 0)
|
||||
assert.equal(env.initialized.length, 0)
|
||||
empty(env)
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Merge and decoder regressions use the repository Node runner.
|
||||
import test from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, multipleSubtitlesHistorical, subtitleVtt } from './helpers/multiple-subtitles.js'
|
||||
|
||||
const { parseTracks, serializeTracks, ...coreUtils } = await loadModules({
|
||||
parseTracks: { file: 'packages/artplayer-plugin-multiple-subtitles/src/merge', name: 'parseTracks' },
|
||||
serializeTracks: { file: 'packages/artplayer-plugin-multiple-subtitles/src/merge', name: 'serializeTracks' },
|
||||
getExt: { file: 'packages/artplayer/src/utils/file', name: 'getExt' },
|
||||
srtToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'srtToVtt' },
|
||||
assToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'assToVtt' },
|
||||
})
|
||||
const candidate = await multipleSubtitlesCandidate()
|
||||
const historical = (await multipleSubtitlesHistorical()).find(item => item.name === 'published-1.2.0-main')
|
||||
function freeze(value) {
|
||||
if (value && typeof value === 'object') {
|
||||
Object.freeze(value)
|
||||
for (const child of Object.values(value)) freeze(child)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
test('Multiple subtitles merge: repeated selection serializes frozen trees without accumulating wrappers', () => {
|
||||
const trees = freeze(parseTracks([subtitleVtt('A'), subtitleVtt('B')], [{ name: 'a' }, { name: 'b' }]))
|
||||
const original = JSON.stringify(trees)
|
||||
const first = serializeTracks(trees)
|
||||
serializeTracks([trees[1], trees[0], trees[1]])
|
||||
assert.equal(serializeTracks(trees), first)
|
||||
assert.equal(JSON.stringify(trees), original)
|
||||
})
|
||||
|
||||
test('Multiple subtitles merge: actual released serializer output is preserved for text, markup and timing cases', async () => {
|
||||
const samples = [
|
||||
subtitleVtt('A & B < C > D'),
|
||||
subtitleVtt('<b>bold</b> tail'),
|
||||
subtitleVtt('<c.red>color</c> <i>italic</i>'),
|
||||
subtitleVtt('<v Speaker>voice</v>'),
|
||||
subtitleVtt('<ruby>text<rt>note</rt></ruby>'),
|
||||
subtitleVtt('line one\nline two'),
|
||||
'WEBVTT\n\n00:02.000 --> 00:05.000\nLater\n\n00:00.000 --> 00:03.000\nEarlier\n',
|
||||
subtitleVtt('best effort').replace('WEBVTT', 'BAD HEADER'),
|
||||
'WEBVTT\n\n',
|
||||
]
|
||||
for (const body of samples) {
|
||||
const environments = [historical, candidate].map(implementation => multipleSubtitlesEnvironment(implementation, { responses: { 'a.vtt': body, 'b.vtt': subtitleVtt('second') } }))
|
||||
const results = await Promise.all(environments.map(env => env.factory({ subtitles: [{ url: 'a.vtt', name: 'a' }, { url: 'b.vtt', name: 'b' }] })(env.art)))
|
||||
for (const selection of [null, ['b', 'a', 'b'], [], ['a']]) {
|
||||
if (selection)
|
||||
results.forEach(result => result.tracks(selection))
|
||||
assert.equal(await environments[1].latestText(), await environments[0].latestText(), body)
|
||||
}
|
||||
results.forEach(result => result.reset())
|
||||
assert.equal(await environments[1].latestText(), await environments[0].latestText(), body)
|
||||
environments[1].emit('destroy')
|
||||
}
|
||||
})
|
||||
|
||||
test('Multiple subtitles request: real core SRT/ASS conversion and encoded text retain released output', async () => {
|
||||
const responses = {
|
||||
'a.SRT?track=1': '1\n00:00:00,000 --> 00:00:02,000\nSRT text\n',
|
||||
'b.ass': 'Dialogue: 0,0:00:00.00,0:00:02.00,Default,,0,0,0,,ASS text',
|
||||
'c.vtt': Buffer.from(subtitleVtt('café'), 'latin1'),
|
||||
'd.vtt': Buffer.from(subtitleVtt('日本語'), 'utf16le'),
|
||||
}
|
||||
const options = { subtitles: [{ url: 'a.SRT?track=1', name: 'srt' }, { url: 'b.ass', name: 'ass' }, { url: 'c.vtt', name: 'latin', encoding: 'windows-1252' }, { url: 'd.vtt', name: 'utf16', encoding: 'utf-16le' }] }
|
||||
const old = multipleSubtitlesEnvironment(historical, { responses, coreUtils })
|
||||
const current = multipleSubtitlesEnvironment(candidate, { responses, coreUtils })
|
||||
await old.factory(options)(old.art)
|
||||
await current.factory(options)(current.art)
|
||||
const text = await current.latestText()
|
||||
assert.equal(text, await old.latestText())
|
||||
for (const value of ['SRT text', 'ASS text', 'café', '日本語']) assert(text.includes(value))
|
||||
current.emit('destroy')
|
||||
})
|
||||
@@ -64,11 +64,11 @@ for (const implementation of implementations) {
|
||||
assert.equal(env.initialized[0].onVttLoad, env.utils.unescape)
|
||||
assert.equal(env.initialized[0].style, env.art.option.subtitle.style)
|
||||
assert.equal(env.initialized[0].type, 'vtt')
|
||||
assert.deepEqual(env.revoked, [''])
|
||||
assert.deepEqual(env.revoked, implementation.version === 'candidate' ? [] : [''])
|
||||
env.art.option.subtitle.encoding = 'utf-16le'
|
||||
result.reset()
|
||||
assert.equal(env.initialized[1].encoding, 'utf-16le')
|
||||
assert.deepEqual(env.revoked, ['', 'blob:subtitle-1'])
|
||||
assert.deepEqual(env.revoked, implementation.version === 'candidate' ? ['blob:subtitle-1'] : ['', 'blob:subtitle-1'])
|
||||
assert.equal(env.blobs.get('blob:subtitle-2').type, 'text/vtt')
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user