var SubtitlesOctopus = function (options) { var supportsWebAssembly = false; try { if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); if (module instanceof WebAssembly.Module) supportsWebAssembly = (new WebAssembly.Instance(module) instanceof WebAssembly.Instance); } } catch (e) { } console.log("WebAssembly support detected: " + (supportsWebAssembly ? "yes" : "no")); var self = this; self.canvas = options.canvas; // HTML canvas element (optional if video specified) self.renderMode = options.renderMode || (options.lossyRender ? 'fast' : (options.blendRender ? 'blend' : 'normal')); // play with those when you need some speed, e.g. for slow devices self.dropAllAnimations = options.dropAllAnimations || false; self.libassMemoryLimit = options.libassMemoryLimit || 0; // set libass bitmap cache memory limit in MiB (approximate) self.libassGlyphLimit = options.libassGlyphLimit || 0; // set libass glyph cache memory limit in MiB (approximate) self.targetFps = options.targetFps || 60; self.prescaleTradeoff = options.prescaleTradeoff || null; // render subtitles less than viewport when less than 1.0 to improve speed, render to more than 1.0 to improve quality; set to null to disable scaling self.softHeightLimit = options.softHeightLimit || 1080; // don't apply prescaleTradeoff < 1 when viewport height is less that this limit self.hardHeightLimit = options.hardHeightLimit || 2160; // don't ever go above this limit self.resizeVariation = options.resizeVariation || 0.2; // by how many a size can vary before it would cause clearance of prerendered buffer self.renderAhead = options.renderAhead || 0; // how many MiB to render ahead and store; 0 to disable (approximate) self.isOurCanvas = false; // (internal) we created canvas and manage it self.video = options.video; // HTML video element (optional if canvas specified) self.canvasParent = null; // (internal) HTML canvas parent element self.fonts = options.fonts || []; // Array with links to fonts used in sub (optional) self.availableFonts = options.availableFonts || []; // Object with all available fonts (optional). Key is font name in lower case, value is link: {"arial": "/font1.ttf"} self.onReadyEvent = options.onReady; // Function called when SubtitlesOctopus is ready (optional) if (supportsWebAssembly) { self.workerUrl = options.workerUrl || 'subtitles-octopus-worker.js'; // Link to WebAssembly worker } else { self.workerUrl = options.legacyWorkerUrl || 'subtitles-octopus-worker-legacy.js'; // Link to legacy worker } self.subUrl = options.subUrl; // Link to sub file (optional if subContent specified) self.subContent = options.subContent || null; // Sub content (optional if subUrl specified) self.onErrorEvent = options.onError; // Function called in case of critical error meaning sub wouldn't be shown and you should use alternative method (for instance it occurs if browser doesn't support web workers). self.debug = options.debug || false; // When debug enabled, some performance info printed in console. self.lastRenderTime = 0; // (internal) Last time we got some frame from worker self.pixelRatio = window.devicePixelRatio || 1; // (internal) Device pixel ratio (for high dpi devices) self.timeOffset = options.timeOffset || 0; // Time offset would be applied to currentTime from video (option) self.renderedItems = []; // used to store items rendered ahead when renderAhead > 0 self.renderAhead = self.renderAhead * 1024 * 1024 * 0.9 /* try to eat less than requested */; self.oneshotState = { eventStart: null, eventOver: false, iteration: 0, renderRequested: false, requestNextTimestamp: -1, prevWidth: null, prevHeight: null } self.hasAlphaBug = false; (function() { if (typeof ImageData.prototype.constructor === 'function') { try { // try actually calling ImageData, as on some browsers it's reported // as existing but calling it errors out as "TypeError: Illegal constructor" new window.ImageData(new Uint8ClampedArray([0, 0, 0, 0]), 1, 1); return; } catch (e) { console.log("detected that ImageData is not constructable despite browser saying so"); } } var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); window.ImageData = function () { var i = 0; if (arguments[0] instanceof Uint8ClampedArray) { var data = arguments[i++]; } var width = arguments[i++]; var height = arguments[i]; var imageData = ctx.createImageData(width, height); if (data) imageData.data.set(data); return imageData; } })(); self.workerError = function (error) { console.error('Worker error: ', error); if (self.onErrorEvent) { self.onErrorEvent(error); } if (!self.debug) { self.dispose(); throw new Error('Worker error: ' + error); } }; // Not tested for repeated usage yet self.init = function () { if (!window.Worker) { self.workerError('worker not supported'); return; } // Worker if (!self.worker) { self.worker = new Worker(self.workerUrl); self.worker.onmessage = self.onWorkerMessage; self.worker.onerror = self.workerError; } self.workerActive = false; self.createCanvas(); self.setVideo(options.video); self.setSubUrl(options.subUrl); self.worker.postMessage({ target: 'worker-init', width: self.canvas.width, height: self.canvas.height, URL: document.URL, currentScript: self.workerUrl, preMain: true, renderMode: self.renderMode, subUrl: self.subUrl, subContent: self.subContent, fonts: self.fonts, availableFonts: self.availableFonts, debug: self.debug, targetFps: self.targetFps, libassMemoryLimit: self.libassMemoryLimit, libassGlyphLimit: self.libassGlyphLimit, renderOnDemand: self.renderAhead > 0, dropAllAnimations: self.dropAllAnimations }); }; self.createCanvas = function () { if (!self.canvas) { if (self.video) { self.isOurCanvas = true; self.canvas = document.createElement('canvas'); self.canvas.className = 'libassjs-canvas'; self.canvas.style.display = 'none'; self.canvasParent = document.createElement('div'); self.canvasParent.className = 'libassjs-canvas-parent'; self.canvasParent.appendChild(self.canvas); if (self.video.nextSibling) { self.video.parentNode.insertBefore(self.canvasParent, self.video.nextSibling); } else { self.video.parentNode.appendChild(self.canvasParent); } } else { if (!self.canvas) { self.workerError('Don\'t know where to render: you should give video or canvas in options.'); } } } self.ctx = self.canvas.getContext('2d'); self.bufferCanvas = document.createElement('canvas'); self.bufferCanvasCtx = self.bufferCanvas.getContext('2d'); // test for alpha bug, where e.g. WebKit can render a transparent pixel // (with alpha == 0) as non-black which then leads to visual artifacts self.bufferCanvas.width = 1; self.bufferCanvas.height = 1; var testBuf = new Uint8ClampedArray([0, 255, 0, 0]); var testImage = new ImageData(testBuf, 1, 1); self.bufferCanvasCtx.clearRect(0, 0, 1, 1); self.ctx.clearRect(0, 0, 1, 1); var prePut = self.ctx.getImageData(0, 0, 1, 1).data; self.bufferCanvasCtx.putImageData(testImage, 0, 0); self.ctx.drawImage(self.bufferCanvas, 0, 0); var postPut = self.ctx.getImageData(0, 0, 1, 1).data; self.hasAlphaBug = prePut[1] != postPut[1]; if (self.hasAlphaBug) { console.log("Detected a browser having issue with transparent pixels, applying workaround"); } }; self.setVideo = function (video) { self.video = video; if (self.video) { var timeupdate = function () { self.setCurrentTime(video.currentTime + self.timeOffset); } self.video.addEventListener("timeupdate", timeupdate, false); self.video.addEventListener("playing", function () { self.setIsPaused(false, video.currentTime + self.timeOffset); }, false); self.video.addEventListener("pause", function () { self.setIsPaused(true, video.currentTime + self.timeOffset); }, false); self.video.addEventListener("seeking", function () { self.video.removeEventListener("timeupdate", timeupdate); }, false); self.video.addEventListener("seeked", function () { self.video.addEventListener("timeupdate", timeupdate, false); self.setCurrentTime(video.currentTime + self.timeOffset); if (self.renderAhead > 0) { _cleanPastRendered(video.currentTime + self.timeOffset, true); } }, false); self.video.addEventListener("ratechange", function () { self.setRate(video.playbackRate); }, false); self.video.addEventListener("timeupdate", function () { self.setCurrentTime(video.currentTime + self.timeOffset); }, false); self.video.addEventListener("waiting", function () { self.setIsPaused(true, video.currentTime + self.timeOffset); }, false); document.addEventListener("fullscreenchange", self.resizeWithTimeout, false); document.addEventListener("mozfullscreenchange", self.resizeWithTimeout, false); document.addEventListener("webkitfullscreenchange", self.resizeWithTimeout, false); document.addEventListener("msfullscreenchange", self.resizeWithTimeout, false); window.addEventListener("resize", self.resizeWithTimeout, false); // Support Element Resize Observer if (typeof ResizeObserver !== "undefined") { self.ro = new ResizeObserver(self.resizeWithTimeout); self.ro.observe(self.video); } if (self.video.videoWidth > 0) { self.resize(); } else { self.video.addEventListener("loadedmetadata", function (e) { e.target.removeEventListener(e.type, arguments.callee); self.resize(); }, false); } } }; self.getVideoPosition = function () { var videoRatio = self.video.videoWidth / self.video.videoHeight; var width = self.video.offsetWidth, height = self.video.offsetHeight; var elementRatio = width / height; var realWidth = width, realHeight = height; if (elementRatio > videoRatio) realWidth = Math.floor(height * videoRatio); else realHeight = Math.floor(width / videoRatio); var x = (width - realWidth) / 2; var y = (height - realHeight) / 2; return { width: realWidth, height: realHeight, x: x, y: y }; }; self.setSubUrl = function (subUrl) { self.subUrl = subUrl; }; function _cleanPastRendered(currentTime, seekClean) { var retainedItems = []; for (var i = 0, len = self.renderedItems.length; i < len; i++) { var item = self.renderedItems[i]; if (item.emptyFinish < 0 || item.emptyFinish >= currentTime) { // item is not yet finished, retain it retainedItems.push(item); } } if (seekClean && retainedItems.length > 0) { // items are ordered by event start time when we push to self.renderedItems, // so first item is the earliest if (currentTime < retainedItems[0].eventStart) { if (retainedItems[0].eventStart - currentTime > 60) { console.info("seeked back too far, cleaning prerender buffer"); retainedItems = []; } else { console.info("seeked backwards, need to free up some buffer"); var size = 0, limit = self.renderAhead * 0.3 /* try to take no more than 1/3 of buffer */; var retain = []; for (var i = 0, len = retainedItems.length; i < len; i++) { var item = retainedItems[i]; size += item.size; if (size >= limit) break; retain.push(item); } retainedItems = retain; } } } var removed = retainedItems.length < self.renderedItems; self.renderedItems = retainedItems; return removed; } function tryRequestOneshot(currentTime, renderNow) { if (!self.renderAhead || self.renderAhead <= 0) return; if (self.oneshotState.renderRequested && !renderNow) return; if (typeof currentTime === 'undefined') { if (!self.video) return; currentTime = self.video.currentTime + self.timeOffset; } var size = 0; for (var i = 0, len = self.renderedItems.length; i < len; i++) { var item = self.renderedItems[i]; if (item.emptyFinish < 0) { console.info('oneshot already reached end-of-events'); return; } if (currentTime >= item.eventStart && currentTime < item.emptyFinish) { // an event for requested time already exists console.debug('not requesting a render for ' + currentTime + ' as event already covering it exists (start=' + item.eventStart + ', empty=' + item.emptyFinish + ')'); return; } size += item.size; } if (size <= self.renderAhead) { lastRendered = currentTime - (renderNow ? 0 : 0.001); if (!self.oneshotState.renderRequested) { self.oneshotState.renderRequested = true; self.worker.postMessage({ target: 'oneshot-render', lastRendered: lastRendered, renderNow: renderNow, iteration: self.oneshotState.iteration }); } else { if (self.workerActive) { console.info('worker busy, requesting to seek'); } self.oneshotState.requestNextTimestamp = lastRendered; } } } function _renderSubtitleEvent(event, currentTime) { var eventOver = event.eventFinish < currentTime; if (self.oneshotState.eventStart == event.eventStart && self.oneshotState.eventOver == eventOver) return; self.oneshotState.eventStart = event.eventStart; self.oneshotState.eventOver = eventOver; var beforeDrawTime = performance.now(); if (event.viewport.width != self.canvas.width || event.viewport.height != self.canvas.height) { self.canvas.width = event.viewport.width; self.canvas.height = event.viewport.height; } self.ctx.clearRect(0, 0, self.canvas.width, self.canvas.height); if (!eventOver) { for (var i = 0; i < event.items.length; i++) { var image = event.items[i]; self.bufferCanvas.width = image.w; self.bufferCanvas.height = image.h; self.bufferCanvasCtx.putImageData(image.image, 0, 0); self.ctx.drawImage(self.bufferCanvas, image.x, image.y); } } if (self.debug) { var drawTime = Math.round(performance.now() - beforeDrawTime); console.log('render: ' + Math.round(event.spentTime - event.blendTime) + ' ms, blend: ' + Math.round(event.blendTime) + ' ms, draw: ' + drawTime + ' ms'); } } function oneshotRender() { window.requestAnimationFrame(oneshotRender); if (!self.video) return; var currentTime = self.video.currentTime + self.timeOffset; var finishTime = -1, eventShown = false, animated = false; for (var i = 0, len = self.renderedItems.length; i < len; i++) { var item = self.renderedItems[i]; if (!eventShown && item.eventStart <= currentTime && (item.emptyFinish < 0 || item.emptyFinish > currentTime)) { _renderSubtitleEvent(item, currentTime); eventShown = true; finishTime = item.emptyFinish; } else if (finishTime >= 0) { // we've already found a known event, now find // the farthest point of consequent events // NOTE: self.renderedItems may have gaps due to seeking if (item.eventStart - finishTime < 0.01) { finishTime = item.emptyFinish; animated = item.animated; } else { break; } } } if (!eventShown) { if (Math.abs(self.oneshotState.requestNextTimestamp - currentTime) > 0.01) { _cleanPastRendered(currentTime); tryRequestOneshot(currentTime, true); } } else if (_cleanPastRendered(currentTime) && finishTime >= 0) { tryRequestOneshot(finishTime, animated); } } self.resetRenderAheadCache = function (isResizing) { if (self.renderAhead > 0) { var newCache = []; if (isResizing && self.oneshotState.prevHeight && self.oneshotState.prevWidth) { if (self.oneshotState.prevHeight == self.canvas.height && self.oneshotState.prevWidth == self.canvas.width) return; var timeLimit = 10, sizeLimit = self.renderAhead * 0.3; if (self.canvas.height >= self.oneshotState.prevHeight * (1.0 - self.resizeVariation) && self.canvas.height <= self.oneshotState.prevHeight * (1.0 + self.resizeVariation) && self.canvas.width >= self.oneshotState.prevWidth * (1.0 - self.resizeVariation) && self.canvas.width <= self.oneshotState.prevWidth * (1.0 + self.resizeVariation)) { console.debug('viewport changes are small, leaving more of prerendered buffer'); timeLimit = 30; sizeLimit = self.renderAhead * 0.5; } var stopTime = self.video.currentTime + self.timeOffset + timeLimit; var size = 0; for (var i = 0; i < self.renderedItems.length; i++) { var item = self.renderedItems[i]; if (item.emptyFinish < 0 || item.emptyFinish >= stopTime) break; size += item.size; if (size >= sizeLimit) break; newCache.push(item); } } console.info('resetting prerender cache'); self.renderedItems = newCache; self.oneshotState.eventStart = null; self.oneshotState.iteration++; self.oneshotState.renderRequested = false; self.oneshotState.prevHeight = self.canvas.height; self.oneshotState.prevWidth = self.canvas.width; window.requestAnimationFrame(oneshotRender); tryRequestOneshot(undefined, true); } } self.renderFrameData = null; function renderFrames() { var data = self.renderFramesData; var beforeDrawTime = performance.now(); self.ctx.clearRect(0, 0, self.canvas.width, self.canvas.height); for (var i = 0; i < data.canvases.length; i++) { var image = data.canvases[i]; self.bufferCanvas.width = image.w; self.bufferCanvas.height = image.h; var imageBuffer = new Uint8ClampedArray(image.buffer); if (self.hasAlphaBug) { for (var j = 3; j < imageBuffer.length; j = j + 4) { imageBuffer[j] = (imageBuffer[j] >= 1) ? imageBuffer[j] : 1; } } var imageData = new ImageData(imageBuffer, image.w, image.h); self.bufferCanvasCtx.putImageData(imageData, 0, 0); self.ctx.drawImage(self.bufferCanvas, image.x, image.y); } if (self.debug) { var drawTime = Math.round(performance.now() - beforeDrawTime); var blendTime = data.blendTime; if (typeof blendTime !== 'undefined') { console.log('render: ' + Math.round(data.spentTime - blendTime) + ' ms, blend: ' + Math.round(blendTime) + ' ms, draw: ' + drawTime + ' ms; TOTAL=' + Math.round(data.spentTime + drawTime) + ' ms'); } else { console.log(Math.round(data.spentTime) + ' ms (+ ' + drawTime + ' ms draw)'); } self.renderStart = performance.now(); } } /** * Lossy Render Mode * */ function renderFastFrames() { var data = self.renderFramesData; var beforeDrawTime = performance.now(); self.ctx.clearRect(0, 0, self.canvas.width, self.canvas.height); for (var i = 0; i < data.bitmaps.length; i++) { var image = data.bitmaps[i]; self.ctx.drawImage(image.bitmap, image.x, image.y); } if (self.debug) { var drawTime = Math.round(performance.now() - beforeDrawTime); console.log(data.bitmaps.length + ' bitmaps, libass: ' + Math.round(data.libassTime) + 'ms, decode: ' + Math.round(data.decodeTime) + 'ms, draw: ' + drawTime + 'ms'); self.renderStart = performance.now(); } } self.workerActive = false; self.frameId = 0; self.onWorkerMessage = function (event) { //dump('\nclient got ' + JSON.stringify(event.data).substr(0, 150) + '\n'); if (!self.workerActive) { self.workerActive = true; if (self.onReadyEvent) { self.onReadyEvent(); } } var data = event.data; switch (data.target) { case 'stdout': { console.log(data.content); break; } case 'console-log': { console.log.apply(console, JSON.parse(data.content)); break; } case 'console-debug': { console.debug.apply(console, JSON.parse(data.content)); break; } case 'console-info': { console.info.apply(console, JSON.parse(data.content)); break; } case 'console-warn': { console.warn.apply(console, JSON.parse(data.content)); break; } case 'console-error': { console.error.apply(console, JSON.parse(data.content)); break; } case 'stderr': { console.error(data.content); break; } case 'window': { window[data.method](); break; } case 'canvas': { switch (data.op) { case 'getContext': { self.ctx = self.canvas.getContext(data.type, data.attributes); break; } case 'resize': { self.resize(data.width, data.height); break; } case 'renderCanvas': { if (self.lastRenderTime < data.time) { self.lastRenderTime = data.time; self.renderFramesData = data; window.requestAnimationFrame(renderFrames); } break; } case 'renderFastCanvas': { if (self.lastRenderTime < data.time) { self.lastRenderTime = data.time; self.renderFramesData = data; window.requestAnimationFrame(renderFastFrames); } break; } case 'setObjectProperty': { self.canvas[data.object][data.property] = data.value; break; } case 'oneshot-result': { if (data.iteration != self.oneshotState.iteration) { console.debug('received stale prerender, ignoring'); return; } if (self.debug) { console.info('oneshot received (start=' + data.eventStart + ', empty=' + data.emptyFinish + '), render: ' + Math.round(data.spentTime) + ' ms'); } self.oneshotState.renderRequested = false; if (Math.abs(data.lastRenderedTime - self.oneshotState.requestNextTimestamp) < 0.01) { self.oneshotState.requestNextTimestamp = -1; } if (data.eventStart - data.lastRenderedTime > 0.01) { // generate bogus empty element, so all timeline is covered anyway self.renderedItems.push({ eventStart: data.lastRenderedTime, eventFinish: data.lastRenderedTime - 0.001, emptyFinish: data.eventStart, viewport: data.viewport, spentTime: 0, blendTime: 0, items: [], animated: false, size: 0 }); } var items = []; var size = 0; for (var i = 0, len = data.canvases.length; i < len; i++) { var item = data.canvases[i]; items.push({ w: item.w, h: item.h, x: item.x, y: item.y, image: new ImageData(new Uint8ClampedArray(item.buffer), item.w, item.h) }); size += item.buffer.byteLength; } var eventSplitted = false; if ((data.emptyFinish > 0 && data.emptyFinish - data.eventStart < 1.0 / self.targetFps) || data.animated) { var newFinish = data.eventStart + 1.0 / self.targetFps; data.emptyFinish = newFinish; data.eventFinish = newFinish; eventSplitted = true; } self.renderedItems.push({ eventStart: data.eventStart, eventFinish: data.eventFinish, emptyFinish: data.emptyFinish, spentTime: data.spentTime, blendTime: data.blendTime, viewport: data.viewport, items: items, animated: data.animated, size: size }); self.renderedItems.sort(function (a, b) { return a.eventStart - b.eventStart; }); if (self.oneshotState.requestNextTimestamp >= 0) { // requesting an out of order event render tryRequestOneshot(self.oneshotState.requestNextTimestamp, true); } else if (data.eventStart < 0) { console.info('oneshot received "end of frames" event'); } else if (data.emptyFinish >= 0) { // there's some more event to render, try requesting next event tryRequestOneshot(data.emptyFinish, eventSplitted); } else { console.info('there are no more events to prerender'); } break; } default: throw 'eh?'; } break; } case 'tick': { self.frameId = data.id; self.worker.postMessage({ target: 'tock', id: self.frameId }); break; } case 'custom': { if (self['onCustomMessage']) { self['onCustomMessage'](event); } else { throw 'Custom message received but client onCustomMessage not implemented.'; } break; } case 'setimmediate': { self.worker.postMessage({ target: 'setimmediate' }); break; } case 'get-events': { console.log(data.target); console.log(data.events); break; } case 'get-styles': { console.log(data.target); console.log(data.styles); break; } default: throw 'what? ' + data.target; } }; function _computeCanvasSize(width, height) { if (self.prescaleTradeoff === null) { if (height > self.hardHeightLimit) { width = width * self.hardHeightLimit / height; height = self.hardHeightLimit; } } else if (self.prescaleTradeoff > 1) { if (height * self.prescaleTradeoff <= self.softHeightLimit) { width *= self.prescaleTradeoff; height *= self.prescaleTradeoff; } else if (height < self.softHeightLimit) { width = width * self.softHeightLimit / height; height = self.softHeightLimit; } else if (height >= self.hardHeightLimit) { width = width * self.hardHeightLimit / height; height = self.hardHeightLimit; } } else if (height >= self.softHeightLimit) { if (height * self.prescaleTradeoff <= self.softHeightLimit) { width = width * self.softHeightLimit / height; height = self.softHeightLimit; } else if (height * self.prescaleTradeoff <= self.hardHeightLimit) { width *= self.prescaleTradeoff; height *= self.prescaleTradeoff; } else { width = width * self.hardHeightLimit / height; height = self.hardHeightLimit; } } return {'width': width, 'height': height}; } self.resize = function (width, height, top, left) { var videoSize = null; top = top || 0; left = left || 0; if ((!width || !height) && self.video) { videoSize = self.getVideoPosition(); var newsize = _computeCanvasSize(videoSize.width * self.pixelRatio, videoSize.height * self.pixelRatio); width = newsize.width; height = newsize.height; var offset = self.canvasParent.getBoundingClientRect().top - self.video.getBoundingClientRect().top; top = videoSize.y - offset; left = videoSize.x; } if (!width || !height) { if (!self.video) { console.error('width or height is 0. You should specify width & height for resize.'); } return; } if ( self.canvas.width != width || self.canvas.height != height || self.canvas.style.top != top || self.canvas.style.left != left ) { self.canvas.width = width; self.canvas.height = height; if (videoSize != null) { self.canvasParent.style.position = 'relative'; self.canvas.style.display = 'block'; self.canvas.style.position = 'absolute'; self.canvas.style.width = videoSize.width + 'px'; self.canvas.style.height = videoSize.height + 'px'; self.canvas.style.top = top + 'px'; self.canvas.style.left = left + 'px'; self.canvas.style.pointerEvents = 'none'; } self.worker.postMessage({ target: 'canvas', width: self.canvas.width, height: self.canvas.height }); self.resetRenderAheadCache(true); } }; self.resizeWithTimeout = function () { self.resize(); }; self.runBenchmark = function () { self.worker.postMessage({ target: 'runBenchmark' }); }; self.customMessage = function (data, options) { options = options || {}; self.worker.postMessage({ target: 'custom', userData: data, preMain: options.preMain }); }; self.setCurrentTime = function (currentTime) { self.worker.postMessage({ target: 'video', currentTime: currentTime }); }; self.setTrackByUrl = function (url) { self.worker.postMessage({ target: 'set-track-by-url', url: url }); self.resetRenderAheadCache(false); }; self.setTrack = function (content) { self.worker.postMessage({ target: 'set-track', content: content }); self.resetRenderAheadCache(false); }; self.freeTrack = function (content) { self.worker.postMessage({ target: 'free-track' }); self.resetRenderAheadCache(false); }; self.render = self.setCurrentTime; self.setIsPaused = function (isPaused, currentTime) { self.worker.postMessage({ target: 'video', isPaused: isPaused, currentTime: currentTime }); }; self.setRate = function (rate) { self.worker.postMessage({ target: 'video', rate: rate }); }; self.dispose = function () { self.worker.postMessage({ target: 'destroy' }); self.worker.terminate(); self.workerActive = false; // Remove the canvas element to remove residual subtitles rendered on player if (self.video) { self.video.parentNode.removeChild(self.canvasParent); } }; self.createEvent = function (event) { self.worker.postMessage({ target: 'create-event', event: event }); }; self.getEvents = function () { self.worker.postMessage({ target: 'get-events' }); }; self.setEvent = function (event, index) { self.worker.postMessage({ target: 'set-event', event: event, index: index }); }; self.removeEvent = function (index) { self.worker.postMessage({ target: 'remove-event', index: index }); }; self.createStyle = function (style) { self.worker.postMessage({ target: 'create-style', style: style }); }; self.getStyles = function () { self.worker.postMessage({ target: 'get-styles' }); }; self.setStyle = function (style, index) { self.worker.postMessage({ target: 'set-style', style: style, index: index }); }; self.removeStyle = function (index) { self.worker.postMessage({ target: 'remove-style', index: index }); }; self.init(); }; if (typeof SubtitlesOctopusOnLoad == 'function') { SubtitlesOctopusOnLoad(); } if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = SubtitlesOctopus } }