jassub 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1 @@
1
- (function(d,l){typeof exports=="object"&&typeof module<"u"?module.exports=l():typeof define=="function"&&define.amd?define(l):(d=typeof globalThis<"u"?globalThis:d||self,d.JASSUB=l())})(this,function(){"use strict";var g=Object.defineProperty;var p=(d,l,h)=>l in d?g(d,l,{enumerable:!0,configurable:!0,writable:!0,value:h}):d[l]=h;var u=(d,l,h)=>(p(d,typeof l!="symbol"?l+"":l,h),h);!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(_){const e=this.getVideoPlaybackQuality(),t=this.mozPresentedFrames||this.mozPaintedFrames||e.totalVideoFrames-e.droppedVideoFrames,s=(n,r)=>{const a=this.getVideoPlaybackQuality(),m=this.mozPresentedFrames||this.mozPaintedFrames||a.totalVideoFrames-a.droppedVideoFrames;if(m>t){const v=this.mozFrameDelay||a.totalFrameDelay-e.totalFrameDelay||0,f=r-n;_(r,{presentationTime:r+v*1e3,expectedDisplayTime:r+f,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+f/1e3,presentedFrames:m,processingDuration:v}),delete this._rvfcpolyfillmap[i]}else this._rvfcpolyfillmap[i]=requestAnimationFrame(v=>s(r,v))},i=Date.now(),o=performance.now();return this._rvfcpolyfillmap[i]=requestAnimationFrame(n=>s(o,n)),i},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(_){cancelAnimationFrame(this._rvfcpolyfillmap[_]),delete this._rvfcpolyfillmap[_]});const d={bt709:"BT.709",bt470bg:"BT.601",smpte170m:"BT.601"},l={"BT.601":{"BT.709":`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418 0 0 0 0 0 1 0'/></filter></svg>#f")`},"BT.709":{"BT.601":`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582 0 0 0 0 0 1 0'/></filter></svg>#f")`},FCC:{"BT.709":`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251
2
- 1.0378 0 0 0 0 0 1 0'/></filter></svg>#f")`,"BT.601":`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996 0 0 0 0 0 1 0'/></filter></svg>#f")`},SMPTE240M:{"BT.709":`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148 0 0 0 0 0 1 0'/></filter></svg>#f")`,"BT.601":`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973 0 0 0 0 0 1 0'/></filter></svg>#f")`}},c=class extends EventTarget{constructor(e={}){super(),globalThis.Worker||this.destroy("Worker not supported"),c._test(),this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&(e.onDemandRender??!0),this.timeOffset=e.timeOffset||0,this._video=e.video,this._videoHeight=0,this._videoWidth=0,this._videoColorSpace=null,this._canvas=e.canvas,this._video&&!this._canvas?(this._canvasParent=document.createElement("div"),this._canvasParent.className="JASSUB",this._canvasParent.style.position="relative",this._canvas=document.createElement("canvas"),this._canvas.style.display="block",this._canvas.style.position="absolute",this._canvas.style.pointerEvents="none",this._canvasParent.appendChild(this._canvas),this._video.nextSibling?this._video.parentNode.insertBefore(this._canvasParent,this._video.nextSibling):this._video.parentNode.appendChild(this._canvasParent)):this._canvas||this.destroy("Don't know where to render: you should give video or canvas in options."),this._bufferCanvas=document.createElement("canvas"),this._bufferCtx=this._bufferCanvas.getContext("2d"),this._canvasctrl=this._canvas,this._ctx=this._canvasctrl.getContext("2d"),this._lastRenderTime=0,this.debug=!!e.debug,this.prescaleFactor=e.prescaleFactor||1,this.prescaleHeightLimit=e.prescaleHeightLimit||1080,this.maxRenderHeight=e.maxRenderHeight||0,this._worker=new Worker(c._supportsWebAssembly?e.workerUrl||"jassub-worker.js":e.legacyWorkerUrl||"jassub-worker-legacy.js"),this._worker.onmessage=t=>this._onmessage(t),this._worker.onerror=t=>this._error(t),this._loaded=new Promise(t=>{this._init=()=>{this._destroyed||(this._worker.postMessage({target:"init",asyncRender:typeof createImageBitmap<"u"&&(e.asyncRender??!0),onDemandRender:this._onDemandRender,width:this._canvasctrl.width||0,height:this._canvasctrl.height||0,preMain:!0,blendMode:e.blendMode||"js",subUrl:e.subUrl,subContent:e.subContent||null,fonts:e.fonts||[],availableFonts:e.availableFonts||{"liberation sans":"./default.woff2"},fallbackFont:e.fallbackFont||"liberation sans",debug:this.debug,targetFps:e.targetFps||24,dropAllAnimations:e.dropAllAnimations,libassMemoryLimit:e.libassMemoryLimit||0,libassGlyphLimit:e.libassGlyphLimit||0,hasAlphaBug:c._hasAlphaBug,offscreenRender:typeof OffscreenCanvas<"u"&&(e.offscreenRender??!0),useLocalFonts:"queryLocalFonts"in self&&(e.useLocalFonts??!0)}),this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=this.setRate.bind(this),this._boundUpdateColorSpace=this._updateColorSpace.bind(this),this._video&&this.setVideo(e.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null),t())}})}static _test(){if(c._supportsWebAssembly!==null)return null;const e=document.createElement("canvas"),t=e.getContext("2d",{willReadFrequently:!0});if(typeof ImageData.prototype.constructor=="function")try{new ImageData(new Uint8ClampedArray([0,0,0,0]),1,1)}catch{console.log("Detected that ImageData is not constructable despite browser saying so"),self.ImageData=function(a,m,v){const f=t.createImageData(m,v);return a&&f.data.set(a),f}}try{if(typeof WebAssembly=="object"&&typeof WebAssembly.instantiate=="function"){const r=new WebAssembly.Module(Uint8Array.of(0,97,115,109,1,0,0,0));r instanceof WebAssembly.Module&&(c._supportsWebAssembly=new WebAssembly.Instance(r)instanceof WebAssembly.Instance)}}catch{c._supportsWebAssembly=!1}const s=document.createElement("canvas"),i=s.getContext("2d",{willReadFrequently:!0});e.width=s.width=1,e.height=s.height=1,t.clearRect(0,0,1,1),i.clearRect(0,0,1,1);const o=i.getImageData(0,0,1,1).data;t.putImageData(new ImageData(new Uint8ClampedArray([0,255,0,0]),1,1),0,0),i.drawImage(e,0,0);const n=i.getImageData(0,0,1,1).data;c._hasAlphaBug=o[1]!==n[1],c._hasAlphaBug&&console.log("Detected a browser having issue with transparent pixels, applying workaround"),e.remove(),s.remove()}resize(e=0,t=0,s=0,i=0,o=(n=>(n=this._video)==null?void 0:n.paused)()){if((!e||!t)&&this._video){const r=this._getVideoPosition();let a=null;if(this._videoWidth){const m=this._video.videoWidth/this._videoWidth,v=this._video.videoHeight/this._videoHeight;a=this._computeCanvasSize((r.width||0)/m,(r.height||0)/v)}else a=this._computeCanvasSize(r.width||0,r.height||0);e=a.width,t=a.height,this._canvasParent&&(s=r.y-(this._canvasParent.getBoundingClientRect().top-this._video.getBoundingClientRect().top),i=r.x),this._canvas.style.width=r.width+"px",this._canvas.style.height=r.height+"px"}this._canvas.style.top=s+"px",this._canvas.style.left=i+"px",this.sendMessage("canvas",{width:e,height:t,force:o&&this.busy===!1})}_getVideoPosition(e=this._video.videoWidth,t=this._video.videoHeight){const s=e/t,{offsetWidth:i,offsetHeight:o}=this._video,n=i/o;e=i,t=o,n>s?e=Math.floor(o*s):t=Math.floor(i/s);const r=(i-e)/2,a=(o-t)/2;return{width:e,height:t,x:r,y:a}}_computeCanvasSize(e=0,t=0){const s=this.prescaleFactor<=0?1:this.prescaleFactor,i=self.devicePixelRatio||1;if(e=e*i,t=t*i,t<=0||e<=0)e=0,t=0;else{const o=s<1?-1:1;let n=t*i;o*n*s<=o*this.prescaleHeightLimit?n*=s:o*n<o*this.prescaleHeightLimit&&(n=this.prescaleHeightLimit),this.maxRenderHeight>0&&n>this.maxRenderHeight&&(n=this.maxRenderHeight),e*=n/t,t=n}return{width:e,height:t}}_timeupdate({type:e}){const s={seeking:!0,waiting:!0,playing:!1}[e];s!=null&&(this._playstate=s),this.setCurrentTime(this._video.paused||this._playstate,this._video.currentTime+this.timeOffset)}_updateColorSpace(){this._video.requestVideoFrameCallback(()=>{const e=new VideoFrame(this._video);this._videoColorSpace=d[e.colorSpace.matrix],e.close()})}setVideo(e){e instanceof HTMLVideoElement?(this._removeListeners(),this._video=e,this._onDemandRender?this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)):(this._playstate=e.paused,e.addEventListener("timeupdate",this._boundTimeUpdate,!1),e.addEventListener("progress",this._boundTimeUpdate,!1),e.addEventListener("waiting",this._boundTimeUpdate,!1),e.addEventListener("seeking",this._boundTimeUpdate,!1),e.addEventListener("playing",this._boundTimeUpdate,!1),e.addEventListener("ratechange",this._boundSetRate,!1),e.addEventListener("resize",this._boundResize,!1)),"VideoFrame"in window&&(e.addEventListener("loadedmetadata",this._boundUpdateColorSpace,!1),e.readyState>2&&this._updateColorSpace()),e.videoWidth>0&&this.resize(),typeof ResizeObserver<"u"&&(this._ro||(this._ro=new ResizeObserver(()=>this.resize())),this._ro.observe(e))):this._error("Video element invalid!")}runBenchmark(){this.sendMessage("runBenchmark")}setTrackByUrl(e){this.sendMessage("setTrackByUrl",{url:e})}setTrack(e){this.sendMessage("setTrack",{content:e})}freeTrack(){this.sendMessage("freeTrack")}setIsPaused(e){this.sendMessage("video",{isPaused:e})}setRate(e){this.sendMessage("video",{rate:e})}setCurrentTime(e,t,s){this.sendMessage("video",{isPaused:e,currentTime:t,rate:s})}createEvent(e){this.sendMessage("createEvent",{event:e})}setEvent(e,t){this.sendMessage("setEvent",{event:e,index:t})}removeEvent(e){this.sendMessage("removeEvent",{index:e})}getEvents(e){this._fetchFromWorker({target:"getEvents"},(t,{events:s})=>{e(t,s)})}createStyle(e){this.sendMessage("createStyle",{style:e})}setStyle(e,t){this.sendMessage("setStyle",{event:e,index:t})}removeStyle(e){this.sendMessage("removeStyle",{index:e})}getStyles(e){this._fetchFromWorker({target:"getStyles"},(t,{styles:s})=>{e(t,s)})}addFont(e){this.sendMessage("addFont",{font:e})}_sendLocalFont(e){try{queryLocalFonts().then(t=>{const s=t==null?void 0:t.find(i=>i.fullName.toLowerCase()===e);s&&s.blob().then(i=>{i.arrayBuffer().then(o=>{this.addFont(new Uint8Array(o))})})})}catch(t){console.warn("Local fonts API:",t)}}_getLocalFont({font:e}){var t;try{(t=navigator==null?void 0:navigator.permissions)!=null&&t.query?navigator.permissions.query({name:"local-fonts"}).then(s=>{s.state==="granted"&&this._sendLocalFont(e)}):this._sendLocalFont(e)}catch(s){console.warn("Local fonts API:",s)}}_unbusy(){this._lastDemandTime?this._demandRender(this._lastDemandTime):this.busy=!1}_handleRVFC(e,{mediaTime:t,width:s,height:i}){if(this._destroyed)return null;this.busy?this._lastDemandTime={mediaTime:t,width:s,height:i}:(this.busy=!0,this._demandRender({mediaTime:t,width:s,height:i})),this._video.requestVideoFrameCallback(this._handleRVFC.bind(this))}_demandRender({mediaTime:e,width:t,height:s}){this._lastDemandTime=null,(t!==this._videoWidth||s!==this._videoHeight)&&(this._videoWidth=t,this._videoHeight=s,this.resize()),this.sendMessage("demand",{time:e+this.timeOffset})}verifyColorSpace(e,t=this._videoColorSpace){!e||!t||e!==t&&(this._ctx.filter=l[e][t])}_render({images:e,async:t,times:s,width:i,height:o,colorSpace:n}){this._unbusy();const r=Date.now();(this._canvasctrl.width!==i||this._canvasctrl.height!==o)&&(this._canvasctrl.width=i,this._canvasctrl.height=o,this.verifyColorSpace(n)),this._ctx.clearRect(0,0,this._canvasctrl.width,this._canvasctrl.height);for(const a of e)a.image&&(t?(this._ctx.drawImage(a.image,a.x,a.y),a.image.close()):(this._bufferCanvas.width=a.w,this._bufferCanvas.height=a.h,this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(a.image)),a.w,a.h),0,0),this._ctx.drawImage(this._bufferCanvas,a.x,a.y)));if(this.debug){s.drawTime=Date.now()-r;let a=0;for(const m in s)a+=s[m];console.log("Bitmaps: "+e.length+" Total: "+Math.round(a)+"ms",s)}}_fixAlpha(e){if(c._hasAlphaBug)for(let t=3;t<e.length;t+=4)e[t]=e[t]>1?e[t]:1;return e}_ready(){this._init(),this.dispatchEvent(new CustomEvent("ready"))}async sendMessage(e,t={},s){await this._loaded,s?this._worker.postMessage({target:e,transferable:s,...t},[...s]):this._worker.postMessage({target:e,...t})}_fetchFromWorker(e,t){try{const s=e.target,i=setTimeout(()=>{n(new Error("Error: Timeout while try to fetch "+s))},5e3),o=({data:r})=>{r.target===s&&(t(null,r),this._worker.removeEventListener("message",o),this._worker.removeEventListener("error",n),clearTimeout(i))},n=r=>{t(r),this._worker.removeEventListener("message",o),this._worker.removeEventListener("error",n),clearTimeout(i)};this._worker.addEventListener("message",o),this._worker.addEventListener("error",n),this._worker.postMessage(e)}catch(s){this._error(s)}}_console({content:e,command:t}){console[t].apply(console,JSON.parse(e))}_onmessage({data:e}){this["_"+e.target]&&this["_"+e.target](e)}_error(e){const t=e instanceof Error?e:e instanceof ErrorEvent?e.error:new Error(e),s=e instanceof Event?new ErrorEvent(e.type,e):new ErrorEvent("error",{error:t});this.dispatchEvent(s),console.error(t)}_removeListeners(){this._video&&(this._ro&&this._ro.unobserve(this._video),this._ctx.filter="none",this._video.removeEventListener("timeupdate",this._boundTimeUpdate),this._video.removeEventListener("progress",this._boundTimeUpdate),this._video.removeEventListener("waiting",this._boundTimeUpdate),this._video.removeEventListener("seeking",this._boundTimeUpdate),this._video.removeEventListener("playing",this._boundTimeUpdate),this._video.removeEventListener("ratechange",this._boundSetRate),this._video.removeEventListener("resize",this._boundResize),this._video.removeEventListener("loadedmetadata",this._boundUpdateColorSpace))}destroy(e){e&&this._error(e),this._video&&this._canvasParent&&this._video.parentNode.removeChild(this._canvasParent),this._destroyed=!0,this._removeListeners(),this.sendMessage("destroy"),this._worker.terminate()}};let h=c;return u(h,"_supportsWebAssembly",null),u(h,"_hasAlphaBug",null),h});
1
+ (function(c,_){typeof exports=="object"&&typeof module<"u"?module.exports=_():typeof define=="function"&&define.amd?define(_):(c=typeof globalThis<"u"?globalThis:c||self,c.JASSUB=_())})(this,function(){"use strict";!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(f){const e=this.getVideoPlaybackQuality(),t=this.mozPresentedFrames||this.mozPaintedFrames||e.totalVideoFrames-e.droppedVideoFrames,s=(n,i)=>{const o=this.getVideoPlaybackQuality(),d=this.mozPresentedFrames||this.mozPaintedFrames||o.totalVideoFrames-o.droppedVideoFrames;if(d>t){const l=this.mozFrameDelay||o.totalFrameDelay-e.totalFrameDelay||0,m=i-n;f(i,{presentationTime:i+l*1e3,expectedDisplayTime:i+m,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+m/1e3,presentedFrames:d,processingDuration:l}),delete this._rvfcpolyfillmap[a]}else this._rvfcpolyfillmap[a]=requestAnimationFrame(l=>s(i,l))},a=Date.now(),r=performance.now();return this._rvfcpolyfillmap[a]=requestAnimationFrame(n=>s(r,n)),a},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(f){cancelAnimationFrame(this._rvfcpolyfillmap[f]),delete this._rvfcpolyfillmap[f]});const c={bt709:"BT709",bt470bg:"BT601",smpte170m:"BT601"},_={BT601:{BT709:"1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418"},BT709:{BT601:"0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582"},FCC:{BT709:"1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378",BT601:"1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996"},SMPTE240M:{BT709:"0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148",BT601:"0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973"}};class h extends EventTarget{constructor(e={}){super(),globalThis.Worker||this.destroy("Worker not supported"),this._loaded=new Promise(t=>{this._init=t}),h._test(),this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&(e.onDemandRender??!0),this._offscreenRender="transferControlToOffscreen"in HTMLCanvasElement.prototype&&!e.canvas&&(e.offscreenRender??!0),this.timeOffset=e.timeOffset||0,this._video=e.video,this._videoHeight=0,this._videoWidth=0,this._videoColorSpace=null,this._canvas=e.canvas,this._video&&!this._canvas?(this._canvasParent=document.createElement("div"),this._canvasParent.className="JASSUB",this._canvasParent.style.position="relative",this._createCanvas(),this._video.nextSibling?this._video.parentNode.insertBefore(this._canvasParent,this._video.nextSibling):this._video.parentNode.appendChild(this._canvasParent)):this._canvas||this.destroy("Don't know where to render: you should give video or canvas in options."),this._bufferCanvas=document.createElement("canvas"),this._bufferCtx=this._bufferCanvas.getContext("2d"),this._canvasctrl=this._offscreenRender?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!this._offscreenRender&&this._canvasctrl.getContext("2d"),this._lastRenderTime=0,this.debug=!!e.debug,this.prescaleFactor=e.prescaleFactor||1,this.prescaleHeightLimit=e.prescaleHeightLimit||1080,this.maxRenderHeight=e.maxRenderHeight||0,this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=this.setRate.bind(this),this._boundUpdateColorSpace=this._updateColorSpace.bind(this),this._video&&this.setVideo(e.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null),this._worker=new Worker(e.workerUrl||"jassub-worker.js"),this._worker.onmessage=t=>this._onmessage(t),this._worker.onerror=t=>this._error(t),this._worker.postMessage({target:"init",wasmUrl:h._supportsSIMD&&e.modernWasmUrl?e.modernWasmUrl:e.wasmUrl||"jassub-worker.wasm",legacyWasmUrl:e.legacyWasmUrl||"jassub-worker.wasm.js",asyncRender:typeof createImageBitmap<"u"&&(e.asyncRender??!0),onDemandRender:this._onDemandRender,width:this._canvasctrl.width||0,height:this._canvasctrl.height||0,blendMode:e.blendMode||"js",subUrl:e.subUrl,subContent:e.subContent||null,fonts:e.fonts||[],availableFonts:e.availableFonts||{"liberation sans":"./default.woff2"},fallbackFont:e.fallbackFont||"liberation sans",debug:this.debug,targetFps:e.targetFps||24,dropAllAnimations:e.dropAllAnimations,dropAllBlur:e.dropAllBlur,libassMemoryLimit:e.libassMemoryLimit||0,libassGlyphLimit:e.libassGlyphLimit||0,useLocalFonts:typeof queryLocalFonts<"u"&&(e.useLocalFonts??!0)}),this._offscreenRender===!0&&this.sendMessage("offscreenCanvas",null,[this._canvasctrl])}_createCanvas(){this._canvas=document.createElement("canvas"),this._canvas.style.display="block",this._canvas.style.position="absolute",this._canvas.style.pointerEvents="none",this._canvasParent.appendChild(this._canvas)}static _supportsSIMD=null;static _hasAlphaBug=null;static _test(){if(h._supportsSIMD!==null)return null;try{h._supportsSIMD=WebAssembly.validate(Uint8Array.of(0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11))}catch{h._supportsSIMD=!1}const e=document.createElement("canvas"),t=e.getContext("2d",{willReadFrequently:!0});if(typeof ImageData.prototype.constructor=="function")try{new ImageData(new Uint8ClampedArray([0,0,0,0]),1,1)}catch{console.log("Detected that ImageData is not constructable despite browser saying so"),self.ImageData=function(o,d,l){const m=t.createImageData(d,l);return o&&m.data.set(o),m}}const s=document.createElement("canvas"),a=s.getContext("2d",{willReadFrequently:!0});e.width=s.width=1,e.height=s.height=1,t.clearRect(0,0,1,1),a.clearRect(0,0,1,1);const r=a.getImageData(0,0,1,1).data;t.putImageData(new ImageData(new Uint8ClampedArray([0,255,0,0]),1,1),0,0),a.drawImage(e,0,0);const n=a.getImageData(0,0,1,1).data;h._hasAlphaBug=r[1]!==n[1],h._hasAlphaBug&&console.log("Detected a browser having issue with transparent pixels, applying workaround"),e.remove(),s.remove()}resize(e=0,t=0,s=0,a=0,r=this._video?.paused){if((!e||!t)&&this._video){const n=this._getVideoPosition();let i=null;if(this._videoWidth){const o=this._video.videoWidth/this._videoWidth,d=this._video.videoHeight/this._videoHeight;i=this._computeCanvasSize((n.width||0)/o,(n.height||0)/d)}else i=this._computeCanvasSize(n.width||0,n.height||0);e=i.width,t=i.height,this._canvasParent&&(s=n.y-(this._canvasParent.getBoundingClientRect().top-this._video.getBoundingClientRect().top),a=n.x),this._canvas.style.width=n.width+"px",this._canvas.style.height=n.height+"px"}this._canvas.style.top=s+"px",this._canvas.style.left=a+"px",r&&this.busy===!1?this.busy=!0:r=!1,this.sendMessage("canvas",{width:e,height:t,force:r})}_getVideoPosition(e=this._video.videoWidth,t=this._video.videoHeight){const s=e/t,{offsetWidth:a,offsetHeight:r}=this._video,n=a/r;e=a,t=r,n>s?e=Math.floor(r*s):t=Math.floor(a/s);const i=(a-e)/2,o=(r-t)/2;return{width:e,height:t,x:i,y:o}}_computeCanvasSize(e=0,t=0){const s=this.prescaleFactor<=0?1:this.prescaleFactor,a=self.devicePixelRatio||1;if(e=e*a,t=t*a,t<=0||e<=0)e=0,t=0;else{const r=s<1?-1:1;let n=t*a;r*n*s<=r*this.prescaleHeightLimit?n*=s:r*n<r*this.prescaleHeightLimit&&(n=this.prescaleHeightLimit),this.maxRenderHeight>0&&n>this.maxRenderHeight&&(n=this.maxRenderHeight),e*=n/t,t=n}return{width:e,height:t}}_timeupdate({type:e}){const s={seeking:!0,waiting:!0,playing:!1}[e];s!=null&&(this._playstate=s),this.setCurrentTime(this._video.paused||this._playstate,this._video.currentTime+this.timeOffset)}setVideo(e){e instanceof HTMLVideoElement?(this._removeListeners(),this._video=e,this._onDemandRender?this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)):(this._playstate=e.paused,e.addEventListener("timeupdate",this._boundTimeUpdate,!1),e.addEventListener("progress",this._boundTimeUpdate,!1),e.addEventListener("waiting",this._boundTimeUpdate,!1),e.addEventListener("seeking",this._boundTimeUpdate,!1),e.addEventListener("playing",this._boundTimeUpdate,!1),e.addEventListener("ratechange",this._boundSetRate,!1),e.addEventListener("resize",this._boundResize,!1)),"VideoFrame"in window&&(e.addEventListener("loadedmetadata",this._boundUpdateColorSpace,!1),e.readyState>2&&this._updateColorSpace()),e.videoWidth>0&&this.resize(),typeof ResizeObserver<"u"&&(this._ro||(this._ro=new ResizeObserver(()=>this.resize())),this._ro.observe(e))):this._error("Video element invalid!")}runBenchmark(){this.sendMessage("runBenchmark")}setTrackByUrl(e){this.sendMessage("setTrackByUrl",{url:e}),this._reAttachOffscreen(),this._ctx&&(this._ctx.filter="none")}setTrack(e){this.sendMessage("setTrack",{content:e}),this._reAttachOffscreen(),this._ctx&&(this._ctx.filter="none")}freeTrack(){this.sendMessage("freeTrack")}setIsPaused(e){this.sendMessage("video",{isPaused:e})}setRate(e){this.sendMessage("video",{rate:e})}setCurrentTime(e,t,s){this.sendMessage("video",{isPaused:e,currentTime:t,rate:s,colorSpace:this._videoColorSpace})}createEvent(e){this.sendMessage("createEvent",{event:e})}setEvent(e,t){this.sendMessage("setEvent",{event:e,index:t})}removeEvent(e){this.sendMessage("removeEvent",{index:e})}getEvents(e){this._fetchFromWorker({target:"getEvents"},(t,{events:s})=>{e(t,s)})}createStyle(e){this.sendMessage("createStyle",{style:e})}setStyle(e,t){this.sendMessage("setStyle",{event:e,index:t})}removeStyle(e){this.sendMessage("removeStyle",{index:e})}getStyles(e){this._fetchFromWorker({target:"getStyles"},(t,{styles:s})=>{e(t,s)})}addFont(e){this.sendMessage("addFont",{font:e})}_sendLocalFont(e){try{queryLocalFonts().then(t=>{const s=t?.find(a=>a.fullName.toLowerCase()===e);s&&s.blob().then(a=>{a.arrayBuffer().then(r=>{this.addFont(new Uint8Array(r))})})})}catch(t){console.warn("Local fonts API:",t)}}_getLocalFont({font:e}){try{navigator?.permissions?.query?navigator.permissions.query({name:"local-fonts"}).then(t=>{t.state==="granted"&&this._sendLocalFont(e)}):this._sendLocalFont(e)}catch(t){console.warn("Local fonts API:",t)}}_unbusy(){this._lastDemandTime?this._demandRender(this._lastDemandTime):this.busy=!1}_handleRVFC(e,{mediaTime:t,width:s,height:a}){if(this._destroyed)return null;this.busy?this._lastDemandTime={mediaTime:t,width:s,height:a}:(this.busy=!0,this._demandRender({mediaTime:t,width:s,height:a})),this._video.requestVideoFrameCallback(this._handleRVFC.bind(this))}_demandRender({mediaTime:e,width:t,height:s}){this._lastDemandTime=null,(t!==this._videoWidth||s!==this._videoHeight)&&(this._videoWidth=t,this._videoHeight=s,this.resize()),this.sendMessage("demand",{time:e+this.timeOffset})}_detachOffscreen(){if(!this._offscreenRender||this._ctx)return null;this._canvas.remove(),this._createCanvas(),this._canvasctrl=this._canvas,this._ctx=this._canvasctrl.getContext("2d"),this.sendMessage("detachOffscreen"),this.busy=!1,this.resize(0,0,0,0,!0)}_reAttachOffscreen(){if(!this._offscreenRender||!this._ctx)return null;this._canvas.remove(),this._createCanvas(),this._canvasctrl=this._canvas.transferControlToOffscreen(),this._ctx=!1,this.sendMessage("offscreenCanvas",null,[this._canvasctrl]),this.resize(0,0,0,0,!0)}_updateColorSpace(){this._video.requestVideoFrameCallback(()=>{try{const e=new VideoFrame(this._video);this._videoColorSpace=c[e.colorSpace.matrix],e.close(),this.sendMessage("getColorSpace")}catch(e){console.warn(e)}})}_verifyColorSpace({subtitleColorSpace:e,videoColorSpace:t=this._videoColorSpace}){!e||!t||e!==t&&(this._detachOffscreen(),this._ctx.filter=`url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${_[e][t]} 0 0 0 0 0 1 0'/></filter></svg>#f")`)}_render({images:e,asyncRender:t,times:s,width:a,height:r,colorSpace:n}){this._unbusy(),this.debug&&(s.IPCTime=Date.now()-s.JSRenderTime),(this._canvasctrl.width!==a||this._canvasctrl.height!==r)&&(this._canvasctrl.width=a,this._canvasctrl.height=r,this._verifyColorSpace({subtitleColorSpace:n})),this._ctx.clearRect(0,0,this._canvasctrl.width,this._canvasctrl.height);for(const i of e)i.image&&(t?(this._ctx.drawImage(i.image,i.x,i.y),i.image.close()):(this._bufferCanvas.width=i.w,this._bufferCanvas.height=i.h,this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)),i.w,i.h),0,0),this._ctx.drawImage(this._bufferCanvas,i.x,i.y)));if(this.debug){s.JSRenderTime=Date.now()-s.JSRenderTime-s.IPCTime;let i=0;const o=s.bitmaps||e.length;delete s.bitmaps;for(const d in s)i+=s[d];console.log("Bitmaps: "+o+" Total: "+(i|0)+"ms",s)}}_fixAlpha(e){if(h._hasAlphaBug)for(let t=3;t<e.length;t+=4)e[t]=e[t]>1?e[t]:1;return e}_ready(){this._init(),this.dispatchEvent(new CustomEvent("ready"))}async sendMessage(e,t={},s){await this._loaded,s?this._worker.postMessage({target:e,transferable:s,...t},[...s]):this._worker.postMessage({target:e,...t})}_fetchFromWorker(e,t){try{const s=e.target,a=setTimeout(()=>{n(new Error("Error: Timeout while try to fetch "+s))},5e3),r=({data:i})=>{i.target===s&&(t(null,i),this._worker.removeEventListener("message",r),this._worker.removeEventListener("error",n),clearTimeout(a))},n=i=>{t(i),this._worker.removeEventListener("message",r),this._worker.removeEventListener("error",n),clearTimeout(a)};this._worker.addEventListener("message",r),this._worker.addEventListener("error",n),this._worker.postMessage(e)}catch(s){this._error(s)}}_console({content:e,command:t}){console[t].apply(console,JSON.parse(e))}_onmessage({data:e}){this["_"+e.target]&&this["_"+e.target](e)}_error(e){const t=e instanceof Error?e:e instanceof ErrorEvent?e.error:new Error(e),s=e instanceof Event?new ErrorEvent(e.type,e):new ErrorEvent("error",{error:t});this.dispatchEvent(s),console.error(t)}_removeListeners(){this._video&&(this._ro&&this._ro.unobserve(this._video),this._ctx&&(this._ctx.filter="none"),this._video.removeEventListener("timeupdate",this._boundTimeUpdate),this._video.removeEventListener("progress",this._boundTimeUpdate),this._video.removeEventListener("waiting",this._boundTimeUpdate),this._video.removeEventListener("seeking",this._boundTimeUpdate),this._video.removeEventListener("playing",this._boundTimeUpdate),this._video.removeEventListener("ratechange",this._boundSetRate),this._video.removeEventListener("resize",this._boundResize),this._video.removeEventListener("loadedmetadata",this._boundUpdateColorSpace))}destroy(e){e&&this._error(e),this._video&&this._canvasParent&&this._video.parentNode.removeChild(this._canvasParent),this._destroyed=!0,this._removeListeners(),this.sendMessage("destroy"),this._worker.terminate()}}return h});
package/index.d.ts CHANGED
@@ -60,9 +60,12 @@ interface JassubOptions {
60
60
  prescaleHeightLimit?: number;
61
61
  maxRenderHeight?: number;
62
62
  dropAllAnimations?: boolean;
63
+ dropAllBlur?: boolean
63
64
 
64
65
  workerUrl?: string;
65
- legacyWorkerUrl?: string;
66
+ wasmUrl?: string;
67
+ legacyWasmUrl?: string;
68
+ modernWasmUrl?: string;
66
69
 
67
70
  subUrl?: string;
68
71
  subContent?: string;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "jassub",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "libass Subtitle Renderer and Parser library for browsers",
5
5
  "main": "src/jassub.js",
6
+ "type": "module",
6
7
  "files": [
7
8
  "dist/*",
8
9
  "src/jassub.js",
@@ -32,11 +33,10 @@
32
33
  },
33
34
  "devDependencies": {
34
35
  "vite": "^4.1.4",
35
- "vite-plugin-static-copy": "^0.13.1"
36
+ "vite-plugin-static-copy": "^0.13.1",
37
+ "terser": "^5.17.3"
36
38
  },
37
39
  "scripts": {
38
- "bundle": "npm run build-lib && npm run build-worker",
39
- "build-lib": "vite build --config vite.config.js",
40
- "build-worker": "vite build --config vite-worker.config.js"
40
+ "build": "node vite.build.js"
41
41
  }
42
42
  }
package/src/jassub.js CHANGED
@@ -1,27 +1,26 @@
1
1
  import 'rvfc-polyfill'
2
2
 
3
3
  const webYCbCrMap = {
4
- bt709: 'BT.709',
4
+ bt709: 'BT709',
5
5
  // these might not be exactly correct? oops?
6
- bt470bg: 'BT.601', // alias BT.601 PAL... whats the difference?
7
- smpte170m: 'BT.601'// alias BT.601 NTSC... whats the difference?
6
+ bt470bg: 'BT601', // alias BT.601 PAL... whats the difference?
7
+ smpte170m: 'BT601'// alias BT.601 NTSC... whats the difference?
8
8
  }
9
9
 
10
10
  const colorMatrixConversionMap = {
11
- 'BT.601': {
12
- 'BT.709': 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'f\'><feColorMatrix type=\'matrix\' values=\'1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418 0 0 0 0 0 1 0\'/></filter></svg>#f")'
11
+ BT601: {
12
+ BT709: '1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418'
13
13
  },
14
- 'BT.709': {
15
- 'BT.601': 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'f\'><feColorMatrix type=\'matrix\' values=\'0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582 0 0 0 0 0 1 0\'/></filter></svg>#f")'
14
+ BT709: {
15
+ BT601: '0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582'
16
16
  },
17
17
  FCC: {
18
- 'BT.709': `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251
19
- 1.0378 0 0 0 0 0 1 0'/></filter></svg>#f")`,
20
- 'BT.601': 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'f\'><feColorMatrix type=\'matrix\' values=\'1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996 0 0 0 0 0 1 0\'/></filter></svg>#f")'
18
+ BT709: '1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378',
19
+ BT601: '1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996'
21
20
  },
22
21
  SMPTE240M: {
23
- 'BT.709': 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'f\'><feColorMatrix type=\'matrix\' values=\'0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148 0 0 0 0 0 1 0\'/></filter></svg>#f")',
24
- 'BT.601': 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'f\'><feColorMatrix type=\'matrix\' values=\'0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973 0 0 0 0 0 1 0\'/></filter></svg>#f")'
22
+ BT709: '0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148',
23
+ BT601: '0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973'
25
24
  }
26
25
  }
27
26
 
@@ -45,8 +44,11 @@ export default class JASSUB extends EventTarget {
45
44
  * @param {Number} [options.prescaleHeightLimit=1080] The height in pixels beyond which the subtitles canvas won't be prescaled.
46
45
  * @param {Number} [options.maxRenderHeight=0] The maximum rendering height in pixels of the subtitles canvas. Beyond this subtitles will be upscaled by the browser.
47
46
  * @param {Boolean} [options.dropAllAnimations=false] Attempt to discard all animated tags. Enabling this may severly mangle complex subtitles and should only be considered as an last ditch effort of uncertain success for hardware otherwise incapable of displaing anything. Will not reliably work with manually edited or allocated events.
47
+ * @param {Boolean} [options.dropAllBlur=false] The holy grail of performance gains. If heavy TS lags a lot, disabling this will make it ~x10 faster. This drops blur from all added subtitle tracks making most text and backgrounds look sharper, this is way less intrusive than dropping all animations, while still offering major performance gains.
48
48
  * @param {String} [options.workerUrl='jassub-worker.js'] The URL of the worker.
49
- * @param {String} [options.legacyWorkerUrl='jassub-worker-legacy.js'] The URL of the legacy worker. Only loaded if the browser doesn't support WASM.
49
+ * @param {String} [options.wasmUrl='jassub-worker.wasm'] The URL of the worker WASM.
50
+ * @param {String} [options.legacyWasmUrl='jassub-worker.wasm.js'] The URL of the worker WASM. Only loaded if the browser doesn't support WASM.
51
+ * @param {String} options.modernWasmUrl The URL of the modern worker WASM. This includes faster ASM instructions, but is only supported by newer browsers, disabled if the URL isn't defined.
50
52
  * @param {String} [options.subUrl=options.subContent] The URL of the subtitle file to play.
51
53
  * @param {String} [options.subContent=options.subUrl] The content of the subtitle file to play.
52
54
  * @param {String[]|Uint8Array[]} [options.fonts] An array of links or Uint8Arrays to the fonts used in the subtitle. If Uint8Array is used the array is copied, not referenced. This forces all the fonts in this array to be loaded by the renderer, regardless of if they are used.
@@ -61,9 +63,17 @@ export default class JASSUB extends EventTarget {
61
63
  if (!globalThis.Worker) {
62
64
  this.destroy('Worker not supported')
63
65
  }
66
+
67
+ this._loaded = new Promise(resolve => {
68
+ this._init = resolve
69
+ })
70
+
64
71
  JASSUB._test()
65
72
  this._onDemandRender = 'requestVideoFrameCallback' in HTMLVideoElement.prototype && (options.onDemandRender ?? true)
66
73
 
74
+ // don't support offscreen rendering on custom canvases, as we can't replace it if colorSpace doesn't match
75
+ this._offscreenRender = 'transferControlToOffscreen' in HTMLCanvasElement.prototype && !options.canvas && (options.offscreenRender ?? true)
76
+
67
77
  this.timeOffset = options.timeOffset || 0
68
78
  this._video = options.video
69
79
  this._videoHeight = 0
@@ -75,11 +85,7 @@ export default class JASSUB extends EventTarget {
75
85
  this._canvasParent.className = 'JASSUB'
76
86
  this._canvasParent.style.position = 'relative'
77
87
 
78
- this._canvas = document.createElement('canvas')
79
- this._canvas.style.display = 'block'
80
- this._canvas.style.position = 'absolute'
81
- this._canvas.style.pointerEvents = 'none'
82
- this._canvasParent.appendChild(this._canvas)
88
+ this._createCanvas()
83
89
 
84
90
  if (this._video.nextSibling) {
85
91
  this._video.parentNode.insertBefore(this._canvasParent, this._video.nextSibling)
@@ -93,8 +99,8 @@ export default class JASSUB extends EventTarget {
93
99
  this._bufferCanvas = document.createElement('canvas')
94
100
  this._bufferCtx = this._bufferCanvas.getContext('2d')
95
101
 
96
- this._canvasctrl = this._canvas
97
- this._ctx = this._canvasctrl.getContext('2d')
102
+ this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas
103
+ this._ctx = !this._offscreenRender && this._canvasctrl.getContext('2d')
98
104
 
99
105
  this._lastRenderTime = 0
100
106
  this.debug = !!options.debug
@@ -103,58 +109,67 @@ export default class JASSUB extends EventTarget {
103
109
  this.prescaleHeightLimit = options.prescaleHeightLimit || 1080
104
110
  this.maxRenderHeight = options.maxRenderHeight || 0 // 0 - no limit.
105
111
 
106
- this._worker = new Worker(JASSUB._supportsWebAssembly ? options.workerUrl || 'jassub-worker.js' : options.legacyWorkerUrl || 'jassub-worker-legacy.js')
107
- this._worker.onmessage = e => this._onmessage(e)
108
- this._worker.onerror = e => this._error(e)
112
+ this._boundResize = this.resize.bind(this)
113
+ this._boundTimeUpdate = this._timeupdate.bind(this)
114
+ this._boundSetRate = this.setRate.bind(this)
115
+ this._boundUpdateColorSpace = this._updateColorSpace.bind(this)
116
+ if (this._video) this.setVideo(options.video)
109
117
 
110
- this._loaded = new Promise(resolve => {
111
- this._init = () => {
112
- if (this._destroyed) return
113
- this._worker.postMessage({
114
- target: 'init',
115
- asyncRender: typeof createImageBitmap !== 'undefined' && (options.asyncRender ?? true),
116
- onDemandRender: this._onDemandRender,
117
- width: this._canvasctrl.width || 0,
118
- height: this._canvasctrl.height || 0,
119
- preMain: true,
120
- blendMode: options.blendMode || 'js',
121
- subUrl: options.subUrl,
122
- subContent: options.subContent || null,
123
- fonts: options.fonts || [],
124
- availableFonts: options.availableFonts || { 'liberation sans': './default.woff2' },
125
- fallbackFont: options.fallbackFont || 'liberation sans',
126
- debug: this.debug,
127
- targetFps: options.targetFps || 24,
128
- dropAllAnimations: options.dropAllAnimations,
129
- libassMemoryLimit: options.libassMemoryLimit || 0,
130
- libassGlyphLimit: options.libassGlyphLimit || 0,
131
- hasAlphaBug: JASSUB._hasAlphaBug,
132
- offscreenRender: typeof OffscreenCanvas !== 'undefined' && (options.offscreenRender ?? true),
133
- useLocalFonts: ('queryLocalFonts' in self) && (options.useLocalFonts ?? true)
134
- })
118
+ if (this._onDemandRender) {
119
+ this.busy = false
120
+ this._lastDemandTime = null
121
+ }
135
122
 
136
- this._boundResize = this.resize.bind(this)
137
- this._boundTimeUpdate = this._timeupdate.bind(this)
138
- this._boundSetRate = this.setRate.bind(this)
139
- this._boundUpdateColorSpace = this._updateColorSpace.bind(this)
140
- if (this._video) this.setVideo(options.video)
123
+ this._worker = new Worker(options.workerUrl || 'jassub-worker.js')
124
+ this._worker.onmessage = e => this._onmessage(e)
125
+ this._worker.onerror = e => this._error(e)
141
126
 
142
- if (this._onDemandRender) {
143
- this.busy = false
144
- this._lastDemandTime = null
145
- }
146
- resolve()
147
- }
127
+ this._worker.postMessage({
128
+ target: 'init',
129
+ wasmUrl: JASSUB._supportsSIMD && options.modernWasmUrl ? options.modernWasmUrl : options.wasmUrl || 'jassub-worker.wasm',
130
+ legacyWasmUrl: options.legacyWasmUrl || 'jassub-worker.wasm.js',
131
+ asyncRender: typeof createImageBitmap !== 'undefined' && (options.asyncRender ?? true),
132
+ onDemandRender: this._onDemandRender,
133
+ width: this._canvasctrl.width || 0,
134
+ height: this._canvasctrl.height || 0,
135
+ blendMode: options.blendMode || 'js',
136
+ subUrl: options.subUrl,
137
+ subContent: options.subContent || null,
138
+ fonts: options.fonts || [],
139
+ availableFonts: options.availableFonts || { 'liberation sans': './default.woff2' },
140
+ fallbackFont: options.fallbackFont || 'liberation sans',
141
+ debug: this.debug,
142
+ targetFps: options.targetFps || 24,
143
+ dropAllAnimations: options.dropAllAnimations,
144
+ dropAllBlur: options.dropAllBlur,
145
+ libassMemoryLimit: options.libassMemoryLimit || 0,
146
+ libassGlyphLimit: options.libassGlyphLimit || 0,
147
+ useLocalFonts: typeof queryLocalFonts !== 'undefined' && (options.useLocalFonts ?? true)
148
148
  })
149
+ if (this._offscreenRender === true) this.sendMessage('offscreenCanvas', null, [this._canvasctrl])
150
+ }
151
+
152
+ _createCanvas () {
153
+ this._canvas = document.createElement('canvas')
154
+ this._canvas.style.display = 'block'
155
+ this._canvas.style.position = 'absolute'
156
+ this._canvas.style.pointerEvents = 'none'
157
+ this._canvasParent.appendChild(this._canvas)
149
158
  }
150
159
 
151
160
  // test support for WASM, ImageData, alphaBug, but only once, on init so it doesn't run when first running the page
152
- static _supportsWebAssembly = null
161
+ static _supportsSIMD = null
153
162
  static _hasAlphaBug = null
154
163
 
155
164
  static _test () {
156
165
  // check if ran previously
157
- if (JASSUB._supportsWebAssembly !== null) return null
166
+ if (JASSUB._supportsSIMD !== null) return null
167
+
168
+ try {
169
+ JASSUB._supportsSIMD = WebAssembly.validate(Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11))
170
+ } catch (e) {
171
+ JASSUB._supportsSIMD = false
172
+ }
158
173
 
159
174
  const canvas1 = document.createElement('canvas')
160
175
  const ctx1 = canvas1.getContext('2d', { willReadFrequently: true })
@@ -176,15 +191,6 @@ export default class JASSUB extends EventTarget {
176
191
  }
177
192
  }
178
193
 
179
- try {
180
- if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') {
181
- const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00))
182
- if (module instanceof WebAssembly.Module) JASSUB._supportsWebAssembly = (new WebAssembly.Instance(module) instanceof WebAssembly.Instance)
183
- }
184
- } catch (e) {
185
- JASSUB._supportsWebAssembly = false
186
- }
187
-
188
194
  // Test for alpha bug, where e.g. WebKit can render a transparent pixel
189
195
  // (with alpha == 0) as non-black which then leads to visual artifacts.
190
196
  const canvas2 = document.createElement('canvas')
@@ -236,7 +242,12 @@ export default class JASSUB extends EventTarget {
236
242
 
237
243
  this._canvas.style.top = top + 'px'
238
244
  this._canvas.style.left = left + 'px'
239
- this.sendMessage('canvas', { width, height, force: force && this.busy === false })
245
+ if (force && this.busy === false) {
246
+ this.busy = true
247
+ } else {
248
+ force = false
249
+ }
250
+ this.sendMessage('canvas', { width, height, force })
240
251
  }
241
252
 
242
253
  _getVideoPosition (width = this._video.videoWidth, height = this._video.videoHeight) {
@@ -295,15 +306,6 @@ export default class JASSUB extends EventTarget {
295
306
  this.setCurrentTime(this._video.paused || this._playstate, this._video.currentTime + this.timeOffset)
296
307
  }
297
308
 
298
- _updateColorSpace () {
299
- this._video.requestVideoFrameCallback(() => {
300
- // eslint-disable-next-line no-undef
301
- const frame = new VideoFrame(this._video)
302
- this._videoColorSpace = webYCbCrMap[frame.colorSpace.matrix]
303
- frame.close()
304
- })
305
- }
306
-
307
309
  /**
308
310
  * Change the video to use as target for event listeners.
309
311
  * @param {HTMLVideoElement} video
@@ -351,6 +353,8 @@ export default class JASSUB extends EventTarget {
351
353
  */
352
354
  setTrackByUrl (url) {
353
355
  this.sendMessage('setTrackByUrl', { url })
356
+ this._reAttachOffscreen()
357
+ if (this._ctx) this._ctx.filter = 'none'
354
358
  }
355
359
 
356
360
  /**
@@ -359,6 +363,8 @@ export default class JASSUB extends EventTarget {
359
363
  */
360
364
  setTrack (content) {
361
365
  this.sendMessage('setTrack', { content })
366
+ this._reAttachOffscreen()
367
+ if (this._ctx) this._ctx.filter = 'none'
362
368
  }
363
369
 
364
370
  /**
@@ -391,7 +397,7 @@ export default class JASSUB extends EventTarget {
391
397
  * @param {Number} [rate] Playback rate.
392
398
  */
393
399
  setCurrentTime (isPaused, currentTime, rate) {
394
- this.sendMessage('video', { isPaused, currentTime, rate })
400
+ this.sendMessage('video', { isPaused, currentTime, rate, colorSpace: this._videoColorSpace })
395
401
  }
396
402
 
397
403
  /**
@@ -587,29 +593,69 @@ export default class JASSUB extends EventTarget {
587
593
  this.sendMessage('demand', { time: mediaTime + this.timeOffset })
588
594
  }
589
595
 
596
+ // if we're using offscreen render, we can't use ctx filters, so we can't use a transfered canvas
597
+ _detachOffscreen () {
598
+ if (!this._offscreenRender || this._ctx) return null
599
+ this._canvas.remove()
600
+ this._createCanvas()
601
+ this._canvasctrl = this._canvas
602
+ this._ctx = this._canvasctrl.getContext('2d')
603
+ this.sendMessage('detachOffscreen')
604
+ // force a render after resize
605
+ this.busy = false
606
+ this.resize(0, 0, 0, 0, true)
607
+ }
608
+
609
+ // if the video or track changed, we need to re-attach the offscreen canvas
610
+ _reAttachOffscreen () {
611
+ if (!this._offscreenRender || !this._ctx) return null
612
+ this._canvas.remove()
613
+ this._createCanvas()
614
+ this._canvasctrl = this._canvas.transferControlToOffscreen()
615
+ this._ctx = false
616
+ this.sendMessage('offscreenCanvas', null, [this._canvasctrl])
617
+ this.resize(0, 0, 0, 0, true)
618
+ }
619
+
620
+ _updateColorSpace () {
621
+ this._video.requestVideoFrameCallback(() => {
622
+ try {
623
+ // eslint-disable-next-line no-undef
624
+ const frame = new VideoFrame(this._video)
625
+ this._videoColorSpace = webYCbCrMap[frame.colorSpace.matrix]
626
+ frame.close()
627
+ this.sendMessage('getColorSpace')
628
+ } catch (e) {
629
+ // sources can be tainted
630
+ console.warn(e)
631
+ }
632
+ })
633
+ }
634
+
590
635
  /**
591
636
  * Veryify the color spaces for subtitles and videos, then apply filters to correct the color of subtitles.
592
- * @param {String} subtitleColorSpace Subtitle color space. One of: BT.601 BT.709 SMPTE240M FCC
593
- * @param {String} videoColorSpace Video color space. One of: BT.601 BT.709
637
+ * @param {String} subtitleColorSpace Subtitle color space. One of: BT601 BT709 SMPTE240M FCC
638
+ * @param {String} videoColorSpace Video color space. One of: BT601 BT709
594
639
  */
595
- verifyColorSpace (subtitleColorSpace, videoColorSpace = this._videoColorSpace) {
640
+ _verifyColorSpace ({ subtitleColorSpace, videoColorSpace = this._videoColorSpace }) {
596
641
  if (!subtitleColorSpace || !videoColorSpace) return
597
642
  if (subtitleColorSpace === videoColorSpace) return
598
- this._ctx.filter = colorMatrixConversionMap[subtitleColorSpace][videoColorSpace]
643
+ this._detachOffscreen()
644
+ this._ctx.filter = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${colorMatrixConversionMap[subtitleColorSpace][videoColorSpace]} 0 0 0 0 0 1 0'/></filter></svg>#f")`
599
645
  }
600
646
 
601
- _render ({ images, async, times, width, height, colorSpace }) {
647
+ _render ({ images, asyncRender, times, width, height, colorSpace }) {
602
648
  this._unbusy()
603
- const drawStartTime = Date.now()
649
+ if (this.debug) times.IPCTime = Date.now() - times.JSRenderTime
604
650
  if (this._canvasctrl.width !== width || this._canvasctrl.height !== height) {
605
651
  this._canvasctrl.width = width
606
652
  this._canvasctrl.height = height
607
- this.verifyColorSpace(colorSpace)
653
+ this._verifyColorSpace({ subtitleColorSpace: colorSpace })
608
654
  }
609
655
  this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height)
610
656
  for (const image of images) {
611
657
  if (image.image) {
612
- if (async) {
658
+ if (asyncRender) {
613
659
  this._ctx.drawImage(image.image, image.x, image.y)
614
660
  image.image.close()
615
661
  } else {
@@ -621,10 +667,12 @@ export default class JASSUB extends EventTarget {
621
667
  }
622
668
  }
623
669
  if (this.debug) {
624
- times.drawTime = Date.now() - drawStartTime
670
+ times.JSRenderTime = Date.now() - times.JSRenderTime - times.IPCTime
625
671
  let total = 0
672
+ const count = times.bitmaps || images.length
673
+ delete times.bitmaps
626
674
  for (const key in times) total += times[key]
627
- console.log('Bitmaps: ' + images.length + ' Total: ' + Math.round(total) + 'ms', times)
675
+ console.log('Bitmaps: ' + count + ' Total: ' + (total | 0) + 'ms', times)
628
676
  }
629
677
  }
630
678
 
@@ -724,7 +772,7 @@ export default class JASSUB extends EventTarget {
724
772
  _removeListeners () {
725
773
  if (this._video) {
726
774
  if (this._ro) this._ro.unobserve(this._video)
727
- this._ctx.filter = 'none'
775
+ if (this._ctx) this._ctx.filter = 'none'
728
776
  this._video.removeEventListener('timeupdate', this._boundTimeUpdate)
729
777
  this._video.removeEventListener('progress', this._boundTimeUpdate)
730
778
  this._video.removeEventListener('waiting', this._boundTimeUpdate)