jassub 1.2.4 → 1.4.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.
package/dist/jassub.es.js CHANGED
@@ -41,7 +41,7 @@ if (!("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPla
41
41
  }
42
42
  const _JASSUB = class extends EventTarget {
43
43
  constructor(options = {}) {
44
- var _a, _b, _c, _d, _e;
44
+ var _a, _b, _c;
45
45
  super();
46
46
  if (!globalThis.Worker) {
47
47
  this.destroy("Worker not supported");
@@ -53,6 +53,8 @@ const _JASSUB = class extends EventTarget {
53
53
  this._onDemandRender = "requestVideoFrameCallback" in HTMLVideoElement.prototype && ((_c = options.onDemandRender) != null ? _c : true);
54
54
  this.timeOffset = options.timeOffset || 0;
55
55
  this._video = options.video;
56
+ this._videoHeight = 0;
57
+ this._videoWidth = 0;
56
58
  this._canvas = options.canvas;
57
59
  if (this._video && !this._canvas) {
58
60
  this._canvasParent = document.createElement("div");
@@ -83,39 +85,44 @@ const _JASSUB = class extends EventTarget {
83
85
  this._worker = new Worker(_JASSUB._supportsWebAssembly ? options.workerUrl || "jassub-worker.js" : options.legacyWorkerUrl || "jassub-worker-legacy.js");
84
86
  this._worker.onmessage = (e) => this._onmessage(e);
85
87
  this._worker.onerror = (e) => this._error(e);
86
- this._worker.postMessage({
87
- target: "init",
88
- asyncRender,
89
- onDemandRender: this._onDemandRender,
90
- width: this._canvas.width,
91
- height: this._canvas.height,
92
- preMain: true,
93
- blendMode,
94
- subUrl: options.subUrl,
95
- subContent: options.subContent || null,
96
- fonts: options.fonts || [],
97
- availableFonts: options.availableFonts || { "liberation sans": "./default.woff2" },
98
- fallbackFont: options.fallbackFont || "liberation sans",
99
- debug: this.debug,
100
- targetFps: options.targetFps || 24,
101
- dropAllAnimations: options.dropAllAnimations,
102
- libassMemoryLimit: options.libassMemoryLimit || 0,
103
- libassGlyphLimit: options.libassGlyphLimit || 0,
104
- hasAlphaBug: _JASSUB._hasAlphaBug,
105
- useLocalFonts: "queryLocalFonts" in self && ((_d = options.useLocalFonts) != null ? _d : true)
106
- });
107
- if (offscreenRender === true)
108
- this.sendMessage("offscreenCanvas", null, [this._canvasctrl]);
109
- this._boundResize = this.resize.bind(this);
110
- this._boundTimeUpdate = this._timeupdate.bind(this);
111
- this._boundSetRate = this.setRate.bind(this);
112
- if (this._video)
113
- this.setVideo(options.video);
114
- if (this._onDemandRender) {
115
- this.busy = false;
116
- this._lastDemandTime = null;
117
- (_e = this._video) == null ? void 0 : _e.requestVideoFrameCallback(this._handleRVFC.bind(this));
118
- }
88
+ this._init = () => {
89
+ var _a2, _b2;
90
+ if (this._destroyed)
91
+ return;
92
+ this._worker.postMessage({
93
+ target: "init",
94
+ asyncRender,
95
+ onDemandRender: this._onDemandRender,
96
+ width: this._canvasctrl.width,
97
+ height: this._canvasctrl.height,
98
+ preMain: true,
99
+ blendMode,
100
+ subUrl: options.subUrl,
101
+ subContent: options.subContent || null,
102
+ fonts: options.fonts || [],
103
+ availableFonts: options.availableFonts || { "liberation sans": "./default.woff2" },
104
+ fallbackFont: options.fallbackFont || "liberation sans",
105
+ debug: this.debug,
106
+ targetFps: options.targetFps || 24,
107
+ dropAllAnimations: options.dropAllAnimations,
108
+ libassMemoryLimit: options.libassMemoryLimit || 0,
109
+ libassGlyphLimit: options.libassGlyphLimit || 0,
110
+ hasAlphaBug: _JASSUB._hasAlphaBug,
111
+ useLocalFonts: "queryLocalFonts" in self && ((_a2 = options.useLocalFonts) != null ? _a2 : true)
112
+ });
113
+ if (offscreenRender === true)
114
+ this.sendMessage("offscreenCanvas", null, [this._canvasctrl]);
115
+ this._boundResize = this.resize.bind(this);
116
+ this._boundTimeUpdate = this._timeupdate.bind(this);
117
+ this._boundSetRate = this.setRate.bind(this);
118
+ if (this._video)
119
+ this.setVideo(options.video);
120
+ if (this._onDemandRender) {
121
+ this.busy = false;
122
+ this._lastDemandTime = null;
123
+ (_b2 = this._video) == null ? void 0 : _b2.requestVideoFrameCallback(this._handleRVFC.bind(this));
124
+ }
125
+ };
119
126
  }
120
127
  static _test() {
121
128
  if (_JASSUB._supportsWebAssembly !== null)
@@ -160,49 +167,36 @@ const _JASSUB = class extends EventTarget {
160
167
  canvas1.remove();
161
168
  canvas2.remove();
162
169
  }
163
- resize(width = 0, height = 0, top = 0, left = 0) {
164
- let videoSize = null;
170
+ resize(width = 0, height = 0, top = 0, left = 0, force = ((_a) => (_a = this._video) == null ? void 0 : _a.paused)()) {
165
171
  if ((!width || !height) && this._video) {
166
- videoSize = this._getVideoPosition();
167
- const newsize = this._computeCanvasSize((videoSize.width || 0) * (self.devicePixelRatio || 1), (videoSize.height || 0) * (self.devicePixelRatio || 1));
168
- width = newsize.width;
169
- height = newsize.height;
172
+ const videoSize = this._getVideoPosition();
173
+ let renderSize = null;
174
+ if (this._videoWidth) {
175
+ const widthRatio = this._video.videoWidth / this._videoWidth;
176
+ const heightRatio = this._video.videoHeight / this._videoHeight;
177
+ renderSize = this._computeCanvasSize((videoSize.width || 0) / widthRatio, (videoSize.height || 0) / heightRatio);
178
+ } else {
179
+ renderSize = this._computeCanvasSize(videoSize.width || 0, videoSize.height || 0);
180
+ }
181
+ width = renderSize.width;
182
+ height = renderSize.height;
170
183
  if (this._canvasParent) {
171
184
  top = videoSize.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top);
172
185
  left = videoSize.x;
173
186
  }
174
- }
175
- if (videoSize != null) {
176
- this._canvas.style.top = top + "px";
177
- this._canvas.style.left = left + "px";
178
187
  this._canvas.style.width = videoSize.width + "px";
179
188
  this._canvas.style.height = videoSize.height + "px";
180
189
  }
181
- if (!(this._canvasctrl.width === width && this._canvasctrl.height === height)) {
182
- if (this._resizeTimeoutBuffer) {
183
- clearTimeout(this._resizeTimeoutBuffer);
184
- this._resizeTimeoutBuffer = setTimeout(() => {
185
- this._resizeTimeoutBuffer = void 0;
186
- this._canvasctrl.width = width;
187
- this._canvasctrl.height = height;
188
- this.sendMessage("canvas", { width, height });
189
- }, 100);
190
- } else {
191
- this._canvasctrl.width = width;
192
- this._canvasctrl.height = height;
193
- this.sendMessage("canvas", { width, height });
194
- this._resizeTimeoutBuffer = setTimeout(() => {
195
- this._resizeTimeoutBuffer = void 0;
196
- }, 100);
197
- }
198
- }
190
+ this._canvas.style.top = top + "px";
191
+ this._canvas.style.left = left + "px";
192
+ this.sendMessage("canvas", { width, height, force: force && this.busy === false });
199
193
  }
200
- _getVideoPosition() {
201
- const videoRatio = this._video.videoWidth / this._video.videoHeight;
194
+ _getVideoPosition(width = this._video.videoWidth, height = this._video.videoHeight) {
195
+ const videoRatio = width / height;
202
196
  const { offsetWidth, offsetHeight } = this._video;
203
197
  const elementRatio = offsetWidth / offsetHeight;
204
- let width = offsetWidth;
205
- let height = offsetHeight;
198
+ width = offsetWidth;
199
+ height = offsetHeight;
206
200
  if (elementRatio > videoRatio) {
207
201
  width = Math.floor(offsetHeight * videoRatio);
208
202
  } else {
@@ -214,12 +208,13 @@ const _JASSUB = class extends EventTarget {
214
208
  }
215
209
  _computeCanvasSize(width = 0, height = 0) {
216
210
  const scalefactor = this.prescaleFactor <= 0 ? 1 : this.prescaleFactor;
211
+ const ratio = self.devicePixelRatio || 1;
217
212
  if (height <= 0 || width <= 0) {
218
213
  width = 0;
219
214
  height = 0;
220
215
  } else {
221
216
  const sgn = scalefactor < 1 ? -1 : 1;
222
- let newH = height;
217
+ let newH = height * ratio;
223
218
  if (sgn * newH * scalefactor <= sgn * this.prescaleHeightLimit) {
224
219
  newH *= scalefactor;
225
220
  } else if (sgn * newH < sgn * this.prescaleHeightLimit) {
@@ -227,7 +222,7 @@ const _JASSUB = class extends EventTarget {
227
222
  }
228
223
  if (this.maxRenderHeight > 0 && newH > this.maxRenderHeight)
229
224
  newH = this.maxRenderHeight;
230
- width *= newH / height;
225
+ width *= ratio * newH / height;
231
226
  height = newH;
232
227
  }
233
228
  return { width, height };
@@ -257,10 +252,10 @@ const _JASSUB = class extends EventTarget {
257
252
  video.addEventListener("seeking", this._boundTimeUpdate, false);
258
253
  video.addEventListener("playing", this._boundTimeUpdate, false);
259
254
  video.addEventListener("ratechange", this._boundSetRate, false);
255
+ video.addEventListener("resize", this._boundResize);
260
256
  }
261
257
  if (video.videoWidth > 0)
262
258
  this.resize();
263
- video.addEventListener("resize", this._boundResize);
264
259
  if (typeof ResizeObserver !== "undefined") {
265
260
  if (!this._ro)
266
261
  this._ro = new ResizeObserver(() => this.resize());
@@ -365,24 +360,35 @@ const _JASSUB = class extends EventTarget {
365
360
  this.busy = false;
366
361
  }
367
362
  }
368
- _handleRVFC(now, { mediaTime }) {
363
+ _handleRVFC(now, { mediaTime, width, height }) {
369
364
  if (this._destroyed)
370
365
  return null;
371
366
  if (this.busy) {
372
- this._lastDemandTime = mediaTime;
367
+ this._lastDemandTime = { mediaTime, width, height };
373
368
  } else {
374
369
  this.busy = true;
375
- this._demandRender(mediaTime);
370
+ this._demandRender({ mediaTime, width, height });
376
371
  }
377
372
  this._video.requestVideoFrameCallback(this._handleRVFC.bind(this));
378
373
  }
379
- _demandRender(time) {
374
+ _demandRender({ mediaTime, width, height }) {
380
375
  this._lastDemandTime = null;
381
- this.sendMessage("demand", { time: time + this.timeOffset });
376
+ if (width !== this._videoWidth || height !== this._videoHeight) {
377
+ this._videoWidth = width;
378
+ this._videoHeight = height;
379
+ this.resize();
380
+ }
381
+ this.sendMessage("demand", { time: mediaTime + this.timeOffset });
382
382
  }
383
- _render({ images, async, times }) {
383
+ _render({ images, async, times, width, height }) {
384
+ this._unbusy();
384
385
  const drawStartTime = Date.now();
385
- this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
386
+ if (this._canvasctrl.width !== width || this._canvasctrl.height !== height) {
387
+ this._canvasctrl.width = width;
388
+ this._canvasctrl.height = height;
389
+ } else {
390
+ this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
391
+ }
386
392
  for (const image of images) {
387
393
  if (image.image) {
388
394
  if (async) {
@@ -413,6 +419,7 @@ const _JASSUB = class extends EventTarget {
413
419
  return uint8;
414
420
  }
415
421
  _ready() {
422
+ this._init();
416
423
  this.dispatchEvent(new CustomEvent("ready"));
417
424
  }
418
425
  sendMessage(target, data = {}, transferable) {
@@ -1 +1 @@
1
- (function(h,n){typeof exports=="object"&&typeof module!="undefined"?module.exports=n():typeof define=="function"&&define.amd?define(n):(h=typeof globalThis!="undefined"?globalThis:h||self,h.JASSUB=n())})(this,function(){"use strict";var _=Object.defineProperty;var v=(h,n,d)=>n in h?_(h,n,{enumerable:!0,configurable:!0,writable:!0,value:d}):h[n]=d;var f=(h,n,d)=>(v(h,typeof n!="symbol"?n+"":n,d),d);!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(d){const e=this.getVideoPlaybackQuality(),t=this.mozPresentedFrames||this.mozPaintedFrames||e.totalVideoFrames-e.droppedVideoFrames,s=(r,o)=>{const l=this.getVideoPlaybackQuality(),m=this.mozPresentedFrames||this.mozPaintedFrames||l.totalVideoFrames-l.droppedVideoFrames;if(m>t){const c=this.mozFrameDelay||l.totalFrameDelay-e.totalFrameDelay||0,u=o-r;d(o,{presentationTime:o+c*1e3,expectedDisplayTime:o+u,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+u/1e3,presentedFrames:m,processingDuration:c}),delete this._rvfcpolyfillmap[a]}else this._rvfcpolyfillmap[a]=requestAnimationFrame(c=>s(o,c))},a=Date.now(),i=performance.now();return this._rvfcpolyfillmap[a]=requestAnimationFrame(r=>s(i,r)),a},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(d){cancelAnimationFrame(this._rvfcpolyfillmap[d]),delete this._rvfcpolyfillmap[d]});const n=class extends EventTarget{constructor(e={}){var i,r,o,l,m;super(),globalThis.Worker||this.destroy("Worker not supported"),n._test();const t=e.blendMode||"js",s=typeof createImageBitmap!="undefined"&&((i=e.asyncRender)!=null?i:!0),a=typeof OffscreenCanvas!="undefined"&&((r=e.offscreenRender)!=null?r:!0);this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&((o=e.onDemandRender)!=null?o:!0),this.timeOffset=e.timeOffset||0,this._video=e.video,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",{desynchronized:!0,willReadFrequently:!0}),this._canvasctrl=a?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!a&&this._canvasctrl.getContext("2d",{desynchronized:!0}),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(n._supportsWebAssembly?e.workerUrl||"jassub-worker.js":e.legacyWorkerUrl||"jassub-worker-legacy.js"),this._worker.onmessage=c=>this._onmessage(c),this._worker.onerror=c=>this._error(c),this._worker.postMessage({target:"init",asyncRender:s,onDemandRender:this._onDemandRender,width:this._canvas.width,height:this._canvas.height,preMain:!0,blendMode:t,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:n._hasAlphaBug,useLocalFonts:"queryLocalFonts"in self&&((l=e.useLocalFonts)!=null?l:!0)}),a===!0&&this.sendMessage("offscreenCanvas",null,[this._canvasctrl]),this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=this.setRate.bind(this),this._video&&this.setVideo(e.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null,(m=this._video)==null||m.requestVideoFrameCallback(this._handleRVFC.bind(this)))}static _test(){if(n._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(l,m,c){const u=t.createImageData(m,c);return l&&u.data.set(l),u}}try{if(typeof WebAssembly=="object"&&typeof WebAssembly.instantiate=="function"){const o=new WebAssembly.Module(Uint8Array.of(0,97,115,109,1,0,0,0));o instanceof WebAssembly.Module&&(n._supportsWebAssembly=new WebAssembly.Instance(o)instanceof WebAssembly.Instance)}}catch{n._supportsWebAssembly=!1}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 i=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 r=a.getImageData(0,0,1,1).data;n._hasAlphaBug=i[1]!==r[1],n._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){let i=null;if((!e||!t)&&this._video){i=this._getVideoPosition();const r=this._computeCanvasSize((i.width||0)*(self.devicePixelRatio||1),(i.height||0)*(self.devicePixelRatio||1));e=r.width,t=r.height,this._canvasParent&&(s=i.y-(this._canvasParent.getBoundingClientRect().top-this._video.getBoundingClientRect().top),a=i.x)}i!=null&&(this._canvas.style.top=s+"px",this._canvas.style.left=a+"px",this._canvas.style.width=i.width+"px",this._canvas.style.height=i.height+"px"),this._canvasctrl.width===e&&this._canvasctrl.height===t||(this._resizeTimeoutBuffer?(clearTimeout(this._resizeTimeoutBuffer),this._resizeTimeoutBuffer=setTimeout(()=>{this._resizeTimeoutBuffer=void 0,this._canvasctrl.width=e,this._canvasctrl.height=t,this.sendMessage("canvas",{width:e,height:t})},100)):(this._canvasctrl.width=e,this._canvasctrl.height=t,this.sendMessage("canvas",{width:e,height:t}),this._resizeTimeoutBuffer=setTimeout(()=>{this._resizeTimeoutBuffer=void 0},100)))}_getVideoPosition(){const e=this._video.videoWidth/this._video.videoHeight,{offsetWidth:t,offsetHeight:s}=this._video,a=t/s;let i=t,r=s;a>e?i=Math.floor(s*e):r=Math.floor(t/e);const o=(t-i)/2,l=(s-r)/2;return{width:i,height:r,x:o,y:l}}_computeCanvasSize(e=0,t=0){const s=this.prescaleFactor<=0?1:this.prescaleFactor;if(t<=0||e<=0)e=0,t=0;else{const a=s<1?-1:1;let i=t;a*i*s<=a*this.prescaleHeightLimit?i*=s:a*i<a*this.prescaleHeightLimit&&(i=this.prescaleHeightLimit),this.maxRenderHeight>0&&i>this.maxRenderHeight&&(i=this.maxRenderHeight),e*=i/t,t=i}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.videoWidth>0&&this.resize(),e.addEventListener("resize",this._boundResize),typeof ResizeObserver!="undefined"&&(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(a=>a.fullName.toLowerCase()===e);s&&s.blob().then(a=>{a.arrayBuffer().then(i=>{this.addFont(new Uint8Array(i))})})})}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}){if(this._destroyed)return null;this.busy?this._lastDemandTime=t:(this.busy=!0,this._demandRender(t)),this._video.requestVideoFrameCallback(this._handleRVFC.bind(this))}_demandRender(e){this._lastDemandTime=null,this.sendMessage("demand",{time:e+this.timeOffset})}_render({images:e,async:t,times:s}){const a=Date.now();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.drawTime=Date.now()-a;let i=0;for(const r in s)i+=s[r];console.log("Bitmaps: "+e.length+" Total: "+Math.round(i)+"ms",s)}}_fixAlpha(e){if(n._hasAlphaBug)for(let t=3;t<e.length;t+=4)e[t]=e[t]>1?e[t]:1;return e}_ready(){this.dispatchEvent(new CustomEvent("ready"))}sendMessage(e,t={},s){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(()=>{r(new Error("Error: Timeout while try to fetch "+s))},5e3),i=({data:o})=>{o.target===s&&(t(null,o),this._worker.removeEventListener("message",i),this._worker.removeEventListener("error",r),clearTimeout(a))},r=o=>{t(o),this._worker.removeEventListener("message",i),this._worker.removeEventListener("error",r),clearTimeout(a)};this._worker.addEventListener("message",i),this._worker.addEventListener("error",r),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){this.dispatchEvent(e instanceof ErrorEvent?e:new ErrorEvent("error",{cause:e instanceof Error?e.cause:e})),e instanceof Error||(e instanceof ErrorEvent?e=e.error:e=new Error("error",{cause:e})),console.error(e)}_removeListeners(){this._video&&(this._ro&&this._ro.unobserve(this._video),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))}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=n;return f(h,"_supportsWebAssembly",null),f(h,"_hasAlphaBug",null),h});
1
+ (function(d,o){typeof exports=="object"&&typeof module!="undefined"?module.exports=o():typeof define=="function"&&define.amd?define(o):(d=typeof globalThis!="undefined"?globalThis:d||self,d.JASSUB=o())})(this,function(){"use strict";var v=Object.defineProperty;var f=(d,o,l)=>o in d?v(d,o,{enumerable:!0,configurable:!0,writable:!0,value:l}):d[o]=l;var u=(d,o,l)=>(f(d,typeof o!="symbol"?o+"":o,l),l);!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(l){const e=this.getVideoPlaybackQuality(),t=this.mozPresentedFrames||this.mozPaintedFrames||e.totalVideoFrames-e.droppedVideoFrames,s=(n,i)=>{const h=this.getVideoPlaybackQuality(),c=this.mozPresentedFrames||this.mozPaintedFrames||h.totalVideoFrames-h.droppedVideoFrames;if(c>t){const m=this.mozFrameDelay||h.totalFrameDelay-e.totalFrameDelay||0,_=i-n;l(i,{presentationTime:i+m*1e3,expectedDisplayTime:i+_,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+_/1e3,presentedFrames:c,processingDuration:m}),delete this._rvfcpolyfillmap[a]}else this._rvfcpolyfillmap[a]=requestAnimationFrame(m=>s(i,m))},a=Date.now(),r=performance.now();return this._rvfcpolyfillmap[a]=requestAnimationFrame(n=>s(r,n)),a},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(l){cancelAnimationFrame(this._rvfcpolyfillmap[l]),delete this._rvfcpolyfillmap[l]});const o=class extends EventTarget{constructor(e={}){var r,n,i;super(),globalThis.Worker||this.destroy("Worker not supported"),o._test();const t=e.blendMode||"js",s=typeof createImageBitmap!="undefined"&&((r=e.asyncRender)!=null?r:!0),a=typeof OffscreenCanvas!="undefined"&&((n=e.offscreenRender)!=null?n:!0);this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&((i=e.onDemandRender)!=null?i:!0),this.timeOffset=e.timeOffset||0,this._video=e.video,this._videoHeight=0,this._videoWidth=0,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",{desynchronized:!0,willReadFrequently:!0}),this._canvasctrl=a?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!a&&this._canvasctrl.getContext("2d",{desynchronized:!0}),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(o._supportsWebAssembly?e.workerUrl||"jassub-worker.js":e.legacyWorkerUrl||"jassub-worker-legacy.js"),this._worker.onmessage=h=>this._onmessage(h),this._worker.onerror=h=>this._error(h),this._init=()=>{var h,c;this._destroyed||(this._worker.postMessage({target:"init",asyncRender:s,onDemandRender:this._onDemandRender,width:this._canvasctrl.width,height:this._canvasctrl.height,preMain:!0,blendMode:t,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:o._hasAlphaBug,useLocalFonts:"queryLocalFonts"in self&&((h=e.useLocalFonts)!=null?h:!0)}),a===!0&&this.sendMessage("offscreenCanvas",null,[this._canvasctrl]),this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=this.setRate.bind(this),this._video&&this.setVideo(e.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null,(c=this._video)==null||c.requestVideoFrameCallback(this._handleRVFC.bind(this))))}}static _test(){if(o._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(h,c,m){const _=t.createImageData(c,m);return h&&_.data.set(h),_}}try{if(typeof WebAssembly=="object"&&typeof WebAssembly.instantiate=="function"){const i=new WebAssembly.Module(Uint8Array.of(0,97,115,109,1,0,0,0));i instanceof WebAssembly.Module&&(o._supportsWebAssembly=new WebAssembly.Instance(i)instanceof WebAssembly.Instance)}}catch{o._supportsWebAssembly=!1}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;o._hasAlphaBug=r[1]!==n[1],o._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=(n=>(n=this._video)==null?void 0:n.paused)()){if((!e||!t)&&this._video){const i=this._getVideoPosition();let h=null;if(this._videoWidth){const c=this._video.videoWidth/this._videoWidth,m=this._video.videoHeight/this._videoHeight;h=this._computeCanvasSize((i.width||0)/c,(i.height||0)/m)}else h=this._computeCanvasSize(i.width||0,i.height||0);e=h.width,t=h.height,this._canvasParent&&(s=i.y-(this._canvasParent.getBoundingClientRect().top-this._video.getBoundingClientRect().top),a=i.x),this._canvas.style.width=i.width+"px",this._canvas.style.height=i.height+"px"}this._canvas.style.top=s+"px",this._canvas.style.left=a+"px",this.sendMessage("canvas",{width:e,height:t,force:r&&this.busy===!1})}_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,h=(r-t)/2;return{width:e,height:t,x:i,y:h}}_computeCanvasSize(e=0,t=0){const s=this.prescaleFactor<=0?1:this.prescaleFactor,a=self.devicePixelRatio||1;if(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*=a*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)),e.videoWidth>0&&this.resize(),typeof ResizeObserver!="undefined"&&(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(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}){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: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})}_render({images:e,async:t,times:s,width:a,height:r}){this._unbusy();const n=Date.now();this._canvasctrl.width!==a||this._canvasctrl.height!==r?(this._canvasctrl.width=a,this._canvasctrl.height=r):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.drawTime=Date.now()-n;let i=0;for(const h in s)i+=s[h];console.log("Bitmaps: "+e.length+" Total: "+Math.round(i)+"ms",s)}}_fixAlpha(e){if(o._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"))}sendMessage(e,t={},s){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){this.dispatchEvent(e instanceof ErrorEvent?e:new ErrorEvent("error",{cause:e instanceof Error?e.cause:e})),e instanceof Error||(e instanceof ErrorEvent?e=e.error:e=new Error("error",{cause:e})),console.error(e)}_removeListeners(){this._video&&(this._ro&&this._ro.unobserve(this._video),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))}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 d=o;return u(d,"_supportsWebAssembly",null),u(d,"_hasAlphaBug",null),d});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jassub",
3
- "version": "1.2.4",
3
+ "version": "1.4.0",
4
4
  "description": "libass Subtitle Renderer and Parser library for browsers",
5
5
  "main": "src/jassub.js",
6
6
  "files": [
package/src/jassub.js CHANGED
@@ -44,6 +44,8 @@ export default class JASSUB extends EventTarget {
44
44
 
45
45
  this.timeOffset = options.timeOffset || 0
46
46
  this._video = options.video
47
+ this._videoHeight = 0
48
+ this._videoWidth = 0
47
49
  this._canvas = options.canvas
48
50
  if (this._video && !this._canvas) {
49
51
  this._canvasParent = document.createElement('div')
@@ -82,38 +84,41 @@ export default class JASSUB extends EventTarget {
82
84
  this._worker.onmessage = e => this._onmessage(e)
83
85
  this._worker.onerror = e => this._error(e)
84
86
 
85
- this._worker.postMessage({
86
- target: 'init',
87
- asyncRender,
88
- onDemandRender: this._onDemandRender,
89
- width: this._canvas.width,
90
- height: this._canvas.height,
91
- preMain: true,
92
- blendMode,
93
- subUrl: options.subUrl,
94
- subContent: options.subContent || null,
95
- fonts: options.fonts || [],
96
- availableFonts: options.availableFonts || { 'liberation sans': './default.woff2' },
97
- fallbackFont: options.fallbackFont || 'liberation sans',
98
- debug: this.debug,
99
- targetFps: options.targetFps || 24,
100
- dropAllAnimations: options.dropAllAnimations,
101
- libassMemoryLimit: options.libassMemoryLimit || 0,
102
- libassGlyphLimit: options.libassGlyphLimit || 0,
103
- hasAlphaBug: JASSUB._hasAlphaBug,
104
- useLocalFonts: ('queryLocalFonts' in self) && (options.useLocalFonts ?? true)
105
- })
106
- if (offscreenRender === true) this.sendMessage('offscreenCanvas', null, [this._canvasctrl])
87
+ this._init = () => {
88
+ if (this._destroyed) return
89
+ this._worker.postMessage({
90
+ target: 'init',
91
+ asyncRender,
92
+ onDemandRender: this._onDemandRender,
93
+ width: this._canvasctrl.width,
94
+ height: this._canvasctrl.height,
95
+ preMain: true,
96
+ blendMode,
97
+ subUrl: options.subUrl,
98
+ subContent: options.subContent || null,
99
+ fonts: options.fonts || [],
100
+ availableFonts: options.availableFonts || { 'liberation sans': './default.woff2' },
101
+ fallbackFont: options.fallbackFont || 'liberation sans',
102
+ debug: this.debug,
103
+ targetFps: options.targetFps || 24,
104
+ dropAllAnimations: options.dropAllAnimations,
105
+ libassMemoryLimit: options.libassMemoryLimit || 0,
106
+ libassGlyphLimit: options.libassGlyphLimit || 0,
107
+ hasAlphaBug: JASSUB._hasAlphaBug,
108
+ useLocalFonts: ('queryLocalFonts' in self) && (options.useLocalFonts ?? true)
109
+ })
110
+ if (offscreenRender === true) this.sendMessage('offscreenCanvas', null, [this._canvasctrl])
107
111
 
108
- this._boundResize = this.resize.bind(this)
109
- this._boundTimeUpdate = this._timeupdate.bind(this)
110
- this._boundSetRate = this.setRate.bind(this)
111
- if (this._video) this.setVideo(options.video)
112
+ this._boundResize = this.resize.bind(this)
113
+ this._boundTimeUpdate = this._timeupdate.bind(this)
114
+ this._boundSetRate = this.setRate.bind(this)
115
+ if (this._video) this.setVideo(options.video)
112
116
 
113
- if (this._onDemandRender) {
114
- this.busy = false
115
- this._lastDemandTime = null
116
- this._video?.requestVideoFrameCallback(this._handleRVFC.bind(this))
117
+ if (this._onDemandRender) {
118
+ this.busy = false
119
+ this._lastDemandTime = null
120
+ this._video?.requestVideoFrameCallback(this._handleRVFC.bind(this))
121
+ }
117
122
  }
118
123
  }
119
124
 
@@ -179,54 +184,41 @@ export default class JASSUB extends EventTarget {
179
184
  * @param {Number} [height=0]
180
185
  * @param {Number} [top=0]
181
186
  * @param {Number} [left=0]
187
+ * @param {Boolean} [force=false]
182
188
  */
183
- resize (width = 0, height = 0, top = 0, left = 0) {
184
- let videoSize = null
189
+ resize (width = 0, height = 0, top = 0, left = 0, force = this._video?.paused) {
185
190
  if ((!width || !height) && this._video) {
186
- videoSize = this._getVideoPosition()
187
- const newsize = this._computeCanvasSize((videoSize.width || 0) * (self.devicePixelRatio || 1), (videoSize.height || 0) * (self.devicePixelRatio || 1))
188
- width = newsize.width
189
- height = newsize.height
191
+ const videoSize = this._getVideoPosition()
192
+ let renderSize = null
193
+ // support anamorphic video
194
+ if (this._videoWidth) {
195
+ const widthRatio = this._video.videoWidth / this._videoWidth
196
+ const heightRatio = this._video.videoHeight / this._videoHeight
197
+ renderSize = this._computeCanvasSize((videoSize.width || 0) / widthRatio, (videoSize.height || 0) / heightRatio)
198
+ } else {
199
+ renderSize = this._computeCanvasSize(videoSize.width || 0, videoSize.height || 0)
200
+ }
201
+ width = renderSize.width
202
+ height = renderSize.height
190
203
  if (this._canvasParent) {
191
204
  top = videoSize.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top)
192
205
  left = videoSize.x
193
206
  }
194
- }
195
-
196
- if (videoSize != null) {
197
- this._canvas.style.top = top + 'px'
198
- this._canvas.style.left = left + 'px'
199
207
  this._canvas.style.width = videoSize.width + 'px'
200
208
  this._canvas.style.height = videoSize.height + 'px'
201
209
  }
202
- if (!(this._canvasctrl.width === width && this._canvasctrl.height === height)) {
203
- // only re-paint if dimensions actually changed
204
- // dont spam re-paints like crazy when re-sizing with animations, but still update instantly without them
205
- if (this._resizeTimeoutBuffer) {
206
- clearTimeout(this._resizeTimeoutBuffer)
207
- this._resizeTimeoutBuffer = setTimeout(() => {
208
- this._resizeTimeoutBuffer = undefined
209
- this._canvasctrl.width = width
210
- this._canvasctrl.height = height
211
- this.sendMessage('canvas', { width, height })
212
- }, 100)
213
- } else {
214
- this._canvasctrl.width = width
215
- this._canvasctrl.height = height
216
- this.sendMessage('canvas', { width, height })
217
- this._resizeTimeoutBuffer = setTimeout(() => {
218
- this._resizeTimeoutBuffer = undefined
219
- }, 100)
220
- }
221
- }
210
+
211
+ this._canvas.style.top = top + 'px'
212
+ this._canvas.style.left = left + 'px'
213
+ this.sendMessage('canvas', { width, height, force: force && this.busy === false })
222
214
  }
223
215
 
224
- _getVideoPosition () {
225
- const videoRatio = this._video.videoWidth / this._video.videoHeight
216
+ _getVideoPosition (width = this._video.videoWidth, height = this._video.videoHeight) {
217
+ const videoRatio = width / height
226
218
  const { offsetWidth, offsetHeight } = this._video
227
219
  const elementRatio = offsetWidth / offsetHeight
228
- let width = offsetWidth
229
- let height = offsetHeight
220
+ width = offsetWidth
221
+ height = offsetHeight
230
222
  if (elementRatio > videoRatio) {
231
223
  width = Math.floor(offsetHeight * videoRatio)
232
224
  } else {
@@ -241,13 +233,14 @@ export default class JASSUB extends EventTarget {
241
233
 
242
234
  _computeCanvasSize (width = 0, height = 0) {
243
235
  const scalefactor = this.prescaleFactor <= 0 ? 1.0 : this.prescaleFactor
236
+ const ratio = self.devicePixelRatio || 1
244
237
 
245
238
  if (height <= 0 || width <= 0) {
246
239
  width = 0
247
240
  height = 0
248
241
  } else {
249
242
  const sgn = scalefactor < 1 ? -1 : 1
250
- let newH = height
243
+ let newH = height * ratio
251
244
  if (sgn * newH * scalefactor <= sgn * this.prescaleHeightLimit) {
252
245
  newH *= scalefactor
253
246
  } else if (sgn * newH < sgn * this.prescaleHeightLimit) {
@@ -256,7 +249,7 @@ export default class JASSUB extends EventTarget {
256
249
 
257
250
  if (this.maxRenderHeight > 0 && newH > this.maxRenderHeight) newH = this.maxRenderHeight
258
251
 
259
- width *= newH / height
252
+ width *= ratio * newH / height
260
253
  height = newH
261
254
  }
262
255
 
@@ -293,9 +286,9 @@ export default class JASSUB extends EventTarget {
293
286
  video.addEventListener('seeking', this._boundTimeUpdate, false)
294
287
  video.addEventListener('playing', this._boundTimeUpdate, false)
295
288
  video.addEventListener('ratechange', this._boundSetRate, false)
289
+ video.addEventListener('resize', this._boundResize)
296
290
  }
297
291
  if (video.videoWidth > 0) this.resize()
298
- video.addEventListener('resize', this._boundResize)
299
292
  // Support Element Resize Observer
300
293
  if (typeof ResizeObserver !== 'undefined') {
301
294
  if (!this._ro) this._ro = new ResizeObserver(() => this.resize())
@@ -531,25 +524,36 @@ export default class JASSUB extends EventTarget {
531
524
  }
532
525
  }
533
526
 
534
- _handleRVFC (now, { mediaTime }) {
527
+ _handleRVFC (now, { mediaTime, width, height }) {
535
528
  if (this._destroyed) return null
536
529
  if (this.busy) {
537
- this._lastDemandTime = mediaTime
530
+ this._lastDemandTime = { mediaTime, width, height }
538
531
  } else {
539
532
  this.busy = true
540
- this._demandRender(mediaTime)
533
+ this._demandRender({ mediaTime, width, height })
541
534
  }
542
535
  this._video.requestVideoFrameCallback(this._handleRVFC.bind(this))
543
536
  }
544
537
 
545
- _demandRender (time) {
538
+ _demandRender ({ mediaTime, width, height }) {
546
539
  this._lastDemandTime = null
547
- this.sendMessage('demand', { time: time + this.timeOffset })
540
+ if (width !== this._videoWidth || height !== this._videoHeight) {
541
+ this._videoWidth = width
542
+ this._videoHeight = height
543
+ this.resize()
544
+ }
545
+ this.sendMessage('demand', { time: mediaTime + this.timeOffset })
548
546
  }
549
547
 
550
- _render ({ images, async, times }) {
548
+ _render ({ images, async, times, width, height }) {
549
+ this._unbusy()
551
550
  const drawStartTime = Date.now()
552
- this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height)
551
+ if (this._canvasctrl.width !== width || this._canvasctrl.height !== height) {
552
+ this._canvasctrl.width = width
553
+ this._canvasctrl.height = height
554
+ } else {
555
+ this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height)
556
+ }
553
557
  for (const image of images) {
554
558
  if (image.image) {
555
559
  if (async) {
@@ -581,6 +585,7 @@ export default class JASSUB extends EventTarget {
581
585
  }
582
586
 
583
587
  _ready () {
588
+ this._init()
584
589
  this.dispatchEvent(new CustomEvent('ready'))
585
590
  }
586
591