mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(core): CORE-04 scope initialization and make destruction reentrant-safe
This commit is contained in:
1 parent
3e38dd0d19
commit
4eec8112fa
32 files changed
+2779
-189
No files matched your search
@@ -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 a=(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"===a(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(a(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 a(t){return t.constructor?t.constructor.name:null}function o(t,e){var a=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,a),i(t,e,a),function(t,e,a){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(a.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=a.slice();c.push(r),n(s,l,c),i(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(a.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=a.slice();d.push(s),n(l,c,d),i(l,c,d),o(l,c,d)})}}(t,e,a),t}function n(t,e,a){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(a.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function i(t,e,a){if("function"===r(e)){var o=e(t,r(t),a);if(!0!==o){var n=r(o);throw"string"===n?new Error(o):"error"===n?o:new Error("[Validator Error]: The scheme for '".concat(a.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(a),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
|
||||
@@ -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,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
|
||||
+304
-55
@@ -583,14 +583,14 @@ function sleep(ms = 0) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
function debounce(func, duration) {
|
||||
let timeout;
|
||||
let timeout2;
|
||||
return function(...args) {
|
||||
const effect = () => {
|
||||
timeout = void 0;
|
||||
timeout2 = void 0;
|
||||
return func.apply(this, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(effect, duration);
|
||||
clearTimeout(timeout2);
|
||||
timeout2 = setTimeout(effect, duration);
|
||||
};
|
||||
}
|
||||
function throttle(func, duration) {
|
||||
@@ -1089,6 +1089,191 @@ class Contextmenu extends Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
class ResourceCleanupError extends Error {
|
||||
constructor(errors) {
|
||||
super("Failed to release ArtPlayer resources");
|
||||
this.errors = errors;
|
||||
this.name = "ResourceCleanupError";
|
||||
}
|
||||
}
|
||||
class ResourceScope {
|
||||
constructor() {
|
||||
this.cleanups = /* @__PURE__ */ new Set();
|
||||
this.disposed = false;
|
||||
}
|
||||
get closed() {
|
||||
return this.disposed;
|
||||
}
|
||||
add(cleanup) {
|
||||
let active = true;
|
||||
const release = () => {
|
||||
if (!active)
|
||||
return;
|
||||
active = false;
|
||||
this.cleanups.delete(release);
|
||||
cleanup();
|
||||
};
|
||||
if (this.closed)
|
||||
release();
|
||||
else
|
||||
this.cleanups.add(release);
|
||||
return release;
|
||||
}
|
||||
child() {
|
||||
const child = new ResourceScope();
|
||||
const release = this.add(() => {
|
||||
child.dispose();
|
||||
});
|
||||
child.add(() => {
|
||||
release();
|
||||
});
|
||||
return child;
|
||||
}
|
||||
dispose() {
|
||||
if (this.closed)
|
||||
return;
|
||||
this.disposed = true;
|
||||
const errors = [];
|
||||
for (const release of Array.from(this.cleanups).reverse()) {
|
||||
try {
|
||||
release();
|
||||
} catch (error2) {
|
||||
if (error2 instanceof ResourceCleanupError)
|
||||
errors.push(...error2.errors);
|
||||
else
|
||||
errors.push(error2);
|
||||
}
|
||||
}
|
||||
if (errors.length)
|
||||
throw new ResourceCleanupError(errors);
|
||||
}
|
||||
}
|
||||
const states = /* @__PURE__ */ new WeakMap();
|
||||
const containers = /* @__PURE__ */ new WeakMap();
|
||||
function beginLifecycle(owner) {
|
||||
states.set(owner, { scope: new ResourceScope(), destroying: false });
|
||||
}
|
||||
function stateOf(owner) {
|
||||
const state2 = states.get(owner);
|
||||
if (!state2)
|
||||
throw new Error("ArtPlayer lifecycle has not been initialized");
|
||||
return state2;
|
||||
}
|
||||
function getScope(owner) {
|
||||
return stateOf(owner).scope;
|
||||
}
|
||||
function ownContainer(owner, container, rollback) {
|
||||
const current = containers.get(container);
|
||||
if (current && current !== owner)
|
||||
throw new Error("Cannot mount multiple instances on the same dom element");
|
||||
const state2 = stateOf(owner);
|
||||
state2.rollback = rollback;
|
||||
containers.set(container, owner);
|
||||
state2.releaseContainer = () => {
|
||||
if (containers.get(container) === owner)
|
||||
containers.delete(container);
|
||||
};
|
||||
}
|
||||
function finishLifecycle(owner) {
|
||||
const state2 = stateOf(owner);
|
||||
state2.rollback = void 0;
|
||||
return !state2.scope.closed;
|
||||
}
|
||||
function destroyInstance(owner, instances2, removeHtml, removeSource, failed = false) {
|
||||
const state2 = stateOf(owner);
|
||||
if (state2.destroying || state2.scope.closed)
|
||||
return;
|
||||
state2.destroying = true;
|
||||
const errors = [];
|
||||
const attempt = (cleanup) => {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (error2) {
|
||||
errors.push(...error2 instanceof ResourceCleanupError ? error2.errors : [error2]);
|
||||
}
|
||||
};
|
||||
if (removeSource && owner.template?.$video)
|
||||
attempt(() => owner.reset());
|
||||
attempt(() => state2.scope.dispose());
|
||||
attempt(() => owner.template?.destroy(removeHtml));
|
||||
const index = instances2.indexOf(owner);
|
||||
if (index !== -1)
|
||||
instances2.splice(index, 1);
|
||||
owner.isDestroy = true;
|
||||
if (!failed) {
|
||||
state2.releaseContainer?.();
|
||||
state2.releaseContainer = void 0;
|
||||
}
|
||||
attempt(() => owner.emit("destroy"));
|
||||
if (failed && state2.rollback)
|
||||
attempt(state2.rollback);
|
||||
state2.rollback = void 0;
|
||||
state2.releaseContainer?.();
|
||||
state2.releaseContainer = void 0;
|
||||
for (const error2 of errors.slice(1))
|
||||
console.warn("Additional ArtPlayer cleanup failure:", error2);
|
||||
if (errors.length)
|
||||
throw errors[0];
|
||||
}
|
||||
function timeout(scope, callback, delay) {
|
||||
if (scope.closed)
|
||||
return () => {
|
||||
};
|
||||
let pending = true;
|
||||
let release = () => {
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
if (!pending)
|
||||
return;
|
||||
release();
|
||||
if (!scope.closed)
|
||||
callback();
|
||||
}, delay);
|
||||
release = scope.add(() => {
|
||||
pending = false;
|
||||
clearTimeout(timer);
|
||||
});
|
||||
return release;
|
||||
}
|
||||
function animationFrame(scope, callback) {
|
||||
if (scope.closed)
|
||||
return () => {
|
||||
};
|
||||
let pending = true;
|
||||
let release = () => {
|
||||
};
|
||||
const frame = requestAnimationFrame((time2) => {
|
||||
if (!pending)
|
||||
return;
|
||||
release();
|
||||
if (!scope.closed)
|
||||
callback(time2);
|
||||
});
|
||||
release = scope.add(() => {
|
||||
pending = false;
|
||||
cancelAnimationFrame(frame);
|
||||
});
|
||||
return release;
|
||||
}
|
||||
function wait(scope, delay = 0) {
|
||||
if (scope.closed)
|
||||
return Promise.resolve(false);
|
||||
return new Promise((resolve) => {
|
||||
let completed = false;
|
||||
let release = () => {
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
completed = true;
|
||||
release();
|
||||
resolve(true);
|
||||
}, delay);
|
||||
release = scope.add(() => {
|
||||
clearTimeout(timer);
|
||||
if (!completed)
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
function airplay$1(option) {
|
||||
return (art) => ({
|
||||
...option,
|
||||
@@ -1576,7 +1761,9 @@ class Control extends Component {
|
||||
);
|
||||
}
|
||||
if (option.quality.length) {
|
||||
sleep().then(() => {
|
||||
wait(getScope(this.art)).then((active) => {
|
||||
if (!active || getScope(this.art).closed)
|
||||
return;
|
||||
this.art.quality = option.quality;
|
||||
});
|
||||
}
|
||||
@@ -1927,7 +2114,13 @@ function resizeInit(art, events) {
|
||||
art.aspectRatio = aspectRatio2;
|
||||
notice.show = "";
|
||||
});
|
||||
const resizeFn = debounce(() => art.emit("resize"), constructor.RESIZE_TIME);
|
||||
const scope = getScope(art);
|
||||
let cancel = () => {
|
||||
};
|
||||
const resizeFn = () => {
|
||||
cancel();
|
||||
cancel = timeout(scope, () => art.emit("resize"), constructor.RESIZE_TIME);
|
||||
};
|
||||
art.on("window:orientationchange", () => resizeFn());
|
||||
art.on("window:resize", () => resizeFn());
|
||||
if (screen && screen.orientation && screen.orientation.onchange) {
|
||||
@@ -1936,17 +2129,19 @@ function resizeInit(art, events) {
|
||||
}
|
||||
function updateInit(art) {
|
||||
if (art.constructor.USE_RAF) {
|
||||
let timer = null;
|
||||
const scope = getScope(art);
|
||||
let cancel = () => {
|
||||
};
|
||||
(function update() {
|
||||
if (art.playing) {
|
||||
art.emit("raf");
|
||||
}
|
||||
if (!art.isDestroy) {
|
||||
timer = requestAnimationFrame(update);
|
||||
cancel = animationFrame(scope, update);
|
||||
}
|
||||
})();
|
||||
art.on("destroy", () => {
|
||||
cancelAnimationFrame(timer);
|
||||
cancel();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1966,9 +2161,13 @@ function viewInit(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
const scopes = /* @__PURE__ */ new WeakMap();
|
||||
class Events {
|
||||
constructor(art) {
|
||||
this.destroyEvents = /* @__PURE__ */ new Set();
|
||||
const scope = getScope(art);
|
||||
scopes.set(this, scope);
|
||||
scope.add(() => this.destroy());
|
||||
this.proxy = this.proxy.bind(this);
|
||||
this.hover = this.hover.bind(this);
|
||||
clickInit(art, this);
|
||||
@@ -1984,6 +2183,9 @@ class Events {
|
||||
if (Array.isArray(name)) {
|
||||
return name.map((item) => this.proxy(target, item, callback, option));
|
||||
}
|
||||
if (scopes.get(this).closed)
|
||||
return () => {
|
||||
};
|
||||
target.addEventListener(name, callback, option);
|
||||
const destroy = () => target.removeEventListener(name, callback, option);
|
||||
this.destroyEvents.add(destroy);
|
||||
@@ -2237,9 +2439,11 @@ class Info extends Component {
|
||||
proxy($infoClose, "click", () => {
|
||||
this.show = false;
|
||||
});
|
||||
let timer = null;
|
||||
let cancel = () => {
|
||||
};
|
||||
const $types = queryAll("[data-video]", $infoPanel) || [];
|
||||
this.art.on("destroy", () => clearTimeout(timer));
|
||||
this.art.on("destroy", () => cancel());
|
||||
const scope = getScope(this.art);
|
||||
function loop() {
|
||||
for (let index = 0; index < $types.length; index++) {
|
||||
const item = $types[index];
|
||||
@@ -2249,7 +2453,7 @@ class Info extends Component {
|
||||
item.textContent = textContent;
|
||||
}
|
||||
}
|
||||
timer = setTimeout(loop, constructor.INFO_LOOP_TIME);
|
||||
cancel = timeout(scope, loop, constructor.INFO_LOOP_TIME);
|
||||
}
|
||||
loop();
|
||||
}
|
||||
@@ -2294,6 +2498,7 @@ class Notice {
|
||||
constructor(art) {
|
||||
this.art = art;
|
||||
this.timer = null;
|
||||
getScope(art).add(() => this.destroy());
|
||||
art.on("destroy", () => this.destroy());
|
||||
}
|
||||
destroy() {
|
||||
@@ -2303,6 +2508,8 @@ class Notice {
|
||||
}
|
||||
}
|
||||
set show(msg) {
|
||||
if (getScope(this.art).closed)
|
||||
return;
|
||||
const {
|
||||
constructor,
|
||||
template: { $player, $noticeInner }
|
||||
@@ -2521,7 +2728,8 @@ function eventInit(art) {
|
||||
});
|
||||
art.on("video:error", async (error2) => {
|
||||
if (reconnectTime < constructor.RECONNECT_TIME_MAX) {
|
||||
await sleep(constructor.RECONNECT_SLEEP_TIME);
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return;
|
||||
reconnectTime += 1;
|
||||
art.url = option.url;
|
||||
notice.show = `${i18n.get("Reconnect")}: ${reconnectTime}`;
|
||||
@@ -2531,7 +2739,8 @@ function eventInit(art) {
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
addClass($player, "art-error");
|
||||
await sleep(constructor.RECONNECT_SLEEP_TIME);
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return;
|
||||
notice.show = i18n.get("Video Load Failed");
|
||||
}
|
||||
});
|
||||
@@ -3288,12 +3497,12 @@ function seekMix(art) {
|
||||
});
|
||||
}
|
||||
function stateMix(art) {
|
||||
const states = ["mini", "pip", "fullscreen", "fullscreenWeb"];
|
||||
const states2 = ["mini", "pip", "fullscreen", "fullscreenWeb"];
|
||||
def(art, "state", {
|
||||
get: () => states.find((name) => art[name]) || "standard",
|
||||
get: () => states2.find((name) => art[name]) || "standard",
|
||||
set(name) {
|
||||
for (let index = 0; index < states.length; index++) {
|
||||
const prop = states[index];
|
||||
for (let index = 0; index < states2.length; index++) {
|
||||
const prop = states2[index];
|
||||
if (prop !== name && art[prop]) {
|
||||
art[prop] = false;
|
||||
}
|
||||
@@ -3490,7 +3699,8 @@ function urlMix(art) {
|
||||
const typeName = option.type || getExt(newUrl);
|
||||
const typeCallback = option.customType[typeName];
|
||||
if (typeName && typeCallback) {
|
||||
await sleep();
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return;
|
||||
art.loading.show = true;
|
||||
typeCallback.call(art, $video, newUrl, art);
|
||||
} else {
|
||||
@@ -3506,7 +3716,8 @@ function urlMix(art) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await sleep();
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return;
|
||||
art.loading.show = true;
|
||||
}
|
||||
}
|
||||
@@ -4438,7 +4649,7 @@ class Setting extends Component {
|
||||
append($panel, $item);
|
||||
}
|
||||
if (item.mounted) {
|
||||
setTimeout(() => item.mounted.call(this.art, item.$item, item), 0);
|
||||
timeout(getScope(this.art), () => item.mounted.call(this.art, item.$item, item), 0);
|
||||
}
|
||||
}
|
||||
render(option = this.option) {
|
||||
@@ -4642,6 +4853,37 @@ class Subtitle extends Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
function captureTemplate(container) {
|
||||
const snapshots = [];
|
||||
function capture(node) {
|
||||
const children = Array.from(node.childNodes);
|
||||
snapshots.push({
|
||||
node,
|
||||
children,
|
||||
attributes: node.nodeType === 1 ? Array.from(node.attributes, (attr) => [attr.name, attr.value]) : void 0,
|
||||
value: node.nodeValue
|
||||
});
|
||||
children.forEach(capture);
|
||||
}
|
||||
capture(container);
|
||||
return () => {
|
||||
for (const { node, children, attributes, value } of snapshots) {
|
||||
while (node.firstChild)
|
||||
node.removeChild(node.firstChild);
|
||||
for (const child of children)
|
||||
node.appendChild(child);
|
||||
if (attributes) {
|
||||
const element = node;
|
||||
for (const attr of Array.from(element.attributes))
|
||||
element.removeAttribute(attr.name);
|
||||
for (const [name, content] of attributes)
|
||||
element.setAttribute(name, content);
|
||||
} else {
|
||||
node.nodeValue = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
class Template {
|
||||
constructor(art) {
|
||||
this.art = art;
|
||||
@@ -4660,6 +4902,7 @@ class Template {
|
||||
"Cannot mount multiple instances on the same dom element"
|
||||
);
|
||||
this.query = this.query.bind(this);
|
||||
ownContainer(art, this.$container, captureTemplate(this.$container));
|
||||
this.$container.dataset.artId = art.id;
|
||||
this.init();
|
||||
}
|
||||
@@ -4851,34 +5094,47 @@ class Artplayer extends Emitter {
|
||||
this.isInput = false;
|
||||
this.isRotate = false;
|
||||
this.isDestroy = false;
|
||||
this.template = new Template(this);
|
||||
this.events = new Events(this);
|
||||
this.storage = new Storage(this);
|
||||
this.icons = new Icons(this);
|
||||
this.i18n = new I18n(this);
|
||||
this.notice = new Notice(this);
|
||||
this.player = new Player(this);
|
||||
this.layers = new Layer(this);
|
||||
this.controls = new Control(this);
|
||||
this.contextmenu = new Contextmenu(this);
|
||||
this.subtitle = new Subtitle(this);
|
||||
this.info = new Info(this);
|
||||
this.loading = new Loading(this);
|
||||
this.hotkey = new Hotkey(this);
|
||||
this.mask = new Mask(this);
|
||||
this.setting = new Setting(this);
|
||||
this.plugins = new Plugins(this);
|
||||
if (typeof readyCallback === "function") {
|
||||
this.on("ready", () => readyCallback.call(this, this));
|
||||
}
|
||||
if (Artplayer.DEBUG) {
|
||||
const log = (msg) => console.log(`[ART.${this.id}] -> ${msg}`);
|
||||
log(`Version@${Artplayer.version}`);
|
||||
for (let index = 0; index < config$1.events.length; index++) {
|
||||
this.on(`video:${config$1.events[index]}`, (event) => log(`Event@${event.type}`));
|
||||
beginLifecycle(this);
|
||||
try {
|
||||
this.template = new Template(this);
|
||||
this.events = new Events(this);
|
||||
this.storage = new Storage(this);
|
||||
this.icons = new Icons(this);
|
||||
this.i18n = new I18n(this);
|
||||
this.notice = new Notice(this);
|
||||
this.player = new Player(this);
|
||||
this.layers = new Layer(this);
|
||||
this.controls = new Control(this);
|
||||
this.contextmenu = new Contextmenu(this);
|
||||
this.subtitle = new Subtitle(this);
|
||||
this.info = new Info(this);
|
||||
this.loading = new Loading(this);
|
||||
this.hotkey = new Hotkey(this);
|
||||
this.mask = new Mask(this);
|
||||
this.setting = new Setting(this);
|
||||
this.plugins = new Plugins(this);
|
||||
if (getScope(this).closed)
|
||||
return;
|
||||
if (typeof readyCallback === "function") {
|
||||
this.on("ready", () => readyCallback.call(this, this));
|
||||
}
|
||||
if (Artplayer.DEBUG) {
|
||||
const log = (msg) => console.log(`[ART.${this.id}] -> ${msg}`);
|
||||
log(`Version@${Artplayer.version}`);
|
||||
for (let index = 0; index < config$1.events.length; index++) {
|
||||
this.on(`video:${config$1.events[index]}`, (event) => log(`Event@${event.type}`));
|
||||
}
|
||||
}
|
||||
if (finishLifecycle(this))
|
||||
instances.push(this);
|
||||
} catch (error2) {
|
||||
try {
|
||||
destroyInstance(this, instances, true, Artplayer.REMOVE_SRC_WHEN_DESTROY, true);
|
||||
} catch (cleanupError) {
|
||||
console.warn("Failed to clean up ArtPlayer initialization:", cleanupError);
|
||||
}
|
||||
throw error2;
|
||||
}
|
||||
instances.push(this);
|
||||
}
|
||||
static get instances() {
|
||||
return instances;
|
||||
@@ -4993,14 +5249,7 @@ class Artplayer extends Emitter {
|
||||
this.video.load();
|
||||
}
|
||||
destroy(removeHtml = true) {
|
||||
if (Artplayer.REMOVE_SRC_WHEN_DESTROY) {
|
||||
this.reset();
|
||||
}
|
||||
this.events.destroy();
|
||||
this.template.destroy(removeHtml);
|
||||
instances.splice(instances.indexOf(this), 1);
|
||||
this.isDestroy = true;
|
||||
this.emit("destroy");
|
||||
destroyInstance(this, instances, removeHtml, Artplayer.REMOVE_SRC_WHEN_DESTROY);
|
||||
}
|
||||
}
|
||||
Artplayer.STYLE = style;
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
"ci:build": "yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js",
|
||||
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
"test:browser:install": "playwright install chromium firefox webkit",
|
||||
|
||||
@@ -55,9 +55,9 @@ not clear unrelated subscribers before destroy dispatch. Internal resource scope
|
||||
`src/lifecycle/scope.ts` owns synchronous cleanup registrations. `resources.ts` supplies
|
||||
DOM listener, timeout, animation frame, request-controller and Blob URL adapters;
|
||||
it depends only on the scope type and native browser APIs. Neither file is exported
|
||||
from Artplayer or Artplayer.utils. CORE-03 introduces and tests this boundary;
|
||||
CORE-04 will connect instance construction/destruction. Existing Events, timers and
|
||||
requests have not yet been transferred, so their historical lifecycle findings remain open.
|
||||
from Artplayer or Artplayer.utils. `instance.ts` connects this boundary to construction
|
||||
and destruction through WeakMaps, adding no fields to public instances. The entry
|
||||
and most UI modules remain JS; their full TS migrations still have separate tasks.
|
||||
|
||||
Create one ResourceScope for an owner and child() for a replaceable operation.
|
||||
Call operation.dispose() on completion or replacement; it detaches from its parent,
|
||||
@@ -83,16 +83,69 @@ engines lacking AbortController: consumers still need closed/generation checks a
|
||||
must own rejected fetch Promises. Release a completed request's operation scope to
|
||||
avoid retaining its controller. objectURL only accepts a Blob it creates a URL for;
|
||||
it never adopts a caller URL. A closed scope immediately revokes the newly created URL.
|
||||
wait(scope, delay) resolves true when its timer finishes and false when disposed;
|
||||
it does not reject on cancellation or leave a pending Promise. Consumers must check
|
||||
both the result and scope.closed immediately after awaiting, since destruction can
|
||||
occur between timer completion and their microtask continuation.
|
||||
|
||||
Run `yarn test:unit` and `yarn typecheck` for scope failures, reentry, ownership,
|
||||
controlled timers, queued RAF and type rejection cases. `resource-scope.spec.js` runs
|
||||
an explicitly identified es2015 internal-source fixture in all three Playwright
|
||||
engines with native DOM, RAF, timers, fetch abort and URL access. It is not evidence
|
||||
that the player has adopted scopes or that an installed tarball exposes this API.
|
||||
engines with native DOM, RAF, timers, fetch abort and URL access. That internal fixture
|
||||
is distinct from lifecycle.spec.js, which exercises the actual player candidate.
|
||||
Fresh fetches test URL revocation; decoded image caches are not a reliable revocation
|
||||
oracle. Keep adapter tests and owner integration tests separate and extend both when
|
||||
moving an existing resource into this boundary.
|
||||
|
||||
## Construction and destruction
|
||||
|
||||
Construction validates options before creating a scope, then initializes the existing
|
||||
subsystems in their historical order. Template reserves the container before mutating
|
||||
it, so reentrant construction cannot claim the same container before instances.push.
|
||||
`template-rollback.ts` captures original nodes, child order, text and attributes before
|
||||
mounting. Success drops the rollback closure; failure restores the same original nodes,
|
||||
including existing DOM listeners and SSR node identity. The two Element casts follow
|
||||
nodeType checks. This is DOM rollback, not a rollback of arbitrary user callback side
|
||||
effects, native media state, external nodes, storage or requests outside the scope.
|
||||
|
||||
Events registers cleanup before installing listeners, so a throw during new Events
|
||||
is covered even before art.events is assigned. Constructor failure releases initialized
|
||||
resources, removes any registry entry, marks isDestroy, emits destroy for plugin-owned
|
||||
cleanup and restores the original container. The original constructor error is rethrown;
|
||||
secondary cleanup failures are reported separately. A synchronously destroyed constructor
|
||||
result is not added to Artplayer.instances. Async plugin result registration remains CORE-08.
|
||||
Container ownership lasts through failed-constructor cleanup and rollback, preventing a
|
||||
new mount inside its destroy callback from being overwritten by the old rollback. Normal
|
||||
destroy releases the container before its event, allowing a replacement to mount there.
|
||||
|
||||
Normal destroy keeps the order reset -> owned resource/DOM listener cleanup -> template
|
||||
removal or art-destroy -> registry removal -> isDestroy=true -> destroy event. An internal
|
||||
guard closes reentry before reset without changing the public isDestroy value observed
|
||||
inside reset. Repeated calls do nothing, including after destroy(false); they cannot erase
|
||||
a replacement player's DOM or splice an unrelated instance at index -1. Resource, template
|
||||
and destroy-event failures are attempted independently; the first thrown value propagates
|
||||
unchanged after cleanup. Emitter's normal exception rule still stops later subscribers in
|
||||
the same dispatch; core cleanup no longer depends only on those subscribers.
|
||||
|
||||
Current ownership covers Events, resize debounce, Info's loop, Notice's timer, update RAF,
|
||||
setting mounted callbacks and wait continuations for customType/empty URL, quality setup
|
||||
and reconnect. Events.proxy keeps original listener identities and destroyEvents shape;
|
||||
after the instance closes it returns inert disposers. Notice refuses new work after closure.
|
||||
The public standalone debounce/throttle/sleep utilities retain their original APIs.
|
||||
|
||||
Remaining per-module migration includes view throttling and optional built-in plugin timers
|
||||
(CORE-17/18), progress/thumbnail work (CORE-19), subtitle requests/Blob URLs (CORE-15),
|
||||
switch Promise settlement (CORE-09), and async plugin results (CORE-08). These are not
|
||||
covered by the claim that BASE-LIFE-04/05 and the specific BASE-PERF-01 resize defect
|
||||
are fixed. Extend resource ownership and its actual owner tests as those tasks land.
|
||||
|
||||
`test/instance-lifecycle.test.js` verifies ordering, reentry, thrown-value preservation
|
||||
and the timer-to-microtask destruction race. `test/browser/lifecycle.spec.js` covers
|
||||
published double-destroy behavior versus its correction, plugin/proxy/partial-Events
|
||||
constructor failures, SSR rollback, normal resize and destroyed delayed work. Run the
|
||||
complete installed-candidate browser suite alongside playback/chapter when changing this
|
||||
boundary; source-only tests cannot establish packaging or real media compatibility.
|
||||
|
||||
## Observable utility behavior
|
||||
|
||||
- `def` is the native Object.defineProperty function. Property keys include symbols;
|
||||
|
||||
Vendored
+1
-1
@@ -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 a=(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"===a(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(a(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 a(t){return t.constructor?t.constructor.name:null}function o(t,e){var a=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,a),i(t,e,a),function(t,e,a){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(a.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=a.slice();c.push(r),n(s,l,c),i(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(a.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=a.slice();d.push(s),n(l,c,d),i(l,c,d),o(l,c,d)})}}(t,e,a),t}function n(t,e,a){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(a.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function i(t,e,a){if("function"===r(e)){var o=e(t,r(t),a);if(!0!==o){var n=r(o);throw"string"===n?new Error(o):"error"===n?o:new Error("[Validator Error]: The scheme for '".concat(a.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(a),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
@@ -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,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
|
||||
+304
-55
@@ -583,14 +583,14 @@ function sleep(ms = 0) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
function debounce(func, duration) {
|
||||
let timeout;
|
||||
let timeout2;
|
||||
return function(...args) {
|
||||
const effect = () => {
|
||||
timeout = void 0;
|
||||
timeout2 = void 0;
|
||||
return func.apply(this, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(effect, duration);
|
||||
clearTimeout(timeout2);
|
||||
timeout2 = setTimeout(effect, duration);
|
||||
};
|
||||
}
|
||||
function throttle(func, duration) {
|
||||
@@ -1089,6 +1089,191 @@ class Contextmenu extends Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
class ResourceCleanupError extends Error {
|
||||
constructor(errors) {
|
||||
super("Failed to release ArtPlayer resources");
|
||||
this.errors = errors;
|
||||
this.name = "ResourceCleanupError";
|
||||
}
|
||||
}
|
||||
class ResourceScope {
|
||||
constructor() {
|
||||
this.cleanups = /* @__PURE__ */ new Set();
|
||||
this.disposed = false;
|
||||
}
|
||||
get closed() {
|
||||
return this.disposed;
|
||||
}
|
||||
add(cleanup) {
|
||||
let active = true;
|
||||
const release = () => {
|
||||
if (!active)
|
||||
return;
|
||||
active = false;
|
||||
this.cleanups.delete(release);
|
||||
cleanup();
|
||||
};
|
||||
if (this.closed)
|
||||
release();
|
||||
else
|
||||
this.cleanups.add(release);
|
||||
return release;
|
||||
}
|
||||
child() {
|
||||
const child = new ResourceScope();
|
||||
const release = this.add(() => {
|
||||
child.dispose();
|
||||
});
|
||||
child.add(() => {
|
||||
release();
|
||||
});
|
||||
return child;
|
||||
}
|
||||
dispose() {
|
||||
if (this.closed)
|
||||
return;
|
||||
this.disposed = true;
|
||||
const errors = [];
|
||||
for (const release of Array.from(this.cleanups).reverse()) {
|
||||
try {
|
||||
release();
|
||||
} catch (error2) {
|
||||
if (error2 instanceof ResourceCleanupError)
|
||||
errors.push(...error2.errors);
|
||||
else
|
||||
errors.push(error2);
|
||||
}
|
||||
}
|
||||
if (errors.length)
|
||||
throw new ResourceCleanupError(errors);
|
||||
}
|
||||
}
|
||||
const states = /* @__PURE__ */ new WeakMap();
|
||||
const containers = /* @__PURE__ */ new WeakMap();
|
||||
function beginLifecycle(owner) {
|
||||
states.set(owner, { scope: new ResourceScope(), destroying: false });
|
||||
}
|
||||
function stateOf(owner) {
|
||||
const state2 = states.get(owner);
|
||||
if (!state2)
|
||||
throw new Error("ArtPlayer lifecycle has not been initialized");
|
||||
return state2;
|
||||
}
|
||||
function getScope(owner) {
|
||||
return stateOf(owner).scope;
|
||||
}
|
||||
function ownContainer(owner, container, rollback) {
|
||||
const current = containers.get(container);
|
||||
if (current && current !== owner)
|
||||
throw new Error("Cannot mount multiple instances on the same dom element");
|
||||
const state2 = stateOf(owner);
|
||||
state2.rollback = rollback;
|
||||
containers.set(container, owner);
|
||||
state2.releaseContainer = () => {
|
||||
if (containers.get(container) === owner)
|
||||
containers.delete(container);
|
||||
};
|
||||
}
|
||||
function finishLifecycle(owner) {
|
||||
const state2 = stateOf(owner);
|
||||
state2.rollback = void 0;
|
||||
return !state2.scope.closed;
|
||||
}
|
||||
function destroyInstance(owner, instances2, removeHtml, removeSource, failed = false) {
|
||||
const state2 = stateOf(owner);
|
||||
if (state2.destroying || state2.scope.closed)
|
||||
return;
|
||||
state2.destroying = true;
|
||||
const errors = [];
|
||||
const attempt = (cleanup) => {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (error2) {
|
||||
errors.push(...error2 instanceof ResourceCleanupError ? error2.errors : [error2]);
|
||||
}
|
||||
};
|
||||
if (removeSource && owner.template?.$video)
|
||||
attempt(() => owner.reset());
|
||||
attempt(() => state2.scope.dispose());
|
||||
attempt(() => owner.template?.destroy(removeHtml));
|
||||
const index = instances2.indexOf(owner);
|
||||
if (index !== -1)
|
||||
instances2.splice(index, 1);
|
||||
owner.isDestroy = true;
|
||||
if (!failed) {
|
||||
state2.releaseContainer?.();
|
||||
state2.releaseContainer = void 0;
|
||||
}
|
||||
attempt(() => owner.emit("destroy"));
|
||||
if (failed && state2.rollback)
|
||||
attempt(state2.rollback);
|
||||
state2.rollback = void 0;
|
||||
state2.releaseContainer?.();
|
||||
state2.releaseContainer = void 0;
|
||||
for (const error2 of errors.slice(1))
|
||||
console.warn("Additional ArtPlayer cleanup failure:", error2);
|
||||
if (errors.length)
|
||||
throw errors[0];
|
||||
}
|
||||
function timeout(scope, callback, delay) {
|
||||
if (scope.closed)
|
||||
return () => {
|
||||
};
|
||||
let pending = true;
|
||||
let release = () => {
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
if (!pending)
|
||||
return;
|
||||
release();
|
||||
if (!scope.closed)
|
||||
callback();
|
||||
}, delay);
|
||||
release = scope.add(() => {
|
||||
pending = false;
|
||||
clearTimeout(timer);
|
||||
});
|
||||
return release;
|
||||
}
|
||||
function animationFrame(scope, callback) {
|
||||
if (scope.closed)
|
||||
return () => {
|
||||
};
|
||||
let pending = true;
|
||||
let release = () => {
|
||||
};
|
||||
const frame = requestAnimationFrame((time2) => {
|
||||
if (!pending)
|
||||
return;
|
||||
release();
|
||||
if (!scope.closed)
|
||||
callback(time2);
|
||||
});
|
||||
release = scope.add(() => {
|
||||
pending = false;
|
||||
cancelAnimationFrame(frame);
|
||||
});
|
||||
return release;
|
||||
}
|
||||
function wait(scope, delay = 0) {
|
||||
if (scope.closed)
|
||||
return Promise.resolve(false);
|
||||
return new Promise((resolve) => {
|
||||
let completed = false;
|
||||
let release = () => {
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
completed = true;
|
||||
release();
|
||||
resolve(true);
|
||||
}, delay);
|
||||
release = scope.add(() => {
|
||||
clearTimeout(timer);
|
||||
if (!completed)
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
function airplay$1(option) {
|
||||
return (art) => ({
|
||||
...option,
|
||||
@@ -1576,7 +1761,9 @@ class Control extends Component {
|
||||
);
|
||||
}
|
||||
if (option.quality.length) {
|
||||
sleep().then(() => {
|
||||
wait(getScope(this.art)).then((active) => {
|
||||
if (!active || getScope(this.art).closed)
|
||||
return;
|
||||
this.art.quality = option.quality;
|
||||
});
|
||||
}
|
||||
@@ -1927,7 +2114,13 @@ function resizeInit(art, events) {
|
||||
art.aspectRatio = aspectRatio2;
|
||||
notice.show = "";
|
||||
});
|
||||
const resizeFn = debounce(() => art.emit("resize"), constructor.RESIZE_TIME);
|
||||
const scope = getScope(art);
|
||||
let cancel = () => {
|
||||
};
|
||||
const resizeFn = () => {
|
||||
cancel();
|
||||
cancel = timeout(scope, () => art.emit("resize"), constructor.RESIZE_TIME);
|
||||
};
|
||||
art.on("window:orientationchange", () => resizeFn());
|
||||
art.on("window:resize", () => resizeFn());
|
||||
if (screen && screen.orientation && screen.orientation.onchange) {
|
||||
@@ -1936,17 +2129,19 @@ function resizeInit(art, events) {
|
||||
}
|
||||
function updateInit(art) {
|
||||
if (art.constructor.USE_RAF) {
|
||||
let timer = null;
|
||||
const scope = getScope(art);
|
||||
let cancel = () => {
|
||||
};
|
||||
(function update() {
|
||||
if (art.playing) {
|
||||
art.emit("raf");
|
||||
}
|
||||
if (!art.isDestroy) {
|
||||
timer = requestAnimationFrame(update);
|
||||
cancel = animationFrame(scope, update);
|
||||
}
|
||||
})();
|
||||
art.on("destroy", () => {
|
||||
cancelAnimationFrame(timer);
|
||||
cancel();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1966,9 +2161,13 @@ function viewInit(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
const scopes = /* @__PURE__ */ new WeakMap();
|
||||
class Events {
|
||||
constructor(art) {
|
||||
this.destroyEvents = /* @__PURE__ */ new Set();
|
||||
const scope = getScope(art);
|
||||
scopes.set(this, scope);
|
||||
scope.add(() => this.destroy());
|
||||
this.proxy = this.proxy.bind(this);
|
||||
this.hover = this.hover.bind(this);
|
||||
clickInit(art, this);
|
||||
@@ -1984,6 +2183,9 @@ class Events {
|
||||
if (Array.isArray(name)) {
|
||||
return name.map((item) => this.proxy(target, item, callback, option));
|
||||
}
|
||||
if (scopes.get(this).closed)
|
||||
return () => {
|
||||
};
|
||||
target.addEventListener(name, callback, option);
|
||||
const destroy = () => target.removeEventListener(name, callback, option);
|
||||
this.destroyEvents.add(destroy);
|
||||
@@ -2237,9 +2439,11 @@ class Info extends Component {
|
||||
proxy($infoClose, "click", () => {
|
||||
this.show = false;
|
||||
});
|
||||
let timer = null;
|
||||
let cancel = () => {
|
||||
};
|
||||
const $types = queryAll("[data-video]", $infoPanel) || [];
|
||||
this.art.on("destroy", () => clearTimeout(timer));
|
||||
this.art.on("destroy", () => cancel());
|
||||
const scope = getScope(this.art);
|
||||
function loop() {
|
||||
for (let index = 0; index < $types.length; index++) {
|
||||
const item = $types[index];
|
||||
@@ -2249,7 +2453,7 @@ class Info extends Component {
|
||||
item.textContent = textContent;
|
||||
}
|
||||
}
|
||||
timer = setTimeout(loop, constructor.INFO_LOOP_TIME);
|
||||
cancel = timeout(scope, loop, constructor.INFO_LOOP_TIME);
|
||||
}
|
||||
loop();
|
||||
}
|
||||
@@ -2294,6 +2498,7 @@ class Notice {
|
||||
constructor(art) {
|
||||
this.art = art;
|
||||
this.timer = null;
|
||||
getScope(art).add(() => this.destroy());
|
||||
art.on("destroy", () => this.destroy());
|
||||
}
|
||||
destroy() {
|
||||
@@ -2303,6 +2508,8 @@ class Notice {
|
||||
}
|
||||
}
|
||||
set show(msg) {
|
||||
if (getScope(this.art).closed)
|
||||
return;
|
||||
const {
|
||||
constructor,
|
||||
template: { $player, $noticeInner }
|
||||
@@ -2521,7 +2728,8 @@ function eventInit(art) {
|
||||
});
|
||||
art.on("video:error", async (error2) => {
|
||||
if (reconnectTime < constructor.RECONNECT_TIME_MAX) {
|
||||
await sleep(constructor.RECONNECT_SLEEP_TIME);
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return;
|
||||
reconnectTime += 1;
|
||||
art.url = option.url;
|
||||
notice.show = `${i18n.get("Reconnect")}: ${reconnectTime}`;
|
||||
@@ -2531,7 +2739,8 @@ function eventInit(art) {
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
addClass($player, "art-error");
|
||||
await sleep(constructor.RECONNECT_SLEEP_TIME);
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return;
|
||||
notice.show = i18n.get("Video Load Failed");
|
||||
}
|
||||
});
|
||||
@@ -3288,12 +3497,12 @@ function seekMix(art) {
|
||||
});
|
||||
}
|
||||
function stateMix(art) {
|
||||
const states = ["mini", "pip", "fullscreen", "fullscreenWeb"];
|
||||
const states2 = ["mini", "pip", "fullscreen", "fullscreenWeb"];
|
||||
def(art, "state", {
|
||||
get: () => states.find((name) => art[name]) || "standard",
|
||||
get: () => states2.find((name) => art[name]) || "standard",
|
||||
set(name) {
|
||||
for (let index = 0; index < states.length; index++) {
|
||||
const prop = states[index];
|
||||
for (let index = 0; index < states2.length; index++) {
|
||||
const prop = states2[index];
|
||||
if (prop !== name && art[prop]) {
|
||||
art[prop] = false;
|
||||
}
|
||||
@@ -3490,7 +3699,8 @@ function urlMix(art) {
|
||||
const typeName = option.type || getExt(newUrl);
|
||||
const typeCallback = option.customType[typeName];
|
||||
if (typeName && typeCallback) {
|
||||
await sleep();
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return;
|
||||
art.loading.show = true;
|
||||
typeCallback.call(art, $video, newUrl, art);
|
||||
} else {
|
||||
@@ -3506,7 +3716,8 @@ function urlMix(art) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await sleep();
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return;
|
||||
art.loading.show = true;
|
||||
}
|
||||
}
|
||||
@@ -4438,7 +4649,7 @@ class Setting extends Component {
|
||||
append($panel, $item);
|
||||
}
|
||||
if (item.mounted) {
|
||||
setTimeout(() => item.mounted.call(this.art, item.$item, item), 0);
|
||||
timeout(getScope(this.art), () => item.mounted.call(this.art, item.$item, item), 0);
|
||||
}
|
||||
}
|
||||
render(option = this.option) {
|
||||
@@ -4642,6 +4853,37 @@ class Subtitle extends Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
function captureTemplate(container) {
|
||||
const snapshots = [];
|
||||
function capture(node) {
|
||||
const children = Array.from(node.childNodes);
|
||||
snapshots.push({
|
||||
node,
|
||||
children,
|
||||
attributes: node.nodeType === 1 ? Array.from(node.attributes, (attr) => [attr.name, attr.value]) : void 0,
|
||||
value: node.nodeValue
|
||||
});
|
||||
children.forEach(capture);
|
||||
}
|
||||
capture(container);
|
||||
return () => {
|
||||
for (const { node, children, attributes, value } of snapshots) {
|
||||
while (node.firstChild)
|
||||
node.removeChild(node.firstChild);
|
||||
for (const child of children)
|
||||
node.appendChild(child);
|
||||
if (attributes) {
|
||||
const element = node;
|
||||
for (const attr of Array.from(element.attributes))
|
||||
element.removeAttribute(attr.name);
|
||||
for (const [name, content] of attributes)
|
||||
element.setAttribute(name, content);
|
||||
} else {
|
||||
node.nodeValue = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
class Template {
|
||||
constructor(art) {
|
||||
this.art = art;
|
||||
@@ -4660,6 +4902,7 @@ class Template {
|
||||
"Cannot mount multiple instances on the same dom element"
|
||||
);
|
||||
this.query = this.query.bind(this);
|
||||
ownContainer(art, this.$container, captureTemplate(this.$container));
|
||||
this.$container.dataset.artId = art.id;
|
||||
this.init();
|
||||
}
|
||||
@@ -4851,34 +5094,47 @@ class Artplayer extends Emitter {
|
||||
this.isInput = false;
|
||||
this.isRotate = false;
|
||||
this.isDestroy = false;
|
||||
this.template = new Template(this);
|
||||
this.events = new Events(this);
|
||||
this.storage = new Storage(this);
|
||||
this.icons = new Icons(this);
|
||||
this.i18n = new I18n(this);
|
||||
this.notice = new Notice(this);
|
||||
this.player = new Player(this);
|
||||
this.layers = new Layer(this);
|
||||
this.controls = new Control(this);
|
||||
this.contextmenu = new Contextmenu(this);
|
||||
this.subtitle = new Subtitle(this);
|
||||
this.info = new Info(this);
|
||||
this.loading = new Loading(this);
|
||||
this.hotkey = new Hotkey(this);
|
||||
this.mask = new Mask(this);
|
||||
this.setting = new Setting(this);
|
||||
this.plugins = new Plugins(this);
|
||||
if (typeof readyCallback === "function") {
|
||||
this.on("ready", () => readyCallback.call(this, this));
|
||||
}
|
||||
if (Artplayer.DEBUG) {
|
||||
const log = (msg) => console.log(`[ART.${this.id}] -> ${msg}`);
|
||||
log(`Version@${Artplayer.version}`);
|
||||
for (let index = 0; index < config$1.events.length; index++) {
|
||||
this.on(`video:${config$1.events[index]}`, (event) => log(`Event@${event.type}`));
|
||||
beginLifecycle(this);
|
||||
try {
|
||||
this.template = new Template(this);
|
||||
this.events = new Events(this);
|
||||
this.storage = new Storage(this);
|
||||
this.icons = new Icons(this);
|
||||
this.i18n = new I18n(this);
|
||||
this.notice = new Notice(this);
|
||||
this.player = new Player(this);
|
||||
this.layers = new Layer(this);
|
||||
this.controls = new Control(this);
|
||||
this.contextmenu = new Contextmenu(this);
|
||||
this.subtitle = new Subtitle(this);
|
||||
this.info = new Info(this);
|
||||
this.loading = new Loading(this);
|
||||
this.hotkey = new Hotkey(this);
|
||||
this.mask = new Mask(this);
|
||||
this.setting = new Setting(this);
|
||||
this.plugins = new Plugins(this);
|
||||
if (getScope(this).closed)
|
||||
return;
|
||||
if (typeof readyCallback === "function") {
|
||||
this.on("ready", () => readyCallback.call(this, this));
|
||||
}
|
||||
if (Artplayer.DEBUG) {
|
||||
const log = (msg) => console.log(`[ART.${this.id}] -> ${msg}`);
|
||||
log(`Version@${Artplayer.version}`);
|
||||
for (let index = 0; index < config$1.events.length; index++) {
|
||||
this.on(`video:${config$1.events[index]}`, (event) => log(`Event@${event.type}`));
|
||||
}
|
||||
}
|
||||
if (finishLifecycle(this))
|
||||
instances.push(this);
|
||||
} catch (error2) {
|
||||
try {
|
||||
destroyInstance(this, instances, true, Artplayer.REMOVE_SRC_WHEN_DESTROY, true);
|
||||
} catch (cleanupError) {
|
||||
console.warn("Failed to clean up ArtPlayer initialization:", cleanupError);
|
||||
}
|
||||
throw error2;
|
||||
}
|
||||
instances.push(this);
|
||||
}
|
||||
static get instances() {
|
||||
return instances;
|
||||
@@ -4993,14 +5249,7 @@ class Artplayer extends Emitter {
|
||||
this.video.load();
|
||||
}
|
||||
destroy(removeHtml = true) {
|
||||
if (Artplayer.REMOVE_SRC_WHEN_DESTROY) {
|
||||
this.reset();
|
||||
}
|
||||
this.events.destroy();
|
||||
this.template.destroy(removeHtml);
|
||||
instances.splice(instances.indexOf(this), 1);
|
||||
this.isDestroy = true;
|
||||
this.emit("destroy");
|
||||
destroyInstance(this, instances, removeHtml, Artplayer.REMOVE_SRC_WHEN_DESTROY);
|
||||
}
|
||||
}
|
||||
Artplayer.STYLE = style;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { wait } from '../lifecycle/resources'
|
||||
import {
|
||||
addClass,
|
||||
append,
|
||||
@@ -9,7 +11,6 @@ import {
|
||||
inverseClass,
|
||||
isMobile,
|
||||
removeClass,
|
||||
sleep,
|
||||
} from '../utils'
|
||||
import Component from '../utils/component'
|
||||
import airplay from './airplay'
|
||||
@@ -127,7 +128,9 @@ export default class Control extends Component {
|
||||
}
|
||||
|
||||
if (option.quality.length) {
|
||||
sleep().then(() => {
|
||||
wait(getScope(this.art)).then((active) => {
|
||||
if (!active || getScope(this.art).closed)
|
||||
return
|
||||
this.art.quality = option.quality
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import clickInit from './clickInit'
|
||||
import gestureInit from './gestureInit'
|
||||
import globalInit from './globalInit'
|
||||
@@ -7,9 +8,14 @@ import resizeInit from './resizeInit'
|
||||
import updateInit from './updateInit'
|
||||
import viewInit from './viewInit'
|
||||
|
||||
const scopes = new WeakMap()
|
||||
|
||||
export default class Events {
|
||||
constructor(art) {
|
||||
this.destroyEvents = new Set()
|
||||
const scope = getScope(art)
|
||||
scopes.set(this, scope)
|
||||
scope.add(() => this.destroy())
|
||||
this.proxy = this.proxy.bind(this)
|
||||
this.hover = this.hover.bind(this)
|
||||
|
||||
@@ -28,6 +34,9 @@ export default class Events {
|
||||
return name.map(item => this.proxy(target, item, callback, option))
|
||||
}
|
||||
|
||||
if (scopes.get(this).closed)
|
||||
return () => {}
|
||||
|
||||
target.addEventListener(name, callback, option)
|
||||
const destroy = () => target.removeEventListener(name, callback, option)
|
||||
this.destroyEvents.add(destroy)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { debounce } from '../utils'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
|
||||
export default function resizeInit(art, events) {
|
||||
const { option, constructor } = art
|
||||
@@ -12,7 +13,12 @@ export default function resizeInit(art, events) {
|
||||
notice.show = ''
|
||||
})
|
||||
|
||||
const resizeFn = debounce(() => art.emit('resize'), constructor.RESIZE_TIME)
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
const resizeFn = () => {
|
||||
cancel()
|
||||
cancel = timeout(scope, () => art.emit('resize'), constructor.RESIZE_TIME)
|
||||
}
|
||||
|
||||
art.on('window:orientationchange', () => resizeFn())
|
||||
art.on('window:resize', () => resizeFn())
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { animationFrame } from '../lifecycle/resources'
|
||||
|
||||
export default function updateInit(art) {
|
||||
if (art.constructor.USE_RAF) {
|
||||
let timer = null;
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {};
|
||||
|
||||
(function update() {
|
||||
if (art.playing) {
|
||||
@@ -8,12 +12,12 @@ export default function updateInit(art) {
|
||||
}
|
||||
|
||||
if (!art.isDestroy) {
|
||||
timer = requestAnimationFrame(update)
|
||||
cancel = animationFrame(scope, update)
|
||||
}
|
||||
})()
|
||||
|
||||
art.on('destroy', () => {
|
||||
cancelAnimationFrame(timer)
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import I18n from './i18n'
|
||||
import Icons from './icons'
|
||||
import Info from './info'
|
||||
import Layer from './layer'
|
||||
import { beginLifecycle, destroyInstance, finishLifecycle, getScope } from './lifecycle/instance'
|
||||
import Loading from './loading'
|
||||
import Mask from './mask'
|
||||
import Notice from './notice'
|
||||
@@ -47,38 +48,54 @@ export default class Artplayer extends Emitter {
|
||||
this.isRotate = false
|
||||
this.isDestroy = false
|
||||
|
||||
this.template = new Template(this)
|
||||
this.events = new Events(this)
|
||||
this.storage = new Storage(this)
|
||||
this.icons = new Icons(this)
|
||||
this.i18n = new I18n(this)
|
||||
this.notice = new Notice(this)
|
||||
this.player = new Player(this)
|
||||
this.layers = new Layer(this)
|
||||
this.controls = new Control(this)
|
||||
this.contextmenu = new Contextmenu(this)
|
||||
this.subtitle = new Subtitle(this)
|
||||
this.info = new Info(this)
|
||||
this.loading = new Loading(this)
|
||||
this.hotkey = new Hotkey(this)
|
||||
this.mask = new Mask(this)
|
||||
this.setting = new Setting(this)
|
||||
this.plugins = new Plugins(this)
|
||||
beginLifecycle(this)
|
||||
try {
|
||||
this.template = new Template(this)
|
||||
this.events = new Events(this)
|
||||
this.storage = new Storage(this)
|
||||
this.icons = new Icons(this)
|
||||
this.i18n = new I18n(this)
|
||||
this.notice = new Notice(this)
|
||||
this.player = new Player(this)
|
||||
this.layers = new Layer(this)
|
||||
this.controls = new Control(this)
|
||||
this.contextmenu = new Contextmenu(this)
|
||||
this.subtitle = new Subtitle(this)
|
||||
this.info = new Info(this)
|
||||
this.loading = new Loading(this)
|
||||
this.hotkey = new Hotkey(this)
|
||||
this.mask = new Mask(this)
|
||||
this.setting = new Setting(this)
|
||||
this.plugins = new Plugins(this)
|
||||
|
||||
if (typeof readyCallback === 'function') {
|
||||
this.on('ready', () => readyCallback.call(this, this))
|
||||
}
|
||||
if (getScope(this).closed)
|
||||
return
|
||||
|
||||
if (Artplayer.DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
const log = msg => console.log(`[ART.${this.id}] -> ${msg}`)
|
||||
log(`Version@${Artplayer.version}`)
|
||||
for (let index = 0; index < config.events.length; index++) {
|
||||
this.on(`video:${config.events[index]}`, event => log(`Event@${event.type}`))
|
||||
if (typeof readyCallback === 'function') {
|
||||
this.on('ready', () => readyCallback.call(this, this))
|
||||
}
|
||||
}
|
||||
|
||||
instances.push(this)
|
||||
if (Artplayer.DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
const log = msg => console.log(`[ART.${this.id}] -> ${msg}`)
|
||||
log(`Version@${Artplayer.version}`)
|
||||
for (let index = 0; index < config.events.length; index++) {
|
||||
this.on(`video:${config.events[index]}`, event => log(`Event@${event.type}`))
|
||||
}
|
||||
}
|
||||
|
||||
if (finishLifecycle(this))
|
||||
instances.push(this)
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
destroyInstance(this, instances, true, Artplayer.REMOVE_SRC_WHEN_DESTROY, true)
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('Failed to clean up ArtPlayer initialization:', cleanupError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
static get instances() {
|
||||
@@ -208,14 +225,7 @@ export default class Artplayer extends Emitter {
|
||||
}
|
||||
|
||||
destroy(removeHtml = true) {
|
||||
if (Artplayer.REMOVE_SRC_WHEN_DESTROY) {
|
||||
this.reset()
|
||||
}
|
||||
this.events.destroy()
|
||||
this.template.destroy(removeHtml)
|
||||
instances.splice(instances.indexOf(this), 1)
|
||||
this.isDestroy = true
|
||||
this.emit('destroy')
|
||||
destroyInstance(this, instances, removeHtml, Artplayer.REMOVE_SRC_WHEN_DESTROY)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getScope } from './lifecycle/instance'
|
||||
import { timeout } from './lifecycle/resources'
|
||||
import { isMobile, queryAll } from './utils'
|
||||
import Component from './utils/component'
|
||||
|
||||
@@ -22,9 +24,10 @@ export default class Info extends Component {
|
||||
this.show = false
|
||||
})
|
||||
|
||||
let timer = null
|
||||
let cancel = () => {}
|
||||
const $types = queryAll('[data-video]', $infoPanel) || []
|
||||
this.art.on('destroy', () => clearTimeout(timer))
|
||||
this.art.on('destroy', () => cancel())
|
||||
const scope = getScope(this.art)
|
||||
|
||||
function loop() {
|
||||
for (let index = 0; index < $types.length; index++) {
|
||||
@@ -35,7 +38,7 @@ export default class Info extends Component {
|
||||
item.textContent = textContent
|
||||
}
|
||||
}
|
||||
timer = setTimeout(loop, constructor.INFO_LOOP_TIME)
|
||||
cancel = timeout(scope, loop, constructor.INFO_LOOP_TIME)
|
||||
}
|
||||
|
||||
loop()
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import ResourceScope, { ResourceCleanupError } from './scope'
|
||||
|
||||
interface LifecycleOwner {
|
||||
isDestroy: boolean
|
||||
reset: () => void
|
||||
emit: (name: 'destroy') => unknown
|
||||
template?: { $video?: unknown, destroy: (removeHtml: boolean) => void }
|
||||
}
|
||||
|
||||
interface State {
|
||||
scope: ResourceScope
|
||||
destroying: boolean
|
||||
rollback?: () => void
|
||||
releaseContainer?: () => void
|
||||
}
|
||||
|
||||
const states = new WeakMap<object, State>()
|
||||
const containers = new WeakMap<Element, object>()
|
||||
|
||||
export function beginLifecycle(owner: object): void {
|
||||
states.set(owner, { scope: new ResourceScope(), destroying: false })
|
||||
}
|
||||
|
||||
function stateOf(owner: object): State {
|
||||
const state = states.get(owner)
|
||||
if (!state)
|
||||
throw new Error('ArtPlayer lifecycle has not been initialized')
|
||||
return state
|
||||
}
|
||||
|
||||
export function getScope(owner: object): ResourceScope {
|
||||
return stateOf(owner).scope
|
||||
}
|
||||
|
||||
export function ownContainer(owner: object, container: Element, rollback: () => void): void {
|
||||
const current = containers.get(container)
|
||||
if (current && current !== owner)
|
||||
throw new Error('Cannot mount multiple instances on the same dom element')
|
||||
const state = stateOf(owner)
|
||||
state.rollback = rollback
|
||||
containers.set(container, owner)
|
||||
state.releaseContainer = () => {
|
||||
if (containers.get(container) === owner)
|
||||
containers.delete(container)
|
||||
}
|
||||
}
|
||||
|
||||
export function finishLifecycle(owner: object): boolean {
|
||||
const state = stateOf(owner)
|
||||
state.rollback = undefined
|
||||
return !state.scope.closed
|
||||
}
|
||||
|
||||
export function destroyInstance(owner: LifecycleOwner, instances: LifecycleOwner[], removeHtml: boolean, removeSource: boolean, failed = false): void {
|
||||
const state = stateOf(owner)
|
||||
if (state.destroying || state.scope.closed)
|
||||
return
|
||||
state.destroying = true
|
||||
const errors: unknown[] = []
|
||||
const attempt = (cleanup: () => void) => {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
catch (error) {
|
||||
errors.push(...(error instanceof ResourceCleanupError ? error.errors : [error]))
|
||||
}
|
||||
}
|
||||
if (removeSource && owner.template?.$video)
|
||||
attempt(() => owner.reset())
|
||||
attempt(() => state.scope.dispose())
|
||||
attempt(() => owner.template?.destroy(removeHtml))
|
||||
const index = instances.indexOf(owner)
|
||||
if (index !== -1)
|
||||
instances.splice(index, 1)
|
||||
owner.isDestroy = true
|
||||
if (!failed) {
|
||||
state.releaseContainer?.()
|
||||
state.releaseContainer = undefined
|
||||
}
|
||||
attempt(() => owner.emit('destroy'))
|
||||
if (failed && state.rollback)
|
||||
attempt(state.rollback)
|
||||
state.rollback = undefined
|
||||
state.releaseContainer?.()
|
||||
state.releaseContainer = undefined
|
||||
// Preserve the first thrown value while making additional failures observable.
|
||||
for (const error of errors.slice(1))
|
||||
console.warn('Additional ArtPlayer cleanup failure:', error)
|
||||
if (errors.length)
|
||||
throw errors[0]
|
||||
}
|
||||
@@ -83,3 +83,22 @@ export function objectURL(scope: ResourceScope, blob: Blob): string {
|
||||
})
|
||||
return url
|
||||
}
|
||||
|
||||
export function wait(scope: ResourceScope, delay = 0): Promise<boolean> {
|
||||
if (scope.closed)
|
||||
return Promise.resolve(false)
|
||||
return new Promise((resolve) => {
|
||||
let completed = false
|
||||
let release = () => {}
|
||||
const timer = setTimeout(() => {
|
||||
completed = true
|
||||
release()
|
||||
resolve(true)
|
||||
}, delay)
|
||||
release = scope.add(() => {
|
||||
clearTimeout(timer)
|
||||
if (!completed)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
interface Snapshot {
|
||||
node: Node
|
||||
children: Node[]
|
||||
attributes?: [string, string][]
|
||||
value: string | null
|
||||
}
|
||||
|
||||
export default function captureTemplate(container: Element): () => void {
|
||||
const snapshots: Snapshot[] = []
|
||||
function capture(node: Node) {
|
||||
const children = Array.from(node.childNodes)
|
||||
snapshots.push({
|
||||
node,
|
||||
children,
|
||||
attributes: node.nodeType === 1 ? Array.from((node as Element).attributes, attr => [attr.name, attr.value]) : undefined,
|
||||
value: node.nodeValue,
|
||||
})
|
||||
children.forEach(capture)
|
||||
}
|
||||
capture(container)
|
||||
return () => {
|
||||
for (const { node, children, attributes, value } of snapshots) {
|
||||
while (node.firstChild)
|
||||
node.removeChild(node.firstChild)
|
||||
for (const child of children)
|
||||
node.appendChild(child)
|
||||
if (attributes) {
|
||||
const element = node as Element
|
||||
for (const attr of Array.from(element.attributes))
|
||||
element.removeAttribute(attr.name)
|
||||
for (const [name, content] of attributes)
|
||||
element.setAttribute(name, content)
|
||||
}
|
||||
else {
|
||||
node.nodeValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { getScope } from './lifecycle/instance'
|
||||
import { addClass, removeClass } from './utils'
|
||||
|
||||
export default class Notice {
|
||||
constructor(art) {
|
||||
this.art = art
|
||||
this.timer = null
|
||||
getScope(art).add(() => this.destroy())
|
||||
|
||||
art.on('destroy', () => this.destroy())
|
||||
}
|
||||
@@ -16,6 +18,8 @@ export default class Notice {
|
||||
}
|
||||
|
||||
set show(msg) {
|
||||
if (getScope(this.art).closed)
|
||||
return
|
||||
const {
|
||||
constructor,
|
||||
template: { $player, $noticeInner },
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import config from '../config'
|
||||
import { addClass, isMobile, setStyle, silencePromise, sleep } from '../utils'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { wait } from '../lifecycle/resources'
|
||||
import { addClass, isMobile, setStyle, silencePromise } from '../utils'
|
||||
|
||||
export default function eventInit(art) {
|
||||
const {
|
||||
@@ -63,7 +65,8 @@ export default function eventInit(art) {
|
||||
|
||||
art.on('video:error', async (error) => {
|
||||
if (reconnectTime < constructor.RECONNECT_TIME_MAX) {
|
||||
await sleep(constructor.RECONNECT_SLEEP_TIME)
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return
|
||||
reconnectTime += 1
|
||||
art.url = option.url
|
||||
notice.show = `${i18n.get('Reconnect')}: ${reconnectTime}`
|
||||
@@ -74,7 +77,8 @@ export default function eventInit(art) {
|
||||
art.loading.show = false
|
||||
art.controls.show = true
|
||||
addClass($player, 'art-error')
|
||||
await sleep(constructor.RECONNECT_SLEEP_TIME)
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return
|
||||
notice.show = i18n.get('Video Load Failed')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { def, getExt, sleep } from '../utils'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { wait } from '../lifecycle/resources'
|
||||
import { def, getExt } from '../utils'
|
||||
|
||||
export default function urlMix(art) {
|
||||
const {
|
||||
@@ -17,7 +19,8 @@ export default function urlMix(art) {
|
||||
const typeCallback = option.customType[typeName]
|
||||
|
||||
if (typeName && typeCallback) {
|
||||
await sleep()
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return
|
||||
art.loading.show = true
|
||||
typeCallback.call(art, $video, newUrl, art)
|
||||
}
|
||||
@@ -36,7 +39,8 @@ export default function urlMix(art) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
await sleep()
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return
|
||||
art.loading.show = true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import {
|
||||
addClass,
|
||||
append,
|
||||
@@ -526,7 +528,7 @@ export default class Setting extends Component {
|
||||
}
|
||||
|
||||
if (item.mounted) {
|
||||
setTimeout(() => item.mounted.call(this.art, item.$item, item), 0)
|
||||
timeout(getScope(this.art), () => item.mounted.call(this.art, item.$item, item), 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { version } from '../package.json'
|
||||
import { ownContainer } from './lifecycle/instance'
|
||||
import captureTemplate from './lifecycle/template-rollback'
|
||||
import { addClass, errorHandle, isMobile, query, replaceElement, supportsFlex } from './utils'
|
||||
|
||||
export default class Template {
|
||||
@@ -25,6 +27,7 @@ export default class Template {
|
||||
)
|
||||
|
||||
this.query = this.query.bind(this)
|
||||
ownContainer(art, this.$container, captureTemplate(this.$container))
|
||||
this.$container.dataset.artId = art.id
|
||||
this.init()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,68 @@
|
||||
# CORE-04:构造回滚与可靠销毁
|
||||
|
||||
## 核心改动
|
||||
|
||||
核心 index.js 已直接接入 lifecycle/instance.ts,实例状态保存在 WeakMap,不增加公开属性。
|
||||
正常构造顺序不变;Template 在修改容器前保留原 DOM,成功后丢弃回滚引用。
|
||||
template-rollback.ts 按原节点、子节点顺序、文本和属性恢复失败挂载,保留旧 DOM 监听和
|
||||
SSR 节点身份。容器在初始化阶段就被预留,避免实例尚未进入注册表时被重入构造占用。
|
||||
|
||||
Events 在安装第一个监听前注册清理,覆盖 new Events 中途失败、art.events 尚未赋值的情况。
|
||||
构造失败会释放已接入的资源、移除实例、标记 isDestroy、发出 destroy 供已有插件清理,
|
||||
并恢复原容器。原始构造错误保持身份和传播,清理错误独立报告。
|
||||
失败回滚结束前保留容器归属,防止 cleanup 回调中的新挂载被旧回滚覆盖;正常 destroy 事件前
|
||||
释放容器,允许该回调创建替代实例。对应回归同时验证两种路径,旧实例重复销毁不影响替代实例。
|
||||
|
||||
destroy 通过内部防重入状态保护整个过程,包括 reset 内触发的重入。正常顺序保持
|
||||
reset → 资源/DOM 监听清理 → template → instances 移除 → isDestroy=true → destroy 事件。
|
||||
reset 中观察到的公开 isDestroy 仍为 false;destroy 回调观察到 true、已移除注册且 DOM
|
||||
符合 removeHtml。重复调用不再改变任何状态,因此 destroy(false) 后再 destroy(true)
|
||||
也不清空保留节点或新的播放器。instances.indexOf 为 -1 时绝不 splice。
|
||||
|
||||
每个内部清理阶段独立执行,失败不会阻断注册表清理和 destroy 派发;第一个错误原样抛出,
|
||||
额外错误报告到 console.warn。Emitter 内单次派发的“抛错停止后续订阅”契约保持;不能借此
|
||||
改动普通 emit。核心资源清理已不单靠 destroy 订阅,因此用户监听抛错不会阻止这些清理。
|
||||
|
||||
## 延迟任务归属
|
||||
|
||||
- resize 防抖改为作用域拥有的 timeout,保留正常合并行为;销毁立即取消等待中的 burst。
|
||||
- Info 循环、update RAF、setting mounted 回调接入作用域;Notice 销毁释放 timer,关闭后不接受新提示。
|
||||
- customType/空 URL、quality 初始化和 reconnect 使用 wait(scope),取消时兑现 false,
|
||||
正常保留 timer → Promise microtask 的顺序;await 后再次检查 closed,覆盖定时器已完成但
|
||||
continuation 尚未运行时发生销毁的竞态。
|
||||
- Events.proxy 的真实 callback、返回 disposer 和 destroyEvents 形状保持;关闭后新增注册返回空清理函数。
|
||||
|
||||
不引入依赖,不改变 Yarn、版本或公开类型入口。内部新增两个 TS 文件;入口及被接入的 JS
|
||||
模块保留渐进迁移,完整入口 TS 收敛属于 CORE-20。
|
||||
|
||||
## 兼容性差异与风险
|
||||
|
||||
API-01/03/04/05/07/08/10:保留公开属性、正常事件顺序、同步返回、removeHtml 和分发路径。
|
||||
BASE-LIFE-04 的误删其他实例、BASE-LIFE-05 的失败构造 DOM/监听残留以及 BASE-PERF-01 的
|
||||
销毁后 resize 重建 notice timer 是有意修复的缺陷。冻结发布基线不改,新增旧版观察和候选正向断言。
|
||||
构造过程中插件主动同步 destroy 的实例不进入注册表。
|
||||
|
||||
这里的 DOM 回滚不承诺恢复任意用户 callback 的外部副作用、存储、原生媒体状态或外部节点。
|
||||
也不表示全部媒体功能的资源已经迁移:字幕请求/URL 为 CORE-15,progress/thumbnail 为
|
||||
CORE-19,view throttle 为 CORE-17,内置插件延迟工作为 CORE-18,切源 Promise 为 CORE-09,
|
||||
异步插件兑现为 CORE-08。它们仍须在对应任务完成,不能因本任务通过而提前允许发布。
|
||||
|
||||
## 验证
|
||||
|
||||
- yarn ci:check:71 项单元、4 项工程、24 项冻结基线,共 99 项;严格源类型检查 17 个 TS 文件。
|
||||
- 四项内部实例测试覆盖销毁顺序、reset 重入/抛错、destroy 监听错误身份及微任务竞态;
|
||||
资源测试增加 wait 完成/取消结算。
|
||||
- 安装 UMD 候选三浏览器完整 108 项通过,无重试/跳过,包括 30 项生命周期对照/修复场景。
|
||||
覆盖真实媒体播放/切源、正常 resize、pending resize/notice/RAF/reconnect/mounted、
|
||||
插件初始化/清理同时失败、早期 proxy 失败、SSR、Events 半初始化失败、重复/重入销毁、重入容器预留和同步关闭构造。
|
||||
- 安装 legacy 候选另跑三个浏览器 30 项生命周期验证;按实际产物映射加载,没有源码回退。
|
||||
- yarn build artplayer / yarn build:i18n 生成 dist 与 docs/compiled。
|
||||
- yarn test:package:release:仓库外实际安装 tarball,27 项运行时和五组类型消费者零诊断。
|
||||
包内架构文档更新后重新打包,并核对浏览器候选 JS 与最终包成员指纹一致。
|
||||
|
||||
证据见 [验证报告](../baselines/instance-lifecycle-validation.json);持续维护地图见
|
||||
[核心架构](../../packages/artplayer/ARCHITECTURE.md)。早期本地测试曾被 ESLint 自动改为未安装
|
||||
的 Vitest 导入,已恢复仓库 Node runner 并明确局部 lint 说明,没有添加另一个测试框架。
|
||||
|
||||
本任务独立提交;回退整个提交可同时恢复源码、生成产物、测试与台账。未推送或发布。
|
||||
下一项 CORE-05 区分输入配置与内部配置,再继续核心类型和模块迁移。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 175 / doing 0 / blocked 0 / done 39 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 174 / doing 0 / blocked 0 / done 40 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
| CORE-01 | artplayer<br>迁移纯工具并冻结导出 | ENG-03, ENG-04, ENG-06, PILOT-01 | utils/time/property/format/file/error/subtitle 的分批 TS 迁移 | Artplayer.utils 导出/参数/绑定保持,纯逻辑与类型测试通过 | M | done |
|
||||
| CORE-02 | artplayer<br>迁移内部 Emitter | CORE-01, BASE-03 | 带类型事件映射的原 Emitter 实现 | ctx、once/off、重入/异常和链式返回保持 | H | done |
|
||||
| CORE-03 | artplayer<br>建立内部资源作用域 | CORE-02 | 监听/订阅/定时器/RAF/请求/URL 的资源登记 | 实例与操作作用域分离,单项清理失败不阻断其他资源 | H | done |
|
||||
| CORE-04 | artplayer<br>修复初始化与销毁生命周期 | CORE-03, ENG-05, BASE-07 | 构造失败回收、重复/重入 destroy、instances 修复 | 事件顺序、removeHtml、多实例和销毁中异步有真实回归;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-04/05 并增加候选正向回归;协调 BASE-PERF-01 销毁后定时器与回调归属 | H | todo |
|
||||
| CORE-04 | artplayer<br>修复初始化与销毁生命周期 | CORE-03, ENG-05, BASE-07 | 构造失败回收、重复/重入 destroy、instances 修复 | 事件顺序、removeHtml、多实例和销毁中异步有真实回归;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-04/05 并增加候选正向回归;协调 BASE-PERF-01 销毁后定时器与回调归属 | H | done |
|
||||
| CORE-05 | artplayer<br>区分输入配置和内部配置 | CORE-01, ENG-04 | Option/ResolvedOption、默认值与校验整理 | 配置合并/错误时机不变,JS 输入仍运行时校验 | M | todo |
|
||||
| CORE-06 | artplayer<br>建立内部媒体与宿主类型 | CORE-05 | 原生 video、canvas shim、UI host 类型 | 不更改公开 art.video 用法,不以 HTMLVideoElement 断言掩盖 shim | M | todo |
|
||||
| CORE-07 | artplayer<br>逐项协调公开声明差异 | CORE-02, CORE-06, BASE-07 | plugins/toggle/cue/setting/static 的旧新类型对照 | 每项有兼容重载/扩展或待决结论,不能删除旧类型样例;处理 BASE-TYPE-01/04,分别保留旧合法声明消费并验证真实返回;协调 BASE-TYPE-05 的 Utils 导出、descriptor 和 timer 旧声明差异 | H | todo |
|
||||
@@ -438,6 +438,7 @@
|
||||
- CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json)
|
||||
- CORE-02: [记录](changes/2026-09-11-CORE-02-typed-emitter.md) [记录](baselines/emitter-validation.json)
|
||||
- CORE-03: [记录](changes/2026-09-11-CORE-03-resource-scope.md) [记录](baselines/resource-scope-validation.json)
|
||||
- CORE-04: [记录](changes/2026-09-11-CORE-04-instance-lifecycle.md) [记录](baselines/instance-lifecycle-validation.json)
|
||||
- PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md)
|
||||
- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json)
|
||||
- PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 进度与证据
|
||||
|
||||
## 当前实施:CORE-04 构造/销毁生命周期完成
|
||||
|
||||
核心入口接入实例作用域,修复重复/重入销毁、失败构造回滚和 pending resize;正常 destroy 回调仍可挂载替代实例。99 项 Node/基线、安装 UMD 三浏览器 108 项及 legacy 三浏览器 30 项通过;严格打包 27 项运行时、五组类型零诊断。见 [交付](changes/2026-09-11-CORE-04-instance-lifecycle.md)。关闭 BASE-LIFE-04/05 与 BASE-PERF-01,字幕/异步插件等后续任务继续开放。当前 214 项,40 完成、174 待办;核心十二个 TS 文件、chapter 五个,主入口仍为 JS 兼容门面。下一项 CORE-05 输入与内部配置类型。
|
||||
|
||||
## 当前实施:CORE-03 内部资源作用域完成
|
||||
|
||||
新增两个核心 TS 文件,明确同步清理、实例/操作作用域、异常汇总及原生资源适配。94 项 Node/基线与 78 项三浏览器检查通过;首次 Firefox 图片缓存误判已保留并改为 fresh fetch 验证。见 [交付](changes/2026-09-11-CORE-03-resource-scope.md)。当前 214 项,39 完成、175 待办;核心十个 TS 文件、chapter 五个。作用域尚未接入播放器,旧生命周期风险仍开放;下一项 CORE-04 直接改构造与销毁。
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
| BASE-LIFE-01 | open / 已复现 | Superseded switch resolves along with the latest source | CORE-09 |
|
||||
| BASE-LIFE-02 | open / 已复现 | Switch remains unsettled during the bounded observation after destroy | CORE-09 |
|
||||
| BASE-LIFE-03 | open / 已复现 | Async plugin registers its result after destroy | CORE-08 |
|
||||
| BASE-LIFE-04 | open / 已复现 | Destroying an instance twice unregisters a different live instance | CORE-04 |
|
||||
| BASE-LIFE-05 | open / 已复现 | Failed constructor leaves initialized markup/listeners until explicit probe cleanup | CORE-04 |
|
||||
| BASE-LIFE-04 | resolved / 已复现 | Destroying an instance twice unregisters a different live instance | CORE-04 |
|
||||
| BASE-LIFE-05 | resolved / 已复现 | Failed constructor leaves initialized markup/listeners until explicit probe cleanup | CORE-04 |
|
||||
| BASE-DOM-01 | open / 已复现 | 控件 Tab 不可达 | CORE-23, CORE-17 |
|
||||
| BASE-DOM-02 | open / 已复现 | 窄容器中控件裁切 | CORE-13, CORE-14 |
|
||||
| BASE-DEMO-01 | open / 源码/产物事实 | 旧 thumbnail 插件示例不对应当前 workspace 包 | SITE-01, EX-03 |
|
||||
@@ -18,7 +18,7 @@
|
||||
| BASE-TYPE-03 | resolved / 已复现 | chapter legacy 缺旧模块解析的声明回退 | ENG-04, PKG-CHAPTER-04 |
|
||||
| BASE-TYPE-04 | open / 源码/产物事实 | plugins.add / toggle 的旧声明返回值与运行时不同 | CORE-07 |
|
||||
| BASE-DIST-01 | open / 源码/产物事实 | thumbnail tool 分发入口与类型文件缺失 | PKG-TOOL-THUMB-01, PKG-TOOL-THUMB-04 |
|
||||
| BASE-PERF-01 | open / 已复现 | 销毁后的 resize 防抖重新创建 notice timer | CORE-17, CORE-04, CORE-18 |
|
||||
| BASE-PERF-01 | resolved / 已复现 | 销毁后的 resize 防抖重新创建 notice timer | CORE-17, CORE-04, CORE-18 |
|
||||
| BASE-SOURCE-01 | open / 源码/产物事实 | 工作区 5.4.1 与采集时真实发布 5.4.0 不同 | BASE-08, REL-01 |
|
||||
| BASE-SOURCE-02 | open / 源码/产物事实 | registry gitHead 与发布包版本对应不可靠 | ENG-07, REL-01 |
|
||||
| BASE-SITE-01 | open / 源码/产物事实 | 文档站实际为静态站点但 manifest 未设 private | SITE-01, REL-01 |
|
||||
|
||||
+28
-7
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"task": "BASE-07",
|
||||
"sourceCommit": "e01f39ff",
|
||||
"updatedAt": "2026-09-10T16:03:52.413Z",
|
||||
"updatedAt": "2026-09-11",
|
||||
"note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.",
|
||||
"items": [
|
||||
{
|
||||
@@ -71,7 +71,7 @@
|
||||
"id": "BASE-LIFE-04",
|
||||
"title": "Destroying an instance twice unregisters a different live instance",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-04"
|
||||
],
|
||||
@@ -80,13 +80,20 @@
|
||||
"refactor/baselines/lifecycle-coverage.md"
|
||||
],
|
||||
"compatibleResolution": "按对应 CORE 任务修复,保持合法旧调用;异步取消需明确结算策略,不把 pending 或重复销毁当正常兼容要求。",
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。"
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-04-instance-lifecycle.md",
|
||||
"refactor/baselines/instance-lifecycle-validation.json",
|
||||
"test/browser/lifecycle.spec.js",
|
||||
"test/instance-lifecycle.test.js"
|
||||
],
|
||||
"resolutionRationale": "内部防重入与 index >= 0 检查保留其他实例;正常 destroy 回调可挂载替代实例,旧实例重复销毁不移除其 DOM。UMD 与 legacy 三浏览器通过。"
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-05",
|
||||
"title": "Failed constructor leaves initialized markup/listeners until explicit probe cleanup",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-04"
|
||||
],
|
||||
@@ -95,7 +102,14 @@
|
||||
"refactor/baselines/lifecycle-coverage.md"
|
||||
],
|
||||
"compatibleResolution": "按对应 CORE 任务修复,保持合法旧调用;异步取消需明确结算策略,不把 pending 或重复销毁当正常兼容要求。",
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。"
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-04-instance-lifecycle.md",
|
||||
"refactor/baselines/instance-lifecycle-validation.json",
|
||||
"test/browser/lifecycle.spec.js",
|
||||
"test/instance-lifecycle.test.js"
|
||||
],
|
||||
"resolutionRationale": "失败构造释放已初始化的监听/已接入定时器并恢复原 DOM/SSR 节点;proxy、插件及 Events 半初始化失败和清理同时抛错均有真实浏览器回归。未迁移的字幕/SDK 等资源仍由对应任务验收。"
|
||||
},
|
||||
{
|
||||
"id": "BASE-DOM-01",
|
||||
@@ -251,7 +265,7 @@
|
||||
"id": "BASE-PERF-01",
|
||||
"title": "销毁后的 resize 防抖重新创建 notice timer",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-17",
|
||||
"CORE-04",
|
||||
@@ -261,7 +275,14 @@
|
||||
"refactor/baselines/performance.json"
|
||||
],
|
||||
"compatibleResolution": "为 debounce/晚到 UI 工作建立销毁归属;不靠等待自然过期掩盖。",
|
||||
"closureCriteria": "正常 resize 保持,destroy 后无该回调及新 notice timer。"
|
||||
"closureCriteria": "正常 resize 保持,destroy 后无该回调及新 notice timer。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-04-instance-lifecycle.md",
|
||||
"refactor/baselines/instance-lifecycle-validation.json",
|
||||
"test/browser/lifecycle.spec.js",
|
||||
"test/instance-lifecycle.test.js"
|
||||
],
|
||||
"resolutionRationale": "resize timeout 由实例 scope 取消,正常 burst 合并保持;销毁后该 resize 不再执行,Notice timer 为 null,已排队 UI 工作不再重建提示。"
|
||||
},
|
||||
{
|
||||
"id": "BASE-SOURCE-01",
|
||||
|
||||
+5
-2
@@ -810,11 +810,14 @@
|
||||
"ENG-05",
|
||||
"BASE-07"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "构造失败回收、重复/重入 destroy、instances 修复",
|
||||
"acceptance": "事件顺序、removeHtml、多实例和销毁中异步有真实回归;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-04/05 并增加候选正向回归;协调 BASE-PERF-01 销毁后定时器与回调归属",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-11-CORE-04-instance-lifecycle.md",
|
||||
"baselines/instance-lifecycle-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CORE-05",
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: repeated destroy preserves other live instances`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const { Artplayer } = window
|
||||
const create = () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
return new Artplayer({ container, url: '/test/pattern.mp4', muted: true })
|
||||
}
|
||||
const first = create()
|
||||
const second = create()
|
||||
let destroys = 0
|
||||
first.on('destroy', () => destroys++)
|
||||
first.destroy(false)
|
||||
first.destroy(true)
|
||||
const result = {
|
||||
secondRegistered: Artplayer.instances.includes(second),
|
||||
retained: !!first.template.$container.querySelector('.art-destroy'),
|
||||
destroys,
|
||||
firstRemoved: !Artplayer.instances.includes(first),
|
||||
}
|
||||
second.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ secondRegistered: core === 'candidate', retained: core === 'candidate', destroys: core === 'candidate' ? 1 : 2, firstRemoved: true })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: constructor plugin failure restores original DOM and clears resources', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
const result = await page.evaluate(async () => {
|
||||
const { Artplayer } = window
|
||||
const container = document.createElement('div')
|
||||
container.dataset.artId = 'original'
|
||||
container.innerHTML = '<button>original</button>'
|
||||
document.body.append(container)
|
||||
const original = container.firstChild
|
||||
let originalClicks = 0
|
||||
original.addEventListener('click', () => originalClicks++)
|
||||
const error = new Error('plugin init failure')
|
||||
let failed
|
||||
let observed
|
||||
let destroys = 0
|
||||
let mounted = 0
|
||||
let customTypes = 0
|
||||
let reservedDuringRollback = false
|
||||
try {
|
||||
void new Artplayer({
|
||||
container,
|
||||
url: '/test/pattern.mp4',
|
||||
customType: { mp4() { customTypes++ } },
|
||||
settings: [{ name: 'delayed', html: 'Delayed', mounted() { mounted++ } }],
|
||||
plugins: [(art) => {
|
||||
failed = art
|
||||
art.notice.show = 'temporary'
|
||||
art.on('destroy', () => {
|
||||
destroys++
|
||||
try {
|
||||
void new Artplayer({ container, url: '/test/pattern.mp4' })
|
||||
}
|
||||
catch (error) { reservedDuringRollback = error.message === 'Cannot mount multiple instances on the same dom element' }
|
||||
throw new Error('controlled plugin cleanup failure')
|
||||
})
|
||||
throw error
|
||||
}],
|
||||
})
|
||||
}
|
||||
catch (actual) { observed = actual === error }
|
||||
// A timer turn exposes cancelled mount/custom-type continuations.
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
original.click()
|
||||
const result = {
|
||||
observed,
|
||||
sameNode: container.firstChild === original,
|
||||
html: container.innerHTML,
|
||||
artId: container.dataset.artId,
|
||||
originalClicks,
|
||||
destroyed: failed.isDestroy,
|
||||
registered: Artplayer.instances.includes(failed),
|
||||
listeners: failed.events.destroyEvents.size,
|
||||
timer: failed.notice.timer,
|
||||
destroys,
|
||||
mounted,
|
||||
customTypes,
|
||||
reservedDuringRollback,
|
||||
}
|
||||
const replacement = new Artplayer({ container, url: '/test/pattern.mp4' })
|
||||
replacement.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ observed: true, sameNode: true, html: '<button>original</button>', artId: 'original', originalClicks: 1, destroyed: true, registered: false, listeners: 0, timer: null, destroys: 1, mounted: 0, customTypes: 0, reservedDuringRollback: true })
|
||||
})
|
||||
|
||||
test('candidate: early proxy failure restores SSR nodes, attributes and text', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = window.Artplayer.html
|
||||
container.setAttribute('data-test', 'SSR')
|
||||
document.body.append(container)
|
||||
const original = container.innerHTML
|
||||
const video = container.querySelector('video')
|
||||
const player = container.querySelector('.art-video-player')
|
||||
const error = new Error('proxy failure')
|
||||
let sameError = false
|
||||
try {
|
||||
void new window.Artplayer({ container, useSSR: true, url: '/test/pattern.mp4', proxy() {
|
||||
player.setAttribute('data-modified', 'yes')
|
||||
player.append(document.createTextNode('changed'))
|
||||
throw error
|
||||
} })
|
||||
}
|
||||
catch (actual) { sameError = actual === error }
|
||||
return {
|
||||
sameError,
|
||||
sameHTML: original === container.innerHTML,
|
||||
sameVideo: video === container.querySelector('video'),
|
||||
artId: container.getAttribute('data-art-id'),
|
||||
data: container.getAttribute('data-test'),
|
||||
count: window.Artplayer.instances.length,
|
||||
}
|
||||
})
|
||||
expect(result).toEqual({ sameError: true, sameHTML: true, sameVideo: true, artId: null, data: 'SSR', count: 0 })
|
||||
})
|
||||
|
||||
test('candidate: reentrant destroy keeps sequence and does not repeat callbacks', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const art = new window.Artplayer({ container, url: '/test/pattern.mp4' })
|
||||
const calls = []
|
||||
let replacement
|
||||
const reset = art.reset.bind(art)
|
||||
art.reset = () => {
|
||||
calls.push(['reset', art.isDestroy])
|
||||
art.destroy(true)
|
||||
reset()
|
||||
}
|
||||
art.on('destroy', () => {
|
||||
calls.push(['destroy', art.isDestroy, window.Artplayer.instances.includes(art), art.events.destroyEvents.size, !!container.querySelector('.art-destroy')])
|
||||
art.destroy(true)
|
||||
replacement = new window.Artplayer({ container, url: '/test/pattern.mp4' })
|
||||
})
|
||||
art.destroy(false)
|
||||
art.destroy(true)
|
||||
const result = { calls, replacementRegistered: window.Artplayer.instances.includes(replacement), players: container.querySelectorAll('.art-video-player').length }
|
||||
replacement.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ calls: [['reset', false], ['destroy', true, false, 0, true]], replacementRegistered: true, players: 1 })
|
||||
})
|
||||
|
||||
test('candidate: destroying cancels queued resize, notice, RAF, reconnect and mounted work', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
const result = await page.evaluate(async () => {
|
||||
const { Artplayer } = window
|
||||
Artplayer.USE_RAF = true
|
||||
Artplayer.RESIZE_TIME = 0
|
||||
Artplayer.RECONNECT_SLEEP_TIME = 0
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
let mounted = 0
|
||||
const art = new Artplayer({ container, url: '/test/pattern.mp4', settings: [{ html: 'Delayed', mounted() {
|
||||
mounted++
|
||||
} }] })
|
||||
let resize = 0
|
||||
let reconnect = 0
|
||||
let raf = 0
|
||||
art.on('resize', () => resize++)
|
||||
art.on('error', () => reconnect++)
|
||||
art.on('raf', () => raf++)
|
||||
art.notice.show = 'before'
|
||||
art.emit('window:resize')
|
||||
art.emit('video:error', new Error('controlled retry'))
|
||||
art.destroy(false)
|
||||
const html = container.innerHTML
|
||||
await new Promise(resolve => requestAnimationFrame(resolve))
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
return {
|
||||
resize,
|
||||
reconnect,
|
||||
raf,
|
||||
mounted,
|
||||
timer: art.notice.timer,
|
||||
sameHTML: container.innerHTML === html,
|
||||
src: art.video.getAttribute('src'),
|
||||
listeners: art.events.destroyEvents.size,
|
||||
}
|
||||
})
|
||||
expect(result).toEqual({ resize: 0, reconnect: 0, raf: 0, mounted: 0, timer: null, sameHTML: true, src: null, listeners: 0 })
|
||||
})
|
||||
|
||||
test('candidate: constructor can synchronously destroy without entering the registry', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const art = new window.Artplayer({ container, url: '/test/pattern.mp4', plugins: [(art) => {
|
||||
art.destroy()
|
||||
return { name: 'closed' }
|
||||
}] })
|
||||
return { destroyed: art.isDestroy, count: window.Artplayer.instances.length, children: container.children.length }
|
||||
})
|
||||
expect(result).toEqual({ destroyed: true, count: 0, children: 0 })
|
||||
})
|
||||
|
||||
test('candidate: partial Events initialization releases listeners before its property is assigned', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = '<span>before</span>'
|
||||
document.body.append(container)
|
||||
const add = EventTarget.prototype.addEventListener
|
||||
const remove = EventTarget.prototype.removeEventListener
|
||||
const active = []
|
||||
const error = new Error('controlled global listener failure')
|
||||
let failed
|
||||
let sameError = false
|
||||
EventTarget.prototype.addEventListener = function (name, callback, options) {
|
||||
if (this === window && name === 'orientationchange')
|
||||
throw error
|
||||
const result = add.call(this, name, callback, options)
|
||||
if (this === window || this === document)
|
||||
active.push({ target: this, name, callback })
|
||||
return result
|
||||
}
|
||||
EventTarget.prototype.removeEventListener = function (name, callback, options) {
|
||||
const result = remove.call(this, name, callback, options)
|
||||
const index = active.findIndex(item => item.target === this && item.name === name && item.callback === callback)
|
||||
if (index !== -1)
|
||||
active.splice(index, 1)
|
||||
return result
|
||||
}
|
||||
try {
|
||||
void new window.Artplayer({ container, url: '/test/pattern.mp4', proxy(art) {
|
||||
failed = art
|
||||
return document.createElement('video')
|
||||
} })
|
||||
}
|
||||
catch (actual) { sameError = actual === error }
|
||||
finally {
|
||||
EventTarget.prototype.addEventListener = add
|
||||
EventTarget.prototype.removeEventListener = remove
|
||||
}
|
||||
return { sameError, assigned: Object.hasOwn(failed, 'events'), active: active.length, destroyed: failed.isDestroy, html: container.innerHTML, count: window.Artplayer.instances.length }
|
||||
})
|
||||
expect(result).toEqual({ sameError: true, assigned: false, active: 0, destroyed: true, html: '<span>before</span>', count: 0 })
|
||||
})
|
||||
|
||||
test('candidate: live resize still coalesces and destroy cancels the next burst', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
window.Artplayer.RESIZE_TIME = 0
|
||||
window.art = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true })
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const art = window.art
|
||||
let resized = 0
|
||||
art.on('resize', () => resized++)
|
||||
const first = new Promise(resolve => art.once('resize', resolve))
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
await first
|
||||
const beforeDestroy = resized
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
art.destroy()
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
return { beforeDestroy, resized, timer: art.notice.timer }
|
||||
})
|
||||
expect(result).toEqual({ beforeDestroy: 1, resized: 1, timer: null })
|
||||
})
|
||||
|
||||
test('candidate: a container is reserved during reentrant initialization', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
const result = await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
let message
|
||||
const art = new window.Artplayer({ container, url: '/test/pattern.mp4', proxy() {
|
||||
try {
|
||||
void new window.Artplayer({ container, url: '/test/pattern.mp4' })
|
||||
}
|
||||
catch (error) { message = error.message }
|
||||
return document.createElement('video')
|
||||
} })
|
||||
const result = { message, registered: window.Artplayer.instances.includes(art), players: container.querySelectorAll('.art-video-player').length, count: window.Artplayer.instances.length }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ message: 'Cannot mount multiple instances on the same dom element', registered: true, players: 1, count: 1 })
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { test } from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const file = 'packages/artplayer/src/lifecycle/instance'
|
||||
const { beginLifecycle, destroyInstance, finishLifecycle, getScope, urlMix } = await loadModules({
|
||||
...Object.fromEntries(['beginLifecycle', 'destroyInstance', 'finishLifecycle', 'getScope'].map(name => [name, { file, name }])),
|
||||
urlMix: 'packages/artplayer/src/player/urlMix',
|
||||
})
|
||||
|
||||
test('instance teardown preserves order, public state and first-call removeHtml across reentry', () => {
|
||||
const calls = []
|
||||
const instances = []
|
||||
const owner = {
|
||||
isDestroy: false,
|
||||
reset() {
|
||||
calls.push(['reset', this.isDestroy])
|
||||
destroyInstance(this, instances, true, true)
|
||||
},
|
||||
template: { $video: {}, destroy(removeHtml) {
|
||||
calls.push(['template', removeHtml])
|
||||
} },
|
||||
emit(name) {
|
||||
calls.push([name, this.isDestroy, instances.includes(this)])
|
||||
},
|
||||
}
|
||||
const keys = Object.keys(owner)
|
||||
beginLifecycle(owner)
|
||||
assert.deepEqual(Object.keys(owner), keys)
|
||||
getScope(owner).add(() => {
|
||||
calls.push(['resources', owner.isDestroy])
|
||||
})
|
||||
assert(finishLifecycle(owner))
|
||||
instances.push(owner)
|
||||
destroyInstance(owner, instances, false, true)
|
||||
destroyInstance(owner, instances, true, true)
|
||||
assert.deepEqual(calls, [['reset', false], ['resources', false], ['template', false], ['destroy', true, false]])
|
||||
})
|
||||
|
||||
test('reset failure still releases resources, removes the instance and dispatches destroy', () => {
|
||||
const error = new Error('reset failed')
|
||||
let released = false
|
||||
let emitted = false
|
||||
const owner = { isDestroy: false, reset() {
|
||||
throw error
|
||||
}, template: { $video: {}, destroy() {} }, emit() { emitted = true } }
|
||||
const instances = [owner]
|
||||
beginLifecycle(owner)
|
||||
getScope(owner).add(() => {
|
||||
released = true
|
||||
})
|
||||
assert.throws(() => destroyInstance(owner, instances, true, true), value => value === error)
|
||||
assert(released && emitted && owner.isDestroy)
|
||||
assert.deepEqual(instances, [])
|
||||
destroyInstance(owner, instances, true, true)
|
||||
})
|
||||
|
||||
test('destroy listener error propagates unchanged after internal cleanup and remains consumed', () => {
|
||||
const error = new Error('listener failed')
|
||||
const sibling = {}
|
||||
const owner = { isDestroy: false, reset() {}, emit() {
|
||||
throw error
|
||||
} }
|
||||
const instances = [owner, sibling]
|
||||
beginLifecycle(owner)
|
||||
assert.throws(() => destroyInstance(owner, instances, true, false), value => value === error)
|
||||
assert(getScope(owner).closed)
|
||||
destroyInstance(owner, instances, true, false)
|
||||
assert.deepEqual(instances, [sibling])
|
||||
})
|
||||
|
||||
test('custom-type continuation cannot start after its timer resolves but before its owner is destroyed', async (context) => {
|
||||
context.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
let calls = 0
|
||||
const owner = {
|
||||
isDestroy: false,
|
||||
reset() {},
|
||||
emit() {},
|
||||
option: { type: 'custom', customType: { custom() { calls++ } } },
|
||||
template: { $video: { src: '' }, destroy() {} },
|
||||
loading: { show: false },
|
||||
}
|
||||
beginLifecycle(owner)
|
||||
urlMix(owner)
|
||||
owner.url = 'test.custom'
|
||||
context.mock.timers.tick(0)
|
||||
destroyInstance(owner, [], true, false)
|
||||
await Promise.resolve()
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(owner.loading.show, false)
|
||||
})
|
||||
@@ -4,10 +4,10 @@ import { test } from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const source = 'packages/artplayer/src/lifecycle/'
|
||||
const { ResourceScope, ResourceCleanupError, timeout, animationFrame, listen, requestController, objectURL, Emitter } = await loadModules({
|
||||
const { ResourceScope, ResourceCleanupError, timeout, animationFrame, listen, requestController, objectURL, wait, Emitter } = await loadModules({
|
||||
ResourceScope: `${source}scope`,
|
||||
ResourceCleanupError: { file: `${source}scope`, name: 'ResourceCleanupError' },
|
||||
...Object.fromEntries(['timeout', 'animationFrame', 'listen', 'requestController', 'objectURL'].map(name => [name, { file: `${source}resources`, name }])),
|
||||
...Object.fromEntries(['timeout', 'animationFrame', 'listen', 'requestController', 'objectURL', 'wait'].map(name => [name, { file: `${source}resources`, name }])),
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
})
|
||||
|
||||
@@ -194,3 +194,16 @@ test('request controller capability absence does not prevent synchronous cleanup
|
||||
assert.equal(requestController(scope), undefined)
|
||||
scope.dispose()
|
||||
})
|
||||
|
||||
test('owned waits resolve on cancellation and preserve normal timer-to-microtask completion', async (context) => {
|
||||
context.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const scope = new ResourceScope()
|
||||
const completed = wait(scope, 5)
|
||||
const cancelled = wait(scope, 10)
|
||||
context.mock.timers.tick(5)
|
||||
assert.equal(await completed, true)
|
||||
scope.dispose()
|
||||
assert.equal(await cancelled, false)
|
||||
assert.equal(await wait(scope), false)
|
||||
context.mock.timers.tick(20)
|
||||
})
|
||||
Reference in new issue
Block a user