refactor(core): CORE-04 scope initialization and make destruction reentrant-safe

This commit is contained in:
Harvey Zhao committed 2026-09-11 00:43:49 +08:00
1 parent 3e38dd0d19
commit 4eec8112fa
32 files changed
+2779 -189

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var 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
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))n.call(e,r)&&s(t,r,e[r]);if(a)for(var r of a(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,a)=>{var n=t=>{try{s(r.next(t))}catch(e){a(e)}},i=t=>{try{s(r.throw(t))}catch(e){a(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(n,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,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
View File
@@ -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
View File
@@ -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",
+58 -5
View File
@@ -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;
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var 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
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))n.call(e,r)&&s(t,r,e[r]);if(a)for(var r of a(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,a)=>{var n=t=>{try{s(r.next(t))}catch(e){a(e)}},i=t=>{try{s(r.throw(t))}catch(e){a(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(n,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,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
View File
@@ -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;
+5 -2
View File
@@ -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
})
}
+9
View File
@@ -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)
+8 -2
View File
@@ -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())
+7 -3
View File
@@ -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()
})
}
}
+46 -36
View File
@@ -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)
}
}
+6 -3
View File
@@ -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
}
}
}
}
+4
View File
@@ -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 },
+7 -3
View File
@@ -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')
}
})
+7 -3
View File
@@ -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
}
},
+3 -1
View File
@@ -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)
}
}
+3
View File
@@ -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
View File
@@ -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)
+4
View File
@@ -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 直接改构造与销毁。
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+298
View File
@@ -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 })
})
+92
View File
@@ -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)
})
+15 -2
View File
@@ -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)
})