subtitle parsing and display finished, TODO: filetype and subtitle verification, multi-track support, multi-video support

This commit is contained in:
ThaUnknown 2020-09-09 21:36:10 +02:00
parent 0b323ff2cd
commit e88bb172c3
4 changed files with 254 additions and 23 deletions

View file

@ -88,6 +88,21 @@ video[src=""] {
display: none;
}
video::cue {
font-family: Roboto, monospace;
color: #fff;
text-shadow: 2px 2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
-2px -2px 0 #000,
2px 0px 0 #000,
0px 2px 0 #000,
-2px 0px 0 #000,
0px -2px 0 #000,
2px 2px 2px #000;
background: none;
}
.controls {
background: linear-gradient(to top, rgba(0, 0, 0, .8), rgba(0, 0, 0, .4) 25%, rgba(0, 0, 0, .2) 50%, rgba(0, 0, 0, .1) 75%, transparent);
}
@ -183,11 +198,11 @@ video[src=""] {
.controls input#prog[type=range]::after,
.controls input#prog[type=range]::before{
.controls input#prog[type=range]::before {
pointer-events: none;
opacity: 0;
position: absolute;
transform: translate(-50%,-100%);
transform: translate(-50%, -100%);
font-family: Roboto, monospace;
color: #ececec;
white-space: nowrap;
@ -197,21 +212,25 @@ video[src=""] {
font-weight: 600;
transition: .2s opacity ease;
}
.controls input#prog[type=range]::after{
.controls input#prog[type=range]::after {
width: 150px;
background: var(--background);
content: "";
height: var(--height);
top: -2rem;
}
.controls input#prog[type=range]::before{
.controls input#prog[type=range]::before {
top: 0rem;
content: attr(data-ts);
}
.controls input#prog[type=range]:active::after,
.controls input#prog[type=range]:active::before{
.controls input#prog[type=range]:active::before {
opacity: 1;
}
.controls input[type=range]:hover::-webkit-slider-thumb {
height: 12px;
width: 12px;
@ -229,7 +248,7 @@ video[src=""] {
transition: all .1s ease
}
#buffering{
#buffering {
border: 4px solid #FFFFFF00;
border-top: 4px solid #fff;
border-radius: 50%;
@ -239,7 +258,8 @@ video[src=""] {
opacity: 1;
transition: .5s opacity ease;
}
#buffering.hidden{
#buffering.hidden {
opacity: 0
}

View file

@ -1,11 +1,125 @@
function parseSubtitles(stream) {
parser = new MatroskaSubtitles()
parser.once('tracks', function (tracks) {
console.log(tracks)
})
parser.on('subtitle', function (subtitle, trackNumber) {
console.log('Track ' + trackNumber + ':', subtitle)
})
console.log(stream)
stream.pipe(parser)
var parser = new MatroskaSubtitles()
let track
parser.once('tracks', function (tracks) {
track = video.addTextTrack('captions', tracks[0].type, tracks[0].language)
track.mode = "showing";
})
parser.on('subtitle', function (subtitle, trackNumber) {
subConvt(subtitle)
})
var re_newline = /\\N/g; // replace \N with newline
var re_softbreak = /\\n/g; // There's no equivalent function in WebVTT.
var re_hardspace = /\\h/g; // Replace with  
var re_style = /\{([^}]+)\}/; // replace style
function subConvt(result) {
let cue = new VTTCue(result.time / 1000, (result.time + result.duration) / 1000, ""),
text = result.text;
// Support for special characters in WebVTT.
// For obvious reasons, the ampersand one *must* be first.
text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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.
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));
if (Math.floor((posNum - 1) / 3) == 1) {
cue.line = 0.5;
} else if (Math.floor((posNum - 1) / 3) == 2) {
cue.line = 0;
}
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));
if (posNum > 8) {
cue.line = 0.5;
} else if (posNum > 4) {
cue.line = 0;
}
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 += '</' + nowClosing + '>';
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 += '</' + tagsToClose.pop() + '>';
}
}
// 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, "&nbsp;");
let content = "<v " + result.style + ">" + text
while (tagsToClose.length > 0) {
content += '</' + tagsToClose.pop() + '>';
}
cue.text = `&nbsp;\r\n${content}\r\n&nbsp;`
track.addCue(cue)
}

94
app/subtitletest2.js Normal file
View file

@ -0,0 +1,94 @@
class ParsedSubtitle {
constructor(subtitle, tracknumber) {
this._trackN = tracknumber
this._subtitle = subtitle
this._time = subtitle.time
this._duration = subtitle.duration
this._text = subtitle.text
this._cue = new VTTCue(subtitle.time / 1000, (subtitle.time + subtitle.duration) / 1000, subtitle.text)
}
get trackNumber() {
return this._trackN
}
get raw() {
return this._subtitle
}
get time() {
return this._time
}
get duration() {
return this._duration
}
get end() {
return this._time + this._duration
}
get text() {
return this._text
}
get vttcue() {
return this._cue
}
get hashid() {
if (this._hash !== undefined) {
return this_hash
}
let result = 17
result = 31 * Math.trunc(this._time)
result = 31 * Math.trunc(this._duration)
return this._hash = result
}
}
class SubtitleHandler {
constructor(parser) {
this._parser = parser
this._subs = []
}
add(sub) {
if (!this._subs.some(item => item.hashid === sub)) {
this._subs.add(sub)
}
}
}
function setSubsTrack(track) {
if (track !== undefined) {
track.mode = "showing"
window._texttrack = track
}
}
function getSubsTrack(callback) {
if (window._texttrack !== undefined && typeof callback === "function") {
callback(window._texttrack)
}
}
parser.once('tracks', function (tracks) {
console.log(tracks)
})
parser.on('subtitle', function (subtitle, trackNumber) {
console.log('Track ' + trackNumber + ':', subtitle)
const sub = new ParsedSubtitle(subtitle, trackNumber)
getSubsTrack(track => {
track.addCue(sub.vttcue)
})
})

View file

@ -34,6 +34,9 @@ function t(a) {
case 2:
addTorrent("https://webtorrent.io/torrents/tears-of-steel.torrent")
break;
case 3:
addTorrent("magnet:?xt=urn:btih:CE9156EB497762F8B7577B71C0647A4B0C3423E1&dn=Inception+%282010%29+720p+-+mkv+-+1.0GB+-+YIFY&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2F9.rarbg.to%3A2920%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.pirateparty.gr%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce")
break;
}
}
WEBTORRENT_ANNOUNCE = announceList
@ -87,10 +90,10 @@ function addTorrent(magnet) {
})
video.src = `${scope}webtorrent/${torrent.infoHash}/${encodeURI(videoFile.path)}`
// createSubParser()
// if (subStream){
// subStream.destroy()
// }
// subStream = videoFile.createReadStream().pipe(parser)
if (subStream){
subStream.destroy()
}
subStream = videoFile.createReadStream().pipe(parser)
document.location.href = "#player"
nowPlaying(selected)
halfmoon.toggleModal("tsearch")
@ -128,8 +131,8 @@ function serveFile(file, req) {
res.body = req.method === 'HEAD' ? '' : 'stream'
parseSubtitles(file.createReadStream(range))
// file.createReadStream(range).pipe(parser)
return [res, req.method === 'GET' && file.createReadStream(range)]
}