refactor(auto-thumbnail): [PKG-AUTO-THUMB-03] own hidden renderers and reject stale seeks

This commit is contained in:
Harvey Zhao committed 2026-09-13 07:23:55 +08:00
1 parent 84721f19ed
commit a128706fe7
23 files changed
+12210 -38

No files matched your search

@@ -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
@@ -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();
});
@@ -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
}