fix(jassub): [PKG-JASSUB-08] transfer offscreen canvas before queued rendering

This commit is contained in:
Harvey Zhao committed 2026-09-14 02:29:12 +08:00
1 parent 0f3c068aae
commit b4b0276fe9
22 files changed
+9667 -29

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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.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 !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.subLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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.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 !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
+5 -1
View File
@@ -147,7 +147,7 @@ const _o = class _o extends EventTarget {
// @ts-ignore // @ts-ignore
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? true), useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? true),
hasBitmapBug: _o._hasBitmapBug hasBitmapBug: _o._hasBitmapBug
}), this._offscreenRender === true && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]); });
}).catch((error) => { }).catch((error) => {
if (!this._destroyed) this.destroy(error); if (!this._destroyed) this.destroy(error);
}); });
@@ -581,6 +581,10 @@ const _o = class _o extends EventTarget {
} }
_ready() { _ready() {
if (this._destroyed) return; if (this._destroyed) return;
if (this._offscreenRender === true && !this._initialOffscreenSent) {
this._initialOffscreenSent = true;
this._worker.postMessage({ target: "offscreenCanvas", transferable: [this._canvasctrl] }, [this._canvasctrl]);
}
this._init(), this.dispatchEvent(new CustomEvent("ready")); this._init(), this.dispatchEvent(new CustomEvent("ready"));
} }
/** /**
+2 -2
View File
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "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", "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", "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/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: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/jassub-offscreen.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:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline", "test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test", "test:browser": "playwright test",
@@ -90,7 +90,7 @@
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs", "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": "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: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 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": "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 test/jassub-offscreen.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": "node --test refactor/scripts/jassub-types.test.mjs",
"test:jassub-types-package": "node refactor/scripts/jassub-package-types.mjs", "test:jassub-types-package": "node refactor/scripts/jassub-package-types.mjs",
"check:ci": "node refactor/scripts/ci-workflow.mjs", "check:ci": "node refactor/scripts/ci-workflow.mjs",
@@ -72,7 +72,15 @@ The original wrapper matched upstream jassub 1.8.8 apart from formatting and the
PKG-JASSUB-07 records its local lifecycle/clock patch separately in PKG-JASSUB-07 records its local lifecycle/clock patch separately in
`../../refactor/baselines/jassub-vendor-patch.json` and the accompanying patch file. `../../refactor/baselines/jassub-vendor-patch.json` and the accompanying patch file.
Keep original and patched fingerprints distinct; do not call the changed wrapper an Keep original and patched fingerprints distinct; do not call the changed wrapper an
unmodified upstream file. The unmodified upstream file. PKG-JASSUB-08 adds a separate follow-up patch in
`../../refactor/baselines/jassub-offscreen-patch.json`: initial offscreen ownership
is transferred synchronously when the Worker reports ready, before resolving the
loaded gate and dispatching the public ready event. Otherwise a queued resize can
ask the Worker to return main-thread images while the instance has no main-thread
context. Duplicate ready delivery must not transfer the same canvas twice. The
explicit main-thread/custom-canvas and unsupported-capability paths keep their
existing selection. Later hybrid detach/reattach remains separate and must retain
its own rendering/lifetime checks. The
worker JS and default font match that archive byte-for-byte. Local WASM instead matches worker JS and default font match that archive byte-for-byte. Local WASM instead matches
the exact Pages nightly blobs associated with source 6b19a04ddfbad8f9bfd3237395788dd76218841b. the exact Pages nightly blobs associated with source 6b19a04ddfbad8f9bfd3237395788dd76218841b.
Its build workflow and seven submodule revisions are pinned in the separate Its build workflow and seven submodule revisions are pinned in the separate
@@ -123,6 +131,11 @@ The candidate's default onDemandRender path is tested through playback/seek/layo
Chromium/Firefox/WebKit. jassub-lifecycle.spec.js additionally checks actual replacement Chromium/Firefox/WebKit. jassub-lifecycle.spec.js additionally checks actual replacement
video pixels, numerical fallback playbackRate, repeated teardown, invalid-URL constructor video pixels, numerical fallback playbackRate, repeated teardown, invalid-URL constructor
rollback and a genuine asynchronous CSP Worker error reaching a pending query. These 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, are distinct from full devices and sustained memory/GPU release, which remain
which remain unverified. See unverified. Set ARTPLAYER_JASSUB_OFFSCREEN=default to omit the option and exercise
the browser's actual default selection. The test copies the displayed canvas bitmap
to a separate readback canvas, so it supports both transferred and main-thread
canvases. PKG-JASSUB-08 records default-mode Chromium/Firefox rendering and WebKit's
capability fallback separately; this does not prove physical Safari/mobile behavior.
See
`../../refactor/baselines/jassub-contract.md` for precise contract and provenance evidence. `../../refactor/baselines/jassub-contract.md` for precise contract and provenance evidence.
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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.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 !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.subLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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.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 !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
@@ -147,7 +147,7 @@ const _o = class _o extends EventTarget {
// @ts-ignore // @ts-ignore
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? true), useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? true),
hasBitmapBug: _o._hasBitmapBug hasBitmapBug: _o._hasBitmapBug
}), this._offscreenRender === true && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]); });
}).catch((error) => { }).catch((error) => {
if (!this._destroyed) this.destroy(error); if (!this._destroyed) this.destroy(error);
}); });
@@ -581,6 +581,10 @@ const _o = class _o extends EventTarget {
} }
_ready() { _ready() {
if (this._destroyed) return; if (this._destroyed) return;
if (this._offscreenRender === true && !this._initialOffscreenSent) {
this._initialOffscreenSent = true;
this._worker.postMessage({ target: "offscreenCanvas", transferable: [this._canvasctrl] }, [this._canvasctrl]);
}
this._init(), this.dispatchEvent(new CustomEvent("ready")); this._init(), this.dispatchEvent(new CustomEvent("ready"));
} }
/** /**
@@ -143,7 +143,7 @@ class o extends EventTarget {
// @ts-ignore // @ts-ignore
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? !0), useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? !0),
hasBitmapBug: o._hasBitmapBug hasBitmapBug: o._hasBitmapBug
}), this._offscreenRender === !0 && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]); });
}).catch((error) => { }).catch((error) => {
if (!this._destroyed) this.destroy(error); if (!this._destroyed) this.destroy(error);
}); });
@@ -580,6 +580,10 @@ class o extends EventTarget {
} }
_ready() { _ready() {
if (this._destroyed) return; if (this._destroyed) return;
if (this._offscreenRender === !0 && !this._initialOffscreenSent) {
this._initialOffscreenSent = true;
this._worker.postMessage({ target: "offscreenCanvas", transferable: [this._canvasctrl] }, [this._canvasctrl]);
}
this._init(), this.dispatchEvent(new CustomEvent("ready")); this._init(), this.dispatchEvent(new CustomEvent("ready"));
} }
/** /**
@@ -0,0 +1,11 @@
{
"task": "PKG-JASSUB-08",
"baselineCommit": "0f3c068aae1415c2d40ede8708f02f4c5864fe7a",
"file": "packages/artplayer-plugin-jassub/src/jassub.es.js",
"originalSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb",
"patchedSha256LF": "6a96af7914f702eb7d6303b466cbe16abddd278040f60cd29f153cdf60e92cb2",
"patchFile": "refactor/baselines/jassub-offscreen.patch",
"patchSha256LF": "b6cb5c4cb3c9fab920724fa7ddfe7a6b58db68d177f720231f5823fc5bfd6dac",
"predecessor": "refactor/baselines/jassub-vendor-patch.json",
"purpose": "Transfer the initial offscreen canvas on Worker readiness before releasing queued resize/render messages; keep later reattachment behavior unchanged."
}
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
diff --git a/packages/artplayer-plugin-jassub/src/jassub.es.js b/packages/artplayer-plugin-jassub/src/jassub.es.js
index 904b40888..6bd2bc2c6 100644
--- a/packages/artplayer-plugin-jassub/src/jassub.es.js
+++ b/packages/artplayer-plugin-jassub/src/jassub.es.js
@@ -143,7 +143,7 @@ class o extends EventTarget {
// @ts-ignore
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);
});
@@ -580,6 +580,10 @@ class o extends EventTarget {
}
_ready() {
if (this._destroyed) return;
+ if (this._offscreenRender === !0 && !this._initialOffscreenSent) {
+ this._initialOffscreenSent = true;
+ this._worker.postMessage({ target: "offscreenCanvas", transferable: [this._canvasctrl] }, [this._canvasctrl]);
+ }
this._init(), this.dispatchEvent(new CustomEvent("ready"));
}
/**
@@ -0,0 +1,78 @@
# PKG-JASSUB-08 默认 offscreen 初始化修复
## 问题与实现
从 `0f3c068aae1415c2d40ede8708f02f4c5864fe7a` 开始补全 05 默认模式验收时,
Chromium/Firefox 三个核心组合都出现真实页面异常:
`this._ctx.clearRect is not a function`。WebKit 本机没有转移能力,默认回退路径
三项通过。这是 3 通过/6 失败的实际结果,不是浏览器连接或媒体网络问题。
Worker 消息证明初始化顺序为 init → canvas → offscreenCanvas。构造时 setVideo
已排队 resize,等待 `_loaded`;初始画布转移后来才排队。ready 解锁时,Worker
先执行 resize,在尚未接收画布时向主线程发回 render。主线程已选 offscreen,
`_ctx=false`,因此空图像消息也会触发 clearRect 异常。字幕随后能够出现不能
抵消这个未处理错误。
本修复只调整初始归属交接:Worker ready 到达后先同步 postMessage 转移画布,
再 resolve `_loaded` 并派发公开 ready。标志阻止重复 ready 转移同一画布两次。
于是所有排队的 resize、track 和 ready 监听器消息在画布交接之后到达 Worker。
已销毁实例仍由现有终态检查拦截。没有全局禁用 offscreen,也不静默吞掉 render
异常;后续 hybrid detach/reattach 保持原样,其完整组合仍属 05。
API-04/05/10/12:公开注册和实例方法同步返回值、选项默认、资源 URL、Worker
载荷和文件均保留,仅修复内部消息顺序。自有 TS 与公共声明不变。无新依赖、
锁文件、版本或运行时目标变化。JASSUB 的原始及 07 来源指纹保留,08 新增
[独立补丁](../baselines/jassub-offscreen.patch)和[补丁记录](../baselines/jassub-offscreen-patch.json)。
校验器沿两个已记录基线验证补丁链,核对旧补丁的实际 Git diff、新补丁可逆性和
最终 source 指纹,不覆盖历史证据。包内 ARCHITECTURE.md 同步初始化顺序及复跑方式。
## 测试和真实证据
[验证记录](../baselines/jassub-offscreen-validation.json)记录本次源码/产物/测试
指纹、失败与通过报告、原生消息顺序、像素快照和最终安装矩阵。
| 验证 | 结果 |
| --- | --- |
| 新增六项顺序与选择测试 | 修复前 5 通过/1 失败,候选 6 通过;另五项保护重复 ready、提前销毁、显式 false、无能力和自定义 canvas |
| 联合 JASSUB | 191 项通过,含历史缺陷基线与新的候选测试,不把全部数量当成新增修复 |
| 源码/main/legacy | 各 74 项通过;类型未变,严格分包 TS 及 lint 通过 |
| 默认模式 main | 9 项通过:三个浏览器 × 候选核心/实际 5.4.0/实际 5.3.1-beta.1 |
| 默认模式 legacy + 生命周期 | 21 项通过,其中默认模式 9 项,显式主线程生命周期 12 项 |
| main 显式主线程 + 生命周期 | 21 项通过,其中播放 9 项、生命周期 12 项 |
| 实际安装 | 15 个当前/旧编译器矩阵通过,准确入口逐条 14 负例及无 interop 消费;核心 68 和插件 15 个打包成员与最终工作区一致 |
共 51 项候选真实浏览器验证,零跳过、零重试通过。实际版本为 Windows Playwright
Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6。前两者记录 available/selected
均 true,WebKit 两者均 false;不能称为三个引擎都验证了 offscreen。
测试读取显示画布的真实 bitmap 到独立 canvas,避免对已经转移的画布调用
getContext;未替换 Worker、WASM、字体或视频解码。仍逐项断言字幕透明像素数量、
seek 前后像素签名变化、播放位置、网页全屏尺寸和销毁次数,且默认能力选择必须
与平台吻合。原六项失败保留为红色证据,新实现不以放宽页面异常检查获得通过。
正常构建三格式及 docs 副本相同。main gzip 从 6,045 到 6,069 字节(+24),
legacy 从 6,385 到 6,411(+26)。安装证据复用 04 的 runner,记录保留原 task
字段,本次验证明确归属 08。包文档更新后才运行最终 pack,没有混用旧候选。
## 复跑与后续
```sh
yarn test:jassub
yarn test:jassub-types-package
yarn build artplayer-plugin-jassub
node --test test/jassub-offscreen.test.js
node --test refactor/scripts/jassub-vendor-patch.test.mjs
```
使用固定 Node/Yarn;设置 ARTPLAYER_JASSUB_ARTIFACT 为正常 main/legacy 文件,
ARTPLAYER_JASSUB_OFFSCREEN=default 后执行
`yarn test:browser test/browser/jassub-native.spec.js --workers=1`。
不设置该 offscreen 变量时仍验证显式主线程路径。每次浏览器运行后先归档共享报告。
关闭 JASSUB-OFFSCREEN-01,新增独立 08 任务与提交,作为 05 的前置。05 仍未完成:
全量切源/hybrid、错误恢复、真实设备与持续 GPU/内存释放不能由上述窗口测试证明。
06 的分发/通知和 VENDOR-04/05 保留,所有包 major、CI/CD、三轮复盘不受影响。
没有 push、tag 或 npm 发布。
回退本 commit 并正常构建恢复 07,保留其生命周期/字幕时钟修复;补丁链校验器
随同回退。不要对运行中的 Worker 热替换画布协议。
+6 -4
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。 > 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 232 项,范围 22 个包及工作区/示例。 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 233 项,范围 22 个包及工作区/示例。
状态:todo 62 / doing 14 / blocked 0 / done 156 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 状态:todo 62 / doing 14 / blocked 0 / done 157 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -27,7 +27,7 @@
| artplayer-plugin-dash-control | 1.1.0 | PKG-DASH-01, PKG-DASH-02, PKG-DASH-03, PKG-DASH-04, PKG-DASH-05, PKG-DASH-06 | | artplayer-plugin-dash-control | 1.1.0 | PKG-DASH-01, PKG-DASH-02, PKG-DASH-03, PKG-DASH-04, PKG-DASH-05, PKG-DASH-06 |
| artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 | | artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 |
| artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-SDK-01, PKG-HLS-05, PKG-HLS-06 | | artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-SDK-01, PKG-HLS-05, PKG-HLS-06 |
| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06, PKG-JASSUB-07 | | artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06, PKG-JASSUB-07, PKG-JASSUB-08 |
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 | | artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 |
| artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 | | artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 |
| artplayer-plugin-vtt-thumbnail | 1.1.0 | PKG-VTT-THUMB-01, PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 | | artplayer-plugin-vtt-thumbnail | 1.1.0 | PKG-VTT-THUMB-01, PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
@@ -233,9 +233,10 @@
| PKG-JASSUB-02 | artplayer-plugin-jassub<br>建立特有行为与错误测试 | PKG-JASSUB-01, ENG-03, ENG-05 | ASS 字体、时钟/seek/倍率/resize、加载失败和销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | | PKG-JASSUB-02 | artplayer-plugin-jassub<br>建立特有行为与错误测试 | PKG-JASSUB-01, ENG-03, ENG-05 | ASS 字体、时钟/seek/倍率/resize、加载失败和销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-JASSUB-03 | artplayer-plugin-jassub<br>整理内部职责与资源 | PKG-JASSUB-02, CORE-15, CORE-16 | 仅整理自有 adapter/销毁;保留第三方文件及来源 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | | PKG-JASSUB-03 | artplayer-plugin-jassub<br>整理内部职责与资源 | PKG-JASSUB-02, CORE-15, CORE-16 | 仅整理自有 adapter/销毁;保留第三方文件及来源 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| 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-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-05 | artplayer-plugin-jassub<br>验证新旧核心和组合 | PKG-JASSUB-04, CORE-22, PKG-JASSUB-07, PKG-JASSUB-08 | 真实 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-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 | done | | 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 |
| PKG-JASSUB-08 | artplayer-plugin-jassub<br>修复默认 offscreen 初始化消息顺序 | PKG-JASSUB-07 | 在释放排队绘制消息前转移初始画布;独立 vendor 补丁、默认模式原生回归和维护文档 | 保留默认能力选择、Worker/资源和公开同步 API;旧红新绿消息顺序测试及真实默认 offscreen 播放/seek/全屏通过,主线程绘制保持 | H | done |
## 5 包迁移:artplayer-plugin-danmuku-mask ## 5 包迁移:artplayer-plugin-danmuku-mask
@@ -602,3 +603,4 @@
- PKG-DANMUKU-10: [记录](changes/2026-09-13-PKG-DANMUKU-10-heatmap-density.md) [记录](baselines/danmuku-heatmap-density-validation.json) - PKG-DANMUKU-10: [记录](changes/2026-09-13-PKG-DANMUKU-10-heatmap-density.md) [记录](baselines/danmuku-heatmap-density-validation.json)
- PKG-DANMUKU-11: [记录](changes/2026-09-13-PKG-DANMUKU-11-visible-lifetime.md) [记录](baselines/danmuku-lifetime-validation.json) - PKG-DANMUKU-11: [记录](changes/2026-09-13-PKG-DANMUKU-11-visible-lifetime.md) [记录](baselines/danmuku-lifetime-validation.json)
- PKG-DANMUKU-12: [记录](changes/2026-09-13-PKG-DANMUKU-12-frame-sampling.md) [记录](baselines/danmuku-frame-sampling-validation.json) - PKG-DANMUKU-12: [记录](changes/2026-09-13-PKG-DANMUKU-12-frame-sampling.md) [记录](baselines/danmuku-frame-sampling-validation.json)
- PKG-JASSUB-08: [记录](changes/2026-09-14-PKG-JASSUB-08-offscreen.md) [记录](baselines/jassub-offscreen-validation.json) [记录](baselines/jassub-offscreen-patch.json)
+12
View File
@@ -1,5 +1,17 @@
# 进度与证据 # 进度与证据
## PKG-JASSUB-08 默认 offscreen 初始化完成
05 默认模式验收发现 Chromium/Firefox 六项真实页面错误;初始 canvas 消息先于
offscreenCanvas,使 Worker 返回主线程无法绘制的 render。现在先交接画布再打开
ready 门槛,保留原能力选择,新增六项顺序/取消/回退测试(旧 5 通过/1 失败)。
联合 191 项、源码/main/legacy 各 74 项通过;默认 main、默认 legacy 加生命周期、
显式主线程加生命周期共 51 项原生验证通过。15 个安装类型矩阵和最终包成员一致。
[变更与复跑](changes/2026-09-14-PKG-JASSUB-08-offscreen.md)及
[证据](baselines/jassub-offscreen-validation.json)保留失败与修复,关闭
JASSUB-OFFSCREEN-01。08 独立交付;05 保留全量组合、hybrid/错误恢复/设备等验收,
06、VENDOR-04/05、独立 major 与全局发布复盘均未完成。
## PKG-JASSUB-07 vendor 生命周期与默认字幕时钟完成 ## PKG-JASSUB-07 vendor 生命周期与默认字幕时钟完成
独立补丁保留原上游身份,修复幂等销毁、构造回滚、跨视频帧归属、数值倍率和 独立补丁保留原上游身份,修复幂等销毁、构造回滚、跨视频帧归属、数值倍率和
+1
View File
@@ -249,3 +249,4 @@
| JASSUB-RATE-01 | resolved / 已复现 | Fallback ratechange sends an Event instead of numeric playbackRate | 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-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 | | JASSUB-QUERY-01 | resolved / 已复现 | Subtitle query errors throw before calling the consumer and leave request listeners attached | PKG-JASSUB-07, PKG-JASSUB-05 |
| JASSUB-OFFSCREEN-01 | resolved / 已复现 | Default offscreen initialization lets queued resize render before Worker canvas ownership, raising clearRect on false context | PKG-JASSUB-08, PKG-JASSUB-05 |
+22
View File
@@ -5599,6 +5599,28 @@
"test/browser/jassub-lifecycle.spec.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." "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."
},
{
"id": "JASSUB-OFFSCREEN-01",
"title": "Default offscreen initialization lets queued resize render before Worker canvas ownership, raising clearRect on false context",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"PKG-JASSUB-08",
"PKG-JASSUB-05"
],
"evidence": [
"test/jassub-offscreen.test.js",
"test/browser/jassub-native.spec.js"
],
"compatibleResolution": "Transfer the initial canvas after Worker readiness but before releasing queued rendering; preserve selected offscreen/default and all worker resources.",
"closureCriteria": "Same ordering test fails on the pre-fix artifact and passes on candidate; actual default offscreen pixels, seek/layout and teardown pass on Chromium/Firefox and capability fallback on WebKit, with main-thread regressions retained.",
"resolutionEvidence": [
"refactor/changes/2026-09-14-PKG-JASSUB-08-offscreen.md",
"refactor/baselines/jassub-offscreen-validation.json",
"test/jassub-offscreen.test.js"
],
"resolutionRationale": "Controlled queue test reproduces pre-fix canvas-before-transfer order. Candidate transfers before opening readiness and passes actual default offscreen Chromium/Firefox playback/seek/layout; Windows WebKit default capability fallback and explicit main-thread/lifecycle regressions remain green. Full device/hybrid/error recovery remains task05."
} }
] ]
} }
@@ -8,15 +8,21 @@ import { hash } from './releases.mjs'
test('JASSUB local vendor patch retains its exact original, changed source and reversible patch identity', () => { 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 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 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 followUp = JSON.parse(fs.readFileSync('refactor/baselines/jassub-offscreen-patch.json', 'utf8'))
const current = execFileSync('git', ['show', `${followUp.baselineCommit}:${record.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
const patch = fs.readFileSync(record.patchFile, '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(original), record.originalSha256LF)
assert.equal(hash(current), record.patchedSha256LF) assert.equal(hash(current), record.patchedSha256LF)
assert.equal(hash(patch), record.patchSha256LF) assert.equal(hash(patch), record.patchSha256LF)
assert.notEqual(record.originalSha256LF, record.patchedSha256LF) assert.notEqual(record.originalSha256LF, record.patchedSha256LF)
execFileSync('git', ['apply', '--check', '--reverse', '--ignore-space-change', record.patchFile], { stdio: 'pipe' }) const committedPatch = execFileSync('git', ['diff', '--no-ext-diff', record.baselineCommit, followUp.baselineCommit, '--', record.file], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(committedPatch), record.patchSha256LF)
assert.equal(followUp.originalSha256LF, record.patchedSha256LF)
assert.equal(hash(fs.readFileSync(record.file, 'utf8').replaceAll('\r\n', '\n')), followUp.patchedSha256LF)
assert.equal(hash(fs.readFileSync(followUp.patchFile, 'utf8').replaceAll('\r\n', '\n')), followUp.patchSha256LF)
execFileSync('git', ['apply', '--check', '--reverse', '--ignore-space-change', followUp.patchFile], { stdio: 'pipe' })
const registry = JSON.parse(fs.readFileSync('refactor/third-party.json', 'utf8')) const registry = JSON.parse(fs.readFileSync('refactor/third-party.json', 'utf8'))
const group = registry.vendored.find(item => item.id === 'jassub-code-and-workers') const group = registry.vendored.find(item => item.id === 'jassub-code-and-workers')
assert.equal(group.localPatch.originalSha256LF, record.originalSha256LF) assert.equal(group.localPatch.originalSha256LF, record.originalSha256LF)
assert.equal(group.fingerprints.find(item => item.path === record.file).sha256, record.patchedSha256LF) assert.equal(group.fingerprints.find(item => item.path === record.file).sha256, followUp.patchedSha256LF)
}) })
+22 -1
View File
@@ -2496,7 +2496,8 @@
"dependsOn": [ "dependsOn": [
"PKG-JASSUB-04", "PKG-JASSUB-04",
"CORE-22", "CORE-22",
"PKG-JASSUB-07" "PKG-JASSUB-07",
"PKG-JASSUB-08"
], ],
"status": "todo", "status": "todo",
"risk": "H", "risk": "H",
@@ -4769,6 +4770,26 @@
"changes/2026-09-13-PKG-DANMUKU-12-frame-sampling.md", "changes/2026-09-13-PKG-DANMUKU-12-frame-sampling.md",
"baselines/danmuku-frame-sampling-validation.json" "baselines/danmuku-frame-sampling-validation.json"
] ]
},
{
"id": "PKG-JASSUB-08",
"phase": "5 包迁移:artplayer-plugin-jassub",
"title": "修复默认 offscreen 初始化消息顺序",
"scope": [
"artplayer-plugin-jassub"
],
"dependsOn": [
"PKG-JASSUB-07"
],
"status": "done",
"risk": "H",
"deliverable": "在释放排队绘制消息前转移初始画布;独立 vendor 补丁、默认模式原生回归和维护文档",
"acceptance": "保留默认能力选择、Worker/资源和公开同步 API;旧红新绿消息顺序测试及真实默认 offscreen 播放/seek/全屏通过,主线程绘制保持",
"evidence": [
"changes/2026-09-14-PKG-JASSUB-08-offscreen.md",
"baselines/jassub-offscreen-validation.json",
"baselines/jassub-offscreen-patch.json"
]
} }
] ]
} }
+5 -3
View File
@@ -354,7 +354,8 @@
"owners": [ "owners": [
"PKG-JASSUB-01", "PKG-JASSUB-01",
"PKG-JASSUB-06", "PKG-JASSUB-06",
"PKG-JASSUB-07" "PKG-JASSUB-07",
"PKG-JASSUB-08"
], ],
"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.", "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.", "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.",
@@ -386,7 +387,7 @@
{ {
"path": "packages/artplayer-plugin-jassub/src/jassub.es.js", "path": "packages/artplayer-plugin-jassub/src/jassub.es.js",
"algorithm": "sha256-lf", "algorithm": "sha256-lf",
"sha256": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb" "sha256": "6a96af7914f702eb7d6303b466cbe16abddd278040f60cd29f153cdf60e92cb2"
}, },
{ {
"path": "packages/artplayer-plugin-jassub/worker/jassub-worker-modern.wasm", "path": "packages/artplayer-plugin-jassub/worker/jassub-worker-modern.wasm",
@@ -408,7 +409,8 @@
"task": "PKG-JASSUB-07", "task": "PKG-JASSUB-07",
"record": "refactor/baselines/jassub-vendor-patch.json", "record": "refactor/baselines/jassub-vendor-patch.json",
"originalSha256LF": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038", "originalSha256LF": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038",
"patchedSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb" "patchedSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb",
"followUp": "refactor/baselines/jassub-offscreen-patch.json"
} }
}, },
{ {
+15 -6
View File
@@ -11,6 +11,7 @@ const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT
const customCanvas = process.env.ARTPLAYER_JASSUB_CUSTOM_CANVAS === 'true' const customCanvas = process.env.ARTPLAYER_JASSUB_CUSTOM_CANVAS === 'true'
const code = artifact ? fs.readFileSync(artifact, 'utf8') : readMember(archive, member).toString() const code = artifact ? fs.readFileSync(artifact, 'utf8') : readMember(archive, member).toString()
const onDemandRender = process.env.ARTPLAYER_JASSUB_ON_DEMAND !== 'false' const onDemandRender = process.env.ARTPLAYER_JASSUB_ON_DEMAND !== 'false'
const defaultOffscreen = process.env.ARTPLAYER_JASSUB_OFFSCREEN === 'default'
const subtitles = `[Script Info] const subtitles = `[Script Info]
ScriptType: v4.00+ ScriptType: v4.00+
PlayResX: 640 PlayResX: 640
@@ -25,7 +26,7 @@ Dialogue: 0,0:00:05.00,0:00:30.00,Default,,0,0,0,,After seek with actual WASM
` `
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) { for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
test(`JASSUB ${artifact ? 'candidate artifact' : `published ${release.version}`} renders actual WASM subtitles through seek and layout with ${core} core, customCanvas=${customCanvas}`, async ({ page, context }, testInfo) => { test(`JASSUB ${artifact ? 'candidate artifact' : `published ${release.version}`} renders actual WASM subtitles through seek and layout with ${core} core, customCanvas=${customCanvas}, defaultOffscreen=${defaultOffscreen}`, async ({ page, context }, testInfo) => {
const resources = [] const resources = []
const external = [] const external = []
const origin = new URL(testInfo.project.use.baseURL).origin const origin = new URL(testInfo.project.use.baseURL).origin
@@ -86,7 +87,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
await page.locator('#play').click() await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1) await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click() await page.locator('#pause').click()
await page.evaluate(async ({ subContent, onDemandRender, customCanvas }) => { await page.evaluate(async ({ subContent, onDemandRender, customCanvas, defaultOffscreen }) => {
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
let canvas let canvas
if (customCanvas) { if (customCanvas) {
@@ -104,20 +105,26 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm', modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm',
availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' }, availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' },
fonts: ['/assets/jassub/default.woff2'], fonts: ['/assets/jassub/default.woff2'],
offscreenRender: false, ...(defaultOffscreen ? {} : { offscreenRender: false }),
onDemandRender, onDemandRender,
...(canvas ? { canvas } : {}), ...(canvas ? { canvas } : {}),
})) }))
window.jassub = window.art.plugins.artplayerPluginJassub.instance window.jassub = window.art.plugins.artplayerPluginJassub.instance
window.jassubNative.offscreen = { available: 'transferControlToOffscreen' in HTMLCanvasElement.prototype, selected: window.jassub._offscreenRender }
window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error))) window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error)))
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
window.jassub.addEventListener('ready', resolve, { once: true }) window.jassub.addEventListener('ready', resolve, { once: true })
window.jassub.addEventListener('error', event => reject(event.error), { once: true }) window.jassub.addEventListener('error', event => reject(event.error), { once: true })
}) })
const readback = document.createElement('canvas')
window.jassubPixels = (phase) => { window.jassubPixels = (phase) => {
const canvas = window.jassub._canvas const canvas = window.jassub._canvas
const rect = canvas.getBoundingClientRect() const rect = canvas.getBoundingClientRect()
const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data readback.width = canvas.width
readback.height = canvas.height
const context = readback.getContext('2d')
context.drawImage(canvas, 0, 0)
const pixels = context.getImageData(0, 0, readback.width, readback.height).data
let visible = 0 let visible = 0
let signature = 0 let signature = 0
for (let i = 3; i < pixels.length; i += 4) { for (let i = 3; i < pixels.length; i += 4) {
@@ -134,7 +141,9 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
window.jassubNative.snapshots.push(state) window.jassubNative.snapshots.push(state)
return state return state
} }
}, { subContent: subtitles, onDemandRender, customCanvas }) }, { subContent: subtitles, onDemandRender, customCanvas, defaultOffscreen })
const offscreen = await page.evaluate(() => window.jassubNative.offscreen)
expect(offscreen.selected).toBe(defaultOffscreen && !customCanvas && offscreen.available)
await page.locator('#play').click() await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100) await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
const before = await page.evaluate(() => window.jassubPixels('before-seek')) const before = await page.evaluate(() => window.jassubPixels('before-seek'))
@@ -172,7 +181,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
} }
finally { finally {
const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message })) const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message }))
await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, customCanvas, source: artifact ? { artifact, sha256: hash(code) } : { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery or device acceptance; candidate adds direct-then-host destruction.' }, null, 2), contentType: 'application/json' }) await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, customCanvas, defaultOffscreen, source: artifact ? { artifact, sha256: hash(code) } : { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: `Actual worker, WASM and local font; offscreen option ${defaultOffscreen ? 'omitted, actual capability/selection recorded' : 'explicitly false'}. Canvas pixels are read from a separate canvas copying the native display bitmap. No full failure recovery, sustained GPU/memory or physical-device acceptance; candidate adds direct-then-host destruction.` }, null, 2), contentType: 'application/json' })
if (!page.isClosed()) { if (!page.isClosed()) {
await page.evaluate(() => { await page.evaluate(() => {
if (window.art && !window.art.isDestroy) if (window.art && !window.art.isDestroy)
+69
View File
@@ -0,0 +1,69 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Offscreen protocol ordering needs deterministic queue coverage.
import test from 'node:test'
import { jassubCandidate, jassubEnvironment } from './helpers/jassub.js'
const candidate = await jassubCandidate()
function environment(offscreen = true) {
const code = `${candidate.code}\nObject.defineProperty(module.exports.default || module.exports, '__canvasClass', { value: HTMLCanvasElement });`
const env = jassubEnvironment({ ...candidate, code })
const transferred = []
if (offscreen) {
env.factory.__canvasClass.prototype.transferControlToOffscreen = function () {
const canvas = { width: this.width, height: this.height }
transferred.push(canvas)
return canvas
}
}
return { ...env, transferred }
}
test('JASSUB initializes offscreen ownership before queued resize, track and ready-listener messages', async () => {
const env = environment()
const { instance } = env.factory()(env.art)
instance.resize(640, 360)
instance.setTrack('[Script Info]')
instance.addEventListener('ready', () => instance.resize(320, 180))
await env.ready()
const messages = env.workers[0].messages
assert.deepEqual(messages.slice(0, 2).map(message => message.target), ['init', 'offscreenCanvas'])
assert.equal(messages[1].transferable[0], env.transferred[0])
assert(messages.slice(2).some(message => message.target === 'canvas'))
assert(messages.slice(2).some(message => message.target === 'setTrack'))
assert.equal(instance._ctx, false)
instance.destroy()
})
test('JASSUB duplicate readiness does not transfer the initial offscreen canvas twice', async () => {
const env = environment()
const { instance } = env.factory()(env.art)
await env.ready()
await env.ready()
assert.equal(env.workers[0].messages.filter(message => message.target === 'offscreenCanvas').length, 1)
instance.destroy()
})
test('JASSUB destroy before readiness never transfers its offscreen canvas', async () => {
const env = environment()
const { instance } = env.factory()(env.art)
instance.destroy()
await env.flush()
instance._ready()
await env.flush()
assert.equal(env.workers[0].messages.length, 0)
assert.equal(env.workers[0].terminated, 1)
})
for (const option of [{ offscreenRender: false }, 'unsupported', 'custom']) {
test(`JASSUB main-thread canvas has no initial transfer for ${JSON.stringify(option)}`, async () => {
const env = environment(option !== 'unsupported')
const options = option === 'custom' ? { canvas: env.createCanvas() } : typeof option === 'object' ? option : {}
const { instance } = env.factory(options)(env.art)
await env.ready()
instance.resize(640, 360)
await env.flush()
assert.equal(env.workers[0].messages.some(message => message.target === 'offscreenCanvas'), false)
assert.equal(instance._ctx, instance._canvas.context)
instance.destroy()
})
}