jassub 1.2.4 → 1.3.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
@@ -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");
@@ -87,8 +89,8 @@ const _JASSUB = class extends EventTarget {
87
89
  target: "init",
88
90
  asyncRender,
89
91
  onDemandRender: this._onDemandRender,
90
- width: this._canvas.width,
91
- height: this._canvas.height,
92
+ width: this._canvasctrl.width,
93
+ height: this._canvasctrl.height,
92
94
  preMain: true,
93
95
  blendMode,
94
96
  subUrl: options.subUrl,
@@ -160,49 +162,36 @@ const _JASSUB = class extends EventTarget {
160
162
  canvas1.remove();
161
163
  canvas2.remove();
162
164
  }
163
- resize(width = 0, height = 0, top = 0, left = 0) {
164
- let videoSize = null;
165
+ resize(width = 0, height = 0, top = 0, left = 0, force = ((_a) => (_a = this._video) == null ? void 0 : _a.paused)()) {
165
166
  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;
167
+ const videoSize = this._getVideoPosition();
168
+ let renderSize = null;
169
+ if (this._videoWidth) {
170
+ const widthRatio = this._video.videoWidth / this._videoWidth;
171
+ const heightRatio = this._video.videoHeight / this._videoHeight;
172
+ renderSize = this._computeCanvasSize((videoSize.width || 0) / widthRatio, (videoSize.height || 0) / heightRatio);
173
+ } else {
174
+ renderSize = this._computeCanvasSize(videoSize.width || 0, videoSize.height || 0);
175
+ }
176
+ width = renderSize.width;
177
+ height = renderSize.height;
170
178
  if (this._canvasParent) {
171
179
  top = videoSize.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top);
172
180
  left = videoSize.x;
173
181
  }
174
- }
175
- if (videoSize != null) {
176
- this._canvas.style.top = top + "px";
177
- this._canvas.style.left = left + "px";
178
182
  this._canvas.style.width = videoSize.width + "px";
179
183
  this._canvas.style.height = videoSize.height + "px";
180
184
  }
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
- }
185
+ this._canvas.style.top = top + "px";
186
+ this._canvas.style.left = left + "px";
187
+ this.sendMessage("canvas", { width, height, force: force && this.busy === false });
199
188
  }
200
- _getVideoPosition() {
201
- const videoRatio = this._video.videoWidth / this._video.videoHeight;
189
+ _getVideoPosition(width = this._video.videoWidth, height = this._video.videoHeight) {
190
+ const videoRatio = width / height;
202
191
  const { offsetWidth, offsetHeight } = this._video;
203
192
  const elementRatio = offsetWidth / offsetHeight;
204
- let width = offsetWidth;
205
- let height = offsetHeight;
193
+ width = offsetWidth;
194
+ height = offsetHeight;
206
195
  if (elementRatio > videoRatio) {
207
196
  width = Math.floor(offsetHeight * videoRatio);
208
197
  } else {
@@ -214,12 +203,13 @@ const _JASSUB = class extends EventTarget {
214
203
  }
215
204
  _computeCanvasSize(width = 0, height = 0) {
216
205
  const scalefactor = this.prescaleFactor <= 0 ? 1 : this.prescaleFactor;
206
+ const ratio = self.devicePixelRatio || 1;
217
207
  if (height <= 0 || width <= 0) {
218
208
  width = 0;
219
209
  height = 0;
220
210
  } else {
221
211
  const sgn = scalefactor < 1 ? -1 : 1;
222
- let newH = height;
212
+ let newH = height * ratio;
223
213
  if (sgn * newH * scalefactor <= sgn * this.prescaleHeightLimit) {
224
214
  newH *= scalefactor;
225
215
  } else if (sgn * newH < sgn * this.prescaleHeightLimit) {
@@ -227,7 +217,7 @@ const _JASSUB = class extends EventTarget {
227
217
  }
228
218
  if (this.maxRenderHeight > 0 && newH > this.maxRenderHeight)
229
219
  newH = this.maxRenderHeight;
230
- width *= newH / height;
220
+ width *= ratio * newH / height;
231
221
  height = newH;
232
222
  }
233
223
  return { width, height };
@@ -257,10 +247,10 @@ const _JASSUB = class extends EventTarget {
257
247
  video.addEventListener("seeking", this._boundTimeUpdate, false);
258
248
  video.addEventListener("playing", this._boundTimeUpdate, false);
259
249
  video.addEventListener("ratechange", this._boundSetRate, false);
250
+ video.addEventListener("resize", this._boundResize);
260
251
  }
261
252
  if (video.videoWidth > 0)
262
253
  this.resize();
263
- video.addEventListener("resize", this._boundResize);
264
254
  if (typeof ResizeObserver !== "undefined") {
265
255
  if (!this._ro)
266
256
  this._ro = new ResizeObserver(() => this.resize());
@@ -365,24 +355,35 @@ const _JASSUB = class extends EventTarget {
365
355
  this.busy = false;
366
356
  }
367
357
  }
368
- _handleRVFC(now, { mediaTime }) {
358
+ _handleRVFC(now, { mediaTime, width, height }) {
369
359
  if (this._destroyed)
370
360
  return null;
371
361
  if (this.busy) {
372
- this._lastDemandTime = mediaTime;
362
+ this._lastDemandTime = { mediaTime, width, height };
373
363
  } else {
374
364
  this.busy = true;
375
- this._demandRender(mediaTime);
365
+ this._demandRender({ mediaTime, width, height });
376
366
  }
377
367
  this._video.requestVideoFrameCallback(this._handleRVFC.bind(this));
378
368
  }
379
- _demandRender(time) {
369
+ _demandRender({ mediaTime, width, height }) {
380
370
  this._lastDemandTime = null;
381
- this.sendMessage("demand", { time: time + this.timeOffset });
371
+ if (width !== this._videoWidth || height !== this._videoHeight) {
372
+ this._videoWidth = width;
373
+ this._videoHeight = height;
374
+ this.resize();
375
+ }
376
+ this.sendMessage("demand", { time: mediaTime + this.timeOffset });
382
377
  }
383
- _render({ images, async, times }) {
378
+ _render({ images, async, times, width, height }) {
379
+ this._unbusy();
384
380
  const drawStartTime = Date.now();
385
- this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
381
+ if (this._canvasctrl.width !== width || this._canvasctrl.height !== height) {
382
+ this._canvasctrl.width = width;
383
+ this._canvasctrl.height = height;
384
+ } else {
385
+ this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
386
+ }
386
387
  for (const image of images) {
387
388
  if (image.image) {
388
389
  if (async) {
package/dist/jassub.js CHANGED
@@ -46,6 +46,8 @@ export default class JASSUB extends EventTarget {
46
46
 
47
47
  this.timeOffset = options.timeOffset || 0
48
48
  this._video = options.video
49
+ this._videoHeight = 0
50
+ this._videoWidth = 0
49
51
  this._canvas = options.canvas
50
52
  if (this._video && !this._canvas) {
51
53
  this._canvasParent = document.createElement('div')
@@ -88,8 +90,8 @@ export default class JASSUB extends EventTarget {
88
90
  target: 'init',
89
91
  asyncRender,
90
92
  onDemandRender: this._onDemandRender,
91
- width: this._canvas.width,
92
- height: this._canvas.height,
93
+ width: this._canvasctrl.width,
94
+ height: this._canvasctrl.height,
93
95
  preMain: true,
94
96
  blendMode,
95
97
  subUrl: options.subUrl,
@@ -181,54 +183,41 @@ export default class JASSUB extends EventTarget {
181
183
  * @param {Number} [height=0]
182
184
  * @param {Number} [top=0]
183
185
  * @param {Number} [left=0]
186
+ * @param {Boolean} [force=false]
184
187
  */
185
- resize (width = 0, height = 0, top = 0, left = 0) {
186
- let videoSize = null
188
+ resize (width = 0, height = 0, top = 0, left = 0, force = this._video?.paused) {
187
189
  if ((!width || !height) && this._video) {
188
- videoSize = this._getVideoPosition()
189
- const newsize = this._computeCanvasSize((videoSize.width || 0) * (self.devicePixelRatio || 1), (videoSize.height || 0) * (self.devicePixelRatio || 1))
190
- width = newsize.width
191
- height = newsize.height
190
+ const videoSize = this._getVideoPosition()
191
+ let renderSize = null
192
+ // support anamorphic video
193
+ if (this._videoWidth) {
194
+ const widthRatio = this._video.videoWidth / this._videoWidth
195
+ const heightRatio = this._video.videoHeight / this._videoHeight
196
+ renderSize = this._computeCanvasSize((videoSize.width || 0) / widthRatio, (videoSize.height || 0) / heightRatio)
197
+ } else {
198
+ renderSize = this._computeCanvasSize(videoSize.width || 0, videoSize.height || 0)
199
+ }
200
+ width = renderSize.width
201
+ height = renderSize.height
192
202
  if (this._canvasParent) {
193
203
  top = videoSize.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top)
194
204
  left = videoSize.x
195
205
  }
196
- }
197
-
198
- if (videoSize != null) {
199
- this._canvas.style.top = top + 'px'
200
- this._canvas.style.left = left + 'px'
201
206
  this._canvas.style.width = videoSize.width + 'px'
202
207
  this._canvas.style.height = videoSize.height + 'px'
203
208
  }
204
- if (!(this._canvasctrl.width === width && this._canvasctrl.height === height)) {
205
- // only re-paint if dimensions actually changed
206
- // dont spam re-paints like crazy when re-sizing with animations, but still update instantly without them
207
- if (this._resizeTimeoutBuffer) {
208
- clearTimeout(this._resizeTimeoutBuffer)
209
- this._resizeTimeoutBuffer = setTimeout(() => {
210
- this._resizeTimeoutBuffer = undefined
211
- this._canvasctrl.width = width
212
- this._canvasctrl.height = height
213
- this.sendMessage('canvas', { width, height })
214
- }, 100)
215
- } else {
216
- this._canvasctrl.width = width
217
- this._canvasctrl.height = height
218
- this.sendMessage('canvas', { width, height })
219
- this._resizeTimeoutBuffer = setTimeout(() => {
220
- this._resizeTimeoutBuffer = undefined
221
- }, 100)
222
- }
223
- }
209
+
210
+ this._canvas.style.top = top + 'px'
211
+ this._canvas.style.left = left + 'px'
212
+ this.sendMessage('canvas', { width, height, force: force && this.busy === false })
224
213
  }
225
214
 
226
- _getVideoPosition () {
227
- const videoRatio = this._video.videoWidth / this._video.videoHeight
215
+ _getVideoPosition (width = this._video.videoWidth, height = this._video.videoHeight) {
216
+ const videoRatio = width / height
228
217
  const { offsetWidth, offsetHeight } = this._video
229
218
  const elementRatio = offsetWidth / offsetHeight
230
- let width = offsetWidth
231
- let height = offsetHeight
219
+ width = offsetWidth
220
+ height = offsetHeight
232
221
  if (elementRatio > videoRatio) {
233
222
  width = Math.floor(offsetHeight * videoRatio)
234
223
  } else {
@@ -243,13 +232,14 @@ export default class JASSUB extends EventTarget {
243
232
 
244
233
  _computeCanvasSize (width = 0, height = 0) {
245
234
  const scalefactor = this.prescaleFactor <= 0 ? 1.0 : this.prescaleFactor
235
+ const ratio = self.devicePixelRatio || 1
246
236
 
247
237
  if (height <= 0 || width <= 0) {
248
238
  width = 0
249
239
  height = 0
250
240
  } else {
251
241
  const sgn = scalefactor < 1 ? -1 : 1
252
- let newH = height
242
+ let newH = height * ratio
253
243
  if (sgn * newH * scalefactor <= sgn * this.prescaleHeightLimit) {
254
244
  newH *= scalefactor
255
245
  } else if (sgn * newH < sgn * this.prescaleHeightLimit) {
@@ -258,7 +248,7 @@ export default class JASSUB extends EventTarget {
258
248
 
259
249
  if (this.maxRenderHeight > 0 && newH > this.maxRenderHeight) newH = this.maxRenderHeight
260
250
 
261
- width *= newH / height
251
+ width *= ratio * newH / height
262
252
  height = newH
263
253
  }
264
254
 
@@ -295,9 +285,9 @@ export default class JASSUB extends EventTarget {
295
285
  video.addEventListener('seeking', this._boundTimeUpdate, false)
296
286
  video.addEventListener('playing', this._boundTimeUpdate, false)
297
287
  video.addEventListener('ratechange', this._boundSetRate, false)
288
+ video.addEventListener('resize', this._boundResize)
298
289
  }
299
290
  if (video.videoWidth > 0) this.resize()
300
- video.addEventListener('resize', this._boundResize)
301
291
  // Support Element Resize Observer
302
292
  if (typeof ResizeObserver !== 'undefined') {
303
293
  if (!this._ro) this._ro = new ResizeObserver(() => this.resize())
@@ -533,25 +523,36 @@ export default class JASSUB extends EventTarget {
533
523
  }
534
524
  }
535
525
 
536
- _handleRVFC (now, { mediaTime }) {
526
+ _handleRVFC (now, { mediaTime, width, height }) {
537
527
  if (this._destroyed) return null
538
528
  if (this.busy) {
539
- this._lastDemandTime = mediaTime
529
+ this._lastDemandTime = { mediaTime, width, height }
540
530
  } else {
541
531
  this.busy = true
542
- this._demandRender(mediaTime)
532
+ this._demandRender({ mediaTime, width, height })
543
533
  }
544
534
  this._video.requestVideoFrameCallback(this._handleRVFC.bind(this))
545
535
  }
546
536
 
547
- _demandRender (time) {
537
+ _demandRender ({ mediaTime, width, height }) {
548
538
  this._lastDemandTime = null
549
- this.sendMessage('demand', { time: time + this.timeOffset })
539
+ if (width !== this._videoWidth || height !== this._videoHeight) {
540
+ this._videoWidth = width
541
+ this._videoHeight = height
542
+ this.resize()
543
+ }
544
+ this.sendMessage('demand', { time: mediaTime + this.timeOffset })
550
545
  }
551
546
 
552
- _render ({ images, async, times }) {
547
+ _render ({ images, async, times, width, height }) {
548
+ this._unbusy()
553
549
  const drawStartTime = Date.now()
554
- this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height)
550
+ if (this._canvasctrl.width !== width || this._canvasctrl.height !== height) {
551
+ this._canvasctrl.width = width
552
+ this._canvasctrl.height = height
553
+ } else {
554
+ this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height)
555
+ }
555
556
  for (const image of images) {
556
557
  if (image.image) {
557
558
  if (async) {
@@ -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,c)=>o in d?v(d,o,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[o]=c;var u=(d,o,c)=>(f(d,typeof o!="symbol"?o+"":o,c),c);!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(c){const e=this.getVideoPlaybackQuality(),t=this.mozPresentedFrames||this.mozPaintedFrames||e.totalVideoFrames-e.droppedVideoFrames,s=(n,i)=>{const h=this.getVideoPlaybackQuality(),m=this.mozPresentedFrames||this.mozPaintedFrames||h.totalVideoFrames-h.droppedVideoFrames;if(m>t){const l=this.mozFrameDelay||h.totalFrameDelay-e.totalFrameDelay||0,_=i-n;c(i,{presentationTime:i+l*1e3,expectedDisplayTime:i+_,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+_/1e3,presentedFrames:m,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(c){cancelAnimationFrame(this._rvfcpolyfillmap[c]),delete this._rvfcpolyfillmap[c]});const o=class extends EventTarget{constructor(e={}){var r,n,i,h,m;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=l=>this._onmessage(l),this._worker.onerror=l=>this._error(l),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,(m=this._video)==null||m.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,m,l){const _=t.createImageData(m,l);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 m=this._video.videoWidth/this._videoWidth,l=this._video.videoHeight/this._videoHeight;h=this._computeCanvasSize((i.width||0)/m,(i.height||0)/l)}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.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.3.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')
@@ -86,8 +88,8 @@ export default class JASSUB extends EventTarget {
86
88
  target: 'init',
87
89
  asyncRender,
88
90
  onDemandRender: this._onDemandRender,
89
- width: this._canvas.width,
90
- height: this._canvas.height,
91
+ width: this._canvasctrl.width,
92
+ height: this._canvasctrl.height,
91
93
  preMain: true,
92
94
  blendMode,
93
95
  subUrl: options.subUrl,
@@ -179,54 +181,41 @@ export default class JASSUB extends EventTarget {
179
181
  * @param {Number} [height=0]
180
182
  * @param {Number} [top=0]
181
183
  * @param {Number} [left=0]
184
+ * @param {Boolean} [force=false]
182
185
  */
183
- resize (width = 0, height = 0, top = 0, left = 0) {
184
- let videoSize = null
186
+ resize (width = 0, height = 0, top = 0, left = 0, force = this._video?.paused) {
185
187
  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
188
+ const videoSize = this._getVideoPosition()
189
+ let renderSize = null
190
+ // support anamorphic video
191
+ if (this._videoWidth) {
192
+ const widthRatio = this._video.videoWidth / this._videoWidth
193
+ const heightRatio = this._video.videoHeight / this._videoHeight
194
+ renderSize = this._computeCanvasSize((videoSize.width || 0) / widthRatio, (videoSize.height || 0) / heightRatio)
195
+ } else {
196
+ renderSize = this._computeCanvasSize(videoSize.width || 0, videoSize.height || 0)
197
+ }
198
+ width = renderSize.width
199
+ height = renderSize.height
190
200
  if (this._canvasParent) {
191
201
  top = videoSize.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top)
192
202
  left = videoSize.x
193
203
  }
194
- }
195
-
196
- if (videoSize != null) {
197
- this._canvas.style.top = top + 'px'
198
- this._canvas.style.left = left + 'px'
199
204
  this._canvas.style.width = videoSize.width + 'px'
200
205
  this._canvas.style.height = videoSize.height + 'px'
201
206
  }
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
- }
207
+
208
+ this._canvas.style.top = top + 'px'
209
+ this._canvas.style.left = left + 'px'
210
+ this.sendMessage('canvas', { width, height, force: force && this.busy === false })
222
211
  }
223
212
 
224
- _getVideoPosition () {
225
- const videoRatio = this._video.videoWidth / this._video.videoHeight
213
+ _getVideoPosition (width = this._video.videoWidth, height = this._video.videoHeight) {
214
+ const videoRatio = width / height
226
215
  const { offsetWidth, offsetHeight } = this._video
227
216
  const elementRatio = offsetWidth / offsetHeight
228
- let width = offsetWidth
229
- let height = offsetHeight
217
+ width = offsetWidth
218
+ height = offsetHeight
230
219
  if (elementRatio > videoRatio) {
231
220
  width = Math.floor(offsetHeight * videoRatio)
232
221
  } else {
@@ -241,13 +230,14 @@ export default class JASSUB extends EventTarget {
241
230
 
242
231
  _computeCanvasSize (width = 0, height = 0) {
243
232
  const scalefactor = this.prescaleFactor <= 0 ? 1.0 : this.prescaleFactor
233
+ const ratio = self.devicePixelRatio || 1
244
234
 
245
235
  if (height <= 0 || width <= 0) {
246
236
  width = 0
247
237
  height = 0
248
238
  } else {
249
239
  const sgn = scalefactor < 1 ? -1 : 1
250
- let newH = height
240
+ let newH = height * ratio
251
241
  if (sgn * newH * scalefactor <= sgn * this.prescaleHeightLimit) {
252
242
  newH *= scalefactor
253
243
  } else if (sgn * newH < sgn * this.prescaleHeightLimit) {
@@ -256,7 +246,7 @@ export default class JASSUB extends EventTarget {
256
246
 
257
247
  if (this.maxRenderHeight > 0 && newH > this.maxRenderHeight) newH = this.maxRenderHeight
258
248
 
259
- width *= newH / height
249
+ width *= ratio * newH / height
260
250
  height = newH
261
251
  }
262
252
 
@@ -293,9 +283,9 @@ export default class JASSUB extends EventTarget {
293
283
  video.addEventListener('seeking', this._boundTimeUpdate, false)
294
284
  video.addEventListener('playing', this._boundTimeUpdate, false)
295
285
  video.addEventListener('ratechange', this._boundSetRate, false)
286
+ video.addEventListener('resize', this._boundResize)
296
287
  }
297
288
  if (video.videoWidth > 0) this.resize()
298
- video.addEventListener('resize', this._boundResize)
299
289
  // Support Element Resize Observer
300
290
  if (typeof ResizeObserver !== 'undefined') {
301
291
  if (!this._ro) this._ro = new ResizeObserver(() => this.resize())
@@ -531,25 +521,36 @@ export default class JASSUB extends EventTarget {
531
521
  }
532
522
  }
533
523
 
534
- _handleRVFC (now, { mediaTime }) {
524
+ _handleRVFC (now, { mediaTime, width, height }) {
535
525
  if (this._destroyed) return null
536
526
  if (this.busy) {
537
- this._lastDemandTime = mediaTime
527
+ this._lastDemandTime = { mediaTime, width, height }
538
528
  } else {
539
529
  this.busy = true
540
- this._demandRender(mediaTime)
530
+ this._demandRender({ mediaTime, width, height })
541
531
  }
542
532
  this._video.requestVideoFrameCallback(this._handleRVFC.bind(this))
543
533
  }
544
534
 
545
- _demandRender (time) {
535
+ _demandRender ({ mediaTime, width, height }) {
546
536
  this._lastDemandTime = null
547
- this.sendMessage('demand', { time: time + this.timeOffset })
537
+ if (width !== this._videoWidth || height !== this._videoHeight) {
538
+ this._videoWidth = width
539
+ this._videoHeight = height
540
+ this.resize()
541
+ }
542
+ this.sendMessage('demand', { time: mediaTime + this.timeOffset })
548
543
  }
549
544
 
550
- _render ({ images, async, times }) {
545
+ _render ({ images, async, times, width, height }) {
546
+ this._unbusy()
551
547
  const drawStartTime = Date.now()
552
- this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height)
548
+ if (this._canvasctrl.width !== width || this._canvasctrl.height !== height) {
549
+ this._canvasctrl.width = width
550
+ this._canvasctrl.height = height
551
+ } else {
552
+ this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height)
553
+ }
553
554
  for (const image of images) {
554
555
  if (image.image) {
555
556
  if (async) {