UNPKG

suptitles

Version:

Renders Blu-ray subtitles in the browser

180 lines (179 loc) 7.25 kB
import { BaseSegment, PresentationCompositionSegment, WindowDefinitionSegment, PaletteDefinitionSegment, ObjectDefinitionSegment, a2h2i, } from './segments.js'; import { getRgb, getPxAlpha } from './imageParser.js'; export default class SUPtitles { constructor(video, link) { this.offset = 0; this.timeout = null; this.lastPalette = null; this.cv = []; this.canvasSizeSet = false; this.playHandler = () => { this.offset = 0; this.cv.map(c => c.getContext('2d').clearRect(0, 0, c.width, c.height)); this.start(); }; this.pauseHandler = () => { clearTimeout(this.timeout); }; console.info('# SUP Starting'); this.video = video; video.addEventListener('play', this.playHandler); video.addEventListener('pause', this.pauseHandler); const canvas = document.createElement('canvas'); canvas.height = 1080; canvas.width = 1920; canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.top = '0'; canvas.style.left = '0'; canvas.style.position = 'absolute'; canvas.style.pointerEvents = 'none'; video.parentNode.appendChild(canvas); this.cv.push(canvas); fetch(link) .then(response => response.arrayBuffer()) .then(buffer => { this.file = new Uint8Array(buffer); console.info('# SUP Ready'); }); } dispose() { clearTimeout(this.timeout); this.timeout = null; this.video.removeEventListener('play', this.playHandler); this.video.removeEventListener('pause', this.pauseHandler); this.file = null; this.offset = null; this.lastPalette = null; this.video = null; this.cv.map(c => (c.outerHTML = '')); this.cv = null; console.info('# SUP Disposed'); } videoTime() { return this.video.currentTime * 1000; } start() { if (this.offset === 0) { while (this.offset < this.file.length) { const pts = a2h2i(this.file, this.offset + 2, this.offset + 6) / 90; const size = 13 + a2h2i(this.file, this.offset + 11, this.offset + 13); const type = a2h2i(this.file, this.offset + 10, this.offset + 11); if (pts > this.videoTime() && type === 22) { break; } else { this.offset += size; } } this.getNextSubtitle(); } } getNextSubtitle() { if (this.offset < this.file.length) { let ended = false; let PCS; let WDS; let PDS; let ODS = []; while (!ended) { const size = 13 + a2h2i(this.file, this.offset + 11, this.offset + 13); const bytes = this.file.slice(this.offset, this.offset + size); const base = new BaseSegment(bytes); switch (base.type) { case 'PCS': PCS = new PresentationCompositionSegment(base); if (!this.canvasSizeSet) { this.cv.map(c => { c.height = PCS.height; c.width = PCS.width; return null; }); this.canvasSizeSet = true; } break; case 'WDS': WDS = new WindowDefinitionSegment(base); break; case 'PDS': PDS = new PaletteDefinitionSegment(base); this.lastPalette = PDS.palette; break; case 'ODS': ODS.push(new ObjectDefinitionSegment(base)); break; case 'END': ended = true; break; default: throw new Error('InvalidSegmentError'); } this.offset += size; } this.timeout = setTimeout(() => { PDS || this.lastPalette ? this.draw(PCS, WDS, PDS, ODS) : console.log('# SUP SKIPPING, NO PALETTE'); this.getNextSubtitle(); }, PCS.base.pts - this.videoTime()); } } draw(PCS, WDS, PDS, ODS) { if (ODS.length > 0) { // DRAW let first = null; ODS.map(o => { if (o.type === 'First') { first = o; } else { let imgData = o.imgData; if (first) { imgData = Uint8Array.from([ ...[].slice.call(first.imgData), ...[].slice.call(o.imgData), ]); } const width = first ? first.width : o.width; const height = first ? first.height : o.height; const object = PCS.getObjectById(first ? first.id : o.id); const xOffset = object.xOffset; const yOffset = object.yOffset; const pixels = this.getPixels(imgData, PDS ? PDS.palette : this.lastPalette, width, height); this.cv[0] // object.windowId .getContext('2d') .putImageData(new ImageData(pixels, width, height), xOffset, yOffset); first = null; } return null; }); } else { // ERASE WDS.windows.map((w) => { if (PCS.windowObjects.length === 0 || (PCS.windowObjects.length && !PCS.getObjectByWindowId(w.windowId))) { this.cv[0] // w.windowId .getContext('2d') .putImageData(new ImageData(new Uint8ClampedArray(w.width * w.height * 4), w.width, w.height), w.xOffset, w.yOffset); } return null; }); } } getPixels(imgData, palette, width, height) { const rgb = getRgb(palette); let [pxMx1, alphaMx1] = getPxAlpha(imgData, palette); const pxls = new Uint8ClampedArray(width * height * 4); for (let h = 0; h < pxMx1.length; h++) { for (let w = 0; w < pxMx1[h].length; w++) { const i = h * pxMx1[h].length + w; pxls[i * 4 + 0] = rgb[pxMx1[h][w]][0]; pxls[i * 4 + 1] = rgb[pxMx1[h][w]][1]; pxls[i * 4 + 2] = rgb[pxMx1[h][w]][2]; pxls[i * 4 + 3] = alphaMx1[h][w]; } } return pxls; } }