mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
fix(jassub): [PKG-JASSUB-09] checkpoint hybrid canvas ownership and frame completion
This commit is contained in:
1 parent
b4b0276fe9
commit
7f185f698d
22 files changed
+18866
-37
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.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
|
||||
!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
|
||||
* 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
|
||||
@@ -528,9 +528,9 @@ const _o = class _o extends EventTarget {
|
||||
}
|
||||
// if the video or track changed, we need to re-attach the offscreen canvas
|
||||
_reAttachOffscreen() {
|
||||
if (!this._offscreenRender || !this._ctx)
|
||||
if (this._destroyed || !this._offscreenRender || !this._ctx)
|
||||
return null;
|
||||
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);
|
||||
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = false, this.busy = false, this._lastDemandTime = null, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, true);
|
||||
}
|
||||
_updateColorSpace() {
|
||||
if (this._destroyed || this._colorCallback != null) return;
|
||||
@@ -560,6 +560,10 @@ const _o = class _o extends EventTarget {
|
||||
!e || !t || e !== t && (this._detachOffscreen(), this._ctx.filter = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${f[e][t]} 0 0 0 0 0 1 0'/></filter></svg>#f")`);
|
||||
}
|
||||
_render({ images: e, asyncRender: t, times: s, width: r, height: n, colorSpace: a }) {
|
||||
if (!this._ctx) {
|
||||
for (const image of e) image.image?.close?.();
|
||||
return;
|
||||
}
|
||||
this._unbusy(), this.debug && (s.IPCTime = Date.now() - s.JSRenderTime), (this._canvasctrl.width !== r || this._canvasctrl.height !== n) && (this._canvasctrl.width = r, this._canvasctrl.height = n, this._verifyColorSpace({ subtitleColorSpace: a })), this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
|
||||
for (const i of e)
|
||||
i.image && (t ? (this._ctx.drawImage(i.image, i.x, i.y), i.image.close()) : (this._bufferCanvas.width = i.w, this._bufferCanvas.height = i.h, this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)), i.w, i.h), 0, 0), this._ctx.drawImage(this._bufferCanvas, i.x, i.y)));
|
||||
|
||||
@@ -79,8 +79,17 @@ loaded gate and dispatching the public ready event. Otherwise a queued resize ca
|
||||
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
|
||||
existing selection. Later hybrid detach/reattach remains separate. PKG-JASSUB-09
|
||||
records `../../refactor/baselines/jassub-hybrid-patch.json`: when a hybrid render
|
||||
arrives after reattachment to offscreen, release its bitmaps before any drawing,
|
||||
color-space correction or busy-state mutation. The newly transferred canvas must
|
||||
remain owned by the current mode. Reattachment is also terminal after destruction,
|
||||
including public setTrack/setTrackByUrl calls. Valid current hybrid frames still
|
||||
draw and release their bitmaps normally. A reattachment retires the old busy/demand
|
||||
state and requests a forced draw from the newly owned offscreen canvas; dropping
|
||||
the old bitmap alone would otherwise leave the renderer waiting forever for its
|
||||
discarded completion. Subsequent frame callbacks update the new pending demand.
|
||||
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
|
||||
@@ -137,5 +146,14 @@ the browser's actual default selection. The test copies the displayed canvas bit
|
||||
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.
|
||||
`jassub-hybrid.spec.js` changes actual ASS color-space metadata and holds one native
|
||||
Worker ImageBitmap message across a public track switch. It verifies bitmap closure,
|
||||
unchanged new canvas ownership and terminal track calls in a real hybrid-capable
|
||||
path. Reports distinguish this controlled delivery timing from ordinary playback.
|
||||
If native VideoFrame exposes no usable color-space matrix (observed on Windows
|
||||
Firefox for this sample), the test verifies ordinary subtitle switching and cleanup
|
||||
only; no hybrid coverage is claimed. Windows WebKit without canvas transfer is
|
||||
likewise a capability fallback control. Further codec/device/hybrid combinations
|
||||
and sustained resource release remain PKG-JASSUB-05.
|
||||
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.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
|
||||
!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
|
||||
* 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
|
||||
@@ -528,9 +528,9 @@ const _o = class _o extends EventTarget {
|
||||
}
|
||||
// if the video or track changed, we need to re-attach the offscreen canvas
|
||||
_reAttachOffscreen() {
|
||||
if (!this._offscreenRender || !this._ctx)
|
||||
if (this._destroyed || !this._offscreenRender || !this._ctx)
|
||||
return null;
|
||||
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);
|
||||
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = false, this.busy = false, this._lastDemandTime = null, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, true);
|
||||
}
|
||||
_updateColorSpace() {
|
||||
if (this._destroyed || this._colorCallback != null) return;
|
||||
@@ -560,6 +560,10 @@ const _o = class _o extends EventTarget {
|
||||
!e || !t || e !== t && (this._detachOffscreen(), this._ctx.filter = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${f[e][t]} 0 0 0 0 0 1 0'/></filter></svg>#f")`);
|
||||
}
|
||||
_render({ images: e, asyncRender: t, times: s, width: r, height: n, colorSpace: a }) {
|
||||
if (!this._ctx) {
|
||||
for (const image of e) image.image?.close?.();
|
||||
return;
|
||||
}
|
||||
this._unbusy(), this.debug && (s.IPCTime = Date.now() - s.JSRenderTime), (this._canvasctrl.width !== r || this._canvasctrl.height !== n) && (this._canvasctrl.width = r, this._canvasctrl.height = n, this._verifyColorSpace({ subtitleColorSpace: a })), this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
|
||||
for (const i of e)
|
||||
i.image && (t ? (this._ctx.drawImage(i.image, i.x, i.y), i.image.close()) : (this._bufferCanvas.width = i.w, this._bufferCanvas.height = i.h, this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)), i.w, i.h), 0, 0), this._ctx.drawImage(this._bufferCanvas, i.x, i.y)));
|
||||
|
||||
@@ -527,9 +527,9 @@ class o extends EventTarget {
|
||||
}
|
||||
// if the video or track changed, we need to re-attach the offscreen canvas
|
||||
_reAttachOffscreen() {
|
||||
if (!this._offscreenRender || !this._ctx)
|
||||
if (this._destroyed || !this._offscreenRender || !this._ctx)
|
||||
return null;
|
||||
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);
|
||||
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = !1, this.busy = !1, this._lastDemandTime = null, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, !0);
|
||||
}
|
||||
_updateColorSpace() {
|
||||
if (this._destroyed || this._colorCallback != null) return;
|
||||
@@ -559,6 +559,10 @@ class o extends EventTarget {
|
||||
!e || !t || e !== t && (this._detachOffscreen(), this._ctx.filter = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${f[e][t]} 0 0 0 0 0 1 0'/></filter></svg>#f")`);
|
||||
}
|
||||
_render({ images: e, asyncRender: t, times: s, width: r, height: n, colorSpace: a }) {
|
||||
if (!this._ctx) {
|
||||
for (const image of e) image.image?.close?.();
|
||||
return;
|
||||
}
|
||||
this._unbusy(), this.debug && (s.IPCTime = Date.now() - s.JSRenderTime), (this._canvasctrl.width !== r || this._canvasctrl.height !== n) && (this._canvasctrl.width = r, this._canvasctrl.height = n, this._verifyColorSpace({ subtitleColorSpace: a })), this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
|
||||
for (const i of e)
|
||||
i.image && (t ? (this._ctx.drawImage(i.image, i.x, i.y), i.image.close()) : (this._bufferCanvas.width = i.w, this._bufferCanvas.height = i.h, this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)), i.w, i.h), 0, 0), this._ctx.drawImage(this._bufferCanvas, i.x, i.y)));
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"task": "PKG-JASSUB-09",
|
||||
"baselineCommit": "b4b0276fe9df13218ece21c3ba6a51b42948e16d",
|
||||
"file": "packages/artplayer-plugin-jassub/src/jassub.es.js",
|
||||
"originalSha256LF": "6a96af7914f702eb7d6303b466cbe16abddd278040f60cd29f153cdf60e92cb2",
|
||||
"patchedSha256LF": "60590f2fca6ffded74f96f8a6001a86c32eb38f5e1fae26338cbbb43e93fb70a",
|
||||
"patchFile": "refactor/baselines/jassub-hybrid.patch",
|
||||
"patchSha256LF": "e1ff5591ea38fc66b46140458db93156c66aaaeceacb5ea433021cd8868651eb",
|
||||
"predecessor": "refactor/baselines/jassub-offscreen-patch.json",
|
||||
"purpose": "Release obsolete hybrid bitmaps before any render/scheduler/color-space effect while the display is offscreen; prevent public track calls from recreating canvas after hybrid destruction."
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,27 @@
|
||||
diff --git a/packages/artplayer-plugin-jassub/src/jassub.es.js b/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
index 6bd2bc2c6..88088b0ca 100644
|
||||
--- a/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
+++ b/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
@@ -527,9 +527,9 @@ class o extends EventTarget {
|
||||
}
|
||||
// if the video or track changed, we need to re-attach the offscreen canvas
|
||||
_reAttachOffscreen() {
|
||||
- if (!this._offscreenRender || !this._ctx)
|
||||
+ if (this._destroyed || !this._offscreenRender || !this._ctx)
|
||||
return null;
|
||||
- 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);
|
||||
+ this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = !1, this.busy = !1, this._lastDemandTime = null, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, !0);
|
||||
}
|
||||
_updateColorSpace() {
|
||||
if (this._destroyed || this._colorCallback != null) return;
|
||||
@@ -559,6 +559,10 @@ class o extends EventTarget {
|
||||
!e || !t || e !== t && (this._detachOffscreen(), this._ctx.filter = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${f[e][t]} 0 0 0 0 0 1 0'/></filter></svg>#f")`);
|
||||
}
|
||||
_render({ images: e, asyncRender: t, times: s, width: r, height: n, colorSpace: a }) {
|
||||
+ if (!this._ctx) {
|
||||
+ for (const image of e) image.image?.close?.();
|
||||
+ return;
|
||||
+ }
|
||||
this._unbusy(), this.debug && (s.IPCTime = Date.now() - s.JSRenderTime), (this._canvasctrl.width !== r || this._canvasctrl.height !== n) && (this._canvasctrl.width = r, this._canvasctrl.height = n, this._verifyColorSpace({ subtitleColorSpace: a })), this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
|
||||
for (const i of e)
|
||||
i.image && (t ? (this._ctx.drawImage(i.image, i.x, i.y), i.image.close()) : (this._bufferCanvas.width = i.w, this._bufferCanvas.height = i.h, this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)), i.w, i.h), 0, 0), this._ctx.drawImage(this._bufferCanvas, i.x, i.y)));
|
||||
@@ -0,0 +1,87 @@
|
||||
# PKG-JASSUB-09 混合绘制的画布与等待状态归属
|
||||
|
||||
状态:doing 检查点。hybrid 修复已有受控及原生证据;Firefox 默认模式的继承
|
||||
回归缺口仍需处理,因此此提交不把 09 或 05 标为完成。
|
||||
|
||||
## 复现与修复
|
||||
|
||||
在 05 补全字幕切换验收时,从 `b4b0276fe9df13218ece21c3ba6a51b42948e16d`
|
||||
发现两类相连的生命周期缺陷。ASS 色彩矩阵和视频不一致时,JASSUB 会从纯
|
||||
offscreen 转为 hybrid:Worker 返回 ImageBitmap,主线程用 canvas filter 修正
|
||||
颜色。公开 setTrack/setTrackByUrl 随后重新转移画布。如果旧 hybrid 帧已经到达
|
||||
消息队列,它会对 `_ctx=false` 执行绘制,或者在尺寸不同时借旧色彩信息重新创建
|
||||
主线程画布,破坏刚完成的转移。销毁 hybrid 实例后调用上述方法也会创建新画布。
|
||||
|
||||
修复保留 API-04/05/08/12 的公开工厂、方法返回、选项、资源和 Worker 协议:
|
||||
|
||||
- 当前画布由 offscreen 所有时,旧 hybrid render 先关闭其 bitmap,立即返回,
|
||||
不改 busy、不做色彩转换、不创建/绘制新画布。正常 hybrid 帧继续绘制和关闭。
|
||||
- reattach 先退休旧模式的 busy/待处理时间,再对新画布请求强制绘制。否则丢弃
|
||||
的旧 render 同时也是等待中的完成通知,后续 RVFC 只积累待处理时间,字幕会
|
||||
持续空白。新的强制绘制及其 unbusy 通知恢复当前帧处理。
|
||||
- reattach 遇到已销毁实例直接返回,两个公开切字幕方法不能重新创建或转移画布。
|
||||
|
||||
没有修改 Worker JS、WASM、字体、公共声明、版本或依赖。原 07/08 补丁保留,
|
||||
新增[补丁记录](../baselines/jassub-hybrid-patch.json)和
|
||||
[独立 diff](../baselines/jassub-hybrid.patch)。来源测试按三段实际 Git 基线验证
|
||||
原文件、修改后文件和 diff 身份,再反向检查最后一个补丁;不是覆盖旧指纹。
|
||||
包内 ARCHITECTURE.md 同步了旧等待退休、强制新绘制、bitmap 和终态归属。
|
||||
|
||||
## 测试设计与中间失败
|
||||
|
||||
新增五项受控边界,和原 offscreen 六项一起,冻结 08 产物为 7 通过/4 失败,
|
||||
最终候选 11 通过。它们分别保护迟到帧不解除新需求、正常帧实际绘制和释放、
|
||||
切换时强制重绘,以及两个终态公开方法。没有把五项全部声称为旧版失败。
|
||||
|
||||
真实浏览器用不同 YCbCr Matrix 的 ASS 触发实际 WASM 色彩响应。Chromium 的
|
||||
VideoFrame 对本视频返回 bt709,因此可自然进入 hybrid。测试截留一条已经从
|
||||
原生 Worker 收到的 ImageBitmap 消息,在同步 setTrack 完成 reattach 后交付,
|
||||
验证新 canvas 身份不变、bitmap width 归零,以及后续字幕像素和终态调用。
|
||||
这是受控消息时序下的真实资源验证,不是未经拦截的全部生产时序或 GPU 耐久证明。
|
||||
|
||||
第一次只检查迟到消息是否抛错是不充分的:旧代码通过色彩修正重新创建画布,
|
||||
可能不抛错。测试补上 canvas 身份和模式断言,旧 Chromium 明确失败。
|
||||
第一次只丢弃旧 bitmap 的候选又出现持续空白,原生 sent 序列停在重连 canvas,
|
||||
推动补上旧 busy/需求退休及 force=true 回归。两次失败都保留。
|
||||
|
||||
Firefox 155.0 对本样本的 VideoFrame matrix 返回 null;WebKit 26.6 没有转移
|
||||
能力。它们仍执行真实字幕切换、像素变化和清理,但不能计入 hybrid 位图交错覆盖。
|
||||
测试按真实能力走明确分支,没有 mock 出色彩矩阵或调用内部方法伪造原生转换。
|
||||
受控测试才直接调用内部模式方法,负责确定性的状态机边界。
|
||||
|
||||
另有一轮 Firefox 在首次像素/网页全屏时超时:该轮同时运行安装类型检查。
|
||||
失败中默认模式的 Worker received 没有 render,只有 unbusy;该页面也未调用
|
||||
track 重新挂载,因此本补丁两个新分支并未在这条失败路径执行。去掉同时运行的
|
||||
重负载检查后,候选 11 通过/1 失败,仍复现 published 核心的全屏超时。
|
||||
08 产物单独播放三项通过,但先执行字幕切换再播放的顺序对照为 2 通过/2 失败,
|
||||
也在两个旧核心复现。说明不能仅归因于本次补丁或 CPU 并发;真正原因未确定。
|
||||
额外的 JASSUB-FIREFOX-OFFSCREEN-01 保持 open,不把重跑成功当作修复依据。
|
||||
|
||||
## 验证、复跑与剩余范围
|
||||
|
||||
最终命令、计数、候选指纹、安装成员和红绿浏览器结果见
|
||||
[验证记录](../baselines/jassub-hybrid-validation.json)。联合检查含旧行为基线,
|
||||
不是全部新功能测试。联合 196 项、源码/main/legacy 各 79 项、实际安装 15 个
|
||||
类型矩阵通过;legacy 切换三项和显式主线程九项原生验证通过。默认 main 最终
|
||||
顺序验证仍是 11 通过/1 失败,不能汇总成全绿。正常 main/legacy/ESM 均由仓库 build 生成并与 docs 副本
|
||||
核对。安装验证在最终包文档和源码构建后重新运行;首次 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
|
||||
yarn test:browser test/browser/jassub-hybrid.spec.js --workers=1
|
||||
```
|
||||
|
||||
设置 ARTPLAYER_JASSUB_ARTIFACT 选择正常 main/legacy;默认播放回归另设置
|
||||
ARTPLAYER_JASSUB_OFFSCREEN=default 并执行 jassub-native.spec.js。不设置后者
|
||||
验证显式主线程路径。归档共享浏览器报告后才开始下一轮;排查媒体等待问题时
|
||||
避免同时运行其他重负载任务,并保留对照范围及原始失败。
|
||||
|
||||
本检查点记录已实现的 hybrid 归属修复,但任务和风险验收仍未关闭。05 继续完整字幕/核心组合、Firefox
|
||||
稳定性、设备和持续 GPU/内存验证;06 与 VENDOR-04/05 保留分发/通知要求。
|
||||
没有声明完整 JASSUB 验收、major 升级、CI/CD 远程运行、三轮复盘或发布已完成。
|
||||
不 push/tag/publish。回退本 commit 并正常构建可恢复 08;不要对运行中 Worker
|
||||
热替换协议或只回退来源指纹而保留不一致的 wrapper。
|
||||
+6
-4
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 233 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 234 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 62 / doing 14 / blocked 0 / done 157 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 62 / doing 15 / blocked 0 / done 157 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 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-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-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-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, PKG-JASSUB-09 |
|
||||
| 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-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,10 +233,11 @@
|
||||
| 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-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, PKG-JASSUB-08 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-JASSUB-05 | artplayer-plugin-jassub<br>验证新旧核心和组合 | PKG-JASSUB-04, CORE-22, PKG-JASSUB-07, PKG-JASSUB-08, PKG-JASSUB-09 | 真实 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 | done |
|
||||
| PKG-JASSUB-08 | artplayer-plugin-jassub<br>修复默认 offscreen 初始化消息顺序 | PKG-JASSUB-07 | 在释放排队绘制消息前转移初始画布;独立 vendor 补丁、默认模式原生回归和维护文档 | 保留默认能力选择、Worker/资源和公开同步 API;旧红新绿消息顺序测试及真实默认 offscreen 播放/seek/全屏通过,主线程绘制保持 | H | done |
|
||||
| PKG-JASSUB-09 | artplayer-plugin-jassub<br>修复 hybrid 迟到绘制与销毁后画布重建 | PKG-JASSUB-08 | 释放旧混合绘制 bitmap,不改变新模式的画布/时钟;销毁后切字幕不重新分配画布;来源补丁、原生排队消息及维护记录 | 旧红新绿验证旧帧不能重建画布/解除新绘制等待;正常 hybrid 绘制保持;真实 Worker/ImageBitmap 交错、主线程及默认模式回归通过,能力缺口明确 | H | doing |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-danmuku-mask
|
||||
|
||||
@@ -604,3 +605,4 @@
|
||||
- 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-JASSUB-08: [记录](changes/2026-09-14-PKG-JASSUB-08-offscreen.md) [记录](baselines/jassub-offscreen-validation.json) [记录](baselines/jassub-offscreen-patch.json)
|
||||
- PKG-JASSUB-09: [记录](changes/2026-09-14-PKG-JASSUB-09-hybrid.md) [记录](baselines/jassub-hybrid-validation.json) [记录](baselines/jassub-hybrid-patch.json)
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-JASSUB-09 hybrid 归属修复检查点(doing)
|
||||
|
||||
修复旧混合帧干扰新 offscreen 画布、被丢弃完成通知导致等待卡死,以及销毁后
|
||||
两个切字幕方法重新分配画布。受控 11 项旧产物 7 通过/4 失败,候选全部通过;
|
||||
联合 196 项、源码/main/legacy 各 79 项和 15 个实际安装类型组合通过。
|
||||
Chromium 实际 Worker/ImageBitmap 跨公开切字幕交错通过,legacy 切换三项和主线程
|
||||
九项通过;Firefox matrix=null、WebKit 无转移能力均只算普通切换控制。
|
||||
默认 main 最终顺序回归 11 通过/1 Firefox 全屏失败,08 产物同顺序也复现旧核心
|
||||
失败;不能归因于并发负载,根因仍待查。09/05 及相关风险不标完成。
|
||||
[检查点记录](changes/2026-09-14-PKG-JASSUB-09-hybrid.md)和
|
||||
[完整红绿证据](baselines/jassub-hybrid-validation.json)保留中间不完整修复和对照。
|
||||
下一步处理 JASSUB-FIREFOX-OFFSCREEN-01,再继续完整分包验收和未完成迁移。
|
||||
|
||||
## PKG-JASSUB-08 默认 offscreen 初始化完成
|
||||
|
||||
05 默认模式验收发现 Chromium/Firefox 六项真实页面错误;初始 canvas 消息先于
|
||||
|
||||
@@ -250,3 +250,5 @@
|
||||
| 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-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 |
|
||||
| JASSUB-HYBRID-01 | open / 已复现 | Late hybrid frames mutate the new offscreen canvas or throw; terminal track calls recreate transferred canvases | PKG-JASSUB-09, PKG-JASSUB-05 |
|
||||
| JASSUB-FIREFOX-OFFSCREEN-01 | open / 已复现 | Windows Firefox default offscreen readback/playback can stall after a preceding subtitle-switch test and web fullscreen | PKG-JASSUB-09, PKG-JASSUB-05 |
|
||||
@@ -5621,6 +5621,40 @@
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-HYBRID-01",
|
||||
"title": "Late hybrid frames mutate the new offscreen canvas or throw; terminal track calls recreate transferred canvases",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-09",
|
||||
"PKG-JASSUB-05"
|
||||
],
|
||||
"evidence": [
|
||||
"test/jassub-offscreen.test.js",
|
||||
"test/browser/jassub-hybrid.spec.js"
|
||||
],
|
||||
"compatibleResolution": "Ignore obsolete hybrid render side effects while releasing their bitmaps, and preserve terminal canvas ownership in reattachment.",
|
||||
"closureCriteria": "Controlled and held-native-bitmap red/green cases preserve the newly reattached canvas and pending demand; terminal public track calls allocate nothing, normal hybrid and default/main-thread rendering remain valid. Native capability limitations are recorded."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-FIREFOX-OFFSCREEN-01",
|
||||
"title": "Windows Firefox default offscreen readback/playback can stall after a preceding subtitle-switch test and web fullscreen",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-09",
|
||||
"PKG-JASSUB-05"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-09-hybrid.md",
|
||||
"refactor/baselines/jassub-hybrid-validation.json",
|
||||
"test/browser/jassub-native.spec.js",
|
||||
"test/browser/jassub-hybrid.spec.js"
|
||||
],
|
||||
"compatibleResolution": "Isolate native media/compositor/readback and sequence effects without globally disabling default offscreen or deleting assertions. The pre-09 artifact also fails in the same sequence; do not claim the new hybrid guard caused or fixed this separate failure.",
|
||||
"closureCriteria": "Identify and verify a compatible fix or a reviewed precise platform boundary with native controls. Preserve known red sequence and successful standalone comparison; candidate playback/fullscreen pixels and lifetime must be justified, not merely rerun to green."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,23 +6,26 @@ 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 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')
|
||||
assert.equal(hash(original), record.originalSha256LF)
|
||||
assert.equal(hash(current), record.patchedSha256LF)
|
||||
assert.equal(hash(patch), record.patchSha256LF)
|
||||
assert.notEqual(record.originalSha256LF, record.patchedSha256LF)
|
||||
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 records = ['vendor', 'offscreen', 'hybrid'].map(name => JSON.parse(fs.readFileSync(`refactor/baselines/jassub-${name}-patch.json`, 'utf8')))
|
||||
for (const [index, record] of records.entries()) {
|
||||
const next = records[index + 1]
|
||||
const original = execFileSync('git', ['show', `${record.baselineCommit}:${record.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
|
||||
const current = (next ? execFileSync('git', ['show', `${next.baselineCommit}:${record.file}`], { encoding: 'utf8' }) : fs.readFileSync(record.file, 'utf8')).replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(original), record.originalSha256LF)
|
||||
assert.equal(hash(current), record.patchedSha256LF)
|
||||
assert.equal(hash(fs.readFileSync(record.patchFile, 'utf8').replaceAll('\r\n', '\n')), record.patchSha256LF)
|
||||
assert.notEqual(record.originalSha256LF, record.patchedSha256LF)
|
||||
if (next) {
|
||||
assert.equal(next.originalSha256LF, record.patchedSha256LF)
|
||||
const committedPatch = execFileSync('git', ['diff', '--no-ext-diff', record.baselineCommit, next.baselineCommit, '--', record.file], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(committedPatch), record.patchSha256LF)
|
||||
}
|
||||
else {
|
||||
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, followUp.patchedSha256LF)
|
||||
assert.equal(group.localPatch.originalSha256LF, records[0].originalSha256LF)
|
||||
assert.equal(group.fingerprints.find(item => item.path === records[0].file).sha256, records.at(-1).patchedSha256LF)
|
||||
})
|
||||
+22
-1
@@ -2497,7 +2497,8 @@
|
||||
"PKG-JASSUB-04",
|
||||
"CORE-22",
|
||||
"PKG-JASSUB-07",
|
||||
"PKG-JASSUB-08"
|
||||
"PKG-JASSUB-08",
|
||||
"PKG-JASSUB-09"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
@@ -4790,6 +4791,26 @@
|
||||
"baselines/jassub-offscreen-validation.json",
|
||||
"baselines/jassub-offscreen-patch.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-JASSUB-09",
|
||||
"phase": "5 包迁移:artplayer-plugin-jassub",
|
||||
"title": "修复 hybrid 迟到绘制与销毁后画布重建",
|
||||
"scope": [
|
||||
"artplayer-plugin-jassub"
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-JASSUB-08"
|
||||
],
|
||||
"status": "doing",
|
||||
"risk": "H",
|
||||
"deliverable": "释放旧混合绘制 bitmap,不改变新模式的画布/时钟;销毁后切字幕不重新分配画布;来源补丁、原生排队消息及维护记录",
|
||||
"acceptance": "旧红新绿验证旧帧不能重建画布/解除新绘制等待;正常 hybrid 绘制保持;真实 Worker/ImageBitmap 交错、主线程及默认模式回归通过,能力缺口明确",
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-JASSUB-09-hybrid.md",
|
||||
"baselines/jassub-hybrid-validation.json",
|
||||
"baselines/jassub-hybrid-patch.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -355,7 +355,8 @@
|
||||
"PKG-JASSUB-01",
|
||||
"PKG-JASSUB-06",
|
||||
"PKG-JASSUB-07",
|
||||
"PKG-JASSUB-08"
|
||||
"PKG-JASSUB-08",
|
||||
"PKG-JASSUB-09"
|
||||
],
|
||||
"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.",
|
||||
@@ -387,7 +388,7 @@
|
||||
{
|
||||
"path": "packages/artplayer-plugin-jassub/src/jassub.es.js",
|
||||
"algorithm": "sha256-lf",
|
||||
"sha256": "6a96af7914f702eb7d6303b466cbe16abddd278040f60cd29f153cdf60e92cb2"
|
||||
"sha256": "60590f2fca6ffded74f96f8a6001a86c32eb38f5e1fae26338cbbb43e93fb70a"
|
||||
},
|
||||
{
|
||||
"path": "packages/artplayer-plugin-jassub/worker/jassub-worker-modern.wasm",
|
||||
@@ -410,7 +411,11 @@
|
||||
"record": "refactor/baselines/jassub-vendor-patch.json",
|
||||
"originalSha256LF": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038",
|
||||
"patchedSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb",
|
||||
"followUp": "refactor/baselines/jassub-offscreen-patch.json"
|
||||
"followUp": "refactor/baselines/jassub-offscreen-patch.json",
|
||||
"followUps": [
|
||||
"refactor/baselines/jassub-offscreen-patch.json",
|
||||
"refactor/baselines/jassub-hybrid-patch.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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 subtitle = `[Script Info]
|
||||
ScriptType: v4.00+
|
||||
PlayResX: 640
|
||||
PlayResY: 360
|
||||
YCbCr Matrix: MATRIX
|
||||
[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, Effect, Text
|
||||
Dialogue: 0,0:00:00.00,0:00:30.00,Default,,0,0,,CAPTION
|
||||
`
|
||||
|
||||
test('JASSUB actual subtitle color-space transitions tolerate a queued native hybrid bitmap and terminal track calls', async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=candidate')
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.evaluate(() => window.createPlayer('/assets/sample/steve-jobs.mp4'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(30)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
try {
|
||||
const capabilities = await page.evaluate(async (subtitle) => {
|
||||
let matrix
|
||||
if (typeof VideoFrame === 'function') {
|
||||
const frame = new VideoFrame(window.art.video)
|
||||
matrix = frame.colorSpace.matrix
|
||||
frame.close()
|
||||
}
|
||||
const matched = matrix === 'bt709' ? 'TV.709' : 'TV.601'
|
||||
const mismatched = matched === 'TV.709' ? 'TV.601' : 'TV.709'
|
||||
const make = (color, caption) => subtitle.replace('MATRIX', color).replace('CAPTION', caption)
|
||||
const probe = window.hybridProbe = { available: 'transferControlToOffscreen' in HTMLCanvasElement.prototype, matrix, held: [], sent: [], received: [], errors: [], matched: make(matched, 'Replacement subtitle after transfer'), mismatched: make(mismatched, 'Original hybrid subtitle') }
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
window.art.plugins.add(register({
|
||||
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'],
|
||||
subContent: probe.mismatched,
|
||||
}))
|
||||
const instance = window.jassub = window.art.plugins.artplayerPluginJassub.instance
|
||||
probe.selected = instance._offscreenRender
|
||||
instance.addEventListener('error', event => probe.errors.push(String(event.error)))
|
||||
const worker = instance._worker
|
||||
const dispatch = worker.onmessage
|
||||
probe.dispatch = event => dispatch.call(worker, event)
|
||||
worker.onmessage = (event) => {
|
||||
probe.received.push({ target: event.data.target, images: event.data.images?.length })
|
||||
if (probe.holdRender && event.data.target === 'render' && event.data.images.some(item => item.image instanceof ImageBitmap)) {
|
||||
probe.holdRender = false
|
||||
probe.held.push(event)
|
||||
return
|
||||
}
|
||||
probe.dispatch(event)
|
||||
}
|
||||
const post = worker.postMessage.bind(worker)
|
||||
worker.postMessage = (message, ...rest) => {
|
||||
probe.sent.push(message.target)
|
||||
return post(message, ...rest)
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
instance.addEventListener('ready', resolve, { once: true })
|
||||
instance.addEventListener('error', event => reject(event.error), { once: true })
|
||||
})
|
||||
const canvas = document.createElement('canvas')
|
||||
probe.pixels = () => {
|
||||
canvas.width = instance._canvas.width
|
||||
canvas.height = instance._canvas.height
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(instance._canvas, 0, 0)
|
||||
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data
|
||||
let visible = 0
|
||||
let signature = 0
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i])
|
||||
visible++
|
||||
signature = (signature + i * data[i]) >>> 0
|
||||
}
|
||||
return { visible, signature }
|
||||
}
|
||||
return { available: probe.available, selected: probe.selected, matrix }
|
||||
}, subtitle)
|
||||
await expect.poll(() => page.evaluate(() => window.hybridProbe.pixels().visible)).toBeGreaterThan(100)
|
||||
const before = await page.evaluate(() => window.hybridProbe.pixels())
|
||||
expect(capabilities.selected).toBe(capabilities.available)
|
||||
const hybridExpected = capabilities.selected && ['bt709', 'bt470bg', 'smpte170m'].includes(capabilities.matrix)
|
||||
if (hybridExpected) {
|
||||
await expect.poll(() => page.evaluate(() => Boolean(window.jassub._ctx))).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.hybridProbe.holdRender = true
|
||||
window.jassub.resize(0, 0, 0, 0, true)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.hybridProbe.held.length)).toBe(1)
|
||||
const delivered = await page.evaluate(() => {
|
||||
const probe = window.hybridProbe
|
||||
const event = probe.held.shift()
|
||||
const images = event.data.images.map(item => item.image)
|
||||
const widths = images.map(image => image.width)
|
||||
window.jassub.setTrack(probe.matched)
|
||||
const canvas = window.jassub._canvas
|
||||
const reattached = window.jassub._ctx === false
|
||||
let failure
|
||||
try {
|
||||
probe.dispatch(event)
|
||||
}
|
||||
catch (error) { failure = error.message }
|
||||
return probe.late = { reattached, widths, closedWidths: images.map(image => image.width), failure, sameCanvas: canvas === window.jassub._canvas, stillOffscreen: window.jassub._ctx === false }
|
||||
})
|
||||
expect(delivered.reattached).toBe(true)
|
||||
expect(delivered.widths.every(width => width > 0)).toBe(true)
|
||||
expect(delivered.failure).toBeUndefined()
|
||||
expect(delivered.sameCanvas).toBe(true)
|
||||
expect(delivered.stillOffscreen).toBe(true)
|
||||
expect(delivered.closedWidths.every(width => width === 0)).toBe(true)
|
||||
}
|
||||
else {
|
||||
await page.evaluate(() => window.jassub.setTrack(window.hybridProbe.matched))
|
||||
}
|
||||
await expect.poll(() => page.evaluate(() => window.hybridProbe.pixels().visible)).toBeGreaterThan(100)
|
||||
await expect.poll(() => page.evaluate(() => window.hybridProbe.pixels().signature)).not.toBe(before.signature)
|
||||
await page.evaluate(() => window.jassub.setTrack(window.hybridProbe.mismatched))
|
||||
if (hybridExpected)
|
||||
await expect.poll(() => page.evaluate(() => Boolean(window.jassub._ctx))).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.hybridProbe.pixels().visible)).toBeGreaterThan(100)
|
||||
const terminal = await page.evaluate(() => {
|
||||
const instance = window.jassub
|
||||
const canvas = instance._canvas
|
||||
instance.destroy()
|
||||
instance.setTrack(window.hybridProbe.matched)
|
||||
instance.setTrackByUrl('/unused-after-destroy.ass')
|
||||
window.art.destroy(false)
|
||||
return window.hybridProbe.terminal = { sameCanvas: canvas === instance._canvas, containers: document.querySelectorAll('.JASSUB').length }
|
||||
})
|
||||
expect(terminal).toEqual({ sameCanvas: true, containers: 0 })
|
||||
expect(await page.evaluate(() => window.hybridProbe.errors)).toEqual([])
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(() => {
|
||||
const probe = window.hybridProbe
|
||||
if (!probe)
|
||||
return null
|
||||
for (const event of probe.held) {
|
||||
for (const item of event.data.images) item.image?.close?.()
|
||||
}
|
||||
return { available: probe.available, selected: probe.selected, matrix: probe.matrix, sent: probe.sent, received: probe.received, late: probe.late, terminal: probe.terminal, errors: probe.errors, busy: window.jassub.busy, lastDemand: window.jassub._lastDemandTime }
|
||||
})
|
||||
await testInfo.attach('jassub-native-hybrid', { body: JSON.stringify({ artifact, sha256: hash(code), state, limitation: 'Actual Worker/WASM/ASS color-space transition and native ImageBitmap; one received render is held and delivered after synchronous public setTrack to reproduce a queued message. Not unmodified event timing, GPU endurance or physical-device evidence.' }), contentType: 'application/json' })
|
||||
await page.evaluate(() => {
|
||||
if (!window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -110,6 +110,12 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
...(canvas ? { canvas } : {}),
|
||||
}))
|
||||
window.jassub = window.art.plugins.artplayerPluginJassub.instance
|
||||
window.jassubNative.renderCalls = []
|
||||
const render = window.jassub._render
|
||||
window.jassub._render = function (message) {
|
||||
window.jassubNative.renderCalls.push({ context: Boolean(this._ctx), images: message.images.length })
|
||||
return render.call(this, message)
|
||||
}
|
||||
window.jassubNative.offscreen = { available: 'transferControlToOffscreen' in HTMLCanvasElement.prototype, selected: window.jassub._offscreenRender }
|
||||
window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error)))
|
||||
await new Promise((resolve, reject) => {
|
||||
|
||||
@@ -67,3 +67,80 @@ for (const option of [{ offscreenRender: false }, 'unsupported', 'custom']) {
|
||||
instance.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
test('JASSUB late hybrid render releases transferred bitmaps without drawing or unlocking the new offscreen demand', async () => {
|
||||
const env = environment()
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance._detachOffscreen()
|
||||
await env.flush()
|
||||
instance.setTrack('[Script Info]')
|
||||
instance.busy = true
|
||||
let closed = 0
|
||||
const image = { close() {
|
||||
closed++
|
||||
} }
|
||||
const frame = { target: 'render', images: [{ image, x: 0, y: 0 }], asyncRender: true, width: 640, height: 360 }
|
||||
assert.doesNotThrow(() => env.workers[0].onmessage({ data: frame }))
|
||||
assert.equal(closed, 1)
|
||||
assert.equal(instance.busy, true)
|
||||
assert.equal(instance._ctx, false)
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test('JASSUB current hybrid render still draws and releases its actual bitmap', async () => {
|
||||
const env = environment()
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance._detachOffscreen()
|
||||
await env.flush()
|
||||
const drawn = []
|
||||
let closed = 0
|
||||
const image = { close() {
|
||||
closed++
|
||||
} }
|
||||
instance._ctx.drawImage = (...args) => drawn.push(args)
|
||||
instance.busy = true
|
||||
env.workers[0].onmessage({ data: { target: 'render', images: [{ image, x: 4, y: 5 }], asyncRender: true, width: 640, height: 360 } })
|
||||
assert.deepEqual(drawn, [[image, 4, 5]])
|
||||
assert.equal(closed, 1)
|
||||
assert.equal(instance.busy, false)
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test('JASSUB reattachment replaces a pending hybrid demand with a forced current offscreen draw', async () => {
|
||||
const env = environment()
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance._detachOffscreen()
|
||||
await env.flush()
|
||||
instance.busy = true
|
||||
instance._lastDemandTime = { mediaTime: 1, width: 640, height: 360 }
|
||||
instance.setTrack('[Script Info]')
|
||||
assert.equal(instance._lastDemandTime, null)
|
||||
assert.equal(instance.busy, true)
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.at(-1).target, 'canvas')
|
||||
assert.equal(env.workers[0].messages.at(-1).force, true)
|
||||
env.workers[0].onmessage({ data: { target: 'unbusy' } })
|
||||
assert.equal(instance.busy, false)
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
for (const method of ['setTrack', 'setTrackByUrl']) {
|
||||
test(`JASSUB ${method} after hybrid destruction does not recreate or transfer a canvas`, async () => {
|
||||
const env = environment()
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance._detachOffscreen()
|
||||
await env.flush()
|
||||
instance.destroy()
|
||||
const transferred = env.transferred.length
|
||||
const canvas = instance._canvas
|
||||
assert.equal(instance[method](method === 'setTrack' ? '[Script Info]' : '/next.ass'), undefined)
|
||||
await env.flush()
|
||||
assert.equal(env.transferred.length, transferred)
|
||||
assert.equal(instance._canvas, canvas)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user