From efd9f42fb984302dd061d5cd9f96bd4de74fcccf Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 04:25:22 +0800 Subject: [PATCH] refactor(thumbnail): [PKG-TOOL-THUMB-03] own extraction jobs and source cancellation --- docs/compiled/artplayer-tool-thumbnail.js | 2 +- .../artplayer-tool-thumbnail.legacy.js | 2 +- docs/compiled/artplayer-tool-thumbnail.mjs | 519 +- package.json | 4 +- .../artplayer-tool-thumbnail/ARCHITECTURE.md | 58 +- .../dist/artplayer-tool-thumbnail.js | 2 +- .../dist/artplayer-tool-thumbnail.legacy.js | 2 +- .../dist/artplayer-tool-thumbnail.mjs | 519 +- .../src/extraction.js | 243 + .../artplayer-tool-thumbnail/src/index.js | 100 +- .../artplayer-tool-thumbnail/src/input.js | 11 + .../artplayer-tool-thumbnail/src/lifecycle.js | 80 + .../artplayer-tool-thumbnail/src/source.js | 113 + .../thumbnail-lifecycle-validation.json | 7406 +++++++++++++++++ .../2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md | 88 + refactor/plan.md | 6 +- refactor/progress.md | 14 + refactor/risk-table.md | 2 +- refactor/risks.json | 15 +- refactor/tasks.json | 6 +- test/browser/thumbnail-tool.spec.js | 94 +- test/thumbnail-lifecycle.test.js | 439 + 22 files changed, 9430 insertions(+), 295 deletions(-) create mode 100644 packages/artplayer-tool-thumbnail/src/extraction.js create mode 100644 packages/artplayer-tool-thumbnail/src/lifecycle.js create mode 100644 packages/artplayer-tool-thumbnail/src/source.js create mode 100644 refactor/baselines/thumbnail-lifecycle-validation.json create mode 100644 refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md create mode 100644 test/thumbnail-lifecycle.test.js diff --git a/docs/compiled/artplayer-tool-thumbnail.js b/docs/compiled/artplayer-tool-thumbnail.js index 32c840117..25c99402c 100644 --- a/docs/compiled/artplayer-tool-thumbnail.js +++ b/docs/compiled/artplayer-tool-thumbnail.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.ArtplayerToolThumbnail=e(),define(function(){return t.ArtplayerToolThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).ArtplayerToolThumbnail=e()}(this,function(){"use strict";class t{on(t,e,i){const n=this.e||(this.e={});return(n[t]||(n[t]=[])).push({fn:e,ctx:i}),this}once(t,e,i){const n=this;function o(...s){n.off(t,o),e.apply(i,s)}return o._=e,this.on(t,o,i)}emit(t,...e){const i=((this.e||(this.e={}))[t]||[]).slice();for(let n=0;nsetTimeout(e,t))).then(()=>this.start());var t;const{width:i,number:n,begin:o,end:s}=this.option,r=this.video.videoHeight/this.video.videoWidth*i;this.option.height=r,this.option.begin=e(o,0,this.video.duration),this.option.end=e(s||this.video.duration,o,this.video.duration),this.errorHandle(this.option.end>this.option.begin,"End time must be greater than the start time"),this.duration=this.option.end-this.option.begin,this.density=n/this.duration,this.errorHandle(this.file&&this.video,"Please select the video file first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment..."),this.errorHandle(this.density<=1,`The preview density cannot be greater than 1, but got ${this.density}`);const h=this.creatScreenshotDate(),a=this.creatCanvas(),l=a.getContext("2d");this.emit("canvas",a);const c=h.map((t,e)=>()=>new Promise(o=>{this.video.oncanplay=()=>{l.drawImage(this.video,t.x,t.y,i,r),a.toBlob(t=>{this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),this.thumbnailUrl=URL.createObjectURL(t),this.emit("update",this.thumbnailUrl,(e+1)/n),this.video.oncanplay=null,o()})},this.video.currentTime=t.time}));return this.processing=!0,(d=c,d.reduce((t,e)=>t.then(e),Promise.resolve())).then(()=>{this.processing=!1,this.emit("done")}).catch(t=>{throw this.processing=!1,this.emit("error",t.message),t});var d}creatScreenshotDate(){return function(t,e){const{number:i,width:n,height:o,column:s,begin:r}=t,h=e/i,a=[r+h];for(;a.length({time:t-h/2,x:e%s*n,y:Math.floor(e/s)*o}))}(this.option,this.duration)}creatCanvas(){return function(t){const{number:e,width:i,height:n,column:o}=t,s=document.createElement("canvas"),r=s.getContext("2d");return s.width=i*o,s.height=Math.ceil(e/o)*n+30,r.fillStyle="black",r.fillRect(0,0,s.width,s.height),r.font="14px Georgia",r.fillStyle="#fff",r.fillText(`From: https://artplayer.org/, Number: ${e}, Width: ${i}, Height: ${n}, Column: ${o}`,10,s.height-11),s}(this.option)}download(){this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const t=function(t,e){const i=document.createElement("a"),n=`${function(t){const e=t.split(".");return e.pop(),e.join(".")}(t.name)}.png`;i.download=n,i.href=e;try{document.body.appendChild(i),i.click()}finally{i.parentNode&&i.parentNode.removeChild(i)}return n}(this.file,this.thumbnailUrl);return this.emit("download",t),this}errorHandle(t,e){if(!t)throw this.emit("error",e),new Error(e)}destroy(){if(r.has(this))return;let t;r.add(this);for(const i of[()=>s(this),()=>this.video.parentNode?.removeChild(this.video),()=>this.videoUrl&&URL.revokeObjectURL(this.videoUrl),()=>this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),()=>this.emit("destroy")])try{i()}catch(e){t||(t=e)}if(t)throw t}}return h}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this;function o(...i){r.off(e,o),t.apply(n,i)}return o._=t,this.on(e,o,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;rn.pause?.(),()=>n.removeAttribute?.("src"),()=>n.load?.(),()=>n.parentNode?.removeChild(n));i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function a(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function l(e,t){let s=t.epoch;const c=e.video;let l,u,d,h,p=!1,f=!1,m=()=>{};const b=[],y=new Promise((e,t)=>{l=e,u=t});y.catch(()=>{});const g=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function v(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{v()}catch(r){t||(t=r)}if(n)try{e.emit("error",t?.message)}catch(o){t=o}u(t)}}function U(t){if(!g())return;f=!0,e.processing=!0;let r=0;const i=()=>{if(!g())return;if(r===t.points.length)return void function(){if(g())try{v(),e.emit("done"),l()}catch(t){let r=t;try{e.emit("error",t?.message)}catch(n){r=n}u(r)}}();const s=t.points[r];let a=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!g()||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,g);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}catch(c){w(c)}}})}catch(l){w(l)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(i)}function E(n=!1){if(g()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,l=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=a(i,0,n.duration),e.option.end=a(s||n.duration,i,n.duration),l(e.option.end>e.option.begin,"End time must be greater than the start time"),l(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,l(e.file&&n,"Please select the video file first"),l(!e.processing,"There is currently a task in progress, please wait a moment..."),l(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),g()||clearTimeout(d)}h={video:c,promise:y,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{g()&&w(new Error(`Unable to load video: media error ${c.error?.code||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeEventListener(e,t)),c.addEventListener(e,t),!g())return i([()=>c.removeEventListener(e,t),...b.splice(0)]),y;c.error?w(new Error(`Unable to load video: media error ${c.error.code}`)):E(!0)}catch(j){throw w(j,!1),j}return y}const u=new WeakMap;function d(e){let t;for(const[r,o]of e.listeners.splice(0))try{e.input.removeEventListener(r,o)}catch(n){t||(t=n)}if(e.wrapper){try{e.input.parentNode&&e.input.parentNode.removeChild(e.input)}catch(n){t||(t=n)}"relative"===e.wrapper.style.position&&(e.wrapper.style.position=e.position)}if(t)throw t}function h(e,t){try{for(const[n,r]of t)e.listeners.push([n,r]),e.input.addEventListener(n,r)}catch(n){try{d(e)}catch{}throw n}}function p(e){const t=u.get(e);t&&(u.delete(e),d(t))}class f extends e{constructor(e={}){super(),this.processing=!1,this.option={};try{this.setup(Object.assign({},f.DEFAULTS,e)),this.video=f.creatVideo(),n(this).video=this.video,this.duration=0,this.inputChange=this.inputChange.bind(this),this.ondrop=this.ondrop.bind(this),function(e,t){const n=u.get(e);n.callbacks=[["change",e.inputChange],["dragover",t],["drop",e.ondrop]],h(n,n.callbacks)}(this,f.ondragover)}catch(t){s(this,"construction failed");try{p(this)}catch{}try{c(this)}catch{}throw t}}static get DEFAULTS(){return{number:60,width:160,height:90,column:10,begin:0,end:Number.NaN}}static ondragover(e){e.preventDefault()}ondrop(e){if(n(this).closed)return;e.preventDefault();const t=e.dataTransfer.files[0];this.loadVideo(t)}setup(e={}){return n(this).closed||function(e,t){const o=n(e),i=++o.inputEpoch,s=()=>!o.closed&&o.inputEpoch===i,c=Object.assign({},e.option,t),l=c.fileInput;e.errorHandle(l instanceof Element,"The 'fileInput' is not a Element");for(const n of["number","width","column","begin","end"])e.errorHandle("number"===typeof c[n],`The '${n}' is not a number`);if(c.number=a(c.number,10,1e3),c.width=a(c.width,10,1e3),c.column=a(c.column,1,1e3),!s())throw r("input setup superseded");const p=u.get(e);if(p&&(l===p.input||l===p.wrapper))return c.fileInput=p.input,e.option=c,c;const f={input:l,wrapper:null,position:"",listeners:[],callbacks:p?.callbacks||null};try{if("INPUT"===l.tagName&&"file"===l.type||(f.wrapper=l,f.position=l.style.position,f.input=document.createElement("input"),f.input.type="file",Object.assign(f.input.style,{position:"absolute",width:"100%",height:"100%",left:"0",top:"0",right:"0",bottom:"0",opacity:"0"}),l.style.position="relative",l.appendChild(f.input)),!s())throw r("input setup superseded");if(f.callbacks&&h(f,f.callbacks),!s())throw r("input setup superseded")}catch(m){try{d(f)}catch{}throw m}c.fileInput=f.input,u.set(e,f),e.option=c,p&&d(p)}(this,e),this}static creatVideo(){const e=document.createElement("video");e.style.position="absolute",e.style.top="-9999px",e.style.left="-9999px",e.muted=!0,e.controls=!0;try{document.body.appendChild(e)}catch(t){throw e.parentNode&&e.parentNode.removeChild(e),t}return e}inputChange(e){if(n(this).closed)return;const t=this.option.fileInput.files[0];this.loadVideo(t),e.target.value=""}loadVideo(e){!function(e,t){const r=n(e);if(!t||r.closed)return;const s=r.epoch,c=e.video,a=c.canPlayType(t.type);if(e.errorHandle("maybe"===a||"probably"===a,`Playback of this file format is not supported: ${t.type}`),r.closed||r.epoch!==s)return;const l=URL.createObjectURL(t);if(r.sourceUrls.add(l),r.closed||r.epoch!==s)return void o(r.sourceUrls,l);const u=++r.epoch;!r.sourceUrl&&!e.file&&r.job?.adopt(u)||r.job?.cancel("source changed"),r.loading=!0;const d=()=>!r.closed&&r.epoch===u,h=r.sourceListeners,p=[];r.sourceListeners=p;let f=()=>{},m=!1;const b=()=>{if(!d()||m||c.currentSrc&&c.currentSrc!==l)return;m=!0,r.loading=!1;const t=new Error(`Unable to load video: media error ${c.error?.code||0}`);r.job?r.job.fail(t):e.emit("error",t.message)},y=()=>{!d()||c.currentSrc&&c.currentSrc!==l||(r.loading=!1,r.job?.ready())};try{if(i(h.splice(0)),!d())return void o(r.sourceUrls,l);for(const[e,t]of[["error",b],["loadedmetadata",y]])if(f=()=>c.removeEventListener(e,t),p.push(f),c.addEventListener(e,t),!d())return i([f,...p.splice(0)]),void o(r.sourceUrls,l);if(e.videoUrl=l,e.file=t,e.emit("file",t),!d())return void o(r.sourceUrls,l);if(c.src=l,!d())return void o(r.sourceUrls,l);r.sourceUrl=l;for(const e of[...r.sourceUrls])e!==l&&o(r.sourceUrls,e);d()&&e.emit("video",c)}catch(g){d()&&(r.loading=!1,r.job?.cancel("source setup failed"));try{i([f,...p.splice(0)])}catch{}throw o(r.sourceUrls,l),g}}(this,e)}start(){return function(e){const t=n(e);if(t.closed){const e=Promise.reject(r("destroyed"));return e.catch(()=>{}),e}return e.errorHandle(!t.job,"There is currently a task in progress, please wait a moment..."),l(e,t)}(this)}creatScreenshotDate(){return function(e,t){const{number:n,width:r,height:o,column:i,begin:s}=e,c=t/n,a=[s+c];for(;a.length({time:e-c/2,x:t%i*r,y:Math.floor(t/i)*o}))}(this.option,this.duration)}creatCanvas(){return function(e){const{number:t,width:n,height:r,column:o}=e,i=document.createElement("canvas"),s=i.getContext("2d");return i.width=n*o,i.height=Math.ceil(t/o)*r+30,s.fillStyle="black",s.fillRect(0,0,i.width,i.height),s.font="14px Georgia",s.fillStyle="#fff",s.fillText(`From: https://artplayer.org/, Number: ${t}, Width: ${n}, Height: ${r}, Column: ${o}`,10,i.height-11),i}(this.option)}download(){if(n(this).closed)return this;this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const e=function(e,t){const n=document.createElement("a"),r=`${function(e){const t=e.split(".");return t.pop(),t.join(".")}(e.name)}.png`;n.download=r,n.href=t;try{document.body.appendChild(n),n.click()}finally{n.parentNode&&n.parentNode.removeChild(n)}return r}(this.file,this.thumbnailUrl);return this.emit("download",e),this}errorHandle(e,t){if(!e)throw this.emit("error",t),new Error(t)}destroy(){s(this)&&i([()=>p(this),()=>c(this),()=>this.emit("destroy")])}}return f}); diff --git a/docs/compiled/artplayer-tool-thumbnail.legacy.js b/docs/compiled/artplayer-tool-thumbnail.legacy.js index 42395657e..543a7a290 100644 --- a/docs/compiled/artplayer-tool-thumbnail.legacy.js +++ b/docs/compiled/artplayer-tool-thumbnail.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.ArtplayerToolThumbnail=e(),define(function(){return t.ArtplayerToolThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).ArtplayerToolThumbnail=e()}(this,function(){"use strict";class t{on(t,e,i){const n=this.e||(this.e={});return(n[t]||(n[t]=[])).push({fn:e,ctx:i}),this}once(t,e,i){const n=this;function o(...s){n.off(t,o),e.apply(i,s)}return o._=e,this.on(t,o,i)}emit(t,...e){const i=((this.e||(this.e={}))[t]||[]).slice();for(let n=0;nsetTimeout(e,t))).then(()=>this.start());var t;const{width:i,number:n,begin:o,end:s}=this.option,r=this.video.videoHeight/this.video.videoWidth*i;this.option.height=r,this.option.begin=e(o,0,this.video.duration),this.option.end=e(s||this.video.duration,o,this.video.duration),this.errorHandle(this.option.end>this.option.begin,"End time must be greater than the start time"),this.duration=this.option.end-this.option.begin,this.density=n/this.duration,this.errorHandle(this.file&&this.video,"Please select the video file first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment..."),this.errorHandle(this.density<=1,`The preview density cannot be greater than 1, but got ${this.density}`);const h=this.creatScreenshotDate(),a=this.creatCanvas(),l=a.getContext("2d");this.emit("canvas",a);const c=h.map((t,e)=>()=>new Promise(o=>{this.video.oncanplay=()=>{l.drawImage(this.video,t.x,t.y,i,r),a.toBlob(t=>{this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),this.thumbnailUrl=URL.createObjectURL(t),this.emit("update",this.thumbnailUrl,(e+1)/n),this.video.oncanplay=null,o()})},this.video.currentTime=t.time}));return this.processing=!0,(d=c,d.reduce((t,e)=>t.then(e),Promise.resolve())).then(()=>{this.processing=!1,this.emit("done")}).catch(t=>{throw this.processing=!1,this.emit("error",t.message),t});var d}creatScreenshotDate(){return function(t,e){const{number:i,width:n,height:o,column:s,begin:r}=t,h=e/i,a=[r+h];for(;a.length({time:t-h/2,x:e%s*n,y:Math.floor(e/s)*o}))}(this.option,this.duration)}creatCanvas(){return function(t){const{number:e,width:i,height:n,column:o}=t,s=document.createElement("canvas"),r=s.getContext("2d");return s.width=i*o,s.height=Math.ceil(e/o)*n+30,r.fillStyle="black",r.fillRect(0,0,s.width,s.height),r.font="14px Georgia",r.fillStyle="#fff",r.fillText(`From: https://artplayer.org/, Number: ${e}, Width: ${i}, Height: ${n}, Column: ${o}`,10,s.height-11),s}(this.option)}download(){this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const t=function(t,e){const i=document.createElement("a"),n=`${function(t){const e=t.split(".");return e.pop(),e.join(".")}(t.name)}.png`;i.download=n,i.href=e;try{document.body.appendChild(i),i.click()}finally{i.parentNode&&i.parentNode.removeChild(i)}return n}(this.file,this.thumbnailUrl);return this.emit("download",t),this}errorHandle(t,e){if(!t)throw this.emit("error",e),new Error(e)}destroy(){if(r.has(this))return;let t;r.add(this);for(const i of[()=>s(this),()=>{var t;return null==(t=this.video.parentNode)?void 0:t.removeChild(this.video)},()=>this.videoUrl&&URL.revokeObjectURL(this.videoUrl),()=>this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),()=>this.emit("destroy")])try{i()}catch(e){t||(t=e)}if(t)throw t}}return h}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this;function o(...i){r.off(e,o),t.apply(n,i)}return o._=t,this.on(e,o,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;r{var e;return null==(e=n.pause)?void 0:e.call(n)},()=>{var e;return null==(e=n.removeAttribute)?void 0:e.call(n,"src")},()=>{var e;return null==(e=n.load)?void 0:e.call(n)},()=>{var e;return null==(e=n.parentNode)?void 0:e.removeChild(n)});i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function l(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function a(e,t){let s=t.epoch;const c=e.video;let a,u,d,h,p=!1,f=!1,m=()=>{};const b=[],v=new Promise((e,t)=>{a=e,u=t});v.catch(()=>{});const y=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function g(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{g()}catch(r){t||(t=r)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(o){t=o}u(t)}}function U(t){if(!y())return;f=!0,e.processing=!0;let r=0;const i=()=>{if(!y())return;if(r===t.points.length)return void function(){if(y())try{g(),e.emit("done"),a()}catch(t){let r=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){r=n}u(r)}}();const s=t.points[r];let l=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!y()||l||c.seeking||void 0!==c.readyState&&c.readyState<2)){l=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!y())return;t.canvas.toBlob(s=>{if(y()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,y);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}catch(c){w(c)}}})}catch(a){w(a)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(i)}function E(n=!1){if(y()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,a=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=l(i,0,n.duration),e.option.end=l(s||n.duration,i,n.duration),a(e.option.end>e.option.begin,"End time must be greater than the start time"),a(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,a(e.file&&n,"Please select the video file first"),a(!e.processing,"There is currently a task in progress, please wait a moment..."),a(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),y()||clearTimeout(d)}h={video:c,promise:v,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{var e;y()&&w(new Error(`Unable to load video: media error ${(null==(e=c.error)?void 0:e.code)||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeEventListener(e,t)),c.addEventListener(e,t),!y())return i([()=>c.removeEventListener(e,t),...b.splice(0)]),v;c.error?w(new Error(`Unable to load video: media error ${c.error.code}`)):E(!0)}catch(j){throw w(j,!1),j}return v}const u=new WeakMap;function d(e){let t;for(const[r,o]of e.listeners.splice(0))try{e.input.removeEventListener(r,o)}catch(n){t||(t=n)}if(e.wrapper){try{e.input.parentNode&&e.input.parentNode.removeChild(e.input)}catch(n){t||(t=n)}"relative"===e.wrapper.style.position&&(e.wrapper.style.position=e.position)}if(t)throw t}function h(e,t){try{for(const[n,r]of t)e.listeners.push([n,r]),e.input.addEventListener(n,r)}catch(n){try{d(e)}catch(r){}throw n}}function p(e){const t=u.get(e);t&&(u.delete(e),d(t))}class f extends e{constructor(e={}){super(),this.processing=!1,this.option={};try{this.setup(Object.assign({},f.DEFAULTS,e)),this.video=f.creatVideo(),n(this).video=this.video,this.duration=0,this.inputChange=this.inputChange.bind(this),this.ondrop=this.ondrop.bind(this),function(e,t){const n=u.get(e);n.callbacks=[["change",e.inputChange],["dragover",t],["drop",e.ondrop]],h(n,n.callbacks)}(this,f.ondragover)}catch(t){s(this,"construction failed");try{p(this)}catch(r){}try{c(this)}catch(r){}throw t}}static get DEFAULTS(){return{number:60,width:160,height:90,column:10,begin:0,end:Number.NaN}}static ondragover(e){e.preventDefault()}ondrop(e){if(n(this).closed)return;e.preventDefault();const t=e.dataTransfer.files[0];this.loadVideo(t)}setup(e={}){return n(this).closed||function(e,t){const o=n(e),i=++o.inputEpoch,s=()=>!o.closed&&o.inputEpoch===i,c=Object.assign({},e.option,t),a=c.fileInput;e.errorHandle(a instanceof Element,"The 'fileInput' is not a Element");for(const n of["number","width","column","begin","end"])e.errorHandle("number"===typeof c[n],`The '${n}' is not a number`);if(c.number=l(c.number,10,1e3),c.width=l(c.width,10,1e3),c.column=l(c.column,1,1e3),!s())throw r("input setup superseded");const p=u.get(e);if(p&&(a===p.input||a===p.wrapper))return c.fileInput=p.input,e.option=c,c;const f={input:a,wrapper:null,position:"",listeners:[],callbacks:(null==p?void 0:p.callbacks)||null};try{if("INPUT"===a.tagName&&"file"===a.type||(f.wrapper=a,f.position=a.style.position,f.input=document.createElement("input"),f.input.type="file",Object.assign(f.input.style,{position:"absolute",width:"100%",height:"100%",left:"0",top:"0",right:"0",bottom:"0",opacity:"0"}),a.style.position="relative",a.appendChild(f.input)),!s())throw r("input setup superseded");if(f.callbacks&&h(f,f.callbacks),!s())throw r("input setup superseded")}catch(m){try{d(f)}catch(b){}throw m}c.fileInput=f.input,u.set(e,f),e.option=c,p&&d(p)}(this,e),this}static creatVideo(){const e=document.createElement("video");e.style.position="absolute",e.style.top="-9999px",e.style.left="-9999px",e.muted=!0,e.controls=!0;try{document.body.appendChild(e)}catch(t){throw e.parentNode&&e.parentNode.removeChild(e),t}return e}inputChange(e){if(n(this).closed)return;const t=this.option.fileInput.files[0];this.loadVideo(t),e.target.value=""}loadVideo(e){!function(e,t){var r,s,c;const l=n(e);if(!t||l.closed)return;const a=l.epoch,u=e.video,d=u.canPlayType(t.type);if(e.errorHandle("maybe"===d||"probably"===d,`Playback of this file format is not supported: ${t.type}`),l.closed||l.epoch!==a)return;const h=URL.createObjectURL(t);if(l.sourceUrls.add(h),l.closed||l.epoch!==a)return void o(l.sourceUrls,h);const p=++l.epoch;!l.sourceUrl&&!e.file&&(null==(r=l.job)?void 0:r.adopt(p))||null==(s=l.job)||s.cancel("source changed"),l.loading=!0;const f=()=>!l.closed&&l.epoch===p,m=l.sourceListeners,b=[];l.sourceListeners=b;let v=()=>{},y=!1;const g=()=>{var t;if(!f()||y||u.currentSrc&&u.currentSrc!==h)return;y=!0,l.loading=!1;const n=new Error(`Unable to load video: media error ${(null==(t=u.error)?void 0:t.code)||0}`);l.job?l.job.fail(n):e.emit("error",n.message)},w=()=>{var e;!f()||u.currentSrc&&u.currentSrc!==h||(l.loading=!1,null==(e=l.job)||e.ready())};try{if(i(m.splice(0)),!f())return void o(l.sourceUrls,h);for(const[e,t]of[["error",g],["loadedmetadata",w]])if(v=()=>u.removeEventListener(e,t),b.push(v),u.addEventListener(e,t),!f())return i([v,...b.splice(0)]),void o(l.sourceUrls,h);if(e.videoUrl=h,e.file=t,e.emit("file",t),!f())return void o(l.sourceUrls,h);if(u.src=h,!f())return void o(l.sourceUrls,h);l.sourceUrl=h;for(const e of[...l.sourceUrls])e!==h&&o(l.sourceUrls,e);f()&&e.emit("video",u)}catch(U){f()&&(l.loading=!1,null==(c=l.job)||c.cancel("source setup failed"));try{i([v,...b.splice(0)])}catch(E){}throw o(l.sourceUrls,h),U}}(this,e)}start(){return function(e){const t=n(e);if(t.closed){const e=Promise.reject(r("destroyed"));return e.catch(()=>{}),e}return e.errorHandle(!t.job,"There is currently a task in progress, please wait a moment..."),a(e,t)}(this)}creatScreenshotDate(){return function(e,t){const{number:n,width:r,height:o,column:i,begin:s}=e,c=t/n,l=[s+c];for(;l.length({time:e-c/2,x:t%i*r,y:Math.floor(t/i)*o}))}(this.option,this.duration)}creatCanvas(){return function(e){const{number:t,width:n,height:r,column:o}=e,i=document.createElement("canvas"),s=i.getContext("2d");return i.width=n*o,i.height=Math.ceil(t/o)*r+30,s.fillStyle="black",s.fillRect(0,0,i.width,i.height),s.font="14px Georgia",s.fillStyle="#fff",s.fillText(`From: https://artplayer.org/, Number: ${t}, Width: ${n}, Height: ${r}, Column: ${o}`,10,i.height-11),i}(this.option)}download(){if(n(this).closed)return this;this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const e=function(e,t){const n=document.createElement("a"),r=`${function(e){const t=e.split(".");return t.pop(),t.join(".")}(e.name)}.png`;n.download=r,n.href=t;try{document.body.appendChild(n),n.click()}finally{n.parentNode&&n.parentNode.removeChild(n)}return r}(this.file,this.thumbnailUrl);return this.emit("download",e),this}errorHandle(e,t){if(!e)throw this.emit("error",t),new Error(t)}destroy(){s(this)&&i([()=>p(this),()=>c(this),()=>this.emit("destroy")])}}return f}); diff --git a/docs/compiled/artplayer-tool-thumbnail.mjs b/docs/compiled/artplayer-tool-thumbnail.mjs index 4d0159c08..e5248b941 100644 --- a/docs/compiled/artplayer-tool-thumbnail.mjs +++ b/docs/compiled/artplayer-tool-thumbnail.mjs @@ -44,11 +44,188 @@ class Emitter { return this; } } -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); +const states = /* @__PURE__ */ new WeakMap(); +function stateFor(tool) { + if (!states.has(tool)) { + states.set(tool, { + closed: false, + epoch: 0, + inputEpoch: 0, + job: null, + video: null, + sourceUrl: null, + thumbnailUrl: null, + sourceUrls: /* @__PURE__ */ new Set(), + thumbnailUrls: /* @__PURE__ */ new Set(), + sourceListeners: [], + loading: false + }); + } + return states.get(tool); } -function runPromisesInSeries(ps) { - return ps.reduce((p, next) => p.then(next), Promise.resolve()); +function cancellation(reason) { + const error = new Error(`Thumbnail task cancelled: ${reason}`); + error.name = "AbortError"; + return error; +} +function revoke(urls, url) { + if (!url) + return; + urls.delete(url); + URL.revokeObjectURL(url); +} +function cleanupAll(callbacks) { + let failure; + for (const callback of callbacks) { + try { + callback(); + } catch (error) { + failure || (failure = error); + } + } + if (failure) + throw failure; +} +function closeState(tool, reason = "destroyed") { + const state = stateFor(tool); + if (state.closed) + return false; + state.closed = true; + state.epoch++; + state.job?.cancel(reason); + return true; +} +function releaseMedia(tool) { + const state = stateFor(tool); + const sources = /* @__PURE__ */ new Set([...state.sourceUrls, tool.videoUrl]); + const thumbnails = /* @__PURE__ */ new Set([...state.thumbnailUrls, tool.thumbnailUrl]); + const media = []; + for (const video of /* @__PURE__ */ new Set([state.video, tool.video])) { + if (video) { + media.push( + () => video.pause?.(), + () => video.removeAttribute?.("src"), + () => video.load?.(), + () => video.parentNode?.removeChild(video) + ); + } + } + cleanupAll([ + ...state.sourceListeners.splice(0), + ...media, + ...[...sources].map((url) => () => revoke(state.sourceUrls, url)), + ...[...thumbnails].map((url) => () => revoke(state.thumbnailUrls, url)) + ]); +} +function loadSource(tool, file) { + const state = stateFor(tool); + if (!file || state.closed) + return; + const initialEpoch = state.epoch; + const video = tool.video; + const support = video.canPlayType(file.type); + tool.errorHandle(support === "maybe" || support === "probably", `Playback of this file format is not supported: ${file.type}`); + if (state.closed || state.epoch !== initialEpoch) + return; + const url = URL.createObjectURL(file); + state.sourceUrls.add(url); + if (state.closed || state.epoch !== initialEpoch) { + revoke(state.sourceUrls, url); + return; + } + const epoch = ++state.epoch; + const initialWait = !state.sourceUrl && !tool.file && state.job?.adopt(epoch); + if (!initialWait) + state.job?.cancel("source changed"); + state.loading = true; + const current = () => !state.closed && state.epoch === epoch; + const previousListeners = state.sourceListeners; + const listeners = []; + state.sourceListeners = listeners; + let pendingCleanup = () => { + }; + let reported = false; + const error = () => { + if (!current() || reported || video.currentSrc && video.currentSrc !== url) + return; + reported = true; + state.loading = false; + const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`); + if (state.job) + state.job.fail(failure); + else + tool.emit("error", failure.message); + }; + const ready = () => { + if (current() && (!video.currentSrc || video.currentSrc === url)) { + state.loading = false; + state.job?.ready(); + } + }; + try { + cleanupAll(previousListeners.splice(0)); + if (!current()) { + revoke(state.sourceUrls, url); + return; + } + for (const [name, callback] of [["error", error], ["loadedmetadata", ready]]) { + pendingCleanup = () => video.removeEventListener(name, callback); + listeners.push(pendingCleanup); + video.addEventListener(name, callback); + if (!current()) { + cleanupAll([pendingCleanup, ...listeners.splice(0)]); + revoke(state.sourceUrls, url); + return; + } + } + tool.videoUrl = url; + tool.file = file; + tool.emit("file", file); + if (!current()) { + revoke(state.sourceUrls, url); + return; + } + video.src = url; + if (!current()) { + revoke(state.sourceUrls, url); + return; + } + state.sourceUrl = url; + for (const previous of [...state.sourceUrls]) { + if (previous !== url) + revoke(state.sourceUrls, previous); + } + if (current()) + tool.emit("video", video); + } catch (error2) { + if (current()) { + state.loading = false; + state.job?.cancel("source setup failed"); + } + try { + cleanupAll([pendingCleanup, ...listeners.splice(0)]); + } catch { + } + revoke(state.sourceUrls, url); + throw error2; + } +} +function replaceThumbnail(tool, blob, live) { + const state = stateFor(tool); + const url = URL.createObjectURL(blob); + state.thumbnailUrls.add(url); + if (!live()) { + revoke(state.thumbnailUrls, url); + return null; + } + const previous = /* @__PURE__ */ new Set([...state.thumbnailUrls, tool.thumbnailUrl]); + state.thumbnailUrl = url; + tool.thumbnailUrl = url; + for (const old of previous) { + if (old && old !== url) + revoke(state.thumbnailUrls, old); + } + return live() ? url : null; } function getFileName(name) { const nameArray = name.split("."); @@ -58,6 +235,236 @@ function getFileName(name) { function clamp(num, a, b) { return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b)); } +function prepare(tool, job) { + const { video } = job; + const { width, number, begin, end } = tool.option; + const height = video.videoHeight / video.videoWidth * width; + const guard = (condition, message) => { + try { + tool.errorHandle(condition, message); + } catch (error) { + job.reported = error; + job.wasReported = true; + throw error; + } + }; + tool.option.height = height; + tool.option.begin = clamp(begin, 0, video.duration); + tool.option.end = clamp(end || video.duration, begin, video.duration); + guard(tool.option.end > tool.option.begin, "End time must be greater than the start time"); + guard(Number.isFinite(video.duration), "Video duration must be finite"); + tool.duration = tool.option.end - tool.option.begin; + tool.density = number / tool.duration; + guard(tool.file && video, "Please select the video file first"); + guard(!tool.processing, "There is currently a task in progress, please wait a moment..."); + guard(tool.density <= 1, `The preview density cannot be greater than 1, but got ${tool.density}`); + const points = tool.creatScreenshotDate(); + const canvas = tool.creatCanvas(); + const context = canvas.getContext("2d"); + tool.emit("canvas", canvas); + return { width, height, number, points, canvas, context }; +} +function createJob(tool, state) { + let epoch = state.epoch; + const video = tool.video; + let resolve; + let reject; + let timer; + let settled = false; + let running = false; + let job; + let frameCleanup = () => { + }; + const listeners = []; + const promise = new Promise((yes, no) => { + resolve = yes; + reject = no; + }); + promise.catch(() => { + }); + const live = () => !settled && !state.closed && state.epoch === epoch && state.job === job; + function finish() { + if (settled) + return false; + settled = true; + if (state.job === job) { + state.job = null; + tool.processing = false; + } + cleanupAll([() => clearTimeout(timer), frameCleanup, ...listeners.splice(0)]); + return true; + } + function fail(error, report = true) { + if (settled) + return; + try { + finish(); + } catch (cleanupError) { + error || (error = cleanupError); + } + if (report) { + try { + tool.emit("error", error?.message); + } catch (listenerError) { + error = listenerError; + } + } + reject(error); + } + function complete() { + if (!live()) + return; + try { + finish(); + tool.emit("done"); + resolve(); + } catch (error) { + let failure = error; + try { + tool.emit("error", error?.message); + } catch (listenerError) { + failure = listenerError; + } + reject(failure); + } + } + function extract(plan) { + if (!live()) + return; + running = true; + tool.processing = true; + let index = 0; + const next = () => { + if (!live()) + return; + if (index === plan.points.length) { + complete(); + return; + } + const point = plan.points[index]; + let drawing = false; + let encoded = false; + const previous = video.oncanplay; + const draw = () => { + if (!live() || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2) + return; + drawing = true; + try { + frameCleanup(); + plan.context.drawImage(video, point.x, point.y, plan.width, plan.height); + if (!live()) + return; + plan.canvas.toBlob((blob) => { + if (!live() || encoded) + return; + encoded = true; + try { + if (!blob) + throw new Error("Unable to create thumbnail image"); + const url = replaceThumbnail(tool, blob, live); + if (!url) + return; + tool.emit("update", url, (index + 1) / plan.number); + index++; + Promise.resolve().then(next); + } catch (error) { + fail(error); + } + }); + } catch (error) { + fail(error); + } + }; + frameCleanup = () => { + if (video.oncanplay === draw) + video.oncanplay = previous; + video.removeEventListener("seeked", draw); + }; + try { + video.oncanplay = draw; + video.addEventListener("seeked", draw); + video.currentTime = point.time; + if (video.readyState >= 2 && !video.seeking) + Promise.resolve().then(draw); + } catch (error) { + fail(error); + } + }; + Promise.resolve().then(next); + } + function ready(synchronous = false) { + if (!live() || running) + return; + clearTimeout(timer); + if (!state.loading && video.duration) { + running = true; + try { + const plan = prepare(tool, job); + extract(plan); + } catch (error) { + fail(error, !synchronous && !(job.wasReported && error === job.reported)); + if (synchronous) + throw error; + } + } else { + timer = setTimeout(() => ready(), 1e3); + if (!live()) + clearTimeout(timer); + } + } + job = { + video, + promise, + ready, + fail, + cancel(reason) { + fail(cancellation(reason), false); + }, + adopt(sourceEpoch) { + if (settled || running || video !== tool.video) + return false; + epoch = sourceEpoch; + return true; + } + }; + state.job = job; + try { + for (const [name, callback] of [ + ["error", () => { + if (live()) + fail(new Error(`Unable to load video: media error ${video.error?.code || 0}`)); + }], + ["loadedmetadata", () => ready()], + ["durationchange", () => ready()] + ]) { + listeners.push(() => video.removeEventListener(name, callback)); + video.addEventListener(name, callback); + if (!live()) { + cleanupAll([() => video.removeEventListener(name, callback), ...listeners.splice(0)]); + return promise; + } + } + if (video.error) + fail(new Error(`Unable to load video: media error ${video.error.code}`)); + else + ready(true); + } catch (error) { + fail(error, false); + throw error; + } + return promise; +} +function startExtraction(tool) { + const state = stateFor(tool); + if (state.closed) { + const promise = Promise.reject(cancellation("destroyed")); + promise.catch(() => { + }); + return promise; + } + tool.errorHandle(!state.job, "There is currently a task in progress, please wait a moment..."); + return createJob(tool, state); +} const inputs = /* @__PURE__ */ new WeakMap(); function release(record) { let failure; @@ -96,6 +503,9 @@ function connect(record, callbacks) { } } function setupInput(tool, patch) { + const state = stateFor(tool); + const epoch = ++state.inputEpoch; + const current = () => !state.closed && state.inputEpoch === epoch; const option = Object.assign({}, tool.option, patch); const target = option.fileInput; tool.errorHandle(target instanceof Element, "The 'fileInput' is not a Element"); @@ -104,9 +514,12 @@ function setupInput(tool, patch) { option.number = clamp(option.number, 10, 1e3); option.width = clamp(option.width, 10, 1e3); option.column = clamp(option.column, 1, 1e3); + if (!current()) + throw cancellation("input setup superseded"); const previous = inputs.get(tool); if (previous && (target === previous.input || target === previous.wrapper)) { option.fileInput = previous.input; + tool.option = option; return option; } const record = { input: target, wrapper: null, position: "", listeners: [], callbacks: previous?.callbacks || null }; @@ -120,8 +533,12 @@ function setupInput(tool, patch) { target.style.position = "relative"; target.appendChild(record.input); } + if (!current()) + throw cancellation("input setup superseded"); if (record.callbacks) connect(record, record.callbacks); + if (!current()) + throw cancellation("input setup superseded"); } catch (error) { try { release(record); @@ -193,7 +610,6 @@ function downloadSheet(file, url) { } return name; } -const destroyed = /* @__PURE__ */ new WeakSet(); class ArtplayerToolThumbnail extends Emitter { constructor(option = {}) { super(); @@ -202,18 +618,19 @@ class ArtplayerToolThumbnail extends Emitter { try { this.setup(Object.assign({}, ArtplayerToolThumbnail.DEFAULTS, option)); this.video = ArtplayerToolThumbnail.creatVideo(); + stateFor(this).video = this.video; this.duration = 0; this.inputChange = this.inputChange.bind(this); this.ondrop = this.ondrop.bind(this); connectInput(this, ArtplayerToolThumbnail.ondragover); } catch (error) { + closeState(this, "construction failed"); try { releaseInput(this); } catch { } try { - if (this.video?.parentNode) - this.video.parentNode.removeChild(this.video); + releaseMedia(this); } catch { } throw error; @@ -233,15 +650,15 @@ class ArtplayerToolThumbnail extends Emitter { event.preventDefault(); } ondrop(event) { - if (destroyed.has(this)) + if (stateFor(this).closed) return; event.preventDefault(); const file = event.dataTransfer.files[0]; this.loadVideo(file); } setup(option = {}) { - if (!destroyed.has(this)) - this.option = setupInput(this, option); + if (!stateFor(this).closed) + setupInput(this, option); return this; } static creatVideo() { @@ -261,71 +678,17 @@ class ArtplayerToolThumbnail extends Emitter { return video; } inputChange(event) { - if (destroyed.has(this)) + if (stateFor(this).closed) return; const file = this.option.fileInput.files[0]; this.loadVideo(file); event.target.value = ""; } loadVideo(file) { - if (file && !destroyed.has(this)) { - const canPlayType = this.video.canPlayType(file.type); - this.errorHandle( - canPlayType === "maybe" || canPlayType === "probably", - `Playback of this file format is not supported: ${file.type}` - ); - const videoUrl = URL.createObjectURL(file); - this.videoUrl = videoUrl; - this.file = file; - this.emit("file", this.file); - this.video.src = videoUrl; - this.emit("video", this.video); - } + loadSource(this, file); } start() { - if (!this.video.duration) - return sleep(1e3).then(() => this.start()); - const { width, number, begin, end } = this.option; - const height = this.video.videoHeight / this.video.videoWidth * width; - this.option.height = height; - this.option.begin = clamp(begin, 0, this.video.duration); - this.option.end = clamp(end || this.video.duration, begin, this.video.duration); - this.errorHandle(this.option.end > this.option.begin, `End time must be greater than the start time`); - this.duration = this.option.end - this.option.begin; - this.density = number / this.duration; - this.errorHandle(this.file && this.video, "Please select the video file first"); - this.errorHandle(!this.processing, "There is currently a task in progress, please wait a moment..."); - this.errorHandle(this.density <= 1, `The preview density cannot be greater than 1, but got ${this.density}`); - const screenshotDate = this.creatScreenshotDate(); - const canvas = this.creatCanvas(); - const context2D = canvas.getContext("2d"); - this.emit("canvas", canvas); - const promiseList = screenshotDate.map((item, index) => () => { - return new Promise((resolve) => { - this.video.oncanplay = () => { - context2D.drawImage(this.video, item.x, item.y, width, height); - canvas.toBlob((blob) => { - if (this.thumbnailUrl) { - URL.revokeObjectURL(this.thumbnailUrl); - } - this.thumbnailUrl = URL.createObjectURL(blob); - this.emit("update", this.thumbnailUrl, (index + 1) / number); - this.video.oncanplay = null; - resolve(); - }); - }; - this.video.currentTime = item.time; - }); - }); - this.processing = true; - return runPromisesInSeries(promiseList).then(() => { - this.processing = false; - this.emit("done"); - }).catch((err) => { - this.processing = false; - this.emit("error", err.message); - throw err; - }); + return startExtraction(this); } creatScreenshotDate() { return screenshotPoints(this.option, this.duration); @@ -334,6 +697,8 @@ class ArtplayerToolThumbnail extends Emitter { return createSheet(this.option); } download() { + if (stateFor(this).closed) + return this; this.errorHandle( this.file && this.thumbnailUrl, "Download does not seem to be ready, please create preview first" @@ -350,25 +715,13 @@ class ArtplayerToolThumbnail extends Emitter { } } destroy() { - if (destroyed.has(this)) + if (!closeState(this)) return; - destroyed.add(this); - let failure; - for (const cleanup of [ + cleanupAll([ () => releaseInput(this), - () => this.video.parentNode?.removeChild(this.video), - () => this.videoUrl && URL.revokeObjectURL(this.videoUrl), - () => this.thumbnailUrl && URL.revokeObjectURL(this.thumbnailUrl), + () => releaseMedia(this), () => this.emit("destroy") - ]) { - try { - cleanup(); - } catch (error) { - failure || (failure = error); - } - } - if (failure) - throw failure; + ]); } } export { diff --git a/package.json b/package.json index 9aeed4739..be52f4177 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js", "typecheck": "node scripts/typecheck.mjs", - "test:unit": "node --test 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:unit": "node --test 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:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs", "test": "yarn test:node && yarn test:baseline", "test:browser": "playwright test", @@ -83,7 +83,7 @@ "generate:media:hls": "node scripts/generate-hls-fixture.mjs", "test:iframe": "node --test test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe.test.js test/iframe-lifecycle.test.js refactor/scripts/iframe-contract.test.mjs refactor/scripts/iframe-types.test.mjs", "test:iframe-types-package": "node refactor/scripts/iframe-package-types.mjs", - "test:thumbnail": "node --test test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs" + "test:thumbnail": "node --test test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-tool-thumbnail/ARCHITECTURE.md b/packages/artplayer-tool-thumbnail/ARCHITECTURE.md index 0cbfd121b..48a43b522 100644 --- a/packages/artplayer-tool-thumbnail/ARCHITECTURE.md +++ b/packages/artplayer-tool-thumbnail/ARCHITECTURE.md @@ -1,19 +1,23 @@ # Thumbnail tool maintenance map -This package is being migrated under PKG-TOOL-THUMB-03. Input ownership and sheet -export are separated; extraction/source cancellation is still being implemented. -Strict TS and published declaration work belongs to PKG-TOOL-THUMB-04. Do not -infer complete migration or release readiness from this checkpoint. +The runtime responsibilities are separated under PKG-TOOL-THUMB-03. Strict TS and +published declaration work belongs to PKG-TOOL-THUMB-04. Runtime restructuring +does not imply complete migration or release readiness. | File | Responsibility | | --- | --- | -| src/index.js | Public class, historical method names, file/video events, current extraction loop and destruction entry | +| src/index.js | Public class, historical method names, construction and destruction entry | +| src/lifecycle.js | Private state, source/input generations, cancellation and resource release | +| src/source.js | File loading, native metadata/error listeners and source/thumbnail Blob URLs | +| src/extraction.js | Owned metadata wait and serial frame job, callback/error completion and cancellation | | src/input.js | Option validation/clamps, file-input wrapper creation, listener registration/replacement and release | | src/sheet.js | Midpoint grid, canvas/footer geometry, temporary download anchor | | src/emitter.js | Existing on/once/emit/off semantics; provenance review remains in 04 | | src/utils.js | Existing clamp, filename, sleep and serial-promise helpers | -The entry delegates to input/sheet modules; neither module imports the entry. +The entry delegates to input/source/extraction/sheet; none imports the entry. +Input/source use lifecycle state, extraction uses lifecycle/source, and sheet uses +only filename calculation. There is no dependency back from helpers to the class. Input records live in a private WeakMap, so callers replacing `option` cannot lose ownership of generated inputs/listeners. Normal construction preserves the existing instance-field order and bound inputChange/ondrop methods. DEFAULTS, @@ -30,9 +34,35 @@ The existing bound ondrop method is now registered correctly. Destroy is idempotent and attempts input, video, current URL and destroy-event cleanup even when one step throws; the first cleanup error escapes afterward. Setup/file-input/drop/load calls cannot recreate input/source resources after -destruction. The emitter is not globally cleared. This does **not yet** cancel -existing extraction timers or late Blob callbacks; pending work and replaced -source URLs remain tracked risks for the rest of 03. +destruction. The emitter is not globally cleared. Destruction closes the instance +before cleanup, cancels its job and releases private URLs even if public URL fields +were overwritten. Current public URLs are also revoked, preserving historical +destroy behavior. The video is paused, its src removed, load resets its decoder +state, and its node is removed. Registration checks for reentrant closure/replacement. + +## Extraction and event ordering + +Loading emits file before assigning video.src, then video synchronously as in +workspace 4.4.0. A source generation prevents an older file callback or URL creation +hook from overwriting a nested newer load. Successful replacement revokes old +source URLs. Native errors report once for their source; stale listeners cannot +fail the latest job. Existing sheet URLs stay available until the next sheet frame +or destruction, matching the previous public behavior. + +start creates one owned job. Metadata waits use native events plus the existing +one-second polling fallback. Waiting before the first file adopts that selection; +replacing an existing source cancels its old job. Duplicate starts are rejected. +Preflight stays synchronous when metadata is ready. Canvas precedes processing=true; +updates observe true, done observes false and may start another job without old +completion clearing the new state. + +Frames wait for readiness/seek, accept readiness/Blob callbacks once and restore +oncanplay only while still owning it. Seek/draw/encoding failures, null Blob and +throwing callbacks settle the promise and detach job resources. Source replacement +or destruction rejects with AbortError without an error event. The public promise +still rejects for awaiting consumers; an internal handler owns ignored cancellation. +Late callbacks cannot create URLs, emit updates or schedule frames. There is no +arbitrary new metadata deadline; waiting can be cancelled by destroy/replacement. Sheet extraction preserves fractional coordinates, historical `creat*` names, the 30-pixel footer and the filename algorithm. The temporary download anchor is @@ -63,8 +93,10 @@ cleanup runs on all three engines, while extraction acceptance needs supported Safari/WebKit evidence in 05. The independent tool example is `docs/assets/example/tool.thumbnail.js`, not the external thumbnail plugin example. -Continue 03 by introducing owned metadata/extraction jobs and source/thumbnail -URL cleanup. Add candidate failure/cancellation regressions and preserve old -tests; do not change default delay/height policy silently. Then migrate the final -module boundaries to strict TS in 04 and verify installed declarations/old paths. +`test/thumbnail-lifecycle.test.js` covers jobs, source cancellation, native errors, +callback failures, reentrancy and private resources. Browser tests hold real PNG +callbacks and replace a selected file during encoding; only the latest job may +complete. Continue strict TS and installed declarations in 04; preserve old tests +and the default delay/height boundary. The old sleep/serial helpers in utils.js are +no longer used by extraction and can be removed during that migration. See [task plan](../../refactor/plan.md) and [risk ledger](../../refactor/risks.json). diff --git a/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js b/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js index 32c840117..25c99402c 100644 --- a/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js +++ b/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.ArtplayerToolThumbnail=e(),define(function(){return t.ArtplayerToolThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).ArtplayerToolThumbnail=e()}(this,function(){"use strict";class t{on(t,e,i){const n=this.e||(this.e={});return(n[t]||(n[t]=[])).push({fn:e,ctx:i}),this}once(t,e,i){const n=this;function o(...s){n.off(t,o),e.apply(i,s)}return o._=e,this.on(t,o,i)}emit(t,...e){const i=((this.e||(this.e={}))[t]||[]).slice();for(let n=0;nsetTimeout(e,t))).then(()=>this.start());var t;const{width:i,number:n,begin:o,end:s}=this.option,r=this.video.videoHeight/this.video.videoWidth*i;this.option.height=r,this.option.begin=e(o,0,this.video.duration),this.option.end=e(s||this.video.duration,o,this.video.duration),this.errorHandle(this.option.end>this.option.begin,"End time must be greater than the start time"),this.duration=this.option.end-this.option.begin,this.density=n/this.duration,this.errorHandle(this.file&&this.video,"Please select the video file first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment..."),this.errorHandle(this.density<=1,`The preview density cannot be greater than 1, but got ${this.density}`);const h=this.creatScreenshotDate(),a=this.creatCanvas(),l=a.getContext("2d");this.emit("canvas",a);const c=h.map((t,e)=>()=>new Promise(o=>{this.video.oncanplay=()=>{l.drawImage(this.video,t.x,t.y,i,r),a.toBlob(t=>{this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),this.thumbnailUrl=URL.createObjectURL(t),this.emit("update",this.thumbnailUrl,(e+1)/n),this.video.oncanplay=null,o()})},this.video.currentTime=t.time}));return this.processing=!0,(d=c,d.reduce((t,e)=>t.then(e),Promise.resolve())).then(()=>{this.processing=!1,this.emit("done")}).catch(t=>{throw this.processing=!1,this.emit("error",t.message),t});var d}creatScreenshotDate(){return function(t,e){const{number:i,width:n,height:o,column:s,begin:r}=t,h=e/i,a=[r+h];for(;a.length({time:t-h/2,x:e%s*n,y:Math.floor(e/s)*o}))}(this.option,this.duration)}creatCanvas(){return function(t){const{number:e,width:i,height:n,column:o}=t,s=document.createElement("canvas"),r=s.getContext("2d");return s.width=i*o,s.height=Math.ceil(e/o)*n+30,r.fillStyle="black",r.fillRect(0,0,s.width,s.height),r.font="14px Georgia",r.fillStyle="#fff",r.fillText(`From: https://artplayer.org/, Number: ${e}, Width: ${i}, Height: ${n}, Column: ${o}`,10,s.height-11),s}(this.option)}download(){this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const t=function(t,e){const i=document.createElement("a"),n=`${function(t){const e=t.split(".");return e.pop(),e.join(".")}(t.name)}.png`;i.download=n,i.href=e;try{document.body.appendChild(i),i.click()}finally{i.parentNode&&i.parentNode.removeChild(i)}return n}(this.file,this.thumbnailUrl);return this.emit("download",t),this}errorHandle(t,e){if(!t)throw this.emit("error",e),new Error(e)}destroy(){if(r.has(this))return;let t;r.add(this);for(const i of[()=>s(this),()=>this.video.parentNode?.removeChild(this.video),()=>this.videoUrl&&URL.revokeObjectURL(this.videoUrl),()=>this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),()=>this.emit("destroy")])try{i()}catch(e){t||(t=e)}if(t)throw t}}return h}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this;function o(...i){r.off(e,o),t.apply(n,i)}return o._=t,this.on(e,o,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;rn.pause?.(),()=>n.removeAttribute?.("src"),()=>n.load?.(),()=>n.parentNode?.removeChild(n));i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function a(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function l(e,t){let s=t.epoch;const c=e.video;let l,u,d,h,p=!1,f=!1,m=()=>{};const b=[],y=new Promise((e,t)=>{l=e,u=t});y.catch(()=>{});const g=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function v(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{v()}catch(r){t||(t=r)}if(n)try{e.emit("error",t?.message)}catch(o){t=o}u(t)}}function U(t){if(!g())return;f=!0,e.processing=!0;let r=0;const i=()=>{if(!g())return;if(r===t.points.length)return void function(){if(g())try{v(),e.emit("done"),l()}catch(t){let r=t;try{e.emit("error",t?.message)}catch(n){r=n}u(r)}}();const s=t.points[r];let a=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!g()||a||c.seeking||void 0!==c.readyState&&c.readyState<2)){a=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!g())return;t.canvas.toBlob(s=>{if(g()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,g);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}catch(c){w(c)}}})}catch(l){w(l)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(i)}function E(n=!1){if(g()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,l=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=a(i,0,n.duration),e.option.end=a(s||n.duration,i,n.duration),l(e.option.end>e.option.begin,"End time must be greater than the start time"),l(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,l(e.file&&n,"Please select the video file first"),l(!e.processing,"There is currently a task in progress, please wait a moment..."),l(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),g()||clearTimeout(d)}h={video:c,promise:y,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{g()&&w(new Error(`Unable to load video: media error ${c.error?.code||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeEventListener(e,t)),c.addEventListener(e,t),!g())return i([()=>c.removeEventListener(e,t),...b.splice(0)]),y;c.error?w(new Error(`Unable to load video: media error ${c.error.code}`)):E(!0)}catch(j){throw w(j,!1),j}return y}const u=new WeakMap;function d(e){let t;for(const[r,o]of e.listeners.splice(0))try{e.input.removeEventListener(r,o)}catch(n){t||(t=n)}if(e.wrapper){try{e.input.parentNode&&e.input.parentNode.removeChild(e.input)}catch(n){t||(t=n)}"relative"===e.wrapper.style.position&&(e.wrapper.style.position=e.position)}if(t)throw t}function h(e,t){try{for(const[n,r]of t)e.listeners.push([n,r]),e.input.addEventListener(n,r)}catch(n){try{d(e)}catch{}throw n}}function p(e){const t=u.get(e);t&&(u.delete(e),d(t))}class f extends e{constructor(e={}){super(),this.processing=!1,this.option={};try{this.setup(Object.assign({},f.DEFAULTS,e)),this.video=f.creatVideo(),n(this).video=this.video,this.duration=0,this.inputChange=this.inputChange.bind(this),this.ondrop=this.ondrop.bind(this),function(e,t){const n=u.get(e);n.callbacks=[["change",e.inputChange],["dragover",t],["drop",e.ondrop]],h(n,n.callbacks)}(this,f.ondragover)}catch(t){s(this,"construction failed");try{p(this)}catch{}try{c(this)}catch{}throw t}}static get DEFAULTS(){return{number:60,width:160,height:90,column:10,begin:0,end:Number.NaN}}static ondragover(e){e.preventDefault()}ondrop(e){if(n(this).closed)return;e.preventDefault();const t=e.dataTransfer.files[0];this.loadVideo(t)}setup(e={}){return n(this).closed||function(e,t){const o=n(e),i=++o.inputEpoch,s=()=>!o.closed&&o.inputEpoch===i,c=Object.assign({},e.option,t),l=c.fileInput;e.errorHandle(l instanceof Element,"The 'fileInput' is not a Element");for(const n of["number","width","column","begin","end"])e.errorHandle("number"===typeof c[n],`The '${n}' is not a number`);if(c.number=a(c.number,10,1e3),c.width=a(c.width,10,1e3),c.column=a(c.column,1,1e3),!s())throw r("input setup superseded");const p=u.get(e);if(p&&(l===p.input||l===p.wrapper))return c.fileInput=p.input,e.option=c,c;const f={input:l,wrapper:null,position:"",listeners:[],callbacks:p?.callbacks||null};try{if("INPUT"===l.tagName&&"file"===l.type||(f.wrapper=l,f.position=l.style.position,f.input=document.createElement("input"),f.input.type="file",Object.assign(f.input.style,{position:"absolute",width:"100%",height:"100%",left:"0",top:"0",right:"0",bottom:"0",opacity:"0"}),l.style.position="relative",l.appendChild(f.input)),!s())throw r("input setup superseded");if(f.callbacks&&h(f,f.callbacks),!s())throw r("input setup superseded")}catch(m){try{d(f)}catch{}throw m}c.fileInput=f.input,u.set(e,f),e.option=c,p&&d(p)}(this,e),this}static creatVideo(){const e=document.createElement("video");e.style.position="absolute",e.style.top="-9999px",e.style.left="-9999px",e.muted=!0,e.controls=!0;try{document.body.appendChild(e)}catch(t){throw e.parentNode&&e.parentNode.removeChild(e),t}return e}inputChange(e){if(n(this).closed)return;const t=this.option.fileInput.files[0];this.loadVideo(t),e.target.value=""}loadVideo(e){!function(e,t){const r=n(e);if(!t||r.closed)return;const s=r.epoch,c=e.video,a=c.canPlayType(t.type);if(e.errorHandle("maybe"===a||"probably"===a,`Playback of this file format is not supported: ${t.type}`),r.closed||r.epoch!==s)return;const l=URL.createObjectURL(t);if(r.sourceUrls.add(l),r.closed||r.epoch!==s)return void o(r.sourceUrls,l);const u=++r.epoch;!r.sourceUrl&&!e.file&&r.job?.adopt(u)||r.job?.cancel("source changed"),r.loading=!0;const d=()=>!r.closed&&r.epoch===u,h=r.sourceListeners,p=[];r.sourceListeners=p;let f=()=>{},m=!1;const b=()=>{if(!d()||m||c.currentSrc&&c.currentSrc!==l)return;m=!0,r.loading=!1;const t=new Error(`Unable to load video: media error ${c.error?.code||0}`);r.job?r.job.fail(t):e.emit("error",t.message)},y=()=>{!d()||c.currentSrc&&c.currentSrc!==l||(r.loading=!1,r.job?.ready())};try{if(i(h.splice(0)),!d())return void o(r.sourceUrls,l);for(const[e,t]of[["error",b],["loadedmetadata",y]])if(f=()=>c.removeEventListener(e,t),p.push(f),c.addEventListener(e,t),!d())return i([f,...p.splice(0)]),void o(r.sourceUrls,l);if(e.videoUrl=l,e.file=t,e.emit("file",t),!d())return void o(r.sourceUrls,l);if(c.src=l,!d())return void o(r.sourceUrls,l);r.sourceUrl=l;for(const e of[...r.sourceUrls])e!==l&&o(r.sourceUrls,e);d()&&e.emit("video",c)}catch(g){d()&&(r.loading=!1,r.job?.cancel("source setup failed"));try{i([f,...p.splice(0)])}catch{}throw o(r.sourceUrls,l),g}}(this,e)}start(){return function(e){const t=n(e);if(t.closed){const e=Promise.reject(r("destroyed"));return e.catch(()=>{}),e}return e.errorHandle(!t.job,"There is currently a task in progress, please wait a moment..."),l(e,t)}(this)}creatScreenshotDate(){return function(e,t){const{number:n,width:r,height:o,column:i,begin:s}=e,c=t/n,a=[s+c];for(;a.length({time:e-c/2,x:t%i*r,y:Math.floor(t/i)*o}))}(this.option,this.duration)}creatCanvas(){return function(e){const{number:t,width:n,height:r,column:o}=e,i=document.createElement("canvas"),s=i.getContext("2d");return i.width=n*o,i.height=Math.ceil(t/o)*r+30,s.fillStyle="black",s.fillRect(0,0,i.width,i.height),s.font="14px Georgia",s.fillStyle="#fff",s.fillText(`From: https://artplayer.org/, Number: ${t}, Width: ${n}, Height: ${r}, Column: ${o}`,10,i.height-11),i}(this.option)}download(){if(n(this).closed)return this;this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const e=function(e,t){const n=document.createElement("a"),r=`${function(e){const t=e.split(".");return t.pop(),t.join(".")}(e.name)}.png`;n.download=r,n.href=t;try{document.body.appendChild(n),n.click()}finally{n.parentNode&&n.parentNode.removeChild(n)}return r}(this.file,this.thumbnailUrl);return this.emit("download",e),this}errorHandle(e,t){if(!e)throw this.emit("error",t),new Error(t)}destroy(){s(this)&&i([()=>p(this),()=>c(this),()=>this.emit("destroy")])}}return f}); diff --git a/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js b/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js index 42395657e..543a7a290 100644 --- a/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js +++ b/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.ArtplayerToolThumbnail=e(),define(function(){return t.ArtplayerToolThumbnail})):(t="undefined"!==typeof globalThis?globalThis:t||self).ArtplayerToolThumbnail=e()}(this,function(){"use strict";class t{on(t,e,i){const n=this.e||(this.e={});return(n[t]||(n[t]=[])).push({fn:e,ctx:i}),this}once(t,e,i){const n=this;function o(...s){n.off(t,o),e.apply(i,s)}return o._=e,this.on(t,o,i)}emit(t,...e){const i=((this.e||(this.e={}))[t]||[]).slice();for(let n=0;nsetTimeout(e,t))).then(()=>this.start());var t;const{width:i,number:n,begin:o,end:s}=this.option,r=this.video.videoHeight/this.video.videoWidth*i;this.option.height=r,this.option.begin=e(o,0,this.video.duration),this.option.end=e(s||this.video.duration,o,this.video.duration),this.errorHandle(this.option.end>this.option.begin,"End time must be greater than the start time"),this.duration=this.option.end-this.option.begin,this.density=n/this.duration,this.errorHandle(this.file&&this.video,"Please select the video file first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment..."),this.errorHandle(this.density<=1,`The preview density cannot be greater than 1, but got ${this.density}`);const h=this.creatScreenshotDate(),a=this.creatCanvas(),l=a.getContext("2d");this.emit("canvas",a);const c=h.map((t,e)=>()=>new Promise(o=>{this.video.oncanplay=()=>{l.drawImage(this.video,t.x,t.y,i,r),a.toBlob(t=>{this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),this.thumbnailUrl=URL.createObjectURL(t),this.emit("update",this.thumbnailUrl,(e+1)/n),this.video.oncanplay=null,o()})},this.video.currentTime=t.time}));return this.processing=!0,(d=c,d.reduce((t,e)=>t.then(e),Promise.resolve())).then(()=>{this.processing=!1,this.emit("done")}).catch(t=>{throw this.processing=!1,this.emit("error",t.message),t});var d}creatScreenshotDate(){return function(t,e){const{number:i,width:n,height:o,column:s,begin:r}=t,h=e/i,a=[r+h];for(;a.length({time:t-h/2,x:e%s*n,y:Math.floor(e/s)*o}))}(this.option,this.duration)}creatCanvas(){return function(t){const{number:e,width:i,height:n,column:o}=t,s=document.createElement("canvas"),r=s.getContext("2d");return s.width=i*o,s.height=Math.ceil(e/o)*n+30,r.fillStyle="black",r.fillRect(0,0,s.width,s.height),r.font="14px Georgia",r.fillStyle="#fff",r.fillText(`From: https://artplayer.org/, Number: ${e}, Width: ${i}, Height: ${n}, Column: ${o}`,10,s.height-11),s}(this.option)}download(){this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const t=function(t,e){const i=document.createElement("a"),n=`${function(t){const e=t.split(".");return e.pop(),e.join(".")}(t.name)}.png`;i.download=n,i.href=e;try{document.body.appendChild(i),i.click()}finally{i.parentNode&&i.parentNode.removeChild(i)}return n}(this.file,this.thumbnailUrl);return this.emit("download",t),this}errorHandle(t,e){if(!t)throw this.emit("error",e),new Error(e)}destroy(){if(r.has(this))return;let t;r.add(this);for(const i of[()=>s(this),()=>{var t;return null==(t=this.video.parentNode)?void 0:t.removeChild(this.video)},()=>this.videoUrl&&URL.revokeObjectURL(this.videoUrl),()=>this.thumbnailUrl&&URL.revokeObjectURL(this.thumbnailUrl),()=>this.emit("destroy")])try{i()}catch(e){t||(t=e)}if(t)throw t}}return h}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolThumbnail=t(),define(function(){return e.ArtplayerToolThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolThumbnail=t()}(this,function(){"use strict";class e{on(e,t,n){const r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this}once(e,t,n){const r=this;function o(...i){r.off(e,o),t.apply(n,i)}return o._=t,this.on(e,o,n)}emit(e,...t){const n=((this.e||(this.e={}))[e]||[]).slice();for(let r=0;r{var e;return null==(e=n.pause)?void 0:e.call(n)},()=>{var e;return null==(e=n.removeAttribute)?void 0:e.call(n,"src")},()=>{var e;return null==(e=n.load)?void 0:e.call(n)},()=>{var e;return null==(e=n.parentNode)?void 0:e.removeChild(n)});i([...t.sourceListeners.splice(0),...c,...[...r].map(e=>()=>o(t.sourceUrls,e)),...[...s].map(e=>()=>o(t.thumbnailUrls,e))])}function l(e,t,n){return Math.max(Math.min(e,Math.max(t,n)),Math.min(t,n))}function a(e,t){let s=t.epoch;const c=e.video;let a,u,d,h,p=!1,f=!1,m=()=>{};const b=[],v=new Promise((e,t)=>{a=e,u=t});v.catch(()=>{});const y=()=>!p&&!t.closed&&t.epoch===s&&t.job===h;function g(){return!p&&(p=!0,t.job===h&&(t.job=null,e.processing=!1),i([()=>clearTimeout(d),m,...b.splice(0)]),!0)}function w(t,n=!0){if(!p){try{g()}catch(r){t||(t=r)}if(n)try{e.emit("error",null==t?void 0:t.message)}catch(o){t=o}u(t)}}function U(t){if(!y())return;f=!0,e.processing=!0;let r=0;const i=()=>{if(!y())return;if(r===t.points.length)return void function(){if(y())try{g(),e.emit("done"),a()}catch(t){let r=t;try{e.emit("error",null==t?void 0:t.message)}catch(n){r=n}u(r)}}();const s=t.points[r];let l=!1,d=!1;const h=c.oncanplay,p=()=>{if(!(!y()||l||c.seeking||void 0!==c.readyState&&c.readyState<2)){l=!0;try{if(m(),t.context.drawImage(c,s.x,s.y,t.width,t.height),!y())return;t.canvas.toBlob(s=>{if(y()&&!d){d=!0;try{if(!s)throw new Error("Unable to create thumbnail image");const c=function(e,t,r){const i=n(e),s=URL.createObjectURL(t);if(i.thumbnailUrls.add(s),!r())return o(i.thumbnailUrls,s),null;const c=new Set([...i.thumbnailUrls,e.thumbnailUrl]);i.thumbnailUrl=s,e.thumbnailUrl=s;for(const n of c)n&&n!==s&&o(i.thumbnailUrls,n);return r()?s:null}(e,s,y);if(!c)return;e.emit("update",c,(r+1)/t.number),r++,Promise.resolve().then(i)}catch(c){w(c)}}})}catch(a){w(a)}}};m=()=>{c.oncanplay===p&&(c.oncanplay=h),c.removeEventListener("seeked",p)};try{c.oncanplay=p,c.addEventListener("seeked",p),c.currentTime=s.time,c.readyState>=2&&!c.seeking&&Promise.resolve().then(p)}catch(f){w(f)}};Promise.resolve().then(i)}function E(n=!1){if(y()&&!f)if(clearTimeout(d),!t.loading&&c.duration){f=!0;try{const t=function(e,t){const{video:n}=t,{width:r,number:o,begin:i,end:s}=e.option,c=n.videoHeight/n.videoWidth*r,a=(n,r)=>{try{e.errorHandle(n,r)}catch(o){throw t.reported=o,t.wasReported=!0,o}};e.option.height=c,e.option.begin=l(i,0,n.duration),e.option.end=l(s||n.duration,i,n.duration),a(e.option.end>e.option.begin,"End time must be greater than the start time"),a(Number.isFinite(n.duration),"Video duration must be finite"),e.duration=e.option.end-e.option.begin,e.density=o/e.duration,a(e.file&&n,"Please select the video file first"),a(!e.processing,"There is currently a task in progress, please wait a moment..."),a(e.density<=1,`The preview density cannot be greater than 1, but got ${e.density}`);const u=e.creatScreenshotDate(),d=e.creatCanvas(),h=d.getContext("2d");return e.emit("canvas",d),{width:r,height:c,number:o,points:u,canvas:d,context:h}}(e,h);U(t)}catch(r){if(w(r,!n&&!(h.wasReported&&r===h.reported)),n)throw r}}else d=setTimeout(()=>E(),1e3),y()||clearTimeout(d)}h={video:c,promise:v,ready:E,fail:w,cancel(e){w(r(e),!1)},adopt:t=>!p&&!f&&c===e.video&&(s=t,!0)},t.job=h;try{for(const[e,t]of[["error",()=>{var e;y()&&w(new Error(`Unable to load video: media error ${(null==(e=c.error)?void 0:e.code)||0}`))}],["loadedmetadata",()=>E()],["durationchange",()=>E()]])if(b.push(()=>c.removeEventListener(e,t)),c.addEventListener(e,t),!y())return i([()=>c.removeEventListener(e,t),...b.splice(0)]),v;c.error?w(new Error(`Unable to load video: media error ${c.error.code}`)):E(!0)}catch(j){throw w(j,!1),j}return v}const u=new WeakMap;function d(e){let t;for(const[r,o]of e.listeners.splice(0))try{e.input.removeEventListener(r,o)}catch(n){t||(t=n)}if(e.wrapper){try{e.input.parentNode&&e.input.parentNode.removeChild(e.input)}catch(n){t||(t=n)}"relative"===e.wrapper.style.position&&(e.wrapper.style.position=e.position)}if(t)throw t}function h(e,t){try{for(const[n,r]of t)e.listeners.push([n,r]),e.input.addEventListener(n,r)}catch(n){try{d(e)}catch(r){}throw n}}function p(e){const t=u.get(e);t&&(u.delete(e),d(t))}class f extends e{constructor(e={}){super(),this.processing=!1,this.option={};try{this.setup(Object.assign({},f.DEFAULTS,e)),this.video=f.creatVideo(),n(this).video=this.video,this.duration=0,this.inputChange=this.inputChange.bind(this),this.ondrop=this.ondrop.bind(this),function(e,t){const n=u.get(e);n.callbacks=[["change",e.inputChange],["dragover",t],["drop",e.ondrop]],h(n,n.callbacks)}(this,f.ondragover)}catch(t){s(this,"construction failed");try{p(this)}catch(r){}try{c(this)}catch(r){}throw t}}static get DEFAULTS(){return{number:60,width:160,height:90,column:10,begin:0,end:Number.NaN}}static ondragover(e){e.preventDefault()}ondrop(e){if(n(this).closed)return;e.preventDefault();const t=e.dataTransfer.files[0];this.loadVideo(t)}setup(e={}){return n(this).closed||function(e,t){const o=n(e),i=++o.inputEpoch,s=()=>!o.closed&&o.inputEpoch===i,c=Object.assign({},e.option,t),a=c.fileInput;e.errorHandle(a instanceof Element,"The 'fileInput' is not a Element");for(const n of["number","width","column","begin","end"])e.errorHandle("number"===typeof c[n],`The '${n}' is not a number`);if(c.number=l(c.number,10,1e3),c.width=l(c.width,10,1e3),c.column=l(c.column,1,1e3),!s())throw r("input setup superseded");const p=u.get(e);if(p&&(a===p.input||a===p.wrapper))return c.fileInput=p.input,e.option=c,c;const f={input:a,wrapper:null,position:"",listeners:[],callbacks:(null==p?void 0:p.callbacks)||null};try{if("INPUT"===a.tagName&&"file"===a.type||(f.wrapper=a,f.position=a.style.position,f.input=document.createElement("input"),f.input.type="file",Object.assign(f.input.style,{position:"absolute",width:"100%",height:"100%",left:"0",top:"0",right:"0",bottom:"0",opacity:"0"}),a.style.position="relative",a.appendChild(f.input)),!s())throw r("input setup superseded");if(f.callbacks&&h(f,f.callbacks),!s())throw r("input setup superseded")}catch(m){try{d(f)}catch(b){}throw m}c.fileInput=f.input,u.set(e,f),e.option=c,p&&d(p)}(this,e),this}static creatVideo(){const e=document.createElement("video");e.style.position="absolute",e.style.top="-9999px",e.style.left="-9999px",e.muted=!0,e.controls=!0;try{document.body.appendChild(e)}catch(t){throw e.parentNode&&e.parentNode.removeChild(e),t}return e}inputChange(e){if(n(this).closed)return;const t=this.option.fileInput.files[0];this.loadVideo(t),e.target.value=""}loadVideo(e){!function(e,t){var r,s,c;const l=n(e);if(!t||l.closed)return;const a=l.epoch,u=e.video,d=u.canPlayType(t.type);if(e.errorHandle("maybe"===d||"probably"===d,`Playback of this file format is not supported: ${t.type}`),l.closed||l.epoch!==a)return;const h=URL.createObjectURL(t);if(l.sourceUrls.add(h),l.closed||l.epoch!==a)return void o(l.sourceUrls,h);const p=++l.epoch;!l.sourceUrl&&!e.file&&(null==(r=l.job)?void 0:r.adopt(p))||null==(s=l.job)||s.cancel("source changed"),l.loading=!0;const f=()=>!l.closed&&l.epoch===p,m=l.sourceListeners,b=[];l.sourceListeners=b;let v=()=>{},y=!1;const g=()=>{var t;if(!f()||y||u.currentSrc&&u.currentSrc!==h)return;y=!0,l.loading=!1;const n=new Error(`Unable to load video: media error ${(null==(t=u.error)?void 0:t.code)||0}`);l.job?l.job.fail(n):e.emit("error",n.message)},w=()=>{var e;!f()||u.currentSrc&&u.currentSrc!==h||(l.loading=!1,null==(e=l.job)||e.ready())};try{if(i(m.splice(0)),!f())return void o(l.sourceUrls,h);for(const[e,t]of[["error",g],["loadedmetadata",w]])if(v=()=>u.removeEventListener(e,t),b.push(v),u.addEventListener(e,t),!f())return i([v,...b.splice(0)]),void o(l.sourceUrls,h);if(e.videoUrl=h,e.file=t,e.emit("file",t),!f())return void o(l.sourceUrls,h);if(u.src=h,!f())return void o(l.sourceUrls,h);l.sourceUrl=h;for(const e of[...l.sourceUrls])e!==h&&o(l.sourceUrls,e);f()&&e.emit("video",u)}catch(U){f()&&(l.loading=!1,null==(c=l.job)||c.cancel("source setup failed"));try{i([v,...b.splice(0)])}catch(E){}throw o(l.sourceUrls,h),U}}(this,e)}start(){return function(e){const t=n(e);if(t.closed){const e=Promise.reject(r("destroyed"));return e.catch(()=>{}),e}return e.errorHandle(!t.job,"There is currently a task in progress, please wait a moment..."),a(e,t)}(this)}creatScreenshotDate(){return function(e,t){const{number:n,width:r,height:o,column:i,begin:s}=e,c=t/n,l=[s+c];for(;l.length({time:e-c/2,x:t%i*r,y:Math.floor(t/i)*o}))}(this.option,this.duration)}creatCanvas(){return function(e){const{number:t,width:n,height:r,column:o}=e,i=document.createElement("canvas"),s=i.getContext("2d");return i.width=n*o,i.height=Math.ceil(t/o)*r+30,s.fillStyle="black",s.fillRect(0,0,i.width,i.height),s.font="14px Georgia",s.fillStyle="#fff",s.fillText(`From: https://artplayer.org/, Number: ${t}, Width: ${n}, Height: ${r}, Column: ${o}`,10,i.height-11),i}(this.option)}download(){if(n(this).closed)return this;this.errorHandle(this.file&&this.thumbnailUrl,"Download does not seem to be ready, please create preview first"),this.errorHandle(!this.processing,"There is currently a task in progress, please wait a moment...");const e=function(e,t){const n=document.createElement("a"),r=`${function(e){const t=e.split(".");return t.pop(),t.join(".")}(e.name)}.png`;n.download=r,n.href=t;try{document.body.appendChild(n),n.click()}finally{n.parentNode&&n.parentNode.removeChild(n)}return r}(this.file,this.thumbnailUrl);return this.emit("download",e),this}errorHandle(e,t){if(!e)throw this.emit("error",t),new Error(t)}destroy(){s(this)&&i([()=>p(this),()=>c(this),()=>this.emit("destroy")])}}return f}); diff --git a/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs b/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs index 4d0159c08..e5248b941 100644 --- a/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs +++ b/packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs @@ -44,11 +44,188 @@ class Emitter { return this; } } -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); +const states = /* @__PURE__ */ new WeakMap(); +function stateFor(tool) { + if (!states.has(tool)) { + states.set(tool, { + closed: false, + epoch: 0, + inputEpoch: 0, + job: null, + video: null, + sourceUrl: null, + thumbnailUrl: null, + sourceUrls: /* @__PURE__ */ new Set(), + thumbnailUrls: /* @__PURE__ */ new Set(), + sourceListeners: [], + loading: false + }); + } + return states.get(tool); } -function runPromisesInSeries(ps) { - return ps.reduce((p, next) => p.then(next), Promise.resolve()); +function cancellation(reason) { + const error = new Error(`Thumbnail task cancelled: ${reason}`); + error.name = "AbortError"; + return error; +} +function revoke(urls, url) { + if (!url) + return; + urls.delete(url); + URL.revokeObjectURL(url); +} +function cleanupAll(callbacks) { + let failure; + for (const callback of callbacks) { + try { + callback(); + } catch (error) { + failure || (failure = error); + } + } + if (failure) + throw failure; +} +function closeState(tool, reason = "destroyed") { + const state = stateFor(tool); + if (state.closed) + return false; + state.closed = true; + state.epoch++; + state.job?.cancel(reason); + return true; +} +function releaseMedia(tool) { + const state = stateFor(tool); + const sources = /* @__PURE__ */ new Set([...state.sourceUrls, tool.videoUrl]); + const thumbnails = /* @__PURE__ */ new Set([...state.thumbnailUrls, tool.thumbnailUrl]); + const media = []; + for (const video of /* @__PURE__ */ new Set([state.video, tool.video])) { + if (video) { + media.push( + () => video.pause?.(), + () => video.removeAttribute?.("src"), + () => video.load?.(), + () => video.parentNode?.removeChild(video) + ); + } + } + cleanupAll([ + ...state.sourceListeners.splice(0), + ...media, + ...[...sources].map((url) => () => revoke(state.sourceUrls, url)), + ...[...thumbnails].map((url) => () => revoke(state.thumbnailUrls, url)) + ]); +} +function loadSource(tool, file) { + const state = stateFor(tool); + if (!file || state.closed) + return; + const initialEpoch = state.epoch; + const video = tool.video; + const support = video.canPlayType(file.type); + tool.errorHandle(support === "maybe" || support === "probably", `Playback of this file format is not supported: ${file.type}`); + if (state.closed || state.epoch !== initialEpoch) + return; + const url = URL.createObjectURL(file); + state.sourceUrls.add(url); + if (state.closed || state.epoch !== initialEpoch) { + revoke(state.sourceUrls, url); + return; + } + const epoch = ++state.epoch; + const initialWait = !state.sourceUrl && !tool.file && state.job?.adopt(epoch); + if (!initialWait) + state.job?.cancel("source changed"); + state.loading = true; + const current = () => !state.closed && state.epoch === epoch; + const previousListeners = state.sourceListeners; + const listeners = []; + state.sourceListeners = listeners; + let pendingCleanup = () => { + }; + let reported = false; + const error = () => { + if (!current() || reported || video.currentSrc && video.currentSrc !== url) + return; + reported = true; + state.loading = false; + const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`); + if (state.job) + state.job.fail(failure); + else + tool.emit("error", failure.message); + }; + const ready = () => { + if (current() && (!video.currentSrc || video.currentSrc === url)) { + state.loading = false; + state.job?.ready(); + } + }; + try { + cleanupAll(previousListeners.splice(0)); + if (!current()) { + revoke(state.sourceUrls, url); + return; + } + for (const [name, callback] of [["error", error], ["loadedmetadata", ready]]) { + pendingCleanup = () => video.removeEventListener(name, callback); + listeners.push(pendingCleanup); + video.addEventListener(name, callback); + if (!current()) { + cleanupAll([pendingCleanup, ...listeners.splice(0)]); + revoke(state.sourceUrls, url); + return; + } + } + tool.videoUrl = url; + tool.file = file; + tool.emit("file", file); + if (!current()) { + revoke(state.sourceUrls, url); + return; + } + video.src = url; + if (!current()) { + revoke(state.sourceUrls, url); + return; + } + state.sourceUrl = url; + for (const previous of [...state.sourceUrls]) { + if (previous !== url) + revoke(state.sourceUrls, previous); + } + if (current()) + tool.emit("video", video); + } catch (error2) { + if (current()) { + state.loading = false; + state.job?.cancel("source setup failed"); + } + try { + cleanupAll([pendingCleanup, ...listeners.splice(0)]); + } catch { + } + revoke(state.sourceUrls, url); + throw error2; + } +} +function replaceThumbnail(tool, blob, live) { + const state = stateFor(tool); + const url = URL.createObjectURL(blob); + state.thumbnailUrls.add(url); + if (!live()) { + revoke(state.thumbnailUrls, url); + return null; + } + const previous = /* @__PURE__ */ new Set([...state.thumbnailUrls, tool.thumbnailUrl]); + state.thumbnailUrl = url; + tool.thumbnailUrl = url; + for (const old of previous) { + if (old && old !== url) + revoke(state.thumbnailUrls, old); + } + return live() ? url : null; } function getFileName(name) { const nameArray = name.split("."); @@ -58,6 +235,236 @@ function getFileName(name) { function clamp(num, a, b) { return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b)); } +function prepare(tool, job) { + const { video } = job; + const { width, number, begin, end } = tool.option; + const height = video.videoHeight / video.videoWidth * width; + const guard = (condition, message) => { + try { + tool.errorHandle(condition, message); + } catch (error) { + job.reported = error; + job.wasReported = true; + throw error; + } + }; + tool.option.height = height; + tool.option.begin = clamp(begin, 0, video.duration); + tool.option.end = clamp(end || video.duration, begin, video.duration); + guard(tool.option.end > tool.option.begin, "End time must be greater than the start time"); + guard(Number.isFinite(video.duration), "Video duration must be finite"); + tool.duration = tool.option.end - tool.option.begin; + tool.density = number / tool.duration; + guard(tool.file && video, "Please select the video file first"); + guard(!tool.processing, "There is currently a task in progress, please wait a moment..."); + guard(tool.density <= 1, `The preview density cannot be greater than 1, but got ${tool.density}`); + const points = tool.creatScreenshotDate(); + const canvas = tool.creatCanvas(); + const context = canvas.getContext("2d"); + tool.emit("canvas", canvas); + return { width, height, number, points, canvas, context }; +} +function createJob(tool, state) { + let epoch = state.epoch; + const video = tool.video; + let resolve; + let reject; + let timer; + let settled = false; + let running = false; + let job; + let frameCleanup = () => { + }; + const listeners = []; + const promise = new Promise((yes, no) => { + resolve = yes; + reject = no; + }); + promise.catch(() => { + }); + const live = () => !settled && !state.closed && state.epoch === epoch && state.job === job; + function finish() { + if (settled) + return false; + settled = true; + if (state.job === job) { + state.job = null; + tool.processing = false; + } + cleanupAll([() => clearTimeout(timer), frameCleanup, ...listeners.splice(0)]); + return true; + } + function fail(error, report = true) { + if (settled) + return; + try { + finish(); + } catch (cleanupError) { + error || (error = cleanupError); + } + if (report) { + try { + tool.emit("error", error?.message); + } catch (listenerError) { + error = listenerError; + } + } + reject(error); + } + function complete() { + if (!live()) + return; + try { + finish(); + tool.emit("done"); + resolve(); + } catch (error) { + let failure = error; + try { + tool.emit("error", error?.message); + } catch (listenerError) { + failure = listenerError; + } + reject(failure); + } + } + function extract(plan) { + if (!live()) + return; + running = true; + tool.processing = true; + let index = 0; + const next = () => { + if (!live()) + return; + if (index === plan.points.length) { + complete(); + return; + } + const point = plan.points[index]; + let drawing = false; + let encoded = false; + const previous = video.oncanplay; + const draw = () => { + if (!live() || drawing || video.seeking || video.readyState !== void 0 && video.readyState < 2) + return; + drawing = true; + try { + frameCleanup(); + plan.context.drawImage(video, point.x, point.y, plan.width, plan.height); + if (!live()) + return; + plan.canvas.toBlob((blob) => { + if (!live() || encoded) + return; + encoded = true; + try { + if (!blob) + throw new Error("Unable to create thumbnail image"); + const url = replaceThumbnail(tool, blob, live); + if (!url) + return; + tool.emit("update", url, (index + 1) / plan.number); + index++; + Promise.resolve().then(next); + } catch (error) { + fail(error); + } + }); + } catch (error) { + fail(error); + } + }; + frameCleanup = () => { + if (video.oncanplay === draw) + video.oncanplay = previous; + video.removeEventListener("seeked", draw); + }; + try { + video.oncanplay = draw; + video.addEventListener("seeked", draw); + video.currentTime = point.time; + if (video.readyState >= 2 && !video.seeking) + Promise.resolve().then(draw); + } catch (error) { + fail(error); + } + }; + Promise.resolve().then(next); + } + function ready(synchronous = false) { + if (!live() || running) + return; + clearTimeout(timer); + if (!state.loading && video.duration) { + running = true; + try { + const plan = prepare(tool, job); + extract(plan); + } catch (error) { + fail(error, !synchronous && !(job.wasReported && error === job.reported)); + if (synchronous) + throw error; + } + } else { + timer = setTimeout(() => ready(), 1e3); + if (!live()) + clearTimeout(timer); + } + } + job = { + video, + promise, + ready, + fail, + cancel(reason) { + fail(cancellation(reason), false); + }, + adopt(sourceEpoch) { + if (settled || running || video !== tool.video) + return false; + epoch = sourceEpoch; + return true; + } + }; + state.job = job; + try { + for (const [name, callback] of [ + ["error", () => { + if (live()) + fail(new Error(`Unable to load video: media error ${video.error?.code || 0}`)); + }], + ["loadedmetadata", () => ready()], + ["durationchange", () => ready()] + ]) { + listeners.push(() => video.removeEventListener(name, callback)); + video.addEventListener(name, callback); + if (!live()) { + cleanupAll([() => video.removeEventListener(name, callback), ...listeners.splice(0)]); + return promise; + } + } + if (video.error) + fail(new Error(`Unable to load video: media error ${video.error.code}`)); + else + ready(true); + } catch (error) { + fail(error, false); + throw error; + } + return promise; +} +function startExtraction(tool) { + const state = stateFor(tool); + if (state.closed) { + const promise = Promise.reject(cancellation("destroyed")); + promise.catch(() => { + }); + return promise; + } + tool.errorHandle(!state.job, "There is currently a task in progress, please wait a moment..."); + return createJob(tool, state); +} const inputs = /* @__PURE__ */ new WeakMap(); function release(record) { let failure; @@ -96,6 +503,9 @@ function connect(record, callbacks) { } } function setupInput(tool, patch) { + const state = stateFor(tool); + const epoch = ++state.inputEpoch; + const current = () => !state.closed && state.inputEpoch === epoch; const option = Object.assign({}, tool.option, patch); const target = option.fileInput; tool.errorHandle(target instanceof Element, "The 'fileInput' is not a Element"); @@ -104,9 +514,12 @@ function setupInput(tool, patch) { option.number = clamp(option.number, 10, 1e3); option.width = clamp(option.width, 10, 1e3); option.column = clamp(option.column, 1, 1e3); + if (!current()) + throw cancellation("input setup superseded"); const previous = inputs.get(tool); if (previous && (target === previous.input || target === previous.wrapper)) { option.fileInput = previous.input; + tool.option = option; return option; } const record = { input: target, wrapper: null, position: "", listeners: [], callbacks: previous?.callbacks || null }; @@ -120,8 +533,12 @@ function setupInput(tool, patch) { target.style.position = "relative"; target.appendChild(record.input); } + if (!current()) + throw cancellation("input setup superseded"); if (record.callbacks) connect(record, record.callbacks); + if (!current()) + throw cancellation("input setup superseded"); } catch (error) { try { release(record); @@ -193,7 +610,6 @@ function downloadSheet(file, url) { } return name; } -const destroyed = /* @__PURE__ */ new WeakSet(); class ArtplayerToolThumbnail extends Emitter { constructor(option = {}) { super(); @@ -202,18 +618,19 @@ class ArtplayerToolThumbnail extends Emitter { try { this.setup(Object.assign({}, ArtplayerToolThumbnail.DEFAULTS, option)); this.video = ArtplayerToolThumbnail.creatVideo(); + stateFor(this).video = this.video; this.duration = 0; this.inputChange = this.inputChange.bind(this); this.ondrop = this.ondrop.bind(this); connectInput(this, ArtplayerToolThumbnail.ondragover); } catch (error) { + closeState(this, "construction failed"); try { releaseInput(this); } catch { } try { - if (this.video?.parentNode) - this.video.parentNode.removeChild(this.video); + releaseMedia(this); } catch { } throw error; @@ -233,15 +650,15 @@ class ArtplayerToolThumbnail extends Emitter { event.preventDefault(); } ondrop(event) { - if (destroyed.has(this)) + if (stateFor(this).closed) return; event.preventDefault(); const file = event.dataTransfer.files[0]; this.loadVideo(file); } setup(option = {}) { - if (!destroyed.has(this)) - this.option = setupInput(this, option); + if (!stateFor(this).closed) + setupInput(this, option); return this; } static creatVideo() { @@ -261,71 +678,17 @@ class ArtplayerToolThumbnail extends Emitter { return video; } inputChange(event) { - if (destroyed.has(this)) + if (stateFor(this).closed) return; const file = this.option.fileInput.files[0]; this.loadVideo(file); event.target.value = ""; } loadVideo(file) { - if (file && !destroyed.has(this)) { - const canPlayType = this.video.canPlayType(file.type); - this.errorHandle( - canPlayType === "maybe" || canPlayType === "probably", - `Playback of this file format is not supported: ${file.type}` - ); - const videoUrl = URL.createObjectURL(file); - this.videoUrl = videoUrl; - this.file = file; - this.emit("file", this.file); - this.video.src = videoUrl; - this.emit("video", this.video); - } + loadSource(this, file); } start() { - if (!this.video.duration) - return sleep(1e3).then(() => this.start()); - const { width, number, begin, end } = this.option; - const height = this.video.videoHeight / this.video.videoWidth * width; - this.option.height = height; - this.option.begin = clamp(begin, 0, this.video.duration); - this.option.end = clamp(end || this.video.duration, begin, this.video.duration); - this.errorHandle(this.option.end > this.option.begin, `End time must be greater than the start time`); - this.duration = this.option.end - this.option.begin; - this.density = number / this.duration; - this.errorHandle(this.file && this.video, "Please select the video file first"); - this.errorHandle(!this.processing, "There is currently a task in progress, please wait a moment..."); - this.errorHandle(this.density <= 1, `The preview density cannot be greater than 1, but got ${this.density}`); - const screenshotDate = this.creatScreenshotDate(); - const canvas = this.creatCanvas(); - const context2D = canvas.getContext("2d"); - this.emit("canvas", canvas); - const promiseList = screenshotDate.map((item, index) => () => { - return new Promise((resolve) => { - this.video.oncanplay = () => { - context2D.drawImage(this.video, item.x, item.y, width, height); - canvas.toBlob((blob) => { - if (this.thumbnailUrl) { - URL.revokeObjectURL(this.thumbnailUrl); - } - this.thumbnailUrl = URL.createObjectURL(blob); - this.emit("update", this.thumbnailUrl, (index + 1) / number); - this.video.oncanplay = null; - resolve(); - }); - }; - this.video.currentTime = item.time; - }); - }); - this.processing = true; - return runPromisesInSeries(promiseList).then(() => { - this.processing = false; - this.emit("done"); - }).catch((err) => { - this.processing = false; - this.emit("error", err.message); - throw err; - }); + return startExtraction(this); } creatScreenshotDate() { return screenshotPoints(this.option, this.duration); @@ -334,6 +697,8 @@ class ArtplayerToolThumbnail extends Emitter { return createSheet(this.option); } download() { + if (stateFor(this).closed) + return this; this.errorHandle( this.file && this.thumbnailUrl, "Download does not seem to be ready, please create preview first" @@ -350,25 +715,13 @@ class ArtplayerToolThumbnail extends Emitter { } } destroy() { - if (destroyed.has(this)) + if (!closeState(this)) return; - destroyed.add(this); - let failure; - for (const cleanup of [ + cleanupAll([ () => releaseInput(this), - () => this.video.parentNode?.removeChild(this.video), - () => this.videoUrl && URL.revokeObjectURL(this.videoUrl), - () => this.thumbnailUrl && URL.revokeObjectURL(this.thumbnailUrl), + () => releaseMedia(this), () => this.emit("destroy") - ]) { - try { - cleanup(); - } catch (error) { - failure || (failure = error); - } - } - if (failure) - throw failure; + ]); } } export { diff --git a/packages/artplayer-tool-thumbnail/src/extraction.js b/packages/artplayer-tool-thumbnail/src/extraction.js new file mode 100644 index 000000000..8f3d2ddb4 --- /dev/null +++ b/packages/artplayer-tool-thumbnail/src/extraction.js @@ -0,0 +1,243 @@ +import { cancellation, cleanupAll, stateFor } from './lifecycle' +import { replaceThumbnail } from './source' +import { clamp } from './utils' + +function prepare(tool, job) { + const { video } = job + const { width, number, begin, end } = tool.option + const height = (video.videoHeight / video.videoWidth) * width + const guard = (condition, message) => { + try { + tool.errorHandle(condition, message) + } + catch (error) { + job.reported = error + job.wasReported = true + throw error + } + } + tool.option.height = height + tool.option.begin = clamp(begin, 0, video.duration) + tool.option.end = clamp(end || video.duration, begin, video.duration) + guard(tool.option.end > tool.option.begin, 'End time must be greater than the start time') + guard(Number.isFinite(video.duration), 'Video duration must be finite') + tool.duration = tool.option.end - tool.option.begin + tool.density = number / tool.duration + guard(tool.file && video, 'Please select the video file first') + guard(!tool.processing, 'There is currently a task in progress, please wait a moment...') + guard(tool.density <= 1, `The preview density cannot be greater than 1, but got ${tool.density}`) + const points = tool.creatScreenshotDate() + const canvas = tool.creatCanvas() + const context = canvas.getContext('2d') + tool.emit('canvas', canvas) + return { width, height, number, points, canvas, context } +} + +function createJob(tool, state) { + let epoch = state.epoch + const video = tool.video + let resolve + let reject + let timer + let settled = false + let running = false + let job + let frameCleanup = () => {} + const listeners = [] + const promise = new Promise((yes, no) => { + resolve = yes + reject = no + }) + // Consumers still receive the rejecting promise; ignored cancellation is owned. + promise.catch(() => {}) + const live = () => !settled && !state.closed && state.epoch === epoch && state.job === job + function finish() { + if (settled) + return false + settled = true + if (state.job === job) { + state.job = null + tool.processing = false + } + cleanupAll([() => clearTimeout(timer), frameCleanup, ...listeners.splice(0)]) + return true + } + function fail(error, report = true) { + if (settled) + return + try { + finish() + } + catch (cleanupError) { + error ||= cleanupError + } + if (report) { + try { + tool.emit('error', error?.message) + } + catch (listenerError) { + error = listenerError + } + } + reject(error) + } + function complete() { + if (!live()) + return + try { + finish() + tool.emit('done') + resolve() + } + catch (error) { + let failure = error + try { + tool.emit('error', error?.message) + } + catch (listenerError) { + failure = listenerError + } + reject(failure) + } + } + function extract(plan) { + if (!live()) + return + running = true + tool.processing = true + let index = 0 + const next = () => { + if (!live()) + return + if (index === plan.points.length) { + complete() + return + } + const point = plan.points[index] + let drawing = false + let encoded = false + const previous = video.oncanplay + const draw = () => { + if (!live() || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2)) + return + drawing = true + try { + frameCleanup() + plan.context.drawImage(video, point.x, point.y, plan.width, plan.height) + if (!live()) + return + plan.canvas.toBlob((blob) => { + if (!live() || encoded) + return + encoded = true + try { + if (!blob) + throw new Error('Unable to create thumbnail image') + const url = replaceThumbnail(tool, blob, live) + if (!url) + return + tool.emit('update', url, (index + 1) / plan.number) + index++ + Promise.resolve().then(next) + } + catch (error) { + fail(error) + } + }) + } + catch (error) { + fail(error) + } + } + frameCleanup = () => { + if (video.oncanplay === draw) + video.oncanplay = previous + video.removeEventListener('seeked', draw) + } + try { + video.oncanplay = draw + video.addEventListener('seeked', draw) + video.currentTime = point.time + if (video.readyState >= 2 && !video.seeking) + Promise.resolve().then(draw) + } + catch (error) { + fail(error) + } + } + Promise.resolve().then(next) + } + function ready(synchronous = false) { + if (!live() || running) + return + clearTimeout(timer) + if (!state.loading && video.duration) { + running = true + try { + const plan = prepare(tool, job) + extract(plan) + } + catch (error) { + fail(error, !synchronous && !(job.wasReported && error === job.reported)) + if (synchronous) + throw error + } + } + else { + timer = setTimeout(() => ready(), 1000) + if (!live()) + clearTimeout(timer) + } + } + job = { + video, + promise, + ready, + fail, + cancel(reason) { fail(cancellation(reason), false) }, + adopt(sourceEpoch) { + if (settled || running || video !== tool.video) + return false + epoch = sourceEpoch + return true + }, + } + state.job = job + try { + for (const [name, callback] of [ + ['error', () => { + if (live()) + fail(new Error(`Unable to load video: media error ${video.error?.code || 0}`)) + }], + ['loadedmetadata', () => ready()], + ['durationchange', () => ready()], + ]) { + listeners.push(() => video.removeEventListener(name, callback)) + video.addEventListener(name, callback) + if (!live()) { + cleanupAll([() => video.removeEventListener(name, callback), ...listeners.splice(0)]) + return promise + } + } + if (video.error) + fail(new Error(`Unable to load video: media error ${video.error.code}`)) + else + ready(true) + } + catch (error) { + fail(error, false) + throw error + } + return promise +} + +export function startExtraction(tool) { + const state = stateFor(tool) + if (state.closed) { + const promise = Promise.reject(cancellation('destroyed')) + promise.catch(() => {}) + return promise + } + tool.errorHandle(!state.job, 'There is currently a task in progress, please wait a moment...') + return createJob(tool, state) +} diff --git a/packages/artplayer-tool-thumbnail/src/index.js b/packages/artplayer-tool-thumbnail/src/index.js index 63f424b21..09b0a8bb4 100644 --- a/packages/artplayer-tool-thumbnail/src/index.js +++ b/packages/artplayer-tool-thumbnail/src/index.js @@ -1,9 +1,9 @@ import Emitter from './emitter' +import { startExtraction } from './extraction' import { connectInput, releaseInput, setupInput } from './input' +import { cleanupAll, closeState, releaseMedia, stateFor } from './lifecycle' import { createSheet, downloadSheet, screenshotPoints } from './sheet' -import { clamp, runPromisesInSeries, sleep } from './utils' - -const destroyed = new WeakSet() +import { loadSource } from './source' export default class ArtplayerToolThumbnail extends Emitter { constructor(option = {}) { @@ -13,19 +13,20 @@ export default class ArtplayerToolThumbnail extends Emitter { try { this.setup(Object.assign({}, ArtplayerToolThumbnail.DEFAULTS, option)) this.video = ArtplayerToolThumbnail.creatVideo() + stateFor(this).video = this.video this.duration = 0 this.inputChange = this.inputChange.bind(this) this.ondrop = this.ondrop.bind(this) connectInput(this, ArtplayerToolThumbnail.ondragover) } catch (error) { + closeState(this, 'construction failed') try { releaseInput(this) } catch {} try { - if (this.video?.parentNode) - this.video.parentNode.removeChild(this.video) + releaseMedia(this) } catch {} throw error @@ -48,7 +49,7 @@ export default class ArtplayerToolThumbnail extends Emitter { } ondrop(event) { - if (destroyed.has(this)) + if (stateFor(this).closed) return event.preventDefault() const file = event.dataTransfer.files[0] @@ -56,8 +57,8 @@ export default class ArtplayerToolThumbnail extends Emitter { } setup(option = {}) { - if (!destroyed.has(this)) - this.option = setupInput(this, option) + if (!stateFor(this).closed) + setupInput(this, option) return this } @@ -80,7 +81,7 @@ export default class ArtplayerToolThumbnail extends Emitter { } inputChange(event) { - if (destroyed.has(this)) + if (stateFor(this).closed) return const file = this.option.fileInput.files[0] this.loadVideo(file) @@ -88,67 +89,11 @@ export default class ArtplayerToolThumbnail extends Emitter { } loadVideo(file) { - if (file && !destroyed.has(this)) { - const canPlayType = this.video.canPlayType(file.type) - this.errorHandle( - canPlayType === 'maybe' || canPlayType === 'probably', - `Playback of this file format is not supported: ${file.type}`, - ) - const videoUrl = URL.createObjectURL(file) - this.videoUrl = videoUrl - this.file = file - this.emit('file', this.file) - this.video.src = videoUrl - this.emit('video', this.video) - } + loadSource(this, file) } start() { - if (!this.video.duration) - return sleep(1000).then(() => this.start()) - const { width, number, begin, end } = this.option - const height = (this.video.videoHeight / this.video.videoWidth) * width - this.option.height = height - this.option.begin = clamp(begin, 0, this.video.duration) - this.option.end = clamp(end || this.video.duration, begin, this.video.duration) - this.errorHandle(this.option.end > this.option.begin, `End time must be greater than the start time`) - this.duration = this.option.end - this.option.begin - this.density = number / this.duration - this.errorHandle(this.file && this.video, 'Please select the video file first') - this.errorHandle(!this.processing, 'There is currently a task in progress, please wait a moment...') - this.errorHandle(this.density <= 1, `The preview density cannot be greater than 1, but got ${this.density}`) - const screenshotDate = this.creatScreenshotDate() - const canvas = this.creatCanvas() - const context2D = canvas.getContext('2d') - this.emit('canvas', canvas) - const promiseList = screenshotDate.map((item, index) => () => { - return new Promise((resolve) => { - this.video.oncanplay = () => { - context2D.drawImage(this.video, item.x, item.y, width, height) - canvas.toBlob((blob) => { - if (this.thumbnailUrl) { - URL.revokeObjectURL(this.thumbnailUrl) - } - this.thumbnailUrl = URL.createObjectURL(blob) - this.emit('update', this.thumbnailUrl, (index + 1) / number) - this.video.oncanplay = null - resolve() - }) - } - this.video.currentTime = item.time - }) - }) - this.processing = true - return runPromisesInSeries(promiseList) - .then(() => { - this.processing = false - this.emit('done') - }) - .catch((err) => { - this.processing = false - this.emit('error', err.message) - throw err - }) + return startExtraction(this) } creatScreenshotDate() { @@ -160,6 +105,8 @@ export default class ArtplayerToolThumbnail extends Emitter { } download() { + if (stateFor(this).closed) + return this this.errorHandle( this.file && this.thumbnailUrl, 'Download does not seem to be ready, please create preview first', @@ -178,23 +125,12 @@ export default class ArtplayerToolThumbnail extends Emitter { } destroy() { - if (destroyed.has(this)) + if (!closeState(this)) return - destroyed.add(this) - let failure - for (const cleanup of [ + cleanupAll([ () => releaseInput(this), - () => this.video.parentNode?.removeChild(this.video), - () => this.videoUrl && URL.revokeObjectURL(this.videoUrl), - () => this.thumbnailUrl && URL.revokeObjectURL(this.thumbnailUrl), + () => releaseMedia(this), () => this.emit('destroy'), - ]) { - try { - cleanup() - } - catch (error) { failure ||= error } - } - if (failure) - throw failure + ]) } } diff --git a/packages/artplayer-tool-thumbnail/src/input.js b/packages/artplayer-tool-thumbnail/src/input.js index f209a9ad4..2c6ac66de 100644 --- a/packages/artplayer-tool-thumbnail/src/input.js +++ b/packages/artplayer-tool-thumbnail/src/input.js @@ -1,3 +1,4 @@ +import { cancellation, stateFor } from './lifecycle' import { clamp } from './utils' const inputs = new WeakMap() @@ -44,6 +45,9 @@ function connect(record, callbacks) { } export function setupInput(tool, patch) { + const state = stateFor(tool) + const epoch = ++state.inputEpoch + const current = () => !state.closed && state.inputEpoch === epoch const option = Object.assign({}, tool.option, patch) const target = option.fileInput tool.errorHandle(target instanceof Element, 'The \'fileInput\' is not a Element') @@ -52,10 +56,13 @@ export function setupInput(tool, patch) { option.number = clamp(option.number, 10, 1000) option.width = clamp(option.width, 10, 1000) option.column = clamp(option.column, 1, 1000) + if (!current()) + throw cancellation('input setup superseded') const previous = inputs.get(tool) if (previous && (target === previous.input || target === previous.wrapper)) { option.fileInput = previous.input + tool.option = option return option } const record = { input: target, wrapper: null, position: '', listeners: [], callbacks: previous?.callbacks || null } @@ -69,8 +76,12 @@ export function setupInput(tool, patch) { target.style.position = 'relative' target.appendChild(record.input) } + if (!current()) + throw cancellation('input setup superseded') if (record.callbacks) connect(record, record.callbacks) + if (!current()) + throw cancellation('input setup superseded') } catch (error) { try { diff --git a/packages/artplayer-tool-thumbnail/src/lifecycle.js b/packages/artplayer-tool-thumbnail/src/lifecycle.js new file mode 100644 index 000000000..40c648e91 --- /dev/null +++ b/packages/artplayer-tool-thumbnail/src/lifecycle.js @@ -0,0 +1,80 @@ +const states = new WeakMap() + +export function stateFor(tool) { + if (!states.has(tool)) { + states.set(tool, { + closed: false, + epoch: 0, + inputEpoch: 0, + job: null, + video: null, + sourceUrl: null, + thumbnailUrl: null, + sourceUrls: new Set(), + thumbnailUrls: new Set(), + sourceListeners: [], + loading: false, + }) + } + return states.get(tool) +} + +export function cancellation(reason) { + const error = new Error(`Thumbnail task cancelled: ${reason}`) + error.name = 'AbortError' + return error +} + +export function revoke(urls, url) { + if (!url) + return + urls.delete(url) + URL.revokeObjectURL(url) +} + +export function cleanupAll(callbacks) { + let failure + for (const callback of callbacks) { + try { + callback() + } + catch (error) { + failure ||= error + } + } + if (failure) + throw failure +} + +export function closeState(tool, reason = 'destroyed') { + const state = stateFor(tool) + if (state.closed) + return false + state.closed = true + state.epoch++ + state.job?.cancel(reason) + return true +} + +export function releaseMedia(tool) { + const state = stateFor(tool) + const sources = new Set([...state.sourceUrls, tool.videoUrl]) + const thumbnails = new Set([...state.thumbnailUrls, tool.thumbnailUrl]) + const media = [] + for (const video of new Set([state.video, tool.video])) { + if (video) { + media.push( + () => video.pause?.(), + () => video.removeAttribute?.('src'), + () => video.load?.(), + () => video.parentNode?.removeChild(video), + ) + } + } + cleanupAll([ + ...state.sourceListeners.splice(0), + ...media, + ...[...sources].map(url => () => revoke(state.sourceUrls, url)), + ...[...thumbnails].map(url => () => revoke(state.thumbnailUrls, url)), + ]) +} diff --git a/packages/artplayer-tool-thumbnail/src/source.js b/packages/artplayer-tool-thumbnail/src/source.js new file mode 100644 index 000000000..ff7f820ef --- /dev/null +++ b/packages/artplayer-tool-thumbnail/src/source.js @@ -0,0 +1,113 @@ +import { cleanupAll, revoke, stateFor } from './lifecycle' + +export function loadSource(tool, file) { + const state = stateFor(tool) + if (!file || state.closed) + return + const initialEpoch = state.epoch + const video = tool.video + const support = video.canPlayType(file.type) + tool.errorHandle(support === 'maybe' || support === 'probably', `Playback of this file format is not supported: ${file.type}`) + if (state.closed || state.epoch !== initialEpoch) + return + const url = URL.createObjectURL(file) + state.sourceUrls.add(url) + if (state.closed || state.epoch !== initialEpoch) { + revoke(state.sourceUrls, url) + return + } + const epoch = ++state.epoch + const initialWait = !state.sourceUrl && !tool.file && state.job?.adopt(epoch) + if (!initialWait) + state.job?.cancel('source changed') + state.loading = true + const current = () => !state.closed && state.epoch === epoch + const previousListeners = state.sourceListeners + const listeners = [] + state.sourceListeners = listeners + let pendingCleanup = () => {} + let reported = false + const error = () => { + if (!current() || reported || (video.currentSrc && video.currentSrc !== url)) + return + reported = true + state.loading = false + const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`) + if (state.job) + state.job.fail(failure) + else + tool.emit('error', failure.message) + } + const ready = () => { + if (current() && (!video.currentSrc || video.currentSrc === url)) { + state.loading = false + state.job?.ready() + } + } + try { + cleanupAll(previousListeners.splice(0)) + if (!current()) { + revoke(state.sourceUrls, url) + return + } + for (const [name, callback] of [['error', error], ['loadedmetadata', ready]]) { + pendingCleanup = () => video.removeEventListener(name, callback) + listeners.push(pendingCleanup) + video.addEventListener(name, callback) + if (!current()) { + cleanupAll([pendingCleanup, ...listeners.splice(0)]) + revoke(state.sourceUrls, url) + return + } + } + tool.videoUrl = url + tool.file = file + tool.emit('file', file) + if (!current()) { + revoke(state.sourceUrls, url) + return + } + video.src = url + if (!current()) { + revoke(state.sourceUrls, url) + return + } + state.sourceUrl = url + for (const previous of [...state.sourceUrls]) { + if (previous !== url) + revoke(state.sourceUrls, previous) + } + if (current()) + tool.emit('video', video) + } + catch (error) { + if (current()) { + state.loading = false + state.job?.cancel('source setup failed') + } + try { + cleanupAll([pendingCleanup, ...listeners.splice(0)]) + } + catch {} + revoke(state.sourceUrls, url) + throw error + } +} + +export function replaceThumbnail(tool, blob, live) { + const state = stateFor(tool) + const url = URL.createObjectURL(blob) + state.thumbnailUrls.add(url) + if (!live()) { + revoke(state.thumbnailUrls, url) + return null + } + const previous = new Set([...state.thumbnailUrls, tool.thumbnailUrl]) + state.thumbnailUrl = url + tool.thumbnailUrl = url + for (const old of previous) { + if (old && old !== url) + revoke(state.thumbnailUrls, old) + } + return live() ? url : null +} diff --git a/refactor/baselines/thumbnail-lifecycle-validation.json b/refactor/baselines/thumbnail-lifecycle-validation.json new file mode 100644 index 000000000..5cd0748ae --- /dev/null +++ b/refactor/baselines/thumbnail-lifecycle-validation.json @@ -0,0 +1,7406 @@ +{ + "schemaVersion": 1, + "task": "PKG-TOOL-THUMB-03", + "verifiedAt": "2026-09-12T20:22:58.522Z", + "startedFrom": "426c0c5d4209aa91be7414b810f1e8dca712c297", + "nodeVersion": "v24.21.0", + "node": { + "frozenBefore": { + "total": 26, + "passed": 2, + "failed": 24 + }, + "source": { + "passed": 40, + "failed": 0 + }, + "main": { + "passed": 40, + "failed": 0 + }, + "legacy": { + "passed": 40, + "failed": 0 + } + }, + "ci": { + "total": 1646, + "unit": 1441, + "engineering": 14, + "baseline": 191, + "productionTs": 330, + "imports": 3, + "builtEsmImport": true + }, + "browser": { + "total": 171, + "retries": 0, + "skips": 0, + "runs": [ + { + "variant": "source", + "counts": { + "actual-tool-scenario": 34, + "candidate-native-input": 3, + "blob-unavailable-control": 17, + "native-media-control": 3 + }, + "cases": [ + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "chromium", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 287, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "firefox", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 1766, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "webkit", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 806, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "chromium", + "kind": "native-media-control", + "status": "passed", + "durationMs": 278, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.046009, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.046009, + "width": 640 + } + ] + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "firefox", + "kind": "native-media-control", + "status": "passed", + "durationMs": 1853, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 3, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.045986, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.045986, + "width": 640 + } + ] + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "webkit", + "kind": "native-media-control", + "status": "passed", + "durationMs": 1958, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 3, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 2, + "error": 0, + "duration": 90.094, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + } + ] + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 8085, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 150, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 30, + "mime": "image/png", + "bytes": 26427, + "nonBlack": 2400, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/201e7377-2945-46de-a267-e6e3cfd452cf", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 205, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 241, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/57da18a7-b085-43f6-8b93-77f8d8d0af84" + }, + "result": { + "value": "C:\\fakepath\\thumbnail-pattern.mp4", + "created": [ + "blob:http://127.0.0.1:8084/57da18a7-b085-43f6-8b93-77f8d8d0af84", + "blob:http://127.0.0.1:8084/6fe91253-e33a-4463-9b37-1758110dd768" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/6fe91253-e33a-4463-9b37-1758110dd768" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 589, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/a69d3ef6-f610-4b0f-87bf-40b216ec792b", + "blob:http://127.0.0.1:8084/4087d798-9165-4bed-a349-e4d1f9b6e5f1" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/a69d3ef6-f610-4b0f-87bf-40b216ec792b" + ], + "pendingState": "pending", + "events": [ + "file", + "canvas", + "video", + "destroy", + "update" + ], + "connected": false, + "bytes": 7540 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 8292, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 150, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 30, + "mime": "image/png", + "bytes": 18224, + "nonBlack": 2400, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/491ff3b2-a653-46b0-ad46-748ff76ef144", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 442, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 583, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/55e97384-fc40-455d-af39-ac33cfb3da3a" + }, + "result": { + "value": "C:\\fakepath\\thumbnail-pattern.mp4", + "created": [ + "blob:http://127.0.0.1:8084/55e97384-fc40-455d-af39-ac33cfb3da3a", + "blob:http://127.0.0.1:8084/0a3fb0b1-472c-499c-89d8-a60bbb1fe5b5" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/0a3fb0b1-472c-499c-89d8-a60bbb1fe5b5" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1777, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/10e4e25d-3511-4e81-b154-cf47a8346187", + "blob:http://127.0.0.1:8084/8a4169cd-e9d4-4878-94e0-efa017f33092" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/10e4e25d-3511-4e81-b154-cf47a8346187" + ], + "pendingState": "pending", + "events": [ + "file", + "canvas", + "video", + "destroy", + "update" + ], + "connected": false, + "bytes": 4980 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1355, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1249, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1379, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1268, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 519, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/672e14ea-d2e2-4c44-b407-3a7fa851c5e5", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 207, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 234, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/04f2df2f-dcf5-4c3c-839c-b10a6e65c443" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/04f2df2f-dcf5-4c3c-839c-b10a6e65c443", + "blob:http://127.0.0.1:8084/a1717fe4-c398-4092-a054-83580468da06" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/a1717fe4-c398-4092-a054-83580468da06" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 337, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/fcfcff5c-f57d-48b7-b4ea-6a1233a93a63", + "blob:http://127.0.0.1:8084/91974e40-9f76-429b-ac02-33c7eba5dbf5" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/fcfcff5c-f57d-48b7-b4ea-6a1233a93a63" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1464, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38895, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/99fffdd5-df46-47c3-8827-e95d16bf7a27", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 403, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 476, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/0dc03b5b-76ec-403b-9944-043ad256e6a0" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/0dc03b5b-76ec-403b-9944-043ad256e6a0", + "blob:http://127.0.0.1:8084/d6584abe-40fe-4adf-9589-a795fbcd9f09" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/d6584abe-40fe-4adf-9589-a795fbcd9f09" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 731, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/3e9f8e01-e357-47c3-bb75-f299968eb1bb", + "blob:http://127.0.0.1:8084/61f3dc7b-68fc-47ea-ae5a-3967247829fd" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/3e9f8e01-e357-47c3-bb75-f299968eb1bb" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1245, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1349, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1298, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1313, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 519, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/32cd37af-02b2-4451-b870-8c48629bc713", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 221, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 242, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/635067f9-62cd-4cc9-8a1f-47b19952818d" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/635067f9-62cd-4cc9-8a1f-47b19952818d", + "blob:http://127.0.0.1:8084/efa049db-f4f0-4b25-b765-c2c41de5d152" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/efa049db-f4f0-4b25-b765-c2c41de5d152" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 337, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/ae923b00-f44d-4b5c-845c-7c70779b1172", + "blob:http://127.0.0.1:8084/4cfe32d8-9bf3-403a-af80-5958e0cee208" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/ae923b00-f44d-4b5c-845c-7c70779b1172" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1794, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38895, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/c787058c-235b-4054-80e2-71fc8eeb0808", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 439, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 581, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/fce22555-47a6-4497-b42b-e3c69c0eb04d" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/fce22555-47a6-4497-b42b-e3c69c0eb04d", + "blob:http://127.0.0.1:8084/5a7026c1-d1ea-4f07-be79-7bf988f2a8d9" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/5a7026c1-d1ea-4f07-be79-7bf988f2a8d9" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 586, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/49801730-901e-413d-886c-63757cf87aca", + "blob:http://127.0.0.1:8084/e9504e68-29ae-4a45-9211-01a35d309c8c" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/49801730-901e-413d-886c-63757cf87aca" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1431, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1427, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1356, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1276, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 527, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 0, + "retiredSrc": null, + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 189, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 213, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 1, + "first": "blob:http://127.0.0.1:8084/ef4bcb23-d862-4d75-accd-fe30c368a71a" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/ef4bcb23-d862-4d75-accd-fe30c368a71a", + "blob:http://127.0.0.1:8084/4a12131c-05f8-4c3f-8106-36632eac4688" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/ef4bcb23-d862-4d75-accd-fe30c368a71a", + "blob:http://127.0.0.1:8084/4a12131c-05f8-4c3f-8106-36632eac4688" + ] + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / native PNG callback delivered after destroy is ignored after cancellation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 333, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/5c9947d2-dc48-4f6f-a0f1-efe73c1150c2" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/5c9947d2-dc48-4f6f-a0f1-efe73c1150c2" + ], + "pendingState": "rejected", + "events": [ + "file", + "video", + "canvas", + "destroy" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / source replacement cancels held native encoding and completes only the latest job", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 491, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-native-source-cancellation", + "data": { + "oldState": "AbortError", + "firstEncodedBytes": 9120, + "source": "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "updateSources": [ + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618" + ], + "created": [ + "blob:http://127.0.0.1:8084/ff5cafa3-37c1-4fc6-bf8a-7f4285a9917f", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/f4781e68-38cf-4592-a57d-6e8e695551ad", + "blob:http://127.0.0.1:8084/16b10617-debb-49bd-93fd-4b4566b22c6d", + "blob:http://127.0.0.1:8084/7a217ce9-9849-46c6-b0e5-53ce71a3cde0", + "blob:http://127.0.0.1:8084/b4ab6465-342f-4700-80e3-16cca10722b9", + "blob:http://127.0.0.1:8084/6af0f50c-ba16-4e40-bd9e-f432459a83d2", + "blob:http://127.0.0.1:8084/114621a9-9c8b-4cc1-a70f-fc450b5ceaf7", + "blob:http://127.0.0.1:8084/fb748ed4-815e-4b0d-a92c-ef8bff97415e", + "blob:http://127.0.0.1:8084/ef3c2874-746f-490f-9b3a-4dfa3f1f94b0", + "blob:http://127.0.0.1:8084/0c4793c7-58e8-4561-a196-fcf842210934", + "blob:http://127.0.0.1:8084/ba41b11f-2b53-431a-a32a-9a385805f8ed" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/ff5cafa3-37c1-4fc6-bf8a-7f4285a9917f", + "blob:http://127.0.0.1:8084/f4781e68-38cf-4592-a57d-6e8e695551ad", + "blob:http://127.0.0.1:8084/16b10617-debb-49bd-93fd-4b4566b22c6d", + "blob:http://127.0.0.1:8084/7a217ce9-9849-46c6-b0e5-53ce71a3cde0", + "blob:http://127.0.0.1:8084/b4ab6465-342f-4700-80e3-16cca10722b9", + "blob:http://127.0.0.1:8084/6af0f50c-ba16-4e40-bd9e-f432459a83d2", + "blob:http://127.0.0.1:8084/114621a9-9c8b-4cc1-a70f-fc450b5ceaf7", + "blob:http://127.0.0.1:8084/fb748ed4-815e-4b0d-a92c-ef8bff97415e", + "blob:http://127.0.0.1:8084/ef3c2874-746f-490f-9b3a-4dfa3f1f94b0", + "blob:http://127.0.0.1:8084/0c4793c7-58e8-4561-a196-fcf842210934", + "blob:http://127.0.0.1:8084/48c2872d-3a4c-4876-b0e8-382f44299618", + "blob:http://127.0.0.1:8084/ba41b11f-2b53-431a-a32a-9a385805f8ed" + ], + "events": [ + "file", + "video", + "canvas", + "file", + "video", + "canvas", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "done", + "destroy" + ], + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1550, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38560, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 0, + "retiredSrc": null, + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 466, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 483, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 1, + "first": "blob:http://127.0.0.1:8084/823a0089-4ce0-4ede-9feb-9faef815c542" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/823a0089-4ce0-4ede-9feb-9faef815c542", + "blob:http://127.0.0.1:8084/b7b06494-4c6d-4fa6-887d-c71cf9759d36" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/823a0089-4ce0-4ede-9feb-9faef815c542", + "blob:http://127.0.0.1:8084/b7b06494-4c6d-4fa6-887d-c71cf9759d36" + ] + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / native PNG callback delivered after destroy is ignored after cancellation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 619, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/950fa191-2bd0-40aa-a130-1f87d5f3c82d" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/950fa191-2bd0-40aa-a130-1f87d5f3c82d" + ], + "pendingState": "rejected", + "events": [ + "file", + "video", + "canvas", + "destroy" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / source replacement cancels held native encoding and completes only the latest job", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1663, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-native-source-cancellation", + "data": { + "oldState": "AbortError", + "firstEncodedBytes": 6496, + "source": "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "updateSources": [ + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd" + ], + "created": [ + "blob:http://127.0.0.1:8084/826f24fc-de36-48ad-b6cf-9eb88ec45b92", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/52525799-9ecf-4dc4-a544-56b0e45c959d", + "blob:http://127.0.0.1:8084/3f08b02f-1764-4bfd-ab95-49157c7ce29e", + "blob:http://127.0.0.1:8084/7ca4309c-6ff6-4a5f-a5ff-61745be2e549", + "blob:http://127.0.0.1:8084/5b5dc2ae-2c25-42bb-a2ff-d0190e23749a", + "blob:http://127.0.0.1:8084/f9714f91-4fff-4189-aaee-3c50684b15f8", + "blob:http://127.0.0.1:8084/c918b994-e1e7-48d8-b660-8d512e249090", + "blob:http://127.0.0.1:8084/293dbd26-cccf-4020-8d4a-2625b8da63c5", + "blob:http://127.0.0.1:8084/fac71326-cadc-4979-b0ec-49dc0c777511", + "blob:http://127.0.0.1:8084/ccb8530a-5c9d-45ac-a30b-85d9e4d8168d", + "blob:http://127.0.0.1:8084/3a817890-7b82-459d-9877-f77a8b4e8868" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/826f24fc-de36-48ad-b6cf-9eb88ec45b92", + "blob:http://127.0.0.1:8084/52525799-9ecf-4dc4-a544-56b0e45c959d", + "blob:http://127.0.0.1:8084/3f08b02f-1764-4bfd-ab95-49157c7ce29e", + "blob:http://127.0.0.1:8084/7ca4309c-6ff6-4a5f-a5ff-61745be2e549", + "blob:http://127.0.0.1:8084/5b5dc2ae-2c25-42bb-a2ff-d0190e23749a", + "blob:http://127.0.0.1:8084/f9714f91-4fff-4189-aaee-3c50684b15f8", + "blob:http://127.0.0.1:8084/c918b994-e1e7-48d8-b660-8d512e249090", + "blob:http://127.0.0.1:8084/293dbd26-cccf-4020-8d4a-2625b8da63c5", + "blob:http://127.0.0.1:8084/fac71326-cadc-4979-b0ec-49dc0c777511", + "blob:http://127.0.0.1:8084/ccb8530a-5c9d-45ac-a30b-85d9e4d8168d", + "blob:http://127.0.0.1:8084/b20abc5c-6c25-4509-afd8-62dcc207eadd", + "blob:http://127.0.0.1:8084/3a817890-7b82-459d-9877-f77a8b4e8868" + ], + "events": [ + "file", + "video", + "canvas", + "file", + "video", + "canvas", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "done", + "destroy" + ], + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1876, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1301, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1327, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / native PNG callback delivered after destroy is ignored after cancellation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1287, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-source / source replacement cancels held native encoding and completes only the latest job", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1262, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-source", + "sha256": "f14504cae03a0b9e17969e211b75c583ad9038c6b17302bb27eabaaacf52de21", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + } + ] + }, + { + "variant": "main", + "counts": { + "actual-tool-scenario": 34, + "candidate-native-input": 3, + "blob-unavailable-control": 17, + "native-media-control": 3 + }, + "cases": [ + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "chromium", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 308, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "firefox", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 1784, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "webkit", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 1200, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "chromium", + "kind": "native-media-control", + "status": "passed", + "durationMs": 281, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.046009, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.046009, + "width": 640 + } + ] + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "firefox", + "kind": "native-media-control", + "status": "passed", + "durationMs": 1902, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.045986, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.045986, + "width": 640 + } + ] + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "webkit", + "kind": "native-media-control", + "status": "passed", + "durationMs": 2854, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 3, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 2, + "error": 0, + "duration": 90.094, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + } + ] + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 8150, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 150, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 30, + "mime": "image/png", + "bytes": 26427, + "nonBlack": 2400, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/ee7c3bd0-e453-47f3-97f4-6b4452b05172", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 223, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 250, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/0d3cbcdf-c5f0-40c3-a0ff-b0b00efa5a60" + }, + "result": { + "value": "C:\\fakepath\\thumbnail-pattern.mp4", + "created": [ + "blob:http://127.0.0.1:8084/0d3cbcdf-c5f0-40c3-a0ff-b0b00efa5a60", + "blob:http://127.0.0.1:8084/01d97782-658c-4184-970a-8c3ad179133a" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/01d97782-658c-4184-970a-8c3ad179133a" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 623, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/3c1741eb-e429-4b71-bfff-f9ec4c61c3c4", + "blob:http://127.0.0.1:8084/3a8561a7-2468-4450-89d2-7dc9b18f25f3" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/3c1741eb-e429-4b71-bfff-f9ec4c61c3c4" + ], + "pendingState": "pending", + "events": [ + "file", + "canvas", + "video", + "destroy", + "update" + ], + "connected": false, + "bytes": 7540 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 8278, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 150, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 30, + "mime": "image/png", + "bytes": 18224, + "nonBlack": 2400, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/bf5a050b-fa17-4492-84e1-72a4d7c397d6", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 486, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1757, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/91aefb41-eceb-4244-a5be-06aa4d56bad7" + }, + "result": { + "value": "C:\\fakepath\\thumbnail-pattern.mp4", + "created": [ + "blob:http://127.0.0.1:8084/91aefb41-eceb-4244-a5be-06aa4d56bad7", + "blob:http://127.0.0.1:8084/1297cb8d-d590-4aa8-b39b-0d87cd5e1fb9" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/1297cb8d-d590-4aa8-b39b-0d87cd5e1fb9" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 856, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/f3d53a57-4208-440f-9c52-6a07d5fe028e", + "blob:http://127.0.0.1:8084/d594fb5b-1474-4507-9429-acaa0497754c" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/f3d53a57-4208-440f-9c52-6a07d5fe028e" + ], + "pendingState": "pending", + "events": [ + "file", + "canvas", + "video", + "destroy", + "update" + ], + "connected": false, + "bytes": 4980 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1435, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1408, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1505, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1580, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 532, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/fababc34-b9cd-4c49-922a-90ed755fccc6", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 225, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 226, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/4f5ba156-d15f-4c4f-8828-e04cc3741c9b" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/4f5ba156-d15f-4c4f-8828-e04cc3741c9b", + "blob:http://127.0.0.1:8084/49903451-f55e-4f74-9e3d-8316b2a1906c" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/49903451-f55e-4f74-9e3d-8316b2a1906c" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 352, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/441a6c1b-0bdf-4386-815f-701988000b0b", + "blob:http://127.0.0.1:8084/7b6dc610-8b15-4dbf-8fe5-83ade28c8e45" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/441a6c1b-0bdf-4386-815f-701988000b0b" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1640, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38895, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/875882a8-2e00-4259-aab5-39935c56d111", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 453, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 649, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/81571526-6518-464e-8de6-ba81ac979610" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/81571526-6518-464e-8de6-ba81ac979610", + "blob:http://127.0.0.1:8084/00acd578-7077-4557-9402-8417ba6011f8" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/00acd578-7077-4557-9402-8417ba6011f8" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 662, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/1291f098-4fbd-4cc0-bb2f-dd45c5ebdf02", + "blob:http://127.0.0.1:8084/b0f59e4a-286c-4d46-a1ca-bc61af708e74" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/1291f098-4fbd-4cc0-bb2f-dd45c5ebdf02" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1633, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1624, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1611, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1532, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 552, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/6319836f-729e-4520-90e6-aeed190fad92", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 202, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 259, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/e387dd0e-24a0-4dc5-ade5-cc6d2bf8307e" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/e387dd0e-24a0-4dc5-ade5-cc6d2bf8307e", + "blob:http://127.0.0.1:8084/9fd99295-0130-44fe-8f2a-c8cf11359cc2" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/9fd99295-0130-44fe-8f2a-c8cf11359cc2" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 368, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/474cb4fe-4680-4d36-8dc4-651233e12ae8", + "blob:http://127.0.0.1:8084/33fbb391-2cbc-4f66-a113-aad0ebb3e037" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/474cb4fe-4680-4d36-8dc4-651233e12ae8" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1688, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38895, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/2f5690e6-c1ce-44d1-81b4-34d49dfb3bb5", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 569, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 688, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/baae6f8a-61a9-4e07-b7a0-a9e0ca2ef76e" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/baae6f8a-61a9-4e07-b7a0-a9e0ca2ef76e", + "blob:http://127.0.0.1:8084/751e8d25-d5f3-4086-abb1-2b345dd204a0" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/751e8d25-d5f3-4086-abb1-2b345dd204a0" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 728, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/d28e2f35-492d-4c3d-95f6-f9e203d78fe4", + "blob:http://127.0.0.1:8084/ef0cbdb7-0a36-4643-800e-b07c9e83195b" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/d28e2f35-492d-4c3d-95f6-f9e203d78fe4" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1702, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1706, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1601, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1693, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 607, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 0, + "retiredSrc": null, + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 190, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 318, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 1, + "first": "blob:http://127.0.0.1:8084/8463c146-bfd6-4796-ba44-c369b0ebae7d" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/8463c146-bfd6-4796-ba44-c369b0ebae7d", + "blob:http://127.0.0.1:8084/2e265a11-1fa2-4c16-a336-2342fa2b8ca6" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/8463c146-bfd6-4796-ba44-c369b0ebae7d", + "blob:http://127.0.0.1:8084/2e265a11-1fa2-4c16-a336-2342fa2b8ca6" + ] + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native PNG callback delivered after destroy is ignored after cancellation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 462, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/3ad005aa-3b76-4942-af92-b9d789201de2" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/3ad005aa-3b76-4942-af92-b9d789201de2" + ], + "pendingState": "rejected", + "events": [ + "file", + "video", + "canvas", + "destroy" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / source replacement cancels held native encoding and completes only the latest job", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 542, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-native-source-cancellation", + "data": { + "oldState": "AbortError", + "firstEncodedBytes": 9120, + "source": "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "updateSources": [ + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90" + ], + "created": [ + "blob:http://127.0.0.1:8084/8c5c88c0-2a7d-4ddc-9a34-4586c4b016df", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/6bedaf7f-d1e3-449f-b831-66320908b103", + "blob:http://127.0.0.1:8084/b5b26a52-6522-40c8-99f9-bb3ad70983b4", + "blob:http://127.0.0.1:8084/2e0d003a-7bb2-4548-b024-347ac095665b", + "blob:http://127.0.0.1:8084/8a1a3c80-588e-4a73-9d26-deeda1a5485d", + "blob:http://127.0.0.1:8084/2e557eee-570d-49cd-ac05-ef11c86f5e81", + "blob:http://127.0.0.1:8084/c119550e-7219-4bc2-be9c-c215ad012916", + "blob:http://127.0.0.1:8084/3474146b-498c-42c7-8808-dd1dafc27ddb", + "blob:http://127.0.0.1:8084/8ba5e6d1-66b3-438c-b872-808616ab9d28", + "blob:http://127.0.0.1:8084/0367a0d1-52ef-468e-9a1f-ed9af48ab70c", + "blob:http://127.0.0.1:8084/cb8cc652-e33e-48a4-a911-02f149c77d9a" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/8c5c88c0-2a7d-4ddc-9a34-4586c4b016df", + "blob:http://127.0.0.1:8084/6bedaf7f-d1e3-449f-b831-66320908b103", + "blob:http://127.0.0.1:8084/b5b26a52-6522-40c8-99f9-bb3ad70983b4", + "blob:http://127.0.0.1:8084/2e0d003a-7bb2-4548-b024-347ac095665b", + "blob:http://127.0.0.1:8084/8a1a3c80-588e-4a73-9d26-deeda1a5485d", + "blob:http://127.0.0.1:8084/2e557eee-570d-49cd-ac05-ef11c86f5e81", + "blob:http://127.0.0.1:8084/c119550e-7219-4bc2-be9c-c215ad012916", + "blob:http://127.0.0.1:8084/3474146b-498c-42c7-8808-dd1dafc27ddb", + "blob:http://127.0.0.1:8084/8ba5e6d1-66b3-438c-b872-808616ab9d28", + "blob:http://127.0.0.1:8084/0367a0d1-52ef-468e-9a1f-ed9af48ab70c", + "blob:http://127.0.0.1:8084/20bc02ff-bc7c-4bd5-b576-279a4d5ade90", + "blob:http://127.0.0.1:8084/cb8cc652-e33e-48a4-a911-02f149c77d9a" + ], + "events": [ + "file", + "video", + "canvas", + "file", + "video", + "canvas", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "done", + "destroy" + ], + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 2352, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38895, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 0, + "retiredSrc": null, + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 498, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 610, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 1, + "first": "blob:http://127.0.0.1:8084/23f48e3f-775b-4d16-b23c-a0b6847ce916" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/23f48e3f-775b-4d16-b23c-a0b6847ce916", + "blob:http://127.0.0.1:8084/79981a7c-2c46-41f1-b3c6-57f121a15bcd" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/23f48e3f-775b-4d16-b23c-a0b6847ce916", + "blob:http://127.0.0.1:8084/79981a7c-2c46-41f1-b3c6-57f121a15bcd" + ] + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native PNG callback delivered after destroy is ignored after cancellation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 657, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/6dde3abb-bd50-4565-98b5-6f90e95f185a" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/6dde3abb-bd50-4565-98b5-6f90e95f185a" + ], + "pendingState": "rejected", + "events": [ + "file", + "video", + "canvas", + "destroy" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / source replacement cancels held native encoding and completes only the latest job", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1388, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-native-source-cancellation", + "data": { + "oldState": "AbortError", + "firstEncodedBytes": 6496, + "source": "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "updateSources": [ + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364" + ], + "created": [ + "blob:http://127.0.0.1:8084/2d42487d-7a72-492b-9585-ef285a3cda70", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/d9e5c163-b152-494c-9bf0-235893b39fe8", + "blob:http://127.0.0.1:8084/7aeeaa95-5584-413a-a017-89f501ae6b40", + "blob:http://127.0.0.1:8084/422ce219-0b03-46a9-a3f7-592350332be7", + "blob:http://127.0.0.1:8084/0427c129-4ab0-4e92-8327-8f8297b19175", + "blob:http://127.0.0.1:8084/fc6317e5-e5cc-420b-98f0-be70b47376cf", + "blob:http://127.0.0.1:8084/1b192ba2-4bc5-49ce-a5f7-acff66ba0151", + "blob:http://127.0.0.1:8084/927a2c6b-2380-4d2f-8fe6-1131784ca630", + "blob:http://127.0.0.1:8084/ef94728a-5fdf-42a6-9168-7eb813f25ae3", + "blob:http://127.0.0.1:8084/ef3988b4-9ac8-4686-bef1-1a76a1bd2578", + "blob:http://127.0.0.1:8084/a0460b5e-0886-4424-bc0f-f6abfd77ec9d" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/2d42487d-7a72-492b-9585-ef285a3cda70", + "blob:http://127.0.0.1:8084/d9e5c163-b152-494c-9bf0-235893b39fe8", + "blob:http://127.0.0.1:8084/7aeeaa95-5584-413a-a017-89f501ae6b40", + "blob:http://127.0.0.1:8084/422ce219-0b03-46a9-a3f7-592350332be7", + "blob:http://127.0.0.1:8084/0427c129-4ab0-4e92-8327-8f8297b19175", + "blob:http://127.0.0.1:8084/fc6317e5-e5cc-420b-98f0-be70b47376cf", + "blob:http://127.0.0.1:8084/1b192ba2-4bc5-49ce-a5f7-acff66ba0151", + "blob:http://127.0.0.1:8084/927a2c6b-2380-4d2f-8fe6-1131784ca630", + "blob:http://127.0.0.1:8084/ef94728a-5fdf-42a6-9168-7eb813f25ae3", + "blob:http://127.0.0.1:8084/ef3988b4-9ac8-4686-bef1-1a76a1bd2578", + "blob:http://127.0.0.1:8084/5c3881b3-690a-4ef0-a15b-a5abf84c8364", + "blob:http://127.0.0.1:8084/a0460b5e-0886-4424-bc0f-f6abfd77ec9d" + ], + "events": [ + "file", + "video", + "canvas", + "file", + "video", + "canvas", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "done", + "destroy" + ], + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1605, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2253, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1570, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native PNG callback delivered after destroy is ignored after cancellation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1604, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / source replacement cancels held native encoding and completes only the latest job", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1384, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + } + ] + }, + { + "variant": "legacy", + "counts": { + "actual-tool-scenario": 34, + "candidate-native-input": 3, + "blob-unavailable-control": 17, + "native-media-control": 3 + }, + "cases": [ + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "chromium", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 1081, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "firefox", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 3114, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-input.spec.js / Thumbnail input ownership replaces real DOM listeners and generated inputs", + "project": "webkit", + "kind": "candidate-native-input", + "status": "passed", + "durationMs": 1840, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-input-ownership", + "data": { + "implementation": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "scope": "Native file input/DOM/listeners only; does not claim media decoding.", + "switched": { + "value": "", + "events": [ + "thumbnail-pattern.mp4" + ], + "firstPosition": "sticky", + "disconnected": true + }, + "result": { + "events": [ + "thumbnail-pattern.mp4", + "thumbnail-pattern.mp4" + ], + "sameError": true, + "notifications": 1, + "inputConnected": false, + "videoConnected": false, + "secondPosition": "absolute", + "inputs": 0 + } + } + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "chromium", + "kind": "native-media-control", + "status": "passed", + "durationMs": 568, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.046009, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.046009, + "width": 640 + } + ] + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "firefox", + "kind": "native-media-control", + "status": "passed", + "durationMs": 7345, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.045986, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 90.045986, + "width": 640 + } + ] + } + ] + }, + { + "title": "thumbnail-native.spec.js / Thumbnail native HTTP and Blob media controls", + "project": "webkit", + "kind": "native-media-control", + "status": "passed", + "durationMs": 3625, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-native-media-controls", + "data": [ + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 3, + "error": 0, + "duration": 8, + "width": 320 + }, + { + "url": "/test/pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 4, + "error": 0, + "duration": 24, + "width": 320 + }, + { + "url": "/test/thumbnail-pattern.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "http", + "outcome": "loaded", + "readyState": 3, + "error": 0, + "duration": 90.094, + "width": 640 + }, + { + "url": "/assets/sample/video.mp4", + "type": "video/mp4", + "transport": "blob", + "outcome": "error", + "readyState": 0, + "error": 4, + "duration": null, + "width": 0 + } + ] + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 8889, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 150, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 30, + "mime": "image/png", + "bytes": 26427, + "nonBlack": 2400, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/b0f0b1fc-8451-4fe6-b498-e00a7b0b934c", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 446, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 474, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/b8e789fb-95a2-437f-9875-2337829fcc37" + }, + "result": { + "value": "C:\\fakepath\\thumbnail-pattern.mp4", + "created": [ + "blob:http://127.0.0.1:8084/b8e789fb-95a2-437f-9875-2337829fcc37", + "blob:http://127.0.0.1:8084/8e557704-d798-498f-b262-ee0ecc7c0b76" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/8e557704-d798-498f-b262-ee0ecc7c0b76" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1006, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/12b56a1b-3bbb-4798-91d5-58b9876b43c9", + "blob:http://127.0.0.1:8084/c2aef5d6-8c06-46ae-9405-12d3bec73dfc" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/12b56a1b-3bbb-4798-91d5-58b9876b43c9" + ], + "pendingState": "pending", + "events": [ + "file", + "canvas", + "video", + "destroy", + "update" + ], + "connected": false, + "bytes": 7540 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 13795, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 150, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 30, + "mime": "image/png", + "bytes": 20020, + "nonBlack": 2400, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/0acbc09b-fb4b-4ab4-ba9c-b03f413ec4e9", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1434, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1192, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/eeb29c1b-2214-4b27-8e08-3b97ccc2ca2e" + }, + "result": { + "value": "C:\\fakepath\\thumbnail-pattern.mp4", + "created": [ + "blob:http://127.0.0.1:8084/eeb29c1b-2214-4b27-8e08-3b97ccc2ca2e", + "blob:http://127.0.0.1:8084/18eca395-a2fe-4340-9a4c-5f70f1fe1387" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/18eca395-a2fe-4340-9a4c-5f70f1fe1387" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1611, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/36e45623-2887-48c6-9ad5-82eb736dce11", + "blob:http://127.0.0.1:8084/fc04dc77-62ff-465c-9321-0c936bc46095" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/36e45623-2887-48c6-9ad5-82eb736dce11" + ], + "pendingState": "pending", + "events": [ + "file", + "canvas", + "video", + "destroy", + "update" + ], + "connected": false, + "bytes": 4980 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2255, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2345, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2092, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool cdn-3.5.31 / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2095, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "cdn-3.5.31", + "sha256": "27fdc506c9b228c40df39f8508b3a42c145571c06c0b9cc60327a007a2064c87", + "recoveredIndividualFile": true, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1175, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/90dc5c2f-84dc-4450-a5ba-692e1073bcd0", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 338, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 536, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/fe31f7bd-e7ec-4003-9707-e23dad252304" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/fe31f7bd-e7ec-4003-9707-e23dad252304", + "blob:http://127.0.0.1:8084/b4d458a9-a380-402a-8325-4d18727b5151" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/b4d458a9-a380-402a-8325-4d18727b5151" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1053, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/d18fc845-a525-4837-9606-c5ce37448065", + "blob:http://127.0.0.1:8084/37c87ff9-cbef-43f1-b4eb-2ff25d800000" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/d18fc845-a525-4837-9606-c5ce37448065" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 3843, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38560, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/7cf8b29c-4f8f-4e4f-a5d7-876c797e9ab9", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1333, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1239, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/4b16610a-116d-42c6-a453-b42f181d9781" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/4b16610a-116d-42c6-a453-b42f181d9781", + "blob:http://127.0.0.1:8084/76a49189-07e8-4d10-b8d6-31e728e835f3" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/76a49189-07e8-4d10-b8d6-31e728e835f3" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1387, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/bba9025b-63b2-4051-9523-e1291f58ade8", + "blob:http://127.0.0.1:8084/c1a0af8d-7a05-4e65-a0e7-a0a3b4b20fe6" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/bba9025b-63b2-4051-9523-e1291f58ade8" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1992, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2158, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2122, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2509, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1043, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/79232f7d-16eb-456a-bcbc-3bcd9320e47f", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 555, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 463, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/0bb3ee13-d3d8-4873-ac7c-81c28234d4a8" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/0bb3ee13-d3d8-4873-ac7c-81c28234d4a8", + "blob:http://127.0.0.1:8084/6b873241-74eb-4d3c-9f47-63f32240a0f0" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/6b873241-74eb-4d3c-9f47-63f32240a0f0" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 885, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/18fd45a1-f830-4e58-ac36-745d50271447", + "blob:http://127.0.0.1:8084/acca6087-cb2c-4056-be85-1523efd77d9e" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/18fd45a1-f830-4e58-ac36-745d50271447" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 3668, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38895, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 4, + "retiredSrc": "blob:http://127.0.0.1:8084/2bd3771e-f41d-4b7c-99e3-6d73ac6e9a1b", + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1038, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1165, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 0, + "first": "blob:http://127.0.0.1:8084/95708545-f224-4a01-958c-11435104d48b" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/95708545-f224-4a01-958c-11435104d48b", + "blob:http://127.0.0.1:8084/dcac212f-3b43-420b-acb9-09fae518c423" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/dcac212f-3b43-420b-acb9-09fae518c423" + ], + "repeated": "NotFoundError" + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1184, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/20f6b3ea-d6cd-4265-be58-c23757651f32", + "blob:http://127.0.0.1:8084/474e8f1a-b22e-410a-b8db-8750c9a21b53" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/20f6b3ea-d6cd-4265-be58-c23757651f32" + ], + "pendingState": "pending", + "events": [ + "file", + "video", + "canvas", + "destroy", + "update" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2124, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / DOM drop dispatch exposes missing listener while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2295, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1911, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool workspace.legacy.js / native PNG callback delivered after destroy still emits update and creates a URL", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1828, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "workspace.legacy.js", + "sha256": "f77419cc62eeaa45c0413f4b04d169f78178fd539443cbf7b1e5db4ad4c95c98", + "recoveredIndividualFile": false, + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 0, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1226, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 44055, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 0, + "retiredSrc": null, + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 657, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 507, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 1, + "first": "blob:http://127.0.0.1:8084/4f42cca9-b4be-4e80-bb2b-5f2d05a7c458" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/4f42cca9-b4be-4e80-bb2b-5f2d05a7c458", + "blob:http://127.0.0.1:8084/08c4c8d1-ddfd-45f1-960c-3c264a0b2033" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/4f42cca9-b4be-4e80-bb2b-5f2d05a7c458", + "blob:http://127.0.0.1:8084/08c4c8d1-ddfd-45f1-960c-3c264a0b2033" + ] + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native PNG callback delivered after destroy is ignored after cancellation", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 686, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/455a0913-393b-450b-b6eb-fa7ef8493398" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/455a0913-393b-450b-b6eb-fa7ef8493398" + ], + "pendingState": "rejected", + "events": [ + "file", + "video", + "canvas", + "destroy" + ], + "connected": false, + "bytes": 9120 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / source replacement cancels held native encoding and completes only the latest job", + "project": "chromium", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 863, + "browser": "153.0.8010.12", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-native-source-cancellation", + "data": { + "oldState": "AbortError", + "firstEncodedBytes": 9120, + "source": "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "updateSources": [ + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb" + ], + "created": [ + "blob:http://127.0.0.1:8084/cd520dcc-bd77-46a3-9f87-a7482b3c80ce", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/2d53da91-7eba-4205-9605-6a4d47e64126", + "blob:http://127.0.0.1:8084/fd0de447-5b28-4cb6-adc6-f7becc59d574", + "blob:http://127.0.0.1:8084/7dec74eb-e01a-49f4-8eb6-db58977660b5", + "blob:http://127.0.0.1:8084/dc5c5f0f-edf6-468c-a8bb-2d7df0a80bee", + "blob:http://127.0.0.1:8084/004a0de2-bdb5-4dba-ab5d-ab0518f6d077", + "blob:http://127.0.0.1:8084/de8b828c-4404-494b-b5a7-e55ed3f4a198", + "blob:http://127.0.0.1:8084/49eae621-e0a3-4364-8fcc-a8ff2461fb0e", + "blob:http://127.0.0.1:8084/010ff74e-531b-4710-b153-942089d7eaf1", + "blob:http://127.0.0.1:8084/4957f9a8-c410-4da4-9d6e-fcbae4bed169", + "blob:http://127.0.0.1:8084/cfe586fb-60e7-4d9f-ba19-6bcb5cb946f0" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/cd520dcc-bd77-46a3-9f87-a7482b3c80ce", + "blob:http://127.0.0.1:8084/2d53da91-7eba-4205-9605-6a4d47e64126", + "blob:http://127.0.0.1:8084/fd0de447-5b28-4cb6-adc6-f7becc59d574", + "blob:http://127.0.0.1:8084/7dec74eb-e01a-49f4-8eb6-db58977660b5", + "blob:http://127.0.0.1:8084/dc5c5f0f-edf6-468c-a8bb-2d7df0a80bee", + "blob:http://127.0.0.1:8084/004a0de2-bdb5-4dba-ab5d-ab0518f6d077", + "blob:http://127.0.0.1:8084/de8b828c-4404-494b-b5a7-e55ed3f4a198", + "blob:http://127.0.0.1:8084/49eae621-e0a3-4364-8fcc-a8ff2461fb0e", + "blob:http://127.0.0.1:8084/010ff74e-531b-4710-b153-942089d7eaf1", + "blob:http://127.0.0.1:8084/4957f9a8-c410-4da4-9d6e-fcbae4bed169", + "blob:http://127.0.0.1:8084/a805e1c7-43b7-439b-9916-740da18819eb", + "blob:http://127.0.0.1:8084/cfe586fb-60e7-4d9f-ba19-6bcb5cb946f0" + ], + "events": [ + "file", + "video", + "canvas", + "file", + "video", + "canvas", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "done", + "destroy" + ], + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 5494, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-extraction", + "data": { + "width": 240, + "height": 210, + "sourceWidth": 320, + "sourceHeight": 180, + "cellHeight": 45, + "mime": "image/png", + "bytes": 38895, + "nonBlack": 3600, + "createdCount": 21, + "revokedCount": 21, + "doneCount": 2, + "updates": [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 + ], + "retiredReadyState": 0, + "retiredSrc": null, + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1055, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1190, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-media-failure", + "data": { + "failed": { + "code": 4, + "toolErrors": 1, + "first": "blob:http://127.0.0.1:8084/8aac7a22-7d3d-4a5f-b041-1501d164c0f5" + }, + "result": { + "value": "", + "created": [ + "blob:http://127.0.0.1:8084/8aac7a22-7d3d-4a5f-b041-1501d164c0f5", + "blob:http://127.0.0.1:8084/0d1030f6-6713-4672-9a3b-76b59aff995c" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/8aac7a22-7d3d-4a5f-b041-1501d164c0f5", + "blob:http://127.0.0.1:8084/0d1030f6-6713-4672-9a3b-76b59aff995c" + ] + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native PNG callback delivered after destroy is ignored after cancellation", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 1268, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-late-native-blob", + "data": { + "count": 1, + "created": [ + "blob:http://127.0.0.1:8084/df309b80-7db9-492d-82a3-5422ede0c3c8" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/df309b80-7db9-492d-82a3-5422ede0c3c8" + ], + "pendingState": "rejected", + "events": [ + "file", + "video", + "canvas", + "destroy" + ], + "connected": false, + "bytes": 6496 + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / source replacement cancels held native encoding and completes only the latest job", + "project": "firefox", + "kind": "actual-tool-scenario", + "status": "passed", + "durationMs": 2437, + "browser": "155.0", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-native-source-cancellation", + "data": { + "oldState": "AbortError", + "firstEncodedBytes": 6496, + "source": "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "updateSources": [ + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383" + ], + "created": [ + "blob:http://127.0.0.1:8084/c5d19cb8-c0a4-470a-8ddc-6e15a43e83d8", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/24fc0e29-b37c-4257-b66c-cdf1bb609548", + "blob:http://127.0.0.1:8084/2dd96c2b-a0bc-49ba-bf80-3a68eaf8452c", + "blob:http://127.0.0.1:8084/1c0d9902-c283-45e5-8ba5-36e2c6ec4634", + "blob:http://127.0.0.1:8084/59aa2794-2d28-4f0f-807f-b9bf6d231f0f", + "blob:http://127.0.0.1:8084/82b603f8-3aa2-4d7c-ae58-2cca6ba0eaa7", + "blob:http://127.0.0.1:8084/3473a0cc-f316-47a5-9616-801b3ad30ec4", + "blob:http://127.0.0.1:8084/df090076-4fb8-46bb-8b51-3773ec99e94b", + "blob:http://127.0.0.1:8084/03aa33f0-f854-46db-88a4-d8332dc18a2e", + "blob:http://127.0.0.1:8084/ae880fa3-bce2-465c-ad34-7fca71f3ae89", + "blob:http://127.0.0.1:8084/95228f74-0d20-4b40-9a4d-1717eed51991" + ], + "revoked": [ + "blob:http://127.0.0.1:8084/c5d19cb8-c0a4-470a-8ddc-6e15a43e83d8", + "blob:http://127.0.0.1:8084/24fc0e29-b37c-4257-b66c-cdf1bb609548", + "blob:http://127.0.0.1:8084/2dd96c2b-a0bc-49ba-bf80-3a68eaf8452c", + "blob:http://127.0.0.1:8084/1c0d9902-c283-45e5-8ba5-36e2c6ec4634", + "blob:http://127.0.0.1:8084/59aa2794-2d28-4f0f-807f-b9bf6d231f0f", + "blob:http://127.0.0.1:8084/82b603f8-3aa2-4d7c-ae58-2cca6ba0eaa7", + "blob:http://127.0.0.1:8084/3473a0cc-f316-47a5-9616-801b3ad30ec4", + "blob:http://127.0.0.1:8084/df090076-4fb8-46bb-8b51-3773ec99e94b", + "blob:http://127.0.0.1:8084/03aa33f0-f854-46db-88a4-d8332dc18a2e", + "blob:http://127.0.0.1:8084/ae880fa3-bce2-465c-ad34-7fca71f3ae89", + "blob:http://127.0.0.1:8084/efe05844-8dd3-4b6e-afcc-2944e4fa1383", + "blob:http://127.0.0.1:8084/95228f74-0d20-4b40-9a4d-1717eed51991" + ], + "events": [ + "file", + "video", + "canvas", + "file", + "video", + "canvas", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "update", + "done", + "destroy" + ], + "connected": false + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native file selection, seek extraction, PNG pixels and repeat generation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1926, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / DOM drop dispatch uses the registered handler while callable ondrop works", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 2307, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / corrupt media, replacement URLs and repeated destroy reproduce baseline gaps", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1882, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / native PNG callback delivered after destroy is ignored after cancellation", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1886, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + }, + { + "title": "thumbnail-tool.spec.js / Thumbnail tool candidate-artifact / source replacement cancels held native encoding and completes only the latest job", + "project": "webkit", + "kind": "blob-unavailable-control", + "status": "passed", + "durationMs": 1698, + "browser": "26.6", + "platform": "win32", + "measurements": [ + { + "name": "thumbnail-runtime", + "data": { + "name": "candidate-artifact", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7", + "originalNpmArchive": false, + "media": "test\\browser\\media\\thumbnail-pattern.mp4" + } + }, + { + "name": "thumbnail-blob-unavailable", + "data": { + "native": { + "http": "loaded", + "error": 4, + "blob": "error" + }, + "tool": { + "errors": 1, + "processing": false, + "duration": null + } + } + } + ] + } + ] + } + ] + }, + "inputs": [ + { + "file": "packages/artplayer-tool-thumbnail/src/index.js", + "sha256": "873de223a8bb3c4d33d8d9409d9139f9fd3bb5d317a4bf07d99245f01ed1fec8" + }, + { + "file": "packages/artplayer-tool-thumbnail/src/input.js", + "sha256": "6771b958ac8b388e6e7ce0a671af80c8422feef6651a2724365ef8c088fc68f1" + }, + { + "file": "packages/artplayer-tool-thumbnail/src/sheet.js", + "sha256": "c671e298734e67d8d3f08bfcb383452f07cb4ed5a606f2903490181df0c0107c" + }, + { + "file": "packages/artplayer-tool-thumbnail/src/lifecycle.js", + "sha256": "e8dfd7256b41d60a7ce08151b498bb2caed4a4f11d27447369301bcd6d39f482" + }, + { + "file": "packages/artplayer-tool-thumbnail/src/source.js", + "sha256": "52f3913bd2a4e1c8ad3246f7633e2fa231affe813ce0485a12e1a1d1fff2bc1f" + }, + { + "file": "packages/artplayer-tool-thumbnail/src/extraction.js", + "sha256": "f649ec7352cacde5fb30040514072b95aa6a76d105832a777cb255f50a583863" + }, + { + "file": "packages/artplayer-tool-thumbnail/src/emitter.js", + "sha256": "5aa71e4317037c06b94e259beed2e964bbbb7117edbe78d29cc941c4916fd2ff" + }, + { + "file": "packages/artplayer-tool-thumbnail/src/utils.js", + "sha256": "dc19fe2f338df7058084eb2301d37db59ec4c914a85ba819a856d270c1267105" + }, + { + "file": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.js", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350" + }, + { + "file": "docs/compiled/artplayer-tool-thumbnail.js", + "sha256": "c2abe80974e20dc3d5a7f95bb7c97a9b9f7d2b783c601fc80cbf668df35b0350" + }, + { + "file": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.legacy.js", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7" + }, + { + "file": "docs/compiled/artplayer-tool-thumbnail.legacy.js", + "sha256": "059291ec9a40c50c6d666df5cde5fbe517c8339ec177645b1a32190dfd499bd7" + }, + { + "file": "packages/artplayer-tool-thumbnail/dist/artplayer-tool-thumbnail.mjs", + "sha256": "d50f75e595d210a1c8234ffb0b425bcf6db14438b80c14dfb8657b1b05d77574" + }, + { + "file": "docs/compiled/artplayer-tool-thumbnail.mjs", + "sha256": "d50f75e595d210a1c8234ffb0b425bcf6db14438b80c14dfb8657b1b05d77574" + }, + { + "file": "packages/artplayer-tool-thumbnail/ARCHITECTURE.md", + "sha256": "179f6ea535e3ce16f66bd303e44fa888f319dbfb6ea4e9edd6651f6a8c4d5c26" + }, + { + "file": "test/thumbnail-input.test.js", + "sha256": "08da28330bcd4ef83bddcea3aa10a95ad9ef075bdf3bb7daee734c0807f4b51a" + }, + { + "file": "test/thumbnail-lifecycle.test.js", + "sha256": "decc5f06f343412815f06d30fff5263580ff106eb266eda7c73fb3eec3dc4403" + }, + { + "file": "test/helpers/thumbnail.js", + "sha256": "e547f3fe4581df6298ed448f1f672fe6e1fb501c3fbe60cc9be805a027fdab56" + }, + { + "file": "test/browser/thumbnail-input.spec.js", + "sha256": "df82785958860b550552507dde59a334d13d916985c0ac85673e210eeaaf3a38" + }, + { + "file": "test/browser/thumbnail-tool.spec.js", + "sha256": "04f322adf9f505f8705253f9225cc92f3bfa8bb1ba9495926a23811b39277089" + }, + { + "file": "test/browser/thumbnail-native.spec.js", + "sha256": "50d2724c4903c2281cf7a5fa8577e4a8174742ac30b48cd3c679eaecc2d27f3e" + } + ], + "logs": [ + { + "file": "refactor/.cache/thumbnail03-jobs-before-final.log", + "sha256": "c94fe80d8773170edf04a068cf2d65ab76ed03db9fa8f026ef5c168b435d0751" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-source.log", + "sha256": "e1abaf831dd9229e3c2a5079cee19cb1febd3e21b17ea8a07209f8a4bdbb7b25" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-main.log", + "sha256": "9051d7b543ad881fa0d0260d9af42dff76a7b25ceb2f4d2e2a6e10f639e5fbfd" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-legacy.log", + "sha256": "821112f9dd55ef6436173ffa5a8c87a038ad1bd63814c6fcb63fb7186d5aeb03" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-build.log", + "sha256": "decfb574370c35485630dd9f5e541fabc1cd4c84455d11526512db6210ebad73" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-lint.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-ci.log", + "sha256": "9dd82914b94788d3b9d2cd44e46bbe6a3a4128e13d2e591747a4f86a4458fcf7" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-imports.log", + "sha256": "9878b206c33c2302c2c5fd2d8be51db62874041a5b64646ab662b6f599555d0d" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-esm.log", + "sha256": "226b54bbc482b150aa090f9868f259587ee671933f5d0e8c18e149e755fba132" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-browser-source-media.log", + "sha256": "aa28b213ad4688077f193bf51441814089c1877edc18a0b4a183a69753b5dd3b" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-browser-source-media.json", + "sha256": "6067fda132442aa12a0efbc3b6ff38f9cca09d9020c8d0f91d1efd3dc33f89fe" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-browser-main-media.log", + "sha256": "78ab22c14a1a80cabae314ae8b793c25115ea048ceac98d766bfa74b21e9de02" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-browser-main-media.json", + "sha256": "a208577a89f2390bb1f8f35523048f5048bf6ff19543bfca61232612a1a01b42" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-browser-legacy-media.log", + "sha256": "b6ab027d717f29380aaff4ed0327b9af6cabddc8f03e2a17ab4ac47018fa60c0" + }, + { + "file": "refactor/.cache/thumbnail03-jobs-browser-legacy-media.json", + "sha256": "7971b38e80467986a7d4968ff1e32ea993d4d4b7dd1f30c3c954c568cd845aff" + } + ], + "supersededProbes": [ + "refactor/.cache/thumbnail03-jobs-browser-probe.json", + "refactor/.cache/thumbnail03-jobs-browser-source-final.json", + "refactor/.cache/thumbnail03-jobs-browser-main-probe.json" + ], + "limitations": [ + "Runtime is still JS; strict TS/types/emitter attribution are task 04.", + "Each browser run includes 17 Blob-unavailable controls, not successful Windows WebKit extraction.", + "Native PNG encoding already submitted cannot itself be aborted; cancellation prevents its late callback effects.", + "Pre-existing recovered 3.5.31 default differences remain an explicit 04/05 policy gate.", + "Current coverage policy measures only core/chapter; no thumbnail percentage claimed.", + "No hosted CI run, next-major version bump, push, tag or npm publication." + ] +} diff --git a/refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md b/refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md new file mode 100644 index 000000000..0ac823a8f --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md @@ -0,0 +1,88 @@ +# PKG-TOOL-THUMB-03 owned extraction and source lifecycle + +Started from 426c0c5d4209aa91be7414b810f1e8dca712c297, following the input/sheet +checkpoint. This completes the runtime responsibility split for 03; strict TS, +emitter provenance and public declarations remain in 04. Existing release/core +integration and Windows WebKit/Safari evidence gates remain in 05/06. + +## Implementation and observable behavior + +The public class delegates to input, sheet, source, extraction and lifecycle +modules. Private WeakMap state owns generations, listeners, jobs and generated +URLs without adding public instance fields. Input setup also guards reentrant +replacement/destruction. Construction failure marks the instance closed and +releases its partial media resources. + +Source loading keeps synchronous file-before-src-before-video ordering. New +successful selections revoke superseded source URLs and cancel the old extraction. +Nested file callbacks and URL-creation hooks cannot overwrite newer selections. +Native metadata/error listeners belong to their source generation. Errors now +report through the tool and settle a waiting/running job; stale callbacks do not +affect the latest source. + +Each extraction has one promise, owned metadata timer/listeners and per-frame +readiness cleanup. It preserves preflight throws, midpoint/grid/footer geometry, +normal progress/done order, undefined completion and the previous oncanplay owner. +A start before the first selected file retains its wait and adopts that first +selection. A replacement of an existing source cancels old work. Existing sheet +URLs remain available until replaced by a new frame or destruction. + +Destruction/replacement rejects cancelled work with AbortError and does not emit +error for intentional cancellation. An internal rejection handler prevents ignored +cancelled jobs from becoming orphan rejections; the returned promise still rejects +for consumers. Native canvas encoding itself has no cancellation operation here: +the job settles immediately and its eventual callback is ignored. No new URL, +update/done event or next frame may be published by that callback. + +Null Blob, seek/draw/toBlob failures and throwing update/error/done callbacks settle +the promise after cleanup. Duplicate readiness/Blob callbacks cannot double-publish +a frame. Completion releases ownership before done so a callback can start another +job. Reentrant listener/timer installation or URL creation is checked after host +calls, including handles registered only after destruction returned. + +Destroy now pauses the native video, removes its src, calls load to reset media +state and removes the node. Cleanup continues after individual failures. Private +URLs are released even if callers changed public URL properties; historical cleanup +of current public URL values is retained. Closed start returns a rejecting promise; +closed download retains chaining and allocates no anchor. + +Default height and video-event timing remain the workspace 4.4.0 behavior. This +does not resolve the pre-existing recovered 3.5.31 default differences documented +in thumbnail-contract.md. No silent default-mode or legacy-CSS decision is made. + +## Verification + +26 new lifecycle cases plus the previous 14 candidate input cases run on source, +main and legacy. Frozen workspace running the new lifecycle assertions has 2 +passes and 24 failures. Normal extraction, cancellation/error paths, native media +errors, late/duplicate callbacks, nested loads/setup, first-selection waiting, +public URL mutation and decoder-reset failure handling are covered. + +Each final browser run has 57 rows: 34 real tool scenarios in Chromium/Firefox, +3 candidate input/DOM cases across all engines, 17 Windows WebKit Blob-unavailable +controls and 3 independent native-media comparisons. Candidate cases now require +error notification, replaced-source URL cleanup and rejected/ignored late encoding; +historical cases still assert their old defects. A real file is replaced while a +native PNG callback is held; only the new source produces ten updates and one done. +Successful extraction also verifies retired video readyState=0 and no src attribute. +The 17 unavailable controls are not successful file extraction. + +Earlier passing source/main probe reports remain archived; final media-reset +source/main/legacy results and hashes are in thumbnail-lifecycle-validation.json. +Normal builds regenerate UMD, legacy and ESM plus byte-identical docs copies. The +new lifecycle file is included in test:thumbnail and the normal unit/CI script. +The coverage policy still measures core/chapter only; no tool percentage is claimed. + +## Completion and continuation + +03 completes the runtime split and the reproduced ownership fixes. The package +ARCHITECTURE.md describes actual dependencies, resources, ordering, tests and +limits. 04 owns strict TS, the now-unused sleep/serial utility cleanup, emitter +attribution, installed public declarations and the explicit historical default +compatibility policy. 05 owns complete old/new-core sheet integration and actual +Safari/WebKit Blob extraction; 06 owns promised ESM/type paths, README demo and +candidate tarball validation. Risks stay open until those gates are met. + +Rollback by reverting this dedicated completion commit and rebuilding the package; +the preceding input checkpoint and all frozen historical tests remain available. +No new dependencies, version bump, push, tag, hosted CI run or npm publication. diff --git a/refactor/plan.md b/refactor/plan.md index 10c360fc6..eeec3d427 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。 -状态:todo 100 / doing 11 / blocked 0 / done 111 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 100 / doing 10 / blocked 0 / done 112 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -357,7 +357,7 @@ | --- | --- | --- | --- | --- | --- | --- | | PKG-TOOL-THUMB-01 | artplayer-tool-thumbnail
核对包契约与历史用法 | BASE-05 | 构造/defaults/事件/方法/历史拼写、.esm.js 入口和缺失 types | 源码/声明/README/demo/可恢复发布内容差异已登记;公开形状和版本范围冻结;区分旧 npm 的 main/CSS 与当前工作区无 main、.esm.js/类型缺失,不把工作区状态冒称旧 npm 发布事实 | H | done | | PKG-TOOL-THUMB-02 | artplayer-tool-thumbnail
建立特有行为与错误测试 | PKG-TOOL-THUMB-01, ENG-03, ENG-05 | 文件输入/拖放、抽帧网格、begin/end、进度/失败/重复任务 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | -| PKG-TOOL-THUMB-03 | artplayer-tool-thumbnail
整理内部职责与资源 | PKG-TOOL-THUMB-02, CORE-01 | 输入、抽帧队列、网格导出、URL/监听清理分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | doing | +| PKG-TOOL-THUMB-03 | artplayer-tool-thumbnail
整理内部职责与资源 | PKG-TOOL-THUMB-02, CORE-01 | 输入、抽帧队列、网格导出、URL/监听清理分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | | PKG-TOOL-THUMB-04 | artplayer-tool-thumbnail
迁移自有源码和公开类型 | PKG-TOOL-THUMB-03, ENG-04, ENG-06, CORE-07 | 补齐真实 API 声明,自有 emitter/utils TS 化 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证 | H | todo | | PKG-TOOL-THUMB-05 | artplayer-tool-thumbnail
验证新旧核心和组合 | PKG-TOOL-THUMB-04, CORE-22 | 工具独立浏览器使用,生成缩略图在核心中显示 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | | PKG-TOOL-THUMB-06 | artplayer-tool-thumbnail
验证分发并同步文档 | PKG-TOOL-THUMB-05, ENG-07 | tool.thumbnail.js、历史 ESM 兼容文件、types 路径和 tarball | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | @@ -537,5 +537,5 @@ - PKG-IFRAME-05: [记录](changes/2026-09-13-PKG-IFRAME-05-integration.md) [记录](baselines/iframe-integration-validation.json) [记录](changes/2026-09-13-PKG-IFRAME-05-history.md) [记录](baselines/iframe-history-validation.json) - PKG-TOOL-THUMB-01: [记录](baselines/thumbnail-release.json) [记录](baselines/thumbnail-contract.md) [记录](changes/2026-09-13-PKG-TOOL-THUMB-01-contract.md) [记录](baselines/thumbnail-contract-validation.json) - PKG-TOOL-THUMB-02: [记录](baselines/thumbnail-behavior-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md) -- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) +- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md) [记录](baselines/thumbnail-lifecycle-validation.json) - PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md) diff --git a/refactor/progress.md b/refactor/progress.md index 01657b2d5..d06a88569 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,19 @@ # 进度与证据 +## PKG-TOOL-THUMB-03 完成:抽帧任务与媒体资源所有权 + +增加lifecycle/source/extraction模块,保留公开类形状与工作区默认行为。元数据等待、 +逐帧回调和URL归属明确;切源/destroy立即取消旧Promise,迟到PNG不能再update或创建URL。 +修复原生错误、null Blob、监听器异常和重入;保留首次选文件前start等待。销毁暂停/清空 +src/load重置媒体,即使部分清理抛错也继续。源码仍为JS,04负责严格TS/声明与来源审查。 +新26项生命周期断言在冻结旧版2通过/24失败;连同输入测试,源码/main/legacy各40通过。 +三份浏览器各57通过:34真实工具场景、3输入场景、17Blob不可用对照、3原生媒体对照。 +实际切源期间持有原生PNG回调验证仅新任务完成,销毁后readyState=0且无src。 +完整CI1646、330生产TS、3导入及候选ESM导入通过。类型/默认历史差异及Safari/分发风险 +仍open。见[变更](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md)与 +[验证](baselines/thumbnail-lifecycle-validation.json)。222项:112 done、10 doing、100 todo。 + + ## PKG-TOOL-THUMB-03 输入与导出源码检查点(doing) 生产入口拆出input/sheet模块,修复drop注册、输入切换与wrapper所有权、构造/安装失败 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 2addd364d..367750b7d 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -177,7 +177,7 @@ | IFRAME-LIFE-01 | open / 已复现 | Iframe timestamp IDs and unowned pending requests/timers lack terminal cleanup | PKG-IFRAME-02, PKG-IFRAME-03, PKG-IFRAME-05 | | IFRAME-TRUST-01 | open / 已复现 | Iframe executable commit protocol accepts messages without source/origin validation | PKG-IFRAME-03, PKG-IFRAME-05 | | IFRAME-DIST-01 | open / 已复现 | Iframe tool name is unavailable at registry observation and old archive includes distinct helper exports | PKG-IFRAME-04, PKG-IFRAME-06 | -| THUMB-COMPAT-01 | open / 已复现 | Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults | PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-04 | +| THUMB-COMPAT-01 | open / 已复现 | Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-05 | | THUMB-LIFE-01 | open / 已复现 | Thumbnail drop binding and repeated destroy fail; pending work and replacement resource ownership need repair | PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-05 | | THUMB-PROVENANCE-01 | open / 源码/产物事实 | Thumbnail local emitter attribution and historical distribution completeness remain unverified | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 | | THUMB-MEDIA-01 | open / 已复现 | Windows WebKit native Blob URLs cannot load tested MP4 media despite successful HTTP controls | PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 | diff --git a/refactor/risks.json b/refactor/risks.json index 38d4b59fc..7db012a8f 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -3853,9 +3853,8 @@ "title": "Thumbnail recovered 3.5.31 and workspace 4.4.0 have conflicting height and event timing defaults", "confirmation": "reproduced", "owners": [ - "PKG-TOOL-THUMB-02", - "PKG-TOOL-THUMB-03", - "PKG-TOOL-THUMB-04" + "PKG-TOOL-THUMB-04", + "PKG-TOOL-THUMB-05" ], "compatibleResolution": "Freeze both implementations and derive a concrete compatibility policy before changing defaults; no silent claim that identical calls have identical historical semantics.", "closureCriteria": "Explicit documented compatibility decision, old/candidate tests and migration examples covering delay, height, input reset and readiness.", @@ -3867,7 +3866,8 @@ "refactor/baselines/thumbnail-behavior-validation.json", "refactor/changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md", "test/thumbnail.test.js", - "test/browser/thumbnail-tool.spec.js" + "test/browser/thumbnail-tool.spec.js", + "refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md" ] }, { @@ -3879,7 +3879,7 @@ "PKG-TOOL-THUMB-03", "PKG-TOOL-THUMB-05" ], - "compatibleResolution": "03 input checkpoint fixes registered drop, input/wrapper ownership, failed setup/construction cleanup, repeated destroy and temporary download anchors. Candidate input regressions contrast 12 old failures; source/main/legacy verify preserved shape. Pending extraction callbacks, source URL replacement, cancellation and failure reentrancy remain open for the rest of 03 and real integration in 05.", + "compatibleResolution": "03 splits input/sheet/source/extraction/lifecycle and fixes reproduced cancellation, Blob URL ownership, native errors, callback failure/reentrancy and decoder reset. Keep risk open for 04 strict types and 05 complete core/Safari integration; Windows WebKit Blob-unavailable controls do not prove extraction.", "closureCriteria": "Deterministic old failure/candidate success for replacement, error and destruction plus real media/browser resource validation.", "status": "open", "evidence": [ @@ -3894,7 +3894,10 @@ "packages/artplayer-tool-thumbnail/ARCHITECTURE.md", "test/thumbnail-input.test.js", "test/browser/thumbnail-input.spec.js", - "refactor/baselines/thumbnail-input-checkpoint.json" + "refactor/baselines/thumbnail-input-checkpoint.json", + "refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md", + "test/thumbnail-lifecycle.test.js", + "refactor/baselines/thumbnail-lifecycle-validation.json" ] }, { diff --git a/refactor/tasks.json b/refactor/tasks.json index b8d2d34cf..51458a007 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -3785,13 +3785,15 @@ "PKG-TOOL-THUMB-02", "CORE-01" ], - "status": "doing", + "status": "done", "risk": "H", "deliverable": "输入、抽帧队列、网格导出、URL/监听清理分离", "acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过", "evidence": [ "changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md", - "baselines/thumbnail-input-checkpoint.json" + "baselines/thumbnail-input-checkpoint.json", + "changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md", + "baselines/thumbnail-lifecycle-validation.json" ] }, { diff --git a/test/browser/thumbnail-tool.spec.js b/test/browser/thumbnail-tool.spec.js index 6e34b93ee..ce93f3d62 100644 --- a/test/browser/thumbnail-tool.spec.js +++ b/test/browser/thumbnail-tool.spec.js @@ -8,7 +8,7 @@ import { expect, test } from './fixtures.js' const sample = fileURLToPath(new URL('./media/thumbnail-pattern.mp4', import.meta.url)) -async function capabilityControl(page, testInfo) { +async function capabilityControl(page, testInfo, lifecycle) { const native = await page.evaluate(() => window.thumbnailNative) if (native.blob === 'loaded') return false @@ -21,13 +21,13 @@ async function capabilityControl(page, testInfo) { tool.destroy() return state }) - expect(state).toEqual({ errors: 0, processing: false, duration: null }) + expect(state).toEqual({ errors: lifecycle ? 1 : 0, processing: false, duration: null }) testInfo.annotations.push({ type: 'capability-control', description: 'Native HTTP succeeds but native Blob and tool Blob return media error 4. Extraction scenario unavailable; no successful extraction claimed.' }) await testInfo.attach('thumbnail-blob-unavailable', { contentType: 'application/json', body: JSON.stringify({ native, tool: state }) }) return true } -const implementations = [...thumbnailHistorical(), { ...await thumbnailCandidate(), inputOwnership: true }] +const implementations = [...thumbnailHistorical(), { ...await thumbnailCandidate(), inputOwnership: true, lifecycle: true }] for (const implementation of implementations) { test.describe(`Thumbnail tool ${implementation.name}`, () => { test.beforeEach(async ({ page }, testInfo) => { @@ -85,7 +85,7 @@ for (const implementation of implementations) { test('native file selection, seek extraction, PNG pixels and repeat generation', async ({ page }, testInfo) => { test.setTimeout(60000) - if (await capabilityControl(page, testInfo)) + if (await capabilityControl(page, testInfo, implementation.lifecycle)) return await page.locator('#input').setInputFiles(sample) await expect.poll(() => page.evaluate(() => tool.video.readyState)).toBeGreaterThanOrEqual(2) @@ -111,8 +111,10 @@ for (const implementation of implementations) { const initialEvents = observed.slice() await tool.start() const second = tool.thumbnailUrl + const sourceWidth = tool.video.videoWidth + const sourceHeight = tool.video.videoHeight tool.destroy() - return { width: image.width, height: image.height, sourceWidth: tool.video.videoWidth, sourceHeight: tool.video.videoHeight, cellHeight: tool.option.height, mime: blob.type, bytes: blob.size, nonBlack, initialEvents, events: observed, first, second, created, revoked, connected: tool.video.isConnected } + return { width: image.width, height: image.height, sourceWidth, sourceHeight, cellHeight: tool.option.height, mime: blob.type, bytes: blob.size, nonBlack, initialEvents, events: observed, first, second, created, revoked, connected: tool.video.isConnected, retiredReadyState: tool.video.readyState, retiredSrc: tool.video.getAttribute('src') } }) expect(result.width).toBe(240) expect(result.cellHeight).toBe(implementation.legacy ? 30 : result.sourceHeight / result.sourceWidth * 80) @@ -125,11 +127,15 @@ for (const implementation of implementations) { expect(result.first).not.toBe(result.second) expect([...result.created].sort()).toEqual([...result.revoked].sort()) expect(result.connected).toBe(false) + if (implementation.lifecycle) { + expect(result.retiredReadyState).toBe(0) + expect(result.retiredSrc).toBe(null) + } await testInfo.attach('thumbnail-extraction', { contentType: 'application/json', body: JSON.stringify(result) }) }) test(`DOM drop dispatch ${implementation.inputOwnership ? 'uses the registered handler' : 'exposes missing listener'} while callable ondrop works`, async ({ page }, testInfo) => { - if (await capabilityControl(page, testInfo)) + if (await capabilityControl(page, testInfo, implementation.lifecycle)) return const result = await page.evaluate(async () => { const bytes = await (await fetch('/test/thumbnail-pattern.mp4')).blob() @@ -149,12 +155,12 @@ for (const implementation of implementations) { }) test('corrupt media, replacement URLs and repeated destroy reproduce baseline gaps', async ({ page }, testInfo) => { - if (await capabilityControl(page, testInfo)) + if (await capabilityControl(page, testInfo, implementation.lifecycle)) return await page.locator('#input').setInputFiles({ name: 'corrupt.mp4', mimeType: 'video/mp4', buffer: Buffer.from('not a media container') }) await expect.poll(() => page.evaluate(() => tool.video.error?.code || 0)).toBeGreaterThan(0) const failed = await page.evaluate(() => ({ code: tool.video.error.code, toolErrors: observed.filter(item => item.event === 'error').length, first: tool.videoUrl })) - expect(failed.toolErrors).toBe(0) + expect(failed.toolErrors).toBe(implementation.lifecycle ? 1 : 0) await page.locator('#input').setInputFiles(sample) await expect.poll(() => page.evaluate(() => tool.video.readyState)).toBeGreaterThanOrEqual(2) const result = await page.evaluate(() => { @@ -168,15 +174,15 @@ for (const implementation of implementations) { return { value, created, revoked, repeated } }) expect(result.created).toHaveLength(2) - expect(result.revoked).toEqual([result.created[1]]) - expect(result.revoked).not.toContain(failed.first) + expect(result.revoked).toEqual(implementation.lifecycle ? result.created : [result.created[1]]) + expect(result.revoked.includes(failed.first)).toBe(!!implementation.lifecycle) expect(result.repeated).toBe(implementation.inputOwnership ? undefined : 'NotFoundError') expect(result.value === '').toBe(!implementation.legacy) await testInfo.attach('thumbnail-media-failure', { contentType: 'application/json', body: JSON.stringify({ failed, result }) }) }) - test('native PNG callback delivered after destroy still emits update and creates a URL', async ({ page }, testInfo) => { - if (await capabilityControl(page, testInfo)) + test(`native PNG callback delivered after destroy ${implementation.lifecycle ? 'is ignored after cancellation' : 'still emits update and creates a URL'}`, async ({ page }, testInfo) => { + if (await capabilityControl(page, testInfo, implementation.lifecycle)) return await page.locator('#input').setInputFiles(sample) await expect.poll(() => page.evaluate(() => tool.video.readyState)).toBeGreaterThanOrEqual(2) @@ -204,13 +210,69 @@ for (const implementation of implementations) { return { count, created, revoked, pendingState, events: observed.map(item => item.event), connected: tool.video.isConnected, bytes: nativeBlob.size } }) expect(result.bytes).toBeGreaterThan(100) - expect(result.created).toHaveLength(result.count + 1) - expect(result.revoked).not.toContain(result.created.at(-1)) - expect(result.events.indexOf('update')).toBeGreaterThan(result.events.indexOf('destroy')) - expect(result.pendingState).toBe('pending') + expect(result.created).toHaveLength(result.count + (implementation.lifecycle ? 0 : 1)) + if (implementation.lifecycle) { + expect(result.revoked).toContain(result.created.at(-1)) + expect(result.events).not.toContain('update') + } + else { + expect(result.revoked).not.toContain(result.created.at(-1)) + expect(result.events.indexOf('update')).toBeGreaterThan(result.events.indexOf('destroy')) + } + expect(result.pendingState).toBe(implementation.lifecycle ? 'rejected' : 'pending') expect(result.connected).toBe(false) await testInfo.attach('thumbnail-late-native-blob', { contentType: 'application/json', body: JSON.stringify(result) }) }) + + if (implementation.lifecycle) { + test('source replacement cancels held native encoding and completes only the latest job', async ({ page }, testInfo) => { + if (await capabilityControl(page, testInfo, true)) + return + await page.locator('#input').setInputFiles(sample) + await expect.poll(() => page.evaluate(() => tool.video.readyState)).toBeGreaterThanOrEqual(2) + await page.evaluate(() => { + const original = HTMLCanvasElement.prototype.toBlob + let first = true + HTMLCanvasElement.prototype.toBlob = function (callback, ...args) { + if (first) { + first = false + original.call(this, (blob) => { + window.firstEncodedBytes = blob.size + window.releaseFirst = () => callback(blob) + }, ...args) + } + else { + original.call(this, callback, ...args) + } + } + window.oldState = 'pending' + window.updateSources = [] + tool.on('update', () => window.updateSources.push(tool.videoUrl)) + tool.start().then(() => { + window.oldState = 'resolved' + }, (error) => { + window.oldState = error.name + }) + }) + await expect.poll(() => page.evaluate(() => typeof window.releaseFirst)).toBe('function') + await page.locator('#input').setInputFiles(sample) + const result = await page.evaluate(async () => { + const source = tool.videoUrl + const next = tool.start() + window.releaseFirst() + await next + tool.destroy() + return { oldState: window.oldState, firstEncodedBytes: window.firstEncodedBytes, source, updateSources: window.updateSources, created, revoked, events: observed.map(item => item.event), connected: tool.video.isConnected } + }) + expect(result.oldState).toBe('AbortError') + expect(result.firstEncodedBytes).toBeGreaterThan(100) + expect(result.updateSources).toEqual(Array.from({ length: 10 }).fill(result.source)) + expect(result.events.filter(event => event === 'done')).toHaveLength(1) + expect([...result.created].sort()).toEqual([...result.revoked].sort()) + expect(result.connected).toBe(false) + await testInfo.attach('thumbnail-native-source-cancellation', { contentType: 'application/json', body: JSON.stringify(result) }) + }) + } }) } /* global created, revoked, ArtplayerToolThumbnail, tool, observed, releaseBlob, pendingState, nativeBlob */ diff --git a/test/thumbnail-lifecycle.test.js b/test/thumbnail-lifecycle.test.js new file mode 100644 index 000000000..97567f405 --- /dev/null +++ b/test/thumbnail-lifecycle.test.js @@ -0,0 +1,439 @@ +import assert from 'node:assert/strict' +// eslint-disable-next-line test/no-import-node-test -- Candidate regressions also run against frozen workspace main. +import test from 'node:test' +import { thumbnailCandidate, thumbnailEnvironment } from './helpers/thumbnail.js' + +const implementation = await thumbnailCandidate() +const flush = () => new Promise(resolve => setImmediate(resolve)) +function dispatch(video, name) { + for (const callback of [...(video.handlers.get(name) || [])]) + callback({ target: video, type: name }) +} +function observe(promise) { + const state = { status: 'pending' } + promise.then(value => Object.assign(state, { status: 'resolved', value }), error => Object.assign(state, { status: 'rejected', error })) + return state +} +function setup(ready = true) { + const env = thumbnailEnvironment(implementation) + env.instance = new env.Factory({ fileInput: new env.Element('input'), number: 10, width: 40, column: 3 }) + env.instance.loadVideo({ name: 'first.mp4', type: 'video/mp4' }) + if (ready) { + env.instance.video.duration = 100 + dispatch(env.instance.video, 'loadedmetadata') + } + return env +} +async function firstFrame(env) { + await flush() + assert.equal(typeof env.instance.video.oncanplay, 'function') + env.instance.video.oncanplay() + await flush() +} +async function complete(env, state, count = 10) { + for (let index = 0; index < count; index++) + await firstFrame(env) + assert.equal(state.status, 'resolved') + assert.equal(state.value, undefined) +} +function cancelled(state) { + assert.equal(state.status, 'rejected') + assert.equal(state.error.name, 'AbortError') +} + +test('Thumbnail owned extraction preserves progress, event order, repeat runs and prior oncanplay', async () => { + const env = setup() + const previous = () => {} + env.instance.video.oncanplay = previous + const events = [] + for (const name of ['canvas', 'update', 'done']) + env.instance.on(name, (value, progress) => events.push([name, progress, env.instance.processing])) + const state = observe(env.instance.start()) + assert.deepEqual(events[0], ['canvas', undefined, false]) + await complete(env, state) + assert.deepEqual(events.filter(item => item[0] === 'update').map(item => item.slice(1)), Array.from({ length: 10 }, (_, i) => [(i + 1) / 10, true])) + assert.deepEqual(events.at(-1), ['done', undefined, false]) + assert.equal(env.instance.video.oncanplay, previous) + assert.equal(env.instance.video.handlers.get('seeked')?.size || 0, 0) + const second = observe(env.instance.start()) + await complete(env, second) + env.instance.destroy() + assert.deepEqual(env.operations.filter(item => item.name === 'createURL').map(item => item.url).sort(), env.operations.filter(item => item.name === 'revokeURL').map(item => item.url).sort()) +}) + +test('Thumbnail cancellation owns both metadata polling and a not-yet-started frame', async () => { + for (const ready of [false, true]) { + const env = setup(ready) + const state = observe(env.instance.start()) + env.instance.destroy() + await flush() + cancelled(state) + assert.equal(env.timers.size, 0) + assert.equal(env.instance.processing, false) + assert.equal(env.operations.filter(item => item.name === 'drawImage').length, 0) + for (const callbacks of env.instance.video.handlers.values()) + assert.equal(callbacks.size, 0) + } +}) + +test('Thumbnail metadata event resumes extraction and duplicate pending start is rejected', async () => { + const env = setup(false) + const state = observe(env.instance.start()) + assert.throws(() => env.instance.start(), /task in progress/) + env.instance.video.duration = 100 + dispatch(env.instance.video, 'loadedmetadata') + assert.equal(env.timers.size, 0) + await complete(env, state) + env.instance.destroy() +}) + +test('Thumbnail native media errors settle waiting and active jobs once', async () => { + for (const ready of [false, true]) { + const env = setup(ready) + const errors = [] + env.instance.on('error', message => errors.push(message)) + const state = observe(env.instance.start()) + await flush() + env.instance.video.error = { code: 3 } + dispatch(env.instance.video, 'error') + await flush() + assert.equal(state.status, 'rejected') + assert.match(state.error.message, /media error 3/) + assert.deepEqual(errors, ['Unable to load video: media error 3']) + assert.equal(env.instance.processing, false) + assert.equal(env.timers.size, 0) + env.instance.destroy() + } +}) + +test('Thumbnail idle native errors report without creating a task and stale source errors are ignored', () => { + const env = setup(false) + const oldError = [...(env.instance.video.handlers.get('error') || [])][0] + const errors = [] + env.instance.on('error', message => errors.push(message)) + env.instance.loadVideo({ name: 'second.mp4', type: 'video/mp4' }) + assert.equal(typeof oldError, 'function') + oldError() + assert.deepEqual(errors, []) + env.instance.video.error = { code: 4 } + dispatch(env.instance.video, 'error') + dispatch(env.instance.video, 'error') + assert.deepEqual(errors, ['Unable to load video: media error 4']) + env.instance.destroy() +}) + +test('Thumbnail destroy during native encoding rejects immediately and ignores the late Blob', async () => { + const env = setup() + env.controls.deferBlobs = true + const updates = [] + env.instance.on('update', value => updates.push(value)) + const state = observe(env.instance.start()) + await firstFrame(env) + env.instance.destroy() + await flush() + cancelled(state) + const count = env.operations.filter(item => item.name === 'createURL').length + env.blobs.shift()({ kind: 'image/png' }) + await flush() + assert.equal(env.operations.filter(item => item.name === 'createURL').length, count) + assert.deepEqual(updates, []) + assert.equal(env.instance.video.handlers.get('seeked')?.size || 0, 0) +}) + +test('Thumbnail switching source cancels the old encoding without cancelling the next job', async () => { + const env = setup() + env.controls.deferBlobs = true + const oldUrl = env.instance.videoUrl + const oldState = observe(env.instance.start()) + await firstFrame(env) + const late = env.blobs.shift() + env.instance.loadVideo({ name: 'second.mp4', type: 'video/mp4' }) + dispatch(env.instance.video, 'loadedmetadata') + const next = observe(env.instance.start()) + await flush() + cancelled(oldState) + late({ kind: 'late-png' }) + assert.equal(env.instance.processing, true) + assert.equal(env.instance.thumbnailUrl, undefined) + assert(env.operations.some(item => item.name === 'revokeURL' && item.url === oldUrl)) + env.controls.deferBlobs = false + await complete(env, next) + env.instance.destroy() +}) + +for (const failure of ['null-blob', 'draw', 'toBlob', 'seek', 'update-listener']) { + test(`Thumbnail ${failure} failure rejects instead of abandoning the extraction promise`, async () => { + const env = setup() + env.controls.deferBlobs = true + const error = new Error(failure) + const errors = [] + env.instance.on('error', message => errors.push(message)) + if (failure === 'draw') + env.context2D.drawImage = () => { throw error } + if (failure === 'toBlob') + env.Element.prototype.toBlob = () => { throw error } + if (failure === 'seek') + Object.defineProperty(env.instance.video, 'currentTime', { get: () => 0, set() { throw error } }) + if (failure === 'update-listener') + env.instance.on('update', () => { throw error }) + const state = observe(env.instance.start()) + if (failure === 'seek') + await flush() + else + await firstFrame(env) + if (failure === 'null-blob' || failure === 'update-listener') + env.blobs.shift()(failure === 'null-blob' ? null : { kind: 'image/png' }) + await flush() + assert.equal(state.status, 'rejected') + if (failure === 'null-blob') + assert.match(state.error.message, /Unable to create thumbnail/) + else + assert.equal(state.error, error) + assert.equal(errors.length, 1) + assert.equal(env.instance.processing, false) + env.instance.destroy() + }) +} + +test('Thumbnail duplicate readiness and duplicate Blob callback cannot duplicate a frame', async () => { + const env = setup() + env.controls.deferBlobs = true + const state = observe(env.instance.start()) + await flush() + const ready = env.instance.video.oncanplay + ready() + ready() + assert.equal(env.blobs.length, 1) + const blob = env.blobs.shift() + const updates = [] + env.instance.on('update', (url, progress) => updates.push(progress)) + blob({ kind: 'image/png' }) + blob({ kind: 'image/png' }) + assert.deepEqual(updates, [0.1]) + env.controls.deferBlobs = false + await complete(env, state, 9) + env.instance.destroy() +}) + +test('Thumbnail canvas listener destruction never starts a frame', async () => { + const env = setup() + const updates = [] + env.instance.on('canvas', () => env.instance.destroy()) + env.instance.on('update', () => updates.push('update')) + const state = observe(env.instance.start()) + await flush() + cancelled(state) + assert.deepEqual(updates, []) + assert.equal(env.instance.processing, false) + assert.equal(env.operations.filter(item => item.name === 'drawImage').length, 0) +}) + +test('Thumbnail file callback destruction and nested load cannot overwrite the latest source', () => { + for (const action of ['destroy', 'replace']) { + const env = setup() + const videos = [] + env.instance.on('video', () => videos.push(env.instance.file.name)) + env.instance.once('file', () => { + if (action === 'destroy') + env.instance.destroy() + else + env.instance.loadVideo({ name: 'latest.mp4', type: 'video/mp4' }) + }) + env.instance.loadVideo({ name: 'outer.mp4', type: 'video/mp4' }) + assert.deepEqual(videos, action === 'destroy' ? [] : ['latest.mp4']) + if (action === 'replace') + assert.equal(env.instance.video.src, env.instance.videoUrl) + env.instance.destroy() + const created = env.operations.filter(item => item.name === 'createURL').map(item => item.url) + const revoked = new Set(env.operations.filter(item => item.name === 'revokeURL').map(item => item.url)) + assert(created.every(url => revoked.has(url))) + } +}) + +test('Thumbnail file callback start waits for metadata of the new source', async () => { + const env = setup() + let state + env.instance.once('file', () => { + state = observe(env.instance.start()) + }) + env.instance.loadVideo({ name: 'second.mp4', type: 'video/mp4' }) + await flush() + assert.equal(env.instance.processing, false) + assert.equal(env.operations.filter(item => item.name === 'drawImage').length, 0) + dispatch(env.instance.video, 'loadedmetadata') + await complete(env, state) + env.instance.destroy() +}) + +test('Thumbnail error listener failure rejects with its error after resources are detached', async () => { + const env = setup() + env.controls.deferBlobs = true + const error = new Error('error listener failed') + env.instance.on('error', () => { + throw error + }) + const state = observe(env.instance.start()) + await firstFrame(env) + env.blobs.shift()(null) + await flush() + assert.equal(state.error, error) + assert.equal(env.instance.processing, false) + env.instance.destroy() +}) + +test('Thumbnail done listener may start a new task without old completion clearing it', async () => { + const env = setup() + let second + env.instance.once('done', () => { + second = observe(env.instance.start()) + }) + const first = observe(env.instance.start()) + await complete(env, first) + assert.equal(env.instance.processing, true) + await complete(env, second) + env.instance.destroy() +}) + +test('Thumbnail private URL ownership survives public URL replacement and closed calls allocate nothing', async () => { + const env = setup() + const first = observe(env.instance.start()) + await complete(env, first) + const owned = env.operations.filter(item => item.name === 'createURL').map(item => item.url) + env.instance.videoUrl = 'blob:external-video' + env.instance.thumbnailUrl = 'blob:external-sheet' + env.instance.destroy() + const revoked = new Set(env.operations.filter(item => item.name === 'revokeURL').map(item => item.url)) + assert(owned.every(url => revoked.has(url))) + const after = observe(env.instance.start()) + assert.equal(env.instance.download(), env.instance) + await flush() + cancelled(after) + assert.equal(env.timers.size, 0) + assert.equal(env.body.children.length, 0) +}) + +test('Thumbnail input attachment reentrant destroy cannot publish new input resources', () => { + const env = setup() + const wrapper = new env.Element('div') + const append = wrapper.appendChild + wrapper.appendChild = function (child) { + append.call(this, child) + env.instance.destroy() + return child + } + assert.throws(() => env.instance.setup({ fileInput: wrapper }), error => error.name === 'AbortError') + assert.equal(wrapper.children.length, 0) + assert.equal(env.body.children.length, 0) +}) + +test('Thumbnail nested input setup wins without outer cleanup overwriting its option', () => { + const env = setup() + const target = env.instance.option.fileInput + const second = new env.Element('input') + const latest = new env.Element('input') + const remove = target.removeEventListener + let nested = false + target.removeEventListener = function (name, callback) { + remove.call(this, name, callback) + if (!nested) { + nested = true + env.instance.setup({ fileInput: latest }) + } + } + env.instance.setup({ fileInput: second }) + assert.equal(env.instance.option.fileInput, latest) + for (const callbacks of second.handlers.values()) + assert.equal(callbacks.size, 0) + assert.equal(latest.handlers.get('change').size, 1) + env.instance.destroy() +}) + +test('Thumbnail source and job listener registration cannot revive resources after reentrant destroy', async () => { + for (const stage of ['source', 'job']) { + const env = setup(false) + const add = env.instance.video.addEventListener + let once = false + env.instance.video.addEventListener = function (name, callback) { + if (!once) { + once = true + env.instance.destroy() + } + add.call(this, name, callback) + } + if (stage === 'source') { + env.instance.loadVideo({ name: 'second.mp4', type: 'video/mp4' }) + } + else { + const state = observe(env.instance.start()) + await flush() + cancelled(state) + } + for (const callbacks of env.instance.video.handlers.values()) + assert.equal(callbacks.size, 0) + assert.equal(env.body.children.length, 0) + } +}) + +test('Thumbnail timer creation reentrant destroy clears the newly returned timer handle', async () => { + const env = setup(false) + const schedule = env.box.setTimeout + env.box.setTimeout = (callback, delay) => { + env.instance.destroy() + return schedule(callback, delay) + } + const state = observe(env.instance.start()) + await flush() + cancelled(state) + assert.equal(env.timers.size, 0) +}) + +test('Thumbnail URL creation reentrant load preserves the newer source and revokes the superseded URL', () => { + const env = setup() + const create = env.box.URL.createObjectURL + let once = false + env.box.URL.createObjectURL = (file) => { + if (!once) { + once = true + env.instance.loadVideo({ name: 'latest.mp4', type: 'video/mp4' }) + } + return create(file) + } + env.instance.loadVideo({ name: 'outer.mp4', type: 'video/mp4' }) + assert.equal(env.instance.file.name, 'latest.mp4') + assert.equal(env.instance.video.src, env.instance.videoUrl) + env.instance.destroy() + const created = env.operations.filter(item => item.name === 'createURL').map(item => item.url) + const revoked = new Set(env.operations.filter(item => item.name === 'revokeURL').map(item => item.url)) + assert(created.every(url => revoked.has(url))) +}) + +test('Thumbnail start before the first file retains its legacy wait-for-selection behavior', async () => { + const env = thumbnailEnvironment(implementation) + env.instance = new env.Factory({ fileInput: new env.Element('input'), number: 10 }) + const state = observe(env.instance.start()) + env.instance.loadVideo({ name: 'first.mp4', type: 'video/mp4' }) + env.instance.video.duration = 100 + dispatch(env.instance.video, 'loadedmetadata') + await complete(env, state) + env.instance.destroy() +}) + +test('Thumbnail destroy clears the media source and resets the decoder even when pause throws', () => { + const env = setup() + const failure = new Error('pause failed') + const events = [] + env.instance.video.pause = () => { + events.push('pause') + throw failure + } + env.instance.video.removeAttribute = (name) => { + events.push(`remove:${name}`) + delete env.instance.video[name] + } + env.instance.video.load = () => events.push('load') + assert.throws(() => env.instance.destroy(), error => error === failure) + assert.deepEqual(events, ['pause', 'remove:src', 'load']) + assert.equal(env.instance.video.src, undefined) + assert.equal(env.instance.video.parentNode, null) + assert.equal(env.operations.filter(item => item.name === 'revokeURL').length, 1) +})