mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -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
@@ -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
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
|
||||
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
|
||||
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
@@ -90,7 +90,7 @@
|
||||
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
|
||||
"test:multiple-subtitles-types-package": "node refactor/scripts/multiple-subtitles-package-types.mjs",
|
||||
"test:jassub": "node --test test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs refactor/scripts/jassub-failures.test.mjs refactor/scripts/jassub-types.test.mjs",
|
||||
"test:jassub": "node --test test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs refactor/scripts/jassub-failures.test.mjs refactor/scripts/jassub-types.test.mjs refactor/scripts/jassub-vendor-patch.test.mjs",
|
||||
"test:jassub-types": "node --test refactor/scripts/jassub-types.test.mjs",
|
||||
"test:jassub-types-package": "node refactor/scripts/jassub-package-types.mjs",
|
||||
"check:ci": "node refactor/scripts/ci-workflow.mjs",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"task": "PKG-JASSUB-07",
|
||||
"baselineCommit": "5e445b1af655a1cafbfa3abca62d45207c042936",
|
||||
"file": "packages/artplayer-plugin-jassub/src/jassub.es.js",
|
||||
"originalSha256LF": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038",
|
||||
"patchedSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb",
|
||||
"patchFile": "refactor/baselines/jassub-vendor.patch",
|
||||
"patchSha256LF": "fe6d429418a676f5d1b9c4daf9cd6a20be27c3a1b18f2a3d691a943188f7bfce",
|
||||
"origin": "Original wrapper matches frozen npm jassub 1.8.8 after format normalization. This is an explicit local patch, not a claim of unchanged upstream bytes.",
|
||||
"changes": [
|
||||
"constructor rollback and cancellation of late initialization",
|
||||
"idempotent destruction and ownership of actual canvas parent",
|
||||
"per-video generations and cancellation for render/color-space callbacks",
|
||||
"numeric fallback playbackRate adapter",
|
||||
"query settlement and cleanup before callbacks, including destroy cancellation",
|
||||
"per-video polyfill IDs/maps and zero-quality-counter media-time fallback capped at 30 Hz per continuous chain"
|
||||
],
|
||||
"unchanged": "Worker JS, WASM, fonts, resource URLs, root declarations and actual public instance identity remain unchanged. Original provenance manifests are not rewritten."
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,373 @@
|
||||
diff --git a/packages/artplayer-plugin-jassub/src/jassub.es.js b/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
index f5a8f292c..904b40888 100644
|
||||
--- a/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
+++ b/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
@@ -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 {
|
||||
@@ -0,0 +1,115 @@
|
||||
# PKG-JASSUB-07 生命周期与字幕时钟修复
|
||||
|
||||
## 变更与兼容边界
|
||||
|
||||
本任务修改已登记的第三方 wrapper `src/jassub.es.js`,保留 vendor JS 例外,
|
||||
不把第三方生成代码机械迁为 TS。自有入口和 registration 继续由两个严格 TS
|
||||
模块负责。原 jassub 1.8.8 来源记录保留,新增可反向校验的
|
||||
[补丁](../baselines/jassub-vendor.patch)及[来源指纹](../baselines/jassub-vendor-patch.json),
|
||||
third-party.json 同时注明原始与本地修改后的身份,防止后续更新覆盖修复。
|
||||
Worker JS、WASM、字体、资源 URL 和旧根声明均未修改。没有新增依赖或锁文件变更。
|
||||
|
||||
公开工厂仍同步返回原生 JASSUB 实例;resize/setVideo/setTrack/destroy 的同步
|
||||
返回值、字幕协议和实例方法保留。本次修复 API-05/08/10/12 的已复现缺陷:
|
||||
|
||||
- 构造任一阶段抛错时,回收此前已创建的 DOM、监听器、帧回调和 Worker,重新
|
||||
抛出原始异常;异步能力检测不再向已销毁实例发送初始化消息。
|
||||
- destroy 首先进入终态,随后尽力完成所有清理,重复调用安全;删除实际归属的
|
||||
字幕容器,保留调用者 canvas/video。排队但尚未发送的消息取消,其 Promise
|
||||
正常结束,不再向已经 terminate 的 Worker 发消息。
|
||||
- setVideo 转移自建容器并释放旧视频订阅。render/color-space 回调分别记录归属
|
||||
和代次,即使旧回调已经排队,也不能重新启动旧循环或传入旧字幕时间。
|
||||
- 事件时钟的 ratechange 读取当前视频的数值 playbackRate,直接 setRate(number)
|
||||
保持原样,不再把 Event 对象发送给 Worker。
|
||||
- getEvents/getStyles 的 timeout/Worker error 缺少数据时不再先解构 undefined。
|
||||
每请求监听器与定时器在回调前释放,过期事件不重复通知。销毁先清理全部请求,
|
||||
再各通知一次取消错误;某个用户回调抛出异常仍完成其他清理和 Worker 终止,
|
||||
最后重新抛出第一个异常。正常查询仍返回真实 WASM 数据。
|
||||
|
||||
原生 CSP 错误在三个浏览器中均观察到 trusted plain Event,并不保证是 ErrorEvent。
|
||||
因此新增可选 `/runtime` 的准确回调类型使用 `Error | Event | null`;旧根声明完全
|
||||
保持,不要求旧入口消费者修改。此入口的迁移说明和 sendMessage 取消语义在
|
||||
包内 types/README.md 更新。Worker 加载错误沿已有 error 通道送达,调用者仍可
|
||||
显式销毁;本任务不承诺自动重试、全量错误恢复或新的查询请求 ID 协议。
|
||||
|
||||
## Windows WebKit 默认字幕停滞
|
||||
|
||||
冻结 npm 1.1.0 的真实播放中,currentTime 已到 6.49 秒且 readyState=4,
|
||||
totalVideoFrames/droppedVideoFrames 始终为零。浏览器没有原生 RVFC,旧 polyfill
|
||||
注册一次但不交付,Worker 仅产生空绘制,没有任何字幕 demand。
|
||||
|
||||
修复只针对 polyfill 中初始和当前质量计数均为零的情况:媒体时间必须有限且
|
||||
确实变化、readyState >= 2、没有正在 seek;按当前媒体时间交付近似回调,并限制
|
||||
每条连续注册链不超过 30 Hz。暂停后完成 seek 可交付,静止暂停不持续交付。
|
||||
presentedFrames 仍如实为零,不虚构解码帧。原生 RVFC 和正常增长的质量计数路径
|
||||
保留,后者不受该近似限速影响。独立视频的 map 和单调 ID 同时修复相同时间戳
|
||||
覆盖回调、跨视频取消和回调异常后未清理的问题。
|
||||
|
||||
这是有运行证据的局部兼容修复,不是全局强制 onDemandRender=false。实际默认
|
||||
onDemandRender=true 的播放、拖动、网页全屏、自定义 canvas 都已验证;所有这些
|
||||
绘制验证显式 offscreenRender=false,不能替代默认 offscreen、设备和 GPU 验收。
|
||||
|
||||
## 验证与中间失败
|
||||
|
||||
[验证记录](../baselines/jassub-vendor-validation.json)保留输入 hash、原始日志位置、
|
||||
安装报告和浏览器 JSON 摘要;本地 cache 不作为唯一永久证据。
|
||||
|
||||
| 层次 | 本次结果及范围 |
|
||||
| --- | --- |
|
||||
| 生命周期回归 | 同一 21 项在冻结旧产物全部失败,候选全部通过 |
|
||||
| Polyfill 回归 | 旧产物 13 通过/11 失败,候选 24 全通过;涵盖 144 Hz 模拟时钟、原生与正常计数不变、多视频和暂停 seek |
|
||||
| 联合测试 | 185 项通过,含旧行为基线、候选修复、真实 WASM 查询、类型、来源和可逆补丁校验 |
|
||||
| 源码与分发 | 源码、main、legacy 各 68 项通过;正常三格式构建及 docs 副本一致 |
|
||||
| 严格类型 | 全仓 406 个生产 TS 文件及既有消费者流程通过 |
|
||||
| 实际安装 | 15 个编译器/包矩阵通过,准确入口每模式 14 个负例、两种无 interop 消费通过;核心 68/JASSUB 15 个安装成员与最终工作区复核一致 |
|
||||
| 真实浏览器 | Windows Playwright Chromium 153.0.8010.12、Firefox 155、WebKit 26.6,main 默认 9、main 生命周期 12、legacy 21、main 自定义 canvas 9,共 51 项通过,无跳过或重试通过 |
|
||||
|
||||
浏览器播放组合包含候选核心、实际 5.4.0 和 5.3.1-beta.1;生命周期错误测试使用
|
||||
候选核心。原生 Worker/WASM/font/video 真实运行,验证字幕像素、切视频、数值倍率、
|
||||
重复销毁、非法 Worker URL 的同步 SyntaxError 回滚,以及 CSP 异步错误原对象
|
||||
向待处理查询交付一次。没有冒充 Chrome 扩展交互或物理 Safari/iOS/Android 验收。
|
||||
|
||||
保留三个测试编写过程中发现的问题及修正依据:
|
||||
|
||||
1. WebKit 首次播放前 seek 会回到零。fixture 先真实播放,再暂停/seek,随后仍检查
|
||||
播放进度和非透明字幕像素,未删除 WebKit 或放松成功条件。
|
||||
2. CSP 不是同步 SecurityError,回调对象也不保证 ErrorEvent。拆出实际非法 URL
|
||||
构造异常测试,并保留 CSP 异步错误及严格对象身份检查。
|
||||
3. 旧“同步返回值与 resize”测试在立即 destroy 后才检查排队消息。现在销毁前确认
|
||||
resize 尺寸、原同步返回值;额外旧红/新绿测试确认 ready 后排队消息在销毁时取消。
|
||||
初次候选 66/67 的失败日志保留,最终源码及两格式各 68 通过。
|
||||
|
||||
受控 DOM helper 的 appendChild 同步修正为先从旧父节点移除,与真实 DOM 移动
|
||||
语义一致;跨容器结论同时有真实浏览器验证。没有以 mock 取代真实播放。
|
||||
|
||||
main gzip 从 5,434 增至 6,045 字节(+611),legacy 从 5,716 增至 6,385(+669)。
|
||||
新增成本用于资源归属、清理和时钟回退;完整原始/gzip/Brotli 数值存于验证记录,
|
||||
不因此调整其他全项目性能门槛。安装验证复用 04 runner,报告保留原任务标识,
|
||||
07 包装记录明确其最终候选来源。
|
||||
|
||||
## 复跑、维护与后续
|
||||
|
||||
使用 .node-version 的 Node 和 Yarn Classic 1.22.22:
|
||||
|
||||
```sh
|
||||
yarn test:jassub
|
||||
yarn typecheck
|
||||
yarn test:jassub-types-package
|
||||
yarn build artplayer-plugin-jassub
|
||||
node --test refactor/scripts/jassub-vendor-patch.test.mjs
|
||||
```
|
||||
|
||||
test:unit 纳入生命周期/polyfill;test:jassub 还纳入补丁身份校验,baseline glob
|
||||
自动覆盖该来源测试。候选源码与 main/legacy 的环境变量用法见包内 ARCHITECTURE.md。
|
||||
浏览器设置 ARTPLAYER_JASSUB_ARTIFACT 为正常构建,再运行
|
||||
`yarn test:browser test/browser/jassub-native.spec.js test/browser/jassub-lifecycle.spec.js --workers=1`;
|
||||
自定义 canvas 另设 ARTPLAYER_JASSUB_CUSTOM_CANVAS=true。浏览器共享报告每次运行后先归档。
|
||||
|
||||
六项本任务风险已用对应证据关闭。05 继续完整组合、真实设备、默认 offscreen 和
|
||||
错误恢复;06 与 SITE-01 保留 VENDOR-04/05 的通知和字体分发门槛。公开类型/导出
|
||||
的更广泛准入风险不在本补丁任务一并关闭。仍需全部独立 major、CI/CD 与三轮发布
|
||||
复盘,不推送或发布。
|
||||
|
||||
回退本任务 commit 并正常构建即可恢复原 wrapper 与声明;先停止/销毁现有实例,
|
||||
不对运行中的 Worker 热替换。上游升级应重新核对原始来源、逐项移植或确认上游
|
||||
已修复,再运行红绿、类型、安装和原生验证,不能只替换文件并更新指纹。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 232 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 63 / doing 14 / blocked 0 / done 155 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 62 / doing 14 / blocked 0 / done 156 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
| PKG-JASSUB-04 | artplayer-plugin-jassub<br>迁移自有源码和公开类型 | PKG-JASSUB-03, ENG-04, ENG-06, CORE-07 | JASSUB option/instance 的兼容类型包装,vendor JS 例外记录 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
|
||||
| PKG-JASSUB-05 | artplayer-plugin-jassub<br>验证新旧核心和组合 | PKG-JASSUB-04, CORE-22, PKG-JASSUB-07 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-JASSUB-06 | artplayer-plugin-jassub<br>验证分发并同步文档 | PKG-JASSUB-05, ENG-07 | jassub.js、外部资源路径、许可和离线失败记录;接续 VENDOR-04/05 完整组件/字体通知、LGPL 源码分发核对及六份未明确字体的分发处置 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录;来源身份已由01冻结,完整通知和未明确字体的发布处理必须闭环,不能以来源相同替代许可结论 | H | todo |
|
||||
| PKG-JASSUB-07 | artplayer-plugin-jassub<br>修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01) | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | todo |
|
||||
| PKG-JASSUB-07 | artplayer-plugin-jassub<br>修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01) | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | done |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-danmuku-mask
|
||||
|
||||
@@ -536,6 +536,7 @@
|
||||
- PKG-JASSUB-02: [记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md) [记录](baselines/jassub-failures-validation.json)
|
||||
- PKG-JASSUB-03: [记录](changes/2026-09-14-PKG-JASSUB-03-registration.md) [记录](baselines/jassub-registration-validation.json)
|
||||
- PKG-JASSUB-04: [记录](changes/2026-09-14-PKG-JASSUB-04-types.md) [记录](baselines/jassub-types-validation.json)
|
||||
- PKG-JASSUB-07: [记录](changes/2026-09-14-PKG-JASSUB-07-vendor.md) [记录](baselines/jassub-vendor-validation.json) [记录](baselines/jassub-vendor-patch.json)
|
||||
- PKG-MASK-01: [记录](baselines/danmuku-mask-release.json) [记录](baselines/danmuku-mask-registry.json) [记录](baselines/danmuku-mask-contract.md) [记录](baselines/danmuku-mask-contract-validation.json) [记录](changes/2026-09-13-PKG-MASK-01-contract.md)
|
||||
- PKG-MASK-02: [记录](changes/2026-09-13-PKG-MASK-02-failures.md) [记录](baselines/danmuku-mask-failures-validation.json)
|
||||
- PKG-MASK-03: [记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md) [记录](baselines/danmuku-mask-lifecycle-validation.json)
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-JASSUB-07 vendor 生命周期与默认字幕时钟完成
|
||||
|
||||
独立补丁保留原上游身份,修复幂等销毁、构造回滚、跨视频帧归属、数值倍率和
|
||||
查询异常清理;Windows WebKit 全零质量计数的默认逐帧停滞有旧失败及候选修复。
|
||||
联合 185 项、源码/main/legacy 各 68 项、406 生产 TS 检查通过;实际安装 15 个
|
||||
类型矩阵,核心 68/JASSUB 15 成员与候选相符。真实浏览器 51 项通过,包括默认
|
||||
onDemand 播放/seek、自定义 canvas、跨父节点换视频、原生构造与 CSP 错误。
|
||||
[变更与限制](changes/2026-09-14-PKG-JASSUB-07-vendor.md)和
|
||||
[验证记录](baselines/jassub-vendor-validation.json)保留测试时序/CSP/WebKit 的中间
|
||||
失败及修正,不冒充设备、默认 offscreen 或完整发布验收。关闭六项已证实风险,
|
||||
05/06、VENDOR-04/05 及全局三轮复盘仍保留。下一步继续分包兼容验收与未完成迁移。
|
||||
|
||||
## PKG-JASSUB-04 自有 TS 与准确类型入口完成
|
||||
|
||||
两个自有模块已迁为严格 TS,编译后的 JS 和三种正常构建产物保持上一任务行为。
|
||||
|
||||
@@ -242,10 +242,10 @@
|
||||
| DANMUKU-TIMING-01 | resolved / 已复现 | Hidden Worker wait is deducted from visible lifetime and can recycle a comment immediately after visible | PKG-DANMUKU-11 |
|
||||
| DANMUKU-SAMPLING-01 | resolved / 已复现 | Serial asynchronous placement stops native frame eligibility sampling and delays existing visible expiry | PKG-DANMUKU-12 |
|
||||
| MASK-SCHEDULING-01 | open / 已复现 | Native Mask startup can coincide with a missed narrow Danmuku timestamp window | PKG-MASK-05 |
|
||||
| JASSUB-DESTROY-01 | open / 已复现 | Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup | PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-07 |
|
||||
| JASSUB-DESTROY-01 | resolved / 已复现 | Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup | PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-07 |
|
||||
| JASSUB-CANVAS-01 | resolved / 已复现 | Custom canvas registration throws after JASSUB Worker allocation | PKG-JASSUB-02, PKG-JASSUB-03 |
|
||||
| JASSUB-CONSTRUCTION-01 | open / 已复现 | Worker construction failure leaves previously allocated JASSUB DOM and listeners | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-VIDEO-FRAME-01 | open / 已复现 | Old video frame callbacks can drive replacement subtitle time and duplicate chains | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-RATE-01 | open / 已复现 | Fallback ratechange sends an Event instead of numeric playbackRate | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-NATIVE-FRAME-01 | open / 已复现 | Windows WebKit default frame-clock path does not produce subtitle demands in actual playback | PKG-JASSUB-02, PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-QUERY-01 | open / 已复现 | Subtitle query errors throw before calling the consumer and leave request listeners attached | PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-CONSTRUCTION-01 | resolved / 已复现 | Worker construction failure leaves previously allocated JASSUB DOM and listeners | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-VIDEO-FRAME-01 | resolved / 已复现 | Old video frame callbacks can drive replacement subtitle time and duplicate chains | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-RATE-01 | resolved / 已复现 | Fallback ratechange sends an Event instead of numeric playbackRate | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-NATIVE-FRAME-01 | resolved / 已复现 | Windows WebKit default frame-clock path does not produce subtitle demands in actual playback | PKG-JASSUB-02, PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-QUERY-01 | resolved / 已复现 | Subtitle query errors throw before calling the consumer and leave request listeners attached | PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
+60
-12
@@ -5430,7 +5430,7 @@
|
||||
"id": "JASSUB-DESTROY-01",
|
||||
"title": "Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-03",
|
||||
@@ -5444,7 +5444,15 @@
|
||||
"test/jassub-registration.test.js"
|
||||
],
|
||||
"compatibleResolution": "Use actual resource ownership and tolerate direct instance teardown before host teardown. Task03 fixes host cleanup after direct teardown, reentry and retry/rollback. Direct vendor repetition and cross-parent cleanup remain task07 and this risk stays open.",
|
||||
"closureCriteria": "Candidate direct/repeated/host teardown and cross-parent video replacement release container, Worker and listeners without throwing or removing user nodes."
|
||||
"closureCriteria": "Candidate direct/repeated/host teardown and cross-parent video replacement release container, Worker and listeners without throwing or removing user nodes.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Controlled old-red/candidate-green repeated direct/host teardown and native cross-parent replacement release the actual container, video listeners and Worker once, preserving user nodes."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-CANVAS-01",
|
||||
@@ -5475,7 +5483,7 @@
|
||||
"id": "JASSUB-CONSTRUCTION-01",
|
||||
"title": "Worker construction failure leaves previously allocated JASSUB DOM and listeners",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
@@ -5485,13 +5493,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Roll back each construction phase and rethrow the original error without assuming new returned an instance.",
|
||||
"closureCriteria": "Controlled and native construction failure do not retain owned DOM/listeners/callbacks; original error identity and successful construction remain intact."
|
||||
"closureCriteria": "Controlled and native construction failure do not retain owned DOM/listeners/callbacks; original error identity and successful construction remain intact.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Original controlled SecurityError identity survives rollback; actual invalid Worker URL produces native SyntaxError and leaves no owned DOM/subscriptions. Late initialization and application messages are cancelled after destruction."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-VIDEO-FRAME-01",
|
||||
"title": "Old video frame callbacks can drive replacement subtitle time and duplicate chains",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
@@ -5501,13 +5517,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Track video generation and callback ownership across setVideo and destroy.",
|
||||
"closureCriteria": "Actually queued old callbacks cannot deliver stale mediaTime or rearm; replacement video owns one loop, with native evidence."
|
||||
"closureCriteria": "Actually queued old callbacks cannot deliver stale mediaTime or rearm; replacement video owns one loop, with native evidence.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Tests execute callbacks actually removed from a pending queue after setVideo/destroy and reject stale time or rearming. Separate real playback after video replacement renders subtitles and tears down successfully in three browser engines and both main/legacy."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-RATE-01",
|
||||
"title": "Fallback ratechange sends an Event instead of numeric playbackRate",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
@@ -5517,13 +5541,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Adapt event subscription to read the current video rate while retaining direct setRate(number).",
|
||||
"closureCriteria": "Real ratechange and direct calls send numeric values, preserve timing, and avoid stale listeners after source replacement."
|
||||
"closureCriteria": "Real ratechange and direct calls send numeric values, preserve timing, and avoid stale listeners after source replacement.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Controlled and actual browser ratechange send the numeric current playbackRate; cross-video listener cleanup and the existing direct setRate contract remain covered."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-NATIVE-FRAME-01",
|
||||
"title": "Windows WebKit default frame-clock path does not produce subtitle demands in actual playback",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07",
|
||||
@@ -5534,13 +5566,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Investigate native frame delivery and establish a compatible capability response; explicit event-clock success is diagnostic only.",
|
||||
"closureCriteria": "Default native behavior is verified or an explicit reviewed environment limitation is recorded; no silent global fallback or substitution of diagnostic mode for default acceptance."
|
||||
"closureCriteria": "Default native behavior is verified or an explicit reviewed environment limitation is recorded; no silent global fallback or substitution of diagnostic mode for default acceptance.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Native Windows WebKit zero-counter starvation is reproduced on the published default clock. A narrow media-time fallback only in the polyfill with all-zero quality counters restores default onDemand playback and seek; candidate default and custom-canvas pixel assertions pass. Native RVFC and normally increasing counters retain their paths. Default offscreen and physical devices remain task05."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-QUERY-01",
|
||||
"title": "Subtitle query errors throw before calling the consumer and leave request listeners attached",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-07",
|
||||
"PKG-JASSUB-05"
|
||||
@@ -5550,7 +5590,15 @@
|
||||
"test/jassub-runtime.test.js"
|
||||
],
|
||||
"compatibleResolution": "Handle absent worker error payloads, release per-request listeners and timers before invoking user callbacks, and preserve successful synchronous query registration and callback data.",
|
||||
"closureCriteria": "Actual vendor timeout and Worker-error paths invoke the consumer exactly once with the original error, leave no request listeners/timers, and successful real-WASM events/styles remain unchanged. Browser error/recovery acceptance remains PKG-JASSUB-05."
|
||||
"closureCriteria": "Actual vendor timeout and Worker-error paths invoke the consumer exactly once with the original error, leave no request listeners/timers, and successful real-WASM events/styles remain unchanged. Browser error/recovery acceptance remains PKG-JASSUB-05.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Actual wrapper timeout and Worker error invoke once with the original error and clean all request listeners/timers first, including throwing user callbacks. Native CSP plain Event identity is preserved; real WASM queries still succeed. Destroy cancellation settles all pending queries after cleanup; complete error recovery remains task05."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Vendor patch provenance is part of the baseline runner.
|
||||
import test from 'node:test'
|
||||
import { hash } from './releases.mjs'
|
||||
|
||||
test('JASSUB local vendor patch retains its exact original, changed source and reversible patch identity', () => {
|
||||
const record = JSON.parse(fs.readFileSync('refactor/baselines/jassub-vendor-patch.json', 'utf8'))
|
||||
const original = execFileSync('git', ['show', `${record.baselineCommit}:${record.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
|
||||
const current = fs.readFileSync(record.file, 'utf8').replaceAll('\r\n', '\n')
|
||||
const patch = fs.readFileSync(record.patchFile, 'utf8').replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(original), record.originalSha256LF)
|
||||
assert.equal(hash(current), record.patchedSha256LF)
|
||||
assert.equal(hash(patch), record.patchSha256LF)
|
||||
assert.notEqual(record.originalSha256LF, record.patchedSha256LF)
|
||||
execFileSync('git', ['apply', '--check', '--reverse', '--ignore-space-change', record.patchFile], { stdio: 'pipe' })
|
||||
const registry = JSON.parse(fs.readFileSync('refactor/third-party.json', 'utf8'))
|
||||
const group = registry.vendored.find(item => item.id === 'jassub-code-and-workers')
|
||||
assert.equal(group.localPatch.originalSha256LF, record.originalSha256LF)
|
||||
assert.equal(group.fingerprints.find(item => item.path === record.file).sha256, record.patchedSha256LF)
|
||||
})
|
||||
+6
-2
@@ -2532,11 +2532,15 @@
|
||||
"PKG-JASSUB-03",
|
||||
"PKG-JASSUB-02"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01)",
|
||||
"acceptance": "保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"baselines/jassub-vendor-validation.json",
|
||||
"baselines/jassub-vendor-patch.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MASK-01",
|
||||
|
||||
@@ -353,9 +353,10 @@
|
||||
],
|
||||
"owners": [
|
||||
"PKG-JASSUB-01",
|
||||
"PKG-JASSUB-06"
|
||||
"PKG-JASSUB-06",
|
||||
"PKG-JASSUB-07"
|
||||
],
|
||||
"sourceStatus": "Wrapper/worker JS match npm 1.8.8 as previously frozen. Both local WASM binaries exactly match Pages nightly 747e392d7f6fe7c38bbcdfe05c9ada46b0b62daf linked to source 6b19a04ddfbad8f9bfd3237395788dd76218841b, with pinned build files and seven submodules. No independent rebuild. See baselines/jassub-provenance.json.",
|
||||
"sourceStatus": "Original wrapper matched npm 1.8.8; PKG-JASSUB-07 now carries an explicitly recorded local lifecycle/clock/query patch (baselines/jassub-vendor-patch.json). Worker JS is unchanged. Both WASM binaries still exactly match Pages nightly 747e392d7f6fe7c38bbcdfe05c9ada46b0b62daf/source 6b19a04ddfbad8f9bfd3237395788dd76218841b. Original wrapper hash, unchanged binaries/fonts, pinned build and submodules remain separately recorded; no independent WASM rebuild.",
|
||||
"licenseStatus": "Matching source LICENSE and nightly COPYRIGHT identified and byte-verified; full component notice assembly, LGPL source/distribution review and full FreeType license remain PKG-JASSUB-06 gates. VENDOR-04 remains open.",
|
||||
"upstreamSources": [
|
||||
"https://github.com/ThaUnknown/jassub/blob/main/LICENSE",
|
||||
@@ -385,7 +386,7 @@
|
||||
{
|
||||
"path": "packages/artplayer-plugin-jassub/src/jassub.es.js",
|
||||
"algorithm": "sha256-lf",
|
||||
"sha256": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038"
|
||||
"sha256": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb"
|
||||
},
|
||||
{
|
||||
"path": "packages/artplayer-plugin-jassub/worker/jassub-worker-modern.wasm",
|
||||
@@ -402,7 +403,13 @@
|
||||
"algorithm": "sha256",
|
||||
"sha256": "73880d99ba9260ac39fc4476491cf254ca784c76f3af2fb03f3f9be8065ad3c0"
|
||||
}
|
||||
]
|
||||
],
|
||||
"localPatch": {
|
||||
"task": "PKG-JASSUB-07",
|
||||
"record": "refactor/baselines/jassub-vendor-patch.json",
|
||||
"originalSha256LF": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038",
|
||||
"patchedSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jassub-font-assets",
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT || 'packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js'
|
||||
const code = fs.readFileSync(artifact, 'utf8')
|
||||
const subtitles = `[Script Info]
|
||||
ScriptType: v4.00+
|
||||
PlayResX: 640
|
||||
PlayResY: 360
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Liberation Sans,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,20,1
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:00.00,0:00:30.00,Default,,0,0,0,,Native lifecycle subtitle
|
||||
`
|
||||
|
||||
async function setup(page) {
|
||||
await page.goto('/test/player.html?core=candidate')
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/assets/sample/steve-jobs.mp4')
|
||||
window.jassubLifecycle = { sent: [], terminated: 0, errors: [] }
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.addEventListener('error', (event) => {
|
||||
window.jassubLifecycle.lastWorkerError = event
|
||||
})
|
||||
}
|
||||
|
||||
postMessage(message, ...rest) {
|
||||
window.jassubLifecycle.sent.push({ target: message.target, time: message.time, rate: message.rate })
|
||||
return super.postMessage(message, ...rest)
|
||||
}
|
||||
|
||||
terminate() {
|
||||
window.jassubLifecycle.terminated++
|
||||
return super.terminate()
|
||||
}
|
||||
}
|
||||
window.jassubOptions = { workerUrl: '/assets/jassub/jassub-worker.js', wasmUrl: '/assets/jassub/jassub-worker.wasm', modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm', availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' }, fonts: ['/assets/jassub/default.woff2'], offscreenRender: false }
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(30)
|
||||
}
|
||||
|
||||
for (const onDemandRender of [true, false]) {
|
||||
test(`JASSUB actual video replacement and repeated destruction, onDemandRender=${onDemandRender}`, async ({ page }, testInfo) => {
|
||||
await setup(page)
|
||||
try {
|
||||
await page.evaluate(async ({ subtitles, onDemandRender }) => {
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
window.art.plugins.add(register({ ...window.jassubOptions, subContent: subtitles, onDemandRender }))
|
||||
const instance = window.jassub = window.art.plugins.artplayerPluginJassub.instance
|
||||
instance.addEventListener('error', event => window.jassubLifecycle.errors.push(String(event.error)))
|
||||
await new Promise(resolve => instance.addEventListener('ready', resolve, { once: true }))
|
||||
const container = document.createElement('div')
|
||||
const video = document.createElement('video')
|
||||
video.width = 640
|
||||
video.height = 360
|
||||
video.muted = true
|
||||
video.preload = 'auto'
|
||||
container.append(video)
|
||||
document.body.append(container)
|
||||
const loaded = new Promise(resolve => video.addEventListener('loadeddata', resolve, { once: true }))
|
||||
video.src = '/assets/sample/steve-jobs.mp4'
|
||||
await loaded
|
||||
await video.play()
|
||||
await new Promise((resolve) => {
|
||||
const playing = () => {
|
||||
if (video.currentTime <= 0.1)
|
||||
return
|
||||
video.removeEventListener('timeupdate', playing)
|
||||
resolve()
|
||||
}
|
||||
video.addEventListener('timeupdate', playing)
|
||||
playing()
|
||||
})
|
||||
video.pause()
|
||||
instance.setVideo(video)
|
||||
window.replacementVideo = video
|
||||
window.replacementContainer = container
|
||||
const seeked = new Promise(resolve => video.addEventListener('seeked', resolve, { once: true }))
|
||||
video.currentTime = 9
|
||||
await seeked
|
||||
video.playbackRate = 1.5
|
||||
await video.play()
|
||||
}, { subtitles, onDemandRender })
|
||||
await expect.poll(() => page.evaluate(() => {
|
||||
const canvas = window.jassub._canvas
|
||||
return canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data.filter((value, index) => index % 4 === 3 && value > 0).length
|
||||
})).toBeGreaterThan(100)
|
||||
const state = await page.evaluate(async () => {
|
||||
const instance = window.jassub
|
||||
const events = await new Promise((resolve, reject) => instance.getEvents((error, rows) => error ? reject(error) : resolve(rows)))
|
||||
const parentMatches = instance._canvasParent.parentNode === window.replacementContainer
|
||||
const originalContainerCount = window.art.template.$player.querySelectorAll('.JASSUB').length
|
||||
const time = window.replacementVideo.currentTime
|
||||
instance.destroy()
|
||||
instance.destroy()
|
||||
window.art.destroy(false)
|
||||
window.replacementVideo.pause()
|
||||
return { parentMatches, originalContainerCount, time, event: events[0], terminated: window.jassubLifecycle.terminated, ownedContainers: document.querySelectorAll('.JASSUB').length, replacementPreserved: window.replacementVideo.isConnected, errors: window.jassubLifecycle.errors, sent: window.jassubLifecycle.sent }
|
||||
})
|
||||
expect(state.parentMatches).toBe(true)
|
||||
expect(state.originalContainerCount).toBe(0)
|
||||
expect(state.time).toBeGreaterThanOrEqual(9)
|
||||
expect(typeof state.event.Style).toBe('number')
|
||||
expect(state.event._index).toBeUndefined()
|
||||
expect(state.terminated).toBe(1)
|
||||
expect(state.ownedContainers).toBe(0)
|
||||
expect(state.replacementPreserved).toBe(true)
|
||||
expect(state.errors).toEqual([])
|
||||
if (!onDemandRender)
|
||||
expect(state.sent.some(message => message.target === 'video' && message.rate === 1.5)).toBe(true)
|
||||
await testInfo.attach('jassub-native-lifecycle', { body: JSON.stringify({ artifact, sha256: hash(code), onDemandRender, state }), contentType: 'application/json' })
|
||||
}
|
||||
finally {
|
||||
await page.evaluate(() => {
|
||||
window.replacementVideo?.pause()
|
||||
if (!window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('JASSUB actual invalid Worker URL construction releases the newly created canvas and media listeners', async ({ page }, testInfo) => {
|
||||
await setup(page)
|
||||
const state = await page.evaluate(() => {
|
||||
const video = window.art.video
|
||||
const listeners = new Map()
|
||||
const add = video.addEventListener.bind(video)
|
||||
const remove = video.removeEventListener.bind(video)
|
||||
video.addEventListener = (name, callback, ...options) => {
|
||||
if (!listeners.has(name))
|
||||
listeners.set(name, new Set())
|
||||
listeners.get(name).add(callback)
|
||||
return add(name, callback, ...options)
|
||||
}
|
||||
video.removeEventListener = (name, callback, ...options) => {
|
||||
listeners.get(name)?.delete(callback)
|
||||
return remove(name, callback, ...options)
|
||||
}
|
||||
let error
|
||||
try {
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
register({ ...window.jassubOptions, workerUrl: 'http://[', onDemandRender: false })(window.art)
|
||||
}
|
||||
catch (failure) { error = { name: failure.name, message: failure.message } }
|
||||
const activeListeners = [...listeners.values()].reduce((sum, values) => sum + values.size, 0)
|
||||
window.art.destroy()
|
||||
return { error, activeListeners, ownedContainers: document.querySelectorAll('.JASSUB').length }
|
||||
})
|
||||
expect(state.error?.name).toBe('SyntaxError')
|
||||
expect(state.activeListeners).toBe(0)
|
||||
expect(state.ownedContainers).toBe(0)
|
||||
await testInfo.attach('jassub-native-construction-failure', { body: JSON.stringify({ artifact, sha256: hash(code), state }), contentType: 'application/json' })
|
||||
})
|
||||
|
||||
test('JASSUB native asynchronous CSP Worker error reaches a pending query and explicit destruction cleans up', async ({ page }, testInfo) => {
|
||||
await setup(page)
|
||||
const state = await page.evaluate(async () => {
|
||||
const meta = document.createElement('meta')
|
||||
meta.httpEquiv = 'Content-Security-Policy'
|
||||
meta.content = 'worker-src \'none\''
|
||||
document.head.append(meta)
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
const { instance } = register({ ...window.jassubOptions, onDemandRender: false })(window.art)
|
||||
let calls = 0
|
||||
const outcome = await new Promise((resolve) => {
|
||||
instance.getEvents((error, rows) => {
|
||||
calls++
|
||||
resolve({ errorIsNativeEvent: error instanceof Event, trusted: error.isTrusted, sameNativeError: error === window.jassubLifecycle.lastWorkerError, type: error.type, rowsAbsent: rows === undefined })
|
||||
})
|
||||
})
|
||||
instance.destroy()
|
||||
instance.destroy()
|
||||
window.art.destroy(false)
|
||||
return { outcome, calls, terminated: window.jassubLifecycle.terminated, ownedContainers: document.querySelectorAll('.JASSUB').length }
|
||||
})
|
||||
expect(state.outcome).toEqual({ errorIsNativeEvent: true, trusted: true, sameNativeError: true, type: 'error', rowsAbsent: true })
|
||||
expect(state.calls).toBe(1)
|
||||
expect(state.terminated).toBe(1)
|
||||
expect(state.ownedContainers).toBe(0)
|
||||
await testInfo.attach('jassub-native-worker-error', { body: JSON.stringify({ artifact, sha256: hash(code), state }), contentType: 'application/json' })
|
||||
})
|
||||
@@ -41,9 +41,25 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
resources.push({ url: response.url(), status: response.status(), contentType: response.headers()['content-type'] })
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.evaluate(() => {
|
||||
window.jassubFramePlatform = { before: typeof HTMLVideoElement.prototype.requestVideoFrameCallback, registered: 0, delivered: 0 }
|
||||
})
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.evaluate(() => {
|
||||
window.jassubNative = { workers: [], snapshots: [], errors: [] }
|
||||
const frames = window.jassubFramePlatform
|
||||
frames.after = typeof HTMLVideoElement.prototype.requestVideoFrameCallback
|
||||
if (frames.after === 'function') {
|
||||
const request = HTMLVideoElement.prototype.requestVideoFrameCallback
|
||||
HTMLVideoElement.prototype.requestVideoFrameCallback = function (callback) {
|
||||
frames.registered++
|
||||
return request.call(this, function (...args) {
|
||||
frames.delivered++
|
||||
frames.lastMetadata = args[1]
|
||||
return callback.apply(this, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
window.jassubNative = { workers: [], snapshots: [], errors: [], frames }
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
@@ -110,7 +126,9 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
signature = (signature + i * pixels[i]) >>> 0
|
||||
}
|
||||
}
|
||||
const state = { phase, visible, signature, width: canvas.width, height: canvas.height, cssWidth: rect.width, cssHeight: rect.height, time: window.art.currentTime, connected: canvas.isConnected }
|
||||
const video = window.art.video
|
||||
const quality = video.getVideoPlaybackQuality?.()
|
||||
const state = { phase, visible, signature, width: canvas.width, height: canvas.height, cssWidth: rect.width, cssHeight: rect.height, time: window.art.currentTime, connected: canvas.isConnected, media: { paused: video.paused, readyState: video.readyState, totalFrames: quality?.totalVideoFrames, droppedFrames: quality?.droppedVideoFrames, decodedFrames: video.webkitDecodedFrameCount, mozPresentedFrames: video.mozPresentedFrames }, onDemand: window.jassub._onDemandRender }
|
||||
window.jassubNative.lastPixel = state
|
||||
if (phase)
|
||||
window.jassubNative.snapshots.push(state)
|
||||
|
||||
@@ -39,6 +39,7 @@ export function jassubEnvironment(implementation, { script = false, simd = false
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
child.parentNode?.removeChild(child)
|
||||
this.children.push(child)
|
||||
child.parentNode = this
|
||||
return child
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Frame clock regressions use the repository runner.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { jassubCandidate } from './helpers/jassub.js'
|
||||
|
||||
const candidate = await jassubCandidate()
|
||||
const interval = 1000 / 30
|
||||
|
||||
function environment({ native = false, quality = true } = {}) {
|
||||
let now = 0
|
||||
let nextFrame = 0
|
||||
const frames = new Map()
|
||||
const cancelled = []
|
||||
class Video {
|
||||
constructor() {
|
||||
this.currentTime = 0
|
||||
this.videoWidth = 640
|
||||
this.videoHeight = 360
|
||||
this.readyState = 4
|
||||
this.paused = false
|
||||
this.seeking = false
|
||||
this.quality = { totalVideoFrames: 0, droppedVideoFrames: 0, totalFrameDelay: 0 }
|
||||
}
|
||||
}
|
||||
if (quality) {
|
||||
Video.prototype.getVideoPlaybackQuality = function () {
|
||||
return { ...this.quality }
|
||||
}
|
||||
}
|
||||
const nativeRequest = () => 123
|
||||
const nativeCancel = () => undefined
|
||||
if (native) {
|
||||
Video.prototype.requestVideoFrameCallback = nativeRequest
|
||||
Video.prototype.cancelVideoFrameCallback = nativeCancel
|
||||
}
|
||||
const originalDescriptors = Object.getOwnPropertyDescriptors(Video.prototype)
|
||||
const module = { exports: {} }
|
||||
const context = {
|
||||
HTMLVideoElement: Video,
|
||||
EventTarget,
|
||||
Map,
|
||||
module,
|
||||
exports: module.exports,
|
||||
performance: { now: () => now },
|
||||
requestAnimationFrame(callback) {
|
||||
const id = nextFrame++
|
||||
frames.set(id, callback)
|
||||
return id
|
||||
},
|
||||
cancelAnimationFrame(id) {
|
||||
cancelled.push(id)
|
||||
frames.delete(id)
|
||||
},
|
||||
}
|
||||
context.window = context
|
||||
context.self = context
|
||||
// Only platform primitives are controlled. Execute the actual package bundle;
|
||||
// no vendor method is substituted or copied into this test.
|
||||
vm.runInNewContext(candidate.code, context, { timeout: 5000 })
|
||||
assert.equal(typeof (module.exports.default || module.exports), 'function')
|
||||
return {
|
||||
Video,
|
||||
frames,
|
||||
cancelled,
|
||||
nativeRequest,
|
||||
nativeCancel,
|
||||
originalDescriptors,
|
||||
frame(time) {
|
||||
assert(time >= now, 'The controlled wall clock is monotonic')
|
||||
now = time
|
||||
for (const [id, callback] of [...frames]) {
|
||||
if (!frames.delete(id))
|
||||
continue
|
||||
callback(time)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function collect(video) {
|
||||
const calls = []
|
||||
const id = video.requestVideoFrameCallback((now, metadata) => calls.push({ now, metadata }))
|
||||
assert.equal(typeof id, 'number')
|
||||
return { id, calls }
|
||||
}
|
||||
|
||||
function hasPending(video, id) {
|
||||
const pending = video._rvfcpolyfillmap
|
||||
assert(pending, 'The actual polyfill exposes its pending request map')
|
||||
return pending instanceof Map ? pending.has(id) : Object.hasOwn(pending, id)
|
||||
}
|
||||
|
||||
test('JASSUB frame polyfill leaves native RVFC and cancellation untouched', () => {
|
||||
const env = environment({ native: true })
|
||||
assert.deepEqual(Object.getOwnPropertyDescriptors(env.Video.prototype), env.originalDescriptors)
|
||||
const video = new env.Video()
|
||||
assert.equal(video.requestVideoFrameCallback, env.nativeRequest)
|
||||
assert.equal(video.cancelVideoFrameCallback, env.nativeCancel)
|
||||
assert(!Object.hasOwn(video, '_rvfcpolyfillmap'))
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('JASSUB frame polyfill preserves the unsupported quality-API boundary', () => {
|
||||
const env = environment({ quality: false })
|
||||
assert.equal(env.Video.prototype.requestVideoFrameCallback, undefined)
|
||||
assert.equal(env.Video.prototype.cancelVideoFrameCallback, undefined)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('JASSUB healthy frame counts retain metadata and bypass fallback throttling', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
video.quality = { totalVideoFrames: 20, droppedVideoFrames: 5, totalFrameDelay: 0.125 }
|
||||
const { calls } = collect(video)
|
||||
video.currentTime = 4
|
||||
video.quality = { totalVideoFrames: 21, droppedVideoFrames: 5, totalFrameDelay: 0.25 }
|
||||
// The new fallback conditions must not alter the original increasing-count path.
|
||||
video.readyState = 1
|
||||
video.seeking = true
|
||||
env.frame(16)
|
||||
assert.equal(calls.length, 1)
|
||||
const { now, metadata } = calls[0]
|
||||
assert.equal(now, 16)
|
||||
assert.equal(metadata.presentedFrames, 16)
|
||||
assert.equal(metadata.processingDuration, 0.125)
|
||||
assert.equal(metadata.presentationTime, 141)
|
||||
assert.equal(metadata.expectedDisplayTime, 32)
|
||||
assert.equal(metadata.mediaTime, 4.016)
|
||||
assert.equal(metadata.width, 640)
|
||||
assert.equal(metadata.height, 360)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
for (const counter of ['mozPresentedFrames', 'mozPaintedFrames']) {
|
||||
test(`JASSUB preserves the ${counter} increasing-count path`, () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
video[counter] = 4
|
||||
const { calls } = collect(video)
|
||||
video[counter] = 5
|
||||
env.frame(1)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].metadata.presentedFrames, 5)
|
||||
})
|
||||
}
|
||||
|
||||
test('JASSUB zero-count fallback waits one 30Hz interval and reports actual media time with zero frames', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
const { calls } = collect(video)
|
||||
video.currentTime = 0.75
|
||||
env.frame(interval - 0.001)
|
||||
assert.equal(calls.length, 0)
|
||||
env.frame(interval)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].now, interval)
|
||||
assert.equal(calls[0].metadata.mediaTime, 0.75)
|
||||
assert.equal(calls[0].metadata.presentedFrames, 0)
|
||||
assert.equal(calls[0].metadata.width, 640)
|
||||
assert.equal(calls[0].metadata.height, 360)
|
||||
env.frame(interval * 3)
|
||||
assert.equal(calls.length, 1, 'A fulfilled request is never delivered twice')
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('JASSUB zero-count fallback remains bounded on a 144Hz wall clock', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
const deliveries = []
|
||||
let pending
|
||||
const request = () => {
|
||||
pending = video.requestVideoFrameCallback((now, metadata) => {
|
||||
deliveries.push({ now, mediaTime: metadata.mediaTime })
|
||||
request()
|
||||
})
|
||||
}
|
||||
request()
|
||||
for (let frame = 1; frame <= 144; frame++) {
|
||||
const time = frame * 1000 / 144
|
||||
video.currentTime = time / 1000
|
||||
env.frame(time)
|
||||
}
|
||||
assert(deliveries.length > 20 && deliveries.length <= 30, `Expected bounded useful fallback, received ${deliveries.length} callbacks`)
|
||||
for (let index = 1; index < deliveries.length; index++)
|
||||
assert(deliveries[index].now - deliveries[index - 1].now >= interval - 1e-9)
|
||||
video.cancelVideoFrameCallback(pending)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('JASSUB resumed real frame counts bypass the preceding fallback interval', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
const fallback = collect(video)
|
||||
video.currentTime = 0.5
|
||||
env.frame(interval)
|
||||
assert.equal(fallback.calls.length, 1)
|
||||
assert.equal(fallback.calls[0].metadata.presentedFrames, 0)
|
||||
const healthy = collect(video)
|
||||
video.quality.totalVideoFrames = 1
|
||||
video.currentTime = 1
|
||||
env.frame(interval + 1)
|
||||
assert.equal(healthy.calls.length, 1, 'Actual frame recovery must not wait for the fallback cap')
|
||||
assert.equal(healthy.calls[0].metadata.presentedFrames, 1)
|
||||
assert.equal(healthy.calls[0].metadata.mediaTime, 1.001)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
for (const paused of [false, true]) {
|
||||
test(`JASSUB unchanged media time does not complete zero-count requests, paused=${paused}`, () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
video.paused = paused
|
||||
video.currentTime = 5
|
||||
const { id, calls } = collect(video)
|
||||
env.frame(40)
|
||||
env.frame(1000)
|
||||
assert.equal(calls.length, 0)
|
||||
video.cancelVideoFrameCallback(id)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
for (const destination of [20, 3]) {
|
||||
test(`JASSUB paused seek to ${destination} waits for seeking completion then delivers the changed time`, () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
video.paused = true
|
||||
video.currentTime = 10
|
||||
const { calls } = collect(video)
|
||||
video.seeking = true
|
||||
video.currentTime = destination
|
||||
env.frame(40)
|
||||
assert.equal(calls.length, 0)
|
||||
video.seeking = false
|
||||
env.frame(60)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].metadata.mediaTime, destination)
|
||||
assert.equal(calls[0].metadata.presentedFrames, 0)
|
||||
})
|
||||
}
|
||||
|
||||
for (const readyState of [0, 1]) {
|
||||
test(`JASSUB zero-count fallback waits for current frame data from readyState=${readyState}`, () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
video.readyState = readyState
|
||||
const { calls } = collect(video)
|
||||
video.currentTime = 1
|
||||
env.frame(40)
|
||||
assert.equal(calls.length, 0)
|
||||
video.readyState = 2
|
||||
env.frame(60)
|
||||
assert.equal(calls.length, 1)
|
||||
})
|
||||
}
|
||||
|
||||
for (const [label, quality] of [
|
||||
['NaN total', { totalVideoFrames: Number.NaN, droppedVideoFrames: 0 }],
|
||||
['missing total', { droppedVideoFrames: 0 }],
|
||||
['NaN dropped', { totalVideoFrames: 0, droppedVideoFrames: Number.NaN }],
|
||||
['missing dropped', { totalVideoFrames: 0 }],
|
||||
['all frames dropped', { totalVideoFrames: 10, droppedVideoFrames: 10 }],
|
||||
]) {
|
||||
test(`JASSUB does not infer zero-count fallback from ${label}`, () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
video.quality = quality
|
||||
const { id, calls } = collect(video)
|
||||
video.currentTime = 1
|
||||
env.frame(40)
|
||||
video.currentTime = 2
|
||||
env.frame(100)
|
||||
assert.equal(calls.length, 0)
|
||||
video.cancelVideoFrameCallback(id)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('JASSUB same-video simultaneous requests have independent handles and cancellation', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
const first = collect(video)
|
||||
const second = collect(video)
|
||||
assert.notEqual(first.id, second.id)
|
||||
assert(second.id > first.id, 'Callback identifiers are monotonic even at the same performance.now')
|
||||
video.cancelVideoFrameCallback(first.id)
|
||||
video.quality.totalVideoFrames = 1
|
||||
env.frame(16)
|
||||
assert.equal(first.calls.length, 0)
|
||||
assert.equal(second.calls.length, 1)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('JASSUB simultaneous requests on different videos cannot cancel each other', () => {
|
||||
const env = environment()
|
||||
const firstVideo = new env.Video()
|
||||
const secondVideo = new env.Video()
|
||||
const first = collect(firstVideo)
|
||||
const second = collect(secondVideo)
|
||||
assert(Object.hasOwn(firstVideo, '_rvfcpolyfillmap'))
|
||||
assert(Object.hasOwn(secondVideo, '_rvfcpolyfillmap'))
|
||||
assert.notEqual(firstVideo._rvfcpolyfillmap, secondVideo._rvfcpolyfillmap)
|
||||
firstVideo.cancelVideoFrameCallback(first.id)
|
||||
firstVideo.cancelVideoFrameCallback(first.id)
|
||||
firstVideo.quality.totalVideoFrames = 1
|
||||
secondVideo.quality.totalVideoFrames = 1
|
||||
env.frame(16)
|
||||
assert.equal(first.calls.length, 0)
|
||||
assert.equal(second.calls.length, 1)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('JASSUB cancellation releases RAF id zero', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
const { id, calls } = collect(video)
|
||||
assert(env.frames.has(0))
|
||||
video.cancelVideoFrameCallback(id)
|
||||
assert.deepEqual(env.cancelled, [0])
|
||||
video.quality.totalVideoFrames = 1
|
||||
env.frame(40)
|
||||
assert.equal(calls.length, 0)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('JASSUB removes a pending callback before invoking it even when it throws', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
const expected = new Error('frame consumer failure')
|
||||
const id = video.requestVideoFrameCallback(() => {
|
||||
assert.equal(hasPending(video, id), false)
|
||||
throw expected
|
||||
})
|
||||
video.quality.totalVideoFrames = 1
|
||||
assert.throws(() => env.frame(16), error => error === expected)
|
||||
assert.equal(hasPending(video, id), false)
|
||||
assert.equal(env.frames.size, 0)
|
||||
const next = collect(video)
|
||||
video.quality.totalVideoFrames = 2
|
||||
env.frame(17)
|
||||
assert.equal(next.calls.length, 1)
|
||||
})
|
||||
|
||||
test('JASSUB callback reentry retains a newly registered request independently', () => {
|
||||
const env = environment()
|
||||
const video = new env.Video()
|
||||
let next
|
||||
const calls = []
|
||||
const first = video.requestVideoFrameCallback(() => {
|
||||
assert.equal(hasPending(video, first), false)
|
||||
calls.push('first')
|
||||
next = video.requestVideoFrameCallback(() => calls.push('next'))
|
||||
video.cancelVideoFrameCallback(first)
|
||||
})
|
||||
video.quality.totalVideoFrames = 1
|
||||
env.frame(16)
|
||||
assert.deepEqual(calls, ['first'])
|
||||
assert(hasPending(video, next))
|
||||
video.quality.totalVideoFrames = 2
|
||||
env.frame(17)
|
||||
assert.deepEqual(calls, ['first', 'next'])
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
@@ -0,0 +1,302 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Vendor lifecycle regressions use the repository runner.
|
||||
import test from 'node:test'
|
||||
import { jassubCandidate, jassubEnvironment } from './helpers/jassub.js'
|
||||
|
||||
const candidate = await jassubCandidate()
|
||||
|
||||
function environment() {
|
||||
// Expose platform controls only; every tested method is the actual bundled vendor.
|
||||
const code = `${candidate.code}\nObject.defineProperty(module.exports.default || module.exports, '__lifecyclePlatform', { value: { Error, ErrorEvent, Video: HTMLVideoElement, setGlobal: (name, value) => { globalThis[name] = value } } });`
|
||||
const env = jassubEnvironment({ ...candidate, code })
|
||||
env.platform = env.factory.__lifecyclePlatform
|
||||
return env
|
||||
}
|
||||
|
||||
function trackListeners(target) {
|
||||
const listeners = new Map()
|
||||
const add = target.addEventListener
|
||||
const remove = target.removeEventListener
|
||||
target.addEventListener = function (type, callback, ...options) {
|
||||
if (!listeners.has(type))
|
||||
listeners.set(type, new Set())
|
||||
listeners.get(type).add(callback)
|
||||
return add?.call(this, type, callback, ...options)
|
||||
}
|
||||
target.removeEventListener = function (type, callback, ...options) {
|
||||
listeners.get(type)?.delete(callback)
|
||||
return remove?.call(this, type, callback, ...options)
|
||||
}
|
||||
return {
|
||||
size: () => [...listeners.values()].reduce((sum, entries) => sum + entries.size, 0),
|
||||
callbacks: type => [...listeners.get(type) || []],
|
||||
emit(type, event) {
|
||||
for (const callback of [...listeners.get(type) || []]) callback(event)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function frames(env) {
|
||||
const pending = new Map()
|
||||
let next = 0
|
||||
env.platform.Video.prototype.requestVideoFrameCallback = function (callback) {
|
||||
pending.set(++next, { video: this, callback })
|
||||
return next
|
||||
}
|
||||
env.platform.Video.prototype.cancelVideoFrameCallback = id => pending.delete(id)
|
||||
return {
|
||||
count: video => [...pending.values()].filter(row => row.video === video).length,
|
||||
take(video) {
|
||||
const entry = [...pending].find(([, row]) => row.video === video)
|
||||
assert(entry, 'take an actually queued native frame callback')
|
||||
pending.delete(entry[0])
|
||||
return entry[1].callback
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function queryEnvironment() {
|
||||
const env = environment()
|
||||
const timers = new Map()
|
||||
const errors = []
|
||||
let next = 0
|
||||
env.platform.setGlobal('setTimeout', (callback) => {
|
||||
timers.set(++next, callback)
|
||||
return next
|
||||
})
|
||||
env.platform.setGlobal('clearTimeout', id => timers.delete(id))
|
||||
// Capture the actual Error allocated by the timeout, without intercepting vendor methods.
|
||||
env.platform.setGlobal('Error', class extends env.platform.Error {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
errors.push(this)
|
||||
}
|
||||
})
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const worker = env.workers[0]
|
||||
const listeners = trackListeners(worker)
|
||||
return {
|
||||
...env,
|
||||
instance,
|
||||
worker,
|
||||
timers,
|
||||
errors,
|
||||
requests: listeners,
|
||||
timeout() {
|
||||
const entry = timers.entries().next().value
|
||||
assert(entry, 'fire an actually registered one-shot timeout')
|
||||
timers.delete(entry[0])
|
||||
entry[1]()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test('JASSUB repeated direct destroy returns synchronously and terminates once', async () => {
|
||||
const env = environment()
|
||||
const listeners = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
assert(listeners.size() > 0)
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
env.emit('destroy')
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(listeners.size(), 0)
|
||||
assert.deepEqual(env.parent.children, [env.art.video])
|
||||
})
|
||||
|
||||
test('JASSUB destroy after ready cancels queued resize and track messages and settles sendMessage', async () => {
|
||||
const env = environment()
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const worker = env.workers[0]
|
||||
const sent = worker.messages.length
|
||||
assert.equal(instance.resize(640, 360, 4, 5, false), undefined)
|
||||
assert.equal(instance.setTrack('[Script Info]'), undefined)
|
||||
const pending = instance.sendMessage('queued-before-destroy')
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
assert.equal(await pending, undefined)
|
||||
await env.flush()
|
||||
assert.equal(worker.messages.length, sent)
|
||||
assert.equal(worker.terminated, 1)
|
||||
})
|
||||
|
||||
test('JASSUB setVideo across parents destroys the actual owned container', async () => {
|
||||
const env = environment()
|
||||
const originalListeners = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const parent = new env.parent.constructor()
|
||||
const video = parent.appendChild(new env.platform.Video())
|
||||
const listeners = trackListeners(video)
|
||||
instance.setVideo(video)
|
||||
assert.equal(originalListeners.size(), 0)
|
||||
assert(listeners.size() > 0)
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
assert.equal(listeners.size(), 0)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.deepEqual(env.parent.children, [env.art.video])
|
||||
assert.deepEqual(parent.children, [video])
|
||||
})
|
||||
|
||||
test('JASSUB Worker constructor SecurityError rolls back DOM and video listeners without replacing the error', async () => {
|
||||
const env = environment()
|
||||
const listeners = trackListeners(env.art.video)
|
||||
const failure = new DOMException('Controlled Worker construction failure', 'SecurityError')
|
||||
env.platform.setGlobal('Worker', class {
|
||||
constructor() { throw failure }
|
||||
})
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
await env.flush()
|
||||
assert.equal(listeners.size(), 0)
|
||||
assert.deepEqual(env.parent.children, [env.art.video])
|
||||
assert.equal(env.workers.length, 0)
|
||||
assert.equal(env.listeners.get('destroy')?.length || 0, 0)
|
||||
})
|
||||
|
||||
test('JASSUB destroy before ready prevents queued initialization and application messages from posting', async () => {
|
||||
const env = environment()
|
||||
const { instance } = env.factory()(env.art)
|
||||
const worker = env.workers[0]
|
||||
const afterTermination = []
|
||||
const post = worker.postMessage
|
||||
worker.postMessage = function (message) {
|
||||
if (this.terminated)
|
||||
afterTermination.push(message.target)
|
||||
return post.call(this, message)
|
||||
}
|
||||
instance.sendMessage('queued-before-destroy')
|
||||
instance.destroy()
|
||||
await env.flush()
|
||||
// A ready event already queued by a real Worker may still be delivered after termination.
|
||||
worker.onmessage?.({ data: { target: 'ready' } })
|
||||
await env.flush()
|
||||
assert.deepEqual(afterTermination, [])
|
||||
assert.equal(worker.messages.some(message => ['init', 'queued-before-destroy'].includes(message.target)), false)
|
||||
assert.equal(worker.terminated, 1)
|
||||
})
|
||||
|
||||
test('JASSUB stale queued video frame cannot drive or duplicate the replacement video loop', async () => {
|
||||
const env = environment()
|
||||
const clock = frames(env)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const oldCallback = clock.take(env.art.video)
|
||||
const video = env.createVideo()
|
||||
video.currentTime = 9
|
||||
instance.setVideo(video)
|
||||
assert.equal(clock.count(video), 1)
|
||||
oldCallback(0, { mediaTime: 123, width: 640, height: 360 })
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.some(message => message.target === 'demand' && message.time === 123), false)
|
||||
assert.equal(clock.count(video), 1)
|
||||
clock.take(video)(0, { mediaTime: 9, width: 640, height: 360 })
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.filter(message => message.target === 'demand').at(-1).time, 9)
|
||||
assert.equal(clock.count(video), 1)
|
||||
instance.destroy()
|
||||
assert.equal(clock.count(video), 0)
|
||||
})
|
||||
|
||||
test('JASSUB fallback ratechange sends the current numeric playbackRate', async () => {
|
||||
const env = environment()
|
||||
const { instance } = env.factory({ onDemandRender: false })(env.art)
|
||||
await env.ready()
|
||||
env.art.video.playbackRate = 1.75
|
||||
env.art.video.dispatchEvent(new Event('ratechange'))
|
||||
await env.flush()
|
||||
const message = env.workers[0].messages.at(-1)
|
||||
assert.equal(message.target, 'video')
|
||||
assert.equal(message.rate, 1.75)
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
for (const method of ['getEvents', 'getStyles']) {
|
||||
for (const mode of ['success', 'timeout', 'worker-error']) {
|
||||
for (const throws of [false, true]) {
|
||||
test(`JASSUB ${method} ${mode} cleans before its single callback${throws ? ' even when that callback throws' : ''}`, async () => {
|
||||
const env = await queryEnvironment()
|
||||
const calls = []
|
||||
const cleanupAtCallback = []
|
||||
const callbackFailure = new Error('Controlled user callback failure')
|
||||
assert.equal(env.instance[method]((...args) => {
|
||||
calls.push(args)
|
||||
cleanupAtCallback.push([env.requests.size(), env.timers.size])
|
||||
if (throws)
|
||||
throw callbackFailure
|
||||
}), undefined)
|
||||
assert.equal(env.worker.messages.at(-1).target, method)
|
||||
const staleMessage = env.requests.callbacks('message')[0]
|
||||
const staleError = env.requests.callbacks('error')[0]
|
||||
const workerError = new env.platform.ErrorEvent('error', { error: new Error('Controlled Worker error') })
|
||||
const result = [{ sample: 1 }]
|
||||
const message = { data: { target: method, [method === 'getEvents' ? 'events' : 'styles']: result } }
|
||||
const complete = () => {
|
||||
if (mode === 'success')
|
||||
env.requests.emit('message', message)
|
||||
else if (mode === 'timeout')
|
||||
env.timeout()
|
||||
else
|
||||
env.requests.emit('error', workerError)
|
||||
}
|
||||
if (throws)
|
||||
assert.throws(complete, error => error === callbackFailure)
|
||||
else
|
||||
assert.doesNotThrow(complete)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.deepEqual(cleanupAtCallback, [[0, 0]])
|
||||
assert.equal(calls[0][0], mode === 'success' ? null : mode === 'timeout' ? env.errors[0] : workerError)
|
||||
assert.equal(calls[0][1], mode === 'success' ? result : undefined)
|
||||
staleMessage(message)
|
||||
staleError(workerError)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(env.requests.size(), 0)
|
||||
assert.equal(env.timers.size, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const throws of [false, true]) {
|
||||
test(`JASSUB destroy settles all pending queries after releasing resources${throws ? ' despite a throwing callback' : ''}`, async () => {
|
||||
const env = await queryEnvironment()
|
||||
const calls = []
|
||||
const callbackFailure = new Error('Controlled destroy callback failure')
|
||||
for (const method of ['getEvents', 'getStyles']) {
|
||||
env.instance[method]((error, result) => {
|
||||
calls.push({ method, error, result, listeners: env.requests.size(), timers: env.timers.size })
|
||||
if (throws && method === 'getEvents')
|
||||
throw callbackFailure
|
||||
})
|
||||
}
|
||||
const lateMessages = env.requests.callbacks('message')
|
||||
const lateErrors = env.requests.callbacks('error')
|
||||
assert(env.requests.size() > 0)
|
||||
assert.equal(env.timers.size, 2)
|
||||
if (throws)
|
||||
assert.throws(() => env.instance.destroy(), error => error === callbackFailure)
|
||||
else
|
||||
assert.equal(env.instance.destroy(), undefined)
|
||||
assert.equal(calls.length, 2)
|
||||
assert.deepEqual(calls.map(call => call.method), ['getEvents', 'getStyles'])
|
||||
for (const call of calls) {
|
||||
assert(call.error instanceof env.platform.Error)
|
||||
assert.match(call.error.message, /destroy/i)
|
||||
assert.equal(call.result, undefined)
|
||||
assert.equal(call.listeners, 0)
|
||||
assert.equal(call.timers, 0)
|
||||
}
|
||||
for (const callback of lateMessages) callback({ data: { target: 'getEvents', events: [] } })
|
||||
for (const callback of lateErrors) callback(new Error('Late Worker failure'))
|
||||
assert.equal(calls.length, 2)
|
||||
assert.equal(env.worker.terminated, 1)
|
||||
assert.equal(env.requests.size(), 0)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.deepEqual(env.parent.children, [env.art.video])
|
||||
assert.equal(env.instance.destroy(), undefined)
|
||||
assert.equal(env.worker.terminated, 1)
|
||||
})
|
||||
}
|
||||
+2
-2
@@ -59,12 +59,12 @@ for (const implementation of implementations) {
|
||||
assert.equal(result.instance.resize(640, 360, 4, 5, false), undefined)
|
||||
assert.equal(result.instance.setVideo(option.video), undefined)
|
||||
assert.equal(result.instance.setTrack('[Script Info]'), undefined)
|
||||
assert.equal(result.instance.destroy(), undefined)
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
const resize = env.workers[0].messages.find(message => message.target === 'canvas')
|
||||
assert.equal(resize.width, 640)
|
||||
assert.equal(resize.height, 360)
|
||||
assert.equal(result.instance.destroy(), undefined)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
})
|
||||
test(`JASSUB ${implementation.name}: omitted options retain worker and font defaults`, async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
|
||||
@@ -41,7 +41,7 @@ instance.addFont(new Uint8Array([0]))
|
||||
instance.setDefaultFont('fallback')
|
||||
instance.runBenchmark()
|
||||
instance.getEvents((failure, events) => {
|
||||
const caught: Error | ErrorEvent | null = failure
|
||||
const caught: Error | Event | null = failure
|
||||
const values: AssEvent[] | undefined = events
|
||||
if (events) {
|
||||
for (const event of events) {
|
||||
|
||||
Reference in new issue
Block a user