mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(auto-thumbnail): [PKG-AUTO-THUMB-03] own hidden renderers and reject stale seeks
This commit is contained in:
1 parent
84721f19ed
commit
a128706fe7
23 files changed
+12210
-38
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.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=document.createElement("video");e.own(()=>i.load()),e.own(()=>i.removeAttribute("src")),e.own(()=>i.pause());for(const n of["onloadedmetadata","onseeked","onerror"])e.own(()=>{i[n]=null});e.active()&&(i.crossOrigin="anonymous",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,{height:r,canvasWidth:o,canvasHeight:a}=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:i.videoHeight,videoWidth:i.videoWidth});if(!e.active())return;const u=document.createElement("canvas");if(!e.active())return;u.width=o,u.height=a;const d=u.getContext("2d");if(!d)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let c=0;const s=e.guard(()=>{c>=t.number?e.dispose():(i.onseeked=e.guard(()=>{if(i.onseeked=null,d.drawImage(i,c%10*t.width,Math.floor(c/10)*r,t.width,r),!e.active())return;let n=!1;u.toBlob(e.guard(i=>{n||(n=!0,e.publish(i,{height:r,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(c+=1,s()))}),"image/jpeg")}),e.active()&&(i.currentTime=n*c/t.number))});s()}),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,c=[];let s=!1;const l={active:()=>!o&&!s&&n===l,own(e){if(s){const n=t([e]);n.failed&&i(n.failure)}else c.push(e)},dispose(){if(s)return;s=!0,n===l&&(n=void 0);const e=t(c.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 c=r;r=d;try{e({url:d,...o})}catch(s){r===d&&(r=a.has(c)?c:void 0);const e=t([()=>u(d)]);throw e.failed&&i(e.failure),s}u(c)}};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(c){throw d(),c}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,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"}}}});
|
||||
@@ -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,r=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable,i=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n;function o(e,t){const r=document.createElement("video");e.own(()=>r.load()),e.own(()=>r.removeAttribute("src")),e.own(()=>r.pause());for(const n of["onloadedmetadata","onseeked","onerror"])e.own(()=>{r[n]=null});e.active()&&(r.crossOrigin="anonymous",r.onerror=e.guard(()=>{throw r.error||new Error("Auto-thumbnail media failed to load")}),r.onloadedmetadata=e.guard(()=>{r.onloadedmetadata=null;const n=r.duration,{height:i,canvasWidth:o,canvasHeight:a}=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 r=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),i=Math.trunc(r*Math.ceil(e.number/10));if(![r,n,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:r,canvasWidth:n,canvasHeight:i}}(t,{duration:n,videoHeight:r.videoHeight,videoWidth:r.videoWidth});if(!e.active())return;const u=document.createElement("canvas");if(!e.active())return;u.width=o,u.height=a;const d=u.getContext("2d");if(!d)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let c=0;const l=e.guard(()=>{c>=t.number?e.dispose():(r.onseeked=e.guard(()=>{if(r.onseeked=null,d.drawImage(r,c%10*t.width,Math.floor(c/10)*i,t.width,i),!e.active())return;let n=!1;u.toBlob(e.guard(r=>{n||(n=!0,e.publish(r,{height:i,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(c+=1,l()))}),"image/jpeg")}),e.active()&&(r.currentTime=n*c/t.number))});l()}),e.active()&&(r.src=t.url))}function a(e){let t,r=!1;for(const i of e)try{i()}catch(n){r||(r=!0,t=n)}return{failed:r,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 f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(c)return;const f=u,h=[];let m=!1;const v={active:()=>!c&&!m&&u===v,own(e){if(m){const t=a([e]);t.failed&&o(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=a(h.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(r){v.fail(r)}},publish(u,c){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(l.add(f),!v.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))r.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))n.call(o,a)&&i(e,a,o[a]);return e})({url:f},c))}catch(m){d===f&&(d=l.has(h)?h:void 0);const e=a([()=>s(f)]);throw e.failed&&o(e.failure),m}s(h)}};return u=v,null==f||f.dispose(),v},cancel:f,destroy:function(){if(c)return;c=!0;const e=a([f,...[...l].map(e=>()=>s(e))]);d=void 0,e.failed&&o(e.failure)},get closed(){return c}}}return function(e){return t=>{return r=null,n=null,i=function*(){const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),n=u(e=>{t.thumbnails=e},r),i=[],d=()=>{const r=n.start();r&&r.guard(()=>{const n=function(e,t){const r={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(r.width))||r.width<=0||!Number.isFinite(Number(r.number))||r.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return r}(e,()=>t.option.url);r.active()&&o(r,n)})()},c=()=>{const e=i.splice(0),t=a([n.destroy,...e]);t.failed&&r(t.failure)};try{for(const[e,r]of[["destroy",c],["restart",n.cancel],["video:loadedmetadata",d]]){if(n.closed)break;i.push(()=>t.off(e,r)),t.on(e,r),n.closed&&t.off(e,r)}}catch(l){throw c(),l}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(r){t(r)}},a=e=>{try{u(i.throw(e))}catch(r){t(r)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(r,n)).next())});var r,n,i}}});
|
||||
!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:"artplayerPluginAutoThumbLine truncated
|
||||
@@ -25,8 +25,9 @@ function sheetSize(config, video) {
|
||||
throw new RangeError("Auto-thumbnail canvas dimensions are invalid");
|
||||
return { height, canvasWidth, canvasHeight };
|
||||
}
|
||||
function extract(job, config) {
|
||||
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());
|
||||
@@ -35,21 +36,41 @@ function extract(job, config) {
|
||||
video[property] = null;
|
||||
});
|
||||
if (!job.active())
|
||||
return;
|
||||
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: video.videoHeight,
|
||||
videoWidth: video.videoWidth
|
||||
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;
|
||||
@@ -66,7 +87,20 @@ function extract(job, config) {
|
||||
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())
|
||||
@@ -84,7 +118,7 @@ function extract(job, config) {
|
||||
}), "image/jpeg");
|
||||
});
|
||||
if (job.active())
|
||||
video.currentTime = duration * index / config.number;
|
||||
video.currentTime = target;
|
||||
});
|
||||
seek();
|
||||
});
|
||||
|
||||
@@ -24,9 +24,16 @@ for video processing.
|
||||
handlers, and the frame/encode sequence. Each valid draw is encoded before the
|
||||
next seek. The final decoder is paused, cleared and reset; the final sheet URL
|
||||
remains owned by the session until another usable sheet replaces it or destroy.
|
||||
- `src/video.js` creates and owns the hidden media element. It is attached to the
|
||||
document root because detached media loses drawable pixels on Windows WebKit.
|
||||
`visibility:hidden` preserves its rendered box; `display:none` and a 1px box do
|
||||
not. Metadata locks the box to intrinsic dimensions, overriding ordinary global
|
||||
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.
|
||||
|
||||
Only the entry module receives ArtPlayer. Extraction receives a guarded job and a
|
||||
configuration snapshot. These internal modules do not add public player fields,
|
||||
configuration snapshot. These internal modules do not add public player fields,
|
||||
plugin methods, or a dependency on a particular core implementation.
|
||||
|
||||
## Compatibility and intentional fixes
|
||||
@@ -39,6 +46,13 @@ Duration and sheet dimensions are captured at metadata time; later duration chan
|
||||
do not move the remaining samples within the current sheet.
|
||||
The old `height` option remains ignored at runtime.
|
||||
|
||||
A `seeked` callback is ignored while another seek is pending. Before drawing,
|
||||
extraction checks that the media time is finite and within 50ms of the requested
|
||||
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.
|
||||
|
||||
The previous empty first encode is removed. Encoding is serial, duplicate native
|
||||
callbacks are consumed once, and source replacement/destruction invalidates old
|
||||
metadata, seek and Blob callbacks. Failure does not revoke an earlier usable
|
||||
@@ -57,6 +71,7 @@ Use the root Yarn toolchain:
|
||||
```sh
|
||||
yarn test:auto-thumbnail
|
||||
yarn test:browser test/browser/auto-thumbnail-lifecycle.spec.js
|
||||
yarn test:browser test/browser/auto-thumbnail-pixels.spec.js
|
||||
yarn build artplayer-plugin-auto-thumbnail
|
||||
```
|
||||
|
||||
@@ -66,11 +81,21 @@ Historical contract/failure tests remain separate and continue to reproduce old
|
||||
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 detached decoder path still needs
|
||||
the recorded Windows WebKit frame/rendering fix (`AUTO-THUMB-PIXEL-01`). Hidden
|
||||
rendering must retain source detail; a 1x1 element producing an averaged pixel is
|
||||
not a correct fix. Frame timing, additional reentry/cleanup boundaries and resource
|
||||
budgets still need the remaining task03 work.
|
||||
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,
|
||||
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
|
||||
reentry/cleanup boundaries and resource budgets still need the remaining task03 work.
|
||||
|
||||
The frozen eight-second timeline has one unique purple first frame followed by
|
||||
red, black, blue and yellow sections. Its command and fingerprint are in
|
||||
`refactor/baselines/auto-thumbnail-timeline-media.json`. Reproduce into a new
|
||||
directory with `ARTPLAYER_FFMPEG` and `node scripts/generate-auto-thumbnail-fixture.mjs
|
||||
<new-directory>`; never silently replace the committed fixture.
|
||||
|
||||
Task04 converts these modules to strict TypeScript and resolves historical public
|
||||
declaration/CommonJS differences. Task05 verifies old/final cores and actual
|
||||
|
||||
+1
-1
@@ -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=document.createElement("video");e.own(()=>i.load()),e.own(()=>i.removeAttribute("src")),e.own(()=>i.pause());for(const n of["onloadedmetadata","onseeked","onerror"])e.own(()=>{i[n]=null});e.active()&&(i.crossOrigin="anonymous",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,{height:r,canvasWidth:o,canvasHeight:a}=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:i.videoHeight,videoWidth:i.videoWidth});if(!e.active())return;const u=document.createElement("canvas");if(!e.active())return;u.width=o,u.height=a;const d=u.getContext("2d");if(!d)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let c=0;const s=e.guard(()=>{c>=t.number?e.dispose():(i.onseeked=e.guard(()=>{if(i.onseeked=null,d.drawImage(i,c%10*t.width,Math.floor(c/10)*r,t.width,r),!e.active())return;let n=!1;u.toBlob(e.guard(i=>{n||(n=!0,e.publish(i,{height:r,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(c+=1,s()))}),"image/jpeg")}),e.active()&&(i.currentTime=n*c/t.number))});s()}),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,c=[];let s=!1;const l={active:()=>!o&&!s&&n===l,own(e){if(s){const n=t([e]);n.failed&&i(n.failure)}else c.push(e)},dispose(){if(s)return;s=!0,n===l&&(n=void 0);const e=t(c.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 c=r;r=d;try{e({url:d,...o})}catch(s){r===d&&(r=a.has(c)?c:void 0);const e=t([()=>u(d)]);throw e.failed&&i(e.failure),s}u(c)}};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(c){throw d(),c}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,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"}}}});
|
||||
+1
-1
@@ -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,r=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable,i=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n;function o(e,t){const r=document.createElement("video");e.own(()=>r.load()),e.own(()=>r.removeAttribute("src")),e.own(()=>r.pause());for(const n of["onloadedmetadata","onseeked","onerror"])e.own(()=>{r[n]=null});e.active()&&(r.crossOrigin="anonymous",r.onerror=e.guard(()=>{throw r.error||new Error("Auto-thumbnail media failed to load")}),r.onloadedmetadata=e.guard(()=>{r.onloadedmetadata=null;const n=r.duration,{height:i,canvasWidth:o,canvasHeight:a}=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 r=Math.floor(e.width*t.videoHeight/t.videoWidth),n=Math.trunc(10*e.width),i=Math.trunc(r*Math.ceil(e.number/10));if(![r,n,i].every(e=>Number.isSafeInteger(e)&&e>0&&e<=4294967295))throw new RangeError("Auto-thumbnail canvas dimensions are invalid");return{height:r,canvasWidth:n,canvasHeight:i}}(t,{duration:n,videoHeight:r.videoHeight,videoWidth:r.videoWidth});if(!e.active())return;const u=document.createElement("canvas");if(!e.active())return;u.width=o,u.height=a;const d=u.getContext("2d");if(!d)throw new Error("Auto-thumbnail canvas context is unavailable");if(!e.active())return;let c=0;const l=e.guard(()=>{c>=t.number?e.dispose():(r.onseeked=e.guard(()=>{if(r.onseeked=null,d.drawImage(r,c%10*t.width,Math.floor(c/10)*i,t.width,i),!e.active())return;let n=!1;u.toBlob(e.guard(r=>{n||(n=!0,e.publish(r,{height:i,column:10,number:t.number,width:t.width,scale:t.scale}),e.active()&&(c+=1,l()))}),"image/jpeg")}),e.active()&&(r.currentTime=n*c/t.number))});l()}),e.active()&&(r.src=t.url))}function a(e){let t,r=!1;for(const i of e)try{i()}catch(n){r||(r=!0,t=n)}return{failed:r,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 f(){const e=u;u=void 0,null==e||e.dispose()}return{start:function(){if(c)return;const f=u,h=[];let m=!1;const v={active:()=>!c&&!m&&u===v,own(e){if(m){const t=a([e]);t.failed&&o(t.failure)}else h.push(e)},dispose(){if(m)return;m=!0,u===v&&(u=void 0);const e=a(h.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(r){v.fail(r)}},publish(u,c){if(!v.active())return;if(!u)throw new Error("Auto-thumbnail encoding returned no Blob");const f=URL.createObjectURL(u);if(l.add(f),!v.active())return void s(f);const h=d;d=f;try{e(((e,o)=>{for(var a in o||(o={}))r.call(o,a)&&i(e,a,o[a]);if(t)for(var a of t(o))n.call(o,a)&&i(e,a,o[a]);return e})({url:f},c))}catch(m){d===f&&(d=l.has(h)?h:void 0);const e=a([()=>s(f)]);throw e.failed&&o(e.failure),m}s(h)}};return u=v,null==f||f.dispose(),v},cancel:f,destroy:function(){if(c)return;c=!0;const e=a([f,...[...l].map(e=>()=>s(e))]);d=void 0,e.failed&&o(e.failure)},get closed(){return c}}}return function(e){return t=>{return r=null,n=null,i=function*(){const r=e=>console.warn("ArtPlayer auto-thumbnail failed:",e),n=u(e=>{t.thumbnails=e},r),i=[],d=()=>{const r=n.start();r&&r.guard(()=>{const n=function(e,t){const r={url:e.url||t(),width:e.width||160,number:e.number||100,scale:e.scale||1};if(!Number.isFinite(Number(r.width))||r.width<=0||!Number.isFinite(Number(r.number))||r.number<=0)throw new TypeError("Auto-thumbnail width and number must be finite positive numbers");return r}(e,()=>t.option.url);r.active()&&o(r,n)})()},c=()=>{const e=i.splice(0),t=a([n.destroy,...e]);t.failed&&r(t.failure)};try{for(const[e,r]of[["destroy",c],["restart",n.cancel],["video:loadedmetadata",d]]){if(n.closed)break;i.push(()=>t.off(e,r)),t.on(e,r),n.closed&&t.off(e,r)}}catch(l){throw c(),l}return{name:"artplayerPluginAutoThumbnail"}},new Promise((e,t)=>{var o=e=>{try{u(i.next(e))}catch(r){t(r)}},a=e=>{try{u(i.throw(e))}catch(r){t(r)}},u=t=>t.done?e(t.value):Promise.resolve(t.value).then(o,a);u((i=i.apply(r,n)).next())});var r,n,i}}});
|
||||
!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:"artplayerPluginAutoThumbLine truncated
|
||||
+39
-5
@@ -25,8 +25,9 @@ function sheetSize(config, video) {
|
||||
throw new RangeError("Auto-thumbnail canvas dimensions are invalid");
|
||||
return { height, canvasWidth, canvasHeight };
|
||||
}
|
||||
function extract(job, config) {
|
||||
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());
|
||||
@@ -35,21 +36,41 @@ function extract(job, config) {
|
||||
video[property] = null;
|
||||
});
|
||||
if (!job.active())
|
||||
return;
|
||||
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: video.videoHeight,
|
||||
videoWidth: video.videoWidth
|
||||
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;
|
||||
@@ -66,7 +87,20 @@ function extract(job, config) {
|
||||
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())
|
||||
@@ -84,7 +118,7 @@ function extract(job, config) {
|
||||
}), "image/jpeg");
|
||||
});
|
||||
if (job.active())
|
||||
video.currentTime = duration * index / config.number;
|
||||
video.currentTime = target;
|
||||
});
|
||||
seek();
|
||||
});
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { sheetSize } from './options'
|
||||
import createVideo from './video'
|
||||
|
||||
export default function extract(job, config) {
|
||||
const video = document.createElement('video')
|
||||
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 })
|
||||
const video = createVideo(job)
|
||||
if (!job.active())
|
||||
return
|
||||
video.crossOrigin = 'anonymous'
|
||||
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: video.videoHeight,
|
||||
videoWidth: video.videoWidth,
|
||||
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
|
||||
@@ -39,7 +40,20 @@ export default function extract(job, config) {
|
||||
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())
|
||||
@@ -57,7 +71,7 @@ export default function extract(job, config) {
|
||||
}), 'image/jpeg')
|
||||
})
|
||||
if (job.active())
|
||||
video.currentTime = duration * index / config.number
|
||||
video.currentTime = target
|
||||
})
|
||||
seek()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export default 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')
|
||||
// WebKit needs a rendered box; display:none and a 1px box lose decoded pixels.
|
||||
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
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"kind": "generated-auto-thumbnail-timeline",
|
||||
"version": "ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev Copyright (c) 2000-2025 the FFmpeg developers",
|
||||
"args": [
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-n",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=purple:s=320x180:r=30:d=0.033333333",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=red:s=320x180:r=30:d=1.966666667",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=black:s=320x180:r=30:d=2",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=blue:s=320x180:r=30:d=2",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=yellow:s=320x180:r=30:d=2",
|
||||
"-filter_complex",
|
||||
"[0:v][1:v][2:v][3:v][4:v]concat=n=5:v=1:a=0[v]",
|
||||
"-map",
|
||||
"[v]",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-g",
|
||||
"30",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
"auto-thumbnail-timeline.mp4"
|
||||
],
|
||||
"width": 320,
|
||||
"height": 180,
|
||||
"fps": 30,
|
||||
"duration": 8,
|
||||
"segments": [
|
||||
[
|
||||
"purple",
|
||||
"0.033333333"
|
||||
],
|
||||
[
|
||||
"red",
|
||||
"1.966666667"
|
||||
],
|
||||
[
|
||||
"black",
|
||||
"2"
|
||||
],
|
||||
[
|
||||
"blue",
|
||||
"2"
|
||||
],
|
||||
[
|
||||
"yellow",
|
||||
"2"
|
||||
]
|
||||
],
|
||||
"purpose": "Unique purple first frame, then red/black/blue/yellow timeline; distinguish missing transparent pixels, legitimate black frames and stale first frames.",
|
||||
"file": {
|
||||
"name": "auto-thumbnail-timeline.mp4",
|
||||
"bytes": 8602,
|
||||
"sha256": "10370156cffada462d3ea00aae0caadd62752369cd6538cc55248b3ddd7bf994"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# PKG-AUTO-THUMB-03 隐藏解码元素检查点(doing)
|
||||
|
||||
新增video.js,独立拥有内部video的挂载、静音、可访问性和移除。使用固定定位、
|
||||
visibility:hidden及metadata时锁定的原始尺寸;不调用play,不用display:none或1px缩放。
|
||||
extraction继续拥有原来的起点采样公式和串行编码。普通全局video样式不会把内部解码
|
||||
元素缩成1px;初始化取消、插入前后同步重入、插入失败及load/pause异常也回收DOM。
|
||||
公开参数、结果、Promise时序和声明未改。正常构建再生dist和docs副本。
|
||||
|
||||
构建main产物的验证又复现一次后续格偏移约1.6秒,失败报告独立保留为
|
||||
auto-thumbnail03-render-browser-verified-main.json(该文件名虽含verified,结果仍是
|
||||
14通过/1失败)。因此继续修复seeked归属:仍在seeking时忽略旧事件,绘制前检查有限
|
||||
currentTime及目标50ms容差;偏离只重试原目标最多3次,再失败清理。新增4项回归覆盖
|
||||
错时重试、重试界限/旧预览保留、seeking旧事件和时间getter销毁重入。最终证据使用
|
||||
browser-timing及unit-timing文件,不把这次失败当成通过或靠重复运行覆盖。
|
||||
|
||||
新增5项单元边界:此前检查点运行29项为25通过/4失败,新实现29通过。第五项load抛错
|
||||
本来就能通过,此次加强其DOM所有权断言。不能将29项都称为本次新修复。
|
||||
|
||||
真实浏览器新增两种像素样本:时间颜色片段及空间颜色图案。时间片段8秒/30fps,首帧
|
||||
独立紫色,随后红、黑、蓝、黄;生成脚本、FFmpeg版本、参数、SHA256保存在媒体清单。
|
||||
重新生成字节完全一致。解码前raw alpha区分“没有画出来”和合法黑画面,实际JPEG解码
|
||||
验证颜色及空间差异,记录采样时间。9项原生生命周期还增加DOM脱离断言。
|
||||
|
||||
**这是局部修复,03和AUTO-THUMB-PIXEL-01保持未完成。** 新的像素验收只覆盖五格中的
|
||||
第2-4格(从0编号);前两格完整记录为诊断数据,不能当成通过。初次广泛断言的报告
|
||||
保留在auto-thumbnail03-render-browser-source-initial.json:发现Firefox早期draw可能
|
||||
透明,Windows WebKit的早期seeked可能时间回退。pattern样本的JPEG中心在色块交界处
|
||||
存在色度混合,末段本身也改变颜色,因此不能错误地要求每一格中心始终纯蓝。
|
||||
当前像素断言使用已知阶段的颜色优势和空间差异;这不意味着早期帧问题被修复。
|
||||
|
||||
独立诊断还证明:WebKit跳回0秒可能保留上一张图;两次rAF、先跳远再返回、fastSeek及
|
||||
playing后立即pause均未稳定解决。唯一紫色首帧在Chromium/Firefox的0秒出现,而本机
|
||||
WebKit可能在约0.04秒才读到;不能简单把所有浏览器偏移0.04秒,也不能用均匀红色片段
|
||||
掩盖一帧的时间错误。相关原始实验摘要保存在本检查点验证记录,物理Safari仍未验证。
|
||||
|
||||
后续继续首帧和早期frame-ready调度,保持任务取消及URL归属,再完成资源预算/其余
|
||||
边界;04转严格TS,05/06做核心组合、设备、真实安装与demo/editor。没有新依赖、
|
||||
发布、推送或任务完成数变化。提交仅是可回退的03检查点。
|
||||
|
||||
见[验证记录](../baselines/auto-thumbnail-hidden-renderer.json)。
|
||||
|
||||
最终33项候选回归:此前检查点25通过/8失败,源码/main/legacy各33通过。最终源码原生
|
||||
连续三轮45项,main/legacy各15项,共75项通过,无重试/跳过/未处理页面错误。完整CI1799项
|
||||
(1553单元+14工程+232基线)及339生产TS通过;另核对4种实际bundle入口与无DOM异步
|
||||
注册,尚不代表安装tarball。三份产物与docs副本逐字节相等,03仍doing,像素风险仍open。
|
||||
|
||||
后续实验auto-thumbnail-frame-callback-probe在loadeddata后对每次seek等待原生帧回调,
|
||||
Chromium/Firefox隐藏和普通元素都能读到五格预期颜色,包括独立紫色首帧;本机WebKit
|
||||
没有requestVideoFrameCallback。这尚未接入生产,下一步要处理回调归属/取消/失效/
|
||||
超时及无该API浏览器的可靠路径,不能把实验结果提前标为修复。
|
||||
+1
-1
@@ -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)
|
||||
- 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-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)
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-AUTO-THUMB-03 隐藏解码与错时seek重试检查点(doing)
|
||||
|
||||
新增video模块,挂载隐藏且保持实际尺寸的解码元素,完善DOM回收及插入/清理重入。
|
||||
真实产物测试发现seeked可能停留在前一格,已增加绘制前时间核对及最多3次原目标重试。
|
||||
33项候选单元在此前检查点25通过/8失败,新源码/main/legacy各33通过。
|
||||
最终原生源码连续三轮45项、main/legacy各15项通过,共75项;其中仅第2-4格有像素
|
||||
验收,前两格及首帧精度保持open。保留先前真实失败,不把重复通过当作全流程完成。
|
||||
新增可再生时间颜色夹具及脚本,严格TS仍属于04,无依赖或版本改动。
|
||||
完整CI1799项、339生产TS通过,4种实际bundle入口通过;见[验证](baselines/auto-thumbnail-hidden-renderer.json),
|
||||
[变更](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md)解释修复和剩余边界。
|
||||
222项:114 done、12 doing、96 todo;本次是03检查点,任务完成数未增加。
|
||||
|
||||
|
||||
## PKG-AUTO-THUMB-03 源码任务与资源所有权检查点(doing)
|
||||
|
||||
生产源码拆为index/options/session/extraction,保持异步公开注册与原有效配置语义。
|
||||
|
||||
+6
-2
@@ -4110,10 +4110,14 @@
|
||||
"evidence": [
|
||||
"test/browser/auto-thumbnail.spec.js",
|
||||
"refactor/baselines/auto-thumbnail-failures.md",
|
||||
"refactor/baselines/auto-thumbnail-failures-validation.json"
|
||||
"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"
|
||||
],
|
||||
"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."
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
-1
@@ -1783,7 +1783,10 @@
|
||||
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
|
||||
"evidence": [
|
||||
"changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md",
|
||||
"baselines/auto-thumbnail-lifecycle-checkpoint.json"
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../refactor/scripts/releases.mjs'
|
||||
|
||||
const output = path.resolve(process.argv[2] || 'refactor/.cache/auto-thumbnail-fixture')
|
||||
assert(!fs.existsSync(output), 'Use a new directory; never overwrite frozen media')
|
||||
const ffmpeg = process.env.ARTPLAYER_FFMPEG || 'ffmpeg'
|
||||
const version = execFileSync(ffmpeg, ['-version'], { encoding: 'utf8' }).split(/\r?\n/)[0]
|
||||
fs.mkdirSync(output, { recursive: true })
|
||||
const segments = [['purple', '0.033333333'], ['red', '1.966666667'], ['black', '2'], ['blue', '2'], ['yellow', '2']]
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
'error',
|
||||
'-n',
|
||||
...segments.flatMap(([color, duration]) => ['-f', 'lavfi', '-i', `color=c=${color}:s=320x180:r=30:d=${duration}`]),
|
||||
'-filter_complex',
|
||||
'[0:v][1:v][2:v][3:v][4:v]concat=n=5:v=1:a=0[v]',
|
||||
'-map',
|
||||
'[v]',
|
||||
'-c:v',
|
||||
'libx264',
|
||||
'-pix_fmt',
|
||||
'yuv420p',
|
||||
'-g',
|
||||
'30',
|
||||
'-movflags',
|
||||
'+faststart',
|
||||
'auto-thumbnail-timeline.mp4',
|
||||
]
|
||||
execFileSync(ffmpeg, args, { cwd: output, stdio: 'pipe', timeout: 120000 })
|
||||
const bytes = fs.readFileSync(path.join(output, 'auto-thumbnail-timeline.mp4'))
|
||||
fs.writeFileSync(path.join(output, 'manifest.json'), `${JSON.stringify({
|
||||
kind: 'generated-auto-thumbnail-timeline',
|
||||
version,
|
||||
args,
|
||||
width: 320,
|
||||
height: 180,
|
||||
fps: 30,
|
||||
duration: 8,
|
||||
segments,
|
||||
purpose: 'Unique purple first frame, then red/black/blue/yellow timeline; distinguish missing transparent pixels, legitimate black frames and stale first frames.',
|
||||
file: { name: 'auto-thumbnail-timeline.mp4', bytes: bytes.length, sha256: hash(bytes) },
|
||||
}, null, 2)}\n`)
|
||||
console.log(`Generated auto-thumbnail timeline in ${output}`)
|
||||
@@ -13,6 +13,7 @@ async function setup(option = { width: 80, number: 2 }) {
|
||||
return env
|
||||
}
|
||||
function assertClosed(env, video = env.videos[0]) {
|
||||
assert.equal(env.attached.has(video), false)
|
||||
assert.equal(video.src, undefined)
|
||||
for (const key of ['onloadedmetadata', 'onseeked', 'onerror']) assert.equal(video[key], null)
|
||||
assert(env.operations.some(item => item.name === 'pause' && item.video === video))
|
||||
@@ -329,3 +330,119 @@ test('Auto-thumbnail candidate keeps the metadata duration snapshot for frame ti
|
||||
assert.deepEqual(env.operations.filter(item => item.name === 'seek').map(item => item.value), [0, 60])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('Auto-thumbnail candidate hides an attached decoder at intrinsic dimensions without playback', async () => {
|
||||
const env = await setup()
|
||||
const video = env.videos[0]
|
||||
assert.equal(env.attached.has(video), true)
|
||||
assert.equal(video['aria-hidden'], 'true')
|
||||
assert.equal(video.tabIndex, -1)
|
||||
assert.equal(video.muted, true)
|
||||
assert.match(video.style.cssText, /visibility:hidden/)
|
||||
assert.doesNotMatch(video.style.cssText, /display:none|width:1px|height:1px/)
|
||||
env.metadata()
|
||||
assert.equal(video.width, 1920)
|
||||
assert.equal(video.height, 1080)
|
||||
assert.equal(video.style.width, '1920px')
|
||||
assert.equal(video.style.height, '1080px')
|
||||
env.art.emit('destroy')
|
||||
assertClosed(env)
|
||||
})
|
||||
|
||||
for (const phase of ['beforeAppend', 'afterAppend']) {
|
||||
test(`Auto-thumbnail candidate ${phase} reentry cannot leave an attached decoder`, async () => {
|
||||
const env = autoThumbnailEnvironment(implementation)
|
||||
await env.factory({})(env.art)
|
||||
env.controls[phase] = () => env.art.emit('destroy')
|
||||
env.art.emit('video:loadedmetadata')
|
||||
assert.equal(env.attached.size, 0)
|
||||
assert.equal(env.videos[0].src, undefined)
|
||||
assertClosed(env)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('Auto-thumbnail candidate failed insertion removes a partially attached decoder', async () => {
|
||||
const env = autoThumbnailEnvironment(implementation)
|
||||
const failure = new Error('Insertion observer failed')
|
||||
env.controls.afterAppend = () => {
|
||||
throw failure
|
||||
}
|
||||
await env.factory({})(env.art)
|
||||
env.art.emit('video:loadedmetadata')
|
||||
assert.equal(env.attached.size, 0)
|
||||
assert.equal(env.videos[0].src, undefined)
|
||||
assert.equal(env.warnings[0][1], failure)
|
||||
assertClosed(env)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('Auto-thumbnail candidate decoder removal still runs after load throws', async () => {
|
||||
const env = await setup()
|
||||
const failure = new Error('Decoder reset failed')
|
||||
env.videos[0].load = () => {
|
||||
throw failure
|
||||
}
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.attached.size, 0)
|
||||
assert.equal(env.videos[0].src, undefined)
|
||||
assert.equal(env.warnings[0][1], failure)
|
||||
assert([...env.listeners.values()].every(set => set.size === 0))
|
||||
})
|
||||
|
||||
test('Auto-thumbnail candidate ignores a stale seeked event while a new seek is in progress', async () => {
|
||||
const env = await setup()
|
||||
env.metadata()
|
||||
env.videos[0].seeking = true
|
||||
env.seeked()
|
||||
assert.equal(env.blobs.length, 0)
|
||||
env.videos[0].seeking = false
|
||||
env.seeked()
|
||||
assert.equal(env.blobs.length, 1)
|
||||
env.art.emit('destroy')
|
||||
assertClosed(env)
|
||||
})
|
||||
|
||||
test('Auto-thumbnail candidate retries a completed seek at the wrong time before drawing', async () => {
|
||||
const env = await setup()
|
||||
env.metadata()
|
||||
env.videos[0].timeOffset = 1
|
||||
env.seeked()
|
||||
assert.equal(env.blobs.length, 0)
|
||||
assert.deepEqual(env.operations.filter(item => item.name === 'seek').map(item => item.value), [0, 0])
|
||||
env.videos[0].timeOffset = 0
|
||||
env.seeked()
|
||||
assert.equal(env.blobs.length, 1)
|
||||
env.art.emit('destroy')
|
||||
assertClosed(env)
|
||||
})
|
||||
|
||||
test('Auto-thumbnail candidate bounds incorrect seek retries and preserves its last usable preview', async () => {
|
||||
const env = await setup()
|
||||
env.metadata()
|
||||
env.seeked()
|
||||
env.finish()
|
||||
const previous = env.art.thumbnails
|
||||
env.videos[0].timeOffset = -60
|
||||
for (let index = 0; index < 5; index++) env.seeked()
|
||||
assert.equal(env.blobs.length, 1)
|
||||
assert.equal(env.art.thumbnails, previous)
|
||||
assert.equal(env.urls.size, 1)
|
||||
assert.equal(env.operations.filter(item => item.name === 'seek').length, 5)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
assertClosed(env)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.urls.size, 0)
|
||||
})
|
||||
|
||||
test('Auto-thumbnail candidate seek-time getters cannot draw after synchronous destruction', async () => {
|
||||
const env = await setup()
|
||||
env.metadata()
|
||||
Object.defineProperty(env.videos[0], 'timeOffset', { get() {
|
||||
env.art.emit('destroy')
|
||||
return 0
|
||||
} })
|
||||
env.seeked()
|
||||
assert.equal(env.blobs.length, 0)
|
||||
assertClosed(env)
|
||||
})
|
||||
@@ -75,7 +75,7 @@ for (const scenario of ['destroy', 'restart', 'complete']) {
|
||||
}
|
||||
const after = { urls: probe.urls.size, updates: probe.updates.length }
|
||||
const decoder = probe.videos[0]
|
||||
const video = { 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', '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 }
|
||||
@@ -87,7 +87,7 @@ for (const scenario of ['destroy', 'restart', 'complete']) {
|
||||
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({ 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] })
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { autoThumbnailCandidate } from '../helpers/auto-thumbnail.js'
|
||||
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) => {
|
||||
await page.goto('/test/player.html?core=published')
|
||||
await page.setContent('<!doctype html><style>video { width: 1px; height: 1px; max-width: 1px; max-height: 1px; display: none }</style>')
|
||||
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 warn = console.warn
|
||||
console.warn = (...args) => probe.warnings.push(args.map(String))
|
||||
document.createElement = function (tag, ...args) {
|
||||
const element = createElement(tag, ...args)
|
||||
if (tag === 'video') {
|
||||
probe.videos.push(element)
|
||||
element.addEventListener('playing', () => probe.playing++)
|
||||
}
|
||||
return element
|
||||
}
|
||||
CanvasRenderingContext2D.prototype.drawImage = function (source, ...args) {
|
||||
const result = drawImage.call(this, source, ...args)
|
||||
if (source instanceof HTMLVideoElement) {
|
||||
const [x, y, width, height] = args
|
||||
const style = getComputedStyle(source)
|
||||
const box = source.getBoundingClientRect()
|
||||
probe.draws.push({
|
||||
time: source.currentTime,
|
||||
duration: source.duration,
|
||||
connected: source.isConnected,
|
||||
visibility: style.visibility,
|
||||
display: style.display,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
raw: [...this.getImageData(x + Math.floor(width / 2), y + Math.floor(height / 2), 1, 1).data],
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
const listeners = new Map()
|
||||
probe.art = {
|
||||
option: { url: `/test/${media}.mp4` },
|
||||
on(name, callback) {
|
||||
if (!listeners.has(name))
|
||||
listeners.set(name, new Set())
|
||||
listeners.get(name).add(callback)
|
||||
},
|
||||
off(name, callback) { listeners.get(name)?.delete(callback) },
|
||||
emit(name) { for (const callback of [...(listeners.get(name) || [])]) callback() },
|
||||
get thumbnails() { return probe.updates.at(-1) },
|
||||
set thumbnails(value) { probe.updates.push(value) },
|
||||
}
|
||||
probe.restore = () => {
|
||||
document.createElement = createElement
|
||||
CanvasRenderingContext2D.prototype.drawImage = drawImage
|
||||
console.warn = warn
|
||||
}
|
||||
await window.artplayerPluginAutoThumbnail({ width: 80, number: 5 })(probe.art)
|
||||
probe.art.emit('video:loadedmetadata')
|
||||
}, media)
|
||||
await expect.poll(() => page.evaluate(() => window.probe.updates.length)).toBe(5)
|
||||
const state = await page.evaluate(async () => {
|
||||
const probe = window.probe
|
||||
const image = document.createElement('img')
|
||||
image.src = probe.updates.at(-1).url
|
||||
await image.decode()
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = image.naturalWidth
|
||||
canvas.height = image.naturalHeight
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(image, 0, 0)
|
||||
const cells = Array.from({ length: 5 }, (_, index) => [...ctx.getImageData(index * 80 + 40, 22, 1, 1).data])
|
||||
const spatial = [10, 20, 40, 60, 70].map(x => [...ctx.getImageData(160 + x, 22, 1, 1).data])
|
||||
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 }
|
||||
})
|
||||
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.',
|
||||
...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.` })
|
||||
expect(state.warnings).toEqual([])
|
||||
expect(state.playing).toBe(0)
|
||||
expect(state.final).toEqual([{ connected: false, src: null, paused: true }])
|
||||
expect(state.draws).toHaveLength(5)
|
||||
for (const [index, draw] of state.draws.entries()) {
|
||||
expect(draw.connected).toBe(true)
|
||||
expect(draw.visibility).toBe('hidden')
|
||||
expect(draw.display).toBe('block')
|
||||
expect(draw.width).toBeGreaterThanOrEqual(80)
|
||||
expect(draw.height).toBeGreaterThanOrEqual(45)
|
||||
if (index > 1) {
|
||||
expect(Math.abs(draw.time - draw.duration * index / 5)).toBeLessThan(0.05)
|
||||
expect(draw.raw[3]).toBe(255)
|
||||
}
|
||||
}
|
||||
if (media === 'auto-thumbnail-timeline') {
|
||||
const [black, blue, yellow] = state.cells.slice(2)
|
||||
expect(Math.max(...black.slice(0, 3))).toBeLessThan(30)
|
||||
expect(blue[2]).toBeGreaterThan(200)
|
||||
expect(Math.max(blue[0], blue[1])).toBeLessThan(40)
|
||||
expect(Math.min(yellow[0], yellow[1])).toBeGreaterThan(200)
|
||||
expect(yellow[2]).toBeLessThan(40)
|
||||
}
|
||||
else {
|
||||
for (const pixel of state.cells.slice(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)
|
||||
expect(new Set(state.spatial.map(pixel => pixel.slice(0, 3).join(','))).size).toBeGreaterThan(2)
|
||||
}
|
||||
})
|
||||
}
|
||||
Binary file not shown.
@@ -76,6 +76,7 @@ assert.equal(hash(iframeCoreBytes), iframeCore.files[iframeCoreMember], 'Histori
|
||||
add('/published-4.5.9/artplayer.js', iframeCoreBytes, { kind: 'npm-release', version: iframeCore.version, integrity: iframeCore.integrity, member: iframeCoreMember })
|
||||
add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/media/audio-tone.m4a')), { kind: 'generated-media', file: 'test/browser/media/audio-tone.m4a' })
|
||||
add('/test/thumbnail-pattern.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-pattern.mp4')), { kind: 'generated-media', file: 'test/browser/media/thumbnail-pattern.mp4' })
|
||||
add('/test/auto-thumbnail-timeline.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/auto-thumbnail-timeline.mp4')), { kind: 'generated-media', file: 'test/browser/media/auto-thumbnail-timeline.mp4' })
|
||||
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
|
||||
add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' })
|
||||
|
||||
@@ -42,12 +42,29 @@ export function autoThumbnailEnvironment(implementation, { script = false } = {}
|
||||
const updates = []
|
||||
const warnings = []
|
||||
let nextUrl = 0
|
||||
const controls = { nullContext: false, drawError: null, encodeError: null, updateError: null, onUpdate: null }
|
||||
const controls = { nullContext: false, drawError: null, encodeError: null, updateError: null, onUpdate: null, beforeAppend: null, afterAppend: null }
|
||||
const attached = new Set()
|
||||
const document = {
|
||||
documentElement: {
|
||||
appendChild(video) {
|
||||
controls.beforeAppend?.(video)
|
||||
attached.add(video)
|
||||
controls.afterAppend?.(video)
|
||||
return video
|
||||
},
|
||||
},
|
||||
createElement(tag) {
|
||||
if (tag === 'video') {
|
||||
const video = {
|
||||
style: {},
|
||||
setAttribute(name, value) { this[name] = value },
|
||||
remove() {
|
||||
attached.delete(this)
|
||||
operations.push({ name: 'remove', video: this })
|
||||
},
|
||||
duration: 120,
|
||||
timeOffset: 0,
|
||||
seeking: false,
|
||||
videoWidth: 1920,
|
||||
videoHeight: 1080,
|
||||
onloadedmetadata: null,
|
||||
@@ -62,7 +79,7 @@ export function autoThumbnailEnvironment(implementation, { script = false } = {}
|
||||
}
|
||||
let time = 0
|
||||
Object.defineProperty(video, 'currentTime', {
|
||||
get: () => time,
|
||||
get: () => time + video.timeOffset,
|
||||
set(value) {
|
||||
time = value
|
||||
operations.push({ name: 'seek', value, handlerInstalled: typeof video.onseeked === 'function', video })
|
||||
@@ -158,6 +175,7 @@ export function autoThumbnailEnvironment(implementation, { script = false } = {}
|
||||
updates,
|
||||
warnings,
|
||||
controls,
|
||||
attached,
|
||||
metadata(video = videos.at(-1)) { video.onloadedmetadata?.() },
|
||||
seeked(video = videos.at(-1)) { video.onseeked?.() },
|
||||
finish(index = 0, blob = { type: 'image/jpeg' }) { blobs[index].callback(blob) },
|
||||
|
||||
Reference in new issue
Block a user