mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 03:11:20 -08:00
fix(multiple-subtitles): render overlapping cues on legacy hosts [PKG-MULTI-SUB-10]
This commit is contained in:
1 parent
31f9024bff
commit
8ae2762a23
24 files changed
+3465
-56
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(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 y=!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 T=[],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),T.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(T.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){y=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!y||A.parse(x,O)){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 N=new i(x.text,p,a,t);x.tree=N.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
|
||||
!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 y=!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 T=[],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),T.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(T.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){y=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!y||A.parse(x,O)){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,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())}),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=[],a=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=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,a=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&o(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&o(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{a(n.next(e))}catch(t){r(t)}},o=e=>{try{a(n.throw(e))}catch(t){r(t)}},a=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,o);a((n=n.apply(e,t)).next())});var u={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},f=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/),o=!1,a=[],l=[],c=[];function f(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])&&f('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(f("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}for(;void 0!=s[r];){for(var y;!o&&""==s[r];)r++;if(!o&&void 0==s[r])break;y=Object.assign({},u,{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")&&f("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")&&(f("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){f("Style blocks cannot appear after the first cue.");continue}w||a.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){f("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,f("Cue identifier needs to be followed by timestamp.");continue}}o=!1;var O=new d(s[r],f),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")){f("Blank line missing before cue."),o=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,f,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")){o=!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:a}}},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 o(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function a(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,o,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=a(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=a(/\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=a(/\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==(o=a(/\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(o,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,a){if(o(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<a&&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."),o(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."),o(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]),o(n),function(e,t){for(var i=e.split(n),r=[Line truncated
|
||||
@@ -55,6 +55,26 @@
|
||||
*
|
||||
* 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 renderLegacyCaptions(art) {
|
||||
const cues = art.subtitle?.textTrack?.activeCues;
|
||||
if (!cues || cues.length < 2)
|
||||
return;
|
||||
const texts = Array.from(cues, (cue) => "text" in cue && typeof cue.text === "string" ? cue.text : "");
|
||||
const target = art.template.$subtitle;
|
||||
if (art.option?.subtitle?.escape) {
|
||||
target.innerHTML = "";
|
||||
for (const text of texts) {
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const element = target.ownerDocument.createElement("div");
|
||||
element.className = "art-subtitle-line";
|
||||
element.textContent = line;
|
||||
target.appendChild(element);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
target.innerHTML = texts.join("");
|
||||
}
|
||||
}
|
||||
var defaultCueSettings = {
|
||||
direction: "horizontal",
|
||||
snapToLines: true,
|
||||
@@ -841,13 +861,15 @@ function serializeTracks(trees) {
|
||||
for (const cue of tree.cues) {
|
||||
const children = [];
|
||||
for (const child of cue.tree.children) {
|
||||
if (child.type === "timestamp")
|
||||
if (child.type === "timestamp") {
|
||||
children.push(markTimestamp(child));
|
||||
else children.push(
|
||||
{ type: "text", value: `<div class="art-subtitle-${tree.name}">` },
|
||||
prepareCueNode(child),
|
||||
{ type: "text", value: "</div>" }
|
||||
);
|
||||
} else {
|
||||
children.push(
|
||||
{ type: "text", value: `<div class="art-subtitle-${tree.name}">` },
|
||||
prepareCueNode(child),
|
||||
{ type: "text", value: "</div>" }
|
||||
);
|
||||
}
|
||||
}
|
||||
cues.push({
|
||||
...cue,
|
||||
@@ -861,9 +883,13 @@ function serializeTracks(trees) {
|
||||
return new WebVTTSerializer().serialize(cues);
|
||||
}
|
||||
function installCaptionView(art, lifetime) {
|
||||
const legacy = !!art.subtitle && "activeCue" in art.subtitle && !("activeCues" in art.subtitle);
|
||||
const event = legacy ? "subtitleUpdate" : "subtitleAfterUpdate";
|
||||
function update() {
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
if (legacy)
|
||||
renderLegacyCaptions(art);
|
||||
for (const marker of Array.from(art.template.$subtitle.getElementsByTagName(`c.${TIMESTAMP_CLASS}`))) {
|
||||
const first = marker.firstChild;
|
||||
if (first?.nodeType === 3)
|
||||
@@ -873,8 +899,8 @@ function installCaptionView(art, lifetime) {
|
||||
}
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
lifetime.own(() => art.off("subtitleAfterUpdate", update));
|
||||
art.on("subtitleAfterUpdate", update);
|
||||
lifetime.own(() => art.off(event, update));
|
||||
art.on(event, update);
|
||||
}
|
||||
function createLifetime(art) {
|
||||
let closed = Boolean(art.isDestroy);
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:test && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.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-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.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/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
|
||||
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.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-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.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/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.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-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs refactor/scripts/auto-thumbnail-types.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-merge.test.js test/multiple-subtitles-entities.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 refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.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 refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
|
||||
"test:multiple-subtitles-types-package": "node refactor/scripts/multiple-subtitles-package-types.mjs",
|
||||
"test:jassub": "node --test test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs refactor/scripts/jassub-failures.test.mjs refactor/scripts/jassub-types.test.mjs refactor/scripts/jassub-vendor-patch.test.mjs",
|
||||
"test:jassub-types": "node --test refactor/scripts/jassub-types.test.mjs",
|
||||
|
||||
@@ -27,8 +27,17 @@ to npm 1.2.0; the runtime entry requires an option object but allows omitted `su
|
||||
Its cancellation Promise also settles pending operations without AbortController support.
|
||||
- `src/render.ts` owns generated Blob URLs and synchronous/async host installation outcomes.
|
||||
Reentrant selections and stale host rejections cannot release the latest selected resource.
|
||||
- `src/caption.ts` owns a subtitleAfterUpdate listener. It unwraps internal timestamp markers
|
||||
in the custom HTML caption layer, preserving subsequent nodes and literal user text.
|
||||
- `src/caption.ts` owns one caption listener and unwraps internal timestamp markers.
|
||||
Hosts with `activeCue` but no `activeCues` use the old `subtitleUpdate` event;
|
||||
other hosts keep `subtitleAfterUpdate`. Capability detection never invokes cue getters.
|
||||
- `src/legacy-caption.ts` displays every native active cue on those older hosts, in
|
||||
native order, when more than one is active. It preserves cue objects/times and the
|
||||
host's update method and scalar event payload. Single/empty intervals remain core
|
||||
rendered. Later `option.subtitle.escape` changes retain escaped line rendering.
|
||||
It owns no timer or media resource; the caption listener is removed by the lifetime.
|
||||
Old event subscribers still run in registration order: listeners registered before
|
||||
the plugin see the core's first-cue DOM; later listeners see the adapted full view.
|
||||
Custom DOM replacements should therefore run after plugin registration.
|
||||
- `src/types.ts` describes selected tracks and narrow host/resource boundaries and reuses public
|
||||
runtime option/result types. The private host/resource types are not a published API.
|
||||
- `src/parser.d.ts` describes the vendored parse result and discriminated cue nodes. Parsed
|
||||
@@ -147,9 +156,14 @@ The task05 matrix in `test/browser/multiple-subtitles-combinations.spec.js` adds
|
||||
core5.1.2/5.1.7/5.3.0/5.4.0/candidate and actual plugin1.0.0/1.1.0/1.2.0.
|
||||
Read native `template.$track.track.cues` for old-core probes: older hosts do not
|
||||
provide the later `subtitle.cues` getter. Core5.1.2/5.1.7 only render their first
|
||||
active cue; the matrix explicitly records lost simultaneous languages for1.1.0+
|
||||
and candidate, while1.0.0's index-merged cue displays both. This remains an open
|
||||
compatibility issue, not an approved limitation. CORE-SUBTITLE-OFFSET-01 corrects
|
||||
active cue; the matrix explicitly records lost simultaneous languages for published
|
||||
1.1.0/1.2.0, while1.0.0's index-merged cue displays both. Task10 adds the candidate
|
||||
view adapter without adopting index merging or altering independent cue timings.
|
||||
`multiple-subtitles-legacy.spec.js` checks asymmetric three-cue overlaps, HTML/CSS,
|
||||
selection/reset and clearing on both old cores. ASS is additionally exercised on
|
||||
5.1.7; published5.1.2's malformed ASS converter is separately tracked by task11.
|
||||
This adapter does not repair an old browser host's stale native active-cue list.
|
||||
CORE-SUBTITLE-OFFSET-01 corrects
|
||||
paused offsets in the candidate core; published Firefox hosts retain their exact
|
||||
historical defect observations. Task09 traced apparent WebKit5.3.0/5.4.0 caption
|
||||
loss to native seeks ending near zero: the cue itself remained intact. A no-plugin
|
||||
|
||||
+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(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 y=!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 T=[],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),T.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(T.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){y=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!y||A.parse(x,O)){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 N=new i(x.text,p,a,t);x.tree=N.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
|
||||
!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 y=!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 T=[],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),T.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(T.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){y=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!y||A.parse(x,O)){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,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())}),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=[],a=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=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,a=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&o(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&o(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{a(n.next(e))}catch(t){r(t)}},o=e=>{try{a(n.throw(e))}catch(t){r(t)}},a=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,o);a((n=n.apply(e,t)).next())});var u={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},f=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/),o=!1,a=[],l=[],c=[];function f(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])&&f('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(f("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}for(;void 0!=s[r];){for(var y;!o&&""==s[r];)r++;if(!o&&void 0==s[r])break;y=Object.assign({},u,{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")&&f("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")&&(f("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){f("Style blocks cannot appear after the first cue.");continue}w||a.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){f("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,f("Cue identifier needs to be followed by timestamp.");continue}}o=!1;var O=new d(s[r],f),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")){f("Blank line missing before cue."),o=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,f,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")){o=!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:a}}},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 o(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function a(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,o,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=a(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=a(/\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=a(/\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==(o=a(/\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(o,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,a){if(o(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<a&&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."),o(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."),o(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]),o(n),function(e,t){for(var i=e.split(n),r=[Line truncated
|
||||
+34
-8
@@ -55,6 +55,26 @@
|
||||
*
|
||||
* 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 renderLegacyCaptions(art) {
|
||||
const cues = art.subtitle?.textTrack?.activeCues;
|
||||
if (!cues || cues.length < 2)
|
||||
return;
|
||||
const texts = Array.from(cues, (cue) => "text" in cue && typeof cue.text === "string" ? cue.text : "");
|
||||
const target = art.template.$subtitle;
|
||||
if (art.option?.subtitle?.escape) {
|
||||
target.innerHTML = "";
|
||||
for (const text of texts) {
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const element = target.ownerDocument.createElement("div");
|
||||
element.className = "art-subtitle-line";
|
||||
element.textContent = line;
|
||||
target.appendChild(element);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
target.innerHTML = texts.join("");
|
||||
}
|
||||
}
|
||||
var defaultCueSettings = {
|
||||
direction: "horizontal",
|
||||
snapToLines: true,
|
||||
@@ -841,13 +861,15 @@ function serializeTracks(trees) {
|
||||
for (const cue of tree.cues) {
|
||||
const children = [];
|
||||
for (const child of cue.tree.children) {
|
||||
if (child.type === "timestamp")
|
||||
if (child.type === "timestamp") {
|
||||
children.push(markTimestamp(child));
|
||||
else children.push(
|
||||
{ type: "text", value: `<div class="art-subtitle-${tree.name}">` },
|
||||
prepareCueNode(child),
|
||||
{ type: "text", value: "</div>" }
|
||||
);
|
||||
} else {
|
||||
children.push(
|
||||
{ type: "text", value: `<div class="art-subtitle-${tree.name}">` },
|
||||
prepareCueNode(child),
|
||||
{ type: "text", value: "</div>" }
|
||||
);
|
||||
}
|
||||
}
|
||||
cues.push({
|
||||
...cue,
|
||||
@@ -861,9 +883,13 @@ function serializeTracks(trees) {
|
||||
return new WebVTTSerializer().serialize(cues);
|
||||
}
|
||||
function installCaptionView(art, lifetime) {
|
||||
const legacy = !!art.subtitle && "activeCue" in art.subtitle && !("activeCues" in art.subtitle);
|
||||
const event = legacy ? "subtitleUpdate" : "subtitleAfterUpdate";
|
||||
function update() {
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
if (legacy)
|
||||
renderLegacyCaptions(art);
|
||||
for (const marker of Array.from(art.template.$subtitle.getElementsByTagName(`c.${TIMESTAMP_CLASS}`))) {
|
||||
const first = marker.firstChild;
|
||||
if (first?.nodeType === 3)
|
||||
@@ -873,8 +899,8 @@ function installCaptionView(art, lifetime) {
|
||||
}
|
||||
if (lifetime.closed)
|
||||
return;
|
||||
lifetime.own(() => art.off("subtitleAfterUpdate", update));
|
||||
art.on("subtitleAfterUpdate", update);
|
||||
lifetime.own(() => art.off(event, update));
|
||||
art.on(event, update);
|
||||
}
|
||||
function createLifetime(art) {
|
||||
let closed = Boolean(art.isDestroy);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Lifetime } from './types'
|
||||
import type { CaptionHost, Lifetime } from './types'
|
||||
import renderLegacyCaptions from './legacy-caption'
|
||||
import { TIMESTAMP_CLASS } from './merge'
|
||||
|
||||
export default function installCaptionView(art: Pick<Artplayer, 'on' | 'off' | 'template'>, lifetime: Lifetime): void {
|
||||
export default function installCaptionView(art: CaptionHost, lifetime: Lifetime): void {
|
||||
const legacy = !!art.subtitle && 'activeCue' in art.subtitle && !('activeCues' in art.subtitle)
|
||||
const event = legacy ? 'subtitleUpdate' : 'subtitleAfterUpdate'
|
||||
function update(): void {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
if (legacy)
|
||||
renderLegacyCaptions(art)
|
||||
// HTML does not pair <c.class> with </c>; unwrap while preserving following caption nodes.
|
||||
for (const marker of Array.from(art.template.$subtitle.getElementsByTagName(`c.${TIMESTAMP_CLASS}`))) {
|
||||
const first = marker.firstChild
|
||||
@@ -16,6 +20,6 @@ export default function installCaptionView(art: Pick<Artplayer, 'on' | 'off' | '
|
||||
}
|
||||
if (lifetime.closed)
|
||||
return
|
||||
lifetime.own(() => art.off('subtitleAfterUpdate', update))
|
||||
art.on('subtitleAfterUpdate', update)
|
||||
lifetime.own(() => art.off(event, update))
|
||||
art.on(event, update)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { CaptionHost } from './types'
|
||||
|
||||
export default function renderLegacyCaptions(art: CaptionHost): void {
|
||||
const cues = art.subtitle?.textTrack?.activeCues
|
||||
if (!cues || cues.length < 2)
|
||||
return
|
||||
|
||||
const texts = Array.from(cues, cue => 'text' in cue && typeof cue.text === 'string' ? cue.text : '')
|
||||
const target = art.template.$subtitle
|
||||
if (art.option?.subtitle?.escape) {
|
||||
target.innerHTML = ''
|
||||
for (const text of texts) {
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const element = target.ownerDocument.createElement('div')
|
||||
element.className = 'art-subtitle-line'
|
||||
element.textContent = line
|
||||
target.appendChild(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
target.innerHTML = texts.join('')
|
||||
}
|
||||
}
|
||||
@@ -34,13 +34,16 @@ export function serializeTracks(trees: readonly (Track | undefined)[]): string {
|
||||
for (const cue of tree.cues) {
|
||||
const children: CueNode[] = []
|
||||
for (const child of cue.tree.children) {
|
||||
if (child.type === 'timestamp')
|
||||
if (child.type === 'timestamp') {
|
||||
children.push(markTimestamp(child))
|
||||
else children.push(
|
||||
{ type: 'text', value: `<div class="art-subtitle-${tree.name}">` },
|
||||
prepareCueNode(child),
|
||||
{ type: 'text', value: '</div>' },
|
||||
)
|
||||
}
|
||||
else {
|
||||
children.push(
|
||||
{ type: 'text', value: `<div class="art-subtitle-${tree.name}">` },
|
||||
prepareCueNode(child),
|
||||
{ type: 'text', value: '</div>' },
|
||||
)
|
||||
}
|
||||
}
|
||||
cues.push({
|
||||
...cue,
|
||||
|
||||
@@ -18,6 +18,11 @@ export interface Lifetime {
|
||||
dispose: Cleanup
|
||||
}
|
||||
export type LifetimeHost = Pick<Artplayer, 'isDestroy' | 'on' | 'off'>
|
||||
export interface CaptionHost extends Pick<Artplayer, 'on' | 'off'> {
|
||||
template: { $subtitle: HTMLElement }
|
||||
option?: { subtitle?: { escape?: boolean } }
|
||||
subtitle?: { readonly textTrack?: TextTrack }
|
||||
}
|
||||
export interface RenderHost {
|
||||
option: { subtitle?: Subtitle }
|
||||
subtitle: { init: (option: Subtitle) => unknown }
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,75 @@
|
||||
# PKG-MULTI-SUB-10: old-core simultaneous caption rendering
|
||||
|
||||
## Problem and boundary
|
||||
|
||||
Actual core5.1.2/5.1.7 render only `subtitle.activeCue`, even when the native track
|
||||
contains multiple active cues. Plugin1.0.0 combined tracks by array index;
|
||||
1.1.0/1.2.0 and the prior candidate preserve independent timing and consequently
|
||||
lose visible languages on these hosts. The corrected regression fails on both
|
||||
old cores with the previous candidate main artifact, at the missing second caption.
|
||||
|
||||
The first regression also exposed a separate5.1.2 ASS conversion defect: its
|
||||
published main bundle collapses required VTT line breaks into spaces, whereas its
|
||||
tarball source preserves them. This yields two native cues rather than three.
|
||||
Task11 owns that fix. The rendering regression now uses VTT/SRT on both cores and
|
||||
additionally ASS on5.1.7; substituting the third track does not constitute ASS
|
||||
acceptance on5.1.2. Original failed runs remain in the evidence index.
|
||||
|
||||
## Implementation and compatibility
|
||||
|
||||
`caption.ts` detects the old `activeCue` surface without the newer `activeCues`
|
||||
surface, using property existence without invoking getters. It registers one old
|
||||
`subtitleUpdate` listener instead of the modern `subtitleAfterUpdate` listener.
|
||||
`legacy-caption.ts` renders all native active cue texts in their original order
|
||||
when multiple cues are active. Single/empty intervals retain core rendering.
|
||||
The existing timestamp cleanup runs after either core path.
|
||||
|
||||
No cue timing, native object, host method identity, source clock or event payload
|
||||
is replaced. The old scalar event still contains only the first cue text. Existing
|
||||
listeners keep their registration order; those before the plugin see the original
|
||||
first-cue view, while later listeners see the combined view. Applications making
|
||||
custom caption DOM replacements should register that listener after the plugin.
|
||||
The plugin already owns this caption layer and sets escape=false; if the user
|
||||
later enables escape, the adapter preserves literal text and old per-line classes.
|
||||
|
||||
The lifetime owns listener removal and rejects late callbacks after destroy.
|
||||
The adapter has no timer, media request or new runtime dependency. Public types,
|
||||
factory methods, export aliases, package versions and vendor code are unchanged.
|
||||
The nearby serializer receives only the braces required by its existing lint rule.
|
||||
|
||||
## Verification
|
||||
|
||||
Exact hashes, archived reports and browser versions are in
|
||||
[the validation index](../baselines/multiple-subtitles-legacy-validation.json).
|
||||
|
||||
- Six new Node regressions cover native order, repeated updates, scalar payloads,
|
||||
listener order, original method identity, single/empty intervals, escape/CRLF,
|
||||
timestamp cleanup, modern isolation and destroy/registration failure cleanup.
|
||||
- The complete plugin suite passes300 tests, including the historical runtime and
|
||||
public type suites. The final listener-order assertions also pass against actual
|
||||
main and legacy artifacts, six cases each.
|
||||
- Source browser matrix:105 cases,104 pass and one old5.4.0/WebKit source-seek
|
||||
failure. All six new old-host overlap cases pass. Historical missing languages
|
||||
and paused-offset observations remain labelled, not counted as correct behavior.
|
||||
- Actual main matrix:63 cases,62 pass and the same old5.4.0/WebKit failure.
|
||||
Its six old-host overlap cases pass; native cue identities, independent time
|
||||
windows, semantic HTML/CSS, repeated updates, selection/reset and clearing hold.
|
||||
- Actual legacy matrix passes12/12: six old-host cases and six modern entity cases.
|
||||
- Strict package TypeScript, root lint and explicit changed-test lint pass; root
|
||||
lint retains one existing generated-doc warning. CI regression suite passes50.
|
||||
The normal build regenerates all three package formats and matching docs copies.
|
||||
|
||||
The broad matrix reopens MULTI-SUB-SWITCH-01: waiting for seeking=false did not
|
||||
make every old-host subsequent seek reliable. The source failure retains its
|
||||
1..3-second cue but media time is0.001494 with no active cues. Task05 owns renewed
|
||||
diagnosis; this adapter does not run on that modern host. No retry, skip, timeout
|
||||
increase or passing rerun is used to erase those failures. The complete package,
|
||||
physical-device matrix, distribution gate and full refactor are not finished.
|
||||
|
||||
## Maintenance and rollback
|
||||
|
||||
Use the legacy browser spec for old-host display changes and combinations for
|
||||
historical profile comparisons. Keep converter, native seek and caption display
|
||||
failures separate. The package architecture and test READMEs map these entrypoints.
|
||||
Revert this task's commit and rebuild the package to remove the adapter; retain
|
||||
the failures and task11 in the working plan. No pushing or publishing is included.
|
||||
+7
-4
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 249 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 251 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 55 / doing 17 / blocked 0 / done 177 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 56 / doing 17 / blocked 0 / done 178 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
| artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 |
|
||||
| artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-SDK-01, PKG-HLS-05, PKG-HLS-06 |
|
||||
| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06, PKG-JASSUB-07, PKG-JASSUB-08, PKG-JASSUB-09 |
|
||||
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-08, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06, PKG-MULTI-SUB-09 |
|
||||
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-08, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06, PKG-MULTI-SUB-09, PKG-MULTI-SUB-10, PKG-MULTI-SUB-11 |
|
||||
| artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 |
|
||||
| artplayer-plugin-vtt-thumbnail | 1.1.0 | PKG-VTT-THUMB-01, PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
|
||||
| artplayer-proxy-canvas | 1.1.0 | PKG-AMBILIGHT-PROXY-01, PKG-CANVAS-01, PKG-CANVAS-02, PKG-CANVAS-03, PKG-CANVAS-04, PKG-CANVAS-05, PKG-CANVAS-06, PKG-FACTORY-01 |
|
||||
@@ -229,8 +229,10 @@
|
||||
| PKG-MULTI-SUB-07 | artplayer-plugin-multiple-subtitles<br>修复字幕内嵌时间戳序列化 | PKG-MULTI-SUB-03 | 保留数字时间戳节点、修正无效 NaN 输出、明确旧/新字幕输出差异 | 旧九份实现可复现,候选有效内嵌时间戳及文字/CSS/原生字幕通过,公开工厂与选择接口不变 | H | done |
|
||||
| PKG-MULTI-SUB-08 | artplayer-plugin-multiple-subtitles<br>修复字幕实体解码与嵌套标签包装 | PKG-MULTI-SUB-04, PKG-MULTI-SUB-07 | 使用解析器既有实体表入口修复多余分号;保护字面文本并以兄弟节点包装标签,保留CSS钩子和时间戳 | 冻结旧实现复现、实体/双重编码/标签/选择重置回归、新旧核心真实字幕显示通过;vendor原文与来源校验不变,类型/分发兼容 | M | done |
|
||||
| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles<br>验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22, PKG-MULTI-SUB-07, PKG-MULTI-SUB-08 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
|
||||
| PKG-MULTI-SUB-06 | artplayer-plugin-multiple-subtitles<br>验证分发并同步文档 | PKG-MULTI-SUB-05, ENG-07, CORE-SUBTITLE-OFFSET-01, PKG-MULTI-SUB-09 | multiple.subtitles.js、parser 来源许可及打包边界 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
| PKG-MULTI-SUB-06 | artplayer-plugin-multiple-subtitles<br>验证分发并同步文档 | PKG-MULTI-SUB-05, ENG-07, CORE-SUBTITLE-OFFSET-01, PKG-MULTI-SUB-09, PKG-MULTI-SUB-10, PKG-MULTI-SUB-11 | multiple.subtitles.js、parser 来源许可及打包边界 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
| PKG-MULTI-SUB-09 | artplayer-plugin-multiple-subtitles, artplayer<br>定位旧核心切源寻址与字幕显示边界 | PKG-MULTI-SUB-08, CORE-19 | 无插件及旧新插件对照、真实事件轨迹、原切源失败归因和组合测试顺序 | 保留旧核心立即寻址观察,候选首次寻址与正常字幕组合通过;不把原生时钟失败误归为字幕丢失 | M | done |
|
||||
| PKG-MULTI-SUB-10 | artplayer-plugin-multiple-subtitles<br>适配旧核心的多条活动字幕渲染 | PKG-MULTI-SUB-08, PKG-MULTI-SUB-09 | 独立旧宿主显示适配、原生异步字幕重叠回归和兼容边界说明 | 旧5.1.2/5.1.7复现先红后绿;保持原生cue对象/时间、旧事件标量和方法身份;多格式显示及清理通过,保留其他组合和设备缺口 | M | done |
|
||||
| PKG-MULTI-SUB-11 | artplayer-plugin-multiple-subtitles<br>处理旧发布核心 ASS 转换器丢失换行 | PKG-MULTI-SUB-10 | 冻结5.1.2发布/源码转换差异,兼容转换适配与真实ASS字幕回归 | 保持已有效转换行为,旧发布ASS缺陷有先红后绿与实际产物证据;不得将VTT替代夹具算作ASS通过 | M | todo |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-jassub
|
||||
|
||||
@@ -649,3 +651,4 @@
|
||||
- ENG-LINT-01: [记录](changes/2026-09-14-ENG-LINT-01-manifest-order.md) [记录](baselines/manifest-lint-validation.json)
|
||||
- CORE-SUBTITLE-OFFSET-01: [记录](changes/2026-09-14-CORE-SUBTITLE-OFFSET-01-paused.md) [记录](baselines/subtitle-offset-validation.json)
|
||||
- PKG-MULTI-SUB-09: [记录](changes/2026-09-14-PKG-MULTI-SUB-09-switch-order.md) [记录](baselines/multiple-subtitles-switch-validation.json)
|
||||
- PKG-MULTI-SUB-10: [记录](changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md) [记录](baselines/multiple-subtitles-legacy-validation.json)
|
||||
@@ -1,5 +1,20 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MULTI-SUB-10 旧核心多条字幕显示适配完成
|
||||
|
||||
插件以activeCue/activeCues能力检测选择旧subtitleUpdate或现代subtitleAfterUpdate,
|
||||
新增独立legacy-caption模块显示原生全部活动cue;保留对象、独立时间、方法身份、
|
||||
旧事件标量和监听顺序,销毁清理,无时钟改写或额外请求。初次回归另发现5.1.2
|
||||
发布ASS转换丢换行,单独登记PKG-MULTI-SUB-11;第三轨VTT替代不计作该版本ASS通过。
|
||||
调整夹具后旧产物在5.1.2/5.1.7均因第二条缺失失败,新源码/main/legacy各6项旧宿主
|
||||
重叠回归通过。全插件300项、实际main/legacy各6项单元、严格TS、lint与CI50项通过。
|
||||
源码扩展105项为104通过1旧5.4.0/WebKit切源失败;main63项为62通过同一旧宿主失败;
|
||||
legacy12项全通过。历史缺陷观察不算兼容通过。新切源失败仍有1至3秒cue、媒体近零秒,
|
||||
说明task09的seeking=false等待不足以保证旧宿主;重新打开SWITCH风险,交由task05继续。
|
||||
三格式及docs副本已正常重建,公开声明/依赖/版本未改;见[变更记录](changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md)
|
||||
和[验证索引](baselines/multiple-subtitles-legacy-validation.json)。251项:178 done、17 doing、56 todo。
|
||||
任务05/06、真实设备及发布门槛仍未完成;下一步先处理task11的旧ASS转换,再继续组合验收。
|
||||
|
||||
## PKG-MULTI-SUB-09 切源显示问题归因完成
|
||||
|
||||
失败时字幕cue仍在1至3秒,媒体却停在约0.002秒;新增无插件/候选插件/旧1.2.0
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
| VTT-THUMB-TYPE-01 | resolved / 已复现 | Published declarations falsely describe async registration as a synchronous result | PKG-VTT-THUMB-04 |
|
||||
| VTT-THUMB-EXPORT-01 | open / 已复现 | Older CommonJS default objects and thumbnails control names differ from latest direct export and vtt-thumbnail name | PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
|
||||
| MULTI-SUB-TYPE-01 | resolved / 已复现 | Declarations omit async registration, tracks/reset and make the subtitles array required | PKG-MULTI-SUB-04 |
|
||||
| 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-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, PKG-MULTI-SUB-10 |
|
||||
| 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 | resolved / 已复现 | HTTP error responses with parseable bodies are installed as subtitles | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
@@ -266,4 +266,5 @@
|
||||
| AUTO-THUMB-ENCODE-TIMEOUT-01 | resolved / 源码/产物事实 | Pending JPEG encoding has no bounded lifetime after frame readiness finishes | PKG-AUTO-THUMB-10 |
|
||||
| MULTI-SUB-MARKUP-01 | resolved / 已复现 | Semantic cue nodes receive HTML wrappers inside their annotation fields | PKG-MULTI-SUB-08 |
|
||||
| MULTI-SUB-OFFSET-01 | resolved / 已复现 | Paused Firefox subtitle offset leaves out-of-time native cues active and captions visible | PKG-MULTI-SUB-05, CORE-SUBTITLE-OFFSET-01 |
|
||||
| MULTI-SUB-SWITCH-01 | resolved / 已复现 | Intermittent WebKit core5.3.0 caption loss after selected-track source switch | PKG-MULTI-SUB-05, PKG-MULTI-SUB-09 |
|
||||
| MULTI-SUB-SWITCH-01 | open / 已复现 | Intermittent WebKit core5.3.0 caption loss after selected-track source switch | PKG-MULTI-SUB-05, PKG-MULTI-SUB-09 |
|
||||
| MULTI-SUB-ASS-01 | open / 已复现 | Published core5.1.2 ASS converter loses required VTT line breaks | PKG-MULTI-SUB-11 |
|
||||
+32
-7
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"task": "BASE-07",
|
||||
"sourceCommit": "e01f39ff",
|
||||
"updatedAt": "2026-09-14",
|
||||
"updatedAt": "2026-09-14T05:23:17.398Z",
|
||||
"note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.",
|
||||
"items": [
|
||||
{
|
||||
@@ -4352,7 +4352,8 @@
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-02",
|
||||
"PKG-MULTI-SUB-03",
|
||||
"PKG-MULTI-SUB-05"
|
||||
"PKG-MULTI-SUB-05",
|
||||
"PKG-MULTI-SUB-10"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/baselines/multiple-subtitles-release.json",
|
||||
@@ -4370,11 +4371,15 @@
|
||||
"test/browser/multiple-subtitles-lifecycle.spec.js",
|
||||
"refactor/changes/2026-09-14-PKG-MULTI-SUB-05-combinations.md",
|
||||
"refactor/baselines/multiple-subtitles-combinations-validation.json",
|
||||
"test/browser/multiple-subtitles-combinations.spec.js"
|
||||
"test/browser/multiple-subtitles-combinations.spec.js",
|
||||
"refactor/changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md",
|
||||
"refactor/baselines/multiple-subtitles-legacy-validation.json",
|
||||
"test/multiple-subtitles-caption.test.js",
|
||||
"test/browser/multiple-subtitles-legacy.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.",
|
||||
"workspaceState": "Native three-engine comparisons on cores5.1.2/5.1.7 show only first active cue displayed for actual plugin1.1.0/1.2.0/candidate; plugin1.0.0 index-merges both languages into one visible cue. Native tracks hold both languages and selecting either alone works. Compatible old-host overlap resolution and complete device coverage remain open."
|
||||
"workspaceState": "Task10 adapts old activeCue-only hosts to display all independently timed native cues. Source/main/legacy regressions cover old5.1.2/5.1.7. Historical1.1/1.2 losses remain explicit; broader profile/device acceptance remains in task05. Old5.1.2 ASS converter is separate task11."
|
||||
},
|
||||
{
|
||||
"id": "MULTI-SUB-LIFE-01",
|
||||
@@ -6029,10 +6034,10 @@
|
||||
"id": "MULTI-SUB-SWITCH-01",
|
||||
"title": "Intermittent WebKit core5.3.0 caption loss after selected-track source switch",
|
||||
"confirmation": "reproduced",
|
||||
"workspaceState": "Original failing snapshots kept the1..3 cue intact but video time was near0.002. Independent no-plugin and published1.2/current-plugin WebKit traces reproduce5 old-core first-seek misses, with promises sometimes settled during native seeking. Candidate already waits and honors every first seek. Actual main three-engine probes and corrected normal caption sequencing pass; old race observations remain explicit.",
|
||||
"workspaceState": "Reopened during task10: both source and actual-main matrices fail old5.4.0/WebKit caption after switch despite waiting seeking=false. Source failure native time0.001494, intact1..3 cue, active=[]; task09 attribution remains supported but the later-seek workaround is insufficient. Candidate core and old5.1 display regressions pass. Task05 must diagnose remaining old-host ordering, without hiding these failures.",
|
||||
"compatibleResolution": "Classify the old-host source/native-seek race separately from caption rendering; preserve immediate-call probes, require candidate first seeks, and wait for real native source completion before independent caption-seek assertions.",
|
||||
"closureCriteria": "Root cause classified with targeted evidence, any required fix validated, and changed candidate/old-core combination rerun without hiding the original failure.",
|
||||
"status": "resolved",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-05",
|
||||
"PKG-MULTI-SUB-09"
|
||||
@@ -6045,13 +6050,33 @@
|
||||
"refactor/baselines/subtitle-offset-validation.json",
|
||||
"refactor/changes/2026-09-14-PKG-MULTI-SUB-09-switch-order.md",
|
||||
"refactor/baselines/multiple-subtitles-switch-validation.json",
|
||||
"test/browser/multiple-subtitles-switch.spec.js"
|
||||
"test/browser/multiple-subtitles-switch.spec.js",
|
||||
"refactor/changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md",
|
||||
"refactor/baselines/multiple-subtitles-legacy-validation.json"
|
||||
],
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-MULTI-SUB-09-switch-order.md",
|
||||
"refactor/baselines/multiple-subtitles-switch-validation.json"
|
||||
],
|
||||
"resolutionRationale": "No-plugin controls reproduce the same miss and a settled seek restores correct old/new captions without changing plugin data. Candidate source code already provides the necessary readiness guard (BASE-LIFE-44/CORE-19 context). This resolves the suspected plugin-switch defect; it does not approve every historical source call or complete package task05."
|
||||
},
|
||||
{
|
||||
"id": "MULTI-SUB-ASS-01",
|
||||
"title": "Published core5.1.2 ASS converter loses required VTT line breaks",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-11"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md",
|
||||
"refactor/baselines/multiple-subtitles-legacy-validation.json",
|
||||
"refactor/baselines/multiple-subtitles-cores.json",
|
||||
"test/browser/multiple-subtitles-legacy.spec.js"
|
||||
],
|
||||
"workspaceState": "Initial three-engine regression gets only2/3 native cues on actual5.1.2 main. Tarball source assToVtt preserves newlines while published main collapses template line breaks to spaces.5.1.7 ASS renders. Current display regression uses VTT for third5.1.2 track and explicitly does not claim ASS acceptance.",
|
||||
"compatibleResolution": "Adapt malformed old conversion in the plugin while preserving valid existing converter output and public API.",
|
||||
"closureCriteria": "Frozen actual-output reproduction, compatible conversion fix and real ASS display verified on old/current hosts and shippable formats."
|
||||
}
|
||||
]
|
||||
}
|
||||
+39
-1
@@ -2431,7 +2431,9 @@
|
||||
"PKG-MULTI-SUB-05",
|
||||
"ENG-07",
|
||||
"CORE-SUBTITLE-OFFSET-01",
|
||||
"PKG-MULTI-SUB-09"
|
||||
"PKG-MULTI-SUB-09",
|
||||
"PKG-MULTI-SUB-10",
|
||||
"PKG-MULTI-SUB-11"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
@@ -5179,6 +5181,42 @@
|
||||
"changes/2026-09-14-PKG-MULTI-SUB-09-switch-order.md",
|
||||
"baselines/multiple-subtitles-switch-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MULTI-SUB-10",
|
||||
"phase": "5 包迁移:artplayer-plugin-multiple-subtitles",
|
||||
"title": "适配旧核心的多条活动字幕渲染",
|
||||
"scope": [
|
||||
"artplayer-plugin-multiple-subtitles"
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-MULTI-SUB-08",
|
||||
"PKG-MULTI-SUB-09"
|
||||
],
|
||||
"status": "done",
|
||||
"risk": "M",
|
||||
"deliverable": "独立旧宿主显示适配、原生异步字幕重叠回归和兼容边界说明",
|
||||
"acceptance": "旧5.1.2/5.1.7复现先红后绿;保持原生cue对象/时间、旧事件标量和方法身份;多格式显示及清理通过,保留其他组合和设备缺口",
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md",
|
||||
"baselines/multiple-subtitles-legacy-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MULTI-SUB-11",
|
||||
"phase": "5 包迁移:artplayer-plugin-multiple-subtitles",
|
||||
"title": "处理旧发布核心 ASS 转换器丢失换行",
|
||||
"scope": [
|
||||
"artplayer-plugin-multiple-subtitles"
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-MULTI-SUB-10"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "M",
|
||||
"deliverable": "冻结5.1.2发布/源码转换差异,兼容转换适配与真实ASS字幕回归",
|
||||
"acceptance": "保持已有效转换行为,旧发布ASS缺陷有先红后绿与实际产物证据;不得将VTT替代夹具算作ASS通过",
|
||||
"evidence": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
# Tests and fixture ownership
|
||||
|
||||
`test/multiple-subtitles-caption.test.js` checks the old-host view adapter with the
|
||||
actual plugin bundle: native order, original update identity/scalar event payload,
|
||||
escape changes, timestamp cleanup, modern-host isolation and destroy/registration
|
||||
failure cleanup. It runs in `test:unit` and `test:multiple-subtitles`, without new
|
||||
dependencies. Native cue timing is separately verified in the browser suite.
|
||||
|
||||
`test/multiple-subtitles-entities.test.js` checks named entities, literal tags,
|
||||
double encoding, nested semantic tags and selection/reset through the real plugin.
|
||||
The vendor parity suite stays unchanged: the plugin uses the parser's existing
|
||||
|
||||
@@ -266,3 +266,14 @@ syntax failures remain in the historical parser tests. Mobile cases use an Andro
|
||||
UA and synthetic DOM touches through the real core handlers, not trusted hardware
|
||||
input. Fullscreen mouse placement first waits for the progress control to be stable;
|
||||
the earlier hidden-preview screenshot timeout remains in the checkpoint evidence.
|
||||
|
||||
`multiple-subtitles-legacy.spec.js` uses real 5.1.2/5.1.7 cores and the candidate
|
||||
plugin to check three independently timed captions, VTT/SRT, semantic HTML/CSS,
|
||||
the original update method and native cue identities, scalar event payloads,
|
||||
repeated updates, selection/reset and empty intervals. It uses ASS for the third
|
||||
track on5.1.7; the5.1.2 published converter's malformed VTT remains a separate
|
||||
task11 failure and is not covered by substituting a VTT fixture. Use
|
||||
`ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT` for actual main/legacy package bytes.
|
||||
The combination suite retains explicit published1.1/1.2 overlap-loss observations
|
||||
while requiring candidate captions to contain both languages. A passing historical
|
||||
observation is not a compatibility pass.
|
||||
@@ -172,18 +172,19 @@ for (const core of ['published-5.1.2', 'published-5.1.7']) {
|
||||
await window.art.plugins.add(window.multipleFactory({ subtitles: [{ name: 'a', url: '/test/combination.vtt' }, { name: 'b', url: '/test/combination.srt' }] }))
|
||||
})
|
||||
const indexMerge = implementation.version === '1.0.0'
|
||||
const displaysBoth = indexMerge || implementation.name === 'candidate'
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$track.track.cues?.length)).toBe(indexMerge ? 1 : 2)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await seek(page, 1.5)
|
||||
await expect(page.locator('.art-subtitle-a')).toHaveText('First')
|
||||
// This is a deficiency probe, not successful simultaneous-caption acceptance.
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveCount(indexMerge ? 1 : 0)
|
||||
// Historical 1.1/1.2 failures remain observations; candidate must display both.
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveCount(displaysBoth ? 1 : 0)
|
||||
const state = await page.evaluate(() => ({ native: Array.from(window.art.template.$track.track.activeCues, cue => cue.getCueAsHTML().textContent), displayed: window.art.template.$subtitle.textContent }))
|
||||
expect(state.native).toEqual(indexMerge ? ['FirstTranslation'] : ['First', 'Translation'])
|
||||
expect(state.displayed).toBe(indexMerge ? 'FirstTranslation' : 'First')
|
||||
await testInfo.attach('single-active-cue-boundary', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, state, simultaneousCaptionsAccepted: indexMerge, unresolvedCandidateGap: !indexMerge, explanation: '1.0.0 combines by array index; 1.1.0 onward retains separately timed cues. Old core displays only its first active cue. Candidate gap remains open, even when this observation test passes.' }) })
|
||||
expect(state.displayed).toBe(displaysBoth ? 'FirstTranslation' : 'First')
|
||||
await testInfo.attach('single-active-cue-boundary', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, state, simultaneousCaptionsAccepted: displaysBoth, historicalCaptionLoss: !displaysBoth, explanation: '1.0.0 combines by array index; 1.1/1.2 retain separate cues but the old core displays only its first. Candidate adapts the old view while retaining separate native timings.' }) })
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['b']))
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveText('Translation')
|
||||
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { multipleSubtitlesCandidate } from '../helpers/multiple-subtitles.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const candidate = await multipleSubtitlesCandidate()
|
||||
|
||||
for (const core of ['published-5.1.2', 'published-5.1.7']) {
|
||||
test(`Multiple subtitles legacy host ${core}: independently timed captions overlap and clear`, async ({ page, browser }, testInfo) => {
|
||||
await page.route('**/test/legacy-a.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:01.000 --> 00:03.000\n<b>A & safe</b>\n' }))
|
||||
await page.route('**/test/legacy-b.srt', route => route.fulfill({ contentType: 'text/plain', body: '1\n00:00:02,000 --> 00:00:04,000\nB\n' }))
|
||||
await page.route('**/test/legacy-c.ass', route => route.fulfill({ contentType: 'text/plain', body: 'Dialogue: 0,0:00:02.25,0:00:02.75,Default,,0,0,0,,C' }))
|
||||
await page.route('**/test/legacy-c.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:02.250 --> 00:02.750\nC\n' }))
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: candidate.code })
|
||||
await page.addStyleTag({ content: '.art-subtitle-a { color: rgb(255, 0, 0); } .art-subtitle-b { color: rgb(0, 0, 255); }' })
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.originalSubtitleUpdate = window.art.subtitle.update
|
||||
window.legacyPayloads = []
|
||||
window.art.on('subtitleUpdate', text => window.legacyPayloads.push(text))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const thirdFormat = core === 'published-5.1.2' ? 'vtt' : 'ass'
|
||||
await page.evaluate(async (thirdFormat) => {
|
||||
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'a', url: '/test/legacy-a.vtt' }, { name: 'b', url: '/test/legacy-b.srt' }, { name: 'c', url: `/test/legacy-c.${thirdFormat}` }] }))
|
||||
}, thirdFormat)
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$track.track.cues?.length)).toBe(3)
|
||||
await page.evaluate(() => {
|
||||
window.cueIdentities = Array.from(window.art.template.$track.track.cues)
|
||||
})
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
async function seek(time) {
|
||||
await page.evaluate(time => new Promise((resolve) => {
|
||||
window.art.once('video:seeked', resolve)
|
||||
window.art.seek = time
|
||||
}), time)
|
||||
}
|
||||
await seek(1.5)
|
||||
await expect(page.locator('.art-subtitle-a b')).toHaveText('A & safe')
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveCount(0)
|
||||
await seek(2.5)
|
||||
await expect(page.locator('.art-subtitle-a b')).toHaveText('A & safe')
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveText('B')
|
||||
await expect(page.locator('.art-subtitle-c')).toHaveText('C')
|
||||
await expect(page.locator('.art-subtitle-a')).toHaveCSS('color', 'rgb(255, 0, 0)')
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveCSS('color', 'rgb(0, 0, 255)')
|
||||
const state = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.subtitle.update()
|
||||
art.subtitle.update()
|
||||
const cues = Array.from(art.template.$track.track.cues)
|
||||
return { sameUpdate: art.subtitle.update === window.originalSubtitleUpdate, sameCues: cues.every((cue, index) => cue === window.cueIdentities[index]), times: cues.map(cue => [cue.startTime, cue.endTime]), legacyPayload: window.legacyPayloads.at(-1), firstText: art.template.$track.track.activeCues[0].text, counts: ['a', 'b', 'c'].map(name => art.template.$subtitle.querySelectorAll(`.art-subtitle-${name}`).length) }
|
||||
})
|
||||
expect(state.sameUpdate).toBe(true)
|
||||
expect(state.sameCues).toBe(true)
|
||||
expect(state.times).toEqual([[1, 3], [2, 4], [2.25, 2.75]])
|
||||
expect(state.legacyPayload).toBe(state.firstText)
|
||||
expect(state.counts).toEqual([1, 1, 1])
|
||||
await seek(3.5)
|
||||
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveText('B')
|
||||
await expect(page.locator('.art-subtitle-c')).toHaveCount(0)
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveCount(0)
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveText('B')
|
||||
await seek(2.5)
|
||||
await expect(page.locator('.art-subtitle-c')).toHaveText('C')
|
||||
await seek(4.5)
|
||||
await expect(page.locator('.art-subtitle-a, .art-subtitle-b, .art-subtitle-c')).toHaveCount(0)
|
||||
await testInfo.attach('legacy-captions', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), thirdFormat, state, scope: 'Native VTT/SRT overlaps (ASS additionally on 5.1.7), semantic HTML/CSS, legacy scalar event payload, original host method and cue identities, repeated update/selection/reset/empty intervals', limitation: '5.1.2 published ASS converter loses required VTT line breaks; tracked separately in PKG-MULTI-SUB-11.' }) })
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Caption adapters use the repository Node runner.
|
||||
import test from 'node:test'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment } from './helpers/multiple-subtitles.js'
|
||||
|
||||
const candidate = await multipleSubtitlesCandidate()
|
||||
|
||||
function host({ modern = false } = {}) {
|
||||
const env = multipleSubtitlesEnvironment(candidate)
|
||||
const { document } = parseHTML('<html><body><div id="subtitle"></div></body></html>')
|
||||
const target = document.getElementById('subtitle')
|
||||
let active = []
|
||||
env.art.template = { $subtitle: target }
|
||||
Object.defineProperty(env.art.subtitle, 'activeCue', {
|
||||
get() { throw new Error('Capability checks must not read a cue') },
|
||||
})
|
||||
Object.defineProperty(env.art.subtitle, 'textTrack', { get: () => ({ activeCues: active }) })
|
||||
if (modern)
|
||||
Object.defineProperty(env.art.subtitle, 'activeCues', { get() { throw new Error('Modern rendering belongs to the core') } })
|
||||
env.art.subtitle.update = () => {
|
||||
target.innerHTML = active[0]?.text || ''
|
||||
if (active.length)
|
||||
env.emit('subtitleUpdate', active[0].text)
|
||||
}
|
||||
return {
|
||||
...env,
|
||||
target,
|
||||
setActive(cues) { active = cues },
|
||||
}
|
||||
}
|
||||
|
||||
test('Multiple subtitles legacy caption view preserves cue order, event payload and original update', async () => {
|
||||
const env = host()
|
||||
const update = env.art.subtitle.update
|
||||
const payloads = []
|
||||
const views = []
|
||||
env.art.on('subtitleUpdate', (value) => {
|
||||
payloads.push(value)
|
||||
views.push(['before', env.target.textContent])
|
||||
})
|
||||
await env.factory({ subtitles: [] })(env.art)
|
||||
env.art.on('subtitleUpdate', () => views.push(['after', env.target.textContent]))
|
||||
const cues = Object.freeze([
|
||||
Object.freeze({ text: '<div class="a"><b>A & safe</b></div>', startTime: 1, endTime: 3 }),
|
||||
Object.freeze({ text: '<div class="b">B</div>', startTime: 2, endTime: 4 }),
|
||||
Object.freeze({ text: '<div class="c">C</div>', startTime: 2.25, endTime: 2.75 }),
|
||||
])
|
||||
env.setActive(cues)
|
||||
for (let index = 0; index < 3; index++) {
|
||||
update()
|
||||
assert.equal(env.target.textContent, 'A & safeBC')
|
||||
assert.deepEqual(Array.from(env.target.children, node => node.className), ['a', 'b', 'c'])
|
||||
}
|
||||
assert.equal(env.art.subtitle.update, update)
|
||||
assert.deepEqual(payloads, [cues[0].text, cues[0].text, cues[0].text])
|
||||
assert.deepEqual(views, Array.from({ length: 3 }, () => [['before', 'A & safe'], ['after', 'A & safeBC']]).flat())
|
||||
assert.equal(env.listeners.get('subtitleAfterUpdate'), undefined)
|
||||
env.setActive([cues[1]])
|
||||
update()
|
||||
assert.equal(env.target.innerHTML, cues[1].text)
|
||||
env.setActive([])
|
||||
update()
|
||||
assert.equal(env.target.innerHTML, '')
|
||||
assert.equal(payloads.length, 4)
|
||||
env.emit('destroy')
|
||||
})
|
||||
|
||||
test('Multiple subtitles legacy view respects later escape changes and blank CRLF lines', async () => {
|
||||
const env = host()
|
||||
await env.factory({ subtitles: [] })(env.art)
|
||||
env.art.option.subtitle.escape = true
|
||||
env.setActive([{ text: '<b>A & B</b>\r\n\r\nTail' }, { text: '<img src="bad" onerror="bad()">' }])
|
||||
env.art.subtitle.update()
|
||||
assert.equal(env.target.querySelectorAll('b, img').length, 0)
|
||||
assert.deepEqual(Array.from(env.target.children, node => node.textContent), ['<b>A & B</b>', '', 'Tail', '<img src="bad" onerror="bad()">'])
|
||||
assert(Array.from(env.target.children).every(node => node.className === 'art-subtitle-line'))
|
||||
env.emit('destroy')
|
||||
})
|
||||
|
||||
test('Multiple subtitles legacy view cleans timestamp markers for both single and overlapping cues', async () => {
|
||||
const env = host()
|
||||
await env.factory({ subtitles: [] })(env.art)
|
||||
const cue = { text: '<div class="a">A<c.artplayer-multiple-subtitles-timestamp><00:02.000></c><b>B</b></div>' }
|
||||
for (const cues of [[cue], [cue, { text: '<div>C</div>' }]]) {
|
||||
env.setActive(cues)
|
||||
env.art.subtitle.update()
|
||||
assert.equal(env.target.querySelector('b').textContent, 'B')
|
||||
assert.equal(env.target.textContent, cues.length === 1 ? 'AB' : 'ABC')
|
||||
assert.equal(env.target.getElementsByTagName('c.artplayer-multiple-subtitles-timestamp').length, 0)
|
||||
}
|
||||
env.emit('destroy')
|
||||
})
|
||||
|
||||
test('Multiple subtitles modern hosts retain their own view and never install the legacy listener', async () => {
|
||||
const env = host({ modern: true })
|
||||
await env.factory({ subtitles: [] })(env.art)
|
||||
env.target.innerHTML = '<span>Core view</span>'
|
||||
env.setActive([{ text: 'A' }, { text: 'B' }])
|
||||
env.emit('subtitleUpdate', 'A')
|
||||
env.emit('subtitleAfterUpdate', ['A', 'B'])
|
||||
assert.equal(env.target.innerHTML, '<span>Core view</span>')
|
||||
assert.equal(env.listeners.get('subtitleUpdate'), undefined)
|
||||
assert.equal(env.listeners.get('subtitleAfterUpdate').size, 1)
|
||||
env.emit('destroy')
|
||||
assert.equal(env.listeners.get('subtitleAfterUpdate').size, 0)
|
||||
})
|
||||
|
||||
test('Multiple subtitles legacy listener is removed on destroy and captured late callbacks are inert', async () => {
|
||||
const env = host()
|
||||
await env.factory({ subtitles: [] })(env.art)
|
||||
const callback = [...env.listeners.get('subtitleUpdate')][0]
|
||||
env.setActive([{ text: 'A' }, { text: 'B' }])
|
||||
env.emit('destroy')
|
||||
env.target.innerHTML = '<span>After destroy</span>'
|
||||
callback('A')
|
||||
assert.equal(env.target.textContent, 'After destroy')
|
||||
assert.equal(env.listeners.get('subtitleUpdate').size, 0)
|
||||
assert.equal(env.listeners.get('destroy').size, 0)
|
||||
})
|
||||
|
||||
test('Multiple subtitles legacy listener registration failure unwinds both listeners', async () => {
|
||||
const env = host()
|
||||
const on = env.art.on
|
||||
const error = new Error('Listener registration failed')
|
||||
env.art.on = (name, callback) => {
|
||||
on(name, callback)
|
||||
if (name === 'subtitleUpdate')
|
||||
throw error
|
||||
return env.art
|
||||
}
|
||||
await assert.rejects(env.factory({ subtitles: [] })(env.art), value => value === error)
|
||||
assert.equal(env.listeners.get('subtitleUpdate').size, 0)
|
||||
assert.equal(env.listeners.get('destroy').size, 0)
|
||||
assert.equal(env.liveBlobs.size, 0)
|
||||
})
|
||||
Reference in new issue
Block a user