let tracks = [], headers = [], subtitleStream const { SubtitleStream } = MatroskaSubtitles function parseSubs(stream) { if (video.src.endsWith(".mkv")) { if (subtitleStream) { subtitleStream = new SubtitleStream(subtitleStream) } else { subtitleStream = new SubtitleStream() // subtitleStream.once('tracks', pTracks => { // pTracks.forEach(track => { // tracks[track.number] = video.addTextTrack('captions', track.type, track.language); // parseHeader(track.header, track.number); // let spacerCue = new VTTCue(0.1, 9999, " ") // spacerCue.line = -1 // tracks[track.number].addCue(spacerCue) // }) // if (video.textTracks[0]) { // video.textTracks[0].mode = "showing" // displayHeader(headers[3]) // } // }) } subtitleStream.on('subtitle', function (subtitle, trackNumber) { // subConvt(subtitle, trackNumber) console.log(subtitle) }) stream.pipe(subtitleStream) } } const re_newline = /\\N/g, // replace \N with newline re_softbreak = /\\n/g, // There's no equivalent function in WebVTT. re_hardspace = /\\h/g, // Replace with   re_style = /\{([^}]+)\}/, // replace style re_header_format = /(?<=\[V4\+? Styles\][\s\S]*Format: [^\n]*)(.+)/i, re_header_style = /(?<=\[V4\+? Styles\][\s\S]*Style: [^\n]*)(.+)/ig; function subConvt(result, trackNumber) { let cue = new VTTCue(result.time / 1000, (result.time + result.duration) / 1000, ""), text = result.text, positioned result.style = result.style.replace(/\ /g, "") if (tracks[trackNumber].label == "ass") { // Support for special characters in WebVTT. // For obvious reasons, the ampersand one *must* be first. text = text.replace(/&/g, "&").replace(//g, ">"); let style, tagsToClose = []; // Places to stash style info. // Subtitles may contain any number of override tags, so we'll loop through // to find them all. while ((style = text.match(re_style))) { let tagsToOpen = [], replaceString = ''; if (style[1] && style[1].split) { // Stop throwing errors on empty tags. style = style[1].split("\\"); // Get an array of override commands. for (let j = 1; j < style.length; j++) { // Extract the current tag name. if (style[j]) { let tagCommand = style[j].match(/[a-zA-Z]+/)[0] // Give special reckognition to one-letter tags. let oneLetter = (tagCommand.length == 1) ? tagCommand : ""; // "New" position commands. It is assumed that bottom center position is the default. if (tagCommand === "an") { let posNum = Number(style[j].substring(2, 3)) positioned = 1 if (Math.floor((posNum - 1) / 3) == 1) { cue.line = 8; } else if (Math.floor((posNum - 1) / 3) == 2) { cue.line = 1; } else { cue.line = -1; } if (posNum % 3 == 1) { cue.align = "start"; } else if (posNum % 3 == 0) { cue.align = "end"; } // Legacy position commands. } else if (oneLetter === "a" && !Number.isNaN(Number(style[j].substring(1, 2)))) { let posNum = Number(style[j].substring(1, 2)); positioned = 1 if (posNum > 8) { cue.line = 8; } else if (posNum > 4) { cue.line = 1; } else { cue.line = -1; } if ((posNum - 1) % 4 == 0) { cue.align = "start"; } else if ((posNum - 1) % 4 == 2) { cue.align = "end"; } // Map simple text decoration commands to equivalent WebVTT text tags. // NOTE: Strikethrough (the 's' tag) is not supported in WebVTT. } else if (['b', 'i', 'u', 's'].includes(oneLetter)) { if (Number(style[j].substring(1, 2)) === 0 // The more elaborate 'b-tag', which we will treat as an on-off selector. || (style[j].match(/b\d{3}/) && Number(style[j].match(/b(\d{3})/)[1]) < 500) ) { // Closing a tag. if (tagsToClose.includes(oneLetter)) { // Nothing needs to be done if this tag isn't already open. // HTML tags must be nested, so we must ensure that any tag nested inside // the tag being closed are also closed, and then opened again once the // current tag is closed. while (tagsToClose.length > 0) { let nowClosing = tagsToClose.pop(); replaceString += ''; if (nowClosing !== oneLetter) { tagsToOpen.push(nowClosing); } else { // There's no need to close the tags that the current tag // is nested within. break; } } } } else { // Opening a tag. if (!tagsToClose.includes(oneLetter)) { // Nothing needs to be done if the tag is already open. // If no, place the tag on the bottom of the stack of tags being opened. tagsToOpen.splice(0, 0, oneLetter); } } } else if (oneLetter === 'r') { // Resetting override tags, by closing all open tags. // TODO: The 'r' tag can also be used to switch to a different named style, // however, named styles haven't been implemented. while (tagsToClose.length > 0) { replaceString += ''; } } // Insert open-tags for tags in the to-open list. while (tagsToOpen.length > 0) { let nowOpening = tagsToOpen.pop(); replaceString += '<' + nowOpening + '>'; tagsToClose.push(nowOpening); } } } } text = text.replace(re_style, replaceString); // Replace override tag. } text = text.replace(re_newline, "\r\n").replace(re_softbreak, " ").replace( re_hardspace, " "); let content = "" + text while (tagsToClose.length > 0) { content += ''; } if (!positioned && headers[trackNumber].styles[result.style][headers[trackNumber].format.indexOf("Alignment")]) { let posNum = Number(headers[trackNumber].styles[result.style][headers[trackNumber].format.indexOf("Alignment")]); if (Math.floor((posNum - 1) / 3) == 1) { cue.line = 8; } else if (Math.floor((posNum - 1) / 3) == 2) { cue.line = 1; } else { cue.line = -1 } if (posNum % 3 == 1) { cue.align = "start"; } else if (posNum % 3 == 0) { cue.align = "end"; } } cue.text = content; } else { cue.text = text; cue.line = -1 } if (!Object.values(tracks[trackNumber].cues).some(c => c.text == cue.text && c.startTime == cue.startTime && c.endTime == cue.endTime)) { tracks[trackNumber].addCue(cue) } } function parseHeader(header, number) { headers[number] = { format: re_header_format.exec(header)[1].replace(/\ /g, "").split(","), styles: { } } header.match(re_header_style).forEach((item, index) => { item = item.replace(/\&h/gi, "").replace(/\ /g, "").split(",") headers[number].styles[item[headers[number].format.indexOf("Name")]] = item }) } function displayHeader(header) { substyles.innerHTML = "" for (let style of Object.values(header.styles)) { let bordCol style[header.format.indexOf("OutlineColour")] ? bordCol = style[header.format.indexOf("OutlineColour")].match(/[\s\S]{1,2}/g).reverse().join("").slice(0, -2) : "#000" substyles.innerHTML += ` video::cue(.${style[header.format.indexOf("Name")]}) { color: #${style[header.format.indexOf("PrimaryColour")] ? style[header.format.indexOf("PrimaryColour")].match(/[\s\S]{1,2}/g).reverse().join("").slice(0, -2) : ""} !important; text-shadow: 2px 2px 0 #${bordCol}, 2px -2px 0 #${bordCol}, -2px 2px 0 #${bordCol}, -2px -2px 0 #${bordCol}, 2px 0px 0 #${bordCol}, 0px 2px 0 #${bordCol}, -2px 0px 0 #${bordCol}, 0px -2px 0 #${bordCol}, 2px 2px 2px #${bordCol}; font-weight: ${style[header.format.indexOf("Bold")] ? style[header.format.indexOf("Bold")] * -1 ? "400" : "300" : ""} !important; font-style: ${style[header.format.indexOf("Italic")] ? style[header.format.indexOf("Italic")] * -1 ? "italic" : "normal" : ""} !important; background: ${style[header.format.indexOf("BorderStyle")] ? style[header.format.indexOf("BorderStyle")] != 3 ? "none" : `#${bordCol}` : ""} !important; }` } } // font-weight: ${style[header.format.indexOf("Bold")] ? style[header.format.indexOf("Bold")] * -1 ? "bold" : "normal" : ""} !important;