fix(jassub): [PKG-JASSUB-07] repair vendor lifecycle and subtitle frame clock

This commit is contained in:
Harvey Zhao committed 2026-09-14 02:17:24 +08:00
1 parent 5e445b1af6
commit 0f3c068aae
29 files changed
+14064 -267

No files matched your search

@@ -12,14 +12,33 @@ Caller-supplied canvas nodes remain caller-owned. The adapter does not invent a
or delete those nodes; vendor-created containers still get z-index 20. Host cleanup
dynamically calls the exposed instance's current destroy method, ignores already
destroyed instances and blocks synchronous reentry. A thrown disposal error is preserved
and the guard resets for a later attempt. Direct repeated vendor destroy is still a
separate PKG-JASSUB-07 problem; the adapter does not replace public instance methods.
and the guard resets for a later attempt. The local vendor patch also makes direct
destroy idempotent; the adapter does not replace public instance methods.
If styling or host subscription throws after construction, registration independently
attempts off with the exact callback and instance cleanup, then rethrows the original
error. Secondary rollback errors do not replace it. An invalid/custom host whose off
throws may retain an inert callback; failed vendor cleanup is not magically repaired.
Failures inside the vendor constructor before it returns an instance remain 07 work.
The vendor constructor rolls back its partially created DOM/listeners/Worker and
rethrows the original construction error. Asynchronous Worker errors keep their
existing event channel; callers can explicitly destroy after a loading failure.
The local vendor lifecycle patch gives each video generation ownership of its main
and color-space frame callbacks. setVideo cancels the previous callbacks, unobserves
the old element, clears old demand/dimensions and moves its own container to a new
parent when needed. Already queued callbacks check generation before reading frames
or scheduling another callback. Color probes close VideoFrame even if reading fails.
Destroy invalidates the generation, disconnects the observer, removes the actual owned
container and terminates the Worker once. Caller-owned canvas/video elements remain.
Capability/init messages and pending sendMessage calls cannot post after destruction.
Each query owns a timer and two Worker listeners. Success, timeout and Worker error
settle once after cleanup; callbacks receive the original error (including a plain
native Event for CSP failures). Destroy cleans every request before notifying any
callback with an Error and undefined data. A throwing callback does not prevent other
request cleanup or Worker termination; the first exception is rethrown afterward.
Queries should be issued after ready; this patch does not invent a Worker request-ID
protocol or change the historical matching of simultaneous same-target responses.
`src/jassub.es.js` owns rendering, media listeners, canvas state, capability checks and worker
messages. Keep this third-party file separate from the adapter's TypeScript migration.
@@ -41,15 +60,19 @@ There is no second implementation and no new factory.default property. Root/lega
types remain unchanged; exact typesVersions mappings support old Node module resolution.
See [type migration notes](types/README.md) before changing either surface.
`src/jassub.es.d.ts` is a private bridge for the frozen vendor JS, not a public export.
`src/jassub.es.d.ts` is a private bridge for the locally patched vendor JS, not a public export.
It adds only the adapter's `_destroyed` read and `_canvasParent.style.zIndex` write to the
accurate instance. The minimal style contract permits the historical numeric 20, which
the native DOM setter converts to a string. The owned TS modules depend on RuntimeOption
and RuntimeResult, while JassubHost needs only video/on/off. The implementation fixture
checks actual Artplayer assignability. No broad any index, allowJs or ts-nocheck hides
owned code; the isolated third-party JS remains an explicit provenance/07 exception.
owned code; the isolated third-party JS remains an explicit provenance exception.
The wrapper matches upstream jassub 1.8.8 apart from formatting and the ESLint header;
The original wrapper matched upstream jassub 1.8.8 apart from formatting and the ESLint header;
PKG-JASSUB-07 records its local lifecycle/clock patch separately in
`../../refactor/baselines/jassub-vendor-patch.json` and the accompanying patch file.
Keep original and patched fingerprints distinct; do not call the changed wrapper an
unmodified upstream file. The
worker JS and default font match that archive byte-for-byte. Local WASM instead matches
the exact Pages nightly blobs associated with source 6b19a04ddfbad8f9bfd3237395788dd76218841b.
Its build workflow and seven submodule revisions are pinned in the separate
@@ -78,17 +101,28 @@ Provenance tests are offline by default; explicit --network refreshes pinned sou
asset comparisons and fails on any request error, without substituting cached evidence.
Set ARTPLAYER_JASSUB_CANDIDATE=1 for current source behavior; ARTPLAYER_JASSUB_ARTIFACT can point
at a main/legacy artifact. Tests use controlled DOM, Worker and SIMD detection, with actual
vendor JavaScript. They do not render ASS or execute worker WASM. Real browser, failures,
repeated destroy and resource cleanup remain later steps. Frozen failure tests now
vendor JavaScript. The runtime query test also executes complete Worker JS and real WASM
through controlled transport; it is not a browser Worker. Frozen failure tests
cover repeated teardown, cross-parent setVideo, custom canvas, Worker construction
failure, stale video frames and incorrect fallback ratechange payloads. They assert
the old failures, not candidate fixes. PKG-JASSUB-03 owns adapter cleanup; 07 owns
separately tracked vendor lifecycle/clock changes and 05 verifies native behavior.
the old failures, not candidate fixes. Candidate lifecycle and polyfill tests separately
verify the local patch. PKG-JASSUB-05 retains complete combination/device acceptance.
`yarn test:browser test/browser/jassub-native.spec.js --workers=1` loads the actual
published wrapper, Worker, WASM and default font with offscreenRender=false.
Chromium/Firefox render and seek correctly; Windows WebKit's default frame-clock
path remains a recorded failure. The onDemandRender=false diagnostic does not
waive the default behavior gate. Native failure recovery, default offscreen mode,
full devices and sustained resource release remain unverified. See
Without ARTPLAYER_JASSUB_ARTIFACT this remains a historical test: the published wrapper
still fails Windows WebKit when its quality counters stay zero. Set that variable to
the normal candidate build to test the fix. Native RVFC and increasing quality counters
retain their existing behavior. Only the polyfill's observed all-zero counters can use
finite changed media time with readyState >= 2 and no active seek; this approximate path
limits each continuous callback chain to 30 Hz and still reports zero presented frames.
Paused seek completion is supported; a static paused video does not keep firing.
Per-video maps and monotonic handles prevent equal-clock callback collisions.
The candidate's default onDemandRender path is tested through playback/seek/layout in
Chromium/Firefox/WebKit. jassub-lifecycle.spec.js additionally checks actual replacement
video pixels, numerical fallback playbackRate, repeated teardown, invalid-URL constructor
rollback and a genuine asynchronous CSP Worker error reaching a pending query. These
are distinct from default offscreen mode, full devices and sustained memory/GPU release,
which remain unverified. See
`../../refactor/baselines/jassub-contract.md` for precise contract and provenance evidence.
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginJassub=t(),define(function(){return e.artplayerPluginJassub})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginJassub=t()}(this,function(){"use strict";var e=Object.defineProperty,t=(t,s,i)=>((t,s,i)=>s in t?e(t,s,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[s]=i)(t,"symbol"!==typeof s?s+"":s,i);typeof HTMLVideoElement<"u"&&!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(e){const t=performance.now(),s=this.getVideoPlaybackQuality(),i=this.mozPresentedFrames||this.mozPaintedFrames||s.totalVideoFrames-s.droppedVideoFrames,a=(r,n)=>{const o=this.getVideoPlaybackQuality(),h=this.mozPresentedFrames||this.mozPaintedFrames||o.totalVideoFrames-o.droppedVideoFrames;if(h>i){const i=this.mozFrameDelay||o.totalFrameDelay-s.totalFrameDelay||0,a=n-r;e(n,{presentationTime:n+1e3*i,expectedDisplayTime:n+a,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+a/1e3,presentedFrames:h,processingDuration:i}),delete this._rvfcpolyfillmap[t]}else this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>a(n,e))};return this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>a(t,e)),t},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(e){cancelAnimationFrame(this._rvfcpolyfillmap[e]),delete this._rvfcpolyfillmap[e]});const s={bt709:"BT709",bt470bg:"BT601",smpte170m:"BT601"},i={BT601:{BT709:"1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418"},BT709:{BT601:"0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582"},FCC:{BT709:"1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378",BT601:"1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996"},SMPTE240M:{BT709:"0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148",BT601:"0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973"}},a=class e extends EventTarget{constructor(t){if(super(),!globalThis.Worker)throw this.destroy("Worker not supported");if(!t)throw this.destroy("No options provided");this._loaded=new Promise(e=>{this._init=e});const s=e._test();if(this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&(t.onDemandRender??!0),this._offscreenRender="transferControlToOffscreen"in HTMLCanvasElement.prototype&&!t.canvas&&(t.offscreenRender??!0),this.timeOffset=t.timeOffset||0,this._video=t.video,this._videoHeight=0,this._videoWidth=0,this._videoColorSpace=null,this._canvas=t.canvas,this._video&&!this._canvas)this._canvasParent=document.createElement("div"),this._canvasParent.className="JASSUB",this._canvasParent.style.position="relative",this._canvas=this._createCanvas(),this._video.insertAdjacentElement("afterend",this._canvasParent);else if(!this._canvas)throw this.destroy("Don't know where to render: you should give video or canvas in options.");if(this._bufferCanvas=document.createElement("canvas"),this._bufferCtx=this._bufferCanvas.getContext("2d"),!this._bufferCtx)throw this.destroy("Canvas rendering not supported");this._canvasctrl=this._offscreenRender?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!this._offscreenRender&&this._canvasctrl.getContext("2d"),this._lastRenderTime=0,this.debug=!!t.debug,this.prescaleFactor=t.prescaleFactor||1,this.prescaleHeightLimit=t.prescaleHeightLimit||1080,this.maxRenderHeight=t.maxRenderHeight||0,this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=this.setRate.bind(this),this._boundUpdateColorSpace=this._updateColorSpace.bind(this),this._video&&this.setVideo(t.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null),this._worker=new Worker(t.workerUrl||"jassub-worker.js"),this._worker.onmessage=e=>this._onmessage(e),this._worker.onerror=e=>this._error(e),s.then(()=>{this._worker.postMessage({target:"init",wasmUrl:e._supportsSIMD&&t.modernWasmUrl?t.modernWasmUrl:t.wasmUrl??"jassub-worker.wasm",legacyWasmUrl:t.legacyWasmUrl??"jassub-worker.wasm.js",asyncRender:typeof createImageBitmap<"u"&&(t.asyncRender??!0),onDemandRender:this._onDemandRender,width:this._canvasctrl.width||0,height:this._canvasctrl.height||0,blendMode:t.blendMode||"js",subUrl:t.subUrl,subContent:t.subContent||null,fonts:t.fonts||[],availableFonts:t.availableFonts||{"liberation sans":"./default.woff2"},fallbackFont:t.fallbackFont||"liberation sans",debug:this.debug,targetFps:t.targetFps||24,dropAllAnimations:t.dropAllAnimations,dropAllBlur:t.dropAllBlur,libassMemoryLimit:t.libassMemoryLimit||0,libassGlyphLimit:t.libassGlyphLimit||0,useLocalFonts:typeof queryLocalFonts<"u"&&(t.useLocalFonts??!0),hasBitmapBug:e._hasBitmapBug}),!0===this._offscreenRender&&this.sendMessage("ofLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginJassub=t(),define(function(){return e.artplayerPluginJassub})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginJassub=t()}(this,function(){"use strict";var e=Object.defineProperty,t=(t,s,i)=>((t,s,i)=>s in t?e(t,s,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[s]=i)(t,"symbol"!==typeof s?s+"":s,i);typeof HTMLVideoElement<"u"&&!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype.requestVideoFrameCallback=function(e){Object.prototype.hasOwnProperty.call(this,"_rvfcpolyfillmap")||(this._rvfcpolyfillmap={});const t=this._rvfcpolyfillid=(this._rvfcpolyfillid||0)+1,s=performance.now(),i=this.getVideoPlaybackQuality(),a=this.currentTime,r=this.mozPresentedFrames||this.mozPaintedFrames||i.totalVideoFrames-i.droppedVideoFrames,n=(o,h)=>{if(!(t in this._rvfcpolyfillmap))return;const d=this.getVideoPlaybackQuality(),l=this.mozPresentedFrames||this.mozPaintedFrames||d.totalVideoFrames-d.droppedVideoFrames,c=l>r,_=0===r&&0===l&&0===i.totalVideoFrames&&0===i.droppedVideoFrames&&0===d.totalVideoFrames&&0===d.droppedVideoFrames&&Number.isFinite(this.currentTime)&&this.currentTime!==a&&this.readyState>=2&&!this.seeking&&h-s>=1e3/30;if(c||_){const s=this.mozFrameDelay||d.totalFrameDelay-i.totalFrameDelay||0,a=h-o;delete this._rvfcpolyfillmap[t],e(h,{presentationTime:h+1e3*s,expectedDisplayTime:h+a,width:this.videoWidth,height:this.videoHeight,mediaTime:_&&!c?Math.max(0,this.currentTime):Math.max(0,this.currentTime||0)+a/1e3,presentedFrames:l,processingDuration:s})}else this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>n(h,e))};return this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>n(s,e)),t},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(e){const t=this._rvfcpolyfillmap;t&&e in t&&(cancelAnimationFrame(t[e]),delete t[e])});const s={bt709:"BT709",bt470bg:"BT601",smpte170m:"BT601"},i={BT601:{BT709:"1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418"},BT709:{BT601:"0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582"},FCC:{BT709:"1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378",BT601:"1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996"},SMPTE240M:{BT709:"0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148",BT601:"0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973"}},a=class e extends EventTarget{constructor(t){let s;super(),this._requests=new Set;try{if(!globalThis.Worker)throw this.destroy("Worker not supported");if(!t)throw this.destroy("No options provided");if(this._loaded=new Promise(e=>{this._init=e}),s=e._test(),this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&(t.onDemandRender??!0),this._offscreenRender="transferControlToOffscreen"in HTMLCanvasElement.prototype&&!t.canvas&&(t.offscreenRender??!0),this.timeOffset=t.timeOffset||0,this._video=t.video,this._videoHeight=0,this._videoWidth=0,this._videoColorSpace=null,this._canvas=t.canvas,this._video&&!this._canvas)this._canvasParent=document.createElement("div"),this._canvasParent.className="JASSUB",this._canvasParent.style.position="relative",this._canvas=this._createCanvas(),this._video.insertAdjacentElement("afterend",this._canvasParent);else if(!this._canvas)throw this.destroy("Don't know where to render: you should give video or canvas in options.");if(this._bufferCanvas=document.createElement("canvas"),this._bufferCtx=this._bufferCanvas.getContext("2d"),!this._bufferCtx)throw this.destroy("Canvas rendering not supported");this._canvasctrl=this._offscreenRender?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!this._offscreenRender&&this._canvasctrl.getContext("2d"),this._lastRenderTime=0,this.debug=!!t.debug,this.prescaleFactor=t.prescaleFactor||1,this.prescaleHeightLimit=t.prescaleHeightLimit||1080,this.maxRenderHeight=t.maxRenderHeight||0,this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=()=>{this._destroyed||this.setRate(this._video.playbackRate)},this._boundUpdateColorSpace=this._updateColorSpace.bind(this),this._video&&this.setVideo(t.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null),this._worker=new Worker(t.workerUrl||"jassub-worker.js"),this._worker.onmessage=e=>this._onmessage(e),this._worker.onerror=e=>this._error(e),s.then(()=>{this._destroyed||(this._worker.postMessage({target:"init",wasmUrl:e._supportsSIMD&&t.modernWasmUrl?t.modernWasmUrl:t.wasmUrl??"jassub-worker.wasm",legacyWasmUrl:t.legacyWasmUrl??"jassub-worker.wasm.js",asyncRender:typeof createImageBitmap<"u"&&(t.asyncRender??!0),onDemandRender:this._onDemandRender,width:this._canvasctrl.width||0,height:this._canvasctrl.height||0,blendMode:t.blendMode||"js",subUrl:t.suLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginJassub=t(),define(function(){return e.artplayerPluginJassub})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginJassub=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(t,s,i)=>s in t?e(t,s,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[s]=i,r=(e,r)=>{for(var n in r||(r={}))s.call(r,n)&&a(e,n,r[n]);if(t)for(var n of t(r))i.call(r,n)&&a(e,n,r[n]);return e},n=(e,t,s)=>a(e,"symbol"!==typeof t?t+"":t,s),o=(e,t,s)=>new Promise((i,a)=>{var r=e=>{try{o(s.next(e))}catch(t){a(t)}},n=e=>{try{o(s.throw(e))}catch(t){a(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(r,n);o((s=s.apply(e,t)).next())});typeof HTMLVideoElement<"u"&&!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(e){const t=performance.now(),s=this.getVideoPlaybackQuality(),i=this.mozPresentedFrames||this.mozPaintedFrames||s.totalVideoFrames-s.droppedVideoFrames,a=(r,n)=>{const o=this.getVideoPlaybackQuality(),h=this.mozPresentedFrames||this.mozPaintedFrames||o.totalVideoFrames-o.droppedVideoFrames;if(h>i){const i=this.mozFrameDelay||o.totalFrameDelay-s.totalFrameDelay||0,a=n-r;e(n,{presentationTime:n+1e3*i,expectedDisplayTime:n+a,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+a/1e3,presentedFrames:h,processingDuration:i}),delete this._rvfcpolyfillmap[t]}else this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>a(n,e))};return this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>a(t,e)),t},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(e){cancelAnimationFrame(this._rvfcpolyfillmap[e]),delete this._rvfcpolyfillmap[e]});const h={bt709:"BT709",bt470bg:"BT601",smpte170m:"BT601"},d={BT601:{BT709:"1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418"},BT709:{BT601:"0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582"},FCC:{BT709:"1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378",BT601:"1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996"},SMPTE240M:{BT709:"0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148",BT601:"0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973"}},l=class e extends EventTarget{constructor(t){var s,i;if(super(),!globalThis.Worker)throw this.destroy("Worker not supported");if(!t)throw this.destroy("No options provided");this._loaded=new Promise(e=>{this._init=e});const a=e._test();if(this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&(null==(s=t.onDemandRender)||s),this._offscreenRender="transferControlToOffscreen"in HTMLCanvasElement.prototype&&!t.canvas&&(null==(i=t.offscreenRender)||i),this.timeOffset=t.timeOffset||0,this._video=t.video,this._videoHeight=0,this._videoWidth=0,this._videoColorSpace=null,this._canvas=t.canvas,this._video&&!this._canvas)this._canvasParent=document.createElement("div"),this._canvasParent.className="JASSUB",this._canvasParent.style.position="relative",this._canvas=this._createCanvas(),this._video.insertAdjacentElement("afterend",this._canvasParent);else if(!this._canvas)throw this.destroy("Don't know where to render: you should give video or canvas in options.");if(this._bufferCanvas=document.createElement("canvas"),this._bufferCtx=this._bufferCanvas.getContext("2d"),!this._bufferCtx)throw this.destroy("Canvas rendering not supported");this._canvasctrl=this._offscreenRender?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!this._offscreenRender&&this._canvasctrl.getContext("2d"),this._lastRenderTime=0,this.debug=!!t.debug,this.prescaleFactor=t.prescaleFactor||1,this.prescaleHeightLimit=t.prescaleHeightLimit||1080,this.maxRenderHeight=t.maxRenderHeight||0,this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=this.setRate.bind(this),this._boundUpdateColorSpace=this._updateColorSpace.bind(this),this._video&&this.setVideo(t.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null),this._worker=new Worker(t.workerUrl||"jassub-worker.js"),this._worker.onmessage=e=>this._onmessage(e),this._worker.onerror=e=>this._error(e),a.then(()=>{var s,i,a,r;this._worker.postMessage({target:"init",wasmUrl:e._supportsSIMD&&t.modernWasmUrl?t.modernWasmUrl:null!=(s=t.wasmUrl)?s:"jassub-worker.wasm",legacyWasmUrl:null!=(i=t.legacyWasmUrl)?i:"jassub-worker.wasm.js",asyncRender:typeof createImageBitmap<"u"&&(null==(a=t.asyncRender)||a),onDemandRender:this._onDemandRender,width:this._canvasctrl.width||0,height:this._canvasctrl.height||0,blendMode:t.blendMode||"js",subUrl:t.subUrl,subLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginJassub=t(),define(function(){return e.artplayerPluginJassub})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginJassub=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(t,s,i)=>s in t?e(t,s,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[s]=i,r=(e,r)=>{for(var n in r||(r={}))s.call(r,n)&&a(e,n,r[n]);if(t)for(var n of t(r))i.call(r,n)&&a(e,n,r[n]);return e},n=(e,t,s)=>a(e,"symbol"!==typeof t?t+"":t,s),o=(e,t,s)=>new Promise((i,a)=>{var r=e=>{try{o(s.next(e))}catch(t){a(t)}},n=e=>{try{o(s.throw(e))}catch(t){a(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(r,n);o((s=s.apply(e,t)).next())});typeof HTMLVideoElement<"u"&&!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype.requestVideoFrameCallback=function(e){Object.prototype.hasOwnProperty.call(this,"_rvfcpolyfillmap")||(this._rvfcpolyfillmap={});const t=this._rvfcpolyfillid=(this._rvfcpolyfillid||0)+1,s=performance.now(),i=this.getVideoPlaybackQuality(),a=this.currentTime,r=this.mozPresentedFrames||this.mozPaintedFrames||i.totalVideoFrames-i.droppedVideoFrames,n=(o,h)=>{if(!(t in this._rvfcpolyfillmap))return;const d=this.getVideoPlaybackQuality(),l=this.mozPresentedFrames||this.mozPaintedFrames||d.totalVideoFrames-d.droppedVideoFrames,c=l>r,_=0===r&&0===l&&0===i.totalVideoFrames&&0===i.droppedVideoFrames&&0===d.totalVideoFrames&&0===d.droppedVideoFrames&&Number.isFinite(this.currentTime)&&this.currentTime!==a&&this.readyState>=2&&!this.seeking&&h-s>=1e3/30;if(c||_){const s=this.mozFrameDelay||d.totalFrameDelay-i.totalFrameDelay||0,a=h-o;delete this._rvfcpolyfillmap[t],e(h,{presentationTime:h+1e3*s,expectedDisplayTime:h+a,width:this.videoWidth,height:this.videoHeight,mediaTime:_&&!c?Math.max(0,this.currentTime):Math.max(0,this.currentTime||0)+a/1e3,presentedFrames:l,processingDuration:s})}else this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>n(h,e))};return this._rvfcpolyfillmap[t]=requestAnimationFrame(e=>n(s,e)),t},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(e){const t=this._rvfcpolyfillmap;t&&e in t&&(cancelAnimationFrame(t[e]),delete t[e])});const h={bt709:"BT709",bt470bg:"BT601",smpte170m:"BT601"},d={BT601:{BT709:"1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418"},BT709:{BT601:"0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582"},FCC:{BT709:"1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378",BT601:"1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996"},SMPTE240M:{BT709:"0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148",BT601:"0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973"}},l=class e extends EventTarget{constructor(t){var s,i;let a;super(),this._requests=new Set;try{if(!globalThis.Worker)throw this.destroy("Worker not supported");if(!t)throw this.destroy("No options provided");if(this._loaded=new Promise(e=>{this._init=e}),a=e._test(),this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&(null==(s=t.onDemandRender)||s),this._offscreenRender="transferControlToOffscreen"in HTMLCanvasElement.prototype&&!t.canvas&&(null==(i=t.offscreenRender)||i),this.timeOffset=t.timeOffset||0,this._video=t.video,this._videoHeight=0,this._videoWidth=0,this._videoColorSpace=null,this._canvas=t.canvas,this._video&&!this._canvas)this._canvasParent=document.createElement("div"),this._canvasParent.className="JASSUB",this._canvasParent.style.position="relative",this._canvas=this._createCanvas(),this._video.insertAdjacentElement("afterend",this._canvasParent);else if(!this._canvas)throw this.destroy("Don't know where to render: you should give video or canvas in options.");if(this._bufferCanvas=document.createElement("canvas"),this._bufferCtx=this._bufferCanvas.getContext("2d"),!this._bufferCtx)throw this.destroy("Canvas rendering not supported");this._canvasctrl=this._offscreenRender?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!this._offscreenRender&&this._canvasctrl.getContext("2d"),this._lastRenderTime=0,this.debug=!!t.debug,this.prescaleFactor=t.prescaleFactor||1,this.prescaleHeightLimit=t.prescaleHeightLimit||1080,this.maxRenderHeight=t.maxRenderHeight||0,this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=()=>{this._destroyed||this.setRate(this._video.playbackRate)},this._boundUpdateColorSpace=this._updateColorSpace.bind(this),this._video&&this.setVideo(t.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null),this._worker=new Worker(t.workerUrl||"jassub-worker.js"),this._worker.onmessage=e=>this._onmessage(e),this._Line truncated
@@ -7,27 +7,46 @@
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype && (HTMLVideoElement.prototype._rvfcpolyfillmap = {}, HTMLVideoElement.prototype.requestVideoFrameCallback = function(c) {
const e = performance.now(), t = this.getVideoPlaybackQuality(), s = this.mozPresentedFrames || this.mozPaintedFrames || t.totalVideoFrames - t.droppedVideoFrames, r = (n, a) => {
const i = this.getVideoPlaybackQuality(), h = this.mozPresentedFrames || this.mozPaintedFrames || i.totalVideoFrames - i.droppedVideoFrames;
if (h > s) {
const d = this.mozFrameDelay || i.totalFrameDelay - t.totalFrameDelay || 0, l = a - n;
c(a, {
presentationTime: a + d * 1e3,
expectedDisplayTime: a + l,
width: this.videoWidth,
height: this.videoHeight,
mediaTime: Math.max(0, this.currentTime || 0) + l / 1e3,
presentedFrames: h,
processingDuration: d
}), delete this._rvfcpolyfillmap[e];
} else
this._rvfcpolyfillmap[e] = requestAnimationFrame((d) => r(a, d));
if (typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype) {
HTMLVideoElement.prototype.requestVideoFrameCallback = function(callback) {
if (!Object.prototype.hasOwnProperty.call(this, "_rvfcpolyfillmap"))
this._rvfcpolyfillmap = {};
const id = this._rvfcpolyfillid = (this._rvfcpolyfillid || 0) + 1;
const started = performance.now(), initial = this.getVideoPlaybackQuality(), initialTime = this.currentTime;
const initialFrames = this.mozPresentedFrames || this.mozPaintedFrames || initial.totalVideoFrames - initial.droppedVideoFrames;
const poll = (previous, now) => {
if (!(id in this._rvfcpolyfillmap)) return;
const quality = this.getVideoPlaybackQuality();
const frames = this.mozPresentedFrames || this.mozPaintedFrames || quality.totalVideoFrames - quality.droppedVideoFrames;
const advanced = frames > initialFrames;
const mediaFallback = initialFrames === 0 && frames === 0 && initial.totalVideoFrames === 0 && initial.droppedVideoFrames === 0 && quality.totalVideoFrames === 0 && quality.droppedVideoFrames === 0 && Number.isFinite(this.currentTime) && this.currentTime !== initialTime && this.readyState >= 2 && !this.seeking && now - started >= 1e3 / 30;
if (advanced || mediaFallback) {
const delay = this.mozFrameDelay || quality.totalFrameDelay - initial.totalFrameDelay || 0;
const elapsed = now - previous;
delete this._rvfcpolyfillmap[id];
callback(now, {
presentationTime: now + delay * 1e3,
expectedDisplayTime: now + elapsed,
width: this.videoWidth,
height: this.videoHeight,
mediaTime: mediaFallback && !advanced ? Math.max(0, this.currentTime) : Math.max(0, this.currentTime || 0) + elapsed / 1e3,
presentedFrames: frames,
processingDuration: delay
});
} else {
this._rvfcpolyfillmap[id] = requestAnimationFrame((time) => poll(now, time));
}
};
this._rvfcpolyfillmap[id] = requestAnimationFrame((time) => poll(started, time));
return id;
};
return this._rvfcpolyfillmap[e] = requestAnimationFrame((n) => r(e, n)), e;
}, HTMLVideoElement.prototype.cancelVideoFrameCallback = function(c) {
cancelAnimationFrame(this._rvfcpolyfillmap[c]), delete this._rvfcpolyfillmap[c];
});
HTMLVideoElement.prototype.cancelVideoFrameCallback = function(id) {
const callbacks = this._rvfcpolyfillmap;
if (!callbacks || !(id in callbacks)) return;
cancelAnimationFrame(callbacks[id]);
delete callbacks[id];
};
}
const _ = {
bt709: "BT709",
// these might not be exactly correct? oops?
@@ -82,47 +101,65 @@ const _o = class _o extends EventTarget {
* @param {Number} [options.libassGlyphLimit] libass glyph cache memory limit in MiB (approximate).
*/
constructor(e) {
if (super(), !globalThis.Worker)
throw this.destroy("Worker not supported");
if (!e)
throw this.destroy("No options provided");
this._loaded = /** @type {Promise<void>} */
new Promise((s) => {
this._init = s;
});
const t = _o._test();
if (this._onDemandRender = "requestVideoFrameCallback" in HTMLVideoElement.prototype && (e.onDemandRender ?? true), this._offscreenRender = "transferControlToOffscreen" in HTMLCanvasElement.prototype && !e.canvas && (e.offscreenRender ?? true), this.timeOffset = e.timeOffset || 0, this._video = e.video, this._videoHeight = 0, this._videoWidth = 0, this._videoColorSpace = null, this._canvas = e.canvas, this._video && !this._canvas)
this._canvasParent = document.createElement("div"), this._canvasParent.className = "JASSUB", this._canvasParent.style.position = "relative", this._canvas = this._createCanvas(), this._video.insertAdjacentElement("afterend", this._canvasParent);
else if (!this._canvas)
throw this.destroy("Don't know where to render: you should give video or canvas in options.");
if (this._bufferCanvas = document.createElement("canvas"), this._bufferCtx = this._bufferCanvas.getContext("2d"), !this._bufferCtx)
throw this.destroy("Canvas rendering not supported");
this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = this.setRate.bind(this), this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = false, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => {
this._worker.postMessage({
target: "init",
wasmUrl: _o._supportsSIMD && e.modernWasmUrl ? e.modernWasmUrl : e.wasmUrl ?? "jassub-worker.wasm",
legacyWasmUrl: e.legacyWasmUrl ?? "jassub-worker.wasm.js",
asyncRender: typeof createImageBitmap < "u" && (e.asyncRender ?? true),
onDemandRender: this._onDemandRender,
width: this._canvasctrl.width || 0,
height: this._canvasctrl.height || 0,
blendMode: e.blendMode || "js",
subUrl: e.subUrl,
subContent: e.subContent || null,
fonts: e.fonts || [],
availableFonts: e.availableFonts || { "liberation sans": "./default.woff2" },
fallbackFont: e.fallbackFont || "liberation sans",
debug: this.debug,
targetFps: e.targetFps || 24,
dropAllAnimations: e.dropAllAnimations,
dropAllBlur: e.dropAllBlur,
libassMemoryLimit: e.libassMemoryLimit || 0,
libassGlyphLimit: e.libassGlyphLimit || 0,
// @ts-ignore
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? true),
hasBitmapBug: _o._hasBitmapBug
}), this._offscreenRender === true && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]);
});
super();
this._requests = /* @__PURE__ */ new Set();
let t;
try {
if (!globalThis.Worker)
throw this.destroy("Worker not supported");
if (!e)
throw this.destroy("No options provided");
this._loaded = /** @type {Promise<void>} */
new Promise((s) => {
this._init = s;
});
t = _o._test();
if (this._onDemandRender = "requestVideoFrameCallback" in HTMLVideoElement.prototype && (e.onDemandRender ?? true), this._offscreenRender = "transferControlToOffscreen" in HTMLCanvasElement.prototype && !e.canvas && (e.offscreenRender ?? true), this.timeOffset = e.timeOffset || 0, this._video = e.video, this._videoHeight = 0, this._videoWidth = 0, this._videoColorSpace = null, this._canvas = e.canvas, this._video && !this._canvas)
this._canvasParent = document.createElement("div"), this._canvasParent.className = "JASSUB", this._canvasParent.style.position = "relative", this._canvas = this._createCanvas(), this._video.insertAdjacentElement("afterend", this._canvasParent);
else if (!this._canvas)
throw this.destroy("Don't know where to render: you should give video or canvas in options.");
if (this._bufferCanvas = document.createElement("canvas"), this._bufferCtx = this._bufferCanvas.getContext("2d"), !this._bufferCtx)
throw this.destroy("Canvas rendering not supported");
this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = () => {
if (!this._destroyed) this.setRate(this._video.playbackRate);
}, this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = false, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => {
if (this._destroyed) return;
this._worker.postMessage({
target: "init",
wasmUrl: _o._supportsSIMD && e.modernWasmUrl ? e.modernWasmUrl : e.wasmUrl ?? "jassub-worker.wasm",
legacyWasmUrl: e.legacyWasmUrl ?? "jassub-worker.wasm.js",
asyncRender: typeof createImageBitmap < "u" && (e.asyncRender ?? true),
onDemandRender: this._onDemandRender,
width: this._canvasctrl.width || 0,
height: this._canvasctrl.height || 0,
blendMode: e.blendMode || "js",
subUrl: e.subUrl,
subContent: e.subContent || null,
fonts: e.fonts || [],
availableFonts: e.availableFonts || { "liberation sans": "./default.woff2" },
fallbackFont: e.fallbackFont || "liberation sans",
debug: this.debug,
targetFps: e.targetFps || 24,
dropAllAnimations: e.dropAllAnimations,
dropAllBlur: e.dropAllBlur,
libassMemoryLimit: e.libassMemoryLimit || 0,
libassGlyphLimit: e.libassGlyphLimit || 0,
// @ts-ignore
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? true),
hasBitmapBug: _o._hasBitmapBug
}), this._offscreenRender === true && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]);
}).catch((error) => {
if (!this._destroyed) this.destroy(error);
});
} catch (error) {
t?.catch(() => {
});
try {
this.destroy();
} catch {
}
throw error;
}
}
_createCanvas() {
return 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._canvas;
@@ -183,6 +220,7 @@ const _o = class _o extends EventTarget {
* @param {Boolean} [force=false]
*/
resize(e = 0, t = 0, s = 0, r = 0, n = this._video?.paused) {
if (this._destroyed) return;
if ((!e || !t) && this._video) {
const a = this._getVideoPosition();
let i = null;
@@ -193,7 +231,7 @@ const _o = class _o extends EventTarget {
i = this._computeCanvasSize(a.width || 0, a.height || 0);
e = i.width, t = i.height, this._canvasParent && (s = a.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top), r = a.x), this._canvas.style.width = a.width + "px", this._canvas.style.height = a.height + "px";
}
this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === false ? this.busy = true : n = false, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video.videoWidth, videoHeight: this._videoHeight || this._video.videoHeight, force: n });
this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === false ? this.busy = true : n = false, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video?.videoWidth || e, videoHeight: this._videoHeight || this._video?.videoHeight || t, force: n });
}
_getVideoPosition(e = this._video.videoWidth, t = this._video.videoHeight) {
const s = e / t, { offsetWidth: r, offsetHeight: n } = this._video, a = r / n;
@@ -213,6 +251,7 @@ const _o = class _o extends EventTarget {
return { width: e, height: t };
}
_timeupdate({ type: e }) {
if (this._destroyed) return;
const s = {
seeking: true,
waiting: true,
@@ -225,7 +264,34 @@ const _o = class _o extends EventTarget {
* @param {HTMLVideoElement} video
*/
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, false), e.addEventListener("progress", this._boundTimeUpdate, false), e.addEventListener("waiting", this._boundTimeUpdate, false), e.addEventListener("seeking", this._boundTimeUpdate, false), e.addEventListener("playing", this._boundTimeUpdate, false), e.addEventListener("ratechange", this._boundSetRate, false), e.addEventListener("resize", this._boundResize, false)), "VideoFrame" in window && (e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, false), e.readyState > 2 && this._updateColorSpace()), e.videoWidth > 0 && this.resize(), typeof ResizeObserver < "u" && (this._ro || (this._ro = new ResizeObserver(() => this.resize())), this._ro.observe(e))) : this._error("Video element invalid!");
if (this._destroyed) return;
if (!(e instanceof HTMLVideoElement)) {
this._error("Video element invalid!");
return;
}
this._removeListeners();
this._video = e;
this._videoWidth = this._videoHeight = 0;
this._videoColorSpace = null;
if (this._canvasParent && this._canvasParent.parentNode !== e.parentNode)
e.insertAdjacentElement("afterend", this._canvasParent);
if (this._onDemandRender) this._requestFrame();
else {
this._playstate = e.paused;
for (const name of ["timeupdate", "progress", "waiting", "seeking", "playing"])
e.addEventListener(name, this._boundTimeUpdate, false);
e.addEventListener("ratechange", this._boundSetRate, false);
e.addEventListener("resize", this._boundResize, false);
}
if ("VideoFrame" in window) {
e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, false);
if (e.readyState > 2) this._updateColorSpace();
}
if (e.videoWidth > 0) this.resize();
if (typeof ResizeObserver < "u") {
this._ro || (this._ro = new ResizeObserver(() => this.resize()));
this._ro.observe(e);
}
}
runBenchmark() {
this.sendMessage("runBenchmark");
@@ -317,7 +383,7 @@ const _o = class _o extends EventTarget {
getEvents(e) {
this._fetchFromWorker({
target: "getEvents"
}, (t, { events: s }) => {
}, (t, { events: s } = {}) => {
e(t, s);
});
}
@@ -392,7 +458,7 @@ const _o = class _o extends EventTarget {
getStyles(e) {
this._fetchFromWorker({
target: "getStyles"
}, (t, { styles: s }) => {
}, (t, { styles: s } = {}) => {
e(t, s);
});
}
@@ -434,12 +500,22 @@ const _o = class _o extends EventTarget {
}
}
_unbusy() {
if (this._destroyed) return;
this._lastDemandTime ? this._demandRender(this._lastDemandTime) : this.busy = false;
}
_handleRVFC(e, { mediaTime: t, width: s, height: r }) {
if (this._destroyed)
return null;
this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = true, this._demandRender({ mediaTime: t, width: s, height: r })), this._video.requestVideoFrameCallback(this._handleRVFC.bind(this));
this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = true, this._demandRender({ mediaTime: t, width: s, height: r })), this._requestFrame();
}
_requestFrame() {
if (this._destroyed || !this._video || this._frameCallback != null) return;
const video = this._video, generation = this._videoGeneration;
this._frameCallback = video.requestVideoFrameCallback((time, metadata) => {
if (this._destroyed || generation !== this._videoGeneration || video !== this._video) return;
this._frameCallback = null;
this._handleRVFC(time, metadata);
});
}
_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 });
@@ -457,12 +533,20 @@ const _o = class _o extends EventTarget {
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = false, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, true);
}
_updateColorSpace() {
this._video.requestVideoFrameCallback(() => {
if (this._destroyed || this._colorCallback != null) return;
const video = this._video, generation = this._videoGeneration;
this._colorCallback = video.requestVideoFrameCallback(() => {
if (this._destroyed || generation !== this._videoGeneration || video !== this._video) return;
this._colorCallback = null;
let frame;
try {
const e = new VideoFrame(this._video);
this._videoColorSpace = _[e.colorSpace.matrix], e.close(), this.sendMessage("getColorSpace");
frame = new VideoFrame(video);
this._videoColorSpace = _[frame.colorSpace.matrix];
this.sendMessage("getColorSpace");
} catch (e) {
console.warn(e);
} finally {
frame?.close();
}
});
}
@@ -496,6 +580,7 @@ const _o = class _o extends EventTarget {
return e;
}
_ready() {
if (this._destroyed) return;
this._init(), this.dispatchEvent(new CustomEvent("ready"));
}
/**
@@ -505,7 +590,10 @@ const _o = class _o extends EventTarget {
* @param {Transferable[]} [transferable] Array of transferables.
*/
async sendMessage(e, t = {}, s) {
await this._loaded, s ? this._worker.postMessage({
if (this._destroyed) return;
await this._loaded;
if (this._destroyed) return;
s ? this._worker.postMessage({
target: e,
transferable: s,
...t
@@ -515,23 +603,53 @@ const _o = class _o extends EventTarget {
});
}
_fetchFromWorker(e, t) {
if (this._destroyed) {
t(new Error("JASSUB instance destroyed"));
return;
}
const worker = this._worker;
let timer, settled = false;
const cleanup = () => {
worker.removeEventListener("message", onMessage);
worker.removeEventListener("error", onError);
clearTimeout(timer);
this._requests.delete(cancel);
};
const finish = (error, data) => {
if (settled) return;
settled = true;
cleanup();
t(error, data);
};
const onMessage = ({ data }) => {
if (data.target === e.target) finish(null, data);
};
const onError = (error) => finish(error);
const cancel = () => {
if (settled) return;
settled = true;
cleanup();
return () => t(new Error("JASSUB instance destroyed"));
};
try {
const s = e.target, r = setTimeout(() => {
a(new Error("Error: Timeout while try to fetch " + s));
}, 5e3), n = ({ data: i }) => {
i.target === s && (t(null, i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r));
}, a = (i) => {
t(i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r);
};
this._worker.addEventListener("message", n), this._worker.addEventListener("error", a), this._worker.postMessage(e);
this._requests.add(cancel);
worker.addEventListener("message", onMessage);
worker.addEventListener("error", onError);
timer = setTimeout(() => onError(new Error("Error: Timeout while try to fetch " + e.target)), 5e3);
worker.postMessage(e);
} catch (s) {
this._error(s);
if (settled) throw s;
finish(s);
}
}
_console({ content: e, command: t }) {
console[t].apply(console, JSON.parse(e));
}
_onmessage({ data: e }) {
if (this._destroyed) {
for (const image of e.images || []) image.image?.close?.();
return;
}
this["_" + e.target] && this["_" + e.target](e);
}
_error(e) {
@@ -539,6 +657,12 @@ const _o = class _o extends EventTarget {
return this.dispatchEvent(s), console.error(t), t;
}
_removeListeners() {
this._videoGeneration = (this._videoGeneration || 0) + 1;
for (const id of [this._frameCallback, this._colorCallback])
if (id != null) this._video?.cancelVideoFrameCallback?.(id);
this._frameCallback = this._colorCallback = null;
this._lastDemandTime = null;
if (this._onDemandRender) this.busy = false;
this._video && (this._ro && this._ro.unobserve(this._video), this._ctx && (this._ctx.filter = "none"), this._video.removeEventListener("timeupdate", this._boundTimeUpdate), this._video.removeEventListener("progress", this._boundTimeUpdate), this._video.removeEventListener("waiting", this._boundTimeUpdate), this._video.removeEventListener("seeking", this._boundTimeUpdate), this._video.removeEventListener("playing", this._boundTimeUpdate), this._video.removeEventListener("ratechange", this._boundSetRate), this._video.removeEventListener("resize", this._boundResize), this._video.removeEventListener("loadedmetadata", this._boundUpdateColorSpace));
}
/**
@@ -546,7 +670,38 @@ const _o = class _o extends EventTarget {
* @param {String|Error} [err] Error to throw when destroying.
*/
destroy(e) {
return e && (e = this._error(e)), this._video && this._canvasParent && this._video.parentNode?.removeChild(this._canvasParent), this._destroyed = true, this._removeListeners(), this.sendMessage("destroy"), this._worker?.terminate(), e;
if (this._destroyed) return e && this._error(e);
this._destroyed = true;
let failure, failed = false;
const attempt = (callback) => {
try {
callback();
} catch (error) {
if (!failed) {
failure = error;
failed = true;
}
}
};
if (e) attempt(() => {
e = this._error(e);
});
attempt(() => this._removeListeners());
attempt(() => this._ro?.disconnect());
attempt(() => this._canvasParent?.remove());
this._init?.();
const callbacks = [];
for (const cancel of [...this._requests || []]) attempt(() => {
const notify = cancel();
if (notify) callbacks.push(notify);
});
for (const notify of callbacks) attempt(notify);
if (this._worker) {
this._worker.onmessage = this._worker.onerror = null;
attempt(() => this._worker.terminate());
}
if (failed) throw failure;
return e;
}
};
// test support for WASM, ImageData, alphaBug, but only once, on init so it doesn't run when first running the page
+186 -42
View File
@@ -1,25 +1,50 @@
/* eslint-disable */
typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype && (HTMLVideoElement.prototype._rvfcpolyfillmap = {}, HTMLVideoElement.prototype.requestVideoFrameCallback = function (c) {
const e = performance.now(), t = this.getVideoPlaybackQuality(), s = this.mozPresentedFrames || this.mozPaintedFrames || t.totalVideoFrames - t.droppedVideoFrames, r = (n, a) => {
const i = this.getVideoPlaybackQuality(), h = this.mozPresentedFrames || this.mozPaintedFrames || i.totalVideoFrames - i.droppedVideoFrames;
if (h > s) {
const d = this.mozFrameDelay || i.totalFrameDelay - t.totalFrameDelay || 0, l = a - n;
c(a, {
presentationTime: a + d * 1e3,
expectedDisplayTime: a + l,
width: this.videoWidth,
height: this.videoHeight,
mediaTime: Math.max(0, this.currentTime || 0) + l / 1e3,
presentedFrames: h,
processingDuration: d
}), delete this._rvfcpolyfillmap[e];
} else
this._rvfcpolyfillmap[e] = requestAnimationFrame((d) => r(a, d));
if (typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype) {
HTMLVideoElement.prototype.requestVideoFrameCallback = function (callback) {
if (!Object.prototype.hasOwnProperty.call(this, "_rvfcpolyfillmap"))
this._rvfcpolyfillmap = {};
const id = this._rvfcpolyfillid = (this._rvfcpolyfillid || 0) + 1;
const started = performance.now(), initial = this.getVideoPlaybackQuality(), initialTime = this.currentTime;
const initialFrames = this.mozPresentedFrames || this.mozPaintedFrames || initial.totalVideoFrames - initial.droppedVideoFrames;
const poll = (previous, now) => {
if (!(id in this._rvfcpolyfillmap)) return;
const quality = this.getVideoPlaybackQuality();
const frames = this.mozPresentedFrames || this.mozPaintedFrames || quality.totalVideoFrames - quality.droppedVideoFrames;
const advanced = frames > initialFrames;
// Some WebKit backends expose quality counters but keep both at zero.
// Approximate media-time delivery only there, capped at 30 Hz; never invent frame counts.
const mediaFallback = initialFrames === 0 && frames === 0
&& initial.totalVideoFrames === 0 && initial.droppedVideoFrames === 0
&& quality.totalVideoFrames === 0 && quality.droppedVideoFrames === 0
&& Number.isFinite(this.currentTime) && this.currentTime !== initialTime
&& this.readyState >= 2 && !this.seeking && now - started >= 1000 / 30;
if (advanced || mediaFallback) {
const delay = this.mozFrameDelay || quality.totalFrameDelay - initial.totalFrameDelay || 0;
const elapsed = now - previous;
delete this._rvfcpolyfillmap[id];
callback(now, {
presentationTime: now + delay * 1e3,
expectedDisplayTime: now + elapsed,
width: this.videoWidth,
height: this.videoHeight,
mediaTime: mediaFallback && !advanced ? Math.max(0, this.currentTime) : Math.max(0, this.currentTime || 0) + elapsed / 1e3,
presentedFrames: frames,
processingDuration: delay
});
} else {
this._rvfcpolyfillmap[id] = requestAnimationFrame((time) => poll(now, time));
}
};
this._rvfcpolyfillmap[id] = requestAnimationFrame((time) => poll(started, time));
return id;
};
return this._rvfcpolyfillmap[e] = requestAnimationFrame((n) => r(e, n)), e;
}, HTMLVideoElement.prototype.cancelVideoFrameCallback = function (c) {
cancelAnimationFrame(this._rvfcpolyfillmap[c]), delete this._rvfcpolyfillmap[c];
});
HTMLVideoElement.prototype.cancelVideoFrameCallback = function (id) {
const callbacks = this._rvfcpolyfillmap;
if (!callbacks || !(id in callbacks)) return;
cancelAnimationFrame(callbacks[id]);
delete callbacks[id];
};
}
const _ = {
bt709: "BT709",
// these might not be exactly correct? oops?
@@ -74,7 +99,11 @@ class o extends EventTarget {
* @param {Number} [options.libassGlyphLimit] libass glyph cache memory limit in MiB (approximate).
*/
constructor(e) {
if (super(), !globalThis.Worker)
super();
this._requests = new Set();
let t;
try {
if (!globalThis.Worker)
throw this.destroy("Worker not supported");
if (!e)
throw this.destroy("No options provided");
@@ -82,14 +111,15 @@ class o extends EventTarget {
new Promise((s) => {
this._init = s;
});
const t = o._test();
t = o._test();
if (this._onDemandRender = "requestVideoFrameCallback" in HTMLVideoElement.prototype && (e.onDemandRender ?? !0), this._offscreenRender = "transferControlToOffscreen" in HTMLCanvasElement.prototype && !e.canvas && (e.offscreenRender ?? !0), this.timeOffset = e.timeOffset || 0, this._video = e.video, this._videoHeight = 0, this._videoWidth = 0, this._videoColorSpace = null, this._canvas = e.canvas, this._video && !this._canvas)
this._canvasParent = document.createElement("div"), this._canvasParent.className = "JASSUB", this._canvasParent.style.position = "relative", this._canvas = this._createCanvas(), this._video.insertAdjacentElement("afterend", this._canvasParent);
else if (!this._canvas)
throw this.destroy("Don't know where to render: you should give video or canvas in options.");
if (this._bufferCanvas = document.createElement("canvas"), this._bufferCtx = this._bufferCanvas.getContext("2d"), !this._bufferCtx)
throw this.destroy("Canvas rendering not supported");
this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = this.setRate.bind(this), this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = !1, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => {
this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = () => { if (!this._destroyed) this.setRate(this._video.playbackRate); }, this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = !1, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => {
if (this._destroyed) return;
this._worker.postMessage({
target: "init",
wasmUrl: o._supportsSIMD && e.modernWasmUrl ? e.modernWasmUrl : e.wasmUrl ?? "jassub-worker.wasm",
@@ -114,7 +144,14 @@ class o extends EventTarget {
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? !0),
hasBitmapBug: o._hasBitmapBug
}), this._offscreenRender === !0 && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]);
}).catch((error) => {
if (!this._destroyed) this.destroy(error);
});
} catch (error) {
t?.catch(() => {});
try { this.destroy(); } catch {}
throw error;
}
}
_createCanvas() {
return 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._canvas;
@@ -182,6 +219,7 @@ class o extends EventTarget {
* @param {Boolean} [force=false]
*/
resize(e = 0, t = 0, s = 0, r = 0, n = this._video?.paused) {
if (this._destroyed) return;
if ((!e || !t) && this._video) {
const a = this._getVideoPosition();
let i = null;
@@ -192,7 +230,7 @@ class o extends EventTarget {
i = this._computeCanvasSize(a.width || 0, a.height || 0);
e = i.width, t = i.height, this._canvasParent && (s = a.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top), r = a.x), this._canvas.style.width = a.width + "px", this._canvas.style.height = a.height + "px";
}
this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === !1 ? this.busy = !0 : n = !1, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video.videoWidth, videoHeight: this._videoHeight || this._video.videoHeight, force: n });
this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === !1 ? this.busy = !0 : n = !1, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video?.videoWidth || e, videoHeight: this._videoHeight || this._video?.videoHeight || t, force: n });
}
_getVideoPosition(e = this._video.videoWidth, t = this._video.videoHeight) {
const s = e / t, { offsetWidth: r, offsetHeight: n } = this._video, a = r / n;
@@ -212,6 +250,7 @@ class o extends EventTarget {
return { width: e, height: t };
}
_timeupdate({ type: e }) {
if (this._destroyed) return;
const s = {
seeking: !0,
waiting: !0,
@@ -224,7 +263,34 @@ class o extends EventTarget {
* @param {HTMLVideoElement} video
*/
setVideo(e) {
e instanceof HTMLVideoElement ? (this._removeListeners(), this._video = e, this._onDemandRender ? this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)) : (this._playstate = e.paused, e.addEventListener("timeupdate", this._boundTimeUpdate, !1), e.addEventListener("progress", this._boundTimeUpdate, !1), e.addEventListener("waiting", this._boundTimeUpdate, !1), e.addEventListener("seeking", this._boundTimeUpdate, !1), e.addEventListener("playing", this._boundTimeUpdate, !1), e.addEventListener("ratechange", this._boundSetRate, !1), e.addEventListener("resize", this._boundResize, !1)), "VideoFrame" in window && (e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, !1), e.readyState > 2 && this._updateColorSpace()), e.videoWidth > 0 && this.resize(), typeof ResizeObserver < "u" && (this._ro || (this._ro = new ResizeObserver(() => this.resize())), this._ro.observe(e))) : this._error("Video element invalid!");
if (this._destroyed) return;
if (!(e instanceof HTMLVideoElement)) {
this._error("Video element invalid!");
return;
}
this._removeListeners();
this._video = e;
this._videoWidth = this._videoHeight = 0;
this._videoColorSpace = null;
if (this._canvasParent && this._canvasParent.parentNode !== e.parentNode)
e.insertAdjacentElement("afterend", this._canvasParent);
if (this._onDemandRender) this._requestFrame();
else {
this._playstate = e.paused;
for (const name of ["timeupdate", "progress", "waiting", "seeking", "playing"])
e.addEventListener(name, this._boundTimeUpdate, false);
e.addEventListener("ratechange", this._boundSetRate, false);
e.addEventListener("resize", this._boundResize, false);
}
if ("VideoFrame" in window) {
e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, false);
if (e.readyState > 2) this._updateColorSpace();
}
if (e.videoWidth > 0) this.resize();
if (typeof ResizeObserver < "u") {
this._ro || (this._ro = new ResizeObserver(() => this.resize()));
this._ro.observe(e);
}
}
runBenchmark() {
this.sendMessage("runBenchmark");
@@ -316,7 +382,7 @@ class o extends EventTarget {
getEvents(e) {
this._fetchFromWorker({
target: "getEvents"
}, (t, { events: s }) => {
}, (t, { events: s } = {}) => {
e(t, s);
});
}
@@ -391,7 +457,7 @@ class o extends EventTarget {
getStyles(e) {
this._fetchFromWorker({
target: "getStyles"
}, (t, { styles: s }) => {
}, (t, { styles: s } = {}) => {
e(t, s);
});
}
@@ -433,12 +499,22 @@ class o extends EventTarget {
}
}
_unbusy() {
if (this._destroyed) return;
this._lastDemandTime ? this._demandRender(this._lastDemandTime) : this.busy = !1;
}
_handleRVFC(e, { mediaTime: t, width: s, height: r }) {
if (this._destroyed)
return null;
this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = !0, this._demandRender({ mediaTime: t, width: s, height: r })), this._video.requestVideoFrameCallback(this._handleRVFC.bind(this));
this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = !0, this._demandRender({ mediaTime: t, width: s, height: r })), this._requestFrame();
}
_requestFrame() {
if (this._destroyed || !this._video || this._frameCallback != null) return;
const video = this._video, generation = this._videoGeneration;
this._frameCallback = video.requestVideoFrameCallback((time, metadata) => {
if (this._destroyed || generation !== this._videoGeneration || video !== this._video) return;
this._frameCallback = null;
this._handleRVFC(time, metadata);
});
}
_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 });
@@ -456,12 +532,20 @@ class o extends EventTarget {
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = !1, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, !0);
}
_updateColorSpace() {
this._video.requestVideoFrameCallback(() => {
if (this._destroyed || this._colorCallback != null) return;
const video = this._video, generation = this._videoGeneration;
this._colorCallback = video.requestVideoFrameCallback(() => {
if (this._destroyed || generation !== this._videoGeneration || video !== this._video) return;
this._colorCallback = null;
let frame;
try {
const e = new VideoFrame(this._video);
this._videoColorSpace = _[e.colorSpace.matrix], e.close(), this.sendMessage("getColorSpace");
frame = new VideoFrame(video);
this._videoColorSpace = _[frame.colorSpace.matrix];
this.sendMessage("getColorSpace");
} catch (e) {
console.warn(e);
} finally {
frame?.close();
}
});
}
@@ -495,6 +579,7 @@ class o extends EventTarget {
return e;
}
_ready() {
if (this._destroyed) return;
this._init(), this.dispatchEvent(new CustomEvent("ready"));
}
/**
@@ -504,7 +589,10 @@ class o extends EventTarget {
* @param {Transferable[]} [transferable] Array of transferables.
*/
async sendMessage(e, t = {}, s) {
await this._loaded, s ? this._worker.postMessage({
if (this._destroyed) return;
await this._loaded;
if (this._destroyed) return;
s ? this._worker.postMessage({
target: e,
transferable: s,
...t
@@ -514,23 +602,51 @@ class o extends EventTarget {
});
}
_fetchFromWorker(e, t) {
if (this._destroyed) {
t(new Error("JASSUB instance destroyed"));
return;
}
const worker = this._worker;
let timer, settled = false;
const cleanup = () => {
worker.removeEventListener("message", onMessage);
worker.removeEventListener("error", onError);
clearTimeout(timer);
this._requests.delete(cancel);
};
const finish = (error, data) => {
if (settled) return;
settled = true;
cleanup();
t(error, data);
};
const onMessage = ({ data }) => { if (data.target === e.target) finish(null, data); };
const onError = (error) => finish(error);
const cancel = () => {
if (settled) return;
settled = true;
cleanup();
return () => t(new Error("JASSUB instance destroyed"));
};
try {
const s = e.target, r = setTimeout(() => {
a(new Error("Error: Timeout while try to fetch " + s));
}, 5e3), n = ({ data: i }) => {
i.target === s && (t(null, i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r));
}, a = (i) => {
t(i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r);
};
this._worker.addEventListener("message", n), this._worker.addEventListener("error", a), this._worker.postMessage(e);
this._requests.add(cancel);
worker.addEventListener("message", onMessage);
worker.addEventListener("error", onError);
timer = setTimeout(() => onError(new Error("Error: Timeout while try to fetch " + e.target)), 5e3);
worker.postMessage(e);
} catch (s) {
this._error(s);
if (settled) throw s;
finish(s);
}
}
_console({ content: e, command: t }) {
console[t].apply(console, JSON.parse(e));
}
_onmessage({ data: e }) {
if (this._destroyed) {
for (const image of e.images || []) image.image?.close?.();
return;
}
this["_" + e.target] && this["_" + e.target](e);
}
_error(e) {
@@ -538,6 +654,12 @@ class o extends EventTarget {
return this.dispatchEvent(s), console.error(t), t;
}
_removeListeners() {
this._videoGeneration = (this._videoGeneration || 0) + 1;
for (const id of [this._frameCallback, this._colorCallback])
if (id != null) this._video?.cancelVideoFrameCallback?.(id);
this._frameCallback = this._colorCallback = null;
this._lastDemandTime = null;
if (this._onDemandRender) this.busy = false;
this._video && (this._ro && this._ro.unobserve(this._video), this._ctx && (this._ctx.filter = "none"), this._video.removeEventListener("timeupdate", this._boundTimeUpdate), this._video.removeEventListener("progress", this._boundTimeUpdate), this._video.removeEventListener("waiting", this._boundTimeUpdate), this._video.removeEventListener("seeking", this._boundTimeUpdate), this._video.removeEventListener("playing", this._boundTimeUpdate), this._video.removeEventListener("ratechange", this._boundSetRate), this._video.removeEventListener("resize", this._boundResize), this._video.removeEventListener("loadedmetadata", this._boundUpdateColorSpace));
}
/**
@@ -545,7 +667,29 @@ class o extends EventTarget {
* @param {String|Error} [err] Error to throw when destroying.
*/
destroy(e) {
return 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(), e;
if (this._destroyed) return e && this._error(e);
this._destroyed = true;
let failure, failed = false;
const attempt = (callback) => {
try { callback(); } catch (error) { if (!failed) { failure = error; failed = true; } }
};
if (e) attempt(() => { e = this._error(e); });
attempt(() => this._removeListeners());
attempt(() => this._ro?.disconnect());
attempt(() => this._canvasParent?.remove());
this._init?.();
const callbacks = [];
for (const cancel of [...this._requests || []]) attempt(() => {
const notify = cancel();
if (notify) callbacks.push(notify);
});
for (const notify of callbacks) attempt(notify);
if (this._worker) {
this._worker.onmessage = this._worker.onerror = null;
attempt(() => this._worker.terminate());
}
if (failed) throw failure;
return e;
}
}
export {
@@ -41,15 +41,18 @@ Switching to `/runtime` intentionally changes type inference. For example,
`instance.resize(640, 360, 0, 0, true)` describes the actual call order.
`await instance.destroy()` remains legal JavaScript, but `.then()` on that result
never worked at runtime. Resource teardown completion is not represented by a Promise.
The callback error channel is `Error | ErrorEvent | null`; an error may omit the data.
The currently frozen vendor has a request timeout/error bug that throws before user
callbacks run. PKG-JASSUB-07 tracks the fix; typing that channel does not fix or guarantee
callback delivery. Query outputs do not contain the upstream npm declaration's `_index`.
The callback error channel is `Error | Event | null`; ErrorEvent is included, and real
CSP Worker failures can deliver a plain Event. Requests clean their listeners/timer
before calling the consumer once. Destroy settles pending callbacks with an Error and
undefined data after all request resources are removed, even if another callback throws.
Query outputs do not contain the upstream npm declaration's `_index`. Issue queries after
ready; the Worker protocol still identifies responses by target, not a new request ID.
The accurate instance extends EventTarget and preserves its normal listener APIs.
`destroy(error)` returns the original Error, or converts a nonempty string to Error;
an empty string is returned unchanged. Repeated direct destruction and construction/
video clock failures remain separate vendor lifecycle work, documented in ARCHITECTURE.md.
an empty string is returned unchanged. Direct destruction is idempotent, and queued
sendMessage operations settle without posting after destruction. Lifecycle and the
limited media-time fallback for zero frame counters are documented in ARCHITECTURE.md.
NodeNext ESM retains the root declaration's historical CommonJS namespace behavior.
If existing code uses the root namespace's `.default` to satisfy that declaration,
@@ -59,8 +62,8 @@ Do not invent `jassub.default`: the current JavaScript factory has no self alias
Older TypeScript resolves `/legacy` and `/runtime` through exact typesVersions mappings.
Existing direct declaration paths are retained; no wildcard remaps unrelated paths.
Maintain `runtime-api.d.ts` against the frozen wrapper and worker behavior, not a new
upstream version. Keep CJS/ESM entry aliases synchronized. Run `yarn test:jassub-types`,
Maintain `runtime-api.d.ts` against the recorded base plus local wrapper patches and
actual worker behavior, not a new upstream version. Keep CJS/ESM entry aliases synchronized. Run `yarn test:jassub-types`,
`yarn test:jassub-types-package`, `yarn test:jassub`, and `yarn typecheck` after changes.
The package runner installs actual historical archives and the candidate outside the
workspace, checks strict positive/negative consumers, and verifies artifact identity.
+5 -6
View File
@@ -88,13 +88,12 @@ export interface AssStyle {
export type AssEventInput = Partial<AssEvent>
export type AssStyleInput = Partial<AssStyle>
/** Timeout errors and native Worker error events are separate channels. */
export type WorkerRequestError = Error | ErrorEvent
/** Timeout errors and native Worker events (including plain CSP error Events). */
export type WorkerRequestError = Error | Event
/**
* Success supplies null plus an array. Error payloads are absent.
* The frozen vendor currently throws before notifying this callback on request
* errors/timeouts; PKG-JASSUB-07 owns that defect. These types do not guarantee
* callback delivery or claim that the error path has been repaired.
* Requests release their listeners/timer before invoking callbacks. Destruction
* settles pending requests with an Error; native Worker error Events retain identity.
*/
export type EventsCallback = (error: WorkerRequestError | null, events?: AssEvent[]) => void
export type StylesCallback = (error: WorkerRequestError | null, styles?: AssStyle[]) => void
@@ -139,7 +138,7 @@ export interface RuntimeInstance extends EventTarget {
disableStyleOverride: () => void
setDefaultFont: (font: string) => void
addFont: (font: FontSource) => void
/** Resolves after posting the message; it is not a Worker-operation acknowledgement. */
/** Resolves after posting, or without posting after destruction; not a Worker acknowledgement. */
sendMessage: (target: string, data?: Record<string, unknown> | null, transferable?: Transferable[]) => Promise<void>
destroy: (() => void) & ((error: Error) => Error) & ((error: string) => Error | '') & ((error: Error | string | undefined) => Error | '' | undefined)