refactor(core): CORE-19 type capture and preserve progress lifetimes

This commit is contained in:
Harvey Zhao committed 2026-09-11 14:24:59 +08:00
1 parent 98a1800032
commit ccf77c4ed2
49 files changed
+31570 -681

No files matched your search

+96 -1
View File
@@ -4,6 +4,101 @@ ArtPlayer keeps its existing constructor, player mixins, plugins and DOM/CSS hoo
The production entry is still `src/index.js`; this document marks actual migrated
boundaries rather than describing the entire core as TypeScript.
## Screenshot capture (CORE-19)
`src/player/screenshotMix.ts` installs the existing immutable getDataURL,
getBlobUrl and screenshot methods. `src/capture/frame.ts` owns synchronous frame
drawing and the asynchronous canvas Blob callback boundary. A capture still draws
before returning its Promise; moving drawing behind await would select a later
video frame. PNG format, extracted-method binding, default filename timing and
download-before-event ordering remain unchanged for live operations.
The public getBlobUrl caller owns URL.revokeObjectURL. Player destruction must not
invalidate a returned URL that a consumer still uses. Asynchronous null Blob or
URL allocation failure now rejects the capture Promise instead of escaping the
callback and leaving it pending. Notice setter failures also reject. Destruction
and source replacement suppress stale notices and screenshot download/event
effects, while an already captured data string or Blob URL still settles normally.
The toolbar consumes its internal rejection after the capture reports a notice;
public calls continue to reject for callers to handle.
Use test/screenshot.test.js, test/types/screenshot.ts and the browser screenshot
spec for capture changes. Browser checks decode real video frames, inspect PNG
pixels, and use a second hostname without CORS headers for native SecurityError.
Installed-package acceptance is recorded in refactor/changes/ for this task.
## Thumbnail previews and image loading (CORE-19)
`src/player/thumbnailsMix.ts` preserves the thumbnails property, configuration
object identity, live restrictions and control DOM. It owns one image cache scope
per configuration, links it to the current thumbnail control and removes its setBar
subscription on destruction. Replacement, control removal and destruction cancel
loading; obsolete completions cannot render. Loading failures release the pending
state so a later interaction may retry. Failures are reported with console.warn
instead of escaping an internal event callback as an unhandled rejection.
The latest hover position wins while an image is pending. A source change prevents
rendering old hover coordinates but does not invalidate an unchanged sprite cache;
a new interaction may use that image. Each style write checks the active control,
configuration and source so reentrant DOM callbacks stop the remaining writes.
`src/thumbnails/layout.ts` contains the sprite and preview geometry. Dimensions,
scale fallback, edge placement and excluded progress endpoints remain unchanged.
The old row/cell calculation was incorrect at exact column multiples and produced
invalid CSS at cell zero. The corrected zero-based crop matches
artplayer-tool-thumbnail's x/index-modulo-column and y/floor-index-over-column
generation. Tests explicitly distinguish this display fix from published behavior.
`src/image/load.ts` implements both image consumers without duplicating decoding:
public utils.loadImg still returns Promise<HTMLImageElement>, and the caller owns
the src Blob URL of a scaled result. The internal loadThumbnailImage receives a
ResourceScope; cancellation fulfills with undefined, and its generated URLs remain
owned until replacement/removal/destroy. No caller-supplied image URL is revoked.
Every request clears load/error handlers; failed scaled decoding revokes its Blob.
Null canvas encoding and asynchronous allocation errors reject public calls. The
public helper is re-exported through utils/dom.js to keep the existing utils shape;
loadThumbnailImage is not added to the public utils barrel.
Use test/thumbnails.test.js and test/types/thumbnails.ts for state, cancellation,
reentry and API types. The browser thumbnails spec uses the numbered
test/browser/media/thumbnail-grid.svg for crop checks and actual image decoding.
Transient retry uses a no-store 503 response; browser caching of malformed successful
responses is not claimed to be controlled by the player. Source-built checks are
intermediate evidence; use the task's final tarball/browser evidence for acceptance.
## Progress and quality restoration (CORE-19)
control/progress/interactions.ts captures the control entry, source and action for
each click or drag. Geometry and setBar callbacks can synchronously remove the
entry, destroy the player, start a new source or start a newer action. Check those
boundaries before emitting or seeking. A source change ends the old drag; a new
mousedown can start a new one. position.ts retains endpoint clamping and the live
progress event before seek, with an optional internal activity predicate.
source/restore-position.ts owns the paused quality position restoration. Native
canplay may arrive while seeking; switch.ts waits for seek completion, restores
rate once and checks the accepted, clamped position. A demonstrated native seek
miss receives at most one correction, allowing 0.05 seconds for media clock
rounding. It does not loop until success. The seeked listener remains until the
correction finishes or the source operation is cancelled. Explicit public seek
supersedes automatic position restoration; reentrant completion cannot resume
twice. media/position-revision.ts also tracks legal public currentTime writes;
a consumer assigning currentTime directly during loading supersedes the automatic
restoration, including writes before metadata. Invalid NaN assignments remain no-ops.
Source cancellation still settles pending work and removes all listeners.
Use test/source.test.js, test/progress.test.js and the browser source and
progress-quality specs. Keep native media events in evidence when changing this
sequence; the original rate-interruption hypothesis was not sufficient to explain
the observed first-seek miss.
urlMix.ts receives media URLs from consumers/adapters and does not revoke them on
replacement. Their creator owns release, including Blob media sources. Screenshot
and public scaled-image URLs similarly belong to callers; internal thumbnail and
subtitle URLs have explicit owned scopes. Real Blob-video decoding is separately
capability-tested: this Windows WebKit runtime rejects the local MP4 and WebM Blob
samples even without ArtPlayer. Do not turn URL readability checks into claims of
native decoding or Apple-device support; see refactor/environment-matrix.md.
## Builtins and prompts (CORE-18)
`src/notice.ts` owns the notice timer and DOM visibility. Its minimal host, private
@@ -1001,7 +1096,7 @@ continuations. Direct assignment failures are reported with console.warn; switch
callers receive the original rejection. `playMix.ts` captures the current source:
its native result/rejection is preserved, while obsolete notice/event/mutex effects
are suppressed. CORE-10 migrates playback properties and CORE-11 owns reconnect
generations; object URL ownership remains CORE-19 work.
generations; CORE-19 removes revocation of caller-owned media URLs on replacement.
`test/source.test.js` checks cancellation, reentry, synchronous events and cleanup;
`test/browser/source.spec.js` compares published/candidate real-media switching and
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
+467 -223
View File
@@ -496,6 +496,132 @@ const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream;
const isIOS13 = isIOS || userAgent.includes("Macintosh") && navigator.maxTouchPoints >= 1;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
function requestImage(url, scale, owner) {
return new Promise((resolve, reject) => {
const request = owner ? owner.child() : new ResourceScope();
let complete = false;
let releaseBlob = () => {
};
function fail(error2) {
if (complete)
return;
complete = true;
try {
request.dispose();
} catch (cleanupError) {
console.warn("Additional image cleanup failure:", cleanupError);
}
try {
releaseBlob();
} catch (cleanupError) {
console.warn("Additional image cleanup failure:", cleanupError);
}
reject(error2);
}
function finish2(image) {
if (complete || request.closed)
return;
complete = true;
try {
request.dispose();
resolve(image);
} catch (error2) {
try {
releaseBlob();
} finally {
reject(error2);
}
}
}
request.add(() => {
if (!complete) {
complete = true;
try {
releaseBlob();
} finally {
resolve(void 0);
}
}
});
if (request.closed)
return;
function watch(image, loaded) {
image.onload = () => {
if (!complete && !request.closed) {
try {
loaded();
} catch (error2) {
fail(error2);
}
}
};
image.onerror = () => fail(new Error(`Image load failed: ${url}`));
request.add(() => {
image.onload = null;
image.onerror = null;
if (!complete)
image.removeAttribute("src");
});
}
try {
const image = new Image();
watch(image, () => {
if (!scale || scale === 1) {
finish2(image);
return;
}
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = image.width * scale;
canvas.height = image.height * scale;
if (!context)
throw new TypeError("Canvas 2D context is unavailable");
if (complete || request.closed)
return;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
if (complete || request.closed)
return;
canvas.toBlob((blob) => {
if (complete || request.closed)
return;
try {
if (!blob)
throw new Error("Unable to encode thumbnail image");
const blobUrl = URL.createObjectURL(blob);
let revoked = false;
const revoke = () => {
if (!revoked) {
revoked = true;
URL.revokeObjectURL(blobUrl);
}
};
releaseBlob = owner ? owner.add(revoke) : revoke;
if (complete || request.closed) {
releaseBlob();
return;
}
const scaled = new Image();
watch(scaled, () => finish2(scaled));
if (!request.closed)
scaled.src = blobUrl;
} catch (error2) {
fail(error2);
}
});
});
if (!request.closed)
image.src = url;
} catch (error2) {
fail(error2);
}
});
}
function loadImg(url, scale) {
return requestImage(url, scale, void 0);
}
function loadThumbnailImage(url, scale, owner) {
return requestImage(url, scale, owner);
}
function setStyleText(id2, style) {
let $style = document.getElementById(id2);
if (!$style) {
@@ -598,38 +724,6 @@ function supportsFlex() {
function getRect(el) {
return el.getBoundingClientRect();
}
function loadImg(url, scale) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
if (!scale || scale === 1) {
resolve(img);
} else {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
const blobUrl = URL.createObjectURL(blob);
const scaledImg = new Image();
scaledImg.onload = function() {
resolve(scaledImg);
};
scaledImg.onerror = function() {
URL.revokeObjectURL(blobUrl);
reject(new Error(`Image load failed: ${url}`));
};
scaledImg.src = blobUrl;
});
}
};
img.onerror = function() {
reject(new Error(`Image load failed: ${url}`));
};
img.src = url;
});
}
function getComposedPath(event) {
if (event.composedPath)
return event.composedPath();
@@ -1619,6 +1713,75 @@ function playAndPause(option) {
}
});
}
const current = /* @__PURE__ */ new WeakMap();
const assignments = /* @__PURE__ */ new WeakMap();
const sources = /* @__PURE__ */ new WeakMap();
function getSourceScope(owner) {
return sources.get(owner) || getScope(owner);
}
function captureSource(owner) {
const operation = current.get(owner);
const source = sources.get(owner);
return () => current.get(owner) === operation && (!operation || operation.active()) && sources.get(owner) === source && (!source || !source.closed);
}
function beginSource(owner) {
const previous = sources.get(owner);
const source = getScope(owner).child();
const scope = source.child();
sources.set(owner, source);
source.add(() => {
if (sources.get(owner) === source)
sources.delete(owner);
});
const operation = {
scope,
assigned: false,
acceptingEvents: false,
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation
};
current.set(owner, operation);
scope.add(() => {
if (current.get(owner) === operation)
current.delete(owner);
operation.onAssigned = void 0;
operation.onError = void 0;
});
previous?.dispose();
return operation;
}
function takeAssignment(owner) {
const operation = assignments.get(owner);
assignments.delete(owner);
return operation || beginSource(owner);
}
function finishAssignment(operation) {
operation.assigned = true;
const callback = operation.onAssigned;
operation.onAssigned = void 0;
if (operation.active())
callback?.();
}
function assignUrl(owner, operation, url) {
assignments.set(owner, operation);
operation.acceptingEvents = true;
try {
owner.url = url;
} finally {
if (assignments.get(owner) === operation) {
assignments.delete(owner);
finishAssignment(operation);
}
}
}
function failSource(operation, error2) {
if (!operation.active())
return;
if (operation.onError)
operation.onError(error2);
else
console.warn("Failed to initialize ArtPlayer source:", error2);
operation.scope.dispose();
}
function getPosFromEvent(art, event) {
const { $progress } = art.template;
const { left } = getRect($progress);
@@ -1630,14 +1793,20 @@ function getPosFromEvent(art, event) {
return { second, time: time2, width, percentage };
}
function setCurrentTime(art, event, active2 = () => true) {
if (!active2())
return;
if (art.isRotate) {
const percentage = (event.touches[0].clientY - art.top) / art.height;
const second = percentage * art.duration;
if (!active2())
return;
art.emit("setBar", "played", percentage, event);
if (active2())
art.seek = second;
} else {
const { second, percentage } = getPosFromEvent(art, event);
if (!active2())
return;
art.emit("setBar", "played", percentage, event);
if (active2())
art.seek = second;
@@ -1647,34 +1816,51 @@ function installProgressInteractions(art, $control) {
const { $progress } = art.template;
const $indicator = $control.querySelector(".art-progress-indicator");
const { on, proxy } = controlEvents(art, $control);
const scope = entryScope($control);
let action = 0;
const capture = () => {
const sourceActive = captureSource(art);
return () => !scope.closed && !isClosing(art) && sourceActive();
};
const captureAction = () => {
const revision = ++action;
const active2 = capture();
return () => revision === action && active2();
};
if (!isMobile) {
let isDragging = false;
let dragging;
proxy($progress, "click", (event) => {
if (event.target !== $indicator) {
setCurrentTime(art, event);
setCurrentTime(art, event, captureAction());
}
});
proxy($progress, "mousemove", (event) => {
const active2 = capture();
const { percentage } = getPosFromEvent(art, event);
art.emit("setBar", "hover", percentage, event);
if (active2())
art.emit("setBar", "hover", percentage, event);
});
proxy($progress, "mouseleave", (event) => {
art.emit("setBar", "hover", 0, event);
});
proxy($progress, "mousedown", (event) => {
isDragging = event.button === 0;
dragging = event.button === 0 ? captureAction() : void 0;
});
on("document:mousemove", (event) => {
if (isDragging) {
const active2 = dragging;
if (active2?.()) {
const { second, percentage } = getPosFromEvent(art, event);
if (!active2() || dragging !== active2)
return;
art.emit("setBar", "played", percentage, event);
art.seek = second;
if (active2() && dragging === active2)
art.seek = second;
} else {
dragging = void 0;
}
});
on("document:mouseup", () => {
if (isDragging) {
isDragging = false;
}
dragging = void 0;
});
}
}
@@ -1814,7 +2000,7 @@ function screenshot$1(option) {
const { icons } = art;
appendElement($control, icons.screenshot);
proxy($control, "click", () => {
art.screenshot();
silencePromise(art.screenshot());
});
}
});
@@ -2278,75 +2464,6 @@ function clickInit(art, events) {
}
});
}
const current = /* @__PURE__ */ new WeakMap();
const assignments = /* @__PURE__ */ new WeakMap();
const sources = /* @__PURE__ */ new WeakMap();
function getSourceScope(owner) {
return sources.get(owner) || getScope(owner);
}
function captureSource(owner) {
const operation = current.get(owner);
const source = sources.get(owner);
return () => current.get(owner) === operation && (!operation || operation.active()) && sources.get(owner) === source && (!source || !source.closed);
}
function beginSource(owner) {
const previous = sources.get(owner);
const source = getScope(owner).child();
const scope = source.child();
sources.set(owner, source);
source.add(() => {
if (sources.get(owner) === source)
sources.delete(owner);
});
const operation = {
scope,
assigned: false,
acceptingEvents: false,
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation
};
current.set(owner, operation);
scope.add(() => {
if (current.get(owner) === operation)
current.delete(owner);
operation.onAssigned = void 0;
operation.onError = void 0;
});
previous?.dispose();
return operation;
}
function takeAssignment(owner) {
const operation = assignments.get(owner);
assignments.delete(owner);
return operation || beginSource(owner);
}
function finishAssignment(operation) {
operation.assigned = true;
const callback = operation.onAssigned;
operation.onAssigned = void 0;
if (operation.active())
callback?.();
}
function assignUrl(owner, operation, url) {
assignments.set(owner, operation);
operation.acceptingEvents = true;
try {
owner.url = url;
} finally {
if (assignments.get(owner) === operation) {
assignments.delete(owner);
finishAssignment(operation);
}
}
}
function failSource(operation, error2) {
if (!operation.active())
return;
if (operation.onError)
operation.onError(error2);
else
console.warn("Failed to initialize ArtPlayer source:", error2);
operation.scope.dispose();
}
function slideDirection(startX, startY, endX, endY) {
const dy = startY - endY;
const dx = endX - startX;
@@ -3473,6 +3590,13 @@ function cssVarMix(art) {
}
});
}
const revisions = /* @__PURE__ */ new WeakMap();
function positionRevision(art) {
return revisions.get(art) || 0;
}
function advancePosition(art) {
revisions.set(art, positionRevision(art) + 1);
}
function currentTimeMix(art) {
const { $video } = art.template;
def(art, "currentTime", {
@@ -3481,6 +3605,7 @@ function currentTimeMix(art) {
const parsed = Number.parseFloat(time2);
if (Number.isNaN(parsed))
return;
advancePosition(art);
$video.currentTime = clamp(parsed, 0, art.duration);
}
});
@@ -4939,46 +5064,66 @@ function rectMix(art) {
}
});
}
function drawFrame(canvas, video) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext("2d");
if (!context)
throw new TypeError("Canvas 2D context is unavailable");
context.drawImage(video, 0, 0);
}
function captureBlobUrl(canvas, resolve, reject) {
canvas.toBlob((blob) => {
try {
if (!blob)
throw new Error("Unable to encode screenshot blob");
resolve(URL.createObjectURL(blob));
} catch (error2) {
reject(error2);
}
});
}
function screenshotMix(art) {
const {
notice,
template: { $video }
} = art;
const $canvas = createElement("canvas");
def(art, "getDataURL", {
value: () => new Promise((resolve, reject) => {
const { notice, template: { $video } } = art;
const canvas = document.createElement("canvas");
function capture(blob) {
const active2 = captureSource(art);
return new Promise((resolve, reject) => {
const fail = (error2) => {
try {
if (!isClosing(art) && active2())
notice.show = error2;
} catch (noticeError) {
reject(noticeError);
return;
}
reject(error2);
};
try {
$canvas.width = $video.videoWidth;
$canvas.height = $video.videoHeight;
$canvas.getContext("2d").drawImage($video, 0, 0);
resolve($canvas.toDataURL("image/png"));
} catch (err) {
notice.show = err;
reject(err);
drawFrame(canvas, $video);
if (blob) {
captureBlobUrl(canvas, resolve, fail);
} else {
resolve(canvas.toDataURL("image/png"));
}
} catch (error2) {
fail(error2);
}
})
});
def(art, "getBlobUrl", {
value: () => new Promise((resolve, reject) => {
try {
$canvas.width = $video.videoWidth;
$canvas.height = $video.videoHeight;
$canvas.getContext("2d").drawImage($video, 0, 0);
$canvas.toBlob((blob) => {
resolve(URL.createObjectURL(blob));
});
} catch (err) {
notice.show = err;
reject(err);
}
})
});
});
}
def(art, "getDataURL", { value: () => capture(false) });
def(art, "getBlobUrl", { value: () => capture(true) });
def(art, "screenshot", {
value: async (name) => {
const active2 = captureSource(art);
const dataUri = await art.getDataURL();
const fileName = name || `artplayer_${secondToTime($video.currentTime)}`;
download(dataUri, `${fileName}.png`);
art.emit("screenshot", dataUri);
if (!isClosing(art) && active2()) {
const fileName = name || `artplayer_${secondToTime($video.currentTime)}`;
if (!isClosing(art) && active2())
download(dataUri, `${fileName}.png`);
if (!isClosing(art) && active2())
art.emit("screenshot", dataUri);
}
return dataUri;
}
});
@@ -5061,6 +5206,48 @@ function listenSource(scope, art, name, callback, once = true) {
});
return guarded;
}
const POSITION_TOLERANCE = 0.05;
function positionRestoration(art, target, active2) {
let expected;
let observedSeek = false;
let corrected = false;
let manual = false;
let revision = positionRevision(art);
const manualPosition = () => manual || positionRevision(art) !== revision;
const seeking = () => !!art.template?.$video?.seeking;
return {
restore() {
if (!active2() || manualPosition())
return;
const previousRevision = positionRevision(art);
art.currentTime = target;
revision = positionRevision(art);
if (revision > previousRevision + 1)
manual = true;
if (!active2())
return;
expected = art.currentTime;
if (!active2() || manualPosition())
return;
observedSeek = seeking();
},
manual() {
manual = true;
},
ready() {
if (!active2() || seeking())
return false;
const missed = expected !== void 0 && Math.abs(art.currentTime - expected) > POSITION_TOLERANCE;
if (!manualPosition() && observedSeek && !corrected && expected !== void 0 && missed) {
corrected = true;
if (!active2())
return false;
art.currentTime = expected;
}
return active2() && !seeking();
}
};
}
function switchSource(art, url, currentTime) {
return new Promise((resolve, reject) => {
if (isClosing(art) || url === art.url) {
@@ -5096,41 +5283,63 @@ function switchSource(art, url, currentTime) {
if (!active2())
return;
const readiness = scope.child();
let canPlay = false;
let resuming = false;
let rateRestored = false;
const position = positionRestoration(art, currentTime, active2);
const resume = async () => {
art.aspectRatio = aspectRatio2;
if (!active2())
return;
if (playing) {
try {
await art.play();
} catch {
}
}
if (!active2())
return;
art.notice.show = "";
settle();
};
const resumeWhenReady = () => {
if (!active2() || resuming || !canPlay)
return;
try {
if (art.template?.$video?.seeking)
return;
if (!active2())
return;
if (!rateRestored) {
rateRestored = true;
art.playbackRate = playbackRate2;
}
if (!position.ready() || !active2() || resuming)
return;
resuming = true;
readiness.dispose();
void resume().catch(fail);
} catch (error2) {
fail(error2);
}
};
const handlers = {
"video:error": fail,
"video:loadedmetadata": () => {
if (!active2())
return;
try {
art.currentTime = currentTime;
position.restore();
} catch (error2) {
fail(error2);
}
},
"video:canplay": () => {
if (!active2())
return;
readiness.dispose();
const resume = async () => {
art.playbackRate = playbackRate2;
if (!active2())
return;
art.aspectRatio = aspectRatio2;
if (!active2())
return;
if (playing) {
try {
await art.play();
} catch {
}
}
if (!active2())
return;
art.notice.show = "";
settle();
};
void resume().catch(fail);
}
canPlay = true;
resumeWhenReady();
},
"video:seeked": resumeWhenReady,
"seek": position.manual
};
const capture = scope.child();
const queued = [];
@@ -5155,7 +5364,7 @@ function switchSource(art, url, currentTime) {
}
const dispatch = {};
for (const name of names)
dispatch[name] = listenSource(readiness, art, name, handlers[name]);
dispatch[name] = listenSource(readiness, art, name, handlers[name], name !== "video:seeked");
for (const [name, event] of queued) {
if (!active2() || readiness.closed)
break;
@@ -5188,69 +5397,106 @@ function themeMix(art) {
}
});
}
function thumbnailLayout(option, geometry) {
const { number, column, width, height, scale } = option;
const { imageWidth, videoWidth, videoHeight, progressWidth, position } = geometry;
const columns = Number(column);
const previewWidth = Number(width) * Number(scale) || imageWidth / columns;
const previewHeight = Number(height) * Number(scale) || previewWidth / (videoWidth / videoHeight);
const index = Math.floor(position / (progressWidth / Number(number)));
const row = Math.floor(index / columns);
const cell = index % columns;
const left = position <= previewWidth / 2 ? 0 : position > progressWidth - previewWidth / 2 ? `${progressWidth - previewWidth}px` : `${position - previewWidth / 2}px`;
return {
height: `${previewHeight}px`,
width: `${previewWidth}px`,
backgroundPosition: `-${cell * previewWidth}px -${row * previewHeight}px`,
left
};
}
function thumbnailsMix(art) {
const {
option,
template: { $progress, $video }
} = art;
let timer = null;
let loding = false;
let image = null;
const { option, template: { $progress, $video } } = art;
let scope = getScope(art).child();
let image;
let loading2 = false;
let control;
let hover;
function reset() {
clearTimeout(timer);
timer = null;
loding = false;
image = null;
const previous = scope;
scope = getScope(art).child();
image = void 0;
loading2 = false;
hover = void 0;
previous.dispose();
}
function showThumbnails(posWidth) {
const $thumbnails = art.controls?.thumbnails;
if (!$thumbnails)
function render() {
const current2 = hover;
const currentScope = scope;
if (!current2 || !image)
return;
const { number, column, width, height, scale } = option.thumbnails;
const width2 = width * scale || image.naturalWidth / column;
const height2 = height * scale || width2 / ($video.videoWidth / $video.videoHeight);
const perWidth = $progress.clientWidth / number;
const perIndex = Math.floor(posWidth / perWidth);
const yIndex = Math.ceil(perIndex / column) - 1;
const xIndex = perIndex % column || column - 1;
setStyle($thumbnails, "backgroundImage", `url(${image.src})`);
setStyle($thumbnails, "height", `${height2}px`);
setStyle($thumbnails, "width", `${width2}px`);
setStyle($thumbnails, "backgroundPosition", `-${xIndex * width2}px -${yIndex * height2}px`);
if (posWidth <= width2 / 2) {
setStyle($thumbnails, "left", 0);
} else if (posWidth > $progress.clientWidth - width2 / 2) {
setStyle($thumbnails, "left", `${$progress.clientWidth - width2}px`);
} else {
setStyle($thumbnails, "left", `${posWidth - width2 / 2}px`);
const active2 = () => !isClosing(art) && !currentScope.closed && scope === currentScope && hover === current2 && current2.sourceActive() && art.controls?.thumbnails === current2.element;
if (!active2())
return;
const position = $progress.clientWidth * current2.percentage;
if (!(position > 0 && position < $progress.clientWidth))
return;
const styles = {
backgroundImage: `url(${image.src})`,
...thumbnailLayout(option.thumbnails, {
imageWidth: image.naturalWidth,
videoWidth: $video.videoWidth,
videoHeight: $video.videoHeight,
progressWidth: $progress.clientWidth,
position
})
};
for (const [key, value] of Object.entries(styles)) {
if (!active2())
return;
setStyle(current2.element, key, value);
}
}
art.on("setBar", async (type, percentage, event) => {
const $thumbnails = art.controls?.thumbnails;
eventSubscriptions(art)("setBar", (type, percentage, event) => {
const element = art.controls?.thumbnails;
const { url, scale } = option.thumbnails;
if (!$thumbnails || !url)
if (isClosing(art) || !element || !url || !(type === "hover" || type === "played" && event && isMobile))
return;
const isMobileDragging = type === "played" && event && isMobile;
if (type === "hover" || isMobileDragging) {
if (!image && !loding) {
loding = true;
image = await loadImg(url, scale);
loding = false;
}
if (!image)
if (control !== element) {
const elementScope = entryScope(element);
if (elementScope.closed)
return;
const width = $progress.clientWidth * percentage;
if (width > 0 && width < $progress.clientWidth) {
showThumbnails(width);
}
control = element;
elementScope.add(() => {
if (control === element) {
control = void 0;
reset();
}
});
}
hover = { percentage, element, sourceActive: captureSource(art) };
if (image) {
render();
} else if (!loading2) {
const currentScope = scope;
loading2 = true;
loadThumbnailImage(url, scale, currentScope).then((loaded) => {
if (scope !== currentScope || currentScope.closed)
return;
loading2 = false;
image = loaded;
render();
}).catch((error2) => {
if (scope === currentScope && !currentScope.closed) {
loading2 = false;
console.warn("ArtPlayer thumbnail load failed:", error2);
}
});
}
});
def(art, "thumbnails", {
get() {
return art.option.thumbnails;
},
set(thumbnails) {
if (thumbnails.url && !art.option.isLive) {
get: () => art.option.thumbnails,
set: (thumbnails) => {
if (!isClosing(art) && thumbnails.url && !art.option.isLive && !isClosing(art)) {
art.option.thumbnails = thumbnails;
reset();
}
@@ -5319,8 +5565,6 @@ function urlMix(art) {
const result = typeCallback.call(art, $video, newUrl, art);
void Promise.resolve(result).catch((error2) => failSource(operation, error2));
} else {
if (oldUrl)
URL.revokeObjectURL(oldUrl);
if (!operation.active())
return;
$video.src = newUrl;
+28
View File
@@ -0,0 +1,28 @@
export type CaptureMedia = CanvasImageSource & {
videoWidth: number
videoHeight: number
currentTime: number
}
export function drawFrame(canvas: HTMLCanvasElement, video: CaptureMedia): void {
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const context = canvas.getContext('2d')
if (!context)
throw new TypeError('Canvas 2D context is unavailable')
context.drawImage(video, 0, 0)
}
export function captureBlobUrl(canvas: HTMLCanvasElement, resolve: (url: string) => void, reject: (error: unknown) => void): void {
canvas.toBlob((blob) => {
try {
if (!blob)
throw new Error('Unable to encode screenshot blob')
// The public getBlobUrl caller owns this URL, including after destroy.
resolve(URL.createObjectURL(blob))
}
catch (error) {
reject(error)
}
})
}
@@ -1,4 +1,7 @@
import type { ControlHost } from '../types'
import { entryScope } from '../../component/resources'
import { isClosing } from '../../lifecycle/instance'
import { captureSource } from '../../source/operation'
import { isMobile } from '../../utils'
import { controlEvents } from '../resources'
import { getPosFromEvent, setCurrentTime } from './position'
@@ -7,17 +10,30 @@ export function installProgressInteractions(art: ControlHost, $control: HTMLDivE
const { $progress } = art.template
const $indicator = $control.querySelector('.art-progress-indicator')
const { on, proxy } = controlEvents(art, $control)
const scope = entryScope($control)
let action = 0
const capture = () => {
const sourceActive = captureSource(art)
return () => !scope.closed && !isClosing(art) && sourceActive()
}
const captureAction = () => {
const revision = ++action
const active = capture()
return () => revision === action && active()
}
if (!isMobile) {
let isDragging = false
let dragging: (() => boolean) | undefined
proxy($progress, 'click', (event) => {
if (event.target !== $indicator) {
setCurrentTime(art, event)
setCurrentTime(art, event, captureAction())
}
})
proxy($progress, 'mousemove', (event) => {
const active = capture()
const { percentage } = getPosFromEvent(art, event)
art.emit('setBar', 'hover', percentage, event)
if (active())
art.emit('setBar', 'hover', percentage, event)
})
proxy($progress, 'mouseleave', (event) => {
@@ -25,21 +41,26 @@ export function installProgressInteractions(art: ControlHost, $control: HTMLDivE
})
proxy($progress, 'mousedown', (event) => {
isDragging = event.button === 0
dragging = event.button === 0 ? captureAction() : undefined
})
on('document:mousemove', (event) => {
if (isDragging) {
const active = dragging
if (active?.()) {
const { second, percentage } = getPosFromEvent(art, event)
if (!active() || dragging !== active)
return
art.emit('setBar', 'played', percentage, event)
art.seek = second
if (active() && dragging === active)
art.seek = second
}
else {
dragging = undefined
}
})
on('document:mouseup', () => {
if (isDragging) {
isDragging = false
}
dragging = undefined
})
}
}
@@ -23,15 +23,21 @@ export function getPosFromEvent(art: ProgressPositionHost, event: ProgressEvent)
}
export function setCurrentTime(art: ProgressPositionHost, event: ProgressEvent, active: () => boolean = () => true) {
if (!active())
return
if (art.isRotate) {
const percentage = ((event as TouchEvent).touches[0]!.clientY - art.top) / art.height
const second = percentage * art.duration
if (!active())
return
art.emit('setBar', 'played', percentage, event)
if (active())
art.seek = second
}
else {
const { second, percentage } = getPosFromEvent(art, event)
if (!active())
return
art.emit('setBar', 'played', percentage, event)
if (active())
art.seek = second
+2 -1
View File
@@ -1,5 +1,6 @@
import type { ControlFactory, ControlOption } from './types'
import { appendElement } from '../component/dom'
import { silencePromise } from '../utils'
import { controlEvents } from './resources'
export default function screenshot(option: ControlOption): ControlFactory {
@@ -12,7 +13,7 @@ export default function screenshot(option: ControlOption): ControlFactory {
appendElement($control, icons.screenshot)
proxy($control, 'click', () => {
art.screenshot()
silencePromise(art.screenshot())
})
},
})
+146
View File
@@ -0,0 +1,146 @@
import ResourceScope from '../lifecycle/scope'
function requestImage(url: string, scale: number | undefined, owner: undefined): Promise<HTMLImageElement>
function requestImage(url: string, scale: number | undefined, owner: ResourceScope): Promise<HTMLImageElement | undefined>
function requestImage(url: string, scale: number | undefined, owner: ResourceScope | undefined): Promise<HTMLImageElement | undefined> {
return new Promise((resolve, reject) => {
const request = owner ? owner.child() : new ResourceScope()
let complete = false
let releaseBlob = () => {}
function fail(error: unknown): void {
if (complete)
return
complete = true
try {
request.dispose()
}
catch (cleanupError) {
console.warn('Additional image cleanup failure:', cleanupError)
}
try {
releaseBlob()
}
catch (cleanupError) {
console.warn('Additional image cleanup failure:', cleanupError)
}
reject(error)
}
function finish(image: HTMLImageElement): void {
if (complete || request.closed)
return
complete = true
try {
request.dispose()
resolve(image)
}
catch (error) {
try {
releaseBlob()
}
finally {
reject(error)
}
}
}
request.add(() => {
if (!complete) {
complete = true
try {
releaseBlob()
}
finally {
resolve(undefined)
}
}
})
if (request.closed)
return
function watch(image: HTMLImageElement, loaded: () => void): void {
image.onload = () => {
if (!complete && !request.closed) {
try {
loaded()
}
catch (error) {
fail(error)
}
}
}
image.onerror = () => fail(new Error(`Image load failed: ${url}`))
request.add(() => {
image.onload = null
image.onerror = null
if (!complete)
image.removeAttribute('src')
})
}
try {
const image = new Image()
watch(image, () => {
if (!scale || scale === 1) {
finish(image)
return
}
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = image.width * scale
canvas.height = image.height * scale
if (!context)
throw new TypeError('Canvas 2D context is unavailable')
if (complete || request.closed)
return
context.drawImage(image, 0, 0, canvas.width, canvas.height)
if (complete || request.closed)
return
canvas.toBlob((blob) => {
if (complete || request.closed)
return
try {
if (!blob)
throw new Error('Unable to encode thumbnail image')
const blobUrl = URL.createObjectURL(blob)
let revoked = false
const revoke = (): undefined => {
if (!revoked) {
revoked = true
URL.revokeObjectURL(blobUrl)
}
}
releaseBlob = owner ? owner.add(revoke) : revoke
if (complete || request.closed) {
releaseBlob()
return
}
const scaled = new Image()
watch(scaled, () => finish(scaled))
if (!request.closed)
scaled.src = blobUrl
}
catch (error) {
fail(error)
}
})
})
if (!request.closed)
image.src = url
}
catch (error) {
fail(error)
}
})
}
// Public callers own a scaled result's image.src object URL.
export function loadImg(url: string, scale?: number): Promise<HTMLImageElement> {
return requestImage(url, scale, undefined)
}
// Internal previews own their result until configuration replacement or destroy.
export function loadThumbnailImage(url: string, scale: number | undefined, owner: ResourceScope): Promise<HTMLImageElement | undefined> {
return requestImage(url, scale, owner)
}
@@ -0,0 +1,9 @@
const revisions = new WeakMap<object, number>()
export function positionRevision(art: object): number {
return revisions.get(art) || 0
}
export function advancePosition(art: object): void {
revisions.set(art, positionRevision(art) + 1)
}
@@ -1,5 +1,6 @@
import type { MediaHost } from '../media/hosts'
import type { Position } from '../media/playback'
import { advancePosition } from '../media/position-revision'
import { clamp, def } from '../utils'
export default function currentTimeMix(art: MediaHost<Pick<HTMLMediaElement, 'currentTime'>> & Pick<Position, 'duration'>): asserts art is typeof art & Position {
@@ -11,6 +12,7 @@ export default function currentTimeMix(art: MediaHost<Pick<HTMLMediaElement, 'cu
const parsed = Number.parseFloat(time as string)
if (Number.isNaN(parsed))
return
advancePosition(art)
$video.currentTime = clamp(parsed, 0, art.duration)
},
})
@@ -1,54 +0,0 @@
import { createElement, def, download, secondToTime } from '../utils'
export default function screenshotMix(art) {
const {
notice,
template: { $video },
} = art
const $canvas = createElement('canvas')
def(art, 'getDataURL', {
value: () =>
new Promise((resolve, reject) => {
try {
$canvas.width = $video.videoWidth
$canvas.height = $video.videoHeight
$canvas.getContext('2d').drawImage($video, 0, 0)
resolve($canvas.toDataURL('image/png'))
}
catch (err) {
notice.show = err
reject(err)
}
}),
})
def(art, 'getBlobUrl', {
value: () =>
new Promise((resolve, reject) => {
try {
$canvas.width = $video.videoWidth
$canvas.height = $video.videoHeight
$canvas.getContext('2d').drawImage($video, 0, 0)
$canvas.toBlob((blob) => {
resolve(URL.createObjectURL(blob))
})
}
catch (err) {
notice.show = err
reject(err)
}
}),
})
def(art, 'screenshot', {
value: async (name) => {
const dataUri = await art.getDataURL()
const fileName = name || `artplayer_${secondToTime($video.currentTime)}`
download(dataUri, `${fileName}.png`)
art.emit('screenshot', dataUri)
return dataUri
},
})
}
@@ -0,0 +1,65 @@
import type { CaptureMedia } from '../capture/frame'
import { captureBlobUrl, drawFrame } from '../capture/frame'
import { isClosing } from '../lifecycle/instance'
import { captureSource } from '../source/operation'
import { def, download, secondToTime } from '../utils'
export interface ScreenshotHost {
template: { $video: CaptureMedia }
notice: { set show(value: unknown) }
getDataURL: () => Promise<string>
emit: (name: 'screenshot', dataUri: string) => unknown
}
export default function screenshotMix(art: ScreenshotHost): void {
const { notice, template: { $video } } = art
const canvas = document.createElement('canvas')
function capture(blob: boolean): Promise<string> {
const active = captureSource(art)
// Draw synchronously as before; awaiting must not capture a later frame.
return new Promise<string>((resolve, reject) => {
const fail = (error: unknown) => {
try {
if (!isClosing(art) && active())
notice.show = error
}
catch (noticeError) {
reject(noticeError)
return
}
reject(error)
}
try {
drawFrame(canvas, $video)
if (blob) {
// Handle failures inside toBlob as well as synchronous canvas errors.
captureBlobUrl(canvas, resolve, fail)
}
else {
resolve(canvas.toDataURL('image/png'))
}
}
catch (error) {
fail(error)
}
})
}
def(art, 'getDataURL', { value: () => capture(false) })
def(art, 'getBlobUrl', { value: () => capture(true) })
def(art, 'screenshot', {
value: async (name?: string): Promise<string> => {
const active = captureSource(art)
const dataUri = await art.getDataURL()
if (!isClosing(art) && active()) {
const fileName = name || `artplayer_${secondToTime($video.currentTime)}`
if (!isClosing(art) && active())
download(dataUri, `${fileName}.png`)
if (!isClosing(art) && active())
art.emit('screenshot', dataUri)
}
return dataUri
},
})
}
@@ -1,84 +0,0 @@
import { def, isMobile, loadImg, setStyle } from '../utils'
export default function thumbnailsMix(art) {
const {
option,
template: { $progress, $video },
} = art
let timer = null
let loding = false
let image = null
function reset() {
clearTimeout(timer)
timer = null
loding = false
image = null
}
function showThumbnails(posWidth) {
const $thumbnails = art.controls?.thumbnails
if (!$thumbnails)
return
const { number, column, width, height, scale } = option.thumbnails
const width2 = width * scale || image.naturalWidth / column
const height2 = height * scale || width2 / ($video.videoWidth / $video.videoHeight)
const perWidth = $progress.clientWidth / number
const perIndex = Math.floor(posWidth / perWidth)
const yIndex = Math.ceil(perIndex / column) - 1
const xIndex = perIndex % column || column - 1
setStyle($thumbnails, 'backgroundImage', `url(${image.src})`)
setStyle($thumbnails, 'height', `${height2}px`)
setStyle($thumbnails, 'width', `${width2}px`)
setStyle($thumbnails, 'backgroundPosition', `-${xIndex * width2}px -${yIndex * height2}px`)
if (posWidth <= width2 / 2) {
setStyle($thumbnails, 'left', 0)
}
else if (posWidth > $progress.clientWidth - width2 / 2) {
setStyle($thumbnails, 'left', `${$progress.clientWidth - width2}px`)
}
else {
setStyle($thumbnails, 'left', `${posWidth - width2 / 2}px`)
}
}
art.on('setBar', async (type, percentage, event) => {
const $thumbnails = art.controls?.thumbnails
const { url, scale } = option.thumbnails
if (!$thumbnails || !url)
return
const isMobileDragging = type === 'played' && event && isMobile
if (type === 'hover' || isMobileDragging) {
if (!image && !loding) {
loding = true
image = await loadImg(url, scale)
loding = false
}
if (!image)
return
const width = $progress.clientWidth * percentage
if (width > 0 && width < $progress.clientWidth) {
showThumbnails(width)
}
}
})
def(art, 'thumbnails', {
get() {
return art.option.thumbnails
},
set(thumbnails) {
if (thumbnails.url && !art.option.isLive) {
art.option.thumbnails = thumbnails
reset()
}
},
})
}
@@ -0,0 +1,115 @@
import type { Thumbnails } from '../../types/option'
import type { SubscriptionHost } from '../component/resources'
import { entryScope } from '../component/resources'
import { eventSubscriptions } from '../events/subscriptions'
import { loadThumbnailImage } from '../image/load'
import { getScope, isClosing } from '../lifecycle/instance'
import { captureSource } from '../source/operation'
import { thumbnailLayout } from '../thumbnails/layout'
import { def, isMobile, setStyle } from '../utils'
interface ThumbnailEvents {
setBar: [type: string, percentage: number, event?: Event]
}
export interface ThumbnailsHost extends SubscriptionHost<ThumbnailEvents> {
option: { thumbnails: Thumbnails, isLive: boolean }
template: { $progress: HTMLElement, $video: { videoWidth: number, videoHeight: number } }
controls?: { thumbnails?: HTMLDivElement }
}
export default function thumbnailsMix(art: ThumbnailsHost): void {
const { option, template: { $progress, $video } } = art
let scope = getScope(art).child()
let image: HTMLImageElement | undefined
let loading = false
let control: HTMLDivElement | undefined
let hover: { percentage: number, element: HTMLDivElement, sourceActive: () => boolean } | undefined
function reset(): void {
const previous = scope
scope = getScope(art).child()
image = undefined
loading = false
hover = undefined
previous.dispose()
}
function render(): void {
const current = hover
const currentScope = scope
if (!current || !image)
return
const active = () => !isClosing(art) && !currentScope.closed && scope === currentScope
&& hover === current && current.sourceActive() && art.controls?.thumbnails === current.element
if (!active())
return
const position = $progress.clientWidth * current.percentage
if (!(position > 0 && position < $progress.clientWidth))
return
const styles = {
backgroundImage: `url(${image.src})`,
...thumbnailLayout(option.thumbnails, {
imageWidth: image.naturalWidth,
videoWidth: $video.videoWidth,
videoHeight: $video.videoHeight,
progressWidth: $progress.clientWidth,
position,
}),
}
for (const [key, value] of Object.entries(styles)) {
if (!active())
return
setStyle(current.element, key, value)
}
}
eventSubscriptions<ThumbnailEvents>(art)('setBar', (type, percentage, event) => {
const element = art.controls?.thumbnails
const { url, scale } = option.thumbnails
if (isClosing(art) || !element || !url || !(type === 'hover' || (type === 'played' && event && isMobile)))
return
if (control !== element) {
const elementScope = entryScope(element)
if (elementScope.closed)
return
control = element
elementScope.add(() => {
if (control === element) {
control = undefined
reset()
}
})
}
hover = { percentage, element, sourceActive: captureSource(art) }
if (image) {
render()
}
else if (!loading) {
const currentScope = scope
loading = true
loadThumbnailImage(url, scale, currentScope).then((loaded) => {
if (scope !== currentScope || currentScope.closed)
return
loading = false
image = loaded
render()
}).catch((error: unknown) => {
if (scope === currentScope && !currentScope.closed) {
loading = false
console.warn('ArtPlayer thumbnail load failed:', error)
}
})
}
})
def(art, 'thumbnails', {
get: () => art.option.thumbnails,
set: (thumbnails: Thumbnails) => {
if (!isClosing(art) && thumbnails.url && !art.option.isLive && !isClosing(art)) {
art.option.thumbnails = thumbnails
reset()
}
},
})
}
+1 -2
View File
@@ -50,8 +50,7 @@ export default function urlMix<Media extends SourceMedia, Host extends UrlHost<M
void Promise.resolve(result).catch(error => failSource(operation, error))
}
else {
if (oldUrl)
URL.revokeObjectURL(oldUrl)
// Media URLs are supplied by callers or adapters, which own revocation.
if (!operation.active())
return
$video.src = newUrl
@@ -0,0 +1,49 @@
import type { SwitchHost } from './types'
import { positionRevision } from '../media/position-revision'
// Native media clocks may round within a frame. Larger misses need one correction.
const POSITION_TOLERANCE = 0.05
export function positionRestoration(art: Pick<SwitchHost, 'currentTime' | 'template'>, target: number, active: () => boolean) {
let expected: number | undefined
let observedSeek = false
let corrected = false
let manual = false
let revision = positionRevision(art)
const manualPosition = () => manual || positionRevision(art) !== revision
const seeking = () => !!art.template?.$video?.seeking
return {
restore(): void {
if (!active() || manualPosition())
return
const previousRevision = positionRevision(art)
art.currentTime = target
revision = positionRevision(art)
// Nested public writes supersede the restoration's own setter call.
if (revision > previousRevision + 1)
manual = true
if (!active())
return
// Keep the actual clamped value accepted by the player setter.
expected = art.currentTime
if (!active() || manualPosition())
return
observedSeek = seeking()
},
manual(): void {
manual = true
},
ready(): boolean {
if (!active() || seeking())
return false
const missed = expected !== undefined && Math.abs(art.currentTime - expected) > POSITION_TOLERANCE
if (!manualPosition() && observedSeek && !corrected && expected !== undefined && missed) {
corrected = true
if (!active())
return false
art.currentTime = expected
}
return active() && !seeking()
},
}
}
+50 -26
View File
@@ -2,6 +2,7 @@ import type { SourceEvent, SourceListener, SwitchHost } from './types'
import { isClosing } from '../lifecycle/instance'
import { listenSource } from './listen'
import { assignUrl, beginSource } from './operation'
import { positionRestoration } from './restore-position'
export function switchSource(art: SwitchHost, url: string, currentTime: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
@@ -39,44 +40,67 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
return
const readiness = scope.child()
let canPlay = false
let resuming = false
let rateRestored = false
const position = positionRestoration(art, currentTime, active)
const resume = async () => {
art.aspectRatio = aspectRatio
if (!active())
return
if (playing) {
// The public play promise still rejects; this internal resume is best effort.
try {
await art.play()
}
catch {}
}
if (!active())
return
art.notice.show = ''
settle()
}
const resumeWhenReady = () => {
if (!active() || resuming || !canPlay)
return
try {
// canplay can arrive before the metadata restoration seek has settled.
if (art.template?.$video?.seeking)
return
if (!active())
return
if (!rateRestored) {
rateRestored = true
art.playbackRate = playbackRate
}
if (!position.ready() || !active() || resuming)
return
resuming = true
readiness.dispose()
void resume().catch(fail)
}
catch (error) {
fail(error)
}
}
const handlers = {
'video:error': fail,
'video:loadedmetadata': () => {
if (!active())
return
try {
art.currentTime = currentTime
position.restore()
}
catch (error) {
fail(error)
}
},
'video:canplay': () => {
if (!active())
return
// Detach readiness handlers before awaiting a possibly pending play.
readiness.dispose()
const resume = async () => {
art.playbackRate = playbackRate
if (!active())
return
art.aspectRatio = aspectRatio
if (!active())
return
if (playing) {
// The public play promise still rejects; this internal resume is best effort.
try {
await art.play()
}
catch {}
}
if (!active())
return
art.notice.show = ''
settle()
}
void resume().catch(fail)
canPlay = true
resumeWhenReady()
},
'video:seeked': resumeWhenReady,
'seek': position.manual,
}
const capture = scope.child()
const queued: [SourceEvent, unknown][] = []
@@ -101,7 +125,7 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
}
const dispatch = {} as Record<SourceEvent, SourceListener>
for (const name of names)
dispatch[name] = listenSource(readiness, art, name, handlers[name])
dispatch[name] = listenSource(readiness, art, name, handlers[name], name !== 'video:seeked')
for (const [name, event] of queued) {
if (!active() || readiness.closed)
break
+2 -1
View File
@@ -1,4 +1,4 @@
export type SourceEvent = 'video:error' | 'video:loadedmetadata' | 'video:canplay'
export type SourceEvent = 'video:error' | 'video:loadedmetadata' | 'video:canplay' | 'video:seeked' | 'seek'
export type SourceListener = (event?: unknown) => void
export interface SourceEvents {
@@ -28,6 +28,7 @@ export interface UrlHost<Media extends SourceMedia, Host> extends UrlTarget, Sou
}
export interface SwitchHost extends UrlTarget, SourceEvents {
template?: { $video?: { readonly seeking?: boolean } }
currentTime: number
readonly playing: boolean
aspectRatio: string
@@ -0,0 +1,32 @@
import type { Thumbnails } from '../../types/option'
export interface ThumbnailGeometry {
imageWidth: number
videoWidth: number
videoHeight: number
progressWidth: number
position: number
}
export function thumbnailLayout(option: Thumbnails, geometry: ThumbnailGeometry) {
const { number, column, width, height, scale } = option
const { imageWidth, videoWidth, videoHeight, progressWidth, position } = geometry
const columns = Number(column)
const previewWidth = Number(width) * Number(scale) || imageWidth / columns
const previewHeight = Number(height) * Number(scale) || previewWidth / (videoWidth / videoHeight)
const index = Math.floor(position / (progressWidth / Number(number)))
// Match the zero-based cells produced by artplayer-tool-thumbnail.
const row = Math.floor(index / columns)
const cell = index % columns
const left = position <= previewWidth / 2
? 0
: position > progressWidth - previewWidth / 2
? `${progressWidth - previewWidth}px`
: `${position - previewWidth / 2}px`
return {
height: `${previewHeight}px`,
width: `${previewWidth}px`,
backgroundPosition: `-${cell * previewWidth}px -${row * previewHeight}px`,
left,
}
}
+1 -41
View File
@@ -1,5 +1,6 @@
import { isMobile } from './compatibility'
export { loadImg } from '../image/load'
export { setStyleText } from '../style/inject'
/** @param {string} selector @param {ParentNode} parent */
@@ -112,47 +113,6 @@ export function getRect(el) {
return el.getBoundingClientRect()
}
export function loadImg(url, scale) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
if (!scale || scale === 1) {
resolve(img)
}
else {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width * scale
canvas.height = img.height * scale
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
canvas.toBlob((blob) => {
const blobUrl = URL.createObjectURL(blob)
const scaledImg = new Image()
scaledImg.onload = function () {
resolve(scaledImg)
}
scaledImg.onerror = function () {
URL.revokeObjectURL(blobUrl)
reject(new Error(`Image load failed: ${url}`))
}
scaledImg.src = blobUrl
})
}
}
img.onerror = function () {
reject(new Error(`Image load failed: ${url}`))
}
img.src = url
})
}
export function getComposedPath(event) {
if (event.composedPath)
return event.composedPath()