From f622218d31dbc866947acf7eb367085eadeb62bd Mon Sep 17 00:00:00 2001 From: ThaUnknown Date: Thu, 15 Apr 2021 00:47:52 +0200 Subject: [PATCH] fixes, auto-detect and convert external subtitle files --- app/js/player.js | 130 ++++++++++++++++++----------- app/js/subtitles-octopus-worker.js | 112 ++++++++++++------------- 2 files changed, 138 insertions(+), 104 deletions(-) diff --git a/app/js/player.js b/app/js/player.js index 215b363..221060a 100644 --- a/app/js/player.js +++ b/app/js/player.js @@ -74,6 +74,7 @@ class TorrentPlayer extends WebTorrent { this.player.addEventListener('fullscreenchange', () => this.updateFullscreen()) this.controls.ppToggle.addEventListener('dblclick', () => this.toggleFullscreen()) + this.video.addEventListener('loadedmetadata', () => this.findSubtitleFiles(this.currentFile)) this.subtitleData = { fonts: ['https://fonts.gstatic.com/s/roboto/v20/KFOlCnqEu92Fr1MmEU9fBBc4.woff2'], headers: [], @@ -170,7 +171,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000 } if ('setPositionState' in navigator.mediaSession) this.video.addEventListener('timeupdate', () => this.updatePositionState()) - this.subtitleExtensions = ['.srt', '.ass', '.vtt'] + this.subtitleExtensions = ['.srt', '.vtt', '.ass', '.ssa'] this.videoExtensions = ['.3g2', '.3gp', '.asf', '.avi', '.dv', '.flv', '.gxf', '.m2ts', '.m4a', '.m4b', '.m4p', '.m4r', '.m4v', '.mkv', '.mov', '.mp4', '.mpd', '.mpeg', '.mpg', '.mxf', '.nut', '.ogm', '.ogv', '.swf', '.ts', '.vob', '.webm', '.wmv', '.wtv'] this.videoFiles = undefined @@ -679,10 +680,10 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000 const label = document.createElement('label') input.name = `${type}-radio-set` input.type = 'radio' - input.id = type === 'captions' ? `${type}-${track?.number || 'off'}-radio` : `${type}-${track.id}-radio` - input.value = type === 'captions' ? track?.number || null : track.id + input.id = type === 'captions' ? `${type}-${track ? track.number : 'off'}-radio` : `${type}-${track.id}-radio` + input.value = type === 'captions' ? track ? track.number : -1 : track.id input.checked = type === 'captions' ? track?.number === this.subtitleData.current : track.enabled - label.htmlFor = type === 'captions' ? `${type}-${track?.number || 'off'}-radio` : `${type}-${track.id}-radio` + label.htmlFor = type === 'captions' ? `${type}-${track ? track.number : 'off'}-radio` : `${type}-${track.id}-radio` label.textContent = track ? type === 'captions' ? (track.language || (!Object.values(this.subtitleData.headers).some(header => header.language === 'eng' || header.language === 'en') ? 'eng' : header.type)) + (track.name ? ' - ' + track.name : '') @@ -714,7 +715,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000 this.subtitleData.timeout = setTimeout(() => { this.subtitleData.timeout = undefined if (this.subtitleData.renderer) { - this.subtitleData.renderer.setTrack(trackNumber ? this.subtitleData.headers[trackNumber].header.slice(0, -1) + Array.from(this.subtitleData.tracks[trackNumber]).join('\n') : this.subtitleData.headers[3].header.slice(0, -1)) + this.subtitleData.renderer.setTrack(trackNumber !== -1 ? this.subtitleData.headers[trackNumber].header.slice(0, -1) + Array.from(this.subtitleData.tracks[trackNumber]).join('\n') : this.subtitleData.defaultHeader) } }, 1000) } @@ -761,7 +762,6 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000 this.subtitleData.parser.destroy() this.selectCaptions(this.subtitleData.current) parser = undefined - this.controls.captionsButton.removeAttribute('disabled') if (!this.video.paused) { this.video.pause() this.playVideo() @@ -770,7 +770,6 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000 }) console.log('Sub parsing started') this.subtitleData.parser = file.createReadStream().pipe(parser) - // when this gets overwritten the parser stays so it might "leak" some RAM??? } else { resolve() } @@ -779,7 +778,6 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000 handleSubtitleParser (parser, skipFile) { parser.once('tracks', tracks => { - this.controls.captionsButton.removeAttribute('disabled') tracks.forEach(track => { if (!this.subtitleData.tracks[track.number]) { // overwrite webvtt or other header with custom one @@ -828,58 +826,94 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000 } } if (!this.subtitleData.renderer) { - console.log() this.subtitleData.renderer = new SubtitlesOctopus(options) this.selectCaptions(this.subtitleData.current) + this.controls.captionsButton.removeAttribute('disabled') } } } - convertFile (file) { - const regex = /^(?:\d+\n)?(\S{9,12})\s?-->\s?(\S{9,12})(.*)\n([\s\S]*)$/i - const subtitles = [] - - for (const split of fileContent.split('\n\n')) { - match = split.match(regex) - if (match) { - if (match[1].length === 9) { - match[1] = '0:' + match[1] - } else { - if (match[1][0] === '0') { - match[1].substring(1) - } - } - match[1].replace(',', '.') - if (match[2].length === 9) { - match[2] = '0:' + match[2] - } else { - if (match[2][0] === '0') { - match[2].substring(1) - } - } - match[2].replace(',', '.') - const matches = match[4].match(/<[^>]+>/g) // create array of all tags - if (matches) { - matches.forEach(matched => { - if (/<\//.test(matched)) { // check if its a closing tag - match[4] = match[4].replace(matched, matched.replace('', '0}')) + convertSubFile (file, isAss, callback) { + const regex = /(?:\d+\n)?(\S{9,12})\s?-->\s?(\S{9,12})(.*)\n([\s\S]*)$/i + file.getBuffer((_err, buffer) => { + const subtitles = isAss ? buffer.toString() : [] + if (isAss) { + callback(subtitles) + } else { + for (const split of buffer.toString().split('\n\n')) { + const match = split.match(regex) + if (match) { + match[1] = match[1].match(/.*[.,]\d{2}/)[0] + match[2] = match[2].match(/.*[.,]\d{2}/)[0] + if (match[1].length === 9) { + match[1] = '0:' + match[1] } else { - match[4] = match[4].replace(matched, matched.replace('<', '{\\').replace('>', '1}')) + if (match[1][0] === '0') { + match[1] = match[1].substring(1) + } } - }) + match[1].replace(',', '.') + if (match[2].length === 9) { + match[2] = '0:' + match[2] + } else { + if (match[2][0] === '0') { + match[2] = match[2].substring(1) + } + } + match[2].replace(',', '.') + const matches = match[4].match(/<[^>]+>/g) // create array of all tags + if (matches) { + matches.forEach(matched => { + if (/<\//.test(matched)) { // check if its a closing tag + match[4] = match[4].replace(matched, matched.replace('', '0}')) + } else { + match[4] = match[4].replace(matched, matched.replace('<', '{\\').replace('>', '1}')) + } + }) + } + subtitles.push('Dialogue: 0,' + match[1].replace(',', '.') + ',' + match[2].replace(',', '.') + ',Default,,0,0,0,,' + match[4]) + } } - subtitles.push('Dialogue: 1,' + match[1] + ',' + match[2] + ',Default,,0,0,0,,' + match[4]) + callback(subtitles) } - } - return subtitles + }) } - findSubtitles (targetFile) { + findSubtitleFiles (targetFile) { const path = targetFile.path.split(targetFile.name)[0] - const subtitleFiles = [] - for (const file of targetFile.torrent_.files.filter(file => this.subtitleExtensions.some(ext => file.name.endsWith(ext)))) { - const split = file.split(path) - if (split.length === 2) subtitleFiles.push(file) + // array of subtitle files that match video name, or all subtitle files when only 1 vid file + const subtitleFiles = targetFile._torrent.files.filter(file => { + return this.subtitleExtensions.some(ext => file.name.endsWith(ext)) && (this.videoFiles.length === 1 ? true : file.path.split(path).length === 2) + }) + if (subtitleFiles.length) { + this.createRadioElement(undefined, 'captions') + this.subtitleData.parsed = true + this.subtitleData.current = 0 + for (const [index, file] of subtitleFiles.entries()) { + const isAss = file.name.endsWith('.ass') || file.name.endsWith('.ssa') + const extension = /\.(\w+)$/ + const name = file.name.replace(targetFile.name, '') === file.name + ? file.name.replace(targetFile.name.replace(extension, ''), '').slice(0, -4).replace(/[,._-]/g, ' ').trim() + : file.name.replace(targetFile.name, '').slice(0, -4).replace(/[,._-]/g, ' ').trim() + const header = { + header: this.subtitleData.defaultHeader, + language: name, + number: index, + type: file.name.match(extension)[1] + } + this.subtitleData.headers.push(header) + this.subtitleData.tracks[index] = [] + this.createRadioElement(header, 'captions') + this.convertSubFile(file, isAss, subtitles => { + if (isAss) { + this.subtitleData.headers[index].header = subtitles + } else { + this.subtitleData.tracks[index] = subtitles + } + if (this.subtitleData.current === index) this.selectCaptions(this.subtitleData.current) + }) + this.initSubtitleRenderer() + } } } @@ -1052,7 +1086,7 @@ const client = new TorrentPlayer({ }) }, onWatched: () => { - if (client.nowPlaying.media?.episodes || client.nowPlaying.media?.nextAiringEpisode?.episode) { + if (client.nowPlaying?.media?.episodes || client.nowPlaying?.media?.nextAiringEpisode?.episode) { if (settings.other2 && (client.nowPlaying.media?.episodes || client.nowPlaying.media?.nextAiringEpisode?.episode > client.nowPlaying.episodeNumber)) { alEntry() } else { diff --git a/app/js/subtitles-octopus-worker.js b/app/js/subtitles-octopus-worker.js index 11e07f2..07c1873 100644 --- a/app/js/subtitles-octopus-worker.js +++ b/app/js/subtitles-octopus-worker.js @@ -2877,7 +2877,7 @@ if ('now' in self.performance === false) { table[i] = new HuffmanCode(0, 0) } ReadHuffmanCode(num_htrees + max_run_length_prefix, table, 0, br) - for (i = 0; i < context_map_size; ) { + for (i = 0; i < context_map_size;) { var code br.readMoreInput() code = ReadSymbol(table, 0, br) @@ -5051,17 +5051,17 @@ var Browser = { typeof MozBlobBuilder !== 'undefined' ? MozBlobBuilder : typeof WebKitBlobBuilder !== 'undefined' - ? WebKitBlobBuilder - : !Browser.hasBlobConstructor - ? console.log('warning: no BlobBuilder') - : null + ? WebKitBlobBuilder + : !Browser.hasBlobConstructor + ? console.log('warning: no BlobBuilder') + : null Browser.URLObject = typeof window !== 'undefined' ? window.URL - ? window.URL - : window.webkitURL + ? window.URL + : window.webkitURL : undefined - if (!Module.noImageDecoding && typeof Browser.URLObject === 'undefined') { + if (!Module.noImageDecoding && !typeof Browser.URLObject === 'undefined') { console.log( 'warning: Browser does not support creating object URLs. Built-in browser image decoding will not be available.' ) @@ -8308,14 +8308,14 @@ var SYSCALLS = { ((tempDouble = stat.size), +Math_abs(tempDouble) >= 1 ? tempDouble > 0 - ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> + ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 - : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> + : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ]), - (HEAP32[(buf + 40) >> 2] = tempI64[0]), - (HEAP32[(buf + 44) >> 2] = tempI64[1]) + (HEAP32[(buf + 40) >> 2] = tempI64[0]), + (HEAP32[(buf + 44) >> 2] = tempI64[1]) HEAP32[(buf + 48) >> 2] = 4096 HEAP32[(buf + 52) >> 2] = stat.blocks HEAP32[(buf + 56) >> 2] = (stat.atime.getTime() / 1e3) | 0 @@ -8329,14 +8329,14 @@ var SYSCALLS = { ((tempDouble = stat.ino), +Math_abs(tempDouble) >= 1 ? tempDouble > 0 - ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> + ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 - : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> + : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ]), - (HEAP32[(buf + 80) >> 2] = tempI64[0]), - (HEAP32[(buf + 84) >> 2] = tempI64[1]) + (HEAP32[(buf + 80) >> 2] = tempI64[0]), + (HEAP32[(buf + 84) >> 2] = tempI64[1]) return 0 }, doMsync: function (addr, stream, len, flags, offset) { @@ -8558,41 +8558,41 @@ function ___sys_getdents64 (fd, dirp, count) { type = FS.isChrdev(child.mode) ? 2 : FS.isDir(child.mode) - ? 4 - : FS.isLink(child.mode) - ? 10 - : 8 + ? 4 + : FS.isLink(child.mode) + ? 10 + : 8 } ;(tempI64 = [ id >>> 0, ((tempDouble = id), +Math_abs(tempDouble) >= 1 ? tempDouble > 0 - ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | + ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 - : ~~+Math_ceil( - (tempDouble - +(~~tempDouble >>> 0)) / 4294967296 - ) >>> 0 + : ~~+Math_ceil( + (tempDouble - +(~~tempDouble >>> 0)) / 4294967296 + ) >>> 0 : 0) ]), - (HEAP32[(dirp + pos) >> 2] = tempI64[0]), - (HEAP32[(dirp + pos + 4) >> 2] = tempI64[1]) + (HEAP32[(dirp + pos) >> 2] = tempI64[0]), + (HEAP32[(dirp + pos + 4) >> 2] = tempI64[1]) ;(tempI64 = [ ((idx + 1) * struct_size) >>> 0, ((tempDouble = (idx + 1) * struct_size), +Math_abs(tempDouble) >= 1 ? tempDouble > 0 - ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | + ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 - : ~~+Math_ceil( - (tempDouble - +(~~tempDouble >>> 0)) / 4294967296 - ) >>> 0 + : ~~+Math_ceil( + (tempDouble - +(~~tempDouble >>> 0)) / 4294967296 + ) >>> 0 : 0) ]), - (HEAP32[(dirp + pos + 8) >> 2] = tempI64[0]), - (HEAP32[(dirp + pos + 12) >> 2] = tempI64[1]) + (HEAP32[(dirp + pos + 8) >> 2] = tempI64[0]), + (HEAP32[(dirp + pos + 12) >> 2] = tempI64[1]) HEAP16[(dirp + pos + 16) >> 1] = 280 HEAP8[(dirp + pos + 18) >> 0] = type stringToUTF8(name, dirp + pos + 19, 256) @@ -8983,10 +8983,10 @@ function _fd_fdstat_get (fd, pbuf) { const type = stream.tty ? 2 : FS.isDir(stream.mode) - ? 3 - : FS.isLink(stream.mode) - ? 7 - : 4 + ? 3 + : FS.isLink(stream.mode) + ? 7 + : 4 HEAP8[pbuf >> 0] = type return 0 } catch (e) { @@ -9022,14 +9022,14 @@ function _fd_seek (fd, offset_low, offset_high, whence, newOffset) { ((tempDouble = stream.position), +Math_abs(tempDouble) >= 1 ? tempDouble > 0 - ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> + ? (Math_min(+Math_floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 - : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> + : ~~+Math_ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0) ]), - (HEAP32[newOffset >> 2] = tempI64[0]), - (HEAP32[(newOffset + 4) >> 2] = tempI64[1]) + (HEAP32[newOffset >> 2] = tempI64[0]), + (HEAP32[(newOffset + 4) >> 2] = tempI64[1]) if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null return 0 } catch (e) { @@ -12816,7 +12816,7 @@ self.setIsPaused = function (isPaused) { self.offscreenRender = function (force) { self.rafId = 0 self.renderPending = false - const startTime = performance.now() + // const startTime = performance.now() const result = self.octObj.renderImage( self.getCurrentTime() + self.delay, self.changed @@ -12824,16 +12824,16 @@ self.offscreenRender = function (force) { const changed = Module.getValue(self.changed, 'i32') if ((Number(changed) !== 0 || force) && self.offscreenCanvas) { const images = self.buildResultImage(result) - const newTime = performance.now() - const libassTime = newTime - startTime + // const newTime = performance.now() + // const libassTime = newTime - startTime const promises = [] for (let i = 0; i < images.length; i++) { promises[i] = createImageBitmap(images[i].image) } Promise.all(promises).then(function (bitmaps) { - const decodeTime = performance.now() - newTime + // const decodeTime = performance.now() - newTime function renderFastFrames () { - const beforeDrawTime = performance.now() + // const beforeDrawTime = performance.now() self.offscreenCanvasCtx.clearRect( 0, 0, @@ -12847,17 +12847,17 @@ self.offscreenRender = function (force) { images[i].y ) } - const drawTime = performance.now() - beforeDrawTime - console.log( - bitmaps.length + - ' bitmaps, libass: ' + - libassTime + - 'ms, decode: ' + - decodeTime + - 'ms, draw: ' + - drawTime + - 'ms' - ) + // const drawTime = performance.now() - beforeDrawTime + // console.log( + // bitmaps.length + + // ' bitmaps, libass: ' + + // libassTime + + // 'ms, decode: ' + + // decodeTime + + // 'ms, draw: ' + + // drawTime + + // 'ms' + // ) } self.requestAnimationFrame(renderFastFrames) })