mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-24 23:51:20 -08:00
fix(jassub): [PKG-JASSUB-07] repair vendor lifecycle and subtitle frame clock
This commit is contained in:
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
|
||||
+235
-80
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user