From 8c57d56f1732e745a4f18f51d735d5c44a203878 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 07:53:29 +0800 Subject: [PATCH] refactor(auto-thumbnail): [PKG-AUTO-THUMB-03] wait for native frame presentation --- .../artplayer-plugin-auto-thumbnail.js | 2 +- .../artplayer-plugin-auto-thumbnail.legacy.js | 2 +- .../artplayer-plugin-auto-thumbnail.mjs | 332 +- package.json | 4 +- .../ARCHITECTURE.md | 20 +- .../dist/artplayer-plugin-auto-thumbnail.js | 2 +- .../artplayer-plugin-auto-thumbnail.legacy.js | 2 +- .../dist/artplayer-plugin-auto-thumbnail.mjs | 332 +- .../src/extraction.js | 22 +- .../src/frames.js | 110 + .../src/video.js | 2 +- .../auto-thumbnail-frame-presentation.json | 9759 +++++++++++++++++ ...13-PKG-AUTO-THUMB-03-frame-presentation.md | 47 + refactor/plan.md | 2 +- refactor/progress.md | 11 + refactor/risks.json | 6 +- refactor/tasks.json | 4 +- test/auto-thumbnail-frames.test.js | 264 + test/browser/auto-thumbnail-lifecycle.spec.js | 43 +- test/browser/auto-thumbnail-pixels.spec.js | 36 +- test/helpers/auto-thumbnail.js | 10 +- 21 files changed, 10716 insertions(+), 296 deletions(-) create mode 100644 packages/artplayer-plugin-auto-thumbnail/src/frames.js create mode 100644 refactor/baselines/auto-thumbnail-frame-presentation.json create mode 100644 refactor/changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md create mode 100644 test/auto-thumbnail-frames.test.js diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.js b/docs/compiled/artplayer-plugin-auto-thumbnail.js index 92ad75057..093637ba6 100644 --- a/docs/compiled/artplayer-plugin-auto-thumbnail.js +++ b/docs/compiled/artplayer-plugin-auto-thumbnail.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e,t){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const i of["onloadedmetadata","onseeked","onerror"])e.own(()=>{t[i]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const n=i.duration,r=i.videoHeight,o=i.videoWidth,{height:a,canvasWidth:u,canvasHeight:d}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const i=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),r=Math.trunc(i*Math.ceil(e.number/10));if(![i,n,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:i,canvasWidth:n,canvasHeight:r}}(t,{duration:n,videoHeight:r,videoWidth:o});if(!e.active())return;i.width=o,i.height=r,i.style.width=`${o}px`,i.style.height=`${r}px`;const s=document.createElement("canvas");if(!e.active())return;s.width=u,s.height=d;const c=s.getContext("2d");if(!c)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let l=0;const h=e.guard(()=>{if(l>=t.number)return void e.dispose();const r=n*l/t.number;let o=0;i.onseeked=e.guard(()=>{if(i.seeking)return;const n=i.currentTime;if(!e.active())return;if(!Number.isFinite(n)||Math.abs(n-r)>.05){if(++o>3)throw new Error("Auto-thumbnail seek did not reach the requested time");return void(i.currentTime=r)}if(i.onseeked=null,c.drawImage(i,l%10*t.width,Math.floor(l/10)*a,t.width,a),!e.active())return;let u=!1;s.toBlob(e.guard(i=>{u||(u=!0,e.publish(i,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(l+=1,h()))}),"image/jpeg")}),e.active()&&(i.currentTime=r)});h()}),e.active()&&(i.src=t.url))}function t(e){let t,i=!1;for(const r of e)try{r()}catch(n){i||(i=!0,t=n)}return{failed:i,failure:t}}return function(i){return async n=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(e,i){let n,r,o=!1;const a=new Set;function u(e){a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=n;n=void 0,e?.dispose()}return{start:function(){if(o)return;const d=n,s=[];let c=!1;const l={active:()=>!o&&!c&&n===l,own(e){if(c){const n=t([e]);n.failed&&i(n.failure)}else s.push(e)},dispose(){if(c)return;c=!0,n===l&&(n=void 0);const e=t(s.splice(0).reverse());e.failed&&i(e.failure)},fail(e){l.active()&&(l.dispose(),i(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(i){l.fail(i)}},publish(n,o){if(!l.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!l.active())return void u(d);const s=r;r=d;try{e({url:d,...o})}catch(c){r===d&&(r=a.has(s)?s:void 0);const e=t([()=>u(d)]);throw e.failed&&i(e.failure),c}u(s)}};return n=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const e=t([d,...[...a].map(e=>()=>u(e))]);r=void 0,e.failed&&i(e.failure)},get closed(){return o}}}(e=>{n.thumbnails=e},r),a=[],u=()=>{const t=o.start();t&&t.guard(()=>{const r=function(e,t){const i={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(i.width))||i.width<=0||!Number.isFinite(Number(i.number))||i.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return i}(i,()=>n.option.url);t.active()&&e(t,r)})()},d=()=>{const e=a.splice(0),i=t([o.destroy,...e]);i.failed&&r(i.failure)};try{for(const[e,t]of[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]]){if(o.closed)break;a.push(()=>n.off(e,t)),n.on(e,t),o.closed&&n.off(e,t)}}catch(s){throw d(),s}return{name:"artplayerPluginAutoThumbnail"}}}}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e){let t,n=!1;for(const r of e)try{r()}catch(i){n||(n=!0,t=i)}return{failed:n,failure:t}}function t(t,n){const i="function"===typeof n.requestVideoFrameCallback&&"function"===typeof n.cancelVideoFrameCallback;let r;function o(){const t=r;if(r=void 0,n.onloadeddata=null,n.onseeked=null,!t)return;const i=e([()=>clearTimeout(t.timer),()=>{null!==t.frame&&n.cancelVideoFrameCallback(t.frame)}]);if(i.failed)throw i.failure}return t.own(o),(e,a)=>{if(o(),!t.active())return;const u={frame:null,timer:null,sought:!1,presented:!i,retries:0,sequence:0,started:!1};r=u;const d=()=>t.active()&&r===u,s=t.guard(()=>{d()&&u.sought&&u.presented&&(o(),t.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!i)return;const e=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&n.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=n.requestVideoFrameCallback(t.guard(()=>{d()&&e===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,s())}));d()&&e===u.sequence&&!o?u.frame=a:n.cancelVideoFrameCallback(a)})(),d()&&(n.currentTime=e)},l=t.guard(()=>{d()&&!u.started&&(u.started=!0,n.onloadeddata=null,n.onseeked=t.guard(()=>{if(!d()||n.seeking)return;const t=n.currentTime;if(d())if(!Number.isFinite(t)||Math.abs(t-e)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,s()}),c())});if(u.timer=setTimeout(t.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!i||n.readyState>=2;d()&&(f?l():n.onloadeddata=l)}}function n(e,n){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const r=i.duration,o=i.videoHeight,a=i.videoWidth,{height:u,canvasWidth:d,canvasHeight:s}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),i=Math.trunc(10*e.width),r=Math.trunc(n*Math.ceil(e.number/10));if(![n,i,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:i,canvasHeight:r}}(n,{duration:r,videoHeight:o,videoWidth:a});if(!e.active())return;i.width=a,i.height=o,i.style.width=`${a}px`,i.style.height=`${o}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=s;const l=c.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,i);let h=0;const m=e.guard(()=>{h>=n.number?e.dispose():f(r*h/n.number,e.guard(()=>{if(l.drawImage(i,h%10*n.width,Math.floor(h/10)*u,n.width,u),!e.active())return;let t=!1;c.toBlob(e.guard(i=>{t||(t=!0,e.publish(i,{height:u,column:10,number:n.number,width:n.width,scale:n.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(i.src=n.url))}return function(t){return async i=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(t,n){let i,r,o=!1;const a=new Set;function u(e){a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=i;i=void 0,e?.dispose()}return{start:function(){if(o)return;const d=i,s=[];let c=!1;const l={active:()=>!o&&!c&&i===l,own(t){if(c){const i=e([t]);i.failed&&n(i.failure)}else s.push(t)},dispose(){if(c)return;c=!0,i===l&&(i=void 0);const t=e(s.splice(0).reverse());t.failed&&n(t.failure)},fail(e){l.active()&&(l.dispose(),n(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(n){l.fail(n)}},publish(i,o){if(!l.active())return;if(!i)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(i);if(a.add(d),!l.active())return void u(d);const s=r;r=d;try{t({url:d,...o})}catch(c){r===d&&(r=a.has(s)?s:void 0);const t=e([()=>u(d)]);throw t.failed&&n(t.failure),c}u(s)}};return i=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const t=e([d,...[...a].map(e=>()=>u(e))]);r=void 0,t.failed&&n(t.failure)},get closed(){return o}}}(e=>{i.thumbnails=e},r),a=[],u=()=>{const e=o.start();e&&e.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(t,()=>i.option.url);e.active()&&n(e,r)})()},d=()=>{const t=a.splice(0),n=e([o.destroy,...t]);n.failed&&r(n.failure)};try{for(const[e,t]of[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]]){if(o.closed)break;a.push(()=>i.off(e,t)),i.on(e,t),o.closed&&i.off(e,t)}}catch(s){throw d(),s}return{name:"artplayerPluginAutoThumbnail"}}}}); diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js b/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js index 18a135348..148680572 100644 --- a/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js +++ b/docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable,r=(t,i,n)=>i in t?e(t,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[i]=n;function o(e,t){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const i of["onloadedmetadata","onseeked","onerror"])e.own(()=>{t[i]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const n=i.duration,r=i.videoHeight,o=i.videoWidth,{height:a,canvasWidth:u,canvasHeight:d}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const i=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),r=Math.trunc(i*Math.ceil(e.number/10));if(![i,n,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:i,canvasWidth:n,canvasHeight:r}}(t,{duration:n,videoHeight:r,videoWidth:o});if(!e.active())return;i.width=o,i.height=r,i.style.width=`${o}px`,i.style.height=`${r}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=u,c.height=d;const l=c.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let s=0;const h=e.guard(()=>{if(s>=t.number)return void e.dispose();const r=n*s/t.number;let o=0;i.onseeked=e.guard(()=>{if(i.seeking)return;const n=i.currentTime;if(!e.active())return;if(!Number.isFinite(n)||Math.abs(n-r)>.05){if(++o>3)throw new Error("Auto-thumbnail seek did not reach the requested time");return void(i.currentTime=r)}if(i.onseeked=null,l.drawImage(i,s%10*t.width,Math.floor(s/10)*a,t.width,a),!e.active())return;let u=!1;c.toBlob(e.guard(i=>{u||(u=!0,e.publish(i,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(s+=1,h()))}),"image/jpeg")}),e.active()&&(i.currentTime=r)});h()}),e.active()&&(i.src=t.url))}function a(e){let t,i=!1;for(const r of e)try{r()}catch(n){i||(i=!0,t=n)}return{failed:i,failure:t}}function u(e,o){let u,d,c=!1;const l=new Set;function s(e){l.delete(e)&&URL.revokeObjectURL(e)}function h(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(c)return;const h=u,f=[];let m=!1;const v={active:()=>!c&&!m&&u===v,own(e){if(m){const t=a([e]);t.failed&&o(t.failure)}else f.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=a(f.splice(0).reverse());e.failed&&o(e.failure)},fail(e){v.active()&&(v.dispose(),o(e))},guard:e=>(...t)=>{if(v.active())try{return e(...t)}catch(i){v.fail(i)}},publish(u,c){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const h=URL.createObjectURL(u);if(l.add(h),!v.active())return void s(h);const f=d;d=h;try{e(((e,o)=>{for(var a in o||(o={}))i.call(o,a)&&r(e,a,o[a]);if(t)for(var a of t(o))n.call(o,a)&&r(e,a,o[a]);return e})({url:h},c))}catch(m){d===h&&(d=l.has(f)?f:void 0);const e=a([()=>s(h)]);throw e.failed&&o(e.failure),m}s(f)}};return u=v,null==h||h.dispose(),v},cancel:h,destroy:function(){if(c)return;c=!0;const e=a([h,...[...l].map(e=>()=>s(e))]);d=void 0,e.failed&&o(e.failure)},get closed(){return c}}}return function(e){return t=>{return i=null,n=null,r=function*(){const i=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),n=u(e=>{t.thumbnails=e},i),r=[],d=()=>{const i=n.start();i&&i.guard(()=>{const n=function(e,t){const i={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(i.width))||i.width<=0||!Number.isFinite(Number(i.number))||i.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return i}(e,()=>t.option.url);i.active()&&o(i,n)})()},c=()=>{const e=r.splice(0),t=a([n.destroy,...e]);t.failed&&i(t.failure)};try{for(const[e,i]of[["destroy",c],["restart",n.cancel],["video:loadedmetadata",d]]){if(n.closed)break;r.push(()=>t.off(e,i)),t.on(e,i),n.closed&&t.off(e,i)}}catch(l){throw c(),l}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(r.next(e))}catch(i){t(i)}},a=e=>{try{u(r.throw(e))}catch(i){t(i)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((r=r.apply(i,n)).next())});var i,n,r}}}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable,i=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;function o(e){let t,n=!1;for(const i of e)try{i()}catch(r){n||(n=!0,t=r)}return{failed:n,failure:t}}function a(e,a){let u,d,l=!1;const c=new Set;function s(e){c.delete(e)&&URL.revokeObjectURL(e)}function f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(l)return;const f=u,h=[];let m=!1;const b={active:()=>!l&&!m&&u===b,own(e){if(m){const t=o([e]);t.failed&&a(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===b&&(u=void 0);const e=o(h.splice(0).reverse());e.failed&&a(e.failure)},fail(e){b.active()&&(b.dispose(),a(e))},guard:e=>(...t)=>{if(b.active())try{return e(...t)}catch(n){b.fail(n)}},publish(u,l){if(!b.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(c.add(f),!b.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))n.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))r.call(o,a)&&i(e,a,o[a]);return e})({url:f},l))}catch(m){d===f&&(d=c.has(h)?h:void 0);const e=o([()=>s(f)]);throw e.failed&&a(e.failure),m}s(h)}};return u=b,null==f||f.dispose(),b},cancel:f,destroy:function(){if(l)return;l=!0;const e=o([f,...[...c].map(e=>()=>s(e))]);d=void 0,e.failed&&a(e.failure)},get closed(){return l}}}function u(e,t){const n="function"===typeof t.requestVideoFrameCallback&&"function"===typeof t.cancelVideoFrameCallback;let r;function i(){const e=r;if(r=void 0,t.onloadeddata=null,t.onseeked=null,!e)return;const n=o([()=>clearTimeout(e.timer),()=>{null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(i),(o,a)=>{if(i(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>e.active()&&r===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(i(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const r=++u.sequence;u.presented=!1;const i=u.frame;if(u.frame=null,null!==i&&t.cancelVideoFrameCallback(i),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&r===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&r===u.sequence&&!o?u.frame=a:t.cancelVideoFrameCallback(a)})(),d()&&(t.currentTime=o)},s=e.guard(()=>{d()&&!u.started&&(u.started=!0,t.onloadeddata=null,t.onseeked=e.guard(()=>{if(!d()||t.seeking)return;const e=t.currentTime;if(d())if(!Number.isFinite(e)||Math.abs(e-o)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,l()}),c())});if(u.timer=setTimeout(e.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||t.readyState>=2;d()&&(f?s():t.onloadeddata=s)}}function d(e,t){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,i=n.videoHeight,o=n.videoWidth,{height:a,canvasWidth:d,canvasHeight:l}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),r=Math.trunc(10*e.width),i=Math.trunc(n*Math.ceil(e.number/10));if(![n,r,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:r,canvasHeight:i}}(t,{duration:r,videoHeight:i,videoWidth:o});if(!e.active())return;n.width=o,n.height=i,n.style.width=`${o}px`,n.style.height=`${i}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=l;const s=c.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=u(e,n);let h=0;const m=e.guard(()=>{h>=t.number?e.dispose():f(r*h/t.number,e.guard(()=>{if(s.drawImage(n,h%10*t.width,Math.floor(h/10)*a,t.width,a),!e.active())return;let r=!1;c.toBlob(e.guard(n=>{r||(r=!0,e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=t.url))}return function(e){return t=>{return n=null,r=null,i=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),r=a(e=>{t.thumbnails=e},n),i=[],u=()=>{const n=r.start();n&&n.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(e,()=>t.option.url);n.active()&&d(n,r)})()},l=()=>{const e=i.splice(0),t=o([r.destroy,...e]);t.failed&&n(t.failure)};try{for(const[e,n]of[["destroy",l],["restart",r.cancel],["video:loadedmetadata",u]]){if(r.closed)break;i.push(()=>t.off(e,n)),t.on(e,n),r.closed&&t.off(e,n)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(n){t(n)}},a=e=>{try{u(i.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(n,r)).next())});var n,r,i}}}); diff --git a/docs/compiled/artplayer-plugin-auto-thumbnail.mjs b/docs/compiled/artplayer-plugin-auto-thumbnail.mjs index a3c006446..77f24658f 100644 --- a/docs/compiled/artplayer-plugin-auto-thumbnail.mjs +++ b/docs/compiled/artplayer-plugin-auto-thumbnail.mjs @@ -4,127 +4,6 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -function readOptions(option, getFallbackUrl) { - const config = { - url: option.url || getFallbackUrl(), - width: option.width || 160, - number: option.number || 100, - scale: option.scale || 1 - }; - if (!Number.isFinite(Number(config.width)) || config.width <= 0 || !Number.isFinite(Number(config.number)) || config.number <= 0) - throw new TypeError("Auto-thumbnail width and number must be finite positive numbers"); - return config; -} -function sheetSize(config, video) { - if (!Number.isFinite(video.duration) || video.duration <= 0 || !Number.isFinite(video.videoWidth) || video.videoWidth <= 0 || !Number.isFinite(video.videoHeight) || video.videoHeight <= 0) - throw new TypeError("Auto-thumbnail requires finite media duration and dimensions"); - const height = Math.floor(config.width * video.videoHeight / video.videoWidth); - const canvasWidth = Math.trunc(config.width * 10); - const canvasHeight = Math.trunc(height * Math.ceil(config.number / 10)); - if (![height, canvasWidth, canvasHeight].every((value) => Number.isSafeInteger(value) && value > 0 && value <= 4294967295)) - throw new RangeError("Auto-thumbnail canvas dimensions are invalid"); - return { height, canvasWidth, canvasHeight }; -} -function createVideo(job) { - const video = document.createElement("video"); - job.own(() => video.remove()); - job.own(() => video.load()); - job.own(() => video.removeAttribute("src")); - job.own(() => video.pause()); - for (const property of ["onloadedmetadata", "onseeked", "onerror"]) - job.own(() => { - video[property] = null; - }); - if (!job.active()) - return video; - video.crossOrigin = "anonymous"; - video.muted = true; - video.playsInline = true; - video.tabIndex = -1; - video.setAttribute("aria-hidden", "true"); - video.style.cssText = "position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none"; - document.documentElement.appendChild(video); - if (!job.active()) - video.remove(); - return video; -} -function extract(job, config) { - const video = createVideo(job); - if (!job.active()) - return; - video.onerror = job.guard(() => { - throw video.error || new Error("Auto-thumbnail media failed to load"); - }); - video.onloadedmetadata = job.guard(() => { - video.onloadedmetadata = null; - const duration = video.duration; - const videoHeight = video.videoHeight; - const videoWidth = video.videoWidth; - const { height, canvasWidth, canvasHeight } = sheetSize(config, { - duration, - videoHeight, - videoWidth - }); - if (!job.active()) - return; - video.width = videoWidth; - video.height = videoHeight; - video.style.width = `${videoWidth}px`; - video.style.height = `${videoHeight}px`; - const canvas = document.createElement("canvas"); - if (!job.active()) - return; - canvas.width = canvasWidth; - canvas.height = canvasHeight; - const ctx = canvas.getContext("2d"); - if (!ctx) - throw new Error("Auto-thumbnail canvas context is unavailable"); - if (!job.active()) - return; - let index = 0; - const seek = job.guard(() => { - if (index >= config.number) { - job.dispose(); - return; - } - const target = duration * index / config.number; - let retries = 0; - video.onseeked = job.guard(() => { - if (video.seeking) - return; - const time = video.currentTime; - if (!job.active()) - return; - if (!Number.isFinite(time) || Math.abs(time - target) > 0.05) { - if (++retries > 3) - throw new Error("Auto-thumbnail seek did not reach the requested time"); - video.currentTime = target; - return; - } - video.onseeked = null; - ctx.drawImage(video, index % 10 * config.width, Math.floor(index / 10) * height, config.width, height); - if (!job.active()) - return; - let delivered = false; - canvas.toBlob(job.guard((blob) => { - if (delivered) - return; - delivered = true; - job.publish(blob, { height, column: 10, number: config.number, width: config.width, scale: config.scale }); - if (job.active()) { - index += 1; - seek(); - } - }), "image/jpeg"); - }); - if (job.active()) - video.currentTime = target; - }); - seek(); - }); - if (job.active()) - video.src = config.url; -} function cleanupAll(actions) { let failure; let failed = false; @@ -246,6 +125,217 @@ function createSession(publish, report) { } }; } +function createFrameReader(job, video) { + const presentedFrames = typeof video.requestVideoFrameCallback === "function" && typeof video.cancelVideoFrameCallback === "function"; + let pending; + function clear() { + const previous = pending; + pending = void 0; + video.onloadeddata = null; + video.onseeked = null; + if (!previous) + return; + const result = cleanupAll([ + () => clearTimeout(previous.timer), + () => { + if (previous.frame !== null) + video.cancelVideoFrameCallback(previous.frame); + } + ]); + if (result.failed) + throw result.failure; + } + job.own(clear); + return (target, draw) => { + clear(); + if (!job.active()) + return; + const current = { frame: null, timer: null, sought: false, presented: !presentedFrames, retries: 0, sequence: 0, started: false }; + pending = current; + const active = () => job.active() && pending === current; + const finish = job.guard(() => { + if (!active() || !current.sought || !current.presented) + return; + clear(); + if (job.active()) + draw(); + }); + const requestFrame = () => { + if (!presentedFrames) + return; + const sequence = ++current.sequence; + current.presented = false; + const previous = current.frame; + current.frame = null; + if (previous !== null) + video.cancelVideoFrameCallback(previous); + if (!active()) + return; + let delivered = false; + const id = video.requestVideoFrameCallback(job.guard(() => { + if (!active() || sequence !== current.sequence || delivered) + return; + delivered = true; + current.frame = null; + current.presented = true; + finish(); + })); + if (active() && sequence === current.sequence && !delivered) + current.frame = id; + else + video.cancelVideoFrameCallback(id); + }; + const seek = () => { + current.sought = false; + requestFrame(); + if (active()) + video.currentTime = target; + }; + const begin = job.guard(() => { + if (!active() || current.started) + return; + current.started = true; + video.onloadeddata = null; + video.onseeked = job.guard(() => { + if (!active() || video.seeking) + return; + const time = video.currentTime; + if (!active()) + return; + if (!Number.isFinite(time) || Math.abs(time - target) > 0.05) { + if (++current.retries > 3) + throw new Error("Auto-thumbnail seek did not reach the requested time"); + seek(); + return; + } + current.sought = true; + finish(); + }); + seek(); + }); + current.timer = setTimeout(job.guard(() => { + if (active()) + throw new Error("Auto-thumbnail frame readiness timed out"); + }), 3e4); + if (!active()) { + clearTimeout(current.timer); + return; + } + const ready = !presentedFrames || video.readyState >= 2; + if (!active()) + return; + if (!ready) + video.onloadeddata = begin; + else + begin(); + }; +} +function readOptions(option, getFallbackUrl) { + const config = { + url: option.url || getFallbackUrl(), + width: option.width || 160, + number: option.number || 100, + scale: option.scale || 1 + }; + if (!Number.isFinite(Number(config.width)) || config.width <= 0 || !Number.isFinite(Number(config.number)) || config.number <= 0) + throw new TypeError("Auto-thumbnail width and number must be finite positive numbers"); + return config; +} +function sheetSize(config, video) { + if (!Number.isFinite(video.duration) || video.duration <= 0 || !Number.isFinite(video.videoWidth) || video.videoWidth <= 0 || !Number.isFinite(video.videoHeight) || video.videoHeight <= 0) + throw new TypeError("Auto-thumbnail requires finite media duration and dimensions"); + const height = Math.floor(config.width * video.videoHeight / video.videoWidth); + const canvasWidth = Math.trunc(config.width * 10); + const canvasHeight = Math.trunc(height * Math.ceil(config.number / 10)); + if (![height, canvasWidth, canvasHeight].every((value) => Number.isSafeInteger(value) && value > 0 && value <= 4294967295)) + throw new RangeError("Auto-thumbnail canvas dimensions are invalid"); + return { height, canvasWidth, canvasHeight }; +} +function createVideo(job) { + const video = document.createElement("video"); + job.own(() => video.remove()); + job.own(() => video.load()); + job.own(() => video.removeAttribute("src")); + job.own(() => video.pause()); + for (const property of ["onloadedmetadata", "onloadeddata", "onseeked", "onerror"]) + job.own(() => { + video[property] = null; + }); + if (!job.active()) + return video; + video.crossOrigin = "anonymous"; + video.muted = true; + video.playsInline = true; + video.tabIndex = -1; + video.setAttribute("aria-hidden", "true"); + video.style.cssText = "position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none"; + document.documentElement.appendChild(video); + if (!job.active()) + video.remove(); + return video; +} +function extract(job, config) { + const video = createVideo(job); + if (!job.active()) + return; + video.onerror = job.guard(() => { + throw video.error || new Error("Auto-thumbnail media failed to load"); + }); + video.onloadedmetadata = job.guard(() => { + video.onloadedmetadata = null; + const duration = video.duration; + const videoHeight = video.videoHeight; + const videoWidth = video.videoWidth; + const { height, canvasWidth, canvasHeight } = sheetSize(config, { + duration, + videoHeight, + videoWidth + }); + if (!job.active()) + return; + video.width = videoWidth; + video.height = videoHeight; + video.style.width = `${videoWidth}px`; + video.style.height = `${videoHeight}px`; + const canvas = document.createElement("canvas"); + if (!job.active()) + return; + canvas.width = canvasWidth; + canvas.height = canvasHeight; + const ctx = canvas.getContext("2d"); + if (!ctx) + throw new Error("Auto-thumbnail canvas context is unavailable"); + if (!job.active()) + return; + const readFrame = createFrameReader(job, video); + let index = 0; + const seek = job.guard(() => { + if (index >= config.number) { + job.dispose(); + return; + } + readFrame(duration * index / config.number, job.guard(() => { + ctx.drawImage(video, index % 10 * config.width, Math.floor(index / 10) * height, config.width, height); + if (!job.active()) + return; + let delivered = false; + canvas.toBlob(job.guard((blob) => { + if (delivered) + return; + delivered = true; + job.publish(blob, { height, column: 10, number: config.number, width: config.width, scale: config.scale }); + if (job.active()) { + index += 1; + seek(); + } + }), "image/jpeg"); + })); + }); + seek(); + }); + if (job.active()) + video.src = config.url; +} function artplayerPluginAutoThumbnail(option) { return async (art) => { const report = (error) => console.warn("ArtPlayer auto-thumbnail failed:", error); diff --git a/package.json b/package.json index 81583db43..269ba0ba8 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/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", + "test:unit": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", "test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs", "test": "yarn test:node && yarn test:baseline", "test:browser": "playwright test", @@ -85,7 +85,7 @@ "test:iframe-types-package": "node refactor/scripts/iframe-package-types.mjs", "test:thumbnail": "node --test test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs refactor/scripts/thumbnail-types.test.mjs", "test:thumbnail-types-package": "node refactor/scripts/thumbnail-package-types.mjs", - "test:auto-thumbnail": "node --test test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs" + "test:auto-thumbnail": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md index f987a109d..82d30e9d3 100644 --- a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md +++ b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md @@ -31,6 +31,12 @@ for video processing. video sizing rules. It is silent, excluded from focus/accessibility, and never played by this implementation. Cleanup also removes the element if insertion, cancellation, pause or decoder reset fails. +- `src/frames.js` owns one pending sample, its seek/data handlers, deadline and + native presentation callback. When request/cancel frame callbacks are both + available it waits for loaded data, then requires both seek completion and frame + presentation before drawing. Callback identities are invalidated on retry and + replacement; late/duplicate delivery cannot complete a newer sample. One job + cleanup handles every frame without accumulating per-frame cleanup closures. Only the entry module receives ArtPlayer. Extraction receives a guarded job and a configuration snapshot. These internal modules do not add public player fields, @@ -52,6 +58,10 @@ sample; a mismatch retries that same target at most three times, then fails with cleanup and retains any previous preview. This prevents observed stale seek events from advancing the sheet, but does not prove frame presentation accuracy: the current time can match even when the drawable first frame is stale. +Each pending data/seek/presentation wait has a 30-second deadline. Expiry cancels +its callback and decoder, reports through the existing warning path, and retains +the last usable preview. This is a per-sample readiness deadline, not a complete +network, encoder, background-tab or aggregate resource policy. The previous empty first encode is removed. Encoding is serial, duplicate native callbacks are consumed once, and source replacement/destruction invalidates old @@ -82,10 +92,12 @@ defects; their passing status does not mean those defects should remain. This is an intermediate `PKG-AUTO-THUMB-03` implementation. Native lifecycle validation does not prove correct pixels. The attached renderer now has native -pixel checks for cells 2-4 (zero-based) on a five-frame sheet, including real black -content, changing colors and spatial detail. Cells 0-1 remain diagnostic only: -initial transparent/stale draws and WebKit frame-time offsets are unresolved -(`AUTO-THUMB-PIXEL-01`). Merely waiting for `seeked`, adding two animation frames, +pixel checks for all five cells when native frame callbacks are available, +including the unique first frame, real black content, changing colors, spatial +detail and presentation timestamps. The callback-less fallback still only has +acceptance for cells 2-4 (zero-based); its cells 0-1 are diagnostic only. Initial +transparent/stale draws and Windows WebKit frame-time offsets remain unresolved +under `AUTO-THUMB-PIXEL-01`. Merely waiting for `seeked`, adding two animation frames, or seeking away and back did not consistently repair the first frame. Do not close this risk based on the later-cell assertions or replace it with a nonblack test; legitimate black frames have opaque pixels. Frame timing, additional diff --git a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js index 92ad75057..093637ba6 100644 --- a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js +++ b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e,t){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const i of["onloadedmetadata","onseeked","onerror"])e.own(()=>{t[i]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const n=i.duration,r=i.videoHeight,o=i.videoWidth,{height:a,canvasWidth:u,canvasHeight:d}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const i=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),r=Math.trunc(i*Math.ceil(e.number/10));if(![i,n,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:i,canvasWidth:n,canvasHeight:r}}(t,{duration:n,videoHeight:r,videoWidth:o});if(!e.active())return;i.width=o,i.height=r,i.style.width=`${o}px`,i.style.height=`${r}px`;const s=document.createElement("canvas");if(!e.active())return;s.width=u,s.height=d;const c=s.getContext("2d");if(!c)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let l=0;const h=e.guard(()=>{if(l>=t.number)return void e.dispose();const r=n*l/t.number;let o=0;i.onseeked=e.guard(()=>{if(i.seeking)return;const n=i.currentTime;if(!e.active())return;if(!Number.isFinite(n)||Math.abs(n-r)>.05){if(++o>3)throw new Error("Auto-thumbnail seek did not reach the requested time");return void(i.currentTime=r)}if(i.onseeked=null,c.drawImage(i,l%10*t.width,Math.floor(l/10)*a,t.width,a),!e.active())return;let u=!1;s.toBlob(e.guard(i=>{u||(u=!0,e.publish(i,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(l+=1,h()))}),"image/jpeg")}),e.active()&&(i.currentTime=r)});h()}),e.active()&&(i.src=t.url))}function t(e){let t,i=!1;for(const r of e)try{r()}catch(n){i||(i=!0,t=n)}return{failed:i,failure:t}}return function(i){return async n=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(e,i){let n,r,o=!1;const a=new Set;function u(e){a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=n;n=void 0,e?.dispose()}return{start:function(){if(o)return;const d=n,s=[];let c=!1;const l={active:()=>!o&&!c&&n===l,own(e){if(c){const n=t([e]);n.failed&&i(n.failure)}else s.push(e)},dispose(){if(c)return;c=!0,n===l&&(n=void 0);const e=t(s.splice(0).reverse());e.failed&&i(e.failure)},fail(e){l.active()&&(l.dispose(),i(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(i){l.fail(i)}},publish(n,o){if(!l.active())return;if(!n)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(n);if(a.add(d),!l.active())return void u(d);const s=r;r=d;try{e({url:d,...o})}catch(c){r===d&&(r=a.has(s)?s:void 0);const e=t([()=>u(d)]);throw e.failed&&i(e.failure),c}u(s)}};return n=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const e=t([d,...[...a].map(e=>()=>u(e))]);r=void 0,e.failed&&i(e.failure)},get closed(){return o}}}(e=>{n.thumbnails=e},r),a=[],u=()=>{const t=o.start();t&&t.guard(()=>{const r=function(e,t){const i={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(i.width))||i.width<=0||!Number.isFinite(Number(i.number))||i.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return i}(i,()=>n.option.url);t.active()&&e(t,r)})()},d=()=>{const e=a.splice(0),i=t([o.destroy,...e]);i.failed&&r(i.failure)};try{for(const[e,t]of[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]]){if(o.closed)break;a.push(()=>n.off(e,t)),n.on(e,t),o.closed&&n.off(e,t)}}catch(s){throw d(),s}return{name:"artplayerPluginAutoThumbnail"}}}}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";function e(e){let t,n=!1;for(const r of e)try{r()}catch(i){n||(n=!0,t=i)}return{failed:n,failure:t}}function t(t,n){const i="function"===typeof n.requestVideoFrameCallback&&"function"===typeof n.cancelVideoFrameCallback;let r;function o(){const t=r;if(r=void 0,n.onloadeddata=null,n.onseeked=null,!t)return;const i=e([()=>clearTimeout(t.timer),()=>{null!==t.frame&&n.cancelVideoFrameCallback(t.frame)}]);if(i.failed)throw i.failure}return t.own(o),(e,a)=>{if(o(),!t.active())return;const u={frame:null,timer:null,sought:!1,presented:!i,retries:0,sequence:0,started:!1};r=u;const d=()=>t.active()&&r===u,s=t.guard(()=>{d()&&u.sought&&u.presented&&(o(),t.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!i)return;const e=++u.sequence;u.presented=!1;const r=u.frame;if(u.frame=null,null!==r&&n.cancelVideoFrameCallback(r),!d())return;let o=!1;const a=n.requestVideoFrameCallback(t.guard(()=>{d()&&e===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,s())}));d()&&e===u.sequence&&!o?u.frame=a:n.cancelVideoFrameCallback(a)})(),d()&&(n.currentTime=e)},l=t.guard(()=>{d()&&!u.started&&(u.started=!0,n.onloadeddata=null,n.onseeked=t.guard(()=>{if(!d()||n.seeking)return;const t=n.currentTime;if(d())if(!Number.isFinite(t)||Math.abs(t-e)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,s()}),c())});if(u.timer=setTimeout(t.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!i||n.readyState>=2;d()&&(f?l():n.onloadeddata=l)}}function n(e,n){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const r=i.duration,o=i.videoHeight,a=i.videoWidth,{height:u,canvasWidth:d,canvasHeight:s}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),i=Math.trunc(10*e.width),r=Math.trunc(n*Math.ceil(e.number/10));if(![n,i,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:i,canvasHeight:r}}(n,{duration:r,videoHeight:o,videoWidth:a});if(!e.active())return;i.width=a,i.height=o,i.style.width=`${a}px`,i.style.height=`${o}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=s;const l=c.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=t(e,i);let h=0;const m=e.guard(()=>{h>=n.number?e.dispose():f(r*h/n.number,e.guard(()=>{if(l.drawImage(i,h%10*n.width,Math.floor(h/10)*u,n.width,u),!e.active())return;let t=!1;c.toBlob(e.guard(i=>{t||(t=!0,e.publish(i,{height:u,column:10,number:n.number,width:n.width,scale:n.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(i.src=n.url))}return function(t){return async i=>{const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),o=function(t,n){let i,r,o=!1;const a=new Set;function u(e){a.delete(e)&&URL.revokeObjectURL(e)}function d(){const e=i;i=void 0,e?.dispose()}return{start:function(){if(o)return;const d=i,s=[];let c=!1;const l={active:()=>!o&&!c&&i===l,own(t){if(c){const i=e([t]);i.failed&&n(i.failure)}else s.push(t)},dispose(){if(c)return;c=!0,i===l&&(i=void 0);const t=e(s.splice(0).reverse());t.failed&&n(t.failure)},fail(e){l.active()&&(l.dispose(),n(e))},guard:e=>(...t)=>{if(l.active())try{return e(...t)}catch(n){l.fail(n)}},publish(i,o){if(!l.active())return;if(!i)throw new Error("Auto-thumbnail encoding returned no Blob");const d=URL.createObjectURL(i);if(a.add(d),!l.active())return void u(d);const s=r;r=d;try{t({url:d,...o})}catch(c){r===d&&(r=a.has(s)?s:void 0);const t=e([()=>u(d)]);throw t.failed&&n(t.failure),c}u(s)}};return i=l,d?.dispose(),l},cancel:d,destroy:function(){if(o)return;o=!0;const t=e([d,...[...a].map(e=>()=>u(e))]);r=void 0,t.failed&&n(t.failure)},get closed(){return o}}}(e=>{i.thumbnails=e},r),a=[],u=()=>{const e=o.start();e&&e.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(t,()=>i.option.url);e.active()&&n(e,r)})()},d=()=>{const t=a.splice(0),n=e([o.destroy,...t]);n.failed&&r(n.failure)};try{for(const[e,t]of[["destroy",d],["restart",o.cancel],["video:loadedmetadata",u]]){if(o.closed)break;a.push(()=>i.off(e,t)),i.on(e,t),o.closed&&i.off(e,t)}}catch(s){throw d(),s}return{name:"artplayerPluginAutoThumbnail"}}}}); diff --git a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js index 18a135348..148680572 100644 --- a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js +++ b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable,r=(t,i,n)=>i in t?e(t,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[i]=n;function o(e,t){const i=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const i of["onloadedmetadata","onseeked","onerror"])e.own(()=>{t[i]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(i.onerror=e.guard(()=>{throw i.error||new Error("Auto-thumbnail media failed to load")}),i.onloadedmetadata=e.guard(()=>{i.onloadedmetadata=null;const n=i.duration,r=i.videoHeight,o=i.videoWidth,{height:a,canvasWidth:u,canvasHeight:d}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const i=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),r=Math.trunc(i*Math.ceil(e.number/10));if(![i,n,r].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:i,canvasWidth:n,canvasHeight:r}}(t,{duration:n,videoHeight:r,videoWidth:o});if(!e.active())return;i.width=o,i.height=r,i.style.width=`${o}px`,i.style.height=`${r}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=u,c.height=d;const l=c.getContext("2d");if(!l)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let s=0;const h=e.guard(()=>{if(s>=t.number)return void e.dispose();const r=n*s/t.number;let o=0;i.onseeked=e.guard(()=>{if(i.seeking)return;const n=i.currentTime;if(!e.active())return;if(!Number.isFinite(n)||Math.abs(n-r)>.05){if(++o>3)throw new Error("Auto-thumbnail seek did not reach the requested time");return void(i.currentTime=r)}if(i.onseeked=null,l.drawImage(i,s%10*t.width,Math.floor(s/10)*a,t.width,a),!e.active())return;let u=!1;c.toBlob(e.guard(i=>{u||(u=!0,e.publish(i,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(s+=1,h()))}),"image/jpeg")}),e.active()&&(i.currentTime=r)});h()}),e.active()&&(i.src=t.url))}function a(e){let t,i=!1;for(const r of e)try{r()}catch(n){i||(i=!0,t=n)}return{failed:i,failure:t}}function u(e,o){let u,d,c=!1;const l=new Set;function s(e){l.delete(e)&&URL.revokeObjectURL(e)}function h(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(c)return;const h=u,f=[];let m=!1;const v={active:()=>!c&&!m&&u===v,own(e){if(m){const t=a([e]);t.failed&&o(t.failure)}else f.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=a(f.splice(0).reverse());e.failed&&o(e.failure)},fail(e){v.active()&&(v.dispose(),o(e))},guard:e=>(...t)=>{if(v.active())try{return e(...t)}catch(i){v.fail(i)}},publish(u,c){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const h=URL.createObjectURL(u);if(l.add(h),!v.active())return void s(h);const f=d;d=h;try{e(((e,o)=>{for(var a in o||(o={}))i.call(o,a)&&r(e,a,o[a]);if(t)for(var a of t(o))n.call(o,a)&&r(e,a,o[a]);return e})({url:h},c))}catch(m){d===h&&(d=l.has(f)?f:void 0);const e=a([()=>s(h)]);throw e.failed&&o(e.failure),m}s(f)}};return u=v,null==h||h.dispose(),v},cancel:h,destroy:function(){if(c)return;c=!0;const e=a([h,...[...l].map(e=>()=>s(e))]);d=void 0,e.failed&&o(e.failure)},get closed(){return c}}}return function(e){return t=>{return i=null,n=null,r=function*(){const i=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),n=u(e=>{t.thumbnails=e},i),r=[],d=()=>{const i=n.start();i&&i.guard(()=>{const n=function(e,t){const i={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(i.width))||i.width<=0||!Number.isFinite(Number(i.number))||i.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return i}(e,()=>t.option.url);i.active()&&o(i,n)})()},c=()=>{const e=r.splice(0),t=a([n.destroy,...e]);t.failed&&i(t.failure)};try{for(const[e,i]of[["destroy",c],["restart",n.cancel],["video:loadedmetadata",d]]){if(n.closed)break;r.push(()=>t.off(e,i)),t.on(e,i),n.closed&&t.off(e,i)}}catch(l){throw c(),l}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(r.next(e))}catch(i){t(i)}},a=e=>{try{u(r.throw(e))}catch(i){t(i)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((r=r.apply(i,n)).next())});var i,n,r}}}); +!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAutoThumbnail=t(),define(function(){return e.artplayerPluginAutoThumbnail})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAutoThumbnail=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable,i=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;function o(e){let t,n=!1;for(const i of e)try{i()}catch(r){n||(n=!0,t=r)}return{failed:n,failure:t}}function a(e,a){let u,d,l=!1;const c=new Set;function s(e){c.delete(e)&&URL.revokeObjectURL(e)}function f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(l)return;const f=u,h=[];let m=!1;const b={active:()=>!l&&!m&&u===b,own(e){if(m){const t=o([e]);t.failed&&a(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===b&&(u=void 0);const e=o(h.splice(0).reverse());e.failed&&a(e.failure)},fail(e){b.active()&&(b.dispose(),a(e))},guard:e=>(...t)=>{if(b.active())try{return e(...t)}catch(n){b.fail(n)}},publish(u,l){if(!b.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(c.add(f),!b.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))n.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))r.call(o,a)&&i(e,a,o[a]);return e})({url:f},l))}catch(m){d===f&&(d=c.has(h)?h:void 0);const e=o([()=>s(f)]);throw e.failed&&a(e.failure),m}s(h)}};return u=b,null==f||f.dispose(),b},cancel:f,destroy:function(){if(l)return;l=!0;const e=o([f,...[...c].map(e=>()=>s(e))]);d=void 0,e.failed&&a(e.failure)},get closed(){return l}}}function u(e,t){const n="function"===typeof t.requestVideoFrameCallback&&"function"===typeof t.cancelVideoFrameCallback;let r;function i(){const e=r;if(r=void 0,t.onloadeddata=null,t.onseeked=null,!e)return;const n=o([()=>clearTimeout(e.timer),()=>{null!==e.frame&&t.cancelVideoFrameCallback(e.frame)}]);if(n.failed)throw n.failure}return e.own(i),(o,a)=>{if(i(),!e.active())return;const u={frame:null,timer:null,sought:!1,presented:!n,retries:0,sequence:0,started:!1};r=u;const d=()=>e.active()&&r===u,l=e.guard(()=>{d()&&u.sought&&u.presented&&(i(),e.active()&&a())}),c=()=>{u.sought=!1,(()=>{if(!n)return;const r=++u.sequence;u.presented=!1;const i=u.frame;if(u.frame=null,null!==i&&t.cancelVideoFrameCallback(i),!d())return;let o=!1;const a=t.requestVideoFrameCallback(e.guard(()=>{d()&&r===u.sequence&&!o&&(o=!0,u.frame=null,u.presented=!0,l())}));d()&&r===u.sequence&&!o?u.frame=a:t.cancelVideoFrameCallback(a)})(),d()&&(t.currentTime=o)},s=e.guard(()=>{d()&&!u.started&&(u.started=!0,t.onloadeddata=null,t.onseeked=e.guard(()=>{if(!d()||t.seeking)return;const e=t.currentTime;if(d())if(!Number.isFinite(e)||Math.abs(e-o)>.05){if(++u.retries>3)throw new Error("Auto-thumbnail seek did not reach the requested time");c()}else u.sought=!0,l()}),c())});if(u.timer=setTimeout(e.guard(()=>{if(d())throw new Error("Auto-thumbnail frame readiness timed out")}),3e4),!d())return void clearTimeout(u.timer);const f=!n||t.readyState>=2;d()&&(f?s():t.onloadeddata=s)}}function d(e,t){const n=function(e){const t=document.createElement("video");e.own(()=>t.remove()),e.own(()=>t.load()),e.own(()=>t.removeAttribute("src")),e.own(()=>t.pause());for(const n of["onloadedmetadata","onloadeddata","onseeked","onerror"])e.own(()=>{t[n]=null});return e.active()?(t.crossOrigin="anonymous",t.muted=!0,t.playsInline=!0,t.tabIndex=-1,t.setAttribute("aria-hidden","true"),t.style.cssText="position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none",document.documentElement.appendChild(t),e.active()||t.remove(),t):t}(e);e.active()&&(n.onerror=e.guard(()=>{throw n.error||new Error("Auto-thumbnail media failed to load")}),n.onloadedmetadata=e.guard(()=>{n.onloadedmetadata=null;const r=n.duration,i=n.videoHeight,o=n.videoWidth,{height:a,canvasWidth:d,canvasHeight:l}=function(e,t){if(!Number.isFinite(t.duration)||t.duration<=0||!Number.isFinite(t.videoWidth)||t.videoWidth<=0||!Number.isFinite(t.videoHeight)||t.videoHeight<=0)throw new TypeError("Auto-thumbnail requires finite media duration and dimensions");const n=Math.floor(e.width*t.videoHeight/t.videoWidth),r=Math.trunc(10*e.width),i=Math.trunc(n*Math.ceil(e.number/10));if(![n,r,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:n,canvasWidth:r,canvasHeight:i}}(t,{duration:r,videoHeight:i,videoWidth:o});if(!e.active())return;n.width=o,n.height=i,n.style.width=`${o}px`,n.style.height=`${i}px`;const c=document.createElement("canvas");if(!e.active())return;c.width=d,c.height=l;const s=c.getContext("2d");if(!s)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;const f=u(e,n);let h=0;const m=e.guard(()=>{h>=t.number?e.dispose():f(r*h/t.number,e.guard(()=>{if(s.drawImage(n,h%10*t.width,Math.floor(h/10)*a,t.width,a),!e.active())return;let r=!1;c.toBlob(e.guard(n=>{r||(r=!0,e.publish(n,{height:a,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(h+=1,m()))}),"image/jpeg")}))});m()}),e.active()&&(n.src=t.url))}return function(e){return t=>{return n=null,r=null,i=function*(){const n=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),r=a(e=>{t.thumbnails=e},n),i=[],u=()=>{const n=r.start();n&&n.guard(()=>{const r=function(e,t){const n={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(n.width))||n.width<=0||!Number.isFinite(Number(n.number))||n.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return n}(e,()=>t.option.url);n.active()&&d(n,r)})()},l=()=>{const e=i.splice(0),t=o([r.destroy,...e]);t.failed&&n(t.failure)};try{for(const[e,n]of[["destroy",l],["restart",r.cancel],["video:loadedmetadata",u]]){if(r.closed)break;i.push(()=>t.off(e,n)),t.on(e,n),r.closed&&t.off(e,n)}}catch(c){throw l(),c}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(n){t(n)}},a=e=>{try{u(i.throw(e))}catch(n){t(n)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(n,r)).next())});var n,r,i}}}); diff --git a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs index a3c006446..77f24658f 100644 --- a/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs +++ b/packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs @@ -4,127 +4,6 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -function readOptions(option, getFallbackUrl) { - const config = { - url: option.url || getFallbackUrl(), - width: option.width || 160, - number: option.number || 100, - scale: option.scale || 1 - }; - if (!Number.isFinite(Number(config.width)) || config.width <= 0 || !Number.isFinite(Number(config.number)) || config.number <= 0) - throw new TypeError("Auto-thumbnail width and number must be finite positive numbers"); - return config; -} -function sheetSize(config, video) { - if (!Number.isFinite(video.duration) || video.duration <= 0 || !Number.isFinite(video.videoWidth) || video.videoWidth <= 0 || !Number.isFinite(video.videoHeight) || video.videoHeight <= 0) - throw new TypeError("Auto-thumbnail requires finite media duration and dimensions"); - const height = Math.floor(config.width * video.videoHeight / video.videoWidth); - const canvasWidth = Math.trunc(config.width * 10); - const canvasHeight = Math.trunc(height * Math.ceil(config.number / 10)); - if (![height, canvasWidth, canvasHeight].every((value) => Number.isSafeInteger(value) && value > 0 && value <= 4294967295)) - throw new RangeError("Auto-thumbnail canvas dimensions are invalid"); - return { height, canvasWidth, canvasHeight }; -} -function createVideo(job) { - const video = document.createElement("video"); - job.own(() => video.remove()); - job.own(() => video.load()); - job.own(() => video.removeAttribute("src")); - job.own(() => video.pause()); - for (const property of ["onloadedmetadata", "onseeked", "onerror"]) - job.own(() => { - video[property] = null; - }); - if (!job.active()) - return video; - video.crossOrigin = "anonymous"; - video.muted = true; - video.playsInline = true; - video.tabIndex = -1; - video.setAttribute("aria-hidden", "true"); - video.style.cssText = "position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none"; - document.documentElement.appendChild(video); - if (!job.active()) - video.remove(); - return video; -} -function extract(job, config) { - const video = createVideo(job); - if (!job.active()) - return; - video.onerror = job.guard(() => { - throw video.error || new Error("Auto-thumbnail media failed to load"); - }); - video.onloadedmetadata = job.guard(() => { - video.onloadedmetadata = null; - const duration = video.duration; - const videoHeight = video.videoHeight; - const videoWidth = video.videoWidth; - const { height, canvasWidth, canvasHeight } = sheetSize(config, { - duration, - videoHeight, - videoWidth - }); - if (!job.active()) - return; - video.width = videoWidth; - video.height = videoHeight; - video.style.width = `${videoWidth}px`; - video.style.height = `${videoHeight}px`; - const canvas = document.createElement("canvas"); - if (!job.active()) - return; - canvas.width = canvasWidth; - canvas.height = canvasHeight; - const ctx = canvas.getContext("2d"); - if (!ctx) - throw new Error("Auto-thumbnail canvas context is unavailable"); - if (!job.active()) - return; - let index = 0; - const seek = job.guard(() => { - if (index >= config.number) { - job.dispose(); - return; - } - const target = duration * index / config.number; - let retries = 0; - video.onseeked = job.guard(() => { - if (video.seeking) - return; - const time = video.currentTime; - if (!job.active()) - return; - if (!Number.isFinite(time) || Math.abs(time - target) > 0.05) { - if (++retries > 3) - throw new Error("Auto-thumbnail seek did not reach the requested time"); - video.currentTime = target; - return; - } - video.onseeked = null; - ctx.drawImage(video, index % 10 * config.width, Math.floor(index / 10) * height, config.width, height); - if (!job.active()) - return; - let delivered = false; - canvas.toBlob(job.guard((blob) => { - if (delivered) - return; - delivered = true; - job.publish(blob, { height, column: 10, number: config.number, width: config.width, scale: config.scale }); - if (job.active()) { - index += 1; - seek(); - } - }), "image/jpeg"); - }); - if (job.active()) - video.currentTime = target; - }); - seek(); - }); - if (job.active()) - video.src = config.url; -} function cleanupAll(actions) { let failure; let failed = false; @@ -246,6 +125,217 @@ function createSession(publish, report) { } }; } +function createFrameReader(job, video) { + const presentedFrames = typeof video.requestVideoFrameCallback === "function" && typeof video.cancelVideoFrameCallback === "function"; + let pending; + function clear() { + const previous = pending; + pending = void 0; + video.onloadeddata = null; + video.onseeked = null; + if (!previous) + return; + const result = cleanupAll([ + () => clearTimeout(previous.timer), + () => { + if (previous.frame !== null) + video.cancelVideoFrameCallback(previous.frame); + } + ]); + if (result.failed) + throw result.failure; + } + job.own(clear); + return (target, draw) => { + clear(); + if (!job.active()) + return; + const current = { frame: null, timer: null, sought: false, presented: !presentedFrames, retries: 0, sequence: 0, started: false }; + pending = current; + const active = () => job.active() && pending === current; + const finish = job.guard(() => { + if (!active() || !current.sought || !current.presented) + return; + clear(); + if (job.active()) + draw(); + }); + const requestFrame = () => { + if (!presentedFrames) + return; + const sequence = ++current.sequence; + current.presented = false; + const previous = current.frame; + current.frame = null; + if (previous !== null) + video.cancelVideoFrameCallback(previous); + if (!active()) + return; + let delivered = false; + const id = video.requestVideoFrameCallback(job.guard(() => { + if (!active() || sequence !== current.sequence || delivered) + return; + delivered = true; + current.frame = null; + current.presented = true; + finish(); + })); + if (active() && sequence === current.sequence && !delivered) + current.frame = id; + else + video.cancelVideoFrameCallback(id); + }; + const seek = () => { + current.sought = false; + requestFrame(); + if (active()) + video.currentTime = target; + }; + const begin = job.guard(() => { + if (!active() || current.started) + return; + current.started = true; + video.onloadeddata = null; + video.onseeked = job.guard(() => { + if (!active() || video.seeking) + return; + const time = video.currentTime; + if (!active()) + return; + if (!Number.isFinite(time) || Math.abs(time - target) > 0.05) { + if (++current.retries > 3) + throw new Error("Auto-thumbnail seek did not reach the requested time"); + seek(); + return; + } + current.sought = true; + finish(); + }); + seek(); + }); + current.timer = setTimeout(job.guard(() => { + if (active()) + throw new Error("Auto-thumbnail frame readiness timed out"); + }), 3e4); + if (!active()) { + clearTimeout(current.timer); + return; + } + const ready = !presentedFrames || video.readyState >= 2; + if (!active()) + return; + if (!ready) + video.onloadeddata = begin; + else + begin(); + }; +} +function readOptions(option, getFallbackUrl) { + const config = { + url: option.url || getFallbackUrl(), + width: option.width || 160, + number: option.number || 100, + scale: option.scale || 1 + }; + if (!Number.isFinite(Number(config.width)) || config.width <= 0 || !Number.isFinite(Number(config.number)) || config.number <= 0) + throw new TypeError("Auto-thumbnail width and number must be finite positive numbers"); + return config; +} +function sheetSize(config, video) { + if (!Number.isFinite(video.duration) || video.duration <= 0 || !Number.isFinite(video.videoWidth) || video.videoWidth <= 0 || !Number.isFinite(video.videoHeight) || video.videoHeight <= 0) + throw new TypeError("Auto-thumbnail requires finite media duration and dimensions"); + const height = Math.floor(config.width * video.videoHeight / video.videoWidth); + const canvasWidth = Math.trunc(config.width * 10); + const canvasHeight = Math.trunc(height * Math.ceil(config.number / 10)); + if (![height, canvasWidth, canvasHeight].every((value) => Number.isSafeInteger(value) && value > 0 && value <= 4294967295)) + throw new RangeError("Auto-thumbnail canvas dimensions are invalid"); + return { height, canvasWidth, canvasHeight }; +} +function createVideo(job) { + const video = document.createElement("video"); + job.own(() => video.remove()); + job.own(() => video.load()); + job.own(() => video.removeAttribute("src")); + job.own(() => video.pause()); + for (const property of ["onloadedmetadata", "onloadeddata", "onseeked", "onerror"]) + job.own(() => { + video[property] = null; + }); + if (!job.active()) + return video; + video.crossOrigin = "anonymous"; + video.muted = true; + video.playsInline = true; + video.tabIndex = -1; + video.setAttribute("aria-hidden", "true"); + video.style.cssText = "position:fixed;left:0;top:0;visibility:hidden;pointer-events:none;display:block;width:auto;height:auto;max-width:none;max-height:none"; + document.documentElement.appendChild(video); + if (!job.active()) + video.remove(); + return video; +} +function extract(job, config) { + const video = createVideo(job); + if (!job.active()) + return; + video.onerror = job.guard(() => { + throw video.error || new Error("Auto-thumbnail media failed to load"); + }); + video.onloadedmetadata = job.guard(() => { + video.onloadedmetadata = null; + const duration = video.duration; + const videoHeight = video.videoHeight; + const videoWidth = video.videoWidth; + const { height, canvasWidth, canvasHeight } = sheetSize(config, { + duration, + videoHeight, + videoWidth + }); + if (!job.active()) + return; + video.width = videoWidth; + video.height = videoHeight; + video.style.width = `${videoWidth}px`; + video.style.height = `${videoHeight}px`; + const canvas = document.createElement("canvas"); + if (!job.active()) + return; + canvas.width = canvasWidth; + canvas.height = canvasHeight; + const ctx = canvas.getContext("2d"); + if (!ctx) + throw new Error("Auto-thumbnail canvas context is unavailable"); + if (!job.active()) + return; + const readFrame = createFrameReader(job, video); + let index = 0; + const seek = job.guard(() => { + if (index >= config.number) { + job.dispose(); + return; + } + readFrame(duration * index / config.number, job.guard(() => { + ctx.drawImage(video, index % 10 * config.width, Math.floor(index / 10) * height, config.width, height); + if (!job.active()) + return; + let delivered = false; + canvas.toBlob(job.guard((blob) => { + if (delivered) + return; + delivered = true; + job.publish(blob, { height, column: 10, number: config.number, width: config.width, scale: config.scale }); + if (job.active()) { + index += 1; + seek(); + } + }), "image/jpeg"); + })); + }); + seek(); + }); + if (job.active()) + video.src = config.url; +} function artplayerPluginAutoThumbnail(option) { return async (art) => { const report = (error) => console.warn("ArtPlayer auto-thumbnail failed:", error); diff --git a/packages/artplayer-plugin-auto-thumbnail/src/extraction.js b/packages/artplayer-plugin-auto-thumbnail/src/extraction.js index a2825b8c4..ee921dcb6 100644 --- a/packages/artplayer-plugin-auto-thumbnail/src/extraction.js +++ b/packages/artplayer-plugin-auto-thumbnail/src/extraction.js @@ -1,3 +1,4 @@ +import createFrameReader from './frames' import { sheetSize } from './options' import createVideo from './video' @@ -34,27 +35,14 @@ export default function extract(job, config) { throw new Error('Auto-thumbnail canvas context is unavailable') if (!job.active()) return + const readFrame = createFrameReader(job, video) let index = 0 const seek = job.guard(() => { if (index >= config.number) { job.dispose() return } - const target = duration * index / config.number - let retries = 0 - video.onseeked = job.guard(() => { - if (video.seeking) - return - const time = video.currentTime - if (!job.active()) - return - if (!Number.isFinite(time) || Math.abs(time - target) > 0.05) { - if (++retries > 3) - throw new Error('Auto-thumbnail seek did not reach the requested time') - video.currentTime = target - return - } - video.onseeked = null + readFrame(duration * index / config.number, job.guard(() => { ctx.drawImage(video, (index % 10) * config.width, Math.floor(index / 10) * height, config.width, height) if (!job.active()) return @@ -69,9 +57,7 @@ export default function extract(job, config) { seek() } }), 'image/jpeg') - }) - if (job.active()) - video.currentTime = target + })) }) seek() }) diff --git a/packages/artplayer-plugin-auto-thumbnail/src/frames.js b/packages/artplayer-plugin-auto-thumbnail/src/frames.js new file mode 100644 index 000000000..65990816a --- /dev/null +++ b/packages/artplayer-plugin-auto-thumbnail/src/frames.js @@ -0,0 +1,110 @@ +import { cleanupAll } from './session' + +export default function createFrameReader(job, video) { + const presentedFrames = typeof video.requestVideoFrameCallback === 'function' + && typeof video.cancelVideoFrameCallback === 'function' + let pending + + function clear() { + const previous = pending + pending = undefined + video.onloadeddata = null + video.onseeked = null + if (!previous) + return + const result = cleanupAll([ + () => clearTimeout(previous.timer), + () => { + if (previous.frame !== null) + video.cancelVideoFrameCallback(previous.frame) + }, + ]) + if (result.failed) + throw result.failure + } + job.own(clear) + + return (target, draw) => { + clear() + if (!job.active()) + return + const current = { frame: null, timer: null, sought: false, presented: !presentedFrames, retries: 0, sequence: 0, started: false } + pending = current + const active = () => job.active() && pending === current + const finish = job.guard(() => { + if (!active() || !current.sought || !current.presented) + return + clear() + if (job.active()) + draw() + }) + const requestFrame = () => { + if (!presentedFrames) + return + const sequence = ++current.sequence + current.presented = false + const previous = current.frame + current.frame = null + if (previous !== null) + video.cancelVideoFrameCallback(previous) + if (!active()) + return + let delivered = false + const id = video.requestVideoFrameCallback(job.guard(() => { + if (!active() || sequence !== current.sequence || delivered) + return + delivered = true + current.frame = null + current.presented = true + finish() + })) + if (active() && sequence === current.sequence && !delivered) + current.frame = id + else + video.cancelVideoFrameCallback(id) + } + const seek = () => { + current.sought = false + requestFrame() + if (active()) + video.currentTime = target + } + const begin = job.guard(() => { + if (!active() || current.started) + return + current.started = true + video.onloadeddata = null + video.onseeked = job.guard(() => { + if (!active() || video.seeking) + return + const time = video.currentTime + if (!active()) + return + if (!Number.isFinite(time) || Math.abs(time - target) > 0.05) { + if (++current.retries > 3) + throw new Error('Auto-thumbnail seek did not reach the requested time') + seek() + return + } + current.sought = true + finish() + }) + seek() + }) + current.timer = setTimeout(job.guard(() => { + if (active()) + throw new Error('Auto-thumbnail frame readiness timed out') + }), 30000) + if (!active()) { + clearTimeout(current.timer) + return + } + const ready = !presentedFrames || video.readyState >= 2 + if (!active()) + return + if (!ready) + video.onloadeddata = begin + else + begin() + } +} diff --git a/packages/artplayer-plugin-auto-thumbnail/src/video.js b/packages/artplayer-plugin-auto-thumbnail/src/video.js index b9498fc4c..33f445359 100644 --- a/packages/artplayer-plugin-auto-thumbnail/src/video.js +++ b/packages/artplayer-plugin-auto-thumbnail/src/video.js @@ -4,7 +4,7 @@ export default function createVideo(job) { job.own(() => video.load()) job.own(() => video.removeAttribute('src')) job.own(() => video.pause()) - for (const property of ['onloadedmetadata', 'onseeked', 'onerror']) + for (const property of ['onloadedmetadata', 'onloadeddata', 'onseeked', 'onerror']) job.own(() => { video[property] = null }) if (!job.active()) return video diff --git a/refactor/baselines/auto-thumbnail-frame-presentation.json b/refactor/baselines/auto-thumbnail-frame-presentation.json new file mode 100644 index 000000000..520661984 --- /dev/null +++ b/refactor/baselines/auto-thumbnail-frame-presentation.json @@ -0,0 +1,9759 @@ +{ + "task": "PKG-AUTO-THUMB-03", + "status": "doing", + "date": "2026-09-12T23:46:36.426Z", + "previousCommit": "a128706fe78e01c5582eec54c1e3419ff8ffc781", + "scope": "Presentation-aware extraction on native callback-capable browsers, with constant job cleanup ownership and per-sample 30-second data/seek/presentation deadlines. Callback-less fallback first pixels remain unresolved.", + "unit": [ + { + "variant": "before", + "tests": 19, + "pass": 2, + "fail": 17, + "log": "refactor/.cache/auto-thumbnail-frames-final-unit-before.log", + "sha256": "868ade926266bbc2a099ed10b3c9478933ee9063c2e7d1e3c4e6964758d6e867", + "scope": "New frame-specific tests only; fallback behavior and the synchronous-presentation observable outcome also pass on the previous checkpoint." + }, + { + "variant": "source", + "tests": 52, + "pass": 52, + "fail": 0, + "log": "refactor/.cache/auto-thumbnail-frames-final-unit-source.log", + "sha256": "17450249d7a4af988240e07a09b797c7ad051641256f2a38152f8746e0424ced", + "scope": "19 frame-specific plus 33 existing lifecycle cases." + }, + { + "variant": "main", + "tests": 52, + "pass": 52, + "fail": 0, + "log": "refactor/.cache/auto-thumbnail-frames-final-unit-main.log", + "sha256": "9cdda48b260e3de63bd326ea7328c5674e297e5863470d91b47a42cbb99315f8", + "scope": "19 frame-specific plus 33 existing lifecycle cases." + }, + { + "variant": "legacy", + "tests": 52, + "pass": 52, + "fail": 0, + "log": "refactor/.cache/auto-thumbnail-frames-final-unit-legacy.log", + "sha256": "7c5de18110bf1d32dad1067c8713ddb47a74514e6ab23a301d40b1201fb20a70", + "scope": "19 frame-specific plus 33 existing lifecycle cases." + } + ], + "native": [ + { + "variant": "source", + "tests": 42, + "retries": 0, + "skipped": 0, + "report": "refactor/.cache/auto-thumbnail-frames-final-browser-source.json", + "sha256": "538ec20c3eb6cb314ed1857b1a778ac1a7558c3c508af772522e3ce43b8a1a44", + "results": "refactor/.cache/auto-thumbnail-frames-final-browser-source-results", + "measurements": [ + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + }, + { + "type": "image/jpeg", + "bytes": 4226 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + }, + { + "type": "image/jpeg", + "bytes": 4226 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + }, + { + "type": "image/jpeg", + "bytes": 6752 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + }, + { + "type": "image/jpeg", + "bytes": 6752 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + }, + { + "type": "image/jpeg", + "bytes": 4215 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + }, + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 126, + 0, + 126, + 255 + ], + [ + 254, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 1, + 252, + 255 + ], + [ + 255, + 255, + 1, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 127, + 0, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 254, + 0, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 255, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 15, + 13, + 138, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 14, + 15, + 141, + 255 + ], + [ + 213, + 61, + 47, + 255 + ] + ], + "spatial": [ + [ + 244, + 1, + 0, + 255 + ], + [ + 5, + 251, + 2, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 255, + 0, + 253, + 255 + ], + [ + 2, + 255, + 253, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 253, + 52, + 0, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.583333, + "currentTime": 1.6 + }, + { + "mediaTime": 3.166667, + "currentTime": 3.2 + }, + { + "mediaTime": 4.791667, + "currentTime": 4.8 + }, + { + "mediaTime": 6.375, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 126, + 0, + 126, + 255 + ], + [ + 254, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 1, + 252, + 255 + ], + [ + 255, + 255, + 1, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 127, + 0, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 254, + 0, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 255, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 15, + 13, + 138, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 14, + 15, + 141, + 255 + ], + [ + 213, + 61, + 47, + 255 + ] + ], + "spatial": [ + [ + 244, + 1, + 0, + 255 + ], + [ + 5, + 251, + 2, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 255, + 0, + 253, + 255 + ], + [ + 2, + 255, + 253, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 253, + 52, + 0, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.583333, + "currentTime": 1.6 + }, + { + "mediaTime": 3.166667, + "currentTime": 3.2 + }, + { + "mediaTime": 4.791667, + "currentTime": 4.8 + }, + { + "mediaTime": 6.375, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 136, + 19, + 126, + 255 + ], + [ + 254, + 24, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 254, + 241, + 2, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 137, + 19, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 24, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 241, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 5, + 15, + 250, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 247, + 64, + 0, + 255 + ] + ], + "spatial": [ + [ + 255, + 22, + 9, + 255 + ], + [ + 0, + 216, + 3, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 255, + 36, + 254, + 255 + ], + [ + 1, + 229, + 254, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 69, + 1, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 136, + 19, + 126, + 255 + ], + [ + 254, + 24, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 254, + 241, + 2, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 137, + 19, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 24, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 241, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 5, + 15, + 250, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 247, + 64, + 0, + 255 + ] + ], + "spatial": [ + [ + 255, + 22, + 9, + 255 + ], + [ + 0, + 216, + 3, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 255, + 36, + 254, + 255 + ], + [ + 1, + 229, + 254, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 69, + 1, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 238, + 14, + 14, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 15, + 239, + 255 + ], + [ + 235, + 236, + 12, + 255 + ] + ], + "spatial": [ + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6015634, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 238, + 14, + 14, + 255 + ] + }, + { + "time": 3.2026369, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 16, + 16, + 255 + ] + }, + { + "time": 4.8020478, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4019029, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 235, + 236, + 12, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 217, + 65, + 51, + 255 + ] + ], + "spatial": [ + [ + 238, + 14, + 15, + 255 + ], + [ + 14, + 238, + 14, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 238, + 13, + 237, + 255 + ], + [ + 14, + 237, + 237, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6006026, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 3.2025446, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 4.8009735, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4020927, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 239, + 62, + 14, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 238, + 14, + 14, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 15, + 239, + 255 + ], + [ + 235, + 236, + 12, + 255 + ] + ], + "spatial": [ + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6025738, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 238, + 14, + 14, + 255 + ] + }, + { + "time": 3.2018507, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 16, + 16, + 255 + ] + }, + { + "time": 4.8007837, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4020943, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 235, + 236, + 12, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "c8dddade6f2fd6e364978ac4b6fabd7276ccf78184b45fcd1a9556493af4c115", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 201, + 71, + 57, + 255 + ] + ], + "spatial": [ + [ + 238, + 14, + 14, + 255 + ], + [ + 14, + 238, + 14, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 239, + 13, + 237, + 255 + ], + [ + 14, + 237, + 237, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6005796, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 3.2004693, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 4.8021193, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4006919, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 238, + 61, + 10, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + } + ] + }, + { + "variant": "main", + "tests": 21, + "retries": 0, + "skipped": 0, + "report": "refactor/.cache/auto-thumbnail-frames-final-browser-main.json", + "sha256": "cea172aae2ee8954936c80a5e26266f2fdf399d768100ccb9d3fd2624d1b913a", + "results": "refactor/.cache/auto-thumbnail-frames-final-browser-main-results", + "measurements": [ + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + }, + { + "type": "image/jpeg", + "bytes": 4226 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + }, + { + "type": "image/jpeg", + "bytes": 6752 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + }, + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 126, + 0, + 126, + 255 + ], + [ + 254, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 1, + 252, + 255 + ], + [ + 255, + 255, + 1, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 127, + 0, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 254, + 0, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 255, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 15, + 13, + 138, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 14, + 15, + 141, + 255 + ], + [ + 213, + 61, + 47, + 255 + ] + ], + "spatial": [ + [ + 244, + 1, + 0, + 255 + ], + [ + 5, + 251, + 2, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 255, + 0, + 253, + 255 + ], + [ + 2, + 255, + 253, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 253, + 52, + 0, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.583333, + "currentTime": 1.6 + }, + { + "mediaTime": 3.166667, + "currentTime": 3.2 + }, + { + "mediaTime": 4.791667, + "currentTime": 4.8 + }, + { + "mediaTime": 6.375, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 136, + 19, + 126, + 255 + ], + [ + 254, + 24, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 254, + 241, + 2, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 137, + 19, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 24, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 241, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 5, + 15, + 250, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 247, + 64, + 0, + 255 + ] + ], + "spatial": [ + [ + 255, + 22, + 9, + 255 + ], + [ + 0, + 216, + 3, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 255, + 36, + 254, + 255 + ], + [ + 1, + 229, + 254, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 69, + 1, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 238, + 14, + 14, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 15, + 239, + 255 + ], + [ + 235, + 236, + 12, + 255 + ] + ], + "spatial": [ + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6006083, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 238, + 14, + 14, + 255 + ] + }, + { + "time": 3.2014815, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 16, + 16, + 255 + ] + }, + { + "time": 4.8011166, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4021892, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 235, + 236, + 12, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 217, + 65, + 51, + 255 + ] + ], + "spatial": [ + [ + 238, + 14, + 15, + 255 + ], + [ + 14, + 238, + 14, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 238, + 13, + 237, + 255 + ], + [ + 14, + 237, + 237, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6006312, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 3.2016275, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 4.8011736, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4039991, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 239, + 62, + 14, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + } + ] + }, + { + "variant": "legacy", + "tests": 21, + "retries": 0, + "skipped": 0, + "report": "refactor/.cache/auto-thumbnail-frames-final-browser-legacy.json", + "sha256": "afc4af73ed9f94748c167f99a582289ad628e8f8b0fec39fed5b8d5a6250a5f4", + "results": "refactor/.cache/auto-thumbnail-frames-final-browser-legacy-results", + "measurements": [ + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 2635 + }, + { + "type": "image/jpeg", + "bytes": 4226 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 4076 + }, + { + "type": "image/jpeg", + "bytes": 6752 + } + ], + "heldFrames": 0, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [], + "heldFrames": 1, + "frameSupport": true, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native destroy owns decoder and encoded URLs", + "scenario": "destroy", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native restart owns decoder and encoded URLs", + "scenario": "restart", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native complete owns decoder and encoded URLs", + "scenario": "complete", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 1, + "updates": 2 + }, + "after": { + "urls": 1, + "updates": 2 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 2 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + }, + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-destroy owns decoder and encoded URLs", + "scenario": "frame-destroy", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate native frame-restart owns decoder and encoded URLs", + "scenario": "frame-restart", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.", + "before": { + "urls": 0, + "updates": 0 + }, + "after": { + "urls": 0, + "updates": 0 + }, + "video": { + "connected": false, + "src": null, + "readyState": 0, + "paused": true, + "handlers": [ + true, + true, + true, + true + ] + }, + "final": { + "urls": 0, + "listeners": 0, + "updates": 0 + }, + "blobs": [ + { + "type": "image/jpeg", + "bytes": 983 + } + ], + "heldFrames": 0, + "frameSupport": false, + "result": { + "name": "artplayerPluginAutoThumbnail" + } + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 126, + 0, + 126, + 255 + ], + [ + 254, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 1, + 252, + 255 + ], + [ + 255, + 255, + 1, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 127, + 0, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 254, + 0, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 255, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 15, + 13, + 138, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 14, + 15, + 141, + 255 + ], + [ + 213, + 61, + 47, + 255 + ] + ], + "spatial": [ + [ + 244, + 1, + 0, + 255 + ], + [ + 5, + 251, + 2, + 255 + ], + [ + 15, + 15, + 139, + 255 + ], + [ + 255, + 0, + 253, + 255 + ], + [ + 2, + 255, + 253, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 253, + 52, + 0, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.583333, + "currentTime": 1.6 + }, + { + "mediaTime": 3.166667, + "currentTime": 3.2 + }, + { + "mediaTime": 4.791667, + "currentTime": 4.8 + }, + { + "mediaTime": 6.375, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 136, + 19, + 126, + 255 + ], + [ + 254, + 24, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 254, + 241, + 2, + 255 + ] + ], + "spatial": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ], + [ + 0, + 0, + 0, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 137, + 19, + 126, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 24, + 0, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 241, + 2, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "firefox", + "version": "155.0", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 5, + 15, + 250, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 247, + 64, + 0, + 255 + ] + ], + "spatial": [ + [ + 255, + 22, + 9, + 255 + ], + [ + 0, + 216, + 3, + 255 + ], + [ + 1, + 14, + 253, + 255 + ], + [ + 255, + 36, + 254, + 255 + ], + [ + 1, + 229, + 254, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 1.6, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 3.2, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 4.8, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 14, + 253, + 255 + ] + }, + { + "time": 6.4, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 255, + 69, + 1, + 255 + ] + } + ], + "presented": [ + { + "mediaTime": 0, + "currentTime": 0 + }, + { + "mediaTime": 1.6, + "currentTime": 1.6 + }, + { + "mediaTime": 3.2, + "currentTime": 3.2 + }, + { + "mediaTime": 4.8, + "currentTime": 4.8 + }, + { + "mediaTime": 6.4, + "currentTime": 6.4 + } + ], + "supportsPresentation": true, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented auto-thumbnail-timeline pixels and records fallback limits", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "media": "auto-thumbnail-timeline", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 238, + 14, + 14, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 15, + 239, + 255 + ], + [ + 235, + 236, + 12, + 255 + ] + ], + "spatial": [ + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ], + [ + 16, + 16, + 16, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6014328, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 238, + 14, + 14, + 255 + ] + }, + { + "time": 3.2026031, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 16, + 16, + 255 + ] + }, + { + "time": 4.8023124, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4013339, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 235, + 236, + 12, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + }, + { + "browser": "webkit", + "version": "26.6", + "title": "Auto-thumbnail candidate verifies presented pattern pixels and records fallback limits", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "media": "pattern", + "scope": "All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.", + "cells": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 217, + 65, + 51, + 255 + ] + ], + "spatial": [ + [ + 238, + 14, + 14, + 255 + ], + [ + 14, + 238, + 14, + 255 + ], + [ + 28, + 28, + 140, + 255 + ], + [ + 239, + 13, + 237, + 255 + ], + [ + 14, + 237, + 237, + 255 + ] + ], + "draws": [ + { + "time": 0, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 0, + 0, + 0, + 0 + ] + }, + { + "time": 1.6005294, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 3.2005638, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 4.8009563, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 16, + 15, + 239, + 255 + ] + }, + { + "time": 6.4060931, + "duration": 8, + "connected": true, + "visibility": "hidden", + "display": "block", + "width": 320, + "height": 180, + "raw": [ + 239, + 62, + 14, + 255 + ] + } + ], + "presented": [], + "supportsPresentation": false, + "final": [ + { + "connected": false, + "src": null, + "paused": true + } + ], + "playing": 0, + "warnings": [] + } + ] + } + ], + "fullCI": { + "status": "passed", + "command": "yarn ci:check", + "unit": 1572, + "engineering": 14, + "baseline": 232, + "total": 1818, + "strictProductionTypeScriptFiles": 339, + "log": "refactor/.cache/auto-thumbnail-frames-final-ci.log", + "sha256": "ba4284694431c836242ef10e5057f39ba2f91cadd6a36aa4918adb4f374ea66b", + "scope": "Auto-thumbnail remains JavaScript for this task03 checkpoint." + }, + "limits": [ + "Windows WebKit 26.6 has no native frame callback; cells 0-1 remain diagnostic while cells 2-4 are asserted. AUTO-THUMB-PIXEL-01 stays open.", + "Native samples and held callbacks use a stub player host, not full core integration or a physical Safari result.", + "The 30-second deadline covers a pending data/seek/presentation sample only; encoder/network/background/aggregate resource policy remains incomplete.", + "Auto-thumbnail source is still JavaScript. Strict TypeScript and historical declarations remain task04." + ], + "previousNative": { + "report": "refactor/.cache/auto-thumbnail-frames-browser-before.json", + "sha256": "df8708056bdf1e64768ede89aadcb7a86d55b34e4623ee4921fbfa8c08b13be9", + "results": "refactor/.cache/auto-thumbnail-frames-browser-before-results", + "passed": 2, + "failed": 4, + "scope": "Previous checkpoint cannot satisfy native presentation assertions on Chromium/Firefox. Firefox also retains an incorrect early pixel. Callback-less WebKit retains the same explicitly partial acceptance; its two passing cases do not close its initial-frame defect." + }, + "sameFrameExperiment": { + "file": "refactor/.cache/auto-thumbnail-same-frame-probe.json", + "sha256": "c96b942f9c09e1ab22053f79f753b27d1513a49509b72da8ecbbc8a7aff8eee1", + "results": [ + { + "browser": "chromium", + "version": "153.0.8010.12", + "mode": "attached-hidden-callback", + "initial": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frame": 0.033333, + "beforeSeek": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.005, + "immediate": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.01, + "immediate": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.02, + "immediate": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.03, + "immediate": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 1 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 1 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.005, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.005, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.01, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.01, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.02, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.02, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.03, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.03, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.04, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.04, + "readyState": 4 + } + ] + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "mode": "attached-callback", + "initial": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frame": 0.033333, + "beforeSeek": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.005, + "immediate": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.01, + "immediate": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.02, + "immediate": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.03, + "immediate": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 1 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 1 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.005, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.005, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.01, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.01, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.02, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.02, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.03, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.03, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.04, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.04, + "readyState": 4 + } + ] + }, + { + "browser": "firefox", + "version": "155.0", + "mode": "attached-hidden-callback", + "initial": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "frame": 0.04, + "beforeSeek": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.005, + "immediate": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.01, + "immediate": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.02, + "immediate": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.03, + "immediate": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 255, + 24, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 255, + 24, + 0, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 1 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.005, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.005, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.01, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.01, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.02, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.02, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.03, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.03, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.04, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.04, + "readyState": 4 + } + ] + }, + { + "browser": "firefox", + "version": "155.0", + "mode": "attached-callback", + "initial": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "frame": 0.04, + "beforeSeek": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.005, + "immediate": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.005, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.01, + "immediate": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.02, + "immediate": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.02, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.03, + "immediate": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.03, + "readyState": 4, + "rgba": [ + 137, + 19, + 126, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 255, + 24, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 255, + 24, + 0, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 1 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.005, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.005, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.01, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.01, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.02, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.02, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.03, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.03, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.04, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.04, + "readyState": 4 + } + ] + } + ], + "scope": "Independent callback API experiment, not a candidate/default-count acceptance: hidden and ordinary Chromium/Firefox elements delivered callbacks for targets 0, .005, .01, .02, .03, .04. First five samples retain unique purple frame; .04 is red. No WebKit claim." + }, + "actualEntries": [ + { + "name": "main-require", + "entry": "./dist/artplayer-plugin-auto-thumbnail.js", + "result": "pass", + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "scope": "Actual built entry and registration without DOM access; not an installed tarball test." + }, + { + "name": "legacy-require", + "entry": "./dist/artplayer-plugin-auto-thumbnail.legacy.js", + "result": "pass", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual built entry and registration without DOM access; not an installed tarball test." + }, + { + "name": "legacy-import", + "entry": "./dist/artplayer-plugin-auto-thumbnail.legacy.js", + "result": "pass", + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "scope": "Actual built entry and registration without DOM access; not an installed tarball test." + }, + { + "name": "module-import", + "entry": "./dist/artplayer-plugin-auto-thumbnail.mjs", + "result": "pass", + "sha256": "6b3ed3bd30de684e55adbe361a1e6010c5de75e8bd27936466b61c6df4a08a05", + "scope": "Actual built entry and registration without DOM access; not an installed tarball test." + } + ], + "artifacts": [ + { + "file": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js", + "copy": "docs/compiled/artplayer-plugin-auto-thumbnail.js", + "bytes": 5991, + "sha256": "7ab0cc25a5832f3978860a280d224023e96cbf2a31824199de272c10fae3af57", + "copiedBytesEqual": true + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js", + "copy": "docs/compiled/artplayer-plugin-auto-thumbnail.legacy.js", + "bytes": 6571, + "sha256": "53efce3aa58773dc8afa03fc1de9f3829f2837b30e9a9438c4208a2f83f54657", + "copiedBytesEqual": true + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs", + "copy": "docs/compiled/artplayer-plugin-auto-thumbnail.mjs", + "bytes": 11367, + "sha256": "6b3ed3bd30de684e55adbe361a1e6010c5de75e8bd27936466b61c6df4a08a05", + "copiedBytesEqual": true + } + ], + "inputs": [ + { + "file": "package.json", + "sha256LF": "2dc53f03b5fe878f2930a20ed891d4ddafdd2cee13f2a536b2833a0849a23abf" + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md", + "sha256LF": "f5a56d8092489f36b0f024b92cbfa25c71bbb530975fc43128d05e06d30d4ec2" + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/src/index.js", + "sha256LF": "6ecc757ac74d961fb0bda13d58582f0845e29b62f10e351a267b67adb7b684b8" + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/src/options.js", + "sha256LF": "11c88fe9682d0736d28a128b352f34832a8ed2a7a82fb6e92e44fc85b7458056" + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/src/session.js", + "sha256LF": "14b69480a829d15c9a74995fc398296ad10983bd3a177aa1ddc0205273afa6ec" + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/src/extraction.js", + "sha256LF": "43ed1d8d4a855de7bab0f04c838eefd5b23ca1cf3d07c0e3c13f0b32364f79c5" + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/src/video.js", + "sha256LF": "21f446ab9570e0ef4a75a4d76db5605559b546b36eddb4128702bc77bdaf4193" + }, + { + "file": "packages/artplayer-plugin-auto-thumbnail/src/frames.js", + "sha256LF": "06565801ccb616211d6fba1858fc12df178bd503ff78128d49d41eca8a4fae3f" + }, + { + "file": "test/helpers/auto-thumbnail.js", + "sha256LF": "1a5a3d468fd382ef694627f9263b5c786b3016f8f690ed67691727c8802f1071" + }, + { + "file": "test/auto-thumbnail-frames.test.js", + "sha256LF": "5d4c28a09b28b9175460ec9f7bd4ea0e3b33a3cc9c6939a81f517103ae1ca987" + }, + { + "file": "test/auto-thumbnail-lifecycle.test.js", + "sha256LF": "f184ff44d0e3d679463407e4e52a32e4c67308625876eb1bc7f96cb2e4fabc02" + }, + { + "file": "test/browser/auto-thumbnail-lifecycle.spec.js", + "sha256LF": "e526c4db0611c777ae9af111070fd742e9f0cefa2bbafe88257315945830d14c" + }, + { + "file": "test/browser/auto-thumbnail-pixels.spec.js", + "sha256LF": "71d14ed9b1e6372720df68b137697703e5ed7160504a91df8bfc732343465860" + } + ], + "caughtBeforeCommit": { + "issue": "A readyState getter destroying the host could restore onloadeddata after cleanup.", + "correction": "Recheck sample identity after reading readiness.", + "preFixTests": 19, + "preFixPassed": 18, + "preFixFailed": 1, + "log": "refactor/.cache/auto-thumbnail-frames-ready-before.log", + "initialPassingFullCI": "refactor/.cache/auto-thumbnail-frames-ci.log", + "initialPassingTests": 1817, + "initialCheckpoint": "refactor/.cache/auto-thumbnail-frames-initial-checkpoint.json" + }, + "followUpExperiment": { + "file": "refactor/.cache/auto-thumbnail-webkit-copy-probe.json", + "sha256": "1a22f5743e0457857fe9529b79328e81b185a0e52668d8ea34bc6e719aa5a09e", + "results": [ + { + "browser": "webkit", + "version": "26.6", + "mode": "attached-hidden-bitmap", + "initial": { + "time": 0, + "readyState": 2, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "frame": null, + "beforeSeek": { + "time": 0, + "readyState": 2, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0.0015394, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 0.0015394, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 0.005, + "immediate": { + "time": 0.0063651, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 0.0063651, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.0416231, + "readyState": 4, + "rgba": [ + 127, + 14, + 127, + 255 + ] + }, + "afterPaint": { + "time": 0.0416231, + "readyState": 4, + "rgba": [ + 127, + 14, + 127, + 255 + ] + } + }, + { + "target": 1.6, + "immediate": { + "time": 1.6004398, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 1.6004398, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 3.2, + "immediate": { + "time": 3.2016417, + "readyState": 4, + "rgba": [ + 16, + 16, + 16, + 255 + ] + }, + "afterPaint": { + "time": 3.2016417, + "readyState": 4, + "rgba": [ + 16, + 16, + 16, + 255 + ] + } + }, + { + "target": 4.8, + "immediate": { + "time": 4.8023997, + "readyState": 4, + "rgba": [ + 16, + 15, + 239, + 255 + ] + }, + "afterPaint": { + "time": 4.8023997, + "readyState": 4, + "rgba": [ + 16, + 15, + 239, + 255 + ] + } + }, + { + "target": 6.4, + "immediate": { + "time": 6.4007271, + "readyState": 3, + "rgba": [ + 235, + 236, + 12, + 255 + ] + }, + "afterPaint": { + "time": 6.4007271, + "readyState": 3, + "rgba": [ + 235, + 236, + 12, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 2 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 2 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0015394, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.0063651, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0063651, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0416231, + "readyState": 4 + }, + { + "event": "canplay", + "time": 1.6004398, + "readyState": 4 + }, + { + "event": "seeked", + "time": 1.6004398, + "readyState": 4 + }, + { + "event": "canplay", + "time": 3.2016417, + "readyState": 4 + }, + { + "event": "seeked", + "time": 3.2016417, + "readyState": 4 + }, + { + "event": "seeked", + "time": 4.8023997, + "readyState": 4 + }, + { + "event": "seeked", + "time": 6.4007271, + "readyState": 3 + } + ] + }, + { + "browser": "webkit", + "version": "26.6", + "mode": "attached-hidden-videoframe", + "error": "Can't find variable: VideoFrame", + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 4 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + } + ] + }, + { + "browser": "webkit", + "version": "26.6", + "mode": "attached-hidden-plain", + "initial": { + "time": 0, + "readyState": 2, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "frame": null, + "beforeSeek": { + "time": 0, + "readyState": 2, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 0.005, + "immediate": { + "time": 0.0071363, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 0.0071363, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.0422188, + "readyState": 4, + "rgba": [ + 127, + 14, + 127, + 255 + ] + }, + "afterPaint": { + "time": 0.0422188, + "readyState": 4, + "rgba": [ + 127, + 14, + 127, + 255 + ] + } + }, + { + "target": 1.6, + "immediate": { + "time": 1.6022376, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 1.6022376, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 3.2, + "immediate": { + "time": 3.2019014, + "readyState": 4, + "rgba": [ + 16, + 16, + 16, + 255 + ] + }, + "afterPaint": { + "time": 3.2019014, + "readyState": 4, + "rgba": [ + 16, + 16, + 16, + 255 + ] + } + }, + { + "target": 4.8, + "immediate": { + "time": 4.8016506, + "readyState": 4, + "rgba": [ + 16, + 15, + 239, + 255 + ] + }, + "afterPaint": { + "time": 4.8016506, + "readyState": 4, + "rgba": [ + 16, + 15, + 239, + 255 + ] + } + }, + { + "target": 6.4, + "immediate": { + "time": 6.4018336, + "readyState": 3, + "rgba": [ + 235, + 236, + 12, + 255 + ] + }, + "afterPaint": { + "time": 6.4018336, + "readyState": 3, + "rgba": [ + 235, + 236, + 12, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 2 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 2 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0071363, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0422188, + "readyState": 4 + }, + { + "event": "seeked", + "time": 1.6022376, + "readyState": 4 + }, + { + "event": "seeked", + "time": 3.2019014, + "readyState": 4 + }, + { + "event": "seeked", + "time": 4.8016506, + "readyState": 4 + }, + { + "event": "seeked", + "time": 6.4018336, + "readyState": 3 + } + ] + } + ], + "scope": "Independent Windows WebKit probe only: createImageBitmap(video) also gave the wrong initial color, like direct drawImage; VideoFrame was unavailable. No production fallback added and no native capability waiver." + }, + "validatedAt": "2026-09-12T23:52:40.657Z", + "noBFrameExperiment": { + "scope": "Independent native media probe, not candidate acceptance. Reencoding the unique-first-frame clip without B frames did not repair Windows WebKit initial pixels; Chromium still reads the purple first frame. Do not apply a universal .04-second offset or waive the fallback risk.", + "media": { + "file": "refactor/.cache/auto-thumbnail-first-frame-no-b.mp4", + "sha256": "a9ee6b8f22af1d9dc9d09d25197148281056c680cbbdae3c33b046bec23c4dff", + "command": "ffmpeg -i auto-thumbnail-timeline.mp4 -c:v libx264 -bf 0 -g 30 -pix_fmt yuv420p -movflags +faststart " + }, + "runs": [ + { + "file": "refactor/.cache/auto-thumbnail-no-b-frame-probe.json", + "sha256": "26e7648b606284b43fca97185d78b9c12a5c654ee139e4b0473ed4006acb2d3e", + "results": [ + { + "browser": "webkit", + "version": "26.6", + "mode": "attached-hidden-plain", + "initial": { + "time": 0, + "readyState": 2, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "frame": null, + "beforeSeek": { + "time": 0, + "readyState": 2, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 0 + ] + } + }, + { + "target": 0, + "immediate": { + "time": 0.0022923, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "afterPaint": { + "time": 0.0022923, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 0 + ] + } + }, + { + "target": 0.01, + "immediate": { + "time": 0.0146568, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 0.0146568, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.042291, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 0.042291, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 0.1, + "immediate": { + "time": 0.1014159, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 0.1014159, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 0.5, + "immediate": { + "time": 0.5011826, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 0.5011826, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 1, + "immediate": { + "time": 1.0005246, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "afterPaint": { + "time": 1.0005246, + "readyState": 4, + "rgba": [ + 238, + 14, + 14, + 255 + ] + } + }, + { + "target": 2.5, + "immediate": { + "time": 2.5005619, + "readyState": 4, + "rgba": [ + 16, + 16, + 16, + 255 + ] + }, + "afterPaint": { + "time": 2.5005619, + "readyState": 4, + "rgba": [ + 16, + 16, + 16, + 255 + ] + } + }, + { + "target": 4.5, + "immediate": { + "time": 4.5017801, + "readyState": 4, + "rgba": [ + 16, + 15, + 239, + 255 + ] + }, + "afterPaint": { + "time": 4.5017801, + "readyState": 4, + "rgba": [ + 16, + 15, + 239, + 255 + ] + } + }, + { + "target": 6.5, + "immediate": { + "time": 6.5019006, + "readyState": 3, + "rgba": [ + 235, + 236, + 12, + 255 + ] + }, + "afterPaint": { + "time": 6.5019006, + "readyState": 3, + "rgba": [ + 235, + 236, + 12, + 255 + ] + } + }, + { + "target": 0, + "immediate": { + "time": 0.0012953, + "readyState": 4, + "rgba": [ + 127, + 14, + 127, + 255 + ] + }, + "afterPaint": { + "time": 0.0012953, + "readyState": 4, + "rgba": [ + 127, + 14, + 127, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 2 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 2 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0022923, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.0146568, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0146568, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.042291, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.1014159, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.1014159, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.5011826, + "readyState": 4 + }, + { + "event": "seeked", + "time": 1.0005246, + "readyState": 4 + }, + { + "event": "seeked", + "time": 2.5005619, + "readyState": 4 + }, + { + "event": "seeked", + "time": 4.5017801, + "readyState": 4 + }, + { + "event": "seeked", + "time": 6.5019006, + "readyState": 3 + }, + { + "event": "canplay", + "time": 0.0012953, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0012953, + "readyState": 4 + } + ] + }, + { + "browser": "webkit", + "version": "26.6", + "mode": "attached-plain", + "error": "seeked timed out", + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 4 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0021202, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0115887, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.0414667, + "readyState": 4 + } + ] + } + ] + }, + { + "file": "refactor/.cache/auto-thumbnail-no-b-frame-control.json", + "sha256": "503a6e2232b7176f3c36eb8ac61fe25b0645880303236a8ae605eef0ec387c2c", + "results": [ + { + "browser": "chromium", + "version": "153.0.8010.12", + "mode": "attached-hidden-plain", + "initial": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frame": null, + "beforeSeek": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.01, + "immediate": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 0.1, + "immediate": { + "time": 0.1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 0.5, + "immediate": { + "time": 0.5, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.5, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 1, + "immediate": { + "time": 1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 2.5, + "immediate": { + "time": 2.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 2.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 255 + ] + } + }, + { + "target": 4.5, + "immediate": { + "time": 4.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 253, + 255 + ] + }, + "afterPaint": { + "time": 4.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 253, + 255 + ] + } + }, + { + "target": 6.5, + "immediate": { + "time": 6.5, + "readyState": 4, + "rgba": [ + 255, + 255, + 2, + 255 + ] + }, + "afterPaint": { + "time": 6.5, + "readyState": 4, + "rgba": [ + 255, + 255, + 2, + 255 + ] + } + }, + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 1 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 1 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.01, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.01, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.04, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.04, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.1, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.1, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 1, + "readyState": 4 + }, + { + "event": "canplay", + "time": 1, + "readyState": 4 + }, + { + "event": "seeked", + "time": 2.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 2.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 4.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 4.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 6.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 6.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + } + ] + }, + { + "browser": "chromium", + "version": "153.0.8010.12", + "mode": "attached-plain", + "initial": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frame": null, + "beforeSeek": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "frames": [ + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.01, + "immediate": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0.01, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + }, + { + "target": 0.04, + "immediate": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.04, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 0.1, + "immediate": { + "time": 0.1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 0.5, + "immediate": { + "time": 0.5, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 0.5, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 1, + "immediate": { + "time": 1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 1, + "readyState": 4, + "rgba": [ + 254, + 0, + 0, + 255 + ] + } + }, + { + "target": 2.5, + "immediate": { + "time": 2.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 255 + ] + }, + "afterPaint": { + "time": 2.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 0, + 255 + ] + } + }, + { + "target": 4.5, + "immediate": { + "time": 4.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 253, + 255 + ] + }, + "afterPaint": { + "time": 4.5, + "readyState": 4, + "rgba": [ + 0, + 0, + 253, + 255 + ] + } + }, + { + "target": 6.5, + "immediate": { + "time": 6.5, + "readyState": 4, + "rgba": [ + 255, + 255, + 2, + 255 + ] + }, + "afterPaint": { + "time": 6.5, + "readyState": 4, + "rgba": [ + 255, + 255, + 2, + 255 + ] + } + }, + { + "target": 0, + "immediate": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "afterPaint": { + "time": 0, + "readyState": 4, + "rgba": [ + 127, + 0, + 126, + 255 + ] + } + } + ], + "transitions": [ + { + "event": "loadedmetadata", + "time": 0, + "readyState": 1 + }, + { + "event": "loadeddata", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 1 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.01, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.01, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.04, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.04, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.1, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.1, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 1, + "readyState": 4 + }, + { + "event": "canplay", + "time": 1, + "readyState": 4 + }, + { + "event": "seeked", + "time": 2.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 2.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 4.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 4.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 6.5, + "readyState": 4 + }, + { + "event": "canplay", + "time": 6.5, + "readyState": 4 + }, + { + "event": "seeked", + "time": 0, + "readyState": 4 + }, + { + "event": "canplay", + "time": 0, + "readyState": 4 + } + ] + } + ] + } + ] + } +} diff --git a/refactor/changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md b/refactor/changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md new file mode 100644 index 000000000..6f1a0c196 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md @@ -0,0 +1,47 @@ +# PKG-AUTO-THUMB-03 原生帧就绪检查点(doing) + +新增frames.js,集中拥有每次抽帧的数据等待、seek、原生帧回调和30秒deadline。 +同时支持requestVideoFrameCallback及cancelVideoFrameCallback时,等待loadeddata后 +注册回调再seek,必须seek完成且帧已呈现才draw。无该能力时仍用已记录的seek路径, +不以UA字符串选择浏览器。每次重试递增回调身份,阻止被取消的旧回调完成当前帧; +每个job只注册一个统一清理函数,避免逐帧累计清理闭包。 + +销毁、切源、正常完成及失败均释放timer、帧回调、data/seek处理器。句柄0有效; +同步注册/销毁交错产生的迟到句柄也回收。清理一个资源抛错仍继续其他清理,失败保留 +之前的可用预览。等待超时使用既有console.warn通道;未新增公开参数、结果或依赖, +不改变注册Promise和目标时间公式。30秒是每次数据/seek/帧等待上限,尚不是全部网络、 +编码、后台标签和整体内存预算;不声称已完成这些边界。 + +新增19项回归,纳入test:unit及test:auto-thumbnail。此前a128706f检查点19项为 +2通过/17失败;两项保留原兼容行为,其余覆盖就绪、两种事件顺序、超时、取消、重试、 +重复/迟到回调、同步重入、清理异常及旧预览保留。加上原33项生命周期共52项。 + +提交前追加审查抓到readyState getter触发销毁后仍可能装回loadeddata处理器;新测试 +在修复前19项18通过/1失败,修复为读完就绪状态后再核对任务身份。此前1817项CI和84项 +原生结果保留为中间证据,最终证据使用frames-final文件,重新构建后核对全部候选。 + +真实像素验收增强为:有原生帧回调时必须五格全部正确,包括单独紫色首帧、第二格红色、 +合法黑色、蓝色和黄色,同时核对实际呈现时间。空间样本防止1px平均色伪修复。新增原生 +回调被挂起期间destroy/restart测试,之后重复交付也不生成URL/写入/恢复decoder。 +无原生回调的浏览器相应场景使用原生Blob挂起,报告明确标出能力和路径。 + +Chromium/Firefox此前的早期像素问题进入候选验证;Windows WebKit26.6没有该帧回调, +前两格继续诊断而不验收,仅第2-4格沿用已有像素断言。未将实验或局部通过扩成全浏览器 +正确,也未关闭AUTO-THUMB-PIXEL-01、标记03完成。物理Safari及核心组合仍属后续验收。 + +正常构建再生三份dist及docs副本,架构地图和任务/风险证据同步。TS仍留在04。 +见[验证](../baselines/auto-thumbnail-frame-presentation.json)。 + +最终源码/main/legacy各52项单元通过;原生源码两轮42项及main/legacy各21项,共84项 +通过,0重试/跳过/未处理页面错误。完整CI1818项(1572单元+14工程+232基线)、339生产TS +及4种实际bundle入口通过;产物与docs副本字节相等。旧产物新浏览器断言2通过/4失败, +包括缺少原生呈现证据和Firefox早期错误像素;不得称为所有旧浏览器都出现同一故障。 + +独立密集采样实验在Chromium/Firefox对0/.005/.01/.02/.03/.04秒均收到帧回调,前五个 +目标保留紫色首帧,.04秒为红色,未出现同一帧重复采样导致等待卡死。该实验不是完整 +默认100格候选验收。WebKit另测createImageBitmap(video)仍有首帧偏移,VideoFrame +不可用;保留原始结果,不据此豁免像素风险。 + +去B帧的独立媒体对照中,Chromium仍读到紫色首帧,而Windows WebKit隐藏元素0秒 +仍为空、.01秒已变红,普通元素的一轮seek还超时;不能把问题简单归因于B帧,也不能 +统一偏移.04秒。该诊断保留输入指纹和生成命令,未修改生产回退算法或声明能力豁免。 diff --git a/refactor/plan.md b/refactor/plan.md index 5592c7d1e..f3e71ffcd 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -496,7 +496,7 @@ - PKG-AUDIO-05: [记录](changes/2026-09-12-PKG-AUDIO-05-combinations.md) [记录](baselines/audio-combinations-first.json) [记录](baselines/audio-combinations-checkpoint.json) [记录](audio-validation.md) - PKG-AUTO-THUMB-01: [记录](baselines/auto-thumbnail-release.json) [记录](baselines/auto-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-AUTO-THUMB-01-contract.md) [记录](baselines/auto-thumbnail-contract-validation.json) - PKG-AUTO-THUMB-02: [记录](baselines/auto-thumbnail-failures.md) [记录](baselines/auto-thumbnail-failures-validation.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-02-failures.md) -- PKG-AUTO-THUMB-03: [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md) [记录](baselines/auto-thumbnail-lifecycle-checkpoint.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md) [记录](baselines/auto-thumbnail-hidden-renderer.json) [记录](baselines/auto-thumbnail-timeline-media.json) +- PKG-AUTO-THUMB-03: [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md) [记录](baselines/auto-thumbnail-lifecycle-checkpoint.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md) [记录](baselines/auto-thumbnail-hidden-renderer.json) [记录](baselines/auto-thumbnail-timeline-media.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md) [记录](baselines/auto-thumbnail-frame-presentation.json) - PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md) - PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md) - PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md) diff --git a/refactor/progress.md b/refactor/progress.md index 4fc649b85..867e238b8 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,16 @@ # 进度与证据 +## PKG-AUTO-THUMB-03 原生帧就绪检查点(doing) + +新增frames模块,对支持的浏览器同时等待seek完成与原生帧呈现,补齐取消、超时、 +重复/迟到回调和重试身份。Chromium/Firefox五格像素全部验收,WebKit缺该API时前两格 +仍只诊断,像素风险继续open。19项新回归此前2通过/17失败;源码/main/legacy各52通过。 +最终原生源码两轮42项,main/legacy各21项,共84项通过;包含真实帧挂起时destroy/restart。 +完整CI1818项、339生产TS及4种bundle入口通过;见[验证](baselines/auto-thumbnail-frame-presentation.json); +[变更](changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md)记录能力边界和后续要求。 +正常构建及文档同步,TS仍属04。222项:114 done、12 doing、96 todo;03未标完成。 + + ## PKG-AUTO-THUMB-03 隐藏解码与错时seek重试检查点(doing) 新增video模块,挂载隐藏且保持实际尺寸的解码元素,完善DOM回收及插入/清理重入。 diff --git a/refactor/risks.json b/refactor/risks.json index 066b5c3fb..5a717fd39 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4113,11 +4113,13 @@ "refactor/baselines/auto-thumbnail-failures-validation.json", "test/browser/auto-thumbnail-pixels.spec.js", "refactor/baselines/auto-thumbnail-hidden-renderer.json", - "refactor/baselines/auto-thumbnail-timeline-media.json" + "refactor/baselines/auto-thumbnail-timeline-media.json", + "test/auto-thumbnail-frames.test.js", + "refactor/baselines/auto-thumbnail-frame-presentation.json" ], "compatibleResolution": "Establish actual decoded-frame readiness for the existing timeline/grid and preserve silent independent extraction; do not waive colored pixel checks as unsupported media.", "closureCriteria": "Candidate source and artifacts generate usable sampled pixels on native browser controls with lifecycle checks; supported Safari and core combinations remain explicit final gates.", - "workspaceState": "Hidden intrinsic-sized attached decoder mitigates detached WebKit black later frames. Source 45 repeated native cases plus main/legacy 15 each verify lifecycle and cells 2-4 only. Initial cells 0-1 remain diagnostic: transparent/stale draws and Windows WebKit first-frame timing remain unresolved. Bounded retries now reject wrong-time seeked events before drawing. Do not close this risk or task03 based on partial pixel acceptance." + "workspaceState": "The frames module now waits for native presentation and seek completion on capable browsers. Chromium/Firefox source/main/legacy assert all five cells including first purple frame, second red cell, legitimate black and presentation times. Callback-less Windows WebKit first two cells remain diagnostic, later cells asserted; physical Safari/core combinations remain open. 84 native lifecycle/pixel cases passed with explicit held native-frame vs Blob-fallback paths; 19 new Node cases fail 17 ways on prior checkpoint and all 52 current cases pass per candidate. Risk and task03 remain open." } ] } diff --git a/refactor/tasks.json b/refactor/tasks.json index 62113cb56..620a55416 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1786,7 +1786,9 @@ "baselines/auto-thumbnail-lifecycle-checkpoint.json", "changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md", "baselines/auto-thumbnail-hidden-renderer.json", - "baselines/auto-thumbnail-timeline-media.json" + "baselines/auto-thumbnail-timeline-media.json", + "changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md", + "baselines/auto-thumbnail-frame-presentation.json" ] }, { diff --git a/test/auto-thumbnail-frames.test.js b/test/auto-thumbnail-frames.test.js new file mode 100644 index 000000000..c267359c2 --- /dev/null +++ b/test/auto-thumbnail-frames.test.js @@ -0,0 +1,264 @@ +import assert from 'node:assert/strict' +// eslint-disable-next-line test/no-import-node-test -- Repository Node frame ownership regression runner. +import test from 'node:test' +import { autoThumbnailCandidate, autoThumbnailEnvironment } from './helpers/auto-thumbnail.js' + +const implementation = await autoThumbnailCandidate() + +async function setup({ readyState = 4, cancel = true } = {}) { + const env = autoThumbnailEnvironment(implementation) + const frames = new Map() + const canceled = [] + let nextFrame = 0 + await env.factory({ width: 80, number: 2 })(env.art) + env.art.emit('video:loadedmetadata') + const video = env.videos[0] + video.readyState = readyState + video.requestVideoFrameCallback = (callback) => { + const id = nextFrame++ + frames.set(id, callback) + return id + } + if (cancel) { + video.cancelVideoFrameCallback = (id) => { + canceled.push(id) + frames.delete(id) + } + } + return Object.assign(env, { video, frames, canceled, present(id = frames.keys().next().value) { + const callback = frames.get(id) + frames.delete(id) + callback(0, { mediaTime: video.currentTime }) + } }) +} + +function closed(env) { + assert.equal(env.attached.size, 0) + assert.equal(env.frames.size, 0) + assert.equal(env.timers.size, 0) + assert.equal(env.video.onloadeddata, null) + assert.equal(env.video.onseeked, null) + assert.equal(env.video.src, undefined) +} + +test('Auto-thumbnail frames wait for loadeddata before starting presentation-aware seeking', async () => { + const env = await setup({ readyState: 1 }) + env.metadata() + assert.equal(env.frames.size, 0) + assert.equal(env.operations.some(item => item.name === 'seek'), false) + assert.equal(env.timers.size, 1) + const loaded = env.video.onloadeddata + env.video.readyState = 4 + loaded() + loaded() + assert.equal(env.frames.size, 1) + assert.equal(env.operations.filter(item => item.name === 'seek').length, 1) + env.art.emit('destroy') + closed(env) +}) + +for (const order of ['seek-first', 'frame-first']) { + test(`Auto-thumbnail frames need both seek completion and presentation: ${order}`, async () => { + const env = await setup() + env.metadata() + const seeked = env.video.onseeked + const frame = env.frames.get(0) + if (order === 'seek-first') + env.seeked() + else env.present() + assert.equal(env.blobs.length, 0) + if (order === 'seek-first') + env.present() + else env.seeked() + assert.equal(env.blobs.length, 1) + assert.equal(env.timers.size, 0) + seeked() + frame() + assert.equal(env.blobs.length, 1) + env.finish() + seeked() + frame() + assert.equal(env.blobs.length, 1, 'Old callbacks cannot complete the next frame') + env.seeked() + env.present() + env.finish(1) + closed(env) + assert.equal(env.updates.length, 2) + env.art.emit('destroy') + assert.equal(env.urls.size, 0) + }) +} + +for (const phase of ['data', 'seek', 'frame']) { + test(`Auto-thumbnail frames release ${phase} waits and ignore callbacks after destroy`, async () => { + const env = await setup({ readyState: phase === 'data' ? 1 : 4 }) + env.metadata() + const loaded = env.video.onloadeddata + const frame = env.frames.get(0) + const seeked = env.video.onseeked + const timer = [...env.timers.values()][0].callback + if (phase === 'frame') + env.seeked() + env.art.emit('destroy') + loaded?.() + frame?.() + seeked?.() + timer() + assert.equal(env.blobs.length, 0) + assert.equal(env.warnings.length, 0) + closed(env) + }) +} + +test('Auto-thumbnail frames invalidate canceled presentation callbacks when retrying the same target', async () => { + const env = await setup() + env.metadata() + const first = env.frames.get(0) + env.video.timeOffset = 1 + env.seeked() + assert.deepEqual(env.canceled, [0]) + first() + env.video.timeOffset = 0 + env.seeked() + assert.equal(env.blobs.length, 0) + env.present(1) + assert.equal(env.blobs.length, 1) + env.art.emit('destroy') + closed(env) +}) + +for (const phase of ['data', 'seek', 'frame']) { + test(`Auto-thumbnail frames time out a stalled ${phase} wait without leaking callbacks`, async () => { + const env = await setup({ readyState: phase === 'data' ? 1 : 4 }) + env.metadata() + if (phase === 'frame') + env.seeked() + const timer = [...env.timers.values()][0] + assert.equal(timer.delay, 30000) + timer.callback() + assert.equal(env.warnings.length, 1) + assert.match(env.warnings[0][1].message, /frame readiness timed out/) + assert.equal(env.blobs.length, 0) + closed(env) + env.art.emit('destroy') + }) +} + +test('Auto-thumbnail frames clean up the decoder even if native callback cancellation throws', async () => { + const env = await setup() + env.metadata() + const cancel = env.video.cancelVideoFrameCallback + const failure = new Error('Frame cancellation failed') + env.video.cancelVideoFrameCallback = (id) => { + cancel(id) + throw failure + } + env.art.emit('destroy') + closed(env) + assert.equal(env.warnings[0][1], failure) +}) + +test('Auto-thumbnail frames recover a callback handle registered across synchronous destruction', async () => { + const env = await setup() + const request = env.video.requestVideoFrameCallback + env.video.requestVideoFrameCallback = (callback) => { + env.art.emit('destroy') + return request(callback) + } + env.metadata() + closed(env) + assert.deepEqual(env.canceled, [0]) + assert.equal(env.operations.some(item => item.name === 'seek'), false) +}) + +test('Auto-thumbnail frames recover a timer handle created across synchronous destruction', async () => { + const env = await setup() + const schedule = env.box.setTimeout + env.box.setTimeout = (...args) => { + env.art.emit('destroy') + return schedule(...args) + } + env.metadata() + closed(env) + assert.equal(env.operations.some(item => item.name === 'seek'), false) +}) + +test('Auto-thumbnail frames preserve fallback seeking when native callback cancellation is absent', async () => { + const env = await setup({ cancel: false }) + env.metadata() + assert.equal(env.frames.size, 0) + env.seeked() + assert.equal(env.blobs.length, 1) + env.art.emit('destroy') + closed(env) +}) + +test('Auto-thumbnail frames preserve an earlier preview if subsequent presentation times out', async () => { + const env = await setup() + env.metadata() + env.seeked() + env.present() + env.finish() + const previous = env.art.thumbnails + env.seeked() + const timer = [...env.timers.values()][0] + timer.callback() + assert.equal(env.art.thumbnails, previous) + assert.equal(env.urls.has(previous.url), true) + closed(env) + env.art.emit('destroy') + assert.equal(env.urls.size, 0) +}) + +test('Auto-thumbnail frames accept synchronous presentation without leaking its returned handle', async () => { + const env = await setup() + const request = env.video.requestVideoFrameCallback + env.video.requestVideoFrameCallback = (callback) => { + const id = request(callback) + callback() + return id + } + env.metadata() + assert.equal(env.frames.size, 0) + assert.equal(env.blobs.length, 0) + env.seeked() + assert.equal(env.blobs.length, 1) + env.art.emit('destroy') + closed(env) +}) + +test('Auto-thumbnail frames release callbacks and the decoder when clearing its timer throws', async () => { + const env = await setup() + env.metadata() + const clear = env.box.clearTimeout + const failure = new Error('Timer cleanup failed') + env.box.clearTimeout = (id) => { + clear(id) + throw failure + } + env.art.emit('destroy') + closed(env) + assert.equal(env.warnings[0][1], failure) +}) + +test('Auto-thumbnail frames own native registration errors and clear the deadline', async () => { + const env = await setup() + const failure = new Error('Frame registration failed') + env.video.requestVideoFrameCallback = () => { + throw failure + } + env.metadata() + closed(env) + assert.equal(env.warnings[0][1], failure) +}) + +test('Auto-thumbnail frames cannot restore a loadeddata handler after readiness getter destruction', async () => { + const env = await setup() + Object.defineProperty(env.video, 'readyState', { get() { + env.art.emit('destroy') + return 1 + } }) + env.metadata() + closed(env) + assert.equal(env.operations.some(item => item.name === 'seek'), false) +}) diff --git a/test/browser/auto-thumbnail-lifecycle.spec.js b/test/browser/auto-thumbnail-lifecycle.spec.js index bbbcf7833..3470c1a06 100644 --- a/test/browser/auto-thumbnail-lifecycle.spec.js +++ b/test/browser/auto-thumbnail-lifecycle.spec.js @@ -3,21 +3,29 @@ import { autoThumbnailCandidate } from '../helpers/auto-thumbnail.js' import { expect, test } from './fixtures.js' const implementation = await autoThumbnailCandidate() -for (const scenario of ['destroy', 'restart', 'complete']) { +for (const scenario of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame-restart']) { test(`Auto-thumbnail candidate native ${scenario} owns decoder and encoded URLs`, async ({ page }, testInfo) => { await page.goto('/test/player.html?core=published') await page.setContent('
') await page.addScriptTag({ content: implementation.code }) - await page.evaluate(async (hold) => { - const probe = window.probe = { videos: [], updates: [], urls: new Set(), blobs: [], listeners: new Map(), hold } + await page.evaluate(async (scenario) => { + const probe = window.probe = { videos: [], updates: [], urls: new Set(), blobs: [], frames: [], frameSupport: false, listeners: new Map(), hold: scenario !== 'complete' } const createElement = document.createElement.bind(document) const createUrl = URL.createObjectURL.bind(URL) const revokeUrl = URL.revokeObjectURL.bind(URL) const toBlob = HTMLCanvasElement.prototype.toBlob document.createElement = function (name, ...args) { const element = createElement(name, ...args) - if (name === 'video') + if (name === 'video') { probe.videos.push(element) + const request = element.requestVideoFrameCallback?.bind(element) + probe.frameSupport = Boolean(request && element.cancelVideoFrameCallback) + if (probe.frameSupport && scenario.startsWith('frame-')) { + element.requestVideoFrameCallback = callback => request((...args) => { + probe.frames.push({ callback, args }) + }) + } + } return element } HTMLCanvasElement.prototype.toBlob = function (callback, type) { @@ -63,32 +71,41 @@ for (const scenario of ['destroy', 'restart', 'complete']) { } probe.result = await window.artplayerPluginAutoThumbnail({ width: 80, number: 2 })(probe.art) probe.art.emit('video:loadedmetadata') - }, scenario !== 'complete') - await expect.poll(() => page.evaluate(() => window.probe.blobs.length)).toBe(scenario === 'complete' ? 2 : 1) + }, scenario) + const holdFrame = scenario.startsWith('frame-') && await page.evaluate(() => window.probe.frameSupport) + await expect.poll(() => page.evaluate(holdFrame => holdFrame ? window.probe.frames.length : window.probe.blobs.length, holdFrame)).toBe(scenario === 'complete' ? 2 : 1) const state = await page.evaluate((scenario) => { const probe = window.probe const before = { urls: probe.urls.size, updates: probe.updates.length } if (scenario !== 'complete') { - probe.art.emit(scenario) - probe.blobs[0].callback(probe.blobs[0].blob) - probe.blobs[0].callback(probe.blobs[0].blob) + probe.art.emit(scenario.replace('frame-', '')) + if (probe.blobs[0]) { + probe.blobs[0].callback(probe.blobs[0].blob) + probe.blobs[0].callback(probe.blobs[0].blob) + } + if (probe.frames[0]) { + probe.frames[0].callback(...probe.frames[0].args) + probe.frames[0].callback(...probe.frames[0].args) + } } const after = { urls: probe.urls.size, updates: probe.updates.length } const decoder = probe.videos[0] - const video = { connected: decoder.isConnected, src: decoder.getAttribute('src'), readyState: decoder.readyState, paused: decoder.paused, handlers: ['onloadedmetadata', 'onseeked', 'onerror'].map(key => decoder[key] === null) } + const video = { connected: decoder.isConnected, src: decoder.getAttribute('src'), readyState: decoder.readyState, paused: decoder.paused, handlers: ['onloadedmetadata', 'onloadeddata', 'onseeked', 'onerror'].map(key => decoder[key] === null) } probe.art.emit('destroy') probe.art.emit('destroy') const final = { urls: probe.urls.size, listeners: [...probe.listeners.values()].reduce((sum, set) => sum + set.size, 0), updates: probe.updates.length } const blobs = probe.blobs.map(({ blob }) => ({ type: blob.type, bytes: blob.size })) probe.restore() - return { before, after, video, final, blobs, result: probe.result } + return { before, after, video, final, blobs, heldFrames: probe.frames.length, frameSupport: probe.frameSupport, result: probe.result } }, scenario) - await testInfo.attach('auto-thumbnail-candidate-native-lifecycle', { contentType: 'application/json', body: JSON.stringify({ scenario, sha256: hash(implementation.code), scope: 'Actual HTTP decoding/seek/JPEG lifecycle with a stub player host. This checkpoint does not establish accurate extracted pixels; AUTO-THUMB-PIXEL-01 remains open.', ...state }) }) + await testInfo.attach('auto-thumbnail-candidate-native-lifecycle', { contentType: 'application/json', body: JSON.stringify({ scenario, sha256: hash(implementation.code), scope: 'Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.', ...state }) }) expect(state.result).toEqual({ name: 'artplayerPluginAutoThumbnail' }) expect(state.before).toEqual(scenario === 'complete' ? { urls: 1, updates: 2 } : { urls: 0, updates: 0 }) expect(state.after).toEqual(state.before) - expect(state.video).toEqual({ connected: false, src: null, readyState: 0, paused: true, handlers: [true, true, true] }) + expect(state.video).toEqual({ connected: false, src: null, readyState: 0, paused: true, handlers: [true, true, true, true] }) expect(state.final).toEqual({ urls: 0, listeners: 0, updates: scenario === 'complete' ? 2 : 0 }) expect(state.blobs.every(blob => blob.type === 'image/jpeg' && blob.bytes > 0)).toBe(true) + expect(state.heldFrames).toBe(holdFrame ? 1 : 0) + expect(state.blobs).toHaveLength(holdFrame ? 0 : scenario === 'complete' ? 2 : 1) }) } diff --git a/test/browser/auto-thumbnail-pixels.spec.js b/test/browser/auto-thumbnail-pixels.spec.js index ba3fb66df..f1f42e795 100644 --- a/test/browser/auto-thumbnail-pixels.spec.js +++ b/test/browser/auto-thumbnail-pixels.spec.js @@ -5,14 +5,14 @@ import { expect, test } from './fixtures.js' const implementation = await autoThumbnailCandidate() for (const media of ['auto-thumbnail-timeline', 'pattern']) { - test(`Auto-thumbnail candidate retains cells 2-4 ${media} pixels in a hidden decoder`, async ({ page, browserName }, testInfo) => { + test(`Auto-thumbnail candidate verifies presented ${media} pixels and records fallback limits`, async ({ page, browserName }, testInfo) => { await page.goto('/test/player.html?core=published') await page.setContent('') await page.addScriptTag({ content: implementation.code }) await page.evaluate(async (media) => { const createElement = document.createElement.bind(document) const drawImage = CanvasRenderingContext2D.prototype.drawImage - const probe = window.probe = { videos: [], updates: [], draws: [], playing: 0, warnings: [] } + const probe = window.probe = { videos: [], updates: [], draws: [], presented: [], playing: 0, warnings: [] } const warn = console.warn console.warn = (...args) => probe.warnings.push(args.map(String)) document.createElement = function (tag, ...args) { @@ -20,6 +20,13 @@ for (const media of ['auto-thumbnail-timeline', 'pattern']) { if (tag === 'video') { probe.videos.push(element) element.addEventListener('playing', () => probe.playing++) + const request = element.requestVideoFrameCallback?.bind(element) + if (request) { + element.requestVideoFrameCallback = callback => request((now, metadata) => { + probe.presented.push({ mediaTime: metadata.mediaTime, currentTime: element.currentTime }) + callback(now, metadata) + }) + } } return element } @@ -79,15 +86,17 @@ for (const media of ['auto-thumbnail-timeline', 'pattern']) { const final = probe.videos.map(video => ({ connected: video.isConnected, src: video.getAttribute('src'), paused: video.paused })) probe.art.emit('destroy') probe.restore() - return { cells, spatial, draws: probe.draws, final, playing: probe.playing, warnings: probe.warnings } + const supportsPresentation = typeof probe.videos[0].requestVideoFrameCallback === 'function' && typeof probe.videos[0].cancelVideoFrameCallback === 'function' + return { cells, spatial, draws: probe.draws, presented: probe.presented, supportsPresentation, final, playing: probe.playing, warnings: probe.warnings } }) await testInfo.attach('auto-thumbnail-candidate-hidden-pixels', { contentType: 'application/json', body: JSON.stringify({ sha256: hash(implementation.code), media, - scope: 'Native decoding and JPEG pixels for cells 2-4, hidden intrinsic-sized rendering and cleanup. Cells 0-1 are diagnostic only: initial drawing/readiness/timestamp behavior remains open under AUTO-THUMB-PIXEL-01. Stub player host, not a core integration.', + scope: 'All five cells are asserted where native frame presentation callbacks are available. Without that capability only cells 2-4 are asserted, with cells 0-1 recorded as unresolved diagnostics under AUTO-THUMB-PIXEL-01. Native HTTP/JPEG and a stub player host, not a core integration.', ...state, }) }) - testInfo.annotations.push({ type: 'remaining-initial-frame-risk', description: `${browserName}: cells 0-1 are evidence only; this test cannot close AUTO-THUMB-PIXEL-01 or PKG-AUTO-THUMB-03.` }) + if (!state.supportsPresentation) + testInfo.annotations.push({ type: 'remaining-initial-frame-risk', description: `${browserName}: no native presentation callback; cells 0-1 remain evidence only and cannot close AUTO-THUMB-PIXEL-01 or PKG-AUTO-THUMB-03.` }) expect(state.warnings).toEqual([]) expect(state.playing).toBe(0) expect(state.final).toEqual([{ connected: false, src: null, paused: true }]) @@ -98,12 +107,25 @@ for (const media of ['auto-thumbnail-timeline', 'pattern']) { expect(draw.display).toBe('block') expect(draw.width).toBeGreaterThanOrEqual(80) expect(draw.height).toBeGreaterThanOrEqual(45) - if (index > 1) { + if (state.supportsPresentation || index > 1) { expect(Math.abs(draw.time - draw.duration * index / 5)).toBeLessThan(0.05) expect(draw.raw[3]).toBe(255) } } + if (state.supportsPresentation) { + expect(state.presented).toHaveLength(5) + for (const [index, frame] of state.presented.entries()) + expect(Math.abs(frame.mediaTime - state.draws[index].duration * index / 5)).toBeLessThan(0.06) + } if (media === 'auto-thumbnail-timeline') { + if (state.supportsPresentation) { + const [purple, red] = state.cells + expect(Math.min(purple[0], purple[2])).toBeGreaterThan(100) + expect(Math.abs(purple[0] - purple[2])).toBeLessThan(30) + expect(purple[1]).toBeLessThan(40) + expect(red[0]).toBeGreaterThan(200) + expect(Math.max(red[1], red[2])).toBeLessThan(40) + } const [black, blue, yellow] = state.cells.slice(2) expect(Math.max(...black.slice(0, 3))).toBeLessThan(30) expect(blue[2]).toBeGreaterThan(200) @@ -112,7 +134,7 @@ for (const media of ['auto-thumbnail-timeline', 'pattern']) { expect(yellow[2]).toBeLessThan(40) } else { - for (const pixel of state.cells.slice(2, 4)) { + for (const pixel of state.cells.slice(state.supportsPresentation ? 0 : 2, 4)) { expect(pixel[2] - Math.max(pixel[0], pixel[1])).toBeGreaterThan(80) } expect(state.cells[4][0] - Math.max(state.cells[4][1], state.cells[4][2])).toBeGreaterThan(80) diff --git a/test/helpers/auto-thumbnail.js b/test/helpers/auto-thumbnail.js index 6a61c277a..f42d6f81b 100644 --- a/test/helpers/auto-thumbnail.js +++ b/test/helpers/auto-thumbnail.js @@ -41,6 +41,8 @@ export function autoThumbnailEnvironment(implementation, { script = false } = {} const listeners = new Map() const updates = [] const warnings = [] + const timers = new Map() + let nextTimer = 0 let nextUrl = 0 const controls = { nullContext: false, drawError: null, encodeError: null, updateError: null, onUpdate: null, beforeAppend: null, afterAppend: null } const attached = new Set() @@ -68,6 +70,7 @@ export function autoThumbnailEnvironment(implementation, { script = false } = {} videoWidth: 1920, videoHeight: 1080, onloadedmetadata: null, + onloadeddata: null, onseeked: null, onerror: null, pause() { operations.push({ name: 'pause', video }) }, @@ -116,7 +119,11 @@ export function autoThumbnailEnvironment(implementation, { script = false } = {} }, } const module = { exports: {} } - const box = { document, URL: { + const box = { document, setTimeout(callback, delay) { + const id = nextTimer++ + timers.set(id, { callback, delay }) + return id + }, clearTimeout(id) { timers.delete(id) }, URL: { createObjectURL(blob) { if (blob == null) throw new TypeError('Blob required') @@ -174,6 +181,7 @@ export function autoThumbnailEnvironment(implementation, { script = false } = {} listeners, updates, warnings, + timers, controls, attached, metadata(video = videos.at(-1)) { video.onloadedmetadata?.() },