refactor(core): CORE-09 isolate source operations and settle cancelled switches

This commit is contained in:
Harvey Zhao committed 2026-09-11 02:34:26 +08:00
1 parent 874dac9f22
commit 44f4e083a9
29 files changed
+4512 -255

No files matched your search

+28
View File
@@ -318,6 +318,34 @@ finding, not a claim that a standalone debounce can know when its owner is destr
## Verification and maintenance
### Source operations (CORE-09)
`player/urlMix.ts` and `player/switchMix.ts` preserve the public descriptors.
`source/types.ts` defines minimal structural media/host capabilities;
`source/operation.ts` owns a current generation and its resource scope;
`source/listen.ts` owns individual media subscriptions; `source/switch.ts`
coordinates assignment, readiness, state restoration and Promise settlement.
A new URL assignment supersedes the prior operation, including direct `art.url`
writes. Superseded/destroyed switches fulfill with undefined; actual source errors
reject with their original value. Fulfillment does not prove the source became
active. Same-string URL calls remain no-ops. Internal resume rejection stays
handled while public play retains rejection. Synchronous proxy events are buffered
until assignment finishes, and operation cleanup does not remove user listeners.
customType still receives the real media object and player. Its arbitrary external
SDK work cannot be cancelled generically: adapters must own that cleanup. The core
guards deferred invocation, ignores obsolete returned failures, and cancels its own
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. Object URL ownership, other playback migration and reconnect
generations remain CORE-19/10/11 work.
`test/source.test.js` checks cancellation, reentry, synchronous events and cleanup;
`test/browser/source.spec.js` compares published/candidate real-media switching and
destruction and verifies candidate customType and resume-failure behavior.
Run `yarn test:unit` for shared published/workspace utility contracts and controlled
timers. `test/utils.test.js` also accepts `ARTPLAYER_TEST_CORE` to test an actual
UMD, legacy or ESM file. `test/types/utils-source.ts` checks source inference, receiver
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.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 o=(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 a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(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(o(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(a=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 a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=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 a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={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 o=(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 a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(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(o(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(a=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 a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=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 a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={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
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.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,o=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))n.call(e,r)&&s(t,r,e[r]);if(a)for(var r of a(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,a)=>{var n=t=>{try{s(r.next(t))}catch(e){a(e)}},i=t=>{try{s(r.throw(t))}catch(e){a(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(n,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.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 a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(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(o(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(a=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 a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=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 a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),u.exports);const f=p(m),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,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.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 n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(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(o(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(n=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 n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=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 n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
+251 -62
View File
@@ -1167,8 +1167,8 @@ function isClosing(owner) {
return state2.destroying || state2.scope.closed;
}
function ownContainer(owner, container, rollback) {
const current = containers.get(container);
if (current && current !== owner)
const current2 = containers.get(container);
if (current2 && current2 !== owner)
throw new Error("Cannot mount multiple instances on the same dom element");
const state2 = stateOf(owner);
state2.rollback = rollback;
@@ -3420,6 +3420,64 @@ function playingMix(art) {
}
});
}
const current = /* @__PURE__ */ new WeakMap();
const assignments = /* @__PURE__ */ new WeakMap();
function captureSource(owner) {
const operation = current.get(owner);
return () => current.get(owner) === operation && (!operation || operation.active());
}
function beginSource(owner) {
const previous = current.get(owner);
const scope = getScope(owner).child();
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?.scope.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 playMix(art) {
const {
i18n,
@@ -3430,11 +3488,21 @@ function playMix(art) {
} = art;
def(art, "play", {
async value() {
const active = captureSource(art);
const result = await $video.play();
notice.show = i18n.get("Play");
if (!active())
return result;
const message = i18n.get("Play");
if (!active())
return result;
notice.show = message;
if (!active())
return result;
art.emit("play");
if (option.mutex) {
for (let index = 0; index < instances2.length; index++) {
if (!active())
break;
const instance = instances2[index];
if (instance !== art) {
instance.pause();
@@ -3617,54 +3685,140 @@ function subtitleOffsetMix(art) {
}
});
}
function switchMix(art) {
function switchUrl(url, currentTime) {
return new Promise((resolve, reject) => {
if (url === art.url) {
resolve();
function listenSource(scope, art, name, callback, once = true) {
let fired = false;
let release;
const guarded = (event) => {
if (scope.closed || fired)
return;
if (once) {
fired = true;
release();
}
callback(event);
};
art[once ? "once" : "on"](name, guarded);
release = scope.add(() => {
art.off(name, guarded);
});
return guarded;
}
function switchSource(art, url, currentTime) {
return new Promise((resolve, reject) => {
if (isClosing(art) || url === art.url) {
resolve();
return;
}
const operation = beginSource(art);
const scope = operation.scope.child();
let settled = false;
const settle = (failed = false, error2) => {
if (settled)
return;
}
settled = true;
operation.onError = void 0;
operation.onAssigned = void 0;
scope.dispose();
if (failed)
reject(error2);
else
resolve();
if (failed)
operation.scope.dispose();
};
scope.add(() => {
settle();
});
const active = () => !settled && operation.active();
const fail = (error2) => settle(true, error2);
operation.onError = fail;
try {
const { playing, aspectRatio: aspectRatio2, playbackRate: playbackRate2 } = art;
art.pause();
art.url = url;
art.notice.show = "";
const handlers = {};
handlers.error = (error2) => {
art.off("video:canplay", handlers.canplay);
art.off("video:loadedmetadata", handlers.metadata);
reject(error2);
};
handlers.metadata = () => {
art.currentTime = currentTime;
};
handlers.canplay = async () => {
art.off("video:error", handlers.error);
art.playbackRate = playbackRate2;
art.aspectRatio = aspectRatio2;
if (playing) {
await silencePromise(art.play());
if (!active())
return;
const readiness = scope.child();
const handlers = {
"video:error": fail,
"video:loadedmetadata": () => {
if (!active())
return;
try {
art.currentTime = currentTime;
} catch (error2) {
fail(error2);
}
},
"video:canplay": () => {
if (!active())
return;
readiness.dispose();
const resume = async () => {
art.playbackRate = playbackRate2;
if (!active())
return;
art.aspectRatio = aspectRatio2;
if (!active())
return;
if (playing) {
try {
await art.play();
} catch {
}
}
if (!active())
return;
art.notice.show = "";
settle();
};
void resume().catch(fail);
}
art.notice.show = "";
resolve();
};
art.once("video:error", handlers.error);
art.once("video:loadedmetadata", handlers.metadata);
art.once("video:canplay", handlers.canplay);
});
}
def(art, "switchQuality", {
value: (url) => {
return switchUrl(url, art.currentTime);
const capture = scope.child();
const queued = [];
const names = Object.keys(handlers);
for (const name of names) {
listenSource(capture, art, name, (event) => {
if (operation.acceptingEvents)
queued.push([name, event]);
}, false);
}
assignUrl(art, operation, url);
if (!active())
return;
art.notice.show = "";
if (!active())
return;
const activate = () => {
capture.dispose();
if (!url) {
settle();
return;
}
const dispatch = {};
for (const name of names)
dispatch[name] = listenSource(readiness, art, name, handlers[name]);
for (const [name, event] of queued) {
if (!active() || readiness.closed)
break;
dispatch[name](event);
}
queued.length = 0;
};
if (operation.assigned)
activate();
else
operation.onAssigned = activate;
} catch (error2) {
fail(error2);
}
});
def(art, "switchUrl", {
value: (url) => {
return switchUrl(url, 0);
}
});
def(art, "switch", {
set: art.switchUrl
});
}
function switchMix(art) {
const switchUrl = (url) => switchSource(art, url, 0);
def(art, "switchQuality", { value: (url) => switchSource(art, url, art.currentTime) });
def(art, "switchUrl", { value: switchUrl });
def(art, "switch", { set: switchUrl });
}
function themeMix(art) {
def(art, "theme", {
@@ -3767,40 +3921,75 @@ function typeMix(art) {
});
}
function urlMix(art) {
const {
option,
template: { $video }
} = art;
const { option, template: { $video } } = art;
def(art, "url", {
get() {
return $video.src;
},
async set(newUrl) {
if (newUrl) {
if (isClosing(art))
return;
const operation = takeAssignment(art);
operation.acceptingEvents = false;
try {
if (!newUrl) {
if (await wait(operation.scope) && operation.active())
art.loading.show = true;
return;
}
const oldUrl = art.url;
const typeName = option.type || getExt(newUrl);
const typeCallback = option.customType[typeName];
if (typeName && typeCallback) {
if (!await wait(getScope(art)) || getScope(art).closed)
if (!await wait(operation.scope) || !operation.active())
return;
art.loading.show = true;
typeCallback.call(art, $video, newUrl, art);
} else {
URL.revokeObjectURL(oldUrl);
$video.src = newUrl;
}
if (!operation.active())
return;
const capture = operation.scope.child();
let ready = false;
let failed = false;
listenSource(capture, art, "video:canplay", () => {
ready = !failed;
});
listenSource(capture, art, "video:error", () => {
failed = true;
});
try {
operation.acceptingEvents = true;
if (typeName && typeCallback) {
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;
}
} finally {
capture.dispose();
}
if (!operation.active())
return;
if (oldUrl !== art.url) {
art.option.url = newUrl;
if (art.isReady && oldUrl) {
art.once("video:canplay", () => {
art.emit("restart", newUrl);
});
if (operation.active() && art.isReady && oldUrl) {
const restart = () => {
if (operation.active())
art.emit("restart", newUrl);
};
if (ready)
restart();
else if (!failed)
listenSource(operation.scope, art, "video:canplay", restart);
}
}
} else {
if (!await wait(getScope(art)) || getScope(art).closed)
return;
art.loading.show = true;
} catch (error2) {
failSource(operation, error2);
} finally {
finishAssignment(operation);
}
}
});
+12 -1
View File
@@ -1,5 +1,6 @@
import type { PlayHost } from '../media/hosts'
import type { PlaybackMethods } from '../media/types'
import { captureSource } from '../source/operation'
import { def } from '../utils'
export default function playMix<Media extends Pick<PlaybackMethods, 'play'>>(art: PlayHost<Media>): asserts art is PlayHost<Media> & { play: () => Promise<Awaited<ReturnType<Media['play']>>> } {
@@ -13,12 +14,22 @@ export default function playMix<Media extends Pick<PlaybackMethods, 'play'>>(art
def(art, 'play', {
async value() {
const active = captureSource(art)
const result = await $video.play()
notice.show = i18n.get('Play')
if (!active())
return result
const message = i18n.get('Play')
if (!active())
return result
notice.show = message
if (!active())
return result
art.emit('play')
if (option.mutex) {
for (let index = 0; index < instances.length; index++) {
if (!active())
break
const instance = instances[index]!
if (instance !== art) {
instance.pause()
@@ -1,63 +0,0 @@
import { def, silencePromise } from '../utils'
export default function switchMix(art) {
function switchUrl(url, currentTime) {
return new Promise((resolve, reject) => {
if (url === art.url) {
resolve()
return
}
const { playing, aspectRatio, playbackRate } = art
art.pause()
art.url = url
art.notice.show = ''
const handlers = {}
handlers.error = (error) => {
art.off('video:canplay', handlers.canplay)
art.off('video:loadedmetadata', handlers.metadata)
reject(error)
}
handlers.metadata = () => {
art.currentTime = currentTime
}
handlers.canplay = async () => {
art.off('video:error', handlers.error)
art.playbackRate = playbackRate
art.aspectRatio = aspectRatio
if (playing) {
await silencePromise(art.play())
}
art.notice.show = ''
resolve()
}
art.once('video:error', handlers.error)
art.once('video:loadedmetadata', handlers.metadata)
art.once('video:canplay', handlers.canplay)
})
}
def(art, 'switchQuality', {
value: (url) => {
return switchUrl(url, art.currentTime)
},
})
def(art, 'switchUrl', {
value: (url) => {
return switchUrl(url, 0)
},
})
def(art, 'switch', {
set: art.switchUrl,
})
}
@@ -0,0 +1,10 @@
import type { SwitchHost, SwitchMethods } from '../source/types'
import { switchSource } from '../source/switch'
import { def } from '../utils'
export default function switchMix(art: SwitchHost): asserts art is SwitchHost & SwitchMethods {
const switchUrl = (url: string) => switchSource(art, url, 0)
def(art, 'switchQuality', { value: (url: string) => switchSource(art, url, art.currentTime) })
def(art, 'switchUrl', { value: switchUrl })
def(art, 'switch', { set: switchUrl })
}
-48
View File
@@ -1,48 +0,0 @@
import { getScope } from '../lifecycle/instance'
import { wait } from '../lifecycle/resources'
import { def, getExt } from '../utils'
export default function urlMix(art) {
const {
option,
template: { $video },
} = art
def(art, 'url', {
get() {
return $video.src
},
async set(newUrl) {
if (newUrl) {
const oldUrl = art.url
const typeName = option.type || getExt(newUrl)
const typeCallback = option.customType[typeName]
if (typeName && typeCallback) {
if (!await wait(getScope(art)) || getScope(art).closed)
return
art.loading.show = true
typeCallback.call(art, $video, newUrl, art)
}
else {
URL.revokeObjectURL(oldUrl)
$video.src = newUrl
}
if (oldUrl !== art.url) {
art.option.url = newUrl
if (art.isReady && oldUrl) {
art.once('video:canplay', () => {
art.emit('restart', newUrl)
})
}
}
}
else {
if (!await wait(getScope(art)) || getScope(art).closed)
return
art.loading.show = true
}
},
})
}
+87
View File
@@ -0,0 +1,87 @@
import type { SourceMedia, UrlHost } from '../source/types'
import { isClosing } from '../lifecycle/instance'
import { wait } from '../lifecycle/resources'
import { listenSource } from '../source/listen'
import { failSource, finishAssignment, takeAssignment } from '../source/operation'
import { def, getExt } from '../utils'
export default function urlMix<Media extends SourceMedia, Host extends UrlHost<Media, Host>>(art: Host): void {
const { option, template: { $video } } = art
def(art, 'url', {
get() {
return $video.src
},
async set(newUrl: string) {
if (isClosing(art))
return
const operation = takeAssignment(art)
operation.acceptingEvents = false
try {
if (!newUrl) {
if (await wait(operation.scope) && operation.active())
art.loading.show = true
return
}
const oldUrl = art.url
const typeName = option.type || getExt(newUrl)
const typeCallback = option.customType[typeName]
if (typeName && typeCallback) {
if (!await wait(operation.scope) || !operation.active())
return
art.loading.show = true
}
if (!operation.active())
return
// Proxies can synchronously dispatch readiness from their src setter.
const capture = operation.scope.child()
let ready = false
let failed = false
listenSource(capture, art, 'video:canplay', () => {
ready = !failed
})
listenSource(capture, art, 'video:error', () => {
failed = true
})
try {
operation.acceptingEvents = true
if (typeName && typeCallback) {
const result = typeCallback.call(art, $video, newUrl, art)
// Preserve immediate customType invocation semantics while owning rejections.
void Promise.resolve(result).catch(error => failSource(operation, error))
}
else {
if (oldUrl)
URL.revokeObjectURL(oldUrl)
if (!operation.active())
return
$video.src = newUrl
}
}
finally {
capture.dispose()
}
if (!operation.active())
return
if (oldUrl !== art.url) {
art.option.url = newUrl
if (operation.active() && art.isReady && oldUrl) {
const restart = () => {
if (operation.active())
art.emit('restart', newUrl)
}
if (ready)
restart()
else if (!failed)
listenSource(operation.scope, art, 'video:canplay', restart)
}
}
}
catch (error) {
failSource(operation, error)
}
finally {
finishAssignment(operation)
}
},
})
}
+21
View File
@@ -0,0 +1,21 @@
import type ResourceScope from '../lifecycle/scope'
import type { SourceEvent, SourceEvents, SourceListener } from './types'
export function listenSource(scope: ResourceScope, art: SourceEvents, name: SourceEvent, callback: SourceListener, once = true): SourceListener {
let fired = false
let release: () => void
const guarded: SourceListener = (event) => {
if (scope.closed || fired)
return
if (once) {
fired = true
release()
}
callback(event)
}
art[once ? 'once' : 'on'](name, guarded)
release = scope.add(() => {
art.off(name, guarded)
})
return guarded
}
@@ -0,0 +1,79 @@
import type ResourceScope from '../lifecycle/scope'
import type { UrlTarget } from './types'
import { getScope, isClosing } from '../lifecycle/instance'
export interface SourceOperation {
scope: ResourceScope
assigned: boolean
acceptingEvents: boolean
onAssigned?: () => void
onError?: (error: unknown) => void
active: () => boolean
}
const current = new WeakMap<object, SourceOperation>()
const assignments = new WeakMap<object, SourceOperation>()
export function captureSource(owner: object): () => boolean {
const operation = current.get(owner)
return () => current.get(owner) === operation && (!operation || operation.active())
}
export function beginSource(owner: object): SourceOperation {
const previous = current.get(owner)
const scope = getScope(owner).child()
const operation: SourceOperation = {
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 = undefined
operation.onError = undefined
})
previous?.scope.dispose()
return operation
}
export function takeAssignment(owner: object): SourceOperation {
const operation = assignments.get(owner)
assignments.delete(owner)
return operation || beginSource(owner)
}
export function finishAssignment(operation: SourceOperation): void {
operation.assigned = true
const callback = operation.onAssigned
operation.onAssigned = undefined
if (operation.active())
callback?.()
}
export function assignUrl(owner: UrlTarget, operation: SourceOperation, url: string): void {
assignments.set(owner, operation)
operation.acceptingEvents = true
try {
owner.url = url
}
finally {
// Also support a host-provided URL setter without a core URL mixin.
if (assignments.get(owner) === operation) {
assignments.delete(owner)
finishAssignment(operation)
}
}
}
export function failSource(operation: SourceOperation, error: unknown): void {
if (!operation.active())
return
if (operation.onError)
operation.onError(error)
else
console.warn('Failed to initialize ArtPlayer source:', error)
operation.scope.dispose()
}
+121
View File
@@ -0,0 +1,121 @@
import type { SourceEvent, SourceListener, SwitchHost } from './types'
import { isClosing } from '../lifecycle/instance'
import { listenSource } from './listen'
import { assignUrl, beginSource } from './operation'
export function switchSource(art: SwitchHost, url: string, currentTime: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (isClosing(art) || url === art.url) {
resolve()
return
}
const operation = beginSource(art)
const scope = operation.scope.child()
let settled = false
const settle = (failed = false, error?: unknown) => {
if (settled)
return
settled = true
operation.onError = undefined
operation.onAssigned = undefined
scope.dispose()
if (failed)
reject(error)
else
resolve()
if (failed)
operation.scope.dispose()
}
scope.add(() => {
settle()
})
const active = () => !settled && operation.active()
const fail = (error: unknown) => settle(true, error)
operation.onError = fail
try {
const { playing, aspectRatio, playbackRate } = art
art.pause()
if (!active())
return
const readiness = scope.child()
const handlers = {
'video:error': fail,
'video:loadedmetadata': () => {
if (!active())
return
try {
art.currentTime = currentTime
}
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)
},
}
const capture = scope.child()
const queued: [SourceEvent, unknown][] = []
const names = Object.keys(handlers) as SourceEvent[]
for (const name of names) {
listenSource(capture, art, name, (event) => {
if (operation.acceptingEvents)
queued.push([name, event])
}, false)
}
assignUrl(art, operation, url)
if (!active())
return
art.notice.show = ''
if (!active())
return
const activate = () => {
capture.dispose()
if (!url) {
settle()
return
}
const dispatch = {} as Record<SourceEvent, SourceListener>
for (const name of names)
dispatch[name] = listenSource(readiness, art, name, handlers[name])
for (const [name, event] of queued) {
if (!active() || readiness.closed)
break
dispatch[name](event)
}
queued.length = 0
}
if (operation.assigned)
activate()
else
operation.onAssigned = activate
}
catch (error) {
fail(error)
}
})
}
+44
View File
@@ -0,0 +1,44 @@
export type SourceEvent = 'video:error' | 'video:loadedmetadata' | 'video:canplay'
export type SourceListener = (event?: unknown) => void
export interface SourceEvents {
on: (name: SourceEvent, callback: SourceListener) => unknown
once: (name: SourceEvent, callback: SourceListener) => unknown
off: (name: SourceEvent, callback: SourceListener) => unknown
}
export interface SourceMedia {
src: string | null
}
export interface UrlTarget {
url: string | null
}
export interface UrlHost<Media extends SourceMedia, Host> extends UrlTarget, SourceEvents {
template: { $video: Media }
option: {
url: string
type: string
customType: Record<string, ((this: Host, video: Media, url: string, art: Host) => unknown) | undefined>
}
loading: { set show(value: boolean) }
isReady: boolean
emit: (name: 'restart', url: string) => unknown
}
export interface SwitchHost extends UrlTarget, SourceEvents {
currentTime: number
readonly playing: boolean
aspectRatio: string
playbackRate: number
pause: () => unknown
play: () => unknown
notice: { set show(value: string) }
}
export interface SwitchMethods {
switchUrl: (url: string) => Promise<void>
switchQuality: (url: string) => Promise<void>
set switch(url: string)
}