mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
refactor(core): CORE-09 isolate source operations and settle cancelled switches
This commit is contained in:
1 parent
874dac9f22
commit
44f4e083a9
29 files changed
+4512
-255
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))n.call(e,r)&&s(t,r,e[r]);if(a)for(var r of a(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,a)=>{var n=t=>{try{s(r.next(t))}catch(e){a(e)}},i=t=>{try{s(r.throw(t))}catch(e){a(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(n,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
+251
-62
@@ -1167,8 +1167,8 @@ function isClosing(owner) {
|
||||
return state2.destroying || state2.scope.closed;
|
||||
}
|
||||
function ownContainer(owner, container, rollback) {
|
||||
const current = containers.get(container);
|
||||
if (current && current !== owner)
|
||||
const current2 = containers.get(container);
|
||||
if (current2 && current2 !== owner)
|
||||
throw new Error("Cannot mount multiple instances on the same dom element");
|
||||
const state2 = stateOf(owner);
|
||||
state2.rollback = rollback;
|
||||
@@ -3420,6 +3420,64 @@ function playingMix(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
const current = /* @__PURE__ */ new WeakMap();
|
||||
const assignments = /* @__PURE__ */ new WeakMap();
|
||||
function captureSource(owner) {
|
||||
const operation = current.get(owner);
|
||||
return () => current.get(owner) === operation && (!operation || operation.active());
|
||||
}
|
||||
function beginSource(owner) {
|
||||
const previous = current.get(owner);
|
||||
const scope = getScope(owner).child();
|
||||
const operation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
acceptingEvents: false,
|
||||
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation
|
||||
};
|
||||
current.set(owner, operation);
|
||||
scope.add(() => {
|
||||
if (current.get(owner) === operation)
|
||||
current.delete(owner);
|
||||
operation.onAssigned = void 0;
|
||||
operation.onError = void 0;
|
||||
});
|
||||
previous?.scope.dispose();
|
||||
return operation;
|
||||
}
|
||||
function takeAssignment(owner) {
|
||||
const operation = assignments.get(owner);
|
||||
assignments.delete(owner);
|
||||
return operation || beginSource(owner);
|
||||
}
|
||||
function finishAssignment(operation) {
|
||||
operation.assigned = true;
|
||||
const callback = operation.onAssigned;
|
||||
operation.onAssigned = void 0;
|
||||
if (operation.active())
|
||||
callback?.();
|
||||
}
|
||||
function assignUrl(owner, operation, url) {
|
||||
assignments.set(owner, operation);
|
||||
operation.acceptingEvents = true;
|
||||
try {
|
||||
owner.url = url;
|
||||
} finally {
|
||||
if (assignments.get(owner) === operation) {
|
||||
assignments.delete(owner);
|
||||
finishAssignment(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
function failSource(operation, error2) {
|
||||
if (!operation.active())
|
||||
return;
|
||||
if (operation.onError)
|
||||
operation.onError(error2);
|
||||
else
|
||||
console.warn("Failed to initialize ArtPlayer source:", error2);
|
||||
operation.scope.dispose();
|
||||
}
|
||||
function playMix(art) {
|
||||
const {
|
||||
i18n,
|
||||
@@ -3430,11 +3488,21 @@ function playMix(art) {
|
||||
} = art;
|
||||
def(art, "play", {
|
||||
async value() {
|
||||
const active = captureSource(art);
|
||||
const result = await $video.play();
|
||||
notice.show = i18n.get("Play");
|
||||
if (!active())
|
||||
return result;
|
||||
const message = i18n.get("Play");
|
||||
if (!active())
|
||||
return result;
|
||||
notice.show = message;
|
||||
if (!active())
|
||||
return result;
|
||||
art.emit("play");
|
||||
if (option.mutex) {
|
||||
for (let index = 0; index < instances2.length; index++) {
|
||||
if (!active())
|
||||
break;
|
||||
const instance = instances2[index];
|
||||
if (instance !== art) {
|
||||
instance.pause();
|
||||
@@ -3617,54 +3685,140 @@ function subtitleOffsetMix(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function switchMix(art) {
|
||||
function switchUrl(url, currentTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (url === art.url) {
|
||||
resolve();
|
||||
function listenSource(scope, art, name, callback, once = true) {
|
||||
let fired = false;
|
||||
let release;
|
||||
const guarded = (event) => {
|
||||
if (scope.closed || fired)
|
||||
return;
|
||||
if (once) {
|
||||
fired = true;
|
||||
release();
|
||||
}
|
||||
callback(event);
|
||||
};
|
||||
art[once ? "once" : "on"](name, guarded);
|
||||
release = scope.add(() => {
|
||||
art.off(name, guarded);
|
||||
});
|
||||
return guarded;
|
||||
}
|
||||
function switchSource(art, url, currentTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isClosing(art) || url === art.url) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const operation = beginSource(art);
|
||||
const scope = operation.scope.child();
|
||||
let settled = false;
|
||||
const settle = (failed = false, error2) => {
|
||||
if (settled)
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
operation.onError = void 0;
|
||||
operation.onAssigned = void 0;
|
||||
scope.dispose();
|
||||
if (failed)
|
||||
reject(error2);
|
||||
else
|
||||
resolve();
|
||||
if (failed)
|
||||
operation.scope.dispose();
|
||||
};
|
||||
scope.add(() => {
|
||||
settle();
|
||||
});
|
||||
const active = () => !settled && operation.active();
|
||||
const fail = (error2) => settle(true, error2);
|
||||
operation.onError = fail;
|
||||
try {
|
||||
const { playing, aspectRatio: aspectRatio2, playbackRate: playbackRate2 } = art;
|
||||
art.pause();
|
||||
art.url = url;
|
||||
art.notice.show = "";
|
||||
const handlers = {};
|
||||
handlers.error = (error2) => {
|
||||
art.off("video:canplay", handlers.canplay);
|
||||
art.off("video:loadedmetadata", handlers.metadata);
|
||||
reject(error2);
|
||||
};
|
||||
handlers.metadata = () => {
|
||||
art.currentTime = currentTime;
|
||||
};
|
||||
handlers.canplay = async () => {
|
||||
art.off("video:error", handlers.error);
|
||||
art.playbackRate = playbackRate2;
|
||||
art.aspectRatio = aspectRatio2;
|
||||
if (playing) {
|
||||
await silencePromise(art.play());
|
||||
if (!active())
|
||||
return;
|
||||
const readiness = scope.child();
|
||||
const handlers = {
|
||||
"video:error": fail,
|
||||
"video:loadedmetadata": () => {
|
||||
if (!active())
|
||||
return;
|
||||
try {
|
||||
art.currentTime = currentTime;
|
||||
} catch (error2) {
|
||||
fail(error2);
|
||||
}
|
||||
},
|
||||
"video:canplay": () => {
|
||||
if (!active())
|
||||
return;
|
||||
readiness.dispose();
|
||||
const resume = async () => {
|
||||
art.playbackRate = playbackRate2;
|
||||
if (!active())
|
||||
return;
|
||||
art.aspectRatio = aspectRatio2;
|
||||
if (!active())
|
||||
return;
|
||||
if (playing) {
|
||||
try {
|
||||
await art.play();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (!active())
|
||||
return;
|
||||
art.notice.show = "";
|
||||
settle();
|
||||
};
|
||||
void resume().catch(fail);
|
||||
}
|
||||
art.notice.show = "";
|
||||
resolve();
|
||||
};
|
||||
art.once("video:error", handlers.error);
|
||||
art.once("video:loadedmetadata", handlers.metadata);
|
||||
art.once("video:canplay", handlers.canplay);
|
||||
});
|
||||
}
|
||||
def(art, "switchQuality", {
|
||||
value: (url) => {
|
||||
return switchUrl(url, art.currentTime);
|
||||
const capture = scope.child();
|
||||
const queued = [];
|
||||
const names = Object.keys(handlers);
|
||||
for (const name of names) {
|
||||
listenSource(capture, art, name, (event) => {
|
||||
if (operation.acceptingEvents)
|
||||
queued.push([name, event]);
|
||||
}, false);
|
||||
}
|
||||
assignUrl(art, operation, url);
|
||||
if (!active())
|
||||
return;
|
||||
art.notice.show = "";
|
||||
if (!active())
|
||||
return;
|
||||
const activate = () => {
|
||||
capture.dispose();
|
||||
if (!url) {
|
||||
settle();
|
||||
return;
|
||||
}
|
||||
const dispatch = {};
|
||||
for (const name of names)
|
||||
dispatch[name] = listenSource(readiness, art, name, handlers[name]);
|
||||
for (const [name, event] of queued) {
|
||||
if (!active() || readiness.closed)
|
||||
break;
|
||||
dispatch[name](event);
|
||||
}
|
||||
queued.length = 0;
|
||||
};
|
||||
if (operation.assigned)
|
||||
activate();
|
||||
else
|
||||
operation.onAssigned = activate;
|
||||
} catch (error2) {
|
||||
fail(error2);
|
||||
}
|
||||
});
|
||||
def(art, "switchUrl", {
|
||||
value: (url) => {
|
||||
return switchUrl(url, 0);
|
||||
}
|
||||
});
|
||||
def(art, "switch", {
|
||||
set: art.switchUrl
|
||||
});
|
||||
}
|
||||
function switchMix(art) {
|
||||
const switchUrl = (url) => switchSource(art, url, 0);
|
||||
def(art, "switchQuality", { value: (url) => switchSource(art, url, art.currentTime) });
|
||||
def(art, "switchUrl", { value: switchUrl });
|
||||
def(art, "switch", { set: switchUrl });
|
||||
}
|
||||
function themeMix(art) {
|
||||
def(art, "theme", {
|
||||
@@ -3767,40 +3921,75 @@ function typeMix(art) {
|
||||
});
|
||||
}
|
||||
function urlMix(art) {
|
||||
const {
|
||||
option,
|
||||
template: { $video }
|
||||
} = art;
|
||||
const { option, template: { $video } } = art;
|
||||
def(art, "url", {
|
||||
get() {
|
||||
return $video.src;
|
||||
},
|
||||
async set(newUrl) {
|
||||
if (newUrl) {
|
||||
if (isClosing(art))
|
||||
return;
|
||||
const operation = takeAssignment(art);
|
||||
operation.acceptingEvents = false;
|
||||
try {
|
||||
if (!newUrl) {
|
||||
if (await wait(operation.scope) && operation.active())
|
||||
art.loading.show = true;
|
||||
return;
|
||||
}
|
||||
const oldUrl = art.url;
|
||||
const typeName = option.type || getExt(newUrl);
|
||||
const typeCallback = option.customType[typeName];
|
||||
if (typeName && typeCallback) {
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
if (!await wait(operation.scope) || !operation.active())
|
||||
return;
|
||||
art.loading.show = true;
|
||||
typeCallback.call(art, $video, newUrl, art);
|
||||
} else {
|
||||
URL.revokeObjectURL(oldUrl);
|
||||
$video.src = newUrl;
|
||||
}
|
||||
if (!operation.active())
|
||||
return;
|
||||
const capture = operation.scope.child();
|
||||
let ready = false;
|
||||
let failed = false;
|
||||
listenSource(capture, art, "video:canplay", () => {
|
||||
ready = !failed;
|
||||
});
|
||||
listenSource(capture, art, "video:error", () => {
|
||||
failed = true;
|
||||
});
|
||||
try {
|
||||
operation.acceptingEvents = true;
|
||||
if (typeName && typeCallback) {
|
||||
const result = typeCallback.call(art, $video, newUrl, art);
|
||||
void Promise.resolve(result).catch((error2) => failSource(operation, error2));
|
||||
} else {
|
||||
if (oldUrl)
|
||||
URL.revokeObjectURL(oldUrl);
|
||||
if (!operation.active())
|
||||
return;
|
||||
$video.src = newUrl;
|
||||
}
|
||||
} finally {
|
||||
capture.dispose();
|
||||
}
|
||||
if (!operation.active())
|
||||
return;
|
||||
if (oldUrl !== art.url) {
|
||||
art.option.url = newUrl;
|
||||
if (art.isReady && oldUrl) {
|
||||
art.once("video:canplay", () => {
|
||||
art.emit("restart", newUrl);
|
||||
});
|
||||
if (operation.active() && art.isReady && oldUrl) {
|
||||
const restart = () => {
|
||||
if (operation.active())
|
||||
art.emit("restart", newUrl);
|
||||
};
|
||||
if (ready)
|
||||
restart();
|
||||
else if (!failed)
|
||||
listenSource(operation.scope, art, "video:canplay", restart);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return;
|
||||
art.loading.show = true;
|
||||
} catch (error2) {
|
||||
failSource(operation, error2);
|
||||
} finally {
|
||||
finishAssignment(operation);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
"ci:build": "yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.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/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
"test:browser:install": "playwright install chromium firefox webkit",
|
||||
|
||||
@@ -318,6 +318,34 @@ finding, not a claim that a standalone debounce can know when its owner is destr
|
||||
|
||||
## Verification and maintenance
|
||||
|
||||
### Source operations (CORE-09)
|
||||
|
||||
`player/urlMix.ts` and `player/switchMix.ts` preserve the public descriptors.
|
||||
`source/types.ts` defines minimal structural media/host capabilities;
|
||||
`source/operation.ts` owns a current generation and its resource scope;
|
||||
`source/listen.ts` owns individual media subscriptions; `source/switch.ts`
|
||||
coordinates assignment, readiness, state restoration and Promise settlement.
|
||||
|
||||
A new URL assignment supersedes the prior operation, including direct `art.url`
|
||||
writes. Superseded/destroyed switches fulfill with undefined; actual source errors
|
||||
reject with their original value. Fulfillment does not prove the source became
|
||||
active. Same-string URL calls remain no-ops. Internal resume rejection stays
|
||||
handled while public play retains rejection. Synchronous proxy events are buffered
|
||||
until assignment finishes, and operation cleanup does not remove user listeners.
|
||||
|
||||
customType still receives the real media object and player. Its arbitrary external
|
||||
SDK work cannot be cancelled generically: adapters must own that cleanup. The core
|
||||
guards deferred invocation, ignores obsolete returned failures, and cancels its own
|
||||
continuations. Direct assignment failures are reported with console.warn; switch
|
||||
callers receive the original rejection. `playMix.ts` captures the current source:
|
||||
its native result/rejection is preserved, while obsolete notice/event/mutex effects
|
||||
are suppressed. Object URL ownership, other playback migration and reconnect
|
||||
generations remain CORE-19/10/11 work.
|
||||
|
||||
`test/source.test.js` checks cancellation, reentry, synchronous events and cleanup;
|
||||
`test/browser/source.spec.js` compares published/candidate real-media switching and
|
||||
destruction and verifies candidate customType and resume-failure behavior.
|
||||
|
||||
Run `yarn test:unit` for shared published/workspace utility contracts and controlled
|
||||
timers. `test/utils.test.js` also accepts `ARTPLAYER_TEST_CORE` to test an actual
|
||||
UMD, legacy or ESM file. `test/types/utils-source.ts` checks source inference, receiver
|
||||
|
||||
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))n.call(e,r)&&s(t,r,e[r]);if(a)for(var r of a(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,a)=>{var n=t=>{try{s(r.next(t))}catch(e){a(e)}},i=t=>{try{s(r.throw(t))}catch(e){a(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(n,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
+251
-62
@@ -1167,8 +1167,8 @@ function isClosing(owner) {
|
||||
return state2.destroying || state2.scope.closed;
|
||||
}
|
||||
function ownContainer(owner, container, rollback) {
|
||||
const current = containers.get(container);
|
||||
if (current && current !== owner)
|
||||
const current2 = containers.get(container);
|
||||
if (current2 && current2 !== owner)
|
||||
throw new Error("Cannot mount multiple instances on the same dom element");
|
||||
const state2 = stateOf(owner);
|
||||
state2.rollback = rollback;
|
||||
@@ -3420,6 +3420,64 @@ function playingMix(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
const current = /* @__PURE__ */ new WeakMap();
|
||||
const assignments = /* @__PURE__ */ new WeakMap();
|
||||
function captureSource(owner) {
|
||||
const operation = current.get(owner);
|
||||
return () => current.get(owner) === operation && (!operation || operation.active());
|
||||
}
|
||||
function beginSource(owner) {
|
||||
const previous = current.get(owner);
|
||||
const scope = getScope(owner).child();
|
||||
const operation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
acceptingEvents: false,
|
||||
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation
|
||||
};
|
||||
current.set(owner, operation);
|
||||
scope.add(() => {
|
||||
if (current.get(owner) === operation)
|
||||
current.delete(owner);
|
||||
operation.onAssigned = void 0;
|
||||
operation.onError = void 0;
|
||||
});
|
||||
previous?.scope.dispose();
|
||||
return operation;
|
||||
}
|
||||
function takeAssignment(owner) {
|
||||
const operation = assignments.get(owner);
|
||||
assignments.delete(owner);
|
||||
return operation || beginSource(owner);
|
||||
}
|
||||
function finishAssignment(operation) {
|
||||
operation.assigned = true;
|
||||
const callback = operation.onAssigned;
|
||||
operation.onAssigned = void 0;
|
||||
if (operation.active())
|
||||
callback?.();
|
||||
}
|
||||
function assignUrl(owner, operation, url) {
|
||||
assignments.set(owner, operation);
|
||||
operation.acceptingEvents = true;
|
||||
try {
|
||||
owner.url = url;
|
||||
} finally {
|
||||
if (assignments.get(owner) === operation) {
|
||||
assignments.delete(owner);
|
||||
finishAssignment(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
function failSource(operation, error2) {
|
||||
if (!operation.active())
|
||||
return;
|
||||
if (operation.onError)
|
||||
operation.onError(error2);
|
||||
else
|
||||
console.warn("Failed to initialize ArtPlayer source:", error2);
|
||||
operation.scope.dispose();
|
||||
}
|
||||
function playMix(art) {
|
||||
const {
|
||||
i18n,
|
||||
@@ -3430,11 +3488,21 @@ function playMix(art) {
|
||||
} = art;
|
||||
def(art, "play", {
|
||||
async value() {
|
||||
const active = captureSource(art);
|
||||
const result = await $video.play();
|
||||
notice.show = i18n.get("Play");
|
||||
if (!active())
|
||||
return result;
|
||||
const message = i18n.get("Play");
|
||||
if (!active())
|
||||
return result;
|
||||
notice.show = message;
|
||||
if (!active())
|
||||
return result;
|
||||
art.emit("play");
|
||||
if (option.mutex) {
|
||||
for (let index = 0; index < instances2.length; index++) {
|
||||
if (!active())
|
||||
break;
|
||||
const instance = instances2[index];
|
||||
if (instance !== art) {
|
||||
instance.pause();
|
||||
@@ -3617,54 +3685,140 @@ function subtitleOffsetMix(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function switchMix(art) {
|
||||
function switchUrl(url, currentTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (url === art.url) {
|
||||
resolve();
|
||||
function listenSource(scope, art, name, callback, once = true) {
|
||||
let fired = false;
|
||||
let release;
|
||||
const guarded = (event) => {
|
||||
if (scope.closed || fired)
|
||||
return;
|
||||
if (once) {
|
||||
fired = true;
|
||||
release();
|
||||
}
|
||||
callback(event);
|
||||
};
|
||||
art[once ? "once" : "on"](name, guarded);
|
||||
release = scope.add(() => {
|
||||
art.off(name, guarded);
|
||||
});
|
||||
return guarded;
|
||||
}
|
||||
function switchSource(art, url, currentTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isClosing(art) || url === art.url) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const operation = beginSource(art);
|
||||
const scope = operation.scope.child();
|
||||
let settled = false;
|
||||
const settle = (failed = false, error2) => {
|
||||
if (settled)
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
operation.onError = void 0;
|
||||
operation.onAssigned = void 0;
|
||||
scope.dispose();
|
||||
if (failed)
|
||||
reject(error2);
|
||||
else
|
||||
resolve();
|
||||
if (failed)
|
||||
operation.scope.dispose();
|
||||
};
|
||||
scope.add(() => {
|
||||
settle();
|
||||
});
|
||||
const active = () => !settled && operation.active();
|
||||
const fail = (error2) => settle(true, error2);
|
||||
operation.onError = fail;
|
||||
try {
|
||||
const { playing, aspectRatio: aspectRatio2, playbackRate: playbackRate2 } = art;
|
||||
art.pause();
|
||||
art.url = url;
|
||||
art.notice.show = "";
|
||||
const handlers = {};
|
||||
handlers.error = (error2) => {
|
||||
art.off("video:canplay", handlers.canplay);
|
||||
art.off("video:loadedmetadata", handlers.metadata);
|
||||
reject(error2);
|
||||
};
|
||||
handlers.metadata = () => {
|
||||
art.currentTime = currentTime;
|
||||
};
|
||||
handlers.canplay = async () => {
|
||||
art.off("video:error", handlers.error);
|
||||
art.playbackRate = playbackRate2;
|
||||
art.aspectRatio = aspectRatio2;
|
||||
if (playing) {
|
||||
await silencePromise(art.play());
|
||||
if (!active())
|
||||
return;
|
||||
const readiness = scope.child();
|
||||
const handlers = {
|
||||
"video:error": fail,
|
||||
"video:loadedmetadata": () => {
|
||||
if (!active())
|
||||
return;
|
||||
try {
|
||||
art.currentTime = currentTime;
|
||||
} catch (error2) {
|
||||
fail(error2);
|
||||
}
|
||||
},
|
||||
"video:canplay": () => {
|
||||
if (!active())
|
||||
return;
|
||||
readiness.dispose();
|
||||
const resume = async () => {
|
||||
art.playbackRate = playbackRate2;
|
||||
if (!active())
|
||||
return;
|
||||
art.aspectRatio = aspectRatio2;
|
||||
if (!active())
|
||||
return;
|
||||
if (playing) {
|
||||
try {
|
||||
await art.play();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (!active())
|
||||
return;
|
||||
art.notice.show = "";
|
||||
settle();
|
||||
};
|
||||
void resume().catch(fail);
|
||||
}
|
||||
art.notice.show = "";
|
||||
resolve();
|
||||
};
|
||||
art.once("video:error", handlers.error);
|
||||
art.once("video:loadedmetadata", handlers.metadata);
|
||||
art.once("video:canplay", handlers.canplay);
|
||||
});
|
||||
}
|
||||
def(art, "switchQuality", {
|
||||
value: (url) => {
|
||||
return switchUrl(url, art.currentTime);
|
||||
const capture = scope.child();
|
||||
const queued = [];
|
||||
const names = Object.keys(handlers);
|
||||
for (const name of names) {
|
||||
listenSource(capture, art, name, (event) => {
|
||||
if (operation.acceptingEvents)
|
||||
queued.push([name, event]);
|
||||
}, false);
|
||||
}
|
||||
assignUrl(art, operation, url);
|
||||
if (!active())
|
||||
return;
|
||||
art.notice.show = "";
|
||||
if (!active())
|
||||
return;
|
||||
const activate = () => {
|
||||
capture.dispose();
|
||||
if (!url) {
|
||||
settle();
|
||||
return;
|
||||
}
|
||||
const dispatch = {};
|
||||
for (const name of names)
|
||||
dispatch[name] = listenSource(readiness, art, name, handlers[name]);
|
||||
for (const [name, event] of queued) {
|
||||
if (!active() || readiness.closed)
|
||||
break;
|
||||
dispatch[name](event);
|
||||
}
|
||||
queued.length = 0;
|
||||
};
|
||||
if (operation.assigned)
|
||||
activate();
|
||||
else
|
||||
operation.onAssigned = activate;
|
||||
} catch (error2) {
|
||||
fail(error2);
|
||||
}
|
||||
});
|
||||
def(art, "switchUrl", {
|
||||
value: (url) => {
|
||||
return switchUrl(url, 0);
|
||||
}
|
||||
});
|
||||
def(art, "switch", {
|
||||
set: art.switchUrl
|
||||
});
|
||||
}
|
||||
function switchMix(art) {
|
||||
const switchUrl = (url) => switchSource(art, url, 0);
|
||||
def(art, "switchQuality", { value: (url) => switchSource(art, url, art.currentTime) });
|
||||
def(art, "switchUrl", { value: switchUrl });
|
||||
def(art, "switch", { set: switchUrl });
|
||||
}
|
||||
function themeMix(art) {
|
||||
def(art, "theme", {
|
||||
@@ -3767,40 +3921,75 @@ function typeMix(art) {
|
||||
});
|
||||
}
|
||||
function urlMix(art) {
|
||||
const {
|
||||
option,
|
||||
template: { $video }
|
||||
} = art;
|
||||
const { option, template: { $video } } = art;
|
||||
def(art, "url", {
|
||||
get() {
|
||||
return $video.src;
|
||||
},
|
||||
async set(newUrl) {
|
||||
if (newUrl) {
|
||||
if (isClosing(art))
|
||||
return;
|
||||
const operation = takeAssignment(art);
|
||||
operation.acceptingEvents = false;
|
||||
try {
|
||||
if (!newUrl) {
|
||||
if (await wait(operation.scope) && operation.active())
|
||||
art.loading.show = true;
|
||||
return;
|
||||
}
|
||||
const oldUrl = art.url;
|
||||
const typeName = option.type || getExt(newUrl);
|
||||
const typeCallback = option.customType[typeName];
|
||||
if (typeName && typeCallback) {
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
if (!await wait(operation.scope) || !operation.active())
|
||||
return;
|
||||
art.loading.show = true;
|
||||
typeCallback.call(art, $video, newUrl, art);
|
||||
} else {
|
||||
URL.revokeObjectURL(oldUrl);
|
||||
$video.src = newUrl;
|
||||
}
|
||||
if (!operation.active())
|
||||
return;
|
||||
const capture = operation.scope.child();
|
||||
let ready = false;
|
||||
let failed = false;
|
||||
listenSource(capture, art, "video:canplay", () => {
|
||||
ready = !failed;
|
||||
});
|
||||
listenSource(capture, art, "video:error", () => {
|
||||
failed = true;
|
||||
});
|
||||
try {
|
||||
operation.acceptingEvents = true;
|
||||
if (typeName && typeCallback) {
|
||||
const result = typeCallback.call(art, $video, newUrl, art);
|
||||
void Promise.resolve(result).catch((error2) => failSource(operation, error2));
|
||||
} else {
|
||||
if (oldUrl)
|
||||
URL.revokeObjectURL(oldUrl);
|
||||
if (!operation.active())
|
||||
return;
|
||||
$video.src = newUrl;
|
||||
}
|
||||
} finally {
|
||||
capture.dispose();
|
||||
}
|
||||
if (!operation.active())
|
||||
return;
|
||||
if (oldUrl !== art.url) {
|
||||
art.option.url = newUrl;
|
||||
if (art.isReady && oldUrl) {
|
||||
art.once("video:canplay", () => {
|
||||
art.emit("restart", newUrl);
|
||||
});
|
||||
if (operation.active() && art.isReady && oldUrl) {
|
||||
const restart = () => {
|
||||
if (operation.active())
|
||||
art.emit("restart", newUrl);
|
||||
};
|
||||
if (ready)
|
||||
restart();
|
||||
else if (!failed)
|
||||
listenSource(operation.scope, art, "video:canplay", restart);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return;
|
||||
art.loading.show = true;
|
||||
} catch (error2) {
|
||||
failSource(operation, error2);
|
||||
} finally {
|
||||
finishAssignment(operation);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PlayHost } from '../media/hosts'
|
||||
import type { PlaybackMethods } from '../media/types'
|
||||
import { captureSource } from '../source/operation'
|
||||
import { def } from '../utils'
|
||||
|
||||
export default function playMix<Media extends Pick<PlaybackMethods, 'play'>>(art: PlayHost<Media>): asserts art is PlayHost<Media> & { play: () => Promise<Awaited<ReturnType<Media['play']>>> } {
|
||||
@@ -13,12 +14,22 @@ export default function playMix<Media extends Pick<PlaybackMethods, 'play'>>(art
|
||||
|
||||
def(art, 'play', {
|
||||
async value() {
|
||||
const active = captureSource(art)
|
||||
const result = await $video.play()
|
||||
notice.show = i18n.get('Play')
|
||||
if (!active())
|
||||
return result
|
||||
const message = i18n.get('Play')
|
||||
if (!active())
|
||||
return result
|
||||
notice.show = message
|
||||
if (!active())
|
||||
return result
|
||||
art.emit('play')
|
||||
|
||||
if (option.mutex) {
|
||||
for (let index = 0; index < instances.length; index++) {
|
||||
if (!active())
|
||||
break
|
||||
const instance = instances[index]!
|
||||
if (instance !== art) {
|
||||
instance.pause()
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { def, silencePromise } from '../utils'
|
||||
|
||||
export default function switchMix(art) {
|
||||
function switchUrl(url, currentTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (url === art.url) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const { playing, aspectRatio, playbackRate } = art
|
||||
|
||||
art.pause()
|
||||
art.url = url
|
||||
art.notice.show = ''
|
||||
|
||||
const handlers = {}
|
||||
|
||||
handlers.error = (error) => {
|
||||
art.off('video:canplay', handlers.canplay)
|
||||
art.off('video:loadedmetadata', handlers.metadata)
|
||||
reject(error)
|
||||
}
|
||||
|
||||
handlers.metadata = () => {
|
||||
art.currentTime = currentTime
|
||||
}
|
||||
|
||||
handlers.canplay = async () => {
|
||||
art.off('video:error', handlers.error)
|
||||
art.playbackRate = playbackRate
|
||||
art.aspectRatio = aspectRatio
|
||||
|
||||
if (playing) {
|
||||
await silencePromise(art.play())
|
||||
}
|
||||
|
||||
art.notice.show = ''
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
art.once('video:error', handlers.error)
|
||||
art.once('video:loadedmetadata', handlers.metadata)
|
||||
art.once('video:canplay', handlers.canplay)
|
||||
})
|
||||
}
|
||||
|
||||
def(art, 'switchQuality', {
|
||||
value: (url) => {
|
||||
return switchUrl(url, art.currentTime)
|
||||
},
|
||||
})
|
||||
|
||||
def(art, 'switchUrl', {
|
||||
value: (url) => {
|
||||
return switchUrl(url, 0)
|
||||
},
|
||||
})
|
||||
|
||||
def(art, 'switch', {
|
||||
set: art.switchUrl,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { SwitchHost, SwitchMethods } from '../source/types'
|
||||
import { switchSource } from '../source/switch'
|
||||
import { def } from '../utils'
|
||||
|
||||
export default function switchMix(art: SwitchHost): asserts art is SwitchHost & SwitchMethods {
|
||||
const switchUrl = (url: string) => switchSource(art, url, 0)
|
||||
def(art, 'switchQuality', { value: (url: string) => switchSource(art, url, art.currentTime) })
|
||||
def(art, 'switchUrl', { value: switchUrl })
|
||||
def(art, 'switch', { set: switchUrl })
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { wait } from '../lifecycle/resources'
|
||||
import { def, getExt } from '../utils'
|
||||
|
||||
export default function urlMix(art) {
|
||||
const {
|
||||
option,
|
||||
template: { $video },
|
||||
} = art
|
||||
|
||||
def(art, 'url', {
|
||||
get() {
|
||||
return $video.src
|
||||
},
|
||||
async set(newUrl) {
|
||||
if (newUrl) {
|
||||
const oldUrl = art.url
|
||||
const typeName = option.type || getExt(newUrl)
|
||||
const typeCallback = option.customType[typeName]
|
||||
|
||||
if (typeName && typeCallback) {
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return
|
||||
art.loading.show = true
|
||||
typeCallback.call(art, $video, newUrl, art)
|
||||
}
|
||||
else {
|
||||
URL.revokeObjectURL(oldUrl)
|
||||
$video.src = newUrl
|
||||
}
|
||||
|
||||
if (oldUrl !== art.url) {
|
||||
art.option.url = newUrl
|
||||
if (art.isReady && oldUrl) {
|
||||
art.once('video:canplay', () => {
|
||||
art.emit('restart', newUrl)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!await wait(getScope(art)) || getScope(art).closed)
|
||||
return
|
||||
art.loading.show = true
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { SourceMedia, UrlHost } from '../source/types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { wait } from '../lifecycle/resources'
|
||||
import { listenSource } from '../source/listen'
|
||||
import { failSource, finishAssignment, takeAssignment } from '../source/operation'
|
||||
import { def, getExt } from '../utils'
|
||||
|
||||
export default function urlMix<Media extends SourceMedia, Host extends UrlHost<Media, Host>>(art: Host): void {
|
||||
const { option, template: { $video } } = art
|
||||
def(art, 'url', {
|
||||
get() {
|
||||
return $video.src
|
||||
},
|
||||
async set(newUrl: string) {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const operation = takeAssignment(art)
|
||||
operation.acceptingEvents = false
|
||||
try {
|
||||
if (!newUrl) {
|
||||
if (await wait(operation.scope) && operation.active())
|
||||
art.loading.show = true
|
||||
return
|
||||
}
|
||||
const oldUrl = art.url
|
||||
const typeName = option.type || getExt(newUrl)
|
||||
const typeCallback = option.customType[typeName]
|
||||
if (typeName && typeCallback) {
|
||||
if (!await wait(operation.scope) || !operation.active())
|
||||
return
|
||||
art.loading.show = true
|
||||
}
|
||||
if (!operation.active())
|
||||
return
|
||||
// Proxies can synchronously dispatch readiness from their src setter.
|
||||
const capture = operation.scope.child()
|
||||
let ready = false
|
||||
let failed = false
|
||||
listenSource(capture, art, 'video:canplay', () => {
|
||||
ready = !failed
|
||||
})
|
||||
listenSource(capture, art, 'video:error', () => {
|
||||
failed = true
|
||||
})
|
||||
try {
|
||||
operation.acceptingEvents = true
|
||||
if (typeName && typeCallback) {
|
||||
const result = typeCallback.call(art, $video, newUrl, art)
|
||||
// Preserve immediate customType invocation semantics while owning rejections.
|
||||
void Promise.resolve(result).catch(error => failSource(operation, error))
|
||||
}
|
||||
else {
|
||||
if (oldUrl)
|
||||
URL.revokeObjectURL(oldUrl)
|
||||
if (!operation.active())
|
||||
return
|
||||
$video.src = newUrl
|
||||
}
|
||||
}
|
||||
finally {
|
||||
capture.dispose()
|
||||
}
|
||||
if (!operation.active())
|
||||
return
|
||||
if (oldUrl !== art.url) {
|
||||
art.option.url = newUrl
|
||||
if (operation.active() && art.isReady && oldUrl) {
|
||||
const restart = () => {
|
||||
if (operation.active())
|
||||
art.emit('restart', newUrl)
|
||||
}
|
||||
if (ready)
|
||||
restart()
|
||||
else if (!failed)
|
||||
listenSource(operation.scope, art, 'video:canplay', restart)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
failSource(operation, error)
|
||||
}
|
||||
finally {
|
||||
finishAssignment(operation)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { SourceEvent, SourceEvents, SourceListener } from './types'
|
||||
|
||||
export function listenSource(scope: ResourceScope, art: SourceEvents, name: SourceEvent, callback: SourceListener, once = true): SourceListener {
|
||||
let fired = false
|
||||
let release: () => void
|
||||
const guarded: SourceListener = (event) => {
|
||||
if (scope.closed || fired)
|
||||
return
|
||||
if (once) {
|
||||
fired = true
|
||||
release()
|
||||
}
|
||||
callback(event)
|
||||
}
|
||||
art[once ? 'once' : 'on'](name, guarded)
|
||||
release = scope.add(() => {
|
||||
art.off(name, guarded)
|
||||
})
|
||||
return guarded
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { UrlTarget } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
|
||||
export interface SourceOperation {
|
||||
scope: ResourceScope
|
||||
assigned: boolean
|
||||
acceptingEvents: boolean
|
||||
onAssigned?: () => void
|
||||
onError?: (error: unknown) => void
|
||||
active: () => boolean
|
||||
}
|
||||
|
||||
const current = new WeakMap<object, SourceOperation>()
|
||||
const assignments = new WeakMap<object, SourceOperation>()
|
||||
|
||||
export function captureSource(owner: object): () => boolean {
|
||||
const operation = current.get(owner)
|
||||
return () => current.get(owner) === operation && (!operation || operation.active())
|
||||
}
|
||||
|
||||
export function beginSource(owner: object): SourceOperation {
|
||||
const previous = current.get(owner)
|
||||
const scope = getScope(owner).child()
|
||||
const operation: SourceOperation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
acceptingEvents: false,
|
||||
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation,
|
||||
}
|
||||
current.set(owner, operation)
|
||||
scope.add(() => {
|
||||
if (current.get(owner) === operation)
|
||||
current.delete(owner)
|
||||
operation.onAssigned = undefined
|
||||
operation.onError = undefined
|
||||
})
|
||||
previous?.scope.dispose()
|
||||
return operation
|
||||
}
|
||||
|
||||
export function takeAssignment(owner: object): SourceOperation {
|
||||
const operation = assignments.get(owner)
|
||||
assignments.delete(owner)
|
||||
return operation || beginSource(owner)
|
||||
}
|
||||
|
||||
export function finishAssignment(operation: SourceOperation): void {
|
||||
operation.assigned = true
|
||||
const callback = operation.onAssigned
|
||||
operation.onAssigned = undefined
|
||||
if (operation.active())
|
||||
callback?.()
|
||||
}
|
||||
|
||||
export function assignUrl(owner: UrlTarget, operation: SourceOperation, url: string): void {
|
||||
assignments.set(owner, operation)
|
||||
operation.acceptingEvents = true
|
||||
try {
|
||||
owner.url = url
|
||||
}
|
||||
finally {
|
||||
// Also support a host-provided URL setter without a core URL mixin.
|
||||
if (assignments.get(owner) === operation) {
|
||||
assignments.delete(owner)
|
||||
finishAssignment(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function failSource(operation: SourceOperation, error: unknown): void {
|
||||
if (!operation.active())
|
||||
return
|
||||
if (operation.onError)
|
||||
operation.onError(error)
|
||||
else
|
||||
console.warn('Failed to initialize ArtPlayer source:', error)
|
||||
operation.scope.dispose()
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { SourceEvent, SourceListener, SwitchHost } from './types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { listenSource } from './listen'
|
||||
import { assignUrl, beginSource } from './operation'
|
||||
|
||||
export function switchSource(art: SwitchHost, url: string, currentTime: number): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (isClosing(art) || url === art.url) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const operation = beginSource(art)
|
||||
const scope = operation.scope.child()
|
||||
let settled = false
|
||||
const settle = (failed = false, error?: unknown) => {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
operation.onError = undefined
|
||||
operation.onAssigned = undefined
|
||||
scope.dispose()
|
||||
if (failed)
|
||||
reject(error)
|
||||
else
|
||||
resolve()
|
||||
if (failed)
|
||||
operation.scope.dispose()
|
||||
}
|
||||
scope.add(() => {
|
||||
settle()
|
||||
})
|
||||
const active = () => !settled && operation.active()
|
||||
const fail = (error: unknown) => settle(true, error)
|
||||
operation.onError = fail
|
||||
try {
|
||||
const { playing, aspectRatio, playbackRate } = art
|
||||
art.pause()
|
||||
if (!active())
|
||||
return
|
||||
|
||||
const readiness = scope.child()
|
||||
const handlers = {
|
||||
'video:error': fail,
|
||||
'video:loadedmetadata': () => {
|
||||
if (!active())
|
||||
return
|
||||
try {
|
||||
art.currentTime = currentTime
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
},
|
||||
'video:canplay': () => {
|
||||
if (!active())
|
||||
return
|
||||
// Detach readiness handlers before awaiting a possibly pending play.
|
||||
readiness.dispose()
|
||||
const resume = async () => {
|
||||
art.playbackRate = playbackRate
|
||||
if (!active())
|
||||
return
|
||||
art.aspectRatio = aspectRatio
|
||||
if (!active())
|
||||
return
|
||||
if (playing) {
|
||||
// The public play promise still rejects; this internal resume is best effort.
|
||||
try {
|
||||
await art.play()
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
if (!active())
|
||||
return
|
||||
art.notice.show = ''
|
||||
settle()
|
||||
}
|
||||
void resume().catch(fail)
|
||||
},
|
||||
}
|
||||
const capture = scope.child()
|
||||
const queued: [SourceEvent, unknown][] = []
|
||||
const names = Object.keys(handlers) as SourceEvent[]
|
||||
for (const name of names) {
|
||||
listenSource(capture, art, name, (event) => {
|
||||
if (operation.acceptingEvents)
|
||||
queued.push([name, event])
|
||||
}, false)
|
||||
}
|
||||
assignUrl(art, operation, url)
|
||||
if (!active())
|
||||
return
|
||||
art.notice.show = ''
|
||||
if (!active())
|
||||
return
|
||||
const activate = () => {
|
||||
capture.dispose()
|
||||
if (!url) {
|
||||
settle()
|
||||
return
|
||||
}
|
||||
const dispatch = {} as Record<SourceEvent, SourceListener>
|
||||
for (const name of names)
|
||||
dispatch[name] = listenSource(readiness, art, name, handlers[name])
|
||||
for (const [name, event] of queued) {
|
||||
if (!active() || readiness.closed)
|
||||
break
|
||||
dispatch[name](event)
|
||||
}
|
||||
queued.length = 0
|
||||
}
|
||||
if (operation.assigned)
|
||||
activate()
|
||||
else
|
||||
operation.onAssigned = activate
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type SourceEvent = 'video:error' | 'video:loadedmetadata' | 'video:canplay'
|
||||
export type SourceListener = (event?: unknown) => void
|
||||
|
||||
export interface SourceEvents {
|
||||
on: (name: SourceEvent, callback: SourceListener) => unknown
|
||||
once: (name: SourceEvent, callback: SourceListener) => unknown
|
||||
off: (name: SourceEvent, callback: SourceListener) => unknown
|
||||
}
|
||||
|
||||
export interface SourceMedia {
|
||||
src: string | null
|
||||
}
|
||||
|
||||
export interface UrlTarget {
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export interface UrlHost<Media extends SourceMedia, Host> extends UrlTarget, SourceEvents {
|
||||
template: { $video: Media }
|
||||
option: {
|
||||
url: string
|
||||
type: string
|
||||
customType: Record<string, ((this: Host, video: Media, url: string, art: Host) => unknown) | undefined>
|
||||
}
|
||||
loading: { set show(value: boolean) }
|
||||
isReady: boolean
|
||||
emit: (name: 'restart', url: string) => unknown
|
||||
}
|
||||
|
||||
export interface SwitchHost extends UrlTarget, SourceEvents {
|
||||
currentTime: number
|
||||
readonly playing: boolean
|
||||
aspectRatio: string
|
||||
playbackRate: number
|
||||
pause: () => unknown
|
||||
play: () => unknown
|
||||
notice: { set show(value: string) }
|
||||
}
|
||||
|
||||
export interface SwitchMethods {
|
||||
switchUrl: (url: string) => Promise<void>
|
||||
switchQuality: (url: string) => Promise<void>
|
||||
set switch(url: string)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,49 @@
|
||||
# CORE-09 URL 与切源操作
|
||||
|
||||
状态:完成;源码、生成产物、安装消费与本地三浏览器检查通过。全项目发布验收仍待后续任务。
|
||||
|
||||
## 行为决策
|
||||
|
||||
涉及 API-02/03/04/05/07/12。旧 switchUrl/switchQuality 返回 Promise<void>,无取消结果类型。保持方法名、描述符、绑定行为、switch setter 与 switchUrl 的函数身份;保持严格字符串同 URL 快速返回。
|
||||
|
||||
每次真实 URL 赋值创建内部操作作用域。新的 switch 或直接 art.url 赋值使旧操作失效,销毁清理所有操作。被替代/销毁的 Promise 以 undefined 正常兑现,不新增 AbortError 拒绝;兑现仅表示该调用已结束,不能证明资源曾成功激活。需要确认当前源的应用仍通过 art.url、媒体事件及当前状态判断。实际媒体错误与 customType 抛出/拒绝保留原错误值;内部恢复播放失败仍正常结束切换,公开 play() 的拒绝不改变。
|
||||
|
||||
连续切源时只允许最新操作恢复位置、速率、比例、清理通知和发出 restart。普通原生事件注册顺序保持;源 setter 或 customType 同步发出媒体事件时,暂存到赋值完成后处理,防止先前遗漏。仅清除本操作监听器,不清空用户 Emitter 订阅。空 URL 保持旧 loading 行为且不替换媒体源;对应 switch 调用结束,不无限等待一个没有启动的加载。
|
||||
|
||||
## 模块
|
||||
|
||||
- player/urlMix.ts:公开 URL 描述符、customType 调用及 restart。
|
||||
- player/switchMix.ts:公开切源方法门面。
|
||||
- source/types.ts:最小媒体、宿主、事件和方法类型。
|
||||
- source/operation.ts:当前操作、URL 赋值令牌、失效与失败。
|
||||
- source/listen.ts:操作拥有的事件订阅与单次调用。
|
||||
- source/switch.ts:切源、事件暂存、状态恢复和 Promise 结算。
|
||||
|
||||
## 兼容边界与责任
|
||||
|
||||
customType 保留真实 video、art、this 和三个参数,不用代理对象替换。其内部已启动的 SDK、请求或用户 Promise 可以继续访问真实对象;核心只能取消尚未调用的回调、忽略过期返回/拒绝并清理自身订阅,不能声称取消任意外部代码。直接 URL 赋值没有可消费的 Promise,当前回调错误通过带原错误值的 console.warn 报告;switch 调用通过自身 Promise 拒绝报告。
|
||||
|
||||
媒体事件本身没有操作 ID;customType/proxy 仍负责让自己的事件与当前资源一致。核心的操作身份检查不能鉴别第三方继续向真实 video 或 art 发送的伪装成当前事件的过期回调。
|
||||
|
||||
补测真实 playMix 曾复现旧恢复把新通知覆盖成 Play;因此 playMix 也捕获当前源身份,晚到结果仍按原值兑现或拒绝,但不再写通知、发 play 或触发互斥暂停。同步正常路径及公开返回值保持。测试既使用受控覆盖函数,也使用实际 playMix,不能用前者代替后者。
|
||||
|
||||
旧 URL 的 revokeObjectURL 行为暂保留;对象 URL 的完整归属审计归 CORE-19。播放模块其余迁移归 CORE-10;过期重连归 CORE-11。不要把本任务的操作监听隔离当作这些模块也已全部处理。
|
||||
|
||||
## 验证与交付
|
||||
|
||||
证据见 [source-operations-validation.json](../baselines/source-operations-validation.json),记录完整源码/测试/包文件 SHA、安装检查、浏览器版本与逐项结果。
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| yarn ci:check | 137 项:108 单元、4 工程、25 基线;严格编译 38 个生产 TS 文件 |
|
||||
| 切源专项 Node | 新增 17 项,覆盖过期、销毁、同 URL、重入、同步事件、错误身份、真实 playMix 和回调身份 |
|
||||
| 安装包类型 | TS 5.9.3 四组消费模式、TS 4.3.5 一组,均零诊断 |
|
||||
| 安装包运行时 | 实际 tarball 仓库外安装,27 项通过;范围为核心及 chapter |
|
||||
| 安装 UMD | Chromium/Firefox/WebKit 共 183 项,通过,无跳过/重试 |
|
||||
| 安装 legacy | 同一套三浏览器共 183 项,通过,无跳过/重试 |
|
||||
|
||||
测试入口:test/source.test.js、test/playback.test.js、test/types/source.ts、test/browser/source.spec.js。BASE-LIFE-01/02/06 已关闭,旧发布基线原样保留。原生 play 的受控拒绝/延迟用于验证 Promise 处理,不作为真实自动播放授权或 codec 支持证明。playMix 补充身份检查前的 180 项 UMD 通过只是阶段证据;最终以包含该修复的 183 项报告及匹配包指纹为准。
|
||||
|
||||
未执行远端 GitHub Actions、实体设备或全部生态包发布验收;这些不计为本任务已完成。
|
||||
|
||||
不升级版本、不改变依赖和锁文件。本任务完成后单独本地提交。回退按该任务完整提交回退源码、生成产物和文档,不能只恢复 JS 文件而保留调用方的 TS 逻辑。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 170 / doing 0 / blocked 0 / done 44 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 169 / doing 0 / blocked 0 / done 45 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
| CORE-06 | artplayer<br>建立内部媒体与宿主类型 | CORE-05 | 原生 video、canvas shim、UI host 类型 | 不更改公开 art.video 用法,不以 HTMLVideoElement 断言掩盖 shim | M | done |
|
||||
| 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 | done |
|
||||
| CORE-08 | artplayer<br>整理插件管理器与扩展类型 | CORE-03, CORE-07 | 注册流程、同步/异步结果、插件/事件扩展接口 | 旧插件同步可见性、重名与命名、销毁中异步插件保持明确语义;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-03 并增加候选正向回归;接续 CORE-07 / BASE-TYPE-04 的同步/异步注册返回声明冲突,内部返回类型精确,保留历史消费者 | H | done |
|
||||
| CORE-09 | artplayer<br>整理 URL 与切源操作 | CORE-04, CORE-06, CORE-08 | url/switch/customType 的操作身份及取消方案 | 并发切源、错误、同 URL、销毁和 Promise 结算有契约;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-01/02/06 并增加候选正向回归 | H | todo |
|
||||
| CORE-09 | artplayer<br>整理 URL 与切源操作 | CORE-04, CORE-06, CORE-08 | url/switch/customType 的操作身份及取消方案 | 并发切源、错误、同 URL、销毁和 Promise 结算有契约;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-01/02/06 并增加候选正向回归 | H | done |
|
||||
| CORE-10 | artplayer<br>迁移播放与时间/状态属性 | CORE-09 | play/pause/toggle/seek/volume/rate/duration 等模块 | 旧返回值、拒绝、mutex、位置恢复、属性描述符与事件通过;接续 CORE-07 / BASE-TYPE-04 的 toggle void 与实际 Promise 分支冲突,不吞拒绝 | H | todo |
|
||||
| CORE-11 | artplayer<br>整理媒体事件、ready 与重连 | CORE-10 | 媒体转发、重连、UI 响应职责分离 | readiness 次数/顺序、过期重连、原错误参数有回归 | H | todo |
|
||||
| CORE-12 | artplayer<br>迁移模板及公开资源入口 | CORE-06, CORE-04, BASE-04 | template/icons/i18n/style、SSR 与 proxy 挂载 | DOM/CSS/global/样式注入、i18n 子路径及 SSR import 保持;不把 SSR import/useSSR 保持扩大为服务端构造播放器;复跑 BASE-05 的 SSR import/模板、浏览器 useSSR 节点复用和非浏览器构造错误;接续 VENDOR-01/02,核对 screenfull/hint 复制版本、修改与分发通知,保留 DOM/CSS/fullscreen 行为 | H | todo |
|
||||
@@ -443,6 +443,7 @@
|
||||
- CORE-06: [记录](changes/2026-09-11-CORE-06-media-hosts.md) [记录](baselines/media-hosts-validation.json)
|
||||
- CORE-07: [记录](changes/2026-09-11-CORE-07-declarations.md) [记录](baselines/declarations-validation.json)
|
||||
- CORE-08: [记录](changes/2026-09-11-CORE-08-plugin-manager.md) [记录](baselines/plugins-validation.json)
|
||||
- CORE-09: [记录](changes/2026-09-11-CORE-09-source-operations.md) [记录](baselines/source-operations-validation.json)
|
||||
- PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md)
|
||||
- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json)
|
||||
- PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 进度与证据
|
||||
|
||||
## 当前实施:CORE-09 URL 与切源操作完成
|
||||
|
||||
URL/switch 门面迁移 TS,新增四个 source 模块,统一切源取消、事件清理、同步代理事件和 Promise 结算;真实 playMix 同步接入源身份,避免晚到播放覆盖新状态。137 项 Node/基线、安装 UMD 与 legacy 各 183 项三浏览器检查、打包 27 项运行时和五组类型检查通过。见 [交付](changes/2026-09-11-CORE-09-source-operations.md)。关闭 BASE-LIFE-01/02/06;customType 外部 SDK 清理、对象 URL 归属和过期重连仍由对应后续任务处理。当前 214 项,45 完成、169 待办;核心 33 个 TS 文件、chapter 5 个。下一项 CORE-10 播放与时间/状态属性。
|
||||
|
||||
## 当前实施:CORE-08 插件管理器 TS 完成
|
||||
|
||||
插件注册、内置装配、命名与内部类型拆成四个 TS 模块;修复销毁后晚到注册,保持 Promise 结算与同步可见性,新增共享类型扩展入口。120 项 Node/基线、安装 UMD 三浏览器 162 项及 legacy 插件/章节/生命周期 90 项通过;严格打包 27 项运行时、五组类型零诊断。见 [交付](changes/2026-09-11-CORE-08-plugin-manager.md)。关闭 BASE-LIFE-03;公开返回声明和各插件自有资源清理仍有后续责任。当前 214 项,44 完成、170 待办;核心 27 个 TS 文件、chapter 5 个。下一项 CORE-09 URL/切源操作。
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
| ID | 状态 / 证据等级 | 条目 | 后续任务 |
|
||||
| --- | --- | --- | --- |
|
||||
| BASE-LIFE-06 | open / 已复现 | Published switch leaves an unhandled rejection and an unsettled promise when resume fails | CORE-09 |
|
||||
| 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-06 | resolved / 已复现 | Published switch leaves an unhandled rejection and an unsettled promise when resume fails | CORE-09 |
|
||||
| BASE-LIFE-01 | resolved / 已复现 | Superseded switch resolves along with the latest source | CORE-09 |
|
||||
| BASE-LIFE-02 | resolved / 已复现 | Switch remains unsettled during the bounded observation after destroy | CORE-09 |
|
||||
| BASE-LIFE-03 | resolved / 已复现 | Async plugin registers its result after destroy | CORE-08 |
|
||||
| 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 |
|
||||
|
||||
+30
-6
@@ -9,7 +9,7 @@
|
||||
"id": "BASE-LIFE-06",
|
||||
"title": "Published switch leaves an unhandled rejection and an unsettled promise when resume fails",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-09"
|
||||
],
|
||||
@@ -20,13 +20,21 @@
|
||||
],
|
||||
"compatibleResolution": "按对应 CORE 任务修复,保持合法旧调用;异步取消需明确结算策略,不把 pending 或重复销毁当正常兼容要求。",
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。",
|
||||
"workspaceState": "Current switchMix uses silencePromise and test/playback.test.js covers resume rejection. Published baseline still reproduces the old failure; preserve the existing workspace mitigation during migration."
|
||||
"workspaceState": "Current switchMix uses silencePromise and test/playback.test.js covers resume rejection. Published baseline still reproduces the old failure; preserve the existing workspace mitigation during migration.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-09-source-operations.md",
|
||||
"refactor/baselines/source-operations-validation.json",
|
||||
"test/source.test.js",
|
||||
"test/playback.test.js",
|
||||
"test/browser/source.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Owned source generations settle superseded/destroyed calls with undefined, prevent stale restart/restoration and actual playMix effects, retain original load failures and best-effort resume settlement. Published baselines remain unchanged; real-media comparisons and controlled native-play tests cover the candidate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-01",
|
||||
"title": "Superseded switch resolves along with the latest source",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-09"
|
||||
],
|
||||
@@ -35,13 +43,21 @@
|
||||
"refactor/baselines/lifecycle-coverage.md"
|
||||
],
|
||||
"compatibleResolution": "按对应 CORE 任务修复,保持合法旧调用;异步取消需明确结算策略,不把 pending 或重复销毁当正常兼容要求。",
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。"
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-09-source-operations.md",
|
||||
"refactor/baselines/source-operations-validation.json",
|
||||
"test/source.test.js",
|
||||
"test/playback.test.js",
|
||||
"test/browser/source.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Owned source generations settle superseded/destroyed calls with undefined, prevent stale restart/restoration and actual playMix effects, retain original load failures and best-effort resume settlement. Published baselines remain unchanged; real-media comparisons and controlled native-play tests cover the candidate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-02",
|
||||
"title": "Switch remains unsettled during the bounded observation after destroy",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-09"
|
||||
],
|
||||
@@ -50,7 +66,15 @@
|
||||
"refactor/baselines/lifecycle-coverage.md"
|
||||
],
|
||||
"compatibleResolution": "按对应 CORE 任务修复,保持合法旧调用;异步取消需明确结算策略,不把 pending 或重复销毁当正常兼容要求。",
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。"
|
||||
"closureCriteria": "候选正向回归和旧新差异记录通过;有界观察问题不得仅靠延长等待隐藏。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-09-source-operations.md",
|
||||
"refactor/baselines/source-operations-validation.json",
|
||||
"test/source.test.js",
|
||||
"test/playback.test.js",
|
||||
"test/browser/source.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Owned source generations settle superseded/destroyed calls with undefined, prevent stale restart/restoration and actual playMix effects, retain original load failures and best-effort resume settlement. Published baselines remain unchanged; real-media comparisons and controlled native-play tests cover the candidate."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-03",
|
||||
|
||||
+5
-2
@@ -911,11 +911,14 @@
|
||||
"CORE-06",
|
||||
"CORE-08"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "url/switch/customType 的操作身份及取消方案",
|
||||
"acceptance": "并发切源、错误、同 URL、销毁和 Promise 结算有契约;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-01/02/06 并增加候选正向回归",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-11-CORE-09-source-operations.md",
|
||||
"baselines/source-operations-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CORE-10",
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: rapid real-media switches identify the last restart and settle superseded work`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?source=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const restarts = []
|
||||
const order = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
art.on('video:canplay', () => order.push('canplay'))
|
||||
art.on('restart', () => order.push('restart'))
|
||||
let firstSettled = false
|
||||
const first = art.switchUrl('/test/pattern.mp4?source=first').then(() => {
|
||||
firstSettled = true
|
||||
})
|
||||
const second = art.switchUrl('/test/pattern.mp4?source=second')
|
||||
await Promise.resolve()
|
||||
const early = firstSettled
|
||||
await Promise.all([first, second])
|
||||
const result = { early, restarts, order, current: new URL(art.video.currentSrc).search, error: art.video.error?.code || 0, time: art.currentTime }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result.early).toBe(core === 'candidate')
|
||||
expect(result.restarts).toEqual(core === 'candidate'
|
||||
? ['/test/pattern.mp4?source=second']
|
||||
: ['/test/pattern.mp4?source=first', '/test/pattern.mp4?source=second'])
|
||||
expect(result.order[0]).toBe('canplay')
|
||||
expect(result.current).toBe('?source=second')
|
||||
expect(result.error).toBe(0)
|
||||
expect(result.time).toBeCloseTo(0, 1)
|
||||
})
|
||||
|
||||
test(`${core}: destroy settles the candidate switch without waiting for detached media`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?destroy=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
let settled = false
|
||||
let rejected = false
|
||||
art.switchUrl('/test/pattern.mp4?destroy=pending').then(() => {
|
||||
settled = true
|
||||
}, () => {
|
||||
rejected = true
|
||||
})
|
||||
art.destroy()
|
||||
// Retain the published baseline's bounded pending observation, not an infinite-pending claim.
|
||||
await new Promise(resolve => setTimeout(resolve, 250))
|
||||
return { settled, rejected, destroyed: art.isDestroy, instances: window.Artplayer.instances.length }
|
||||
})
|
||||
expect(result).toEqual({ settled: core === 'candidate', rejected: false, destroyed: true, instances: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: quality switch restores position/rate and preserves real customType identities', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?custom=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.currentTime = 3
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.seeking)).toBe(false)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const calls = []
|
||||
art.option.type = 'controlled'
|
||||
art.option.customType.controlled = function (video, url, owner) {
|
||||
calls.push({ thisIdentity: this === art, ownerIdentity: owner === art, videoIdentity: video === art.video, count: arguments.length, url })
|
||||
video.src = url
|
||||
}
|
||||
art.playbackRate = 1.5
|
||||
art.aspectRatio = '16:9'
|
||||
const restart = []
|
||||
art.on('restart', url => restart.push(url))
|
||||
await art.switchQuality('/test/pattern.mp4?custom=next')
|
||||
const result = { calls, restart, time: art.currentTime, rate: art.playbackRate, ratio: art.aspectRatio, paused: art.video.paused, error: art.video.error?.code || 0 }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result.calls).toEqual([{ thisIdentity: true, ownerIdentity: true, videoIdentity: true, count: 3, url: '/test/pattern.mp4?custom=next' }])
|
||||
expect(result.restart).toEqual(['/test/pattern.mp4?custom=next'])
|
||||
expect(result.time).toBeCloseTo(3, 1)
|
||||
expect(result.rate).toBe(1.5)
|
||||
expect(result.ratio).toBe('16:9')
|
||||
expect(result.paused).toBe(true)
|
||||
expect(result.error).toBe(0)
|
||||
})
|
||||
|
||||
test('candidate: real switch settles when the controlled native resume rejects', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?resume=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const error = new DOMException('controlled resume rejection', 'NotAllowedError')
|
||||
let calls = 0
|
||||
Object.defineProperty(art.video, 'play', { configurable: true, value() {
|
||||
calls++
|
||||
return Promise.reject(error)
|
||||
} })
|
||||
const value = await art.switchUrl('/test/pattern.mp4?resume=next')
|
||||
const result = { settledUndefined: value === undefined, calls, source: new URL(art.video.currentSrc).search, paused: art.video.paused }
|
||||
delete art.video.play
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ settledUndefined: true, calls: 1, source: '?resume=next', paused: true })
|
||||
})
|
||||
|
||||
test('candidate: late native play completion cannot publish stale player effects', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?late=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const plays = []
|
||||
art.on('play', () => plays.push('play'))
|
||||
let resolveNative
|
||||
let started
|
||||
const invoked = new Promise(resolve => started = resolve)
|
||||
Object.defineProperty(art.video, 'play', { configurable: true, value() {
|
||||
started()
|
||||
return new Promise(resolve => resolveNative = resolve)
|
||||
} })
|
||||
const switching = art.switchUrl('/test/pattern.mp4?late=resume')
|
||||
await invoked
|
||||
art.url = '/test/pattern.mp4?late=latest'
|
||||
await switching
|
||||
art.notice.show = 'Latest source notice'
|
||||
resolveNative(42)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { plays, notice: art.template.$noticeInner.textContent }
|
||||
delete art.video.play
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ plays: [], notice: 'Latest source notice' })
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mock } from 'node:test'
|
||||
|
||||
export function playbackFactory({ Emitter, playMix, pauseMix, toggleMix, switchMix }) {
|
||||
export function playbackFactory({ Emitter, playMix, pauseMix, toggleMix, switchMix, beginLifecycle }) {
|
||||
return function createArt(play = () => Promise.resolve(), playing = false) {
|
||||
const art = Object.assign(new Emitter(), {
|
||||
url: 'old.mp4',
|
||||
@@ -19,6 +19,7 @@ export function playbackFactory({ Emitter, playMix, pauseMix, toggleMix, switchM
|
||||
},
|
||||
},
|
||||
})
|
||||
beginLifecycle(art)
|
||||
Object.defineProperty(art, 'playing', { get: () => !art.template.$video.paused })
|
||||
for (const mix of [playMix, pauseMix, toggleMix, switchMix])
|
||||
mix(art)
|
||||
|
||||
@@ -13,6 +13,8 @@ const modules = await loadModules({
|
||||
pauseMix: 'packages/artplayer/src/player/pauseMix',
|
||||
toggleMix: 'packages/artplayer/src/player/toggleMix',
|
||||
switchMix: 'packages/artplayer/src/player/switchMix',
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
const { silencePromise, Hotkey } = modules
|
||||
const createArt = playbackFactory(modules)
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { mock, test } from 'node:test'
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { Emitter, urlMix, switchMix, playMix, beginLifecycle, getScope } = await loadModules({
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
urlMix: 'packages/artplayer/src/player/urlMix',
|
||||
switchMix: 'packages/artplayer/src/player/switchMix',
|
||||
playMix: 'packages/artplayer/src/player/playMix',
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
|
||||
function createArt(customType) {
|
||||
const art = Object.assign(new Emitter(), {
|
||||
template: { $video: { src: 'old.mp4' } },
|
||||
option: { url: 'old.mp4', type: customType ? 'custom' : '', customType: { custom: customType } },
|
||||
isReady: true,
|
||||
playing: false,
|
||||
currentTime: 37,
|
||||
playbackRate: 1.5,
|
||||
aspectRatio: '16:9',
|
||||
loading: { show: false },
|
||||
notice: { show: '' },
|
||||
pause: mock.fn(),
|
||||
play: mock.fn(() => Promise.resolve()),
|
||||
})
|
||||
beginLifecycle(art)
|
||||
urlMix(art)
|
||||
switchMix(art)
|
||||
return art
|
||||
}
|
||||
|
||||
test('superseded switches settle before readiness and only the latest restores state/restarts', async () => {
|
||||
const art = createArt()
|
||||
const restarts = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
const first = art.switchQuality('first.mp4')
|
||||
art.currentTime = 9
|
||||
const second = art.switchQuality('second.mp4')
|
||||
assert.equal(await first, undefined)
|
||||
art.currentTime = 0
|
||||
art.emit('video:loadedmetadata')
|
||||
assert.equal(art.currentTime, 9)
|
||||
art.emit('video:canplay')
|
||||
assert.equal(await second, undefined)
|
||||
assert.deepEqual(restarts, ['second.mp4'])
|
||||
assert.deepEqual(Object.keys(art.e), ['restart'])
|
||||
})
|
||||
|
||||
test('direct URL assignment cancels an outstanding switch and stale restart listeners', async () => {
|
||||
const art = createArt()
|
||||
const restarts = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
const switching = art.switchQuality('switch.mp4')
|
||||
art.url = 'direct.mp4'
|
||||
await switching
|
||||
art.currentTime = 4
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:canplay')
|
||||
assert.equal(art.currentTime, 4)
|
||||
assert.deepEqual(restarts, ['direct.mp4'])
|
||||
})
|
||||
|
||||
test('scope disposal settles a pending switch and prevents post-destroy work', async () => {
|
||||
const art = createArt()
|
||||
const switching = art.switchUrl('pending.mp4')
|
||||
getScope(art).dispose()
|
||||
assert.equal(await switching, undefined)
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
art.url = 'closed.mp4'
|
||||
await art.switchUrl('also-closed.mp4')
|
||||
assert.equal(art.url, 'pending.mp4')
|
||||
assert.equal(art.pause.mock.callCount(), 1)
|
||||
})
|
||||
|
||||
test('same URL remains a no-op and switch setter retains the public function identity', async () => {
|
||||
const art = createArt()
|
||||
art.notice.show = 'keep'
|
||||
assert.equal(await art.switchUrl('old.mp4'), undefined)
|
||||
assert.equal(art.pause.mock.callCount(), 0)
|
||||
assert.equal(art.notice.show, 'keep')
|
||||
assert.equal(Object.getOwnPropertyDescriptor(art, 'switch').set, art.switchUrl)
|
||||
for (const name of ['url', 'switch', 'switchUrl', 'switchQuality']) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(art, name)
|
||||
assert.equal(descriptor.enumerable, false)
|
||||
assert.equal(descriptor.configurable, false)
|
||||
}
|
||||
})
|
||||
|
||||
test('a reentrant pause cannot make its superseded switch overwrite the latest URL', async () => {
|
||||
const art = createArt()
|
||||
art.pause = () => {
|
||||
art.url = 'reentrant.mp4'
|
||||
}
|
||||
await art.switchUrl('stale.mp4')
|
||||
assert.equal(art.url, 'reentrant.mp4')
|
||||
})
|
||||
|
||||
test('synchronous media setters cannot lose metadata, readiness or original errors', async () => {
|
||||
for (const fails of [false, true]) {
|
||||
const art = createArt()
|
||||
const error = new Error('synchronous source failure')
|
||||
let src = 'old.mp4'
|
||||
Object.defineProperty(art.template.$video, 'src', {
|
||||
get: () => src,
|
||||
set(value) {
|
||||
src = value
|
||||
if (fails) {
|
||||
art.emit('video:error', error)
|
||||
}
|
||||
else {
|
||||
art.currentTime = 0
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:canplay')
|
||||
}
|
||||
},
|
||||
})
|
||||
const switching = art.switchQuality('sync.mp4')
|
||||
if (fails) {
|
||||
await assert.rejects(switching, actual => actual === error)
|
||||
}
|
||||
else {
|
||||
await switching
|
||||
assert.equal(art.currentTime, 37)
|
||||
}
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('late resume fulfillment or rejection cannot clear a newer operation notice', async () => {
|
||||
for (const rejects of [false, true]) {
|
||||
const art = createArt()
|
||||
art.playing = true
|
||||
let finish
|
||||
art.play = () => new Promise((resolve, reject) => {
|
||||
finish = rejects ? reject : resolve
|
||||
})
|
||||
const first = art.switchUrl('first.mp4')
|
||||
art.emit('video:canplay')
|
||||
art.playing = false
|
||||
const second = art.switchUrl('second.mp4')
|
||||
await first
|
||||
art.notice.show = 'new source notice'
|
||||
finish(new Error('late play'))
|
||||
await setImmediate()
|
||||
assert.equal(art.notice.show, 'new source notice')
|
||||
art.emit('video:canplay')
|
||||
await second
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('state restoration errors reject without leaking handlers or unhandled promises', async () => {
|
||||
for (const property of ['currentTime', 'playbackRate', 'aspectRatio']) {
|
||||
const art = createArt()
|
||||
const error = new Error(property)
|
||||
const switching = art.switchQuality('new.mp4')
|
||||
const previous = art[property]
|
||||
Object.defineProperty(art, property, {
|
||||
get: () => previous,
|
||||
set() {
|
||||
throw error
|
||||
},
|
||||
})
|
||||
const rejected = assert.rejects(switching, actual => actual === error)
|
||||
art.emit(property === 'currentTime' ? 'video:loadedmetadata' : 'video:canplay')
|
||||
await rejected
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('customType deferral ignores the old resource and preserves callback identities', async () => {
|
||||
let called
|
||||
const art = createArt(function (video, url, owner) {
|
||||
called = [this, video, url, owner, arguments.length]
|
||||
video.src = url
|
||||
owner.emit('video:loadedmetadata')
|
||||
owner.emit('video:canplay')
|
||||
})
|
||||
const switching = art.switchQuality('custom.mp4')
|
||||
art.currentTime = 0
|
||||
art.emit('video:canplay')
|
||||
assert.equal(called, undefined)
|
||||
await switching
|
||||
assert.deepEqual(called, [art, art.template.$video, 'custom.mp4', art, 3])
|
||||
assert.equal(art.currentTime, 37)
|
||||
assert.equal(art.option.url, 'custom.mp4')
|
||||
})
|
||||
|
||||
test('customType failures reject switches with the original thrown/rejected value', async () => {
|
||||
for (const asynchronous of [false, true]) {
|
||||
const error = new Error('customType failed')
|
||||
const art = createArt(() => {
|
||||
if (asynchronous)
|
||||
return Promise.reject(error)
|
||||
throw error
|
||||
})
|
||||
await assert.rejects(art.switchUrl('failed.mp4'), actual => actual === error)
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('superseded customType callbacks never start and empty URL switches settle', async () => {
|
||||
const callback = mock.fn((video, url, art) => {
|
||||
video.src = url
|
||||
art.emit('video:canplay')
|
||||
})
|
||||
const art = createArt(callback)
|
||||
const first = art.switchUrl('first.mp4')
|
||||
const second = art.switchUrl('second.mp4')
|
||||
await Promise.all([first, second])
|
||||
assert.equal(callback.mock.callCount(), 1)
|
||||
assert.equal(callback.mock.calls[0].arguments[1], 'second.mp4')
|
||||
await art.switchUrl('')
|
||||
assert.equal(art.loading.show, true)
|
||||
assert.equal(art.url, 'second.mp4')
|
||||
})
|
||||
|
||||
test('reentrant media listeners and property setters cannot restore stale state', async () => {
|
||||
for (const boundary of ['metadata', 'rate']) {
|
||||
const art = createArt()
|
||||
if (boundary === 'metadata') {
|
||||
art.once('video:loadedmetadata', () => {
|
||||
art.url = 'reentrant.mp4'
|
||||
art.currentTime = 6
|
||||
})
|
||||
}
|
||||
const switching = art.switchQuality('stale.mp4')
|
||||
art.currentTime = 0
|
||||
art.aspectRatio = 'default'
|
||||
if (boundary === 'rate') {
|
||||
Object.defineProperty(art, 'playbackRate', {
|
||||
get: () => 1,
|
||||
set() {
|
||||
art.url = 'reentrant.mp4'
|
||||
},
|
||||
})
|
||||
}
|
||||
art.emit(boundary === 'metadata' ? 'video:loadedmetadata' : 'video:canplay')
|
||||
await switching
|
||||
assert.equal(art.url, 'reentrant.mp4')
|
||||
assert.equal(art.aspectRatio, 'default')
|
||||
if (boundary === 'metadata')
|
||||
assert.equal(art.currentTime, 6)
|
||||
getScope(art).dispose()
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('direct customType rejection is reported once; obsolete returned failures are ignored', async (t) => {
|
||||
const warning = t.mock.method(console, 'warn', () => {})
|
||||
const error = new Error('direct custom source')
|
||||
let rejectLate
|
||||
let started
|
||||
const invoked = new Promise(resolve => started = resolve)
|
||||
const art = createArt(() => new Promise((resolve, reject) => {
|
||||
rejectLate = reject
|
||||
started()
|
||||
}))
|
||||
art.url = 'late.mp4'
|
||||
await invoked
|
||||
art.option.type = ''
|
||||
art.url = 'current.mp4'
|
||||
rejectLate(error)
|
||||
await setImmediate()
|
||||
assert.equal(warning.mock.callCount(), 0)
|
||||
art.option.type = 'custom'
|
||||
art.option.customType.custom = () => Promise.reject(error)
|
||||
await Object.getOwnPropertyDescriptor(art, 'url').set.call(art, 'failed.mp4')
|
||||
await setImmediate()
|
||||
assert.equal(warning.mock.callCount(), 1)
|
||||
assert.deepEqual(warning.mock.calls[0].arguments, ['Failed to initialize ArtPlayer source:', error])
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
})
|
||||
|
||||
test('same-URL calls do not cancel the still-loading original operation', async () => {
|
||||
const art = createArt()
|
||||
let firstSettled = false
|
||||
const first = art.switchQuality('pending.mp4').then(() => firstSettled = true)
|
||||
await art.switchUrl('pending.mp4')
|
||||
assert.equal(firstSettled, false)
|
||||
art.currentTime = 0
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:canplay')
|
||||
await first
|
||||
assert.equal(art.currentTime, 37)
|
||||
assert.equal(art.pause.mock.callCount(), 1)
|
||||
})
|
||||
|
||||
test('synchronous readiness replay consumes metadata once and emits restart once', async () => {
|
||||
const art = createArt()
|
||||
const restarts = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
let src = 'old.mp4'
|
||||
Object.defineProperty(art.template.$video, 'src', {
|
||||
get: () => src,
|
||||
set(value) {
|
||||
src = value
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:loadedmetadata')
|
||||
},
|
||||
})
|
||||
const switching = art.switchQuality('sync.mp4')
|
||||
assert.equal(art.currentTime, 37)
|
||||
art.currentTime = 4
|
||||
art.emit('video:loadedmetadata')
|
||||
assert.equal(art.currentTime, 4)
|
||||
art.emit('video:canplay')
|
||||
art.emit('video:canplay')
|
||||
await switching
|
||||
assert.deepEqual(restarts, ['sync.mp4'])
|
||||
assert.deepEqual(Object.keys(art.e), ['restart'])
|
||||
})
|
||||
|
||||
test('the real play mixin cannot publish stale resume effects after source replacement', async () => {
|
||||
const art = createArt()
|
||||
art.i18n = { get: key => key }
|
||||
art.constructor = { instances: [] }
|
||||
let resolveNative
|
||||
art.template.$video.play = () => new Promise(resolve => resolveNative = resolve)
|
||||
playMix(art)
|
||||
art.playing = true
|
||||
const plays = []
|
||||
art.on('play', () => plays.push('play'))
|
||||
const switching = art.switchUrl('pending-resume.mp4')
|
||||
art.emit('video:canplay')
|
||||
art.url = 'latest.mp4'
|
||||
await switching
|
||||
art.notice.show = 'latest notice'
|
||||
resolveNative(42)
|
||||
await setImmediate()
|
||||
assert.equal(art.notice.show, 'latest notice')
|
||||
assert.deepEqual(plays, [])
|
||||
})
|
||||
|
||||
test('public pending play preserves its result while obsolete notification/event/mutex work stops', async () => {
|
||||
for (const destroy of [false, true]) {
|
||||
const art = createArt()
|
||||
art.i18n = { get: key => key }
|
||||
const other = { pause: mock.fn() }
|
||||
art.constructor = { instances: [other] }
|
||||
art.option.mutex = true
|
||||
art.url = 'initial.mp4'
|
||||
let resolveNative
|
||||
art.template.$video.play = () => new Promise(resolve => resolveNative = resolve)
|
||||
playMix(art)
|
||||
const plays = mock.fn()
|
||||
art.on('play', plays)
|
||||
const playing = art.play()
|
||||
if (destroy)
|
||||
getScope(art).dispose()
|
||||
else
|
||||
art.url = 'replacement.mp4'
|
||||
art.notice.show = 'preserve'
|
||||
resolveNative(42)
|
||||
assert.equal(await playing, 42)
|
||||
assert.equal(art.notice.show, 'preserve')
|
||||
assert.equal(plays.mock.callCount(), 0)
|
||||
assert.equal(other.pause.mock.callCount(), 0)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { CanvasMedia, NativeMedia } from '../../packages/artplayer/src/media/types'
|
||||
import type { SourceMedia, SwitchHost, UrlHost } from '../../packages/artplayer/src/source/types'
|
||||
import switchMix from '../../packages/artplayer/src/player/switchMix'
|
||||
import urlMix from '../../packages/artplayer/src/player/urlMix'
|
||||
|
||||
declare const native: NativeMedia
|
||||
declare const canvas: CanvasMedia
|
||||
const sourceMedia: SourceMedia[] = [native, canvas]
|
||||
void sourceMedia
|
||||
|
||||
declare const switching: SwitchHost
|
||||
switchMix(switching)
|
||||
const pending: Promise<void> = switching.switchQuality('movie.mp4')
|
||||
void pending
|
||||
switching.switch = 'movie.mp4'
|
||||
// @ts-expect-error Source URLs remain strings.
|
||||
switching.switchUrl(42)
|
||||
// @ts-expect-error Switching does not resolve with a media element or success boolean.
|
||||
const wrong: Promise<boolean> = switching.switchUrl('movie.mp4')
|
||||
void wrong
|
||||
|
||||
interface CustomHost extends UrlHost<CanvasMedia, CustomHost> {
|
||||
extension: number
|
||||
}
|
||||
declare const custom: CustomHost
|
||||
custom.option.customType.canvas = function (video, url, art) {
|
||||
this satisfies CustomHost
|
||||
const owner: CustomHost = art
|
||||
const surface: HTMLCanvasElement = video
|
||||
const source: string = url
|
||||
void [owner, surface, source]
|
||||
// @ts-expect-error A canvas proxy is not a native video element.
|
||||
const wrongMedia: HTMLVideoElement = video
|
||||
void wrongMedia
|
||||
}
|
||||
urlMix<CanvasMedia, CustomHost>(custom)
|
||||
Reference in new issue
Block a user