From beaa93d6cc796120018c6cc5f83eb19a1fec4990 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 11:01:47 +0800 Subject: [PATCH] refactor(multiple-subtitles): [PKG-MULTI-SUB-03] own requests and subtitle resources --- .../artplayer-plugin-multiple-subtitles.js | 2 +- ...player-plugin-multiple-subtitles.legacy.js | 2 +- .../artplayer-plugin-multiple-subtitles.mjs | 259 +- package.json | 4 +- .../ARCHITECTURE.md | 40 +- .../artplayer-plugin-multiple-subtitles.js | 2 +- ...player-plugin-multiple-subtitles.legacy.js | 2 +- .../artplayer-plugin-multiple-subtitles.mjs | 259 +- .../src/index.js | 110 +- .../src/lifetime.js | 48 + .../src/merge.js | 30 + .../src/render.js | 68 + .../src/request.js | 33 + .../multiple-subtitles-resources.json | 6559 +++++++++++++++++ .../2026-09-13-PKG-MULTI-SUB-03-resources.md | 73 + refactor/plan.md | 5 +- refactor/progress.md | 17 + refactor/risk-table.md | 4 +- refactor/risks.json | 58 +- refactor/tasks.json | 7 +- .../multiple-subtitles-lifecycle.spec.js | 169 + test/helpers/multiple-subtitles.js | 8 +- test/multiple-subtitles-lifecycle.test.js | 319 + test/multiple-subtitles-merge.test.js | 76 + test/multiple-subtitles.test.js | 4 +- 25 files changed, 7908 insertions(+), 250 deletions(-) create mode 100644 packages/artplayer-plugin-multiple-subtitles/src/lifetime.js create mode 100644 packages/artplayer-plugin-multiple-subtitles/src/merge.js create mode 100644 packages/artplayer-plugin-multiple-subtitles/src/render.js create mode 100644 packages/artplayer-plugin-multiple-subtitles/src/request.js create mode 100644 refactor/baselines/multiple-subtitles-resources.json create mode 100644 refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md create mode 100644 test/browser/multiple-subtitles-lifecycle.spec.js create mode 100644 test/multiple-subtitles-lifecycle.test.js create mode 100644 test/multiple-subtitles-merge.test.js diff --git a/docs/compiled/artplayer-plugin-multiple-subtitles.js b/docs/compiled/artplayer-plugin-multiple-subtitles.js index 5c7e6eafb..83ae8cdbb 100644 --- a/docs/compiled/artplayer-plugin-multiple-subtitles.js +++ b/docs/compiled/artplayer-plugin-multiple-subtitles.js @@ -55,4 +55,4 @@ * * d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=t,this.parse=function(a,s){a=a.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=a.split(/\r\n|\r|\n/),f=!1,c=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(gb&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}for(;void 0!=l[o];){for(var x;!f&&""==l[o];)o++;if(!f&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||c.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}f=!1;var O=new n(l[o],p),L=0;if(u.length>0&&(L=u[u.length-1].startTime),!T||O.parse(x,L)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),f=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var I=new i(x.text,p,s,t);x.tree=I.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}}return u.sort(function(e,t){return e.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(f)){c=f.split(","),f=c[0];var m=c[1]}if("%"!=f[f.length-1]){s("Text position must be a percentage.");continue}if(parseInt(f,10)>100||parseInt(f,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=f.slice(0,f.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=f[f.length-1]){s("Size must be a percentage.");continue}if(parseInt(f,10)>100){s("Size cannot be >100%.");continue}var g=f.slice(0,f.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(f)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=f}else s("Invalid setting.")}}(e.substring(a),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[a])return t;s("Timestamp must not have trailing characters.")}},i=function(e,t,i,a){this.entities=a;var s=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var f=e[r];if("data"==t)if("&"==f)i=f,t="escape";else if("<"==f&&""==n)t="tag";else{if("<"==f||void 0==f)return["text",n];n+=f}else if("escape"==t){if("<"==f||void 0==f){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):s.entities[i]?n+=s.entities[i]:n+=i,["text",n]}if("&"==f)o("Incorrect escape."),n+=i,i=f;else if(/[a-z#0-9]/i.test(f))i+=f;else if(";"==f){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):s.entities[i+f]?n+=s.entities[i+f]:(e=Object.keys(a).find(e=>i.startsWith(e)))?n+=s.entities[e]+i.slice(e.length)+f:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+f,t="data"}else if("tag"==t)if("\t"==f||"\n"==f||"\f"==f||" "==f)t="start tag annotation";else if("."==f)t="start tag class";else if("/"==f)t="end tag";else if(/\d/.test(f))n=f,t="timestamp tag";else{if(">"==f||void 0==f)return">"==f&&r++,["start tag","",[],""];n=f,t="start tag"}else if("start tag"==t)if("\t"==f||"\f"==f||" "==f)t="start tag annotation";else if("\n"==f)i=f,t="start tag annotation";else if("."==f)t="start tag class";else{if(">"==f||void 0==f)return">"==f&&r++,["start tag",n,[],""];n+=f}else if("start tag class"==t)if("\t"==f||"\f"==f||" "==f)i&&l.push(i),i="",t="start tag annotation";else if("\n"==f)i&&l.push(i),i=f,t="start tag annotation";else if("."==f)i&&l.push(i),i="";else{if(">"==f||void 0==f)return">"==f&&r++,i&&l.push(i),["start tag",n,l,""];i+=f}else if("start tag annotation"==t){if(">"==f||void 0==f)return">"==f&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=f}else if("end tag"==t){if(">"==f||void 0==f)return">"==f&&r++,["end tag",n];n+=f}else if("timestamp tag"==t){if(">"==f||void 0==f)return">"==f&&r++,["timestamp",n];n+=f}else o("Never happens.");r++}}this.parse=function(t,a){var s={children:[]},f=s,c=[];function u(e){f.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:f}),f=f.children[f.children.length-1]}function d(e){for(var t=f;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])f.children.push({type:"text",value:p[1],parent:f});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==f.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),f.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),f.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==f.name?f=f.parent:"ruby"==p[1]&&"rt"==f.name?f=f.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var g=new n(p[1],o).parseTimestamp();void 0!=g&&((g<=t||g>=a)&&o("Timestamp must be between start timestamp and end timestamp."),c.length>0&&c[c.length-1]>=g&&o("Timestamp must be greater than any previous timestamp."),f.children.push({type:"timestamp",value:g,parent:f}),c.push(g))}}for(;f.parent;)"v"!=f.name&&o("Required end tag missing."),f=f.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(s)}},a=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,a=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),a=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+a).padStart(2,"0")+"."+t}function n(e){for(var i="",a=0;a/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var r=0;r"}else"timestamp"==s.type?i+="<"+t(s.value)+">":i+="<"+s.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function a(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var s=0;s{const{unescape:i,getExt:s,srtToVtt:r,assToVtt:o}=n.constructor.utils,l=new t,f=new a,c=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i}){const a=await fetch(e.url),s=await a.arrayBuffer(),r=new TextDecoder(e.encoding||"utf-8").decode(s);switch(e.type||t(e.url)){case"srt":return n(r);case"ass":return i(r);case"vtt":return r;default:return""}}(e,{getExt:s,srtToVtt:r,assToVtt:o}))),u=c.map((t,n)=>{const i=l.parse(t,"metadata");return i.url=e[n].url,i.name=e[n].name,i});let d="";function p(e=[]){const a=function(e){const n=(new t).parse("","metadata");for(let t=0;t${n.value}`}}}n.cues.push(...i.cues)}return n}(e),s=f.serialize(a.cues);URL.revokeObjectURL(d);const r=URL.createObjectURL(new Blob([s],{type:"text/vtt"}));d=r,n.option.subtitle.escape=!1,n.subtitle.init({...n.option.subtitle,url:r,type:"vtt",onVttLoad:i})}return p(u),{name:"multipleSubtitles",tracks:(e=[])=>p(e.map(e=>u.find(t=>t.name===e))),reset:()=>p(u)}}}}); +!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),(gb&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var O=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||O.parse(x,A)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var L=new i(x.text,p,a,t);x.tree=L.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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;r100)){a("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){a("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){a("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){a("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){a("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){a("Size must be a percentage.");continue}if(parseInt(c,10)>100){a("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){a("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){a("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){a("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else a("Invalid setting.")}}(e.substring(s),t),!0}else a("No valid timestamp separator found.");else a("No valid timestamp separator found.");else a("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[s])return t;a("Timestamp must not have trailing characters.")}},i=function(e,t,i,s){this.entities=s;var a=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var c=e[r];if("data"==t)if("&"==c)i=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[i]?n+=a.entities[i]:n+=i,["text",n]}if("&"==c)o("Incorrect escape."),n+=i,i=c;else if(/[a-z#0-9]/i.test(c))i+=c;else if(";"==c){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[i+c]?n+=a.entities[i+c]:(e=Object.keys(s).find(e=>i.startsWith(e)))?n+=a.entities[e]+i.slice(e.length)+c:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)i=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)i&&l.push(i),i="",t="start tag annotation";else if("\n"==c)i&&l.push(i),i=c,t="start tag annotation";else if("."==c)i&&l.push(i),i="";else{if(">"==c||void 0==c)return">"==c&&r++,i&&l.push(i),["start tag",n,l,""];i+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["timestamp",n];n+=c}else o("Never happens.");r++}}this.parse=function(t,s){var a={children:[]},c=a,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function d(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])c.children.push({type:"text",value:p[1],parent:c});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==c.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),c.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),c.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==c.name?c=c.parent:"ruby"==p[1]&&"rt"==c.name?c=c.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var g=new n(p[1],o).parseTimestamp();void 0!=g&&((g<=t||g>=s)&&o("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=g&&o("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:g,parent:c}),f.push(g))}}for(;c.parent;)"v"!=c.name&&o("Required end tag missing."),c=c.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},s=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,s=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),s=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+s).padStart(2,"0")+"."+t}function n(e){for(var i="",s=0;s/g,">");else if("object"==a.type){if(i+="<"+a.name,a.classes)for(var r=0;r"}else"timestamp"==a.type?i+="<"+t(a.value)+">":i+="<"+a.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function s(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var a=0;a{if(t.closed)return;const a=URL.createObjectURL(new Blob([s],{type:"text/vtt"})),r=function(e){let n=!1,i=()=>{};const s={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(s.free),s}(a);if(t.closed)return;const o=i;let l,c,f=!1;i=r;try{if(l=e.option.subtitle,c=l.escape,t.closed||i!==r)return;l.escape=!1,f=!0;const s={...l,url:a,type:"vtt",onVttLoad:n};if(t.closed||i!==r)return;const o=e.subtitle.init(s);Promise.resolve(o).catch(e=>{i===r&&(i=null,r.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(u){throw i===r&&(i=o,f&&!1===l.escape&&(l.escape=c)),r.free(),u}finally{o!==i&&o?.free(),r!==i&&r.free()}}}return function({subtitles:e=[]}){return async n=>{const{unescape:i,getExt:r,srtToVtt:o,assToVtt:l}=n.constructor.utils,c=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const s=new Promise(e=>{i=e});function a(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const r={get closed(){return t},own:e=>(t?a(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,s]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)a(t)}};if(!t){r.own(()=>e.off("destroy",r.dispose));try{e.on("destroy",r.dispose)}catch(o){throw r.dispose(),o}}return r}(n),f=a(n,c,i);let u=[];function d(e){c.closed||f(function(e){const t=[];for(const n of e)for(const e of n.cues)t.push({...e,tree:{...e.tree,children:e.tree.children.map(e=>({...e,value:`
${e.value}
`}))}});return(new s).serialize(t)}(e))}const p={name:"multipleSubtitles",tracks(e=[]){c.closed||d(e.map(e=>u.find(t=>t.name===e)))},reset(){d(u)}};try{if(c.closed)return p;const n=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i},s){if(s.closed)return;const a="function"===typeof AbortController?new AbortController:null,r=s.own(()=>a?.abort());try{const r=e.url;if(s.closed)return;const o=await s.wait(a?fetch(r,{signal:a.signal}):fetch(r));if(s.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=await s.wait(o.arrayBuffer());if(s.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw a?.abort(),o}finally{r()}}(e,{getExt:r,srtToVtt:o,assToVtt:l},c)));return c.closed||(u=function(e,n){const i=new t;return e.map((e,t)=>{const s=i.parse(e,"metadata");return s.url=n[t].url,s.name=n[t].name,s})}(n,e),d(u)),p}catch(m){throw c.dispose(),m}}}}); diff --git a/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js b/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js index 14454c165..8724c0863 100644 --- a/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js +++ b/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js @@ -55,4 +55,4 @@ * * d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))a.call(t,n)&&r(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&r(e,n,t[n]);return e},l=(e,t,n)=>new Promise((i,a)=>{var s=e=>{try{o(n.next(e))}catch(t){a(t)}},r=e=>{try{o(n.throw(e))}catch(t){a(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,r);o((n=n.apply(e,t)).next())}),f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},c=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),a=0,s=t.split(/\r\n|\r|\n/),r=!1,o=[],l=[],c=[];function p(e,t){c.push({message:e,line:a+1,col:t})}var m=s[a],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(gb&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),a++;""!=s[a]&&void 0!=s[a];){if(p("No blank line after the signature."),-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}for(;void 0!=s[a];){for(var x;!r&&""==s[a];)a++;if(!r&&void 0==s[a])break;x=Object.assign({},f,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==s[a].indexOf("--\x3e")){if(x.id=s[a],/^NOTE($|[ \t])/.test(x.id)){for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),a++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(s[a]),a++;if(l.length){p("Style blocks cannot appear after the first cue.");continue}w||o.push(y.join("\n"));continue}if(""==s[++a]||void 0==s[a]){p("Cue identifier cannot be standalone.");continue}if(-1==s[a].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}r=!1;var O=new u(s[a],p),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!T||O.parse(x,P)){for(a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){p("Blank line missing before cue."),r=!0;break}""!=x.text&&(x.text+="\n"),x.text+=s[a],a++}var I=new d(x.text,p,n,e);x.tree=I.parse(x.startTime,x.endTime),l.push(x)}else for(x=null,a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}}return l.sort(function(e,t){return e.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(f)){c=f.split(","),f=c[0];var m=c[1]}if("%"!=f[f.length-1]){s("Text position must be a percentage.");continue}if(parseInt(f,10)>100||parseInt(f,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=f.slice(0,f.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=f[f.length-1]){s("Size must be a percentage.");continue}if(parseInt(f,10)>100){s("Size cannot be >100%.");continue}var g=f.slice(0,f.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(f)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=f}else s("Invalid setting.")}}(e.substring(a),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[a])return t;s("Timestamp must not have trailing characters.")}},d=function(e,t,n,i){this.entities=i;var a=this,s=(e=e,0),r=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var f=e[s];if("data"==t)if("&"==f)o=f,t="escape";else if("<"==f&&""==n)t="tag";else{if("<"==f||void 0==f)return["text",n];n+=f}else if("escape"==t){if("<"==f||void 0==f){let e;return r("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[o]?n+=a.entities[o]:n+=o,["text",n]}if("&"==f)r("Incorrect escape."),n+=o,o=f;else if(/[a-z#0-9]/i.test(f))o+=f;else if(";"==f){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[o+f]?n+=a.entities[o+f]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=a.entities[e]+o.slice(e.length)+f:(r("Incorrect escape."),n+=o+";"),t="data"}else r("Incorrect escape."),n+=o+f,t="data"}else if("tag"==t)if("\t"==f||"\n"==f||"\f"==f||" "==f)t="start tag annotation";else if("."==f)t="start tag class";else if("/"==f)t="end tag";else if(/\d/.test(f))n=f,t="timestamp tag";else{if(">"==f||void 0==f)return">"==f&&s++,["start tag","",[],""];n=f,t="start tag"}else if("start tag"==t)if("\t"==f||"\f"==f||" "==f)t="start tag annotation";else if("\n"==f)o=f,t="start tag annotation";else if("."==f)t="start tag class";else{if(">"==f||void 0==f)return">"==f&&s++,["start tag",n,[],""];n+=f}else if("start tag class"==t)if("\t"==f||"\f"==f||" "==f)o&&l.push(o),o="",t="start tag annotation";else if("\n"==f)o&&l.push(o),o=f,t="start tag annotation";else if("."==f)o&&l.push(o),o="";else{if(">"==f||void 0==f)return">"==f&&s++,o&&l.push(o),["start tag",n,l,""];o+=f}else if("start tag annotation"==t){if(">"==f||void 0==f)return">"==f&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=f}else if("end tag"==t){if(">"==f||void 0==f)return">"==f&&s++,["end tag",n];n+=f}else if("timestamp tag"==t){if(">"==f||void 0==f)return">"==f&&s++,["timestamp",n];n+=f}else r("Never happens.");s++}}this.parse=function(t,i){var a={children:[]},f=a,c=[];function d(e){f.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:f}),f=f.children[f.children.length-1]}function p(e){for(var t=f;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])f.children.push({type:"text",value:m[1],parent:f});else if("start tag"==m[0]){"chapters"==n&&r("Start tags not allowed in chapter title text.");var g=m[1];"v"!=g&&"lang"!=g&&""!=m[3]&&r("Only and can have an annotation."),"c"==g||"i"==g||"b"==g||"u"==g||"ruby"==g||"rt"==g&&"ruby"==f.name?d(m):"v"==g?(p("v")&&r(" cannot be nested inside itself."),d(m),f.value=m[3],m[3]||r(" requires an annotation.")):"lang"==g?(d(m),f.value=m[3]):r("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&r("End tags not allowed in chapter title text."),m[1]==f.name?f=f.parent:"ruby"==m[1]&&"rt"==f.name?f=f.parent.parent:r("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&r("Timestamp not allowed in chapter title text.");var h=new u(m[1],r).parseTimestamp();void 0!=h&&((h<=t||h>=i)&&r("Timestamp must be between start timestamp and end timestamp."),c.length>0&&c[c.length-1]>=h&&r("Timestamp must be greater than any previous timestamp."),f.children.push({type:"timestamp",value:h,parent:f}),c.push(h))}}for(;f.parent;)"v"!=f.name&&r("Required end tag missing."),f=f.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},p=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,a=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),a=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+a).padStart(2,"0")+"."+t}function t(n){for(var i="",a=0;a/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var r=0;r"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var a="WEBVTT\n\n";if(t)for(var s=0;sl(null,null,function*(){const{unescape:a,getExt:s,srtToVtt:r,assToVtt:f}=i.constructor.utils,u=new c,d=new p,m=yield Promise.all(e.map(e=>function(e,t){return l(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i}){const a=yield fetch(e.url),s=yield a.arrayBuffer(),r=new TextDecoder(e.encoding||"utf-8").decode(s);switch(e.type||t(e.url)){case"srt":return n(r);case"ass":return i(r);case"vtt":return r;default:return""}})}(e,{getExt:s,srtToVtt:r,assToVtt:f}))),g=m.map((t,n)=>{const i=u.parse(t,"metadata");return i.url=e[n].url,i.name=e[n].name,i});let h="";function v(e=[]){const s=function(e){const t=(new c).parse("","metadata");for(let n=0;n${n.value}`}}}t.cues.push(...i.cues)}return t}(e),r=d.serialize(s.cues);URL.revokeObjectURL(h);const l=URL.createObjectURL(new Blob([r],{type:"text/vtt"}));var f;h=l,i.option.subtitle.escape=!1,i.subtitle.init((f=o({},i.option.subtitle),t(f,n({url:l,type:"vtt",onVttLoad:a}))))}return v(g),{name:"multipleSubtitles",tracks:(e=[])=>v(e.map(e=>g.find(t=>t.name===e))),reset:()=>v(g)}})}}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())});var f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),a=!1,o=[],l=[],c=[];function u(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(gb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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=0;a100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){s("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){s("Size must be a percentage.");continue}if(parseInt(c,10)>100){s("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else s("Invalid setting.")}}(e.substring(r),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[r])return t;s("Timestamp must not have trailing characters.")}},p=function(e,t,n,i){this.entities=i;var r=this,s=(e=e,0),a=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var c=e[s];if("data"==t)if("&"==c)o=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return a("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):r.entities[o]?n+=r.entities[o]:n+=o,["text",n]}if("&"==c)a("Incorrect escape."),n+=o,o=c;else if(/[a-z#0-9]/i.test(c))o+=c;else if(";"==c){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):r.entities[o+c]?n+=r.entities[o+c]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=r.entities[e]+o.slice(e.length)+c:(a("Incorrect escape."),n+=o+";"),t="data"}else a("Incorrect escape."),n+=o+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)o=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)o&&l.push(o),o="",t="start tag annotation";else if("\n"==c)o&&l.push(o),o=c,t="start tag annotation";else if("."==c)o&&l.push(o),o="";else{if(">"==c||void 0==c)return">"==c&&s++,o&&l.push(o),["start tag",n,l,""];o+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["timestamp",n];n+=c}else a("Never happens.");s++}}this.parse=function(t,i){var r={children:[]},c=r,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function p(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])c.children.push({type:"text",value:m[1],parent:c});else if("start tag"==m[0]){"chapters"==n&&a("Start tags not allowed in chapter title text.");var g=m[1];"v"!=g&&"lang"!=g&&""!=m[3]&&a("Only and can have an annotation."),"c"==g||"i"==g||"b"==g||"u"==g||"ruby"==g||"rt"==g&&"ruby"==c.name?u(m):"v"==g?(p("v")&&a(" cannot be nested inside itself."),u(m),c.value=m[3],m[3]||a(" requires an annotation.")):"lang"==g?(u(m),c.value=m[3]):a("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&a("End tags not allowed in chapter title text."),m[1]==c.name?c=c.parent:"ruby"==m[1]&&"rt"==c.name?c=c.parent.parent:a("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&a("Timestamp not allowed in chapter title text.");var h=new d(m[1],a).parseTimestamp();void 0!=h&&((h<=t||h>=i)&&a("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=h&&a("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:h,parent:c}),f.push(h))}}for(;c.parent;)"v"!=c.name&&a("Required end tag missing."),c=c.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(r)}},m=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,r=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),r=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+r).padStart(2,"0")+"."+t}function t(n){for(var i="",r=0;r/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var a=0;a"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var r="WEBVTT\n\n";if(t)for(var s=0;s{if(t.closed)return;const s=URL.createObjectURL(new Blob([r],{type:"text/vtt"})),a=function(e){let n=!1,i=()=>{};const r={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(r.free),r}(s);if(t.closed)return;const c=i;let f,u,d=!1;i=a;try{if(f=e.option.subtitle,u=f.escape,t.closed||i!==a)return;f.escape=!1,d=!0;const r=l(o({},f),{url:s,type:"vtt",onVttLoad:n});if(t.closed||i!==a)return;const c=e.subtitle.init(r);Promise.resolve(c).catch(e=>{i===a&&(i=null,a.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(p){throw i===a&&(i=c,d&&!1===f.escape&&(f.escape=u)),a.free(),p}finally{c!==i&&(null==c||c.free()),a!==i&&a.free()}}}return function({subtitles:e=[]}){return t=>c(null,null,function*(){const{unescape:n,getExt:i,srtToVtt:r,assToVtt:s}=t.constructor.utils,a=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const r=new Promise(e=>{i=e});function s(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const a={get closed(){return t},own:e=>(t?s(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,r]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)s(t)}};if(!t){a.own(()=>e.off("destroy",a.dispose));try{e.on("destroy",a.dispose)}catch(o){throw a.dispose(),o}}return a}(t),f=g(t,a,n);let d=[];function p(e){a.closed||f(function(e){const t=[];for(const n of e)for(const e of n.cues)t.push(l(o({},e),{tree:l(o({},e.tree),{children:e.tree.children.map(e=>l(o({},e),{value:`
${e.value}
`}))})}));return(new m).serialize(t)}(e))}const h={name:"multipleSubtitles",tracks(e=[]){a.closed||p(e.map(e=>d.find(t=>t.name===e)))},reset(){p(d)}};try{if(a.closed)return h;const t=yield Promise.all(e.map(e=>function(e,t,n){return c(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i},r){if(r.closed)return;const s="function"===typeof AbortController?new AbortController:null,a=r.own(()=>null==s?void 0:s.abort());try{const a=e.url;if(r.closed)return;const o=yield r.wait(s?fetch(a,{signal:s.signal}):fetch(a));if(r.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=yield r.wait(o.arrayBuffer());if(r.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw null==s||s.abort(),o}finally{a()}})}(e,{getExt:i,srtToVtt:r,assToVtt:s},a)));return a.closed||(d=function(e,t){const n=new u;return e.map((e,i)=>{const r=n.parse(e,"metadata");return r.url=t[i].url,r.name=t[i].name,r})}(t,e),p(d)),h}catch(v){throw a.dispose(),v}})}}); diff --git a/docs/compiled/artplayer-plugin-multiple-subtitles.mjs b/docs/compiled/artplayer-plugin-multiple-subtitles.mjs index 4befa73d6..fe8eb5a22 100644 --- a/docs/compiled/artplayer-plugin-multiple-subtitles.mjs +++ b/docs/compiled/artplayer-plugin-multiple-subtitles.mjs @@ -55,6 +55,56 @@ * * d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. */ +function createLifetime(art) { + let closed = Boolean(art.isDestroy); + const cleanups = /* @__PURE__ */ new Set(); + let cancel; + const cancelled = new Promise((resolve) => { + cancel = resolve; + }); + function run(cleanup) { + try { + cleanup(); + } catch (error) { + console.warn("Failed to clean up multiple subtitles:", error); + } + } + const lifetime = { + get closed() { + return closed; + }, + own(cleanup) { + if (closed) + run(cleanup); + else + cleanups.add(cleanup); + return () => cleanups.delete(cleanup); + }, + wait(value) { + return Promise.race([value, cancelled]); + }, + dispose() { + if (closed) + return; + closed = true; + cancel(); + const pending = [...cleanups].reverse(); + cleanups.clear(); + for (const cleanup of pending) + run(cleanup); + } + }; + if (!closed) { + lifetime.own(() => art.off("destroy", lifetime.dispose)); + try { + art.on("destroy", lifetime.dispose); + } catch (error) { + lifetime.dispose(); + throw error; + } + } + return lifetime; +} var defaultCueSettings = { direction: "horizontal", snapToLines: true, @@ -799,85 +849,166 @@ var WebVTTSerializer = function() { return result; }; }; -async function loadVtt(option, { getExt, srtToVtt, assToVtt }) { - const response = await fetch(option.url); - const buffer = await response.arrayBuffer(); - const decoder = new TextDecoder(option.encoding || "utf-8"); - const text = decoder.decode(buffer); - switch (option.type || getExt(option.url)) { - case "srt": { - return srtToVtt(text); - } - case "ass": { - return assToVtt(text); - } - case "vtt": { - return text; - } - default: - return ""; - } -} -function mergeTrees(trees) { +function parseTracks(vtts, subtitles) { const parser = new WebVTTParser(); - const result = parser.parse("", "metadata"); - for (let i = 0; i < trees.length; i++) { - const tree = trees[i]; - if (!tree.updated) { - tree.updated = true; - for (let j = 0; j < tree.cues.length; j++) { - const cue = tree.cues[j]; - for (let k = 0; k < cue.tree.children.length; k++) { - const children = cue.tree.children[k]; - children.value = `
${children.value}
`; + return vtts.map((vtt, index) => { + const tree = parser.parse(vtt, "metadata"); + tree.url = subtitles[index].url; + tree.name = subtitles[index].name; + return tree; + }); +} +function serializeTracks(trees) { + const cues = []; + for (const tree of trees) { + for (const cue of tree.cues) { + cues.push({ + ...cue, + tree: { + ...cue.tree, + children: cue.tree.children.map((child) => ({ + ...child, + value: `
${child.value}
` + })) } - } + }); } - result.cues.push(...tree.cues); } - return result; + return new WebVTTSerializer().serialize(cues); +} +function createRenderer(art, lifetime, unescape) { + let current = null; + function own(url) { + let freed = false; + let release = () => { + }; + const entry = { + free() { + if (freed) + return; + freed = true; + release(); + URL.revokeObjectURL(url); + } + }; + release = lifetime.own(entry.free); + return entry; + } + return (vtt) => { + if (lifetime.closed) + return; + const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" })); + const entry = own(url); + if (lifetime.closed) + return; + const previous = current; + let option; + let escape; + let configured = false; + current = entry; + try { + option = art.option.subtitle; + escape = option.escape; + if (lifetime.closed || current !== entry) + return; + option.escape = false; + configured = true; + const config = { ...option, url, type: "vtt", onVttLoad: unescape }; + if (lifetime.closed || current !== entry) + return; + const pending = art.subtitle.init(config); + Promise.resolve(pending).catch((error) => { + if (current === entry) { + current = null; + entry.free(); + } + if (!lifetime.closed) + console.warn("Failed to initialize multiple subtitles:", error); + }); + } catch (error) { + if (current === entry) { + current = previous; + if (configured && option.escape === false) + option.escape = escape; + } + entry.free(); + throw error; + } finally { + if (previous !== current) + previous?.free(); + if (entry !== current) + entry.free(); + } + }; +} +async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) { + if (lifetime.closed) + return; + const controller = typeof AbortController === "function" ? new AbortController() : null; + const release = lifetime.own(() => controller?.abort()); + try { + const url = option.url; + if (lifetime.closed) + return; + const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url)); + if (lifetime.closed) + return; + if (response.ok === false) + throw new Error(`Failed to fetch multiple subtitles: HTTP ${response.status}`); + const buffer = await lifetime.wait(response.arrayBuffer()); + if (lifetime.closed) + return; + const text = new TextDecoder(option.encoding || "utf-8").decode(buffer); + switch (option.type || getExt(option.url)) { + case "srt": + return srtToVtt(text); + case "ass": + return assToVtt(text); + case "vtt": + return text; + default: + return ""; + } + } catch (error) { + controller?.abort(); + throw error; + } finally { + release(); + } } function artplayerPluginMultipleSubtitles({ subtitles = [] }) { return async (art) => { const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils; - const parser = new WebVTTParser(); - const seri = new WebVTTSerializer(); - const vtts = await Promise.all( - subtitles.map((option) => { - return loadVtt(option, { getExt, srtToVtt, assToVtt }); - }) - ); - const trees = vtts.map((vtt, index) => { - const tree = parser.parse(vtt, "metadata"); - tree.url = subtitles[index].url; - tree.name = subtitles[index].name; - return tree; - }); - let lastUrl = ""; - function setTracks(trees2 = []) { - const tree = mergeTrees(trees2); - const vtt = seri.serialize(tree.cues); - URL.revokeObjectURL(lastUrl); - const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" })); - lastUrl = url; - art.option.subtitle.escape = false; - art.subtitle.init({ - ...art.option.subtitle, - url, - type: "vtt", - onVttLoad: unescape - }); + const lifetime = createLifetime(art); + const render = createRenderer(art, lifetime, unescape); + let trees = []; + function setTracks(selected) { + if (!lifetime.closed) + render(serializeTracks(selected)); } - setTracks(trees); - return { + const result = { name: "multipleSubtitles", tracks(names = []) { - return setTracks(names.map((name) => trees.find((tree) => tree.name === name))); + if (!lifetime.closed) + setTracks(names.map((name) => trees.find((tree) => tree.name === name))); }, reset() { - return setTracks(trees); + setTracks(trees); } }; + try { + if (lifetime.closed) + return result; + const vtts = await Promise.all(subtitles.map((option) => loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime))); + if (lifetime.closed) + return result; + trees = parseTracks(vtts, subtitles); + setTracks(trees); + return result; + } catch (error) { + lifetime.dispose(); + throw error; + } }; } export { diff --git a/package.json b/package.json index 065d5cdba..14f12f8dc 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js", "typecheck": "node scripts/typecheck.mjs", - "test:unit": "node --test test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", + "test:unit": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", "test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs", "test": "yarn test:node && yarn test:baseline", "test:browser": "playwright test", @@ -88,7 +88,7 @@ "test:auto-thumbnail": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs", "test:vtt-thumbnail": "node --test test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs refactor/scripts/vtt-thumbnail-types.test.mjs", "test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs", - "test:multiple-subtitles": "node --test test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs" + "test:multiple-subtitles": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md b/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md index bc526a9be..d736c8c86 100644 --- a/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md +++ b/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md @@ -8,9 +8,15 @@ task PKG-MULTI-SUB-04 owns that compatibility work. Do not infer runtime behavio ## Current module boundaries -- `src/index.js` currently combines download/decode, conversion, parsing, merging, Blob URL - replacement and public registration. Task 03 will separate pure merge, requests and owned - resources while preserving the factory and CSS hooks. +- `src/index.js` composes registration, named selection and the unchanged public result. +- `src/request.js` owns each fetch through byte decoding and delegates SRT/ASS conversion + to the existing core utils. Failure aborts the request; registration failure closes siblings. +- `src/merge.js` parses metadata and serializes selected cues without mutating parsed trees. + It depends only on the vendor parser, not the player or browser resource APIs. +- `src/lifetime.js` owns the destroy listener, request cancellation and resource callbacks. + Its cancellation Promise also settles pending operations without AbortController support. +- `src/render.js` owns generated Blob URLs and synchronous/async host installation outcomes. + Reentrant selections and stale host rejections cannot release the latest selected resource. - `src/parser.js` is vendored WebVTT parser/serializer code with a CC0 dedication. It is not owned plugin code to mechanically migrate to TypeScript. Keep it distinct from task 04. The pinned upstream reference and full adaptation check are in @@ -23,19 +29,22 @@ task PKG-MULTI-SUB-04 owns that compatibility work. Do not infer runtime behavio All tracks download concurrently and decode through TextDecoder. Explicit type wins over the URL extension; SRT/ASS conversion delegates to existing core utils. `onParser` appears in declarations but is not called. Parsing runs in metadata mode. Each track's top-level -cue text receives a name-based div once; current merging concatenates cue arrays by track +cue text receives a name-based div in a copied cue; merging concatenates cue arrays by track order, whereas 1.0.0 merged by cue index and retained the first track's timestamps. -Each selection serializes a new VTT, revokes the preceding URL, creates a text/vtt Blob, -sets the live subtitle option's escape to false and calls subtitle.init with the live -configuration plus URL/type/onVttLoad overrides. The latest URL and pending requests have -no destroy owner yet. These are explicit future fixes, not behavior to silently call safe. +Each selection serializes VTT and allocates a text/vtt Blob before replacing the preceding +URL. It sets the live subtitle option's escape to false and calls subtitle.init with live +configuration plus URL/type/onVttLoad overrides. Allocation failure preserves the prior URL; +synchronous installation failure releases the new URL and restores the previous ownership. +Async host failure is observed and releases only the failed active installation, with a warning +while alive. The plugin cannot generically undo host side effects after asynchronous failure. +Registration does not await host media loading, and selection methods remain void. -The failure baseline now reproduces late fetch/body installation, retained and newly allocated -URLs after destroy, ignored HTTP failures, orphaned URLs after init failures, and revoking the -old URL before a new allocation can fail. Async init rejection is currently unobserved by the -plugin. Task 03 must handle these internal failures while retaining the void selection methods. -Keep the current parser's best-effort behavior separate from these resource fixes: invalid-header +Destroy cancels pending requests, settles registration with an inert result, releases owned +URLs and removes its listener. Retained methods become no-ops; restart does not refetch tracks. +Do not restore the shared escape option on normal destroy: another consumer may now own it. +The historical failure suite deliberately retains the old outcomes; the candidate lifecycle +suite verifies their fixes. Preserve parser best-effort behavior independently: invalid-header diagnostics can coexist with usable cues, and metadata is intentionally read after download. ## Vendor boundary and validation @@ -51,6 +60,7 @@ Use the pinned Node and Yarn 1.22.22: ```sh yarn test:multiple-subtitles yarn test:browser test/browser/multiple-subtitles-history.spec.js +yarn test:browser test/browser/multiple-subtitles-lifecycle.spec.js yarn build artplayer-plugin-multiple-subtitles ``` @@ -63,5 +73,9 @@ uses actual 1.2.0 main with published/candidate cores: SRT display and selection fetch after destroy, and native Blob readability/late reset allocations. It covers Chromium, Firefox and Windows WebKit; it is not all historical cores, ASS display or physical Safari. The failure unit suite uses real current core converters and TextDecoder for SRT/ASS/encodings. +Candidate lifecycle and merge suites cover cancellation, failures, resource ownership, +reentrancy and immutable serialization. The candidate browser suite adds real video source +switching, HTTP rejection and Blob revocation. For main/legacy validation, set the artifact +environment variable above before running it; source builds are used when it is absent. Tasks 05/06 still own complete core/device combinations and demo acceptance. See `../../refactor/baselines/multiple-subtitles-contract.md` for exact historical differences. diff --git a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js index 5c7e6eafb..83ae8cdbb 100644 --- a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js +++ b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js @@ -55,4 +55,4 @@ * * d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=t,this.parse=function(a,s){a=a.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=a.split(/\r\n|\r|\n/),f=!1,c=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(gb&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}for(;void 0!=l[o];){for(var x;!f&&""==l[o];)o++;if(!f&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||c.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}f=!1;var O=new n(l[o],p),L=0;if(u.length>0&&(L=u[u.length-1].startTime),!T||O.parse(x,L)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),f=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var I=new i(x.text,p,s,t);x.tree=I.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){f=!0;break}o++}}return u.sort(function(e,t){return e.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(f)){c=f.split(","),f=c[0];var m=c[1]}if("%"!=f[f.length-1]){s("Text position must be a percentage.");continue}if(parseInt(f,10)>100||parseInt(f,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=f.slice(0,f.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=f[f.length-1]){s("Size must be a percentage.");continue}if(parseInt(f,10)>100){s("Size cannot be >100%.");continue}var g=f.slice(0,f.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(f)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=f}else s("Invalid setting.")}}(e.substring(a),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[a])return t;s("Timestamp must not have trailing characters.")}},i=function(e,t,i,a){this.entities=a;var s=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var f=e[r];if("data"==t)if("&"==f)i=f,t="escape";else if("<"==f&&""==n)t="tag";else{if("<"==f||void 0==f)return["text",n];n+=f}else if("escape"==t){if("<"==f||void 0==f){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):s.entities[i]?n+=s.entities[i]:n+=i,["text",n]}if("&"==f)o("Incorrect escape."),n+=i,i=f;else if(/[a-z#0-9]/i.test(f))i+=f;else if(";"==f){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):s.entities[i+f]?n+=s.entities[i+f]:(e=Object.keys(a).find(e=>i.startsWith(e)))?n+=s.entities[e]+i.slice(e.length)+f:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+f,t="data"}else if("tag"==t)if("\t"==f||"\n"==f||"\f"==f||" "==f)t="start tag annotation";else if("."==f)t="start tag class";else if("/"==f)t="end tag";else if(/\d/.test(f))n=f,t="timestamp tag";else{if(">"==f||void 0==f)return">"==f&&r++,["start tag","",[],""];n=f,t="start tag"}else if("start tag"==t)if("\t"==f||"\f"==f||" "==f)t="start tag annotation";else if("\n"==f)i=f,t="start tag annotation";else if("."==f)t="start tag class";else{if(">"==f||void 0==f)return">"==f&&r++,["start tag",n,[],""];n+=f}else if("start tag class"==t)if("\t"==f||"\f"==f||" "==f)i&&l.push(i),i="",t="start tag annotation";else if("\n"==f)i&&l.push(i),i=f,t="start tag annotation";else if("."==f)i&&l.push(i),i="";else{if(">"==f||void 0==f)return">"==f&&r++,i&&l.push(i),["start tag",n,l,""];i+=f}else if("start tag annotation"==t){if(">"==f||void 0==f)return">"==f&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=f}else if("end tag"==t){if(">"==f||void 0==f)return">"==f&&r++,["end tag",n];n+=f}else if("timestamp tag"==t){if(">"==f||void 0==f)return">"==f&&r++,["timestamp",n];n+=f}else o("Never happens.");r++}}this.parse=function(t,a){var s={children:[]},f=s,c=[];function u(e){f.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:f}),f=f.children[f.children.length-1]}function d(e){for(var t=f;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])f.children.push({type:"text",value:p[1],parent:f});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==f.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),f.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),f.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==f.name?f=f.parent:"ruby"==p[1]&&"rt"==f.name?f=f.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var g=new n(p[1],o).parseTimestamp();void 0!=g&&((g<=t||g>=a)&&o("Timestamp must be between start timestamp and end timestamp."),c.length>0&&c[c.length-1]>=g&&o("Timestamp must be greater than any previous timestamp."),f.children.push({type:"timestamp",value:g,parent:f}),c.push(g))}}for(;f.parent;)"v"!=f.name&&o("Required end tag missing."),f=f.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(s)}},a=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,a=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),a=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+a).padStart(2,"0")+"."+t}function n(e){for(var i="",a=0;a/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var r=0;r"}else"timestamp"==s.type?i+="<"+t(s.value)+">":i+="<"+s.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function a(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var s=0;s{const{unescape:i,getExt:s,srtToVtt:r,assToVtt:o}=n.constructor.utils,l=new t,f=new a,c=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i}){const a=await fetch(e.url),s=await a.arrayBuffer(),r=new TextDecoder(e.encoding||"utf-8").decode(s);switch(e.type||t(e.url)){case"srt":return n(r);case"ass":return i(r);case"vtt":return r;default:return""}}(e,{getExt:s,srtToVtt:r,assToVtt:o}))),u=c.map((t,n)=>{const i=l.parse(t,"metadata");return i.url=e[n].url,i.name=e[n].name,i});let d="";function p(e=[]){const a=function(e){const n=(new t).parse("","metadata");for(let t=0;t${n.value}`}}}n.cues.push(...i.cues)}return n}(e),s=f.serialize(a.cues);URL.revokeObjectURL(d);const r=URL.createObjectURL(new Blob([s],{type:"text/vtt"}));d=r,n.option.subtitle.escape=!1,n.subtitle.init({...n.option.subtitle,url:r,type:"vtt",onVttLoad:i})}return p(u),{name:"multipleSubtitles",tracks:(e=[])=>p(e.map(e=>u.find(t=>t.name===e))),reset:()=>p(u)}}}}); +!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),(gb&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var O=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||O.parse(x,A)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var L=new i(x.text,p,a,t);x.tree=L.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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;r100)){a("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){a("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){a("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){a("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){a("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){a("Size must be a percentage.");continue}if(parseInt(c,10)>100){a("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){a("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){a("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){a("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else a("Invalid setting.")}}(e.substring(s),t),!0}else a("No valid timestamp separator found.");else a("No valid timestamp separator found.");else a("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[s])return t;a("Timestamp must not have trailing characters.")}},i=function(e,t,i,s){this.entities=s;var a=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var c=e[r];if("data"==t)if("&"==c)i=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[i]?n+=a.entities[i]:n+=i,["text",n]}if("&"==c)o("Incorrect escape."),n+=i,i=c;else if(/[a-z#0-9]/i.test(c))i+=c;else if(";"==c){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[i+c]?n+=a.entities[i+c]:(e=Object.keys(s).find(e=>i.startsWith(e)))?n+=a.entities[e]+i.slice(e.length)+c:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)i=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)i&&l.push(i),i="",t="start tag annotation";else if("\n"==c)i&&l.push(i),i=c,t="start tag annotation";else if("."==c)i&&l.push(i),i="";else{if(">"==c||void 0==c)return">"==c&&r++,i&&l.push(i),["start tag",n,l,""];i+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["timestamp",n];n+=c}else o("Never happens.");r++}}this.parse=function(t,s){var a={children:[]},c=a,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function d(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])c.children.push({type:"text",value:p[1],parent:c});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==c.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),c.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),c.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==c.name?c=c.parent:"ruby"==p[1]&&"rt"==c.name?c=c.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var g=new n(p[1],o).parseTimestamp();void 0!=g&&((g<=t||g>=s)&&o("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=g&&o("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:g,parent:c}),f.push(g))}}for(;c.parent;)"v"!=c.name&&o("Required end tag missing."),c=c.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},s=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,s=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),s=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+s).padStart(2,"0")+"."+t}function n(e){for(var i="",s=0;s/g,">");else if("object"==a.type){if(i+="<"+a.name,a.classes)for(var r=0;r"}else"timestamp"==a.type?i+="<"+t(a.value)+">":i+="<"+a.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function s(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var a=0;a{if(t.closed)return;const a=URL.createObjectURL(new Blob([s],{type:"text/vtt"})),r=function(e){let n=!1,i=()=>{};const s={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(s.free),s}(a);if(t.closed)return;const o=i;let l,c,f=!1;i=r;try{if(l=e.option.subtitle,c=l.escape,t.closed||i!==r)return;l.escape=!1,f=!0;const s={...l,url:a,type:"vtt",onVttLoad:n};if(t.closed||i!==r)return;const o=e.subtitle.init(s);Promise.resolve(o).catch(e=>{i===r&&(i=null,r.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(u){throw i===r&&(i=o,f&&!1===l.escape&&(l.escape=c)),r.free(),u}finally{o!==i&&o?.free(),r!==i&&r.free()}}}return function({subtitles:e=[]}){return async n=>{const{unescape:i,getExt:r,srtToVtt:o,assToVtt:l}=n.constructor.utils,c=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const s=new Promise(e=>{i=e});function a(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const r={get closed(){return t},own:e=>(t?a(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,s]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)a(t)}};if(!t){r.own(()=>e.off("destroy",r.dispose));try{e.on("destroy",r.dispose)}catch(o){throw r.dispose(),o}}return r}(n),f=a(n,c,i);let u=[];function d(e){c.closed||f(function(e){const t=[];for(const n of e)for(const e of n.cues)t.push({...e,tree:{...e.tree,children:e.tree.children.map(e=>({...e,value:`
${e.value}
`}))}});return(new s).serialize(t)}(e))}const p={name:"multipleSubtitles",tracks(e=[]){c.closed||d(e.map(e=>u.find(t=>t.name===e)))},reset(){d(u)}};try{if(c.closed)return p;const n=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i},s){if(s.closed)return;const a="function"===typeof AbortController?new AbortController:null,r=s.own(()=>a?.abort());try{const r=e.url;if(s.closed)return;const o=await s.wait(a?fetch(r,{signal:a.signal}):fetch(r));if(s.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=await s.wait(o.arrayBuffer());if(s.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw a?.abort(),o}finally{r()}}(e,{getExt:r,srtToVtt:o,assToVtt:l},c)));return c.closed||(u=function(e,n){const i=new t;return e.map((e,t)=>{const s=i.parse(e,"metadata");return s.url=n[t].url,s.name=n[t].name,s})}(n,e),d(u)),p}catch(m){throw c.dispose(),m}}}}); diff --git a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js index 14454c165..8724c0863 100644 --- a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js +++ b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js @@ -55,4 +55,4 @@ * * d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))a.call(t,n)&&r(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&r(e,n,t[n]);return e},l=(e,t,n)=>new Promise((i,a)=>{var s=e=>{try{o(n.next(e))}catch(t){a(t)}},r=e=>{try{o(n.throw(e))}catch(t){a(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,r);o((n=n.apply(e,t)).next())}),f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},c=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),a=0,s=t.split(/\r\n|\r|\n/),r=!1,o=[],l=[],c=[];function p(e,t){c.push({message:e,line:a+1,col:t})}var m=s[a],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(gb&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),a++;""!=s[a]&&void 0!=s[a];){if(p("No blank line after the signature."),-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}for(;void 0!=s[a];){for(var x;!r&&""==s[a];)a++;if(!r&&void 0==s[a])break;x=Object.assign({},f,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==s[a].indexOf("--\x3e")){if(x.id=s[a],/^NOTE($|[ \t])/.test(x.id)){for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),a++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(a++;""!=s[a]&&void 0!=s[a];)-1!=s[a].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(s[a]),a++;if(l.length){p("Style blocks cannot appear after the first cue.");continue}w||o.push(y.join("\n"));continue}if(""==s[++a]||void 0==s[a]){p("Cue identifier cannot be standalone.");continue}if(-1==s[a].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}r=!1;var O=new u(s[a],p),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!T||O.parse(x,P)){for(a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){p("Blank line missing before cue."),r=!0;break}""!=x.text&&(x.text+="\n"),x.text+=s[a],a++}var I=new d(x.text,p,n,e);x.tree=I.parse(x.startTime,x.endTime),l.push(x)}else for(x=null,a++;""!=s[a]&&void 0!=s[a];){if(-1!=s[a].indexOf("--\x3e")){r=!0;break}a++}}return l.sort(function(e,t){return e.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||parseInt(t,10)>59)&&(l="hours"),":"==e[a])if(a++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[a]){if(":"!=e[a])return void s("No seconds found or minutes is greater than 59.");if(a++,2!=(i=o(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[a])if(a++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime"==e[a]){if(a++,i.test(e[a])&&s("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[a]),r(n),function(e,t){for(var i=e.split(n),a=[],r=0;r100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(f)){c=f.split(","),f=c[0];var m=c[1]}if("%"!=f[f.length-1]){s("Text position must be a percentage.");continue}if(parseInt(f,10)>100||parseInt(f,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=f.slice(0,f.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=f[f.length-1]){s("Size must be a percentage.");continue}if(parseInt(f,10)>100){s("Size cannot be >100%.");continue}var g=f.slice(0,f.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(f)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=f}else s("Invalid setting.")}}(e.substring(a),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[a])return t;s("Timestamp must not have trailing characters.")}},d=function(e,t,n,i){this.entities=i;var a=this,s=(e=e,0),r=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var f=e[s];if("data"==t)if("&"==f)o=f,t="escape";else if("<"==f&&""==n)t="tag";else{if("<"==f||void 0==f)return["text",n];n+=f}else if("escape"==t){if("<"==f||void 0==f){let e;return r("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[o]?n+=a.entities[o]:n+=o,["text",n]}if("&"==f)r("Incorrect escape."),n+=o,o=f;else if(/[a-z#0-9]/i.test(f))o+=f;else if(";"==f){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[o+f]?n+=a.entities[o+f]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=a.entities[e]+o.slice(e.length)+f:(r("Incorrect escape."),n+=o+";"),t="data"}else r("Incorrect escape."),n+=o+f,t="data"}else if("tag"==t)if("\t"==f||"\n"==f||"\f"==f||" "==f)t="start tag annotation";else if("."==f)t="start tag class";else if("/"==f)t="end tag";else if(/\d/.test(f))n=f,t="timestamp tag";else{if(">"==f||void 0==f)return">"==f&&s++,["start tag","",[],""];n=f,t="start tag"}else if("start tag"==t)if("\t"==f||"\f"==f||" "==f)t="start tag annotation";else if("\n"==f)o=f,t="start tag annotation";else if("."==f)t="start tag class";else{if(">"==f||void 0==f)return">"==f&&s++,["start tag",n,[],""];n+=f}else if("start tag class"==t)if("\t"==f||"\f"==f||" "==f)o&&l.push(o),o="",t="start tag annotation";else if("\n"==f)o&&l.push(o),o=f,t="start tag annotation";else if("."==f)o&&l.push(o),o="";else{if(">"==f||void 0==f)return">"==f&&s++,o&&l.push(o),["start tag",n,l,""];o+=f}else if("start tag annotation"==t){if(">"==f||void 0==f)return">"==f&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=f}else if("end tag"==t){if(">"==f||void 0==f)return">"==f&&s++,["end tag",n];n+=f}else if("timestamp tag"==t){if(">"==f||void 0==f)return">"==f&&s++,["timestamp",n];n+=f}else r("Never happens.");s++}}this.parse=function(t,i){var a={children:[]},f=a,c=[];function d(e){f.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:f}),f=f.children[f.children.length-1]}function p(e){for(var t=f;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])f.children.push({type:"text",value:m[1],parent:f});else if("start tag"==m[0]){"chapters"==n&&r("Start tags not allowed in chapter title text.");var g=m[1];"v"!=g&&"lang"!=g&&""!=m[3]&&r("Only and can have an annotation."),"c"==g||"i"==g||"b"==g||"u"==g||"ruby"==g||"rt"==g&&"ruby"==f.name?d(m):"v"==g?(p("v")&&r(" cannot be nested inside itself."),d(m),f.value=m[3],m[3]||r(" requires an annotation.")):"lang"==g?(d(m),f.value=m[3]):r("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&r("End tags not allowed in chapter title text."),m[1]==f.name?f=f.parent:"ruby"==m[1]&&"rt"==f.name?f=f.parent.parent:r("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&r("Timestamp not allowed in chapter title text.");var h=new u(m[1],r).parseTimestamp();void 0!=h&&((h<=t||h>=i)&&r("Timestamp must be between start timestamp and end timestamp."),c.length>0&&c[c.length-1]>=h&&r("Timestamp must be greater than any previous timestamp."),f.children.push({type:"timestamp",value:h,parent:f}),c.push(h))}}for(;f.parent;)"v"!=f.name&&r("Required end tag missing."),f=f.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},p=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,a=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),a=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+a).padStart(2,"0")+"."+t}function t(n){for(var i="",a=0;a/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var r=0;r"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var a="WEBVTT\n\n";if(t)for(var s=0;sl(null,null,function*(){const{unescape:a,getExt:s,srtToVtt:r,assToVtt:f}=i.constructor.utils,u=new c,d=new p,m=yield Promise.all(e.map(e=>function(e,t){return l(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i}){const a=yield fetch(e.url),s=yield a.arrayBuffer(),r=new TextDecoder(e.encoding||"utf-8").decode(s);switch(e.type||t(e.url)){case"srt":return n(r);case"ass":return i(r);case"vtt":return r;default:return""}})}(e,{getExt:s,srtToVtt:r,assToVtt:f}))),g=m.map((t,n)=>{const i=u.parse(t,"metadata");return i.url=e[n].url,i.name=e[n].name,i});let h="";function v(e=[]){const s=function(e){const t=(new c).parse("","metadata");for(let n=0;n${n.value}`}}}t.cues.push(...i.cues)}return t}(e),r=d.serialize(s.cues);URL.revokeObjectURL(h);const l=URL.createObjectURL(new Blob([r],{type:"text/vtt"}));var f;h=l,i.option.subtitle.escape=!1,i.subtitle.init((f=o({},i.option.subtitle),t(f,n({url:l,type:"vtt",onVttLoad:a}))))}return v(g),{name:"multipleSubtitles",tracks:(e=[])=>v(e.map(e=>g.find(t=>t.name===e))),reset:()=>v(g)}})}}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())});var f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=function(e){e||(e={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),a=!1,o=[],l=[],c=[];function u(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(gb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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=0;a100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){s("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){s("Size must be a percentage.");continue}if(parseInt(c,10)>100){s("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else s("Invalid setting.")}}(e.substring(r),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[r])return t;s("Timestamp must not have trailing characters.")}},p=function(e,t,n,i){this.entities=i;var r=this,s=(e=e,0),a=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var c=e[s];if("data"==t)if("&"==c)o=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return a("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):r.entities[o]?n+=r.entities[o]:n+=o,["text",n]}if("&"==c)a("Incorrect escape."),n+=o,o=c;else if(/[a-z#0-9]/i.test(c))o+=c;else if(";"==c){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):r.entities[o+c]?n+=r.entities[o+c]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=r.entities[e]+o.slice(e.length)+c:(a("Incorrect escape."),n+=o+";"),t="data"}else a("Incorrect escape."),n+=o+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)o=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)o&&l.push(o),o="",t="start tag annotation";else if("\n"==c)o&&l.push(o),o=c,t="start tag annotation";else if("."==c)o&&l.push(o),o="";else{if(">"==c||void 0==c)return">"==c&&s++,o&&l.push(o),["start tag",n,l,""];o+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["timestamp",n];n+=c}else a("Never happens.");s++}}this.parse=function(t,i){var r={children:[]},c=r,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function p(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])c.children.push({type:"text",value:m[1],parent:c});else if("start tag"==m[0]){"chapters"==n&&a("Start tags not allowed in chapter title text.");var g=m[1];"v"!=g&&"lang"!=g&&""!=m[3]&&a("Only and can have an annotation."),"c"==g||"i"==g||"b"==g||"u"==g||"ruby"==g||"rt"==g&&"ruby"==c.name?u(m):"v"==g?(p("v")&&a(" cannot be nested inside itself."),u(m),c.value=m[3],m[3]||a(" requires an annotation.")):"lang"==g?(u(m),c.value=m[3]):a("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&a("End tags not allowed in chapter title text."),m[1]==c.name?c=c.parent:"ruby"==m[1]&&"rt"==c.name?c=c.parent.parent:a("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&a("Timestamp not allowed in chapter title text.");var h=new d(m[1],a).parseTimestamp();void 0!=h&&((h<=t||h>=i)&&a("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=h&&a("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:h,parent:c}),f.push(h))}}for(;c.parent;)"v"!=c.name&&a("Required end tag missing."),c=c.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(r)}},m=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,r=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),r=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+r).padStart(2,"0")+"."+t}function t(n){for(var i="",r=0;r/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var a=0;a"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var r="WEBVTT\n\n";if(t)for(var s=0;s{if(t.closed)return;const s=URL.createObjectURL(new Blob([r],{type:"text/vtt"})),a=function(e){let n=!1,i=()=>{};const r={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(r.free),r}(s);if(t.closed)return;const c=i;let f,u,d=!1;i=a;try{if(f=e.option.subtitle,u=f.escape,t.closed||i!==a)return;f.escape=!1,d=!0;const r=l(o({},f),{url:s,type:"vtt",onVttLoad:n});if(t.closed||i!==a)return;const c=e.subtitle.init(r);Promise.resolve(c).catch(e=>{i===a&&(i=null,a.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(p){throw i===a&&(i=c,d&&!1===f.escape&&(f.escape=u)),a.free(),p}finally{c!==i&&(null==c||c.free()),a!==i&&a.free()}}}return function({subtitles:e=[]}){return t=>c(null,null,function*(){const{unescape:n,getExt:i,srtToVtt:r,assToVtt:s}=t.constructor.utils,a=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const r=new Promise(e=>{i=e});function s(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const a={get closed(){return t},own:e=>(t?s(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,r]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)s(t)}};if(!t){a.own(()=>e.off("destroy",a.dispose));try{e.on("destroy",a.dispose)}catch(o){throw a.dispose(),o}}return a}(t),f=g(t,a,n);let d=[];function p(e){a.closed||f(function(e){const t=[];for(const n of e)for(const e of n.cues)t.push(l(o({},e),{tree:l(o({},e.tree),{children:e.tree.children.map(e=>l(o({},e),{value:`
${e.value}
`}))})}));return(new m).serialize(t)}(e))}const h={name:"multipleSubtitles",tracks(e=[]){a.closed||p(e.map(e=>d.find(t=>t.name===e)))},reset(){p(d)}};try{if(a.closed)return h;const t=yield Promise.all(e.map(e=>function(e,t,n){return c(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i},r){if(r.closed)return;const s="function"===typeof AbortController?new AbortController:null,a=r.own(()=>null==s?void 0:s.abort());try{const a=e.url;if(r.closed)return;const o=yield r.wait(s?fetch(a,{signal:s.signal}):fetch(a));if(r.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=yield r.wait(o.arrayBuffer());if(r.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw null==s||s.abort(),o}finally{a()}})}(e,{getExt:i,srtToVtt:r,assToVtt:s},a)));return a.closed||(d=function(e,t){const n=new u;return e.map((e,i)=>{const r=n.parse(e,"metadata");return r.url=t[i].url,r.name=t[i].name,r})}(t,e),p(d)),h}catch(v){throw a.dispose(),v}})}}); diff --git a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs index 4befa73d6..fe8eb5a22 100644 --- a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs +++ b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs @@ -55,6 +55,56 @@ * * d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. */ +function createLifetime(art) { + let closed = Boolean(art.isDestroy); + const cleanups = /* @__PURE__ */ new Set(); + let cancel; + const cancelled = new Promise((resolve) => { + cancel = resolve; + }); + function run(cleanup) { + try { + cleanup(); + } catch (error) { + console.warn("Failed to clean up multiple subtitles:", error); + } + } + const lifetime = { + get closed() { + return closed; + }, + own(cleanup) { + if (closed) + run(cleanup); + else + cleanups.add(cleanup); + return () => cleanups.delete(cleanup); + }, + wait(value) { + return Promise.race([value, cancelled]); + }, + dispose() { + if (closed) + return; + closed = true; + cancel(); + const pending = [...cleanups].reverse(); + cleanups.clear(); + for (const cleanup of pending) + run(cleanup); + } + }; + if (!closed) { + lifetime.own(() => art.off("destroy", lifetime.dispose)); + try { + art.on("destroy", lifetime.dispose); + } catch (error) { + lifetime.dispose(); + throw error; + } + } + return lifetime; +} var defaultCueSettings = { direction: "horizontal", snapToLines: true, @@ -799,85 +849,166 @@ var WebVTTSerializer = function() { return result; }; }; -async function loadVtt(option, { getExt, srtToVtt, assToVtt }) { - const response = await fetch(option.url); - const buffer = await response.arrayBuffer(); - const decoder = new TextDecoder(option.encoding || "utf-8"); - const text = decoder.decode(buffer); - switch (option.type || getExt(option.url)) { - case "srt": { - return srtToVtt(text); - } - case "ass": { - return assToVtt(text); - } - case "vtt": { - return text; - } - default: - return ""; - } -} -function mergeTrees(trees) { +function parseTracks(vtts, subtitles) { const parser = new WebVTTParser(); - const result = parser.parse("", "metadata"); - for (let i = 0; i < trees.length; i++) { - const tree = trees[i]; - if (!tree.updated) { - tree.updated = true; - for (let j = 0; j < tree.cues.length; j++) { - const cue = tree.cues[j]; - for (let k = 0; k < cue.tree.children.length; k++) { - const children = cue.tree.children[k]; - children.value = `
${children.value}
`; + return vtts.map((vtt, index) => { + const tree = parser.parse(vtt, "metadata"); + tree.url = subtitles[index].url; + tree.name = subtitles[index].name; + return tree; + }); +} +function serializeTracks(trees) { + const cues = []; + for (const tree of trees) { + for (const cue of tree.cues) { + cues.push({ + ...cue, + tree: { + ...cue.tree, + children: cue.tree.children.map((child) => ({ + ...child, + value: `
${child.value}
` + })) } - } + }); } - result.cues.push(...tree.cues); } - return result; + return new WebVTTSerializer().serialize(cues); +} +function createRenderer(art, lifetime, unescape) { + let current = null; + function own(url) { + let freed = false; + let release = () => { + }; + const entry = { + free() { + if (freed) + return; + freed = true; + release(); + URL.revokeObjectURL(url); + } + }; + release = lifetime.own(entry.free); + return entry; + } + return (vtt) => { + if (lifetime.closed) + return; + const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" })); + const entry = own(url); + if (lifetime.closed) + return; + const previous = current; + let option; + let escape; + let configured = false; + current = entry; + try { + option = art.option.subtitle; + escape = option.escape; + if (lifetime.closed || current !== entry) + return; + option.escape = false; + configured = true; + const config = { ...option, url, type: "vtt", onVttLoad: unescape }; + if (lifetime.closed || current !== entry) + return; + const pending = art.subtitle.init(config); + Promise.resolve(pending).catch((error) => { + if (current === entry) { + current = null; + entry.free(); + } + if (!lifetime.closed) + console.warn("Failed to initialize multiple subtitles:", error); + }); + } catch (error) { + if (current === entry) { + current = previous; + if (configured && option.escape === false) + option.escape = escape; + } + entry.free(); + throw error; + } finally { + if (previous !== current) + previous?.free(); + if (entry !== current) + entry.free(); + } + }; +} +async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) { + if (lifetime.closed) + return; + const controller = typeof AbortController === "function" ? new AbortController() : null; + const release = lifetime.own(() => controller?.abort()); + try { + const url = option.url; + if (lifetime.closed) + return; + const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url)); + if (lifetime.closed) + return; + if (response.ok === false) + throw new Error(`Failed to fetch multiple subtitles: HTTP ${response.status}`); + const buffer = await lifetime.wait(response.arrayBuffer()); + if (lifetime.closed) + return; + const text = new TextDecoder(option.encoding || "utf-8").decode(buffer); + switch (option.type || getExt(option.url)) { + case "srt": + return srtToVtt(text); + case "ass": + return assToVtt(text); + case "vtt": + return text; + default: + return ""; + } + } catch (error) { + controller?.abort(); + throw error; + } finally { + release(); + } } function artplayerPluginMultipleSubtitles({ subtitles = [] }) { return async (art) => { const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils; - const parser = new WebVTTParser(); - const seri = new WebVTTSerializer(); - const vtts = await Promise.all( - subtitles.map((option) => { - return loadVtt(option, { getExt, srtToVtt, assToVtt }); - }) - ); - const trees = vtts.map((vtt, index) => { - const tree = parser.parse(vtt, "metadata"); - tree.url = subtitles[index].url; - tree.name = subtitles[index].name; - return tree; - }); - let lastUrl = ""; - function setTracks(trees2 = []) { - const tree = mergeTrees(trees2); - const vtt = seri.serialize(tree.cues); - URL.revokeObjectURL(lastUrl); - const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" })); - lastUrl = url; - art.option.subtitle.escape = false; - art.subtitle.init({ - ...art.option.subtitle, - url, - type: "vtt", - onVttLoad: unescape - }); + const lifetime = createLifetime(art); + const render = createRenderer(art, lifetime, unescape); + let trees = []; + function setTracks(selected) { + if (!lifetime.closed) + render(serializeTracks(selected)); } - setTracks(trees); - return { + const result = { name: "multipleSubtitles", tracks(names = []) { - return setTracks(names.map((name) => trees.find((tree) => tree.name === name))); + if (!lifetime.closed) + setTracks(names.map((name) => trees.find((tree) => tree.name === name))); }, reset() { - return setTracks(trees); + setTracks(trees); } }; + try { + if (lifetime.closed) + return result; + const vtts = await Promise.all(subtitles.map((option) => loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime))); + if (lifetime.closed) + return result; + trees = parseTracks(vtts, subtitles); + setTracks(trees); + return result; + } catch (error) { + lifetime.dispose(); + throw error; + } }; } export { diff --git a/packages/artplayer-plugin-multiple-subtitles/src/index.js b/packages/artplayer-plugin-multiple-subtitles/src/index.js index 2181492fd..7fca6c715 100644 --- a/packages/artplayer-plugin-multiple-subtitles/src/index.js +++ b/packages/artplayer-plugin-multiple-subtitles/src/index.js @@ -1,95 +1,41 @@ -import { WebVTTParser, WebVTTSerializer } from './parser' - -async function loadVtt(option, { getExt, srtToVtt, assToVtt }) { - const response = await fetch(option.url) - const buffer = await response.arrayBuffer() - const decoder = new TextDecoder(option.encoding || 'utf-8') - const text = decoder.decode(buffer) - switch (option.type || getExt(option.url)) { - case 'srt': { - return srtToVtt(text) - } - case 'ass': { - return assToVtt(text) - } - case 'vtt': { - return text - } - default: - return '' - } -} - -function mergeTrees(trees) { - const parser = new WebVTTParser() - const result = parser.parse('', 'metadata') - - for (let i = 0; i < trees.length; i++) { - const tree = trees[i] - - if (!tree.updated) { - tree.updated = true - for (let j = 0; j < tree.cues.length; j++) { - const cue = tree.cues[j] - for (let k = 0; k < cue.tree.children.length; k++) { - const children = cue.tree.children[k] - children.value = `
${children.value}
` - } - } - } - - result.cues.push(...tree.cues) - } - - return result -} +import createLifetime from './lifetime' +import { parseTracks, serializeTracks } from './merge' +import createRenderer from './render' +import loadVtt from './request' export default function artplayerPluginMultipleSubtitles({ subtitles = [] }) { return async (art) => { const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils - - const parser = new WebVTTParser() - const seri = new WebVTTSerializer() - - const vtts = await Promise.all( - subtitles.map((option) => { - return loadVtt(option, { getExt, srtToVtt, assToVtt }) - }), - ) - - const trees = vtts.map((vtt, index) => { - const tree = parser.parse(vtt, 'metadata') - tree.url = subtitles[index].url - tree.name = subtitles[index].name - return tree - }) - - let lastUrl = '' - function setTracks(trees = []) { - const tree = mergeTrees(trees) - const vtt = seri.serialize(tree.cues) - URL.revokeObjectURL(lastUrl) - const url = URL.createObjectURL(new Blob([vtt], { type: 'text/vtt' })) - lastUrl = url - art.option.subtitle.escape = false - art.subtitle.init({ - ...art.option.subtitle, - url, - type: 'vtt', - onVttLoad: unescape, - }) + const lifetime = createLifetime(art) + const render = createRenderer(art, lifetime, unescape) + let trees = [] + function setTracks(selected) { + if (!lifetime.closed) + render(serializeTracks(selected)) } - - setTracks(trees) - - return { + const result = { name: 'multipleSubtitles', tracks(names = []) { - return setTracks(names.map(name => trees.find(tree => tree.name === name))) + if (!lifetime.closed) + setTracks(names.map(name => trees.find(tree => tree.name === name))) }, reset() { - return setTracks(trees) + setTracks(trees) }, } + try { + if (lifetime.closed) + return result + const vtts = await Promise.all(subtitles.map(option => loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime))) + if (lifetime.closed) + return result + trees = parseTracks(vtts, subtitles) + setTracks(trees) + return result + } + catch (error) { + lifetime.dispose() + throw error + } } } diff --git a/packages/artplayer-plugin-multiple-subtitles/src/lifetime.js b/packages/artplayer-plugin-multiple-subtitles/src/lifetime.js new file mode 100644 index 000000000..232120f70 --- /dev/null +++ b/packages/artplayer-plugin-multiple-subtitles/src/lifetime.js @@ -0,0 +1,48 @@ +export default function createLifetime(art) { + let closed = Boolean(art.isDestroy) + const cleanups = new Set() + let cancel + const cancelled = new Promise((resolve) => { + cancel = resolve + }) + function run(cleanup) { + try { + cleanup() + } + catch (error) { + console.warn('Failed to clean up multiple subtitles:', error) + } + } + const lifetime = { + get closed() { return closed }, + own(cleanup) { + if (closed) + run(cleanup) + else + cleanups.add(cleanup) + return () => cleanups.delete(cleanup) + }, + wait(value) { return Promise.race([value, cancelled]) }, + dispose() { + if (closed) + return + closed = true + cancel() + const pending = [...cleanups].reverse() + cleanups.clear() + for (const cleanup of pending) + run(cleanup) + }, + } + if (!closed) { + lifetime.own(() => art.off('destroy', lifetime.dispose)) + try { + art.on('destroy', lifetime.dispose) + } + catch (error) { + lifetime.dispose() + throw error + } + } + return lifetime +} diff --git a/packages/artplayer-plugin-multiple-subtitles/src/merge.js b/packages/artplayer-plugin-multiple-subtitles/src/merge.js new file mode 100644 index 000000000..2d90773a1 --- /dev/null +++ b/packages/artplayer-plugin-multiple-subtitles/src/merge.js @@ -0,0 +1,30 @@ +import { WebVTTParser, WebVTTSerializer } from './parser' + +export function parseTracks(vtts, subtitles) { + const parser = new WebVTTParser() + return vtts.map((vtt, index) => { + const tree = parser.parse(vtt, 'metadata') + tree.url = subtitles[index].url + tree.name = subtitles[index].name + return tree + }) +} + +export function serializeTracks(trees) { + const cues = [] + for (const tree of trees) { + for (const cue of tree.cues) { + cues.push({ + ...cue, + tree: { + ...cue.tree, + children: cue.tree.children.map(child => ({ + ...child, + value: `
${child.value}
`, + })), + }, + }) + } + } + return new WebVTTSerializer().serialize(cues) +} diff --git a/packages/artplayer-plugin-multiple-subtitles/src/render.js b/packages/artplayer-plugin-multiple-subtitles/src/render.js new file mode 100644 index 000000000..7ed0c8402 --- /dev/null +++ b/packages/artplayer-plugin-multiple-subtitles/src/render.js @@ -0,0 +1,68 @@ +export default function createRenderer(art, lifetime, unescape) { + let current = null + function own(url) { + let freed = false + let release = () => {} + const entry = { + free() { + if (freed) + return + freed = true + release() + URL.revokeObjectURL(url) + }, + } + release = lifetime.own(entry.free) + return entry + } + return (vtt) => { + if (lifetime.closed) + return + // Allocate before replacing the active URL so allocation failure preserves it. + const url = URL.createObjectURL(new Blob([vtt], { type: 'text/vtt' })) + const entry = own(url) + if (lifetime.closed) + return + const previous = current + let option + let escape + let configured = false + current = entry + try { + option = art.option.subtitle + escape = option.escape + if (lifetime.closed || current !== entry) + return + option.escape = false + configured = true + const config = { ...option, url, type: 'vtt', onVttLoad: unescape } + if (lifetime.closed || current !== entry) + return + const pending = art.subtitle.init(config) + // Selection stays void and registration does not await the host's media loading. + Promise.resolve(pending).catch((error) => { + if (current === entry) { + current = null + entry.free() + } + if (!lifetime.closed) + console.warn('Failed to initialize multiple subtitles:', error) + }) + } + catch (error) { + if (current === entry) { + current = previous + if (configured && option.escape === false) + option.escape = escape + } + entry.free() + throw error + } + finally { + if (previous !== current) + previous?.free() + if (entry !== current) + entry.free() + } + } +} diff --git a/packages/artplayer-plugin-multiple-subtitles/src/request.js b/packages/artplayer-plugin-multiple-subtitles/src/request.js new file mode 100644 index 000000000..670ab54cc --- /dev/null +++ b/packages/artplayer-plugin-multiple-subtitles/src/request.js @@ -0,0 +1,33 @@ +export default async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) { + if (lifetime.closed) + return + const controller = typeof AbortController === 'function' ? new AbortController() : null + const release = lifetime.own(() => controller?.abort()) + try { + const url = option.url + if (lifetime.closed) + return + const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url)) + if (lifetime.closed) + return + if (response.ok === false) + throw new Error(`Failed to fetch multiple subtitles: HTTP ${response.status}`) + const buffer = await lifetime.wait(response.arrayBuffer()) + if (lifetime.closed) + return + const text = new TextDecoder(option.encoding || 'utf-8').decode(buffer) + switch (option.type || getExt(option.url)) { + case 'srt': return srtToVtt(text) + case 'ass': return assToVtt(text) + case 'vtt': return text + default: return '' + } + } + catch (error) { + controller?.abort() + throw error + } + finally { + release() + } +} diff --git a/refactor/baselines/multiple-subtitles-resources.json b/refactor/baselines/multiple-subtitles-resources.json new file mode 100644 index 000000000..15f2fbe88 --- /dev/null +++ b/refactor/baselines/multiple-subtitles-resources.json @@ -0,0 +1,6559 @@ +{ + "schemaVersion": 1, + "task": "PKG-MULTI-SUB-03", + "status": "done", + "parentCommit": "36cd15109586fd2edb848f359a4578ab4a39908d", + "checks": [ + { + "command": "node --test test/multiple-subtitles.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-merge.test.js", + "candidate": "source", + "tests": 31, + "pass": 31, + "file": "refactor/.cache/multiple-subtitles03-candidate-source.log", + "sha256": "4dfef51b7b92497e0b585f26f68a4d4f299dba22e610e9879fd524336c043d76" + }, + { + "command": "node --test test/multiple-subtitles.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-merge.test.js", + "candidate": "main", + "tests": 31, + "pass": 31, + "file": "refactor/.cache/multiple-subtitles03-candidate-main.log", + "sha256": "8557833413a71e0a4aeb4165dba2c2c87ee8395bf69bf24d9d221c1e63130f2f" + }, + { + "command": "node --test test/multiple-subtitles.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-merge.test.js", + "candidate": "legacy", + "tests": 31, + "pass": 31, + "file": "refactor/.cache/multiple-subtitles03-candidate-legacy.log", + "sha256": "5418f61f7859e5e1f9e729927c2b1c3146605b46c6778f92e4620ad4cb153e40" + }, + { + "command": "yarn build artplayer-plugin-multiple-subtitles", + "file": "refactor/.cache/multiple-subtitles03-build.log", + "sha256": "15040a618b551a19a689fa555ce6d004c4809f1b29af2441a3e86604f163498a" + }, + { + "command": "yarn test:multiple-subtitles", + "file": "refactor/.cache/multiple-subtitles03-package.log", + "sha256": "a418fa3ea1ec4f76d5e81a7741ae0792869b953941dbd5c8330b7738467c6041", + "tests": 241, + "pass": 241, + "fail": 0, + "composition": "215 historical normal/failure/vendor/contract + 23 candidate lifecycle + 3 candidate merge/decoder tests" + }, + { + "command": "Explicit source, helper, candidate and native test lint", + "file": "refactor/.cache/multiple-subtitles03-lint.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "command": "Actual ESM import", + "file": "refactor/.cache/multiple-subtitles03-esm.log", + "sha256": "229250f42b8224bb8593c2b7ad776b81c7538853cec0b6853b35c2cdaacdaa62", + "scope": "Default-only module shape, named factory and registrar; not a full installed consumer matrix." + }, + { + "command": "yarn ci:check", + "file": "refactor/.cache/multiple-subtitles03-ci.log", + "sha256": "cac0a774071c40d4c85f3f03124d6ebd3114155e414f2950dd7d0b3014c93d67", + "tests": { + "unit": 1963, + "engineering": 14, + "baseline": 334, + "total": 2311 + }, + "strictProductionTypeScriptFiles": 346, + "warnings": [ + "Existing generated docs/assets/ts/artplayer.d.ts unused ts/no-namespace suppression.", + "Yarn Classic DEP0169 url.parse deprecation." + ] + } + ], + "manifests": { + "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015": { + "kind": "artplayer-browser-test", + "resources": { + "/candidate/artplayer.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073 + }, + "/compiled/artplayer.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073, + "aliasOf": "/candidate/artplayer.js" + }, + "/uncompiled/artplayer/index.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.ts" + }, + "sha256": "e8945bc9d3ff0a6e247772a662f874df13875f7dbeb949968943b9589f6e2946", + "bytes": 378073, + "aliasOf": "/candidate/artplayer.js" + }, + "/candidate/artplayer-plugin-chapter.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts" + }, + "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a", + "bytes": 8529 + }, + "/compiled/artplayer-plugin-chapter.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts" + }, + "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a", + "bytes": 8529, + "aliasOf": "/candidate/artplayer-plugin-chapter.js" + }, + "/uncompiled/artplayer-plugin-chapter/index.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts" + }, + "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a", + "bytes": 8529, + "aliasOf": "/candidate/artplayer-plugin-chapter.js" + }, + "/published/artplayer.js": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "/published/artplayer-plugin-chapter.js": { + "source": { + "kind": "npm-release", + "version": "1.1.0", + "integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==", + "member": "package/dist/artplayer-plugin-chapter.js" + }, + "sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0", + "bytes": 4973 + }, + "/test/pattern.mp4": { + "source": { + "kind": "generated-media", + "file": "test/browser/media/pattern.mp4" + }, + "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233", + "bytes": 251538 + }, + "/test/pattern.webm": { + "source": { + "kind": "generated-media", + "file": "test/browser/media/pattern.webm" + }, + "sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f", + "bytes": 162073 + }, + "/published-4.5.5/artplayer.js": { + "source": { + "kind": "npm-release", + "version": "4.5.5", + "integrity": "sha512-HeAKbe3od9ZgFQVSBCg5FlixJsefDEBC8txVG19yZgvzlcJuiurxPlasap+GHuex/ftG+jmSXnTRtuNrTki2Kw==", + "member": "package/dist/artplayer.js" + }, + "sha256": "3467e626ca29d2f7884e75eb75b8b90de74a25761ffa2e272576c9b5a98fa0a4", + "bytes": 153582 + }, + "/published-5.1.7/artplayer.js": { + "source": { + "kind": "npm-release", + "version": "5.1.7", + "integrity": "sha512-N/QdNlTwsIHUsWOHMPj0pq97v3qmgXZHfhy1q7I+8YN3/XakJiJJyVJgGaeBjU+y/fUIrzBhwpBaX+NFwK9/qw==", + "member": "package/dist/artplayer.js" + }, + "sha256": "484ccc3f22446ec6fd0dd8b8a5602c740b601d55097a1ae043262d92aab35357", + "bytes": 155940 + }, + "/published-4.5.9/artplayer.js": { + "source": { + "kind": "npm-release", + "version": "4.5.9", + "integrity": "sha512-x5JzGdXkw4GFdztvPkVnm8TOWrGDcvGKKMPb9wuP6UfyftlLM3UQctmmalfAMnQSxugJwq2Kb9ix9OG2SgbJJA==", + "member": "package/dist/artplayer.js" + }, + "sha256": "09a9f09e3a64a02ca7c27af710d7254f886f0bfb09fd54bf3a728ce863e5a497", + "bytes": 156408 + }, + "/test/audio-tone.m4a": { + "source": { + "kind": "generated-media", + "file": "test/browser/media/audio-tone.m4a" + }, + "sha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "bytes": 132436 + }, + "/test/thumbnail-pattern.mp4": { + "source": { + "kind": "generated-media", + "file": "test/browser/media/thumbnail-pattern.mp4" + }, + "sha256": "4c977be04d97f6ac1342ac4959b8e61678f390d88a088430e4d0200137a03d1c", + "bytes": 752916 + }, + "/test/auto-thumbnail-timeline.mp4": { + "source": { + "kind": "generated-media", + "file": "test/browser/media/auto-thumbnail-timeline.mp4" + }, + "sha256": "10370156cffada462d3ea00aae0caadd62752369cd6538cc55248b3ddd7bf994", + "bytes": 8602 + }, + "/test/declaration-cues.vtt": { + "source": { + "kind": "test-subtitles", + "file": "test/browser/media/declaration-cues.vtt" + }, + "sha256": "5d149d4e641ce41cbb83376835c03daf1fb89f3b9f568fa59e064d51e2b57704", + "bytes": 54 + }, + "/test/thumbnail-grid.svg": { + "source": { + "kind": "test-thumbnail-grid", + "file": "test/browser/media/thumbnail-grid.svg" + }, + "sha256": "bb3f1ec9895d37def77ca4b192d3cd73577a2668558cc99de1588f511d65d3e6", + "bytes": 14969 + }, + "/test/legacy-safe-area.js": { + "source": { + "kind": "frozen-own-source", + "file": "test/helpers/legacy-safe-area.js", + "commit": "ccf77c4e" + }, + "sha256": "ce0368618ef6363b78271b8bcc7973abd0478e797cc464ddd912929e6dfa501a", + "bytes": 795 + }, + "/test/iframe-boundary-child.html": { + "source": { + "kind": "iframe-fixture", + "file": "test/browser/iframe-boundary-child.html" + }, + "sha256": "4721de185f606de7d576e71395127257e476765455e2a947ded79df4ebed901a", + "bytes": 1451 + }, + "/assets/sample/video.mp4": { + "source": { + "kind": "docs-media", + "file": "docs/assets/sample/video.mp4" + }, + "sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980", + "bytes": 5818706 + }, + "/test/performance.js": { + "source": { + "kind": "adapted-baseline-fixture", + "file": "refactor/fixtures/performance.js" + }, + "sha256": "2318f39bdb5f3a3348d755cb4335907ae4adc944be3a435839c1bba201e81500", + "bytes": 8994 + } + } + } + }, + "browser": [ + { + "command": "yarn test:browser test/browser/multiple-subtitles-lifecycle.spec.js", + "candidate": "source", + "file": "refactor/.cache/multiple-subtitles03-browser-source-verified.json", + "sha256": "d0b4ff72c750e3ddedd9321e3a48edf33d53340ee27177471eba9fb1b1371df6", + "resultsDirectory": "refactor/.cache/multiple-subtitles03-browser-source-verified-results", + "stats": { + "startTime": "2026-09-13T02:52:39.043Z", + "duration": 19845.509000000002, + "expected": 30, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/7d400bc4-61da-4163-a3ba-8e5f2297401e", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/c078a4bf-04c0-48f5-9408-b617ea4b5005", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/14c8b98e-b76e-410d-b2b3-1aa8fc551020", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/b2bb985f-1acc-47d1-88ae-69142d10be1a", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/7d400bc4-61da-4163-a3ba-8e5f2297401e", + "blob:http://127.0.0.1:8084/86160f8a-5d53-461b-9a97-c600d4106aa3", + "blob:http://127.0.0.1:8084/c078a4bf-04c0-48f5-9408-b617ea4b5005", + "blob:http://127.0.0.1:8084/c91248a1-dee5-4bde-b534-1e7a624001cc", + "blob:http://127.0.0.1:8084/14c8b98e-b76e-410d-b2b3-1aa8fc551020", + "blob:http://127.0.0.1:8084/9f1e1c2e-229e-47ac-a0a6-4f30d00330a5", + "blob:http://127.0.0.1:8084/b2bb985f-1acc-47d1-88ae-69142d10be1a" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: net::ERR_FILE_NOT_FOUND" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "blob:http://127.0.0.1:8084/08e82e04-9eea-4e2e-b910-3050b5274064", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_FILE_NOT_FOUND" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/08e82e04-9eea-4e2e-b910-3050b5274064", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/08e82e04-9eea-4e2e-b910-3050b5274064" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-error.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/783dcc74-d136-43c0-bf4f-f4566117baf6", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/64857f66-fb38-4136-9123-08c3263f1c27", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/783dcc74-d136-43c0-bf4f-f4566117baf6", + "blob:http://127.0.0.1:8084/6c61532c-e52e-42e6-bcad-a268d249bd23", + "blob:http://127.0.0.1:8084/64857f66-fb38-4136-9123-08c3263f1c27" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/784e8be8-ab9a-4e0f-b719-41c00bccf804", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/0a12087d-15ff-4a0c-a71e-b18ab154fdef", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/33173fc2-1440-47a8-b9cb-46d9514bb0c4", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/5fb0fc4a-ba50-497d-a3ab-1e3666d1bf21", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/784e8be8-ab9a-4e0f-b719-41c00bccf804", + "blob:http://127.0.0.1:8084/c5bd7e60-fe55-4e80-8d96-19e46c6fcfd6", + "blob:http://127.0.0.1:8084/0a12087d-15ff-4a0c-a71e-b18ab154fdef", + "blob:http://127.0.0.1:8084/2ad1126e-562e-4698-a706-4c01bda0e053", + "blob:http://127.0.0.1:8084/33173fc2-1440-47a8-b9cb-46d9514bb0c4", + "blob:http://127.0.0.1:8084/36bea7d4-03a8-4350-9e12-c5f50d23f268", + "blob:http://127.0.0.1:8084/232ad718-0adf-4a71-9aba-25ff8d5bad48", + "blob:http://127.0.0.1:8084/5fb0fc4a-ba50-497d-a3ab-1e3666d1bf21" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: net::ERR_FILE_NOT_FOUND" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "blob:http://127.0.0.1:8084/84b0a72c-0da4-4553-8f61-0200d3105bc9", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_FILE_NOT_FOUND" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/84b0a72c-0da4-4553-8f61-0200d3105bc9", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/7c57a37e-4579-48e0-8f00-792afae1d4c6", + "blob:http://127.0.0.1:8084/84b0a72c-0da4-4553-8f61-0200d3105bc9" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-error.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/5aaf31d6-41bf-4836-b37d-4664820154ef", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/616790b0-8d71-4ff3-a198-507c066677a7", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/5aaf31d6-41bf-4836-b37d-4664820154ef", + "blob:http://127.0.0.1:8084/987d76ab-4e0c-4939-bad4-5a53211adc51", + "blob:http://127.0.0.1:8084/96efab59-2a1b-4787-8c95-5a4cc220cf1d", + "blob:http://127.0.0.1:8084/616790b0-8d71-4ff3-a198-507c066677a7" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/099b5300-4885-4b5b-abad-ba2625e580f7", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/fcc7b5d2-abc5-474a-8575-1fd75a077666", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/0fa67302-4a41-4246-a92d-032ba7d21258", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/8d34dbca-1e1a-4763-a983-476b63aeac39", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/099b5300-4885-4b5b-abad-ba2625e580f7", + "blob:http://127.0.0.1:8084/b82687e7-abbf-4774-928c-db7c6083a5bc", + "blob:http://127.0.0.1:8084/fcc7b5d2-abc5-474a-8575-1fd75a077666", + "blob:http://127.0.0.1:8084/b20b0e0a-165c-4460-a619-f7f0a027f54d", + "blob:http://127.0.0.1:8084/0fa67302-4a41-4246-a92d-032ba7d21258", + "blob:http://127.0.0.1:8084/40b175f9-548e-48e0-b60e-fad028f5e6f2", + "blob:http://127.0.0.1:8084/8d34dbca-1e1a-4763-a983-476b63aeac39" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/bb1f2063-b307-4249-a1e5-047aa0776daf", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/bb1f2063-b307-4249-a1e5-047aa0776daf" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/f8295be3-080e-442e-9b35-5d5485bbba91", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/fcc6de86-bf04-4f8e-a2fd-803ce0679d86", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/f8295be3-080e-442e-9b35-5d5485bbba91", + "blob:http://127.0.0.1:8084/de5c0197-7e3d-4524-96af-a13867346eb8", + "blob:http://127.0.0.1:8084/fcc6de86-bf04-4f8e-a2fd-803ce0679d86" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/2e145d4b-70f6-4a0b-b516-539b4b3187d1", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/554b54d5-5e47-4fc5-bca2-7c3bd1bd05c9", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/1713265b-bc5c-440d-a61f-ce53f886c7fb", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/701247bd-887e-4614-a28d-ed87378330f5", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/2e145d4b-70f6-4a0b-b516-539b4b3187d1", + "blob:http://127.0.0.1:8084/324fe9dc-792b-42c8-ae0a-f57a65764fa3", + "blob:http://127.0.0.1:8084/554b54d5-5e47-4fc5-bca2-7c3bd1bd05c9", + "blob:http://127.0.0.1:8084/e41cdf9b-a92e-451b-a07c-2b6a320335b6", + "blob:http://127.0.0.1:8084/1713265b-bc5c-440d-a61f-ce53f886c7fb", + "blob:http://127.0.0.1:8084/230fe499-f532-4ada-b271-cec9abe4cfd6", + "blob:http://127.0.0.1:8084/b5cf7cc2-750c-4bb9-a4bc-e322b3404de0", + "blob:http://127.0.0.1:8084/701247bd-887e-4614-a28d-ed87378330f5" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/3519f228-8f3e-4323-98eb-ded2b186a9be", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/3c32ec93-4797-4ff1-840f-377591fa894d", + "blob:http://127.0.0.1:8084/3519f228-8f3e-4323-98eb-ded2b186a9be" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/de1e4240-fa4d-403a-a159-b7ee227b0f57", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/6859e402-8151-41f7-b125-4f6b518199d9", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/de1e4240-fa4d-403a-a159-b7ee227b0f57", + "blob:http://127.0.0.1:8084/4db6276d-35be-47bb-ab57-f3cfca2e8f89", + "blob:http://127.0.0.1:8084/c44ab2b2-af32-4407-b9b3-8b1e3756d3e6", + "blob:http://127.0.0.1:8084/6859e402-8151-41f7-b125-4f6b518199d9" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/89904031-1e30-48e7-8c0a-a1e6569b3d77", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/13258ae9-31d8-48fd-b0f6-9062f84dd9d7", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/bbcf85ca-5d36-45c0-89d8-fd2237c7e79d", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/86ceddd5-7a09-4198-8db4-864fb18cd702", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/89904031-1e30-48e7-8c0a-a1e6569b3d77", + "blob:http://127.0.0.1:8084/8bbd1a24-9701-48e9-ac8a-aeba69daf933", + "blob:http://127.0.0.1:8084/13258ae9-31d8-48fd-b0f6-9062f84dd9d7", + "blob:http://127.0.0.1:8084/e6af7a77-04d4-4694-9924-d56ff27006b3", + "blob:http://127.0.0.1:8084/bbcf85ca-5d36-45c0-89d8-fd2237c7e79d", + "blob:http://127.0.0.1:8084/f74af89b-4a63-4d96-a043-fc1cc5a4c1cf", + "blob:http://127.0.0.1:8084/86ceddd5-7a09-4198-8db4-864fb18cd702" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/36c55c05-4e65-4348-886a-7160e9b3dc15", + "resourceType": "fetch", + "failure": null + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/36c55c05-4e65-4348-886a-7160e9b3dc15", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/36c55c05-4e65-4348-886a-7160e9b3dc15" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/e67e32fa-ecff-491c-bbde-f62bf88b41a7", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/bce86e88-c13a-44d0-8d16-1c5f36f31e36", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/e67e32fa-ecff-491c-bbde-f62bf88b41a7", + "blob:http://127.0.0.1:8084/17d9c364-1545-4d60-be5b-d8a3165421be", + "blob:http://127.0.0.1:8084/bce86e88-c13a-44d0-8d16-1c5f36f31e36" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/3e2e7cba-0d57-477c-b407-bcc0e39c5fc8", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/91e3d4f3-284c-4117-9c55-be9ad53f6b7b", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/c3aabd5e-7a3c-4f8b-bbe8-d716131b4990", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/d37ebfff-b9b9-4513-9a5f-24ed3d488f30", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/3e2e7cba-0d57-477c-b407-bcc0e39c5fc8", + "blob:http://127.0.0.1:8084/19f9efcc-7e0b-4536-8bf2-9809f040f226", + "blob:http://127.0.0.1:8084/91e3d4f3-284c-4117-9c55-be9ad53f6b7b", + "blob:http://127.0.0.1:8084/6e789b9d-d16b-4ff2-a60d-f2bb2ffdec35", + "blob:http://127.0.0.1:8084/c3aabd5e-7a3c-4f8b-bbe8-d716131b4990", + "blob:http://127.0.0.1:8084/0e7d8dd8-de6c-4bc5-bbb6-080d5938ae65", + "blob:http://127.0.0.1:8084/d4e2140e-115c-4261-b956-b58cec7dc918", + "blob:http://127.0.0.1:8084/d37ebfff-b9b9-4513-9a5f-24ed3d488f30" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/9dd1cf24-30e6-4974-bd16-2ed43b353c5e", + "resourceType": "fetch", + "failure": null + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/9dd1cf24-30e6-4974-bd16-2ed43b353c5e", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/90087a1e-2315-4eb3-a9ae-be5d36ee15f1", + "blob:http://127.0.0.1:8084/9dd1cf24-30e6-4974-bd16-2ed43b353c5e" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "65fad17077a45caa5111c0f3cc26de60990acff60bf9bccc30552eb952d9e23a" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/8b6748cd-d4ef-4f42-b975-e4b24e5aaa83", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/0a4711b5-4cf6-4f6e-a4fc-f8e2a1a06365", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/8b6748cd-d4ef-4f42-b975-e4b24e5aaa83", + "blob:http://127.0.0.1:8084/f4ec4757-ecc0-4b74-805c-15a3b3cc24db", + "blob:http://127.0.0.1:8084/782d4b26-ce72-4cf0-bbdc-4cff26e3b956", + "blob:http://127.0.0.1:8084/0a4711b5-4cf6-4f6e-a4fc-f8e2a1a06365" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + } + ] + }, + { + "command": "yarn test:browser test/browser/multiple-subtitles-lifecycle.spec.js", + "candidate": "main", + "file": "refactor/.cache/multiple-subtitles03-browser-main-verified.json", + "sha256": "9b7b265cedbd27750e4a034e5c47c07e8e8baf5396a02bef9b2aece2ee26f8a3", + "resultsDirectory": "refactor/.cache/multiple-subtitles03-browser-main-verified-results", + "stats": { + "startTime": "2026-09-13T02:52:59.584Z", + "duration": 18232.763, + "expected": 30, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/9e5fd45f-5c3c-4d0b-82bc-6c968e0894b0", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/8f00a8cc-d1c3-4489-bab6-29bb4ecb12bc", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/2cd19df8-eae4-497c-b61b-aa4ce439faf1", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/ae4276f5-5bca-408b-9f52-16b324d0e196", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/9e5fd45f-5c3c-4d0b-82bc-6c968e0894b0", + "blob:http://127.0.0.1:8084/3c480ca6-74d9-4c4b-a770-48f4c9d9dc7e", + "blob:http://127.0.0.1:8084/8f00a8cc-d1c3-4489-bab6-29bb4ecb12bc", + "blob:http://127.0.0.1:8084/ac0efd84-7945-4019-8565-42b4ce520fb8", + "blob:http://127.0.0.1:8084/2cd19df8-eae4-497c-b61b-aa4ce439faf1", + "blob:http://127.0.0.1:8084/e4b090ba-401b-442c-81c1-7bd77ba5332c", + "blob:http://127.0.0.1:8084/ae4276f5-5bca-408b-9f52-16b324d0e196" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: net::ERR_FILE_NOT_FOUND" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "blob:http://127.0.0.1:8084/749c0a95-62e8-408a-baec-780cf430b82f", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_FILE_NOT_FOUND" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/749c0a95-62e8-408a-baec-780cf430b82f", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/749c0a95-62e8-408a-baec-780cf430b82f" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-error.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/c84a1fa1-192f-4572-b434-ed1b2cd00e18", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/5cbd355d-23d8-4dbb-9d58-f59c864ecc4d", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/c84a1fa1-192f-4572-b434-ed1b2cd00e18", + "blob:http://127.0.0.1:8084/6e5669de-37cc-45d8-901d-8428ba974ef6", + "blob:http://127.0.0.1:8084/5cbd355d-23d8-4dbb-9d58-f59c864ecc4d" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/8a542b75-b674-4a4e-8598-9abb95d87e45", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/48a8e5da-b6f1-4245-bebd-6076285fedc5", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/aa206858-e186-4944-bac1-9db6e24b1880", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/ec555855-0816-4894-b9c2-0daf8dc93bcd", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/8a542b75-b674-4a4e-8598-9abb95d87e45", + "blob:http://127.0.0.1:8084/094acde6-2dac-455e-8c7b-0068edcaa8ca", + "blob:http://127.0.0.1:8084/48a8e5da-b6f1-4245-bebd-6076285fedc5", + "blob:http://127.0.0.1:8084/04078ca4-8190-4b36-8312-8329e6075065", + "blob:http://127.0.0.1:8084/aa206858-e186-4944-bac1-9db6e24b1880", + "blob:http://127.0.0.1:8084/a69433e8-d9d7-46d6-a381-3928fc13098f", + "blob:http://127.0.0.1:8084/744455a3-716d-4fb3-941a-1dccb5b18ae7", + "blob:http://127.0.0.1:8084/ec555855-0816-4894-b9c2-0daf8dc93bcd" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: net::ERR_FILE_NOT_FOUND" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "blob:http://127.0.0.1:8084/6d759ad1-ab20-46f4-ac79-d4c965a5491f", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_FILE_NOT_FOUND" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/6d759ad1-ab20-46f4-ac79-d4c965a5491f", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/8b155877-bb93-4fa4-b550-24fed64bf78b", + "blob:http://127.0.0.1:8084/6d759ad1-ab20-46f4-ac79-d4c965a5491f" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-error.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/6189672c-8372-4573-bdb0-09d0b80e65a0", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/2217a8bc-24ca-4f45-9060-60e8edda1149", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/6189672c-8372-4573-bdb0-09d0b80e65a0", + "blob:http://127.0.0.1:8084/e959bf97-4f8c-4e76-9866-0fa73c774112", + "blob:http://127.0.0.1:8084/1b2f13ea-dadf-451c-a356-1b9372956beb", + "blob:http://127.0.0.1:8084/2217a8bc-24ca-4f45-9060-60e8edda1149" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/c68027f4-29a4-48d6-be24-b349b2e56ef3", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/1b8f9834-5715-4839-9b12-23cd99a851e8", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/a0fc5d72-35af-406b-8254-53c46a9e61f9", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/204e8678-4197-4273-8f4d-51cd7ac4bbc4", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/c68027f4-29a4-48d6-be24-b349b2e56ef3", + "blob:http://127.0.0.1:8084/1eca83ee-eb1d-4ab5-995c-5c3e03cdaf55", + "blob:http://127.0.0.1:8084/1b8f9834-5715-4839-9b12-23cd99a851e8", + "blob:http://127.0.0.1:8084/00d60f27-4bbf-4210-b747-33593ce16420", + "blob:http://127.0.0.1:8084/a0fc5d72-35af-406b-8254-53c46a9e61f9", + "blob:http://127.0.0.1:8084/517d3204-1841-4249-b95e-a1bba1c6db9d", + "blob:http://127.0.0.1:8084/204e8678-4197-4273-8f4d-51cd7ac4bbc4" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/8d1cb18f-2979-4ef9-8ef4-e0ff99ca43d8", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/8d1cb18f-2979-4ef9-8ef4-e0ff99ca43d8" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/0d74b32d-226a-494d-8962-34681929c5e3", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/0c08f49e-9050-432f-aa35-5558ebc37cfc", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/0d74b32d-226a-494d-8962-34681929c5e3", + "blob:http://127.0.0.1:8084/77cbe72f-fe0a-490d-acfd-e93a83b35c8f", + "blob:http://127.0.0.1:8084/0c08f49e-9050-432f-aa35-5558ebc37cfc" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/be6a4acb-f7b3-41cc-b84c-20f6cbf3b5c5", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/88bf1ad4-45b0-465a-a95c-ec40d9ff0f02", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/b4c3f840-f5d8-43de-bfc4-c56c3dd6c0dc", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/8db54300-408e-4304-8a01-c733699fb891", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/be6a4acb-f7b3-41cc-b84c-20f6cbf3b5c5", + "blob:http://127.0.0.1:8084/579450cb-d688-47a9-869c-29e665007d25", + "blob:http://127.0.0.1:8084/88bf1ad4-45b0-465a-a95c-ec40d9ff0f02", + "blob:http://127.0.0.1:8084/14255e81-28ab-4fae-860c-578a5d6f0334", + "blob:http://127.0.0.1:8084/b4c3f840-f5d8-43de-bfc4-c56c3dd6c0dc", + "blob:http://127.0.0.1:8084/b385be20-5808-46d3-a16f-a82ad0c918e2", + "blob:http://127.0.0.1:8084/c41f7112-3bd2-4e31-9228-8726c6941281", + "blob:http://127.0.0.1:8084/8db54300-408e-4304-8a01-c733699fb891" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/cbbf0d57-d2a7-4f8a-b318-4f4afb2ae3a8", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/36e090ec-27b4-43f1-8f67-ff9c076808f8", + "blob:http://127.0.0.1:8084/cbbf0d57-d2a7-4f8a-b318-4f4afb2ae3a8" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/afaaa6a0-b944-44e2-929d-3dfcb9ee4c29", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/31a45ddb-9686-46c5-8766-375a8b7c61db", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/afaaa6a0-b944-44e2-929d-3dfcb9ee4c29", + "blob:http://127.0.0.1:8084/48d86e2d-4b35-4ef4-9a1a-9d5989f4d951", + "blob:http://127.0.0.1:8084/08a6b615-932f-4115-a281-dc8b5ed559f6", + "blob:http://127.0.0.1:8084/31a45ddb-9686-46c5-8766-375a8b7c61db" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/63860c5c-eb0a-4aa9-9044-b5141335dff9", + "resourceType": "other", + "failure": null + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/7d31a9ea-dafe-492a-8cfb-7694bf769465", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/1e764cd6-beea-458b-8324-f517c667d2d6", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/c5481878-4bfd-40b5-995c-7f4d069c4810", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/fcbf29c5-2770-4908-b938-0e59d4d70d4d", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/7d31a9ea-dafe-492a-8cfb-7694bf769465", + "blob:http://127.0.0.1:8084/111b3fce-91a7-4202-b106-232df2d4584a", + "blob:http://127.0.0.1:8084/1e764cd6-beea-458b-8324-f517c667d2d6", + "blob:http://127.0.0.1:8084/f532d5ba-9c52-4f6a-b673-c7dac2a5b15d", + "blob:http://127.0.0.1:8084/c5481878-4bfd-40b5-995c-7f4d069c4810", + "blob:http://127.0.0.1:8084/63860c5c-eb0a-4aa9-9044-b5141335dff9", + "blob:http://127.0.0.1:8084/fcbf29c5-2770-4908-b938-0e59d4d70d4d" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/885ce845-bb76-49e9-8810-a01d5d2963cb", + "resourceType": "fetch", + "failure": null + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/885ce845-bb76-49e9-8810-a01d5d2963cb", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/885ce845-bb76-49e9-8810-a01d5d2963cb" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/716f1325-0b9c-467f-b3df-d2d63ec583ce", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/337c7048-4467-42bc-8b65-f3d4d80396b6", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/716f1325-0b9c-467f-b3df-d2d63ec583ce", + "blob:http://127.0.0.1:8084/735f0b05-dd9b-4ddd-bf5b-b9d5d9a24853", + "blob:http://127.0.0.1:8084/337c7048-4467-42bc-8b65-f3d4d80396b6" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/65e600f4-5861-4874-83de-fe3c975f5f47", + "resourceType": "other", + "failure": null + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/e4b7ad9a-20ca-4de7-b78e-bc4941c4dd46", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/19c70502-f0b3-40d0-89d6-3db7dc52a5a4", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/37ff920f-645d-4d5f-a1a9-840709790b9d", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/7e3792f3-2d5d-46e0-9038-a44188597b31", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/e4b7ad9a-20ca-4de7-b78e-bc4941c4dd46", + "blob:http://127.0.0.1:8084/3bdb4ad1-16fc-4b71-ad21-bcca3611ff05", + "blob:http://127.0.0.1:8084/19c70502-f0b3-40d0-89d6-3db7dc52a5a4", + "blob:http://127.0.0.1:8084/8f3cef93-34a2-4259-a88d-2fbc0b57c8f8", + "blob:http://127.0.0.1:8084/37ff920f-645d-4d5f-a1a9-840709790b9d", + "blob:http://127.0.0.1:8084/65e600f4-5861-4874-83de-fe3c975f5f47", + "blob:http://127.0.0.1:8084/df2117f8-552d-411d-b04b-80b85f9085fd", + "blob:http://127.0.0.1:8084/7e3792f3-2d5d-46e0-9038-a44188597b31" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/3d3da328-2196-42a1-9e0f-33520c4f32fe", + "resourceType": "fetch", + "failure": null + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/3d3da328-2196-42a1-9e0f-33520c4f32fe", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/38f13e4f-b6db-49ee-a8f6-82c020c16fd2", + "blob:http://127.0.0.1:8084/3d3da328-2196-42a1-9e0f-33520c4f32fe" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/80245ed0-cc2c-4d8a-a376-9d6f4d6abf75", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/a74b6c4b-5800-45ee-b8f8-286f3b003fed", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/80245ed0-cc2c-4d8a-a376-9d6f4d6abf75", + "blob:http://127.0.0.1:8084/a869104f-b773-4a48-a1cd-adec9d116088", + "blob:http://127.0.0.1:8084/3a75d819-edb5-4327-a143-c0eb547d7bda", + "blob:http://127.0.0.1:8084/a74b6c4b-5800-45ee-b8f8-286f3b003fed" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + } + ] + }, + { + "command": "yarn test:browser test/browser/multiple-subtitles-lifecycle.spec.js", + "candidate": "legacy", + "file": "refactor/.cache/multiple-subtitles03-browser-legacy-verified.json", + "sha256": "f85db85ed4e48c55c146d075b39704e4098e66c6728f341e78c9e6439425799b", + "resultsDirectory": "refactor/.cache/multiple-subtitles03-browser-legacy-verified-results", + "stats": { + "startTime": "2026-09-13T02:53:18.543Z", + "duration": 18423.069000000003, + "expected": 30, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/79195f48-3895-4908-a6c7-ea73c2c8eab6", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/0c29bab2-755e-489f-9391-8f67a0b66c34", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/d0b64b3d-cafc-47ec-bb7d-27f6b434f26a", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/b9c4b80c-7a4d-4656-8915-c71438aebc22", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/79195f48-3895-4908-a6c7-ea73c2c8eab6", + "blob:http://127.0.0.1:8084/876f4323-5f03-49e1-a37d-f278e5893d4b", + "blob:http://127.0.0.1:8084/0c29bab2-755e-489f-9391-8f67a0b66c34", + "blob:http://127.0.0.1:8084/2f78580e-4eee-42c2-9f6d-17061273281c", + "blob:http://127.0.0.1:8084/d0b64b3d-cafc-47ec-bb7d-27f6b434f26a", + "blob:http://127.0.0.1:8084/2f3954de-a5e3-451f-ae65-d2e68928c139", + "blob:http://127.0.0.1:8084/b9c4b80c-7a4d-4656-8915-c71438aebc22" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: net::ERR_FILE_NOT_FOUND" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "blob:http://127.0.0.1:8084/b0525317-34b9-400a-ae22-4e37c607aee3", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_FILE_NOT_FOUND" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/b0525317-34b9-400a-ae22-4e37c607aee3", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/b0525317-34b9-400a-ae22-4e37c607aee3" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-error.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/3c4e7e3d-f6f5-4843-bbaf-0817ae4ac497", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/4a36c63f-ddb3-436c-b604-ccb8628cba62", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/3c4e7e3d-f6f5-4843-bbaf-0817ae4ac497", + "blob:http://127.0.0.1:8084/c3a1dd73-41f8-4314-981a-24ad02983019", + "blob:http://127.0.0.1:8084/4a36c63f-ddb3-436c-b604-ccb8628cba62" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/df4e4f25-f1cc-4e4e-ad19-56637a582b8b", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/f47069a8-9a8f-4d7f-bd92-f6b06f46b416", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/cb05bd83-1a98-459a-9f0d-d91dcf6f9927", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/9a80ad69-ee32-4402-a77b-acf5f6935542", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/df4e4f25-f1cc-4e4e-ad19-56637a582b8b", + "blob:http://127.0.0.1:8084/4e7a6c7d-87c8-4df1-9035-32c6e174064a", + "blob:http://127.0.0.1:8084/f47069a8-9a8f-4d7f-bd92-f6b06f46b416", + "blob:http://127.0.0.1:8084/7b2b0700-2080-4b7b-8a54-fc491b091da0", + "blob:http://127.0.0.1:8084/cb05bd83-1a98-459a-9f0d-d91dcf6f9927", + "blob:http://127.0.0.1:8084/0e64b3e1-e8ed-4839-984a-9f5eb73e267c", + "blob:http://127.0.0.1:8084/d69bbe70-57a9-428a-8cb0-7973eadfb605", + "blob:http://127.0.0.1:8084/9a80ad69-ee32-4402-a77b-acf5f6935542" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: net::ERR_FILE_NOT_FOUND" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "blob:http://127.0.0.1:8084/c13c6798-70e5-4552-b959-012261a51311", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_FILE_NOT_FOUND" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/c13c6798-70e5-4552-b959-012261a51311", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/68c80e71-4c66-4937-b52d-f84b336383db", + "blob:http://127.0.0.1:8084/c13c6798-70e5-4552-b959-012261a51311" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/multiple-error.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "chromium", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "153.0.8010.12", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/pattern.mp4", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + }, + { + "url": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "resourceType": "media", + "failure": { + "errorText": "net::ERR_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/f5e8cfff-dbcd-4572-be99-5a9bf12e0b44", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/cc00b3ef-be20-4566-be1f-fff6934d6fe9", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/f5e8cfff-dbcd-4572-be99-5a9bf12e0b44", + "blob:http://127.0.0.1:8084/a1abf481-2945-4196-9d1b-fba0c62d4969", + "blob:http://127.0.0.1:8084/4c81d583-7225-44d6-900d-2ace15cc5c61", + "blob:http://127.0.0.1:8084/cc00b3ef-be20-4566-be1f-fff6934d6fe9" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/1cb98f74-e8db-4d0b-87db-fb8ead0c3fb9", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/423c52bf-1e19-4d60-9ec1-40b942677c1c", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/62d5c4e4-e5e9-4ca7-9eb3-c478261a609c", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/3c8cd76a-4a8d-4e36-bdd5-e41eb43ef7df", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/1cb98f74-e8db-4d0b-87db-fb8ead0c3fb9", + "blob:http://127.0.0.1:8084/2aa0e13b-39ad-4474-8751-ad165ee73b3f", + "blob:http://127.0.0.1:8084/423c52bf-1e19-4d60-9ec1-40b942677c1c", + "blob:http://127.0.0.1:8084/971d1136-73ec-4afa-a746-8104253124e8", + "blob:http://127.0.0.1:8084/62d5c4e4-e5e9-4ca7-9eb3-c478261a609c", + "blob:http://127.0.0.1:8084/de999915-b222-4224-b37b-6d87d747fe8a", + "blob:http://127.0.0.1:8084/3c8cd76a-4a8d-4e36-bdd5-e41eb43ef7df" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/8a884cef-25cc-40dd-aa5c-8f49df965b87", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/8a884cef-25cc-40dd-aa5c-8f49df965b87" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/d336a7f8-52a4-464f-9a2b-a4636341cec4", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/11a45fe4-0dcd-429e-b787-2241f27cae71", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/d336a7f8-52a4-464f-9a2b-a4636341cec4", + "blob:http://127.0.0.1:8084/78ada6cb-6489-4c3e-b4c9-37916d2507ce", + "blob:http://127.0.0.1:8084/11a45fe4-0dcd-429e-b787-2241f27cae71" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/90132b88-f3a8-4325-a4fa-b73fd5e4ec28", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/41aba34c-7a08-4d3c-b278-af2236adf1a6", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/6de7cb1d-08c4-4137-86ab-d4f467a1f323", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/ddc92039-0b43-40e9-89e4-898830b79f9a", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/90132b88-f3a8-4325-a4fa-b73fd5e4ec28", + "blob:http://127.0.0.1:8084/7b9ec1f2-aae7-43ae-8ca5-bf6ebe3ab01c", + "blob:http://127.0.0.1:8084/41aba34c-7a08-4d3c-b278-af2236adf1a6", + "blob:http://127.0.0.1:8084/d248de93-a061-4995-a025-ba09cc3ae54e", + "blob:http://127.0.0.1:8084/6de7cb1d-08c4-4137-86ab-d4f467a1f323", + "blob:http://127.0.0.1:8084/a7f91041-6494-4219-bc24-42d604fc864c", + "blob:http://127.0.0.1:8084/48d8bdc8-cf2b-47ef-8c67-92dbbde7acd9", + "blob:http://127.0.0.1:8084/ddc92039-0b43-40e9-89e4-898830b79f9a" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "NS_BINDING_ABORTED" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/32c538b6-9ccd-422f-81e1-2e00bc23b9a9", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/d3b27045-e68d-4cd6-a548-5d20b9a39680", + "blob:http://127.0.0.1:8084/32c538b6-9ccd-422f-81e1-2e00bc23b9a9" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "firefox", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "155.0", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/66e50ff0-2c91-42e2-8bbe-82de8d5f179d", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/84c75c64-63dc-40e9-9cc6-b97abf0c277f", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/66e50ff0-2c91-42e2-8bbe-82de8d5f179d", + "blob:http://127.0.0.1:8084/48ea45a8-2f62-43ec-b4ec-c85b43e97f65", + "blob:http://127.0.0.1:8084/279c796f-a7c9-4ced-bc9a-5e96577e2954", + "blob:http://127.0.0.1:8084/84c75c64-63dc-40e9-9cc6-b97abf0c277f" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: SRT display and selectors survive the resource refactor", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/ebc2bcf8-4091-4b85-9ff2-c5b34866ee60", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/eb862836-fe83-4dd5-99dd-1abe3d9cbb0c", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/7fe7cd7a-c5ee-4233-84db-706f09852ddd", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/efaa2dc3-954d-4b06-b628-ac0b4eb84745", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/ebc2bcf8-4091-4b85-9ff2-c5b34866ee60", + "blob:http://127.0.0.1:8084/f0610074-b304-4fc2-8d3a-2b6368ad8c5e", + "blob:http://127.0.0.1:8084/eb862836-fe83-4dd5-99dd-1abe3d9cbb0c", + "blob:http://127.0.0.1:8084/4a30b7ee-9571-4857-bcbc-7e52efb6890d", + "blob:http://127.0.0.1:8084/7fe7cd7a-c5ee-4233-84db-706f09852ddd", + "blob:http://127.0.0.1:8084/3de0289f-d00f-484b-bef7-4b8d2aa35cd4", + "blob:http://127.0.0.1:8084/efaa2dc3-954d-4b06-b628-ac0b4eb84745" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy settles a pending native request before it responds", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: destroy revokes the Blob and retained reset stays inert", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/bc9012af-0a92-4f5c-859a-c4379e3ccae8", + "resourceType": "fetch", + "failure": null + } + ] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/bc9012af-0a92-4f5c-859a-c4379e3ccae8", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "blob:http://127.0.0.1:8084/bc9012af-0a92-4f5c-859a-c4379e3ccae8" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: native HTTP error rejects without installing subtitles", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate published core: video source switching retains cached subtitle selection", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=published", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "published", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/c6674431-ed15-47d1-8da8-b907d0c7cacf", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/a493c3b9-7f14-48ba-832a-7be406a38269", + "destroyed": false + } + ], + "revoked": [ + "http://127.0.0.1:8084/test/player.html?core=published", + "http://127.0.0.1:8084/test/pattern.mp4", + "blob:http://127.0.0.1:8084/c6674431-ed15-47d1-8da8-b907d0c7cacf", + "blob:http://127.0.0.1:8084/1d013c3f-db13-47ef-b64f-a6bd7b98f141", + "blob:http://127.0.0.1:8084/a493c3b9-7f14-48ba-832a-7be406a38269" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: SRT display and selectors survive the resource refactor", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/fe1d766b-6b13-4e9d-a918-f2b345a4ee19", + "resourceType": "other", + "failure": null + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "nativeSrtDisplay": true, + "released": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/ebc84445-3297-437e-8227-398063eee470", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/9f15c04a-a5c0-49c3-bc2d-0e0a9bbe0987", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/2d74194e-bf13-4fba-af16-d61335fe798a", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/7ed90c8f-65d4-4203-b09f-cbe5b5ab25f3", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/ebc84445-3297-437e-8227-398063eee470", + "blob:http://127.0.0.1:8084/dde3e39a-75b8-4c64-8020-3edda4ba4d30", + "blob:http://127.0.0.1:8084/9f15c04a-a5c0-49c3-bc2d-0e0a9bbe0987", + "blob:http://127.0.0.1:8084/80d66de7-8567-4a74-a282-fbdd411960fe", + "blob:http://127.0.0.1:8084/2d74194e-bf13-4fba-af16-d61335fe798a", + "blob:http://127.0.0.1:8084/fe1d766b-6b13-4e9d-a918-f2b345a4ee19", + "blob:http://127.0.0.1:8084/6f5de84f-8a0d-442d-aff6-28e6ab5e6f00", + "blob:http://127.0.0.1:8084/7ed90c8f-65d4-4203-b09f-cbe5b5ab25f3" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-en.srt", + "/test/multiple-jp.srt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy settles a pending native request before it responds", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [ + { + "url": "http://127.0.0.1:8084/test/multiple-held.vtt", + "resourceType": "fetch", + "failure": { + "errorText": "Load request cancelled" + } + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "name": "multipleSubtitles", + "voidReset": true, + "initialized": 0, + "aborted": true + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": { + "aborted": true + }, + "errors": [], + "requests": [ + "/test/multiple-held.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: destroy revokes the Blob and retained reset stays inert", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource" + ], + "failedRequests": [ + { + "url": "blob:http://127.0.0.1:8084/694e97b9-ff4f-4972-a0b7-5d895721741e", + "resourceType": "fetch", + "failure": null + } + ] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "voidReset": true, + "inaccessible": true, + "initialized": 1, + "revoked": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/694e97b9-ff4f-4972-a0b7-5d895721741e", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/2a64fcc4-fd4a-4c7e-ba7c-11ed8ac0d79c", + "blob:http://127.0.0.1:8084/694e97b9-ff4f-4972-a0b7-5d895721741e" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-ready.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: native HTTP error rejects without installing subtitles", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 (Not Found)" + ], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "message": "Failed to fetch multiple subtitles: HTTP 404", + "initialized": 0 + }, + "probe": { + "initialized": [], + "revoked": [], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-error.vtt" + ] + } + } + }, + { + "title": "Multiple subtitles candidate candidate core: video source switching retains cached subtitle selection", + "project": "webkit", + "status": "passed", + "manifest": "29a2e98ce35bc23aff2d9b1b325fd4a7148b42f2c0657a8c8022e6ddb5df9015", + "environment": { + "browser": "26.6", + "platform": "win32", + "state": { + "url": "http://127.0.0.1:8084/test/player.html?core=candidate", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15", + "events": [ + "video:loadedmetadata", + "ready", + "video:loadedmetadata", + "video:seeked", + "video:playing", + "video:pause", + "destroy" + ], + "media": { + "source": "http://127.0.0.1:8084/test/pattern.mp4?multiple-restart=1", + "width": 0, + "height": 0, + "duration": null, + "currentTime": 0 + } + }, + "errors": [], + "consoleErrors": [], + "failedRequests": [] + }, + "evidence": { + "core": "candidate", + "implementation": { + "name": "candidate", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21" + }, + "observation": { + "requests": [ + "/test/multiple-restart.vtt" + ], + "nativeSourceSwitch": true + }, + "probe": { + "initialized": [ + { + "url": "blob:http://127.0.0.1:8084/95bd1193-0ee1-4daf-b1fa-47ba3a05b0ff", + "destroyed": false + }, + { + "url": "blob:http://127.0.0.1:8084/8959a869-33f7-4db1-be36-740a4d57a5e3", + "destroyed": false + } + ], + "revoked": [ + "blob:http://127.0.0.1:8084/95bd1193-0ee1-4daf-b1fa-47ba3a05b0ff", + "blob:http://127.0.0.1:8084/45b4574a-e5f0-4dc4-91c5-5c1bedb85bf7", + "blob:http://127.0.0.1:8084/5d036e9e-a02b-4c76-9ec6-b3141a968a4e", + "blob:http://127.0.0.1:8084/8959a869-33f7-4db1-be36-740a4d57a5e3" + ], + "signal": null, + "errors": [], + "requests": [ + "/test/multiple-restart.vtt" + ] + } + } + } + ] + } + ], + "sources": [ + { + "file": "packages/artplayer-plugin-multiple-subtitles/src/index.js", + "sha256": "585c8db864f1f4a3e5124bfc5cecbe9a8efa71bfedc4362ae4c7623861bf82ef" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/src/lifetime.js", + "sha256": "f879fe2fd790a38ddfdfaf94cf517d9eab88d97f14d4aa7de2262d7223b7d854" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/src/merge.js", + "sha256": "c3c55c8b24fa736ec1afb59e6c7e0c511043c2da4a8f327a138e591f45addd25" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/src/parser.js", + "sha256": "ba582074d57fa2d72880e6e4253fe14646787916b2b68e2a1e748e685cf96ab1" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/src/render.js", + "sha256": "33a9619294e75c018030c7077e921ecfc45080e331e2d865e817882e5e08d8f8" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/src/request.js", + "sha256": "df480dbd7e6689783bf5217a1d47f3b9e1f4284645808694b8c10b17cb168d97" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/types/artplayer-plugin-multiple-subtitles.d.ts", + "sha256": "ab5dae1d0bdfcf2515cb842b1d465edf21dd195fe2fc5b0cfe351a8c06eb965c" + } + ], + "artifacts": [ + { + "file": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js", + "sha256": "af7433fb5aa43c2ff5514f604754cf98cb776c6b0525ea509161ec994438f607", + "copied": "docs/compiled/artplayer-plugin-multiple-subtitles.js" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js", + "sha256": "723acedd49f8102b9d8cfe1dbc1f9ce01989be6302f25064a11dc1fd95654f21", + "copied": "docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js" + }, + { + "file": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs", + "sha256": "e4e03622d728fd8d85edfef1d95d2a7b844bb662e9bc34c8fd2d40d64c5a724e", + "copied": "docs/compiled/artplayer-plugin-multiple-subtitles.mjs" + } + ], + "testInputs": [ + { + "file": "test/helpers/multiple-subtitles.js", + "sha256": "701a9faab5da2cd23e19906a5f200178c8f099549cfbc09f0b4100af03cc3e00" + }, + { + "file": "test/multiple-subtitles.test.js", + "sha256": "455bd38a7b7c2bae62f84c364c502bb6c02eee8c3d01e50295b7d86dd6b7aba7" + }, + { + "file": "test/multiple-subtitles-lifecycle.test.js", + "sha256": "22c95046cb5af5dbc0044cb8559452d9bf646b8e5ed9c9d0c80db8ba1c6272b4" + }, + { + "file": "test/multiple-subtitles-merge.test.js", + "sha256": "cb86c9caf58aa62af3492455d20fe2631e6df6259e164892d3612f983ab2a1c1" + }, + { + "file": "test/browser/multiple-subtitles-lifecycle.spec.js", + "sha256": "b9d59cafdd3be4712046f243310cc2e52473057a3c772174fbc39e242de2b05f" + } + ], + "remaining": "Strict TS/public declarations remain task 04. Full historical core support, physical Safari/mobile, native ASS/encoding rendering, styling/editor and installed consumers remain tasks 05/06. Source/main/legacy native tests use published/candidate core fixtures. The public async registration and void selection timing are preserved; arbitrary host state cannot be rolled back after async rejection." +} diff --git a/refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md b/refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md new file mode 100644 index 000000000..53af10c43 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md @@ -0,0 +1,73 @@ +# PKG-MULTI-SUB-03 模块边界与资源修复 + +本次修改生产源码,保持工厂名称/必需参数、async 注册结果、name/tracks/reset、同步 void +选择方法和原分发入口。自有 JS 先按职责拆清,严格 TS 与公开声明仍由 04 完成。 +原始 parser、许可和公开类型未改,没有新增依赖或改变包版本。 + +## 结构变化 + +| 文件 | 实际职责 | +| --- | --- | +| index.js | 注册编排、名称选择、活跃状态检查和公开返回对象 | +| request.js | 每轨请求、原生状态检查、字节解码与既有核心格式转换 | +| merge.js | 解析元数据和按选择顺序序列化;通过复制 cue 包装文本,原树不变 | +| lifetime.js | 本次注册的 destroy 监听、请求取消、Promise 提前结束与清理登记 | +| render.js | Blob 所有权、新旧 URL 替换、重入保护及宿主 init 失败处理 | +| parser.js | 保持原始 vendor 边界,不因本次拆分改写算法 | + +模块依赖由入口向请求/纯解析/安装流动;仅入口和宿主安装/生命周期需要 art,纯 merge +不依赖 DOM、fetch 或播放器。没有引入要求用户升级核心的共享运行时依赖。 + +## 独立缺陷修正 + +- 销毁中 fetch/body 通过取消信号和关闭 Promise 结束;无 AbortController 的旧环境仍 + 能及时结束注册并忽略迟到结果。返回相同形状的失活结果,不新增 destroy 拒绝。 +- 单轨失败保留原传输/解码异常身份,关闭整个注册并取消兄弟请求;HTTP ok=false 明确 + 拒绝且取消未消费的响应,不再把错误页面当作有效字幕。 +- 最后 Blob URL 在 destroy 时释放。保留的 tracks/reset 此后返回 undefined,不再 + 读取名称、分配 URL 或触发宿主。重复 destroy 不重复释放。 +- 分配新 URL 成功后才尝试替换;分配失败保留旧 URL。同步 init 失败释放新 URL, + 保留先前资源并回滚该次 escape 写入;首次注册失败同时移除监听。 +- 观察宿主真实 Promise 的 rejection,释放失败安装对应的 URL,并在活跃时以 console.warn + 保留诊断。正常注册仍不等待宿主加载;tracks/reset 仍为 void。宿主核心可能自行显示 + notice,这里不引入新公开事件,也不假装异步错误会从旧 void 方法同步抛出。 +- 老 init 的迟到错误不释放后来成功替换的资源;同步重入选择、配置读取失败,以及 + 用户 getter/init 中触发 destroy 都有明确归属检查。 + +异步 init 失败发生时,宿主可能已经更新自身状态;插件不能通用回滚第三方宿主的副作用。 +此时释放失败资源,保留旧异步接口时序并记录错误;调用方可在仍活跃时重新 reset。 +正常 destroy 不恢复共享 subtitle.escape,避免覆盖后续插件/用户的配置。 + +## 保留的兼容边界 + +1. 采用已发布 1.2.0 的逐轨连接语义;没有恢复 1.0.0 按下标丢 cue 的缺陷。 +2. 轨内 parser 排序、轨间选择顺序、同名取第一个、重复选择、未知名 TypeError、空轨、 + onParser 未执行,以及坏 header 仍可保留可用 cue 的容错均保留。 +3. option.type 优先、当前核心 SRT/ASS 转换、encoding 默认与原生解码行为保持。 + 轨元数据在下载结束后读取;restart 不重新下载字幕,配置在每次安装时读取。 +4. 不再调用无实际资源的 revokeObjectURL('')。历史测试仍断言旧调用记录,候选正常 + 测试仅对这一项无资源清理差异作明确区分,不放宽真实 URL 的释放断言。 +5. 合并不再原地标记 updated/改 cue;冻结树重复序列化测试和九种真实旧产物逐字对照 + 证明输出保持,另对实际核心转换和 Windows-1252/UTF-16LE 字节逐字比较。 + +## 验证与限制 + +命令及准确运行结果、输入指纹、原生浏览器观察见 +[验证记录](../baselines/multiple-subtitles-resources.json)。新生命周期/merge 文件接入 +test:unit 与 test:multiple-subtitles,生产三入口由正常 yarn build 生成。 +浏览器矩阵使用发布/候选核心、Chromium/Firefox/Windows WebKit 和源码/现代/legacy +插件,覆盖 SRT 显示/选择、挂起请求销毁、原生 Blob 不可访问、HTTP 失败及真实视频切源。 +其中资源/错误场景直接执行公开注册函数,显示与切源场景使用真实 plugins.add。 + +最终本包 241 项通过;源码/main/legacy 各 31 项通过(其中冻结树纯函数用例固定读取源码, +其余正常/生命周期/转换对照通过选定入口)。三入口原生浏览器各 30 项,共 90 项通过。 +完整 CI 2311 项通过:1963 单元、14 工程、334 基线;严格生产 TS 仍 346 文件。 +状态更新后 9 项风险/契约元数据测试通过。显式 lint 无错误或警告;全量 lint 仍有既有 +生成声明 unused-disable 警告。实际 ESM 只验证导出形状和工厂,不冒充完整安装消费者。 + +这不是全部历史核心、实体 Safari/移动设备、ASS 原生显示、样式/编辑器或 npm 发布准入。 +HOST/HTTP 在本次边界回归后关闭;LIFE/MERGE 的完整支持范围组合仍由 05 补齐。 +每项状态以 tasks/risks 与验证记录为准;后续 04 迁移 TS 时重跑相同生产入口用例。 + +独立本地任务提交,不推送、不发布。回退本提交恢复旧单文件和生成产物,同时重新引入 +02 已复现的泄漏/失败行为;02 历史基线及浏览器证据保留。 diff --git a/refactor/plan.md b/refactor/plan.md index 48ffe8ee1..ffbc36d40 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。 -状态:todo 90 / doing 13 / blocked 0 / done 119 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 89 / doing 13 / blocked 0 / done 120 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -218,7 +218,7 @@ | --- | --- | --- | --- | --- | --- | --- | | PKG-MULTI-SUB-01 | artplayer-plugin-multiple-subtitles
核对包契约与历史用法 | BASE-05 | subtitles/onParser、multipleSubtitles 名称及实际返回方法 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 VENDOR-03,定位 WebVTT parser 来源/修订及 CC0 头,保留解析/序列化行为 | H | done | | PKG-MULTI-SUB-02 | artplayer-plugin-multiple-subtitles
建立特有行为与错误测试 | PKG-MULTI-SUB-01, ENG-03, ENG-05 | VTT/SRT/ASS、编码/重叠 cue/排序/空轨/失败与切换 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | -| PKG-MULTI-SUB-03 | artplayer-plugin-multiple-subtitles
整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo | +| PKG-MULTI-SUB-03 | artplayer-plugin-multiple-subtitles
整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | | PKG-MULTI-SUB-04 | artplayer-plugin-multiple-subtitles
迁移自有源码和公开类型 | PKG-MULTI-SUB-03, ENG-04, ENG-06, CORE-07 | 准确 parser/cue/tree/result 类型与真实异步形状 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo | | PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles
验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | | PKG-MULTI-SUB-06 | artplayer-plugin-multiple-subtitles
验证分发并同步文档 | PKG-MULTI-SUB-05, ENG-07 | multiple.subtitles.js、parser 来源许可及打包边界 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | @@ -513,6 +513,7 @@ - PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md) [记录](baselines/dash-seek-diagnosis.json) - PKG-MULTI-SUB-01: [记录](baselines/multiple-subtitles-release.json) [记录](baselines/multiple-subtitles-contract.md) [记录](baselines/multiple-subtitles-contract-validation.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-01-contract.md) - PKG-MULTI-SUB-02: [记录](baselines/multiple-subtitles-failures.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-02-failures.md) +- PKG-MULTI-SUB-03: [记录](baselines/multiple-subtitles-resources.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-03-resources.md) - PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs) - PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json) - PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index 3628f243e..5c9cff4bf 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,22 @@ # 进度与证据 +## PKG-MULTI-SUB-03 模块与资源修复完成 + +生产源码拆为入口、请求、纯解析合并、生命周期和宿主安装五个模块,vendor parser 不变。 +修复 HTTP 失败、兄弟请求取消、fetch/body 销毁中结算、Blob 泄漏、分配失败替换顺序、 +同步/异步安装失败与重入归属;保留 async 注册与 void tracks/reset、轨序、元数据和容错。 +九种 markup/timing/空轨输入及真实转换/编码逐字对照已发布 1.2.0,冻结树重复序列化通过。 +源码/main/legacy 各 31 项通过;本包 241 项通过,两新增文件已接入主 test:unit。 +三入口配发布/候选核心,在 Chromium/Firefox/Windows WebKit 共 90 项通过:真实 SRT、 +选择/清空/reset、请求销毁、Blob 不可访问、HTTP 失败和真实视频切源无重复字幕下载。 +完整 CI 2311 项通过(1963 单元、14 工程、334 基线);严格生产 TS 仍 346,04 继续 TS。 +正常生产构建及实际 ESM 导入通过,三个 dist 与 docs/compiled 对应字节一致。 +HTTP/HOST 风险关闭;LIFE/MERGE 完整支持范围组合仍待 05。见 +[变更](changes/2026-09-13-PKG-MULTI-SUB-03-resources.md)和[验证](baselines/multiple-subtitles-resources.json)。 +222 项:120 done、13 doing、89 todo;196 项风险。独立本地提交,不推送、不发布。 +下一步 MULTI-SUB-04 自有模块严格 TS 与旧消费者公开类型;VTT 类型取舍继续待决。 + + ## PKG-MULTI-SUB-02 错误与生命周期复现完成 9 份历史实现各 18 类异常/边界,共 162 项;本包命令及主 test:unit 已接入, diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 57ba27bb8..023ccdd83 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -198,5 +198,5 @@ | MULTI-SUB-MERGE-01 | open / 已复现 | 1.0.0 merges cues by index while later versions concatenate independently timed track cues | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 | | MULTI-SUB-LIFE-01 | open / 已复现 | Pending downloads and final object URL have no destroy owner | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 | | MULTI-SUB-EXPORT-01 | open / 已复现 | Older CommonJS default objects conflict with export-assignment declarations; latest exports a direct factory | PKG-MULTI-SUB-04, PKG-MULTI-SUB-06 | -| MULTI-SUB-HTTP-01 | open / 已复现 | HTTP error responses with parseable bodies are installed as subtitles | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 | -| MULTI-SUB-HOST-01 | open / 已复现 | Subtitle init rejections are ignored and failed installation or URL replacement leaks or invalidates resources | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 | +| MULTI-SUB-HTTP-01 | resolved / 已复现 | HTTP error responses with parseable bodies are installed as subtitles | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 | +| MULTI-SUB-HOST-01 | resolved / 已复现 | Subtitle init rejections are ignored and failed installation or URL replacement leaks or invalidates resources | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 | diff --git a/refactor/risks.json b/refactor/risks.json index 5a0e1495e..8fa7db94b 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4279,10 +4279,16 @@ "test/multiple-subtitles-failures.test.js", "test/browser/multiple-subtitles-history.spec.js", "refactor/baselines/multiple-subtitles-failures.json", - "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md" + "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md", + "refactor/baselines/multiple-subtitles-resources.json", + "refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md", + "test/multiple-subtitles-lifecycle.test.js", + "test/multiple-subtitles-merge.test.js", + "test/browser/multiple-subtitles-lifecycle.spec.js" ], "compatibleResolution": "Keep historical merge profiles explicit and test real overlap/timing before choosing compatibility behavior.", - "closureCriteria": "Unaligned lengths, overlap, sorting, names and real subtitle display across supported cores are verified." + "closureCriteria": "Unaligned lengths, overlap, sorting, names and real subtitle display across supported cores are verified.", + "workspaceState": "Candidate resource fixes and serialization parity are verified in task 03. This risk remains open for the complete supported-core/device coverage owned by task 05." }, { "id": "MULTI-SUB-LIFE-01", @@ -4302,10 +4308,16 @@ "test/multiple-subtitles-failures.test.js", "test/browser/multiple-subtitles-history.spec.js", "refactor/baselines/multiple-subtitles-failures.json", - "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md" + "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md", + "refactor/baselines/multiple-subtitles-resources.json", + "refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md", + "test/multiple-subtitles-lifecycle.test.js", + "test/multiple-subtitles-merge.test.js", + "test/browser/multiple-subtitles-lifecycle.spec.js" ], "compatibleResolution": "Reproduce stale completion and own requests and URLs without changing public selection methods.", - "closureCriteria": "Cancellation, failures, source changes, repeated selection and destroy are verified with native core coverage." + "closureCriteria": "Cancellation, failures, source changes, repeated selection and destroy are verified with native core coverage.", + "workspaceState": "Candidate resource fixes and serialization parity are verified in task 03. This risk remains open for the complete supported-core/device coverage owned by task 05." }, { "id": "MULTI-SUB-EXPORT-01", @@ -4331,7 +4343,7 @@ "compatibleResolution": "Reject actual HTTP failures with explicit diagnostics while preserving successful conversion and original transport errors.", "closureCriteria": "Native HTTP errors, transport/body failures, sibling cancellation and successful requests have candidate regressions.", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "PKG-MULTI-SUB-03", "PKG-MULTI-SUB-05" @@ -4340,8 +4352,21 @@ "test/multiple-subtitles-failures.test.js", "test/browser/multiple-subtitles-history.spec.js", "refactor/baselines/multiple-subtitles-failures.json", - "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md" - ] + "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md", + "refactor/baselines/multiple-subtitles-resources.json", + "refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md", + "test/multiple-subtitles-lifecycle.test.js", + "test/multiple-subtitles-merge.test.js", + "test/browser/multiple-subtitles-lifecycle.spec.js" + ], + "resolutionEvidence": [ + "refactor/baselines/multiple-subtitles-resources.json", + "refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md", + "test/multiple-subtitles-lifecycle.test.js", + "test/multiple-subtitles-merge.test.js", + "test/browser/multiple-subtitles-lifecycle.spec.js" + ], + "resolutionRationale": "HTTP failures reject before body consumption and abort their requests. Exact transport/body errors, sibling cancellation, conversion success and native 404 rejection pass candidate regressions; source/main/legacy run with both core fixtures." }, { "id": "MULTI-SUB-HOST-01", @@ -4349,7 +4374,7 @@ "compatibleResolution": "Own installation results and URL rollback while preserving void selection methods and normal registration timing.", "closureCriteria": "Synchronous and asynchronous host failures, stale rejection, allocation failure and destroy leave no plugin-owned live resources.", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "PKG-MULTI-SUB-03", "PKG-MULTI-SUB-05" @@ -4358,8 +4383,21 @@ "test/multiple-subtitles-failures.test.js", "test/browser/multiple-subtitles-history.spec.js", "refactor/baselines/multiple-subtitles-failures.json", - "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md" - ] + "refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md", + "refactor/baselines/multiple-subtitles-resources.json", + "refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md", + "test/multiple-subtitles-lifecycle.test.js", + "test/multiple-subtitles-merge.test.js", + "test/browser/multiple-subtitles-lifecycle.spec.js" + ], + "resolutionEvidence": [ + "refactor/baselines/multiple-subtitles-resources.json", + "refactor/changes/2026-09-13-PKG-MULTI-SUB-03-resources.md", + "test/multiple-subtitles-lifecycle.test.js", + "test/multiple-subtitles-merge.test.js", + "test/browser/multiple-subtitles-lifecycle.spec.js" + ], + "resolutionRationale": "Owned installations retain prior URLs on synchronous failure, observe async rejections, release the failed active URL without affecting newer installs, and clean up on destroy. Controlled sync/async/reentrant/getter failures and native Blob revocation pass; void methods and registration timing are preserved." } ] } diff --git a/refactor/tasks.json b/refactor/tasks.json index c139a2c31..cd8c138ab 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -2297,11 +2297,14 @@ "PKG-MULTI-SUB-02", "CORE-15" ], - "status": "todo", + "status": "done", "risk": "H", "deliverable": "解析与合并/获取/渲染分离,过期请求及对象 URL 清理", "acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过", - "evidence": [] + "evidence": [ + "baselines/multiple-subtitles-resources.json", + "changes/2026-09-13-PKG-MULTI-SUB-03-resources.md" + ] }, { "id": "PKG-MULTI-SUB-04", diff --git a/test/browser/multiple-subtitles-lifecycle.spec.js b/test/browser/multiple-subtitles-lifecycle.spec.js new file mode 100644 index 000000000..b5088f67e --- /dev/null +++ b/test/browser/multiple-subtitles-lifecycle.spec.js @@ -0,0 +1,169 @@ +import { hash } from '../../refactor/scripts/releases.mjs' +import { multipleSubtitlesCandidate } from '../helpers/multiple-subtitles.js' +import { expect, test } from './fixtures.js' + +const implementation = await multipleSubtitlesCandidate() +const vtt = 'WEBVTT\n\n00:00.000 --> 00:08.000\nCached caption\n' +const srt = text => `1\n00:00:00,000 --> 00:00:08,000\n${text}\n` + +async function prepare(page, core) { + await page.goto(`/test/player.html?core=${core}`) + await page.addScriptTag({ content: implementation.code }) + await page.evaluate(() => { + window.createPlayer('/test/pattern.mp4') + window.multipleProbe = { initialized: [], revoked: [], signal: null, errors: [], requests: [] } + const fetch = window.fetch.bind(window) + window.multipleNativeFetch = fetch + window.fetch = (url, options) => { + if (typeof url === 'string' && url.startsWith('/test/multiple-')) + window.multipleProbe.requests.push(url) + if (url === '/test/multiple-held.vtt') + window.multipleProbe.signal = options?.signal + return fetch(url, options) + } + const revoke = URL.revokeObjectURL.bind(URL) + URL.revokeObjectURL = (url) => { + window.multipleProbe.revoked.push(url) + revoke(url) + } + const init = window.art.subtitle.init.bind(window.art.subtitle) + window.art.subtitle.init = (option) => { + window.multipleProbe.initialized.push({ url: option.url, destroyed: window.art.isDestroy }) + const result = init(option) + Promise.resolve(result).catch(error => window.multipleProbe.errors.push({ name: error.name, message: error.message })) + return result + } + }) + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) +} + +async function evidence(page, testInfo, core, observation) { + const probe = await page.evaluate(() => ({ ...window.multipleProbe, signal: window.multipleProbe.signal ? { aborted: window.multipleProbe.signal.aborted } : null })) + expect(probe.errors).toEqual([]) + await testInfo.attach('multiple-subtitles-lifecycle', { body: JSON.stringify({ core, implementation: { name: implementation.name, sha256: hash(implementation.code) }, observation, probe }), contentType: 'application/json' }) +} + +for (const core of ['published', 'candidate']) { + test(`Multiple subtitles candidate ${core} core: SRT display and selectors survive the resource refactor`, async ({ page }, testInfo) => { + await page.route('**/test/multiple-en.srt', route => route.fulfill({ body: srt('English'), contentType: 'text/plain' })) + await page.route('**/test/multiple-jp.srt', route => route.fulfill({ body: srt('日本語'), contentType: 'text/plain' })) + await prepare(page, core) + await page.evaluate(async () => { + await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'en', url: '/test/multiple-en.srt' }, { name: 'jp', url: '/test/multiple-jp.srt' }] })) + }) + await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2) + await page.locator('#play').click() + await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1) + await page.locator('#pause').click() + await expect(page.locator('.art-subtitle-en')).toHaveText('English') + await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語') + await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['jp'])) + await expect(page.locator('.art-subtitle-en')).toHaveCount(0) + await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語') + await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([])) + await expect(page.locator('.art-subtitle-jp')).toHaveCount(0) + await page.evaluate(() => window.art.plugins.multipleSubtitles.reset()) + await expect(page.locator('.art-subtitle-en')).toHaveText('English') + await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語') + const released = await page.evaluate(() => { + window.art.destroy(false) + return window.multipleProbe.initialized.every(item => window.multipleProbe.revoked.includes(item.url)) + }) + expect(released).toBe(true) + await evidence(page, testInfo, core, { nativeSrtDisplay: true, released }) + }) + + test(`Multiple subtitles candidate ${core} core: destroy settles a pending native request before it responds`, async ({ page }, testInfo) => { + let requested + let release + const started = new Promise((resolve) => { + requested = resolve + }) + const held = new Promise((resolve) => { + release = resolve + }) + await page.route('**/test/multiple-held.vtt', async (route) => { + requested() + await held + await route.fulfill({ body: vtt, contentType: 'text/vtt' }) + }) + try { + await prepare(page, core) + await page.evaluate(() => { + window.multiplePending = window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-held.vtt', name: 'held' }] })(window.art) + }) + await started + const state = await page.evaluate(async () => { + window.art.destroy(false) + const result = await window.multiplePending + return { name: result.name, voidReset: result.reset() === undefined, initialized: window.multipleProbe.initialized.length, aborted: window.multipleProbe.signal.aborted } + }) + expect(state).toEqual({ name: 'multipleSubtitles', voidReset: true, initialized: 0, aborted: true }) + release() + await evidence(page, testInfo, core, state) + } + finally { release() } + }) + + test(`Multiple subtitles candidate ${core} core: destroy revokes the Blob and retained reset stays inert`, async ({ page }, testInfo) => { + await page.route('**/test/multiple-ready.vtt', route => route.fulfill({ body: vtt, contentType: 'text/vtt' })) + await prepare(page, core) + await page.evaluate(async () => { + window.multipleResult = await window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-ready.vtt', name: 'ready' }] })(window.art) + }) + await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1) + const state = await page.evaluate(async () => { + const url = window.multipleProbe.initialized[0].url + window.art.destroy(false) + const voidReset = window.multipleResult.reset() === undefined + let inaccessible = false + try { + await window.multipleNativeFetch(url) + } + catch (error) { + inaccessible = error.name === 'TypeError' + } + return { voidReset, inaccessible, initialized: window.multipleProbe.initialized.length, revoked: window.multipleProbe.revoked.includes(url) } + }) + expect(state).toEqual({ voidReset: true, inaccessible: true, initialized: 1, revoked: true }) + await evidence(page, testInfo, core, state) + }) + + test(`Multiple subtitles candidate ${core} core: native HTTP error rejects without installing subtitles`, async ({ page }, testInfo) => { + await page.route('**/test/multiple-error.vtt', route => route.fulfill({ status: 404, body: vtt, contentType: 'text/vtt' })) + await prepare(page, core) + const state = await page.evaluate(async () => { + let message + try { + await window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-error.vtt', name: 'error' }] })(window.art) + } + catch (error) { + message = error.message + } + window.art.destroy(false) + return { message, initialized: window.multipleProbe.initialized.length } + }) + expect(state).toEqual({ message: 'Failed to fetch multiple subtitles: HTTP 404', initialized: 0 }) + await evidence(page, testInfo, core, state) + }) + + test(`Multiple subtitles candidate ${core} core: video source switching retains cached subtitle selection`, async ({ page }, testInfo) => { + await page.route('**/test/multiple-restart.vtt', route => route.fulfill({ body: vtt, contentType: 'text/vtt' })) + await prepare(page, core) + await page.evaluate(async () => { + await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-restart.vtt', name: 'restart' }] })) + }) + await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1) + await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4?multiple-restart=1')) + await page.evaluate(() => window.art.plugins.multipleSubtitles.reset()) + await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1) + await page.locator('#play').click() + await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1) + await page.locator('#pause').click() + await expect(page.locator('.art-subtitle-restart')).toHaveText('Cached caption') + const requests = await page.evaluate(() => window.multipleProbe.requests) + expect(requests).toEqual(['/test/multiple-restart.vtt']) + await page.evaluate(() => window.art.destroy(false)) + await evidence(page, testInfo, core, { requests, nativeSourceSwitch: true }) + }) +} diff --git a/test/helpers/multiple-subtitles.js b/test/helpers/multiple-subtitles.js index bb416dae0..60bb4da4b 100644 --- a/test/helpers/multiple-subtitles.js +++ b/test/helpers/multiple-subtitles.js @@ -28,7 +28,7 @@ export async function multipleSubtitlesHistorical() { return implementations } -export function multipleSubtitlesEnvironment(implementation, { script = false, responses = {}, fetchResponse, coreUtils, onInit, onCreate } = {}) { +export function multipleSubtitlesEnvironment(implementation, { script = false, responses = {}, fetchResponse, coreUtils, onInit, onCreate, abortController = true } = {}) { const requests = [] const requestOptions = [] const blobs = new Map() @@ -38,6 +38,7 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r const initStates = [] const converted = [] const listeners = new Map() + const warnings = [] const module = { exports: {} } const utils = { getExt: url => url.split('.').pop(), @@ -80,7 +81,8 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r const context = { Blob, TextDecoder, - console: { log() {}, warn() {}, error() {} }, + AbortController: abortController ? AbortController : undefined, + console: { log() {}, warn(...args) { warnings.push(args) }, error() {} }, URL: { createObjectURL(blob) { onCreate?.(blob) @@ -115,5 +117,5 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r for (const callback of [...(listeners.get(name) || [])]) callback(...args) } - return { factory, exported, art, utils, requests, requestOptions, blobs, liveBlobs, revoked, initialized, initStates, converted, listeners, emit, responseFor, latestText } + return { factory, exported, art, utils, requests, requestOptions, blobs, liveBlobs, revoked, initialized, initStates, converted, listeners, warnings, emit, responseFor, latestText } } diff --git a/test/multiple-subtitles-lifecycle.test.js b/test/multiple-subtitles-lifecycle.test.js new file mode 100644 index 000000000..eb35a33cb --- /dev/null +++ b/test/multiple-subtitles-lifecycle.test.js @@ -0,0 +1,319 @@ +import assert from 'node:assert/strict' +// eslint-disable-next-line test/no-import-node-test -- Lifecycle regressions use the repository Node runner. +import test from 'node:test' +import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, subtitleVtt } from './helpers/multiple-subtitles.js' + +const candidate = await multipleSubtitlesCandidate() +const create = options => multipleSubtitlesEnvironment(candidate, options) +const options = { subtitles: [{ url: 'a.vtt', name: 'a' }] } +function deferred() { + let resolve + let reject + const promise = new Promise((yes, no) => { + resolve = yes + reject = no + }) + return { promise, resolve, reject } +} +async function flush() { + for (let index = 0; index < 20; index++) + await Promise.resolve() +} +function empty(env) { + assert.equal(env.liveBlobs.size, 0) + assert.equal(env.listeners.get('destroy')?.size || 0, 0) +} + +for (const abortController of [true, false]) { + for (const phase of ['fetch', 'body']) { + test(`Multiple subtitles candidate: destroy cancels ${phase}, AbortController=${abortController}`, async () => { + const held = deferred() + const started = deferred() + const env = create({ abortController, fetchResponse: (_url, _options, responseFor) => { + if (phase === 'fetch') { + started.resolve() + return held.promise + } + return { ...responseFor('a.vtt'), arrayBuffer() { + started.resolve() + return held.promise + } } + } }) + const pending = env.factory(options)(env.art) + await started.promise + env.emit('destroy') + const result = await pending + assert.equal(result.name, 'multipleSubtitles') + assert.equal(result.reset(), undefined) + assert.equal(result.tracks(['missing']), undefined) + assert.equal(env.requestOptions[0]?.signal.aborted, abortController ? true : undefined) + assert.equal(env.initialized.length, 0) + empty(env) + held.reject(new Error('late rejected operation')) + await flush() + assert.equal(env.initialized.length, 0) + assert.equal(env.warnings.length, 0) + }) + } +} + +test('Multiple subtitles candidate: already destroyed hosts do not request or subscribe', async () => { + const env = create() + env.emit('destroy') + const result = await env.factory(options)(env.art) + result.reset() + assert.equal(env.requests.length, 0) + assert.equal(env.initialized.length, 0) + empty(env) +}) + +test('Multiple subtitles candidate: failed request preserves error identity and cancels a sibling body', async () => { + const failed = deferred() + const body = deferred() + const started = deferred() + const error = new Error('network failed') + const env = create({ fetchResponse: url => url === 'a.vtt' + ? failed.promise + : { ok: true, arrayBuffer() { + started.resolve() + return body.promise + } } }) + const pending = env.factory({ subtitles: [...options.subtitles, { url: 'b.vtt', name: 'b' }] })(env.art) + await started.promise + failed.reject(error) + await assert.rejects(pending, caught => caught === error) + assert.equal(env.requestOptions[1].signal.aborted, true) + body.resolve(new TextEncoder().encode(subtitleVtt('late')).buffer) + await flush() + assert.equal(env.initialized.length, 0) + empty(env) +}) + +test('Multiple subtitles candidate: HTTP failure rejects without reading or installing its body', async () => { + let read = false + const env = create({ fetchResponse: () => ({ ok: false, status: 404, arrayBuffer() { + read = true + } }) }) + await assert.rejects(env.factory(options)(env.art), { message: 'Failed to fetch multiple subtitles: HTTP 404' }) + assert.equal(read, false) + assert.equal(env.requestOptions[0].signal.aborted, true) + assert.equal(env.initialized.length, 0) + empty(env) +}) + +test('Multiple subtitles candidate: body failure and bad encoding release registration resources', async () => { + const error = new Error('body failed') + const env = create({ fetchResponse: () => ({ ok: true, arrayBuffer: () => Promise.reject(error) }) }) + await assert.rejects(env.factory(options)(env.art), caught => caught === error) + empty(env) + const invalid = create() + await assert.rejects(invalid.factory({ subtitles: [{ ...options.subtitles[0], encoding: 'invalid-label' }] })(invalid.art), { name: 'RangeError' }) + empty(invalid) +}) + +test('Multiple subtitles candidate: destroy releases the last URL and makes retained methods inert', async () => { + const env = create() + const result = await env.factory(options)(env.art) + env.emit('destroy') + env.emit('destroy') + assert.equal(result.reset(), undefined) + assert.equal(result.tracks(), undefined) + assert.deepEqual(env.revoked, ['blob:subtitle-1']) + assert.equal(env.initialized.length, 1) + empty(env) +}) + +test('Multiple subtitles candidate: allocation failure preserves the old URL and reset recovers', async () => { + let calls = 0 + const error = new Error('allocation failed') + const env = create({ onCreate() { + if (++calls === 2) + throw error + } }) + const result = await env.factory(options)(env.art) + assert.throws(() => result.reset(), caught => caught === error) + assert.equal(env.liveBlobs.has('blob:subtitle-1'), true) + assert.equal(env.revoked.length, 0) + result.reset() + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-2']) + env.emit('destroy') + empty(env) +}) + +test('Multiple subtitles candidate: synchronous initial host failure rolls back escape and frees the new URL', async () => { + const error = new Error('init failed') + const env = create({ onInit() { + throw error + } }) + await assert.rejects(env.factory(options)(env.art), caught => caught === error) + assert.equal(env.art.option.subtitle.escape, true) + empty(env) +}) + +test('Multiple subtitles candidate: a failed synchronous replacement retains the previous resource', async () => { + let calls = 0 + const error = new Error('replacement failed') + const env = create({ onInit() { + if (++calls === 2) + throw error + } }) + const result = await env.factory(options)(env.art) + assert.throws(() => result.reset(), caught => caught === error) + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-1']) + result.reset() + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-3']) + env.emit('destroy') + empty(env) +}) + +test('Multiple subtitles candidate: host media loading does not delay registration or change void selection', async () => { + const held = deferred() + const env = create({ onInit: () => held.promise }) + const result = await env.factory(options)(env.art) + assert.equal(result.name, 'multipleSubtitles') + assert.equal(result.reset(), undefined) + assert.equal(env.initialized.length, 2) + held.resolve() + await flush() + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-2']) + env.emit('destroy') + empty(env) +}) + +test('Multiple subtitles candidate: asynchronous rejection is observed and only frees its own URL', async () => { + const operations = [deferred(), deferred()] + let calls = 0 + const env = create({ onInit: () => operations[calls++].promise }) + const result = await env.factory(options)(env.art) + result.reset() + operations[0].reject(new Error('stale failure')) + await flush() + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-2']) + operations[1].reject(new Error('current failure')) + await flush() + assert.equal(env.liveBlobs.size, 0) + assert.equal(env.warnings.length, 2) + env.emit('destroy') + empty(env) +}) + +test('Multiple subtitles candidate: destroy in host init does not install resources again', async () => { + const env = create({ onInit() { + env.emit('destroy') + } }) + const result = await env.factory(options)(env.art) + result.reset() + assert.equal(env.initialized.length, 1) + assert.deepEqual(env.revoked, ['blob:subtitle-1']) + empty(env) +}) + +test('Multiple subtitles candidate: reentrant selection keeps only the newest resource', async () => { + let result + let reenter = false + const env = create({ onInit() { + if (reenter) { + reenter = false + result.tracks([]) + } + } }) + result = await env.factory(options)(env.art) + reenter = true + result.reset() + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-3']) + assert.equal(await env.latestText(), 'WEBVTT\n\n') + env.emit('destroy') + empty(env) +}) + +test('Multiple subtitles candidate: unknown names throw before allocation and do not corrupt later resets', async () => { + const env = create() + const result = await env.factory(options)(env.art) + assert.throws(() => result.tracks(['missing']), { name: 'TypeError' }) + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-1']) + result.reset() + assert.equal((env.utils.unescape(await env.latestText()).match(/
{ + const env = create({ onCreate() { + env.emit('destroy') + } }) + await env.factory(options)(env.art) + assert.equal(env.initialized.length, 0) + empty(env) +}) + +test('Multiple subtitles candidate: throwing live configuration preserves the old resource', async () => { + const env = create() + const result = await env.factory(options)(env.art) + const old = env.art.option.subtitle + const error = new Error('configuration getter failed') + Object.defineProperty(env.art.option, 'subtitle', { configurable: true, get() { + throw error + } }) + assert.throws(() => result.reset(), caught => caught === error) + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-1']) + Object.defineProperty(env.art.option, 'subtitle', { value: old }) + result.reset() + env.emit('destroy') + empty(env) +}) + +test('Multiple subtitles candidate: destroy from live configuration stops the following init', async () => { + const env = create() + const result = await env.factory(options)(env.art) + Object.defineProperty(env.art.option.subtitle, 'style', { enumerable: true, get() { + env.emit('destroy') + return {} + } }) + result.reset() + assert.equal(env.initialized.length, 1) + empty(env) +}) + +test('Multiple subtitles candidate: reentrant successful selection survives the outer host error', async () => { + let result + let reenter = false + const error = new Error('outer init failed') + const env = create({ onInit() { + if (reenter) { + reenter = false + result.tracks([]) + throw error + } + } }) + result = await env.factory(options)(env.art) + reenter = true + assert.throws(() => result.reset(), caught => caught === error) + assert.deepEqual([...env.liveBlobs.keys()], ['blob:subtitle-3']) + env.emit('destroy') + empty(env) +}) + +test('Multiple subtitles candidate: late host rejection after destroy is handled without another write', async () => { + const host = deferred() + const env = create({ onInit: () => host.promise }) + await env.factory(options)(env.art) + env.emit('destroy') + host.reject(new Error('late host failure')) + await flush() + assert.equal(env.warnings.length, 0) + assert.equal(env.initialized.length, 1) + assert.deepEqual(env.revoked, ['blob:subtitle-1']) + empty(env) +}) + +test('Multiple subtitles candidate: destroy from a URL getter stops the request before dispatch', async () => { + const env = create() + const entry = { name: 'a', get url() { + env.emit('destroy') + return 'a.vtt' + } } + await env.factory({ subtitles: [entry] })(env.art) + assert.equal(env.requests.length, 0) + assert.equal(env.initialized.length, 0) + empty(env) +}) diff --git a/test/multiple-subtitles-merge.test.js b/test/multiple-subtitles-merge.test.js new file mode 100644 index 000000000..3b4b87351 --- /dev/null +++ b/test/multiple-subtitles-merge.test.js @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict' +import { Buffer } from 'node:buffer' +// eslint-disable-next-line test/no-import-node-test -- Merge and decoder regressions use the repository Node runner. +import test from 'node:test' +import { loadModules } from './helpers/load.js' +import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, multipleSubtitlesHistorical, subtitleVtt } from './helpers/multiple-subtitles.js' + +const { parseTracks, serializeTracks, ...coreUtils } = await loadModules({ + parseTracks: { file: 'packages/artplayer-plugin-multiple-subtitles/src/merge', name: 'parseTracks' }, + serializeTracks: { file: 'packages/artplayer-plugin-multiple-subtitles/src/merge', name: 'serializeTracks' }, + getExt: { file: 'packages/artplayer/src/utils/file', name: 'getExt' }, + srtToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'srtToVtt' }, + assToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'assToVtt' }, +}) +const candidate = await multipleSubtitlesCandidate() +const historical = (await multipleSubtitlesHistorical()).find(item => item.name === 'published-1.2.0-main') +function freeze(value) { + if (value && typeof value === 'object') { + Object.freeze(value) + for (const child of Object.values(value)) freeze(child) + } + return value +} + +test('Multiple subtitles merge: repeated selection serializes frozen trees without accumulating wrappers', () => { + const trees = freeze(parseTracks([subtitleVtt('A'), subtitleVtt('B')], [{ name: 'a' }, { name: 'b' }])) + const original = JSON.stringify(trees) + const first = serializeTracks(trees) + serializeTracks([trees[1], trees[0], trees[1]]) + assert.equal(serializeTracks(trees), first) + assert.equal(JSON.stringify(trees), original) +}) + +test('Multiple subtitles merge: actual released serializer output is preserved for text, markup and timing cases', async () => { + const samples = [ + subtitleVtt('A & B < C > D'), + subtitleVtt('bold tail'), + subtitleVtt('color italic'), + subtitleVtt('voice'), + subtitleVtt('textnote'), + subtitleVtt('line one\nline two'), + 'WEBVTT\n\n00:02.000 --> 00:05.000\nLater\n\n00:00.000 --> 00:03.000\nEarlier\n', + subtitleVtt('best effort').replace('WEBVTT', 'BAD HEADER'), + 'WEBVTT\n\n', + ] + for (const body of samples) { + const environments = [historical, candidate].map(implementation => multipleSubtitlesEnvironment(implementation, { responses: { 'a.vtt': body, 'b.vtt': subtitleVtt('second') } })) + const results = await Promise.all(environments.map(env => env.factory({ subtitles: [{ url: 'a.vtt', name: 'a' }, { url: 'b.vtt', name: 'b' }] })(env.art))) + for (const selection of [null, ['b', 'a', 'b'], [], ['a']]) { + if (selection) + results.forEach(result => result.tracks(selection)) + assert.equal(await environments[1].latestText(), await environments[0].latestText(), body) + } + results.forEach(result => result.reset()) + assert.equal(await environments[1].latestText(), await environments[0].latestText(), body) + environments[1].emit('destroy') + } +}) + +test('Multiple subtitles request: real core SRT/ASS conversion and encoded text retain released output', async () => { + const responses = { + 'a.SRT?track=1': '1\n00:00:00,000 --> 00:00:02,000\nSRT text\n', + 'b.ass': 'Dialogue: 0,0:00:00.00,0:00:02.00,Default,,0,0,0,,ASS text', + 'c.vtt': Buffer.from(subtitleVtt('café'), 'latin1'), + 'd.vtt': Buffer.from(subtitleVtt('日本語'), 'utf16le'), + } + const options = { subtitles: [{ url: 'a.SRT?track=1', name: 'srt' }, { url: 'b.ass', name: 'ass' }, { url: 'c.vtt', name: 'latin', encoding: 'windows-1252' }, { url: 'd.vtt', name: 'utf16', encoding: 'utf-16le' }] } + const old = multipleSubtitlesEnvironment(historical, { responses, coreUtils }) + const current = multipleSubtitlesEnvironment(candidate, { responses, coreUtils }) + await old.factory(options)(old.art) + await current.factory(options)(current.art) + const text = await current.latestText() + assert.equal(text, await old.latestText()) + for (const value of ['SRT text', 'ASS text', 'café', '日本語']) assert(text.includes(value)) + current.emit('destroy') +}) diff --git a/test/multiple-subtitles.test.js b/test/multiple-subtitles.test.js index a3f223e00..f2aca3557 100644 --- a/test/multiple-subtitles.test.js +++ b/test/multiple-subtitles.test.js @@ -64,11 +64,11 @@ for (const implementation of implementations) { assert.equal(env.initialized[0].onVttLoad, env.utils.unescape) assert.equal(env.initialized[0].style, env.art.option.subtitle.style) assert.equal(env.initialized[0].type, 'vtt') - assert.deepEqual(env.revoked, ['']) + assert.deepEqual(env.revoked, implementation.version === 'candidate' ? [] : ['']) env.art.option.subtitle.encoding = 'utf-16le' result.reset() assert.equal(env.initialized[1].encoding, 'utf-16le') - assert.deepEqual(env.revoked, ['', 'blob:subtitle-1']) + assert.deepEqual(env.revoked, implementation.version === 'candidate' ? ['blob:subtitle-1'] : ['', 'blob:subtitle-1']) assert.equal(env.blobs.get('blob:subtitle-2').type, 'text/vtt') })