mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
refactor(core): CORE-11 split media events and scope reconnect lifetimes
This commit is contained in:
1 parent
5e31274f1d
commit
b8039ad1f1
27 files changed
+4844
-475
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,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
|
||||
!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
|
||||
+224
-146
@@ -2770,105 +2770,241 @@ function durationMix(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function eventInit(art) {
|
||||
const {
|
||||
i18n,
|
||||
notice,
|
||||
option,
|
||||
constructor,
|
||||
proxy,
|
||||
template: { $player, $video, $poster }
|
||||
} = art;
|
||||
let reconnectTime = 0;
|
||||
function forwardMediaEvents(art) {
|
||||
const { proxy, template: { $video } } = art;
|
||||
for (let index = 0; index < config$1.events.length; index++) {
|
||||
proxy($video, config$1.events[index], (event) => {
|
||||
art.emit(`video:${event.type}`, event);
|
||||
if (!isClosing(art))
|
||||
art.emit(`video:${event.type}`, event);
|
||||
});
|
||||
}
|
||||
art.on("video:canplay", () => {
|
||||
reconnectTime = 0;
|
||||
art.loading.show = false;
|
||||
}
|
||||
function listenMedia(art, name, callback, once = false) {
|
||||
const guarded = (event) => {
|
||||
if (!isClosing(art))
|
||||
callback(event);
|
||||
};
|
||||
art[once ? "once" : "on"](name, guarded);
|
||||
getScope(art).add(() => {
|
||||
art.off(name, guarded);
|
||||
});
|
||||
art.once("video:canplay", () => {
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
}
|
||||
function showMediaUI(art, changes, active) {
|
||||
for (const [name, value] of changes) {
|
||||
if (isClosing(art) || active && !active())
|
||||
return;
|
||||
art[name].show = value;
|
||||
}
|
||||
}
|
||||
const current = /* @__PURE__ */ new WeakMap();
|
||||
const assignments = /* @__PURE__ */ new WeakMap();
|
||||
const sources = /* @__PURE__ */ new WeakMap();
|
||||
function getSourceScope(owner) {
|
||||
return sources.get(owner) || getScope(owner);
|
||||
}
|
||||
function captureSource(owner) {
|
||||
const operation = current.get(owner);
|
||||
const source = sources.get(owner);
|
||||
return () => current.get(owner) === operation && (!operation || operation.active()) && sources.get(owner) === source && (!source || !source.closed);
|
||||
}
|
||||
function beginSource(owner) {
|
||||
const previous = sources.get(owner);
|
||||
const source = getScope(owner).child();
|
||||
const scope = source.child();
|
||||
sources.set(owner, source);
|
||||
source.add(() => {
|
||||
if (sources.get(owner) === source)
|
||||
sources.delete(owner);
|
||||
});
|
||||
const operation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
acceptingEvents: false,
|
||||
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation
|
||||
};
|
||||
current.set(owner, operation);
|
||||
scope.add(() => {
|
||||
if (current.get(owner) === operation)
|
||||
current.delete(owner);
|
||||
operation.onAssigned = void 0;
|
||||
operation.onError = void 0;
|
||||
});
|
||||
previous?.dispose();
|
||||
return operation;
|
||||
}
|
||||
function takeAssignment(owner) {
|
||||
const operation = assignments.get(owner);
|
||||
assignments.delete(owner);
|
||||
return operation || beginSource(owner);
|
||||
}
|
||||
function finishAssignment(operation) {
|
||||
operation.assigned = true;
|
||||
const callback = operation.onAssigned;
|
||||
operation.onAssigned = void 0;
|
||||
if (operation.active())
|
||||
callback?.();
|
||||
}
|
||||
function assignUrl(owner, operation, url) {
|
||||
assignments.set(owner, operation);
|
||||
operation.acceptingEvents = true;
|
||||
try {
|
||||
owner.url = url;
|
||||
} finally {
|
||||
if (assignments.get(owner) === operation) {
|
||||
assignments.delete(owner);
|
||||
finishAssignment(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
function failSource(operation, error2) {
|
||||
if (!operation.active())
|
||||
return;
|
||||
if (operation.onError)
|
||||
operation.onError(error2);
|
||||
else
|
||||
console.warn("Failed to initialize ArtPlayer source:", error2);
|
||||
operation.scope.dispose();
|
||||
}
|
||||
function installEnded(art) {
|
||||
const { option } = art;
|
||||
listenMedia(art, "video:ended", () => {
|
||||
if (option.loop) {
|
||||
const active = captureSource(art);
|
||||
art.seek = 0;
|
||||
if (isClosing(art) || !active())
|
||||
return;
|
||||
silencePromise(art.play());
|
||||
if (active())
|
||||
showMediaUI(art, [["controls", false], ["mask", false]], active);
|
||||
} else {
|
||||
showMediaUI(art, [["controls", true], ["mask", true]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
function installPlaybackUI(art, reconnect) {
|
||||
const { template: { $poster } } = art;
|
||||
listenMedia(art, "video:loadedmetadata", () => {
|
||||
art.emit("resize");
|
||||
if (isMobile)
|
||||
showMediaUI(art, [["loading", false], ["controls", true], ["mask", true]]);
|
||||
});
|
||||
listenMedia(art, "video:loadstart", () => {
|
||||
reconnect.loadStart();
|
||||
showMediaUI(art, [["loading", true], ["mask", false], ["controls", true]]);
|
||||
});
|
||||
listenMedia(art, "video:pause", () => {
|
||||
showMediaUI(art, [["controls", true], ["mask", true]]);
|
||||
});
|
||||
listenMedia(art, "video:play", () => {
|
||||
showMediaUI(art, [["mask", false]]);
|
||||
if (!isClosing(art))
|
||||
setStyle($poster, "display", "none");
|
||||
});
|
||||
listenMedia(art, "video:playing", () => showMediaUI(art, [["mask", false]]));
|
||||
listenMedia(art, "video:progress", () => {
|
||||
if (art.playing)
|
||||
showMediaUI(art, [["loading", false]]);
|
||||
});
|
||||
listenMedia(art, "video:seeked", () => showMediaUI(art, [["loading", false], ["mask", true]]));
|
||||
listenMedia(art, "video:seeking", () => showMediaUI(art, [["loading", true], ["mask", false]]));
|
||||
listenMedia(art, "video:timeupdate", () => showMediaUI(art, [["mask", false]]));
|
||||
listenMedia(art, "video:waiting", () => showMediaUI(art, [["loading", true], ["mask", false]]));
|
||||
}
|
||||
function installReadiness(art, reconnect) {
|
||||
listenMedia(art, "video:canplay", () => {
|
||||
reconnect.reset();
|
||||
showMediaUI(art, [["loading", false]]);
|
||||
});
|
||||
listenMedia(art, "video:canplay", () => {
|
||||
showMediaUI(art, [["loading", false], ["controls", true], ["mask", true]]);
|
||||
if (isClosing(art))
|
||||
return;
|
||||
art.isReady = true;
|
||||
art.emit("ready");
|
||||
});
|
||||
art.on("video:ended", () => {
|
||||
if (option.loop) {
|
||||
art.seek = 0;
|
||||
silencePromise(art.play());
|
||||
art.controls.show = false;
|
||||
art.mask.show = false;
|
||||
} else {
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
}, true);
|
||||
}
|
||||
function createReconnect(art) {
|
||||
const { option, constructor, i18n, notice, template: { $player } } = art;
|
||||
let source = getSourceScope(art);
|
||||
let attempts = 0;
|
||||
let pending;
|
||||
const run = async (error2) => {
|
||||
if (isClosing(art))
|
||||
return;
|
||||
const current2 = getSourceScope(art);
|
||||
if (current2 !== source) {
|
||||
source = current2;
|
||||
attempts = 0;
|
||||
}
|
||||
});
|
||||
art.on("video:error", async (error2) => {
|
||||
if (reconnectTime < constructor.RECONNECT_TIME_MAX) {
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
if (pending && !pending.closed)
|
||||
return;
|
||||
const attempt = current2.child();
|
||||
pending = attempt;
|
||||
attempt.add(() => {
|
||||
if (pending === attempt)
|
||||
pending = void 0;
|
||||
});
|
||||
const active = () => !isClosing(art) && !attempt.closed && current2 === getSourceScope(art);
|
||||
const retry = attempts < constructor.RECONNECT_TIME_MAX;
|
||||
try {
|
||||
if (!retry) {
|
||||
showMediaUI(art, [["mask", true], ["loading", false], ["controls", true]], active);
|
||||
if (!active())
|
||||
return;
|
||||
addClass($player, "art-error");
|
||||
}
|
||||
if (!await wait(attempt, constructor.RECONNECT_SLEEP_TIME) || !active())
|
||||
return;
|
||||
reconnectTime += 1;
|
||||
art.url = option.url;
|
||||
notice.show = `${i18n.get("Reconnect")}: ${reconnectTime}`;
|
||||
art.emit("error", error2, reconnectTime);
|
||||
} else {
|
||||
art.mask.show = true;
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
addClass($player, "art-error");
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return;
|
||||
notice.show = i18n.get("Video Load Failed");
|
||||
if (retry) {
|
||||
const url = option.url;
|
||||
if (!active())
|
||||
return;
|
||||
attempts += 1;
|
||||
const attemptNumber = attempts;
|
||||
attempt.dispose();
|
||||
const operation = beginSource(art);
|
||||
source = getSourceScope(art);
|
||||
assignUrl(art, operation, url);
|
||||
if (!operation.active())
|
||||
return;
|
||||
const message = `${i18n.get("Reconnect")}: ${attemptNumber}`;
|
||||
if (!operation.active())
|
||||
return;
|
||||
notice.show = message;
|
||||
if (operation.active())
|
||||
art.emit("error", error2, attemptNumber);
|
||||
} else {
|
||||
const message = i18n.get("Video Load Failed");
|
||||
if (active())
|
||||
notice.show = message;
|
||||
}
|
||||
} finally {
|
||||
attempt.dispose();
|
||||
}
|
||||
});
|
||||
art.on("video:loadedmetadata", () => {
|
||||
art.emit("resize");
|
||||
if (isMobile) {
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
};
|
||||
return {
|
||||
reset() {
|
||||
attempts = 0;
|
||||
pending?.dispose();
|
||||
removeClass($player, "art-error");
|
||||
},
|
||||
loadStart() {
|
||||
removeClass($player, "art-error");
|
||||
},
|
||||
schedule(error2) {
|
||||
void run(error2).catch((failure) => {
|
||||
console.warn("ArtPlayer reconnect failed:", failure);
|
||||
});
|
||||
}
|
||||
});
|
||||
art.on("video:loadstart", () => {
|
||||
art.loading.show = true;
|
||||
art.mask.show = false;
|
||||
art.controls.show = true;
|
||||
});
|
||||
art.on("video:pause", () => {
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
});
|
||||
art.on("video:play", () => {
|
||||
art.mask.show = false;
|
||||
setStyle($poster, "display", "none");
|
||||
});
|
||||
art.on("video:playing", () => {
|
||||
art.mask.show = false;
|
||||
});
|
||||
art.on("video:progress", () => {
|
||||
if (art.playing) {
|
||||
art.loading.show = false;
|
||||
}
|
||||
});
|
||||
art.on("video:seeked", () => {
|
||||
art.loading.show = false;
|
||||
art.mask.show = true;
|
||||
});
|
||||
art.on("video:seeking", () => {
|
||||
art.loading.show = true;
|
||||
art.mask.show = false;
|
||||
});
|
||||
art.on("video:timeupdate", () => {
|
||||
art.mask.show = false;
|
||||
});
|
||||
art.on("video:waiting", () => {
|
||||
art.loading.show = true;
|
||||
art.mask.show = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
function eventInit(art) {
|
||||
forwardMediaEvents(art);
|
||||
const reconnect = createReconnect(art);
|
||||
installReadiness(art, reconnect);
|
||||
installEnded(art);
|
||||
listenMedia(art, "video:error", reconnect.schedule);
|
||||
installPlaybackUI(art, reconnect);
|
||||
}
|
||||
function flipMix(art) {
|
||||
const {
|
||||
@@ -3412,64 +3548,6 @@ 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,
|
||||
|
||||
+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/source.test.js test/playback-properties.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/playback-properties.test.js test/media-events.test.js",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
"test:browser:install": "playwright install chromium firefox webkit",
|
||||
|
||||
@@ -99,15 +99,15 @@ when changing defaults, schema or resolution; also rerun installed-package valid
|
||||
|
||||
## Current TypeScript boundary: utilities
|
||||
|
||||
| Module | Responsibility and constraints |
|
||||
| --- | --- |
|
||||
| `src/utils/index.ts` | Existing export barrel; no added runtime names or wrapper methods |
|
||||
| `src/utils/format.ts` | Clamp, capitalization, clock formatting and HTML entity conversion; reusable private lookup maps |
|
||||
| `src/utils/property.ts` | Native defineProperty alias, own-property inspection and recursive merge; keys are written as own data properties |
|
||||
| `src/utils/time.ts` | Sleep, trailing debounce and leading throttle; infer argument tuple/receiver, preserve scheduling and synchronous return behavior |
|
||||
| `src/utils/error.ts` | ArtPlayerError, truthiness guard and internal rejection handling; public media promises are handled elsewhere |
|
||||
| `src/utils/file.ts` | Historical extension parsing and transient download anchor lifecycle |
|
||||
| `src/utils/subtitle.ts` | Existing SRT/ASS-to-VTT text conversions and VTT Blob creation |
|
||||
| Module | Responsibility and constraints |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `src/utils/index.ts` | Existing export barrel; no added runtime names or wrapper methods |
|
||||
| `src/utils/format.ts` | Clamp, capitalization, clock formatting and HTML entity conversion; reusable private lookup maps |
|
||||
| `src/utils/property.ts` | Native defineProperty alias, own-property inspection and recursive merge; keys are written as own data properties |
|
||||
| `src/utils/time.ts` | Sleep, trailing debounce and leading throttle; infer argument tuple/receiver, preserve scheduling and synchronous return behavior |
|
||||
| `src/utils/error.ts` | ArtPlayerError, truthiness guard and internal rejection handling; public media promises are handled elsewhere |
|
||||
| `src/utils/file.ts` | Historical extension parsing and transient download anchor lifecycle |
|
||||
| `src/utils/subtitle.ts` | Existing SRT/ASS-to-VTT text conversions and VTT Blob creation |
|
||||
|
||||
These modules do not import the player, UI components or each other except for the
|
||||
barrel. Existing DOM and browser capability utilities remain JS and are re-exported
|
||||
@@ -133,7 +133,7 @@ throw stops that dispatch and propagates unchanged. Off accepts either a once wr
|
||||
or its original callback; duplicate registrations of a callback are removed together.
|
||||
The once wrapper unsubscribes before invoking user code and now records consumption,
|
||||
so a nested dispatch cannot execute a captured copy twice. This fixes a demonstrated
|
||||
old defect without changing ordinary snapshot behavior. Event names such as __proto__
|
||||
old defect without changing ordinary snapshot behavior. Event names such as **proto**
|
||||
and toString use own properties rather than inherited Object.prototype values.
|
||||
|
||||
Shared contracts in test/contracts/emitter.js run against published, workspace and
|
||||
@@ -318,6 +318,42 @@ finding, not a claim that a standalone debounce can know when its owner is destr
|
||||
|
||||
## Verification and maintenance
|
||||
|
||||
### Media events and reconnect (CORE-11)
|
||||
|
||||
`player/eventInit.ts` preserves registration order while assembling
|
||||
`media/events/{forward,readiness,playback,reconnect,listen,types}.ts`.
|
||||
Forwarding keeps the configured names, original Event object and existing proxy.
|
||||
Owned Emitter handlers detach on destruction; public subscriptions remain intact.
|
||||
UI changes stop if a synchronous component callback closes the instance, so a
|
||||
first-ready control callback cannot cause a ghost ready after destroy.
|
||||
|
||||
Source ownership is now layered:
|
||||
|
||||
```text
|
||||
instance scope
|
||||
current source scope
|
||||
source operation scope (switch completion/failure)
|
||||
pending reconnect scope
|
||||
```
|
||||
|
||||
The source scope survives a rejected switch, allowing the current resource to retry.
|
||||
Playback guards capture both source and operation identity, including late play
|
||||
after a failed switch has already removed its operation from the current map.
|
||||
Changing URL releases the old source scope and all its pending work. Reconnect
|
||||
coalesces duplicate errors into one pending attempt, preserves its first Event,
|
||||
uses the existing delay/limit, and resets on canplay or an independent new source.
|
||||
It still reads option.url when the retry starts; reentrant source replacement
|
||||
invalidates that attempt. Attempt numbers are captured before source assignment
|
||||
so synchronous proxy canplay cannot change the reported number. Recovery cancels
|
||||
pending failure notices and clears art-error. Internal asynchronous observer errors
|
||||
are reported through console.warn with the original thrown value.
|
||||
|
||||
`test/media-events.test.js` covers forwarding, sequence, cancellation, retry budget,
|
||||
failed-switch recovery and reentry. Its browser counterpart uses real HTTP 503 and
|
||||
local media, compares old/new stale retry and ghost-ready behavior, and checks the
|
||||
mobile metadata branch with an Android UA (not a physical device). Proxy-owned SDK
|
||||
events still need adapter-level source identity; native Event has no source token.
|
||||
|
||||
### Playback properties (CORE-10)
|
||||
|
||||
The toggle, currentTime, seek/forward/backward, volume/muted, playbackRate,
|
||||
@@ -365,8 +401,8 @@ guards deferred invocation, ignores obsolete returned failures, and cancels its
|
||||
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.
|
||||
are suppressed. CORE-10 migrates playback properties and CORE-11 owns reconnect
|
||||
generations; object URL ownership remains CORE-19 work.
|
||||
|
||||
`test/source.test.js` checks cancellation, reentry, synchronous events and cleanup;
|
||||
`test/browser/source.spec.js` compares published/candidate real-media switching and
|
||||
|
||||
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,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
|
||||
!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
|
||||
+224
-146
@@ -2770,105 +2770,241 @@ function durationMix(art) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function eventInit(art) {
|
||||
const {
|
||||
i18n,
|
||||
notice,
|
||||
option,
|
||||
constructor,
|
||||
proxy,
|
||||
template: { $player, $video, $poster }
|
||||
} = art;
|
||||
let reconnectTime = 0;
|
||||
function forwardMediaEvents(art) {
|
||||
const { proxy, template: { $video } } = art;
|
||||
for (let index = 0; index < config$1.events.length; index++) {
|
||||
proxy($video, config$1.events[index], (event) => {
|
||||
art.emit(`video:${event.type}`, event);
|
||||
if (!isClosing(art))
|
||||
art.emit(`video:${event.type}`, event);
|
||||
});
|
||||
}
|
||||
art.on("video:canplay", () => {
|
||||
reconnectTime = 0;
|
||||
art.loading.show = false;
|
||||
}
|
||||
function listenMedia(art, name, callback, once = false) {
|
||||
const guarded = (event) => {
|
||||
if (!isClosing(art))
|
||||
callback(event);
|
||||
};
|
||||
art[once ? "once" : "on"](name, guarded);
|
||||
getScope(art).add(() => {
|
||||
art.off(name, guarded);
|
||||
});
|
||||
art.once("video:canplay", () => {
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
}
|
||||
function showMediaUI(art, changes, active) {
|
||||
for (const [name, value] of changes) {
|
||||
if (isClosing(art) || active && !active())
|
||||
return;
|
||||
art[name].show = value;
|
||||
}
|
||||
}
|
||||
const current = /* @__PURE__ */ new WeakMap();
|
||||
const assignments = /* @__PURE__ */ new WeakMap();
|
||||
const sources = /* @__PURE__ */ new WeakMap();
|
||||
function getSourceScope(owner) {
|
||||
return sources.get(owner) || getScope(owner);
|
||||
}
|
||||
function captureSource(owner) {
|
||||
const operation = current.get(owner);
|
||||
const source = sources.get(owner);
|
||||
return () => current.get(owner) === operation && (!operation || operation.active()) && sources.get(owner) === source && (!source || !source.closed);
|
||||
}
|
||||
function beginSource(owner) {
|
||||
const previous = sources.get(owner);
|
||||
const source = getScope(owner).child();
|
||||
const scope = source.child();
|
||||
sources.set(owner, source);
|
||||
source.add(() => {
|
||||
if (sources.get(owner) === source)
|
||||
sources.delete(owner);
|
||||
});
|
||||
const operation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
acceptingEvents: false,
|
||||
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation
|
||||
};
|
||||
current.set(owner, operation);
|
||||
scope.add(() => {
|
||||
if (current.get(owner) === operation)
|
||||
current.delete(owner);
|
||||
operation.onAssigned = void 0;
|
||||
operation.onError = void 0;
|
||||
});
|
||||
previous?.dispose();
|
||||
return operation;
|
||||
}
|
||||
function takeAssignment(owner) {
|
||||
const operation = assignments.get(owner);
|
||||
assignments.delete(owner);
|
||||
return operation || beginSource(owner);
|
||||
}
|
||||
function finishAssignment(operation) {
|
||||
operation.assigned = true;
|
||||
const callback = operation.onAssigned;
|
||||
operation.onAssigned = void 0;
|
||||
if (operation.active())
|
||||
callback?.();
|
||||
}
|
||||
function assignUrl(owner, operation, url) {
|
||||
assignments.set(owner, operation);
|
||||
operation.acceptingEvents = true;
|
||||
try {
|
||||
owner.url = url;
|
||||
} finally {
|
||||
if (assignments.get(owner) === operation) {
|
||||
assignments.delete(owner);
|
||||
finishAssignment(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
function failSource(operation, error2) {
|
||||
if (!operation.active())
|
||||
return;
|
||||
if (operation.onError)
|
||||
operation.onError(error2);
|
||||
else
|
||||
console.warn("Failed to initialize ArtPlayer source:", error2);
|
||||
operation.scope.dispose();
|
||||
}
|
||||
function installEnded(art) {
|
||||
const { option } = art;
|
||||
listenMedia(art, "video:ended", () => {
|
||||
if (option.loop) {
|
||||
const active = captureSource(art);
|
||||
art.seek = 0;
|
||||
if (isClosing(art) || !active())
|
||||
return;
|
||||
silencePromise(art.play());
|
||||
if (active())
|
||||
showMediaUI(art, [["controls", false], ["mask", false]], active);
|
||||
} else {
|
||||
showMediaUI(art, [["controls", true], ["mask", true]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
function installPlaybackUI(art, reconnect) {
|
||||
const { template: { $poster } } = art;
|
||||
listenMedia(art, "video:loadedmetadata", () => {
|
||||
art.emit("resize");
|
||||
if (isMobile)
|
||||
showMediaUI(art, [["loading", false], ["controls", true], ["mask", true]]);
|
||||
});
|
||||
listenMedia(art, "video:loadstart", () => {
|
||||
reconnect.loadStart();
|
||||
showMediaUI(art, [["loading", true], ["mask", false], ["controls", true]]);
|
||||
});
|
||||
listenMedia(art, "video:pause", () => {
|
||||
showMediaUI(art, [["controls", true], ["mask", true]]);
|
||||
});
|
||||
listenMedia(art, "video:play", () => {
|
||||
showMediaUI(art, [["mask", false]]);
|
||||
if (!isClosing(art))
|
||||
setStyle($poster, "display", "none");
|
||||
});
|
||||
listenMedia(art, "video:playing", () => showMediaUI(art, [["mask", false]]));
|
||||
listenMedia(art, "video:progress", () => {
|
||||
if (art.playing)
|
||||
showMediaUI(art, [["loading", false]]);
|
||||
});
|
||||
listenMedia(art, "video:seeked", () => showMediaUI(art, [["loading", false], ["mask", true]]));
|
||||
listenMedia(art, "video:seeking", () => showMediaUI(art, [["loading", true], ["mask", false]]));
|
||||
listenMedia(art, "video:timeupdate", () => showMediaUI(art, [["mask", false]]));
|
||||
listenMedia(art, "video:waiting", () => showMediaUI(art, [["loading", true], ["mask", false]]));
|
||||
}
|
||||
function installReadiness(art, reconnect) {
|
||||
listenMedia(art, "video:canplay", () => {
|
||||
reconnect.reset();
|
||||
showMediaUI(art, [["loading", false]]);
|
||||
});
|
||||
listenMedia(art, "video:canplay", () => {
|
||||
showMediaUI(art, [["loading", false], ["controls", true], ["mask", true]]);
|
||||
if (isClosing(art))
|
||||
return;
|
||||
art.isReady = true;
|
||||
art.emit("ready");
|
||||
});
|
||||
art.on("video:ended", () => {
|
||||
if (option.loop) {
|
||||
art.seek = 0;
|
||||
silencePromise(art.play());
|
||||
art.controls.show = false;
|
||||
art.mask.show = false;
|
||||
} else {
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
}, true);
|
||||
}
|
||||
function createReconnect(art) {
|
||||
const { option, constructor, i18n, notice, template: { $player } } = art;
|
||||
let source = getSourceScope(art);
|
||||
let attempts = 0;
|
||||
let pending;
|
||||
const run = async (error2) => {
|
||||
if (isClosing(art))
|
||||
return;
|
||||
const current2 = getSourceScope(art);
|
||||
if (current2 !== source) {
|
||||
source = current2;
|
||||
attempts = 0;
|
||||
}
|
||||
});
|
||||
art.on("video:error", async (error2) => {
|
||||
if (reconnectTime < constructor.RECONNECT_TIME_MAX) {
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
if (pending && !pending.closed)
|
||||
return;
|
||||
const attempt = current2.child();
|
||||
pending = attempt;
|
||||
attempt.add(() => {
|
||||
if (pending === attempt)
|
||||
pending = void 0;
|
||||
});
|
||||
const active = () => !isClosing(art) && !attempt.closed && current2 === getSourceScope(art);
|
||||
const retry = attempts < constructor.RECONNECT_TIME_MAX;
|
||||
try {
|
||||
if (!retry) {
|
||||
showMediaUI(art, [["mask", true], ["loading", false], ["controls", true]], active);
|
||||
if (!active())
|
||||
return;
|
||||
addClass($player, "art-error");
|
||||
}
|
||||
if (!await wait(attempt, constructor.RECONNECT_SLEEP_TIME) || !active())
|
||||
return;
|
||||
reconnectTime += 1;
|
||||
art.url = option.url;
|
||||
notice.show = `${i18n.get("Reconnect")}: ${reconnectTime}`;
|
||||
art.emit("error", error2, reconnectTime);
|
||||
} else {
|
||||
art.mask.show = true;
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
addClass($player, "art-error");
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return;
|
||||
notice.show = i18n.get("Video Load Failed");
|
||||
if (retry) {
|
||||
const url = option.url;
|
||||
if (!active())
|
||||
return;
|
||||
attempts += 1;
|
||||
const attemptNumber = attempts;
|
||||
attempt.dispose();
|
||||
const operation = beginSource(art);
|
||||
source = getSourceScope(art);
|
||||
assignUrl(art, operation, url);
|
||||
if (!operation.active())
|
||||
return;
|
||||
const message = `${i18n.get("Reconnect")}: ${attemptNumber}`;
|
||||
if (!operation.active())
|
||||
return;
|
||||
notice.show = message;
|
||||
if (operation.active())
|
||||
art.emit("error", error2, attemptNumber);
|
||||
} else {
|
||||
const message = i18n.get("Video Load Failed");
|
||||
if (active())
|
||||
notice.show = message;
|
||||
}
|
||||
} finally {
|
||||
attempt.dispose();
|
||||
}
|
||||
});
|
||||
art.on("video:loadedmetadata", () => {
|
||||
art.emit("resize");
|
||||
if (isMobile) {
|
||||
art.loading.show = false;
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
};
|
||||
return {
|
||||
reset() {
|
||||
attempts = 0;
|
||||
pending?.dispose();
|
||||
removeClass($player, "art-error");
|
||||
},
|
||||
loadStart() {
|
||||
removeClass($player, "art-error");
|
||||
},
|
||||
schedule(error2) {
|
||||
void run(error2).catch((failure) => {
|
||||
console.warn("ArtPlayer reconnect failed:", failure);
|
||||
});
|
||||
}
|
||||
});
|
||||
art.on("video:loadstart", () => {
|
||||
art.loading.show = true;
|
||||
art.mask.show = false;
|
||||
art.controls.show = true;
|
||||
});
|
||||
art.on("video:pause", () => {
|
||||
art.controls.show = true;
|
||||
art.mask.show = true;
|
||||
});
|
||||
art.on("video:play", () => {
|
||||
art.mask.show = false;
|
||||
setStyle($poster, "display", "none");
|
||||
});
|
||||
art.on("video:playing", () => {
|
||||
art.mask.show = false;
|
||||
});
|
||||
art.on("video:progress", () => {
|
||||
if (art.playing) {
|
||||
art.loading.show = false;
|
||||
}
|
||||
});
|
||||
art.on("video:seeked", () => {
|
||||
art.loading.show = false;
|
||||
art.mask.show = true;
|
||||
});
|
||||
art.on("video:seeking", () => {
|
||||
art.loading.show = true;
|
||||
art.mask.show = false;
|
||||
});
|
||||
art.on("video:timeupdate", () => {
|
||||
art.mask.show = false;
|
||||
});
|
||||
art.on("video:waiting", () => {
|
||||
art.loading.show = true;
|
||||
art.mask.show = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
function eventInit(art) {
|
||||
forwardMediaEvents(art);
|
||||
const reconnect = createReconnect(art);
|
||||
installReadiness(art, reconnect);
|
||||
installEnded(art);
|
||||
listenMedia(art, "video:error", reconnect.schedule);
|
||||
installPlaybackUI(art, reconnect);
|
||||
}
|
||||
function flipMix(art) {
|
||||
const {
|
||||
@@ -3412,64 +3548,6 @@ 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,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { MediaEventHost } from './types'
|
||||
import config from '../../config'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
|
||||
export function forwardMediaEvents(art: MediaEventHost): void {
|
||||
const { proxy, template: { $video } } = art
|
||||
for (let index = 0; index < config.events.length; index++) {
|
||||
proxy($video, config.events[index]!, (event: Event) => {
|
||||
if (!isClosing(art))
|
||||
art.emit(`video:${event.type}`, event)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MediaEventName, MediaEvents, MediaListener, MediaUI } from './types'
|
||||
import { getScope, isClosing } from '../../lifecycle/instance'
|
||||
|
||||
export function listenMedia(art: MediaEvents, name: MediaEventName, callback: MediaListener, once = false): void {
|
||||
const guarded: MediaListener = (event) => {
|
||||
if (!isClosing(art))
|
||||
callback(event)
|
||||
}
|
||||
art[once ? 'once' : 'on'](name, guarded)
|
||||
getScope(art).add(() => {
|
||||
art.off(name, guarded)
|
||||
})
|
||||
}
|
||||
|
||||
export function showMediaUI(art: MediaUI, changes: [keyof MediaUI, boolean][], active?: () => boolean): void {
|
||||
for (const [name, value] of changes) {
|
||||
if (isClosing(art) || (active && !active()))
|
||||
return
|
||||
art[name].show = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { MediaEventHost, Reconnect } from './types'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
import { captureSource } from '../../source/operation'
|
||||
import { isMobile, setStyle, silencePromise } from '../../utils'
|
||||
import { listenMedia, showMediaUI } from './listen'
|
||||
|
||||
export function installEnded(art: MediaEventHost): void {
|
||||
const { option } = art
|
||||
listenMedia(art, 'video:ended', () => {
|
||||
if (option.loop) {
|
||||
const active = captureSource(art)
|
||||
art.seek = 0
|
||||
if (isClosing(art) || !active())
|
||||
return
|
||||
silencePromise(art.play())
|
||||
if (active())
|
||||
showMediaUI(art, [['controls', false], ['mask', false]], active)
|
||||
}
|
||||
else {
|
||||
showMediaUI(art, [['controls', true], ['mask', true]])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function installPlaybackUI(art: MediaEventHost, reconnect: Reconnect): void {
|
||||
const { template: { $poster } } = art
|
||||
listenMedia(art, 'video:loadedmetadata', () => {
|
||||
art.emit('resize')
|
||||
if (isMobile)
|
||||
showMediaUI(art, [['loading', false], ['controls', true], ['mask', true]])
|
||||
})
|
||||
listenMedia(art, 'video:loadstart', () => {
|
||||
reconnect.loadStart()
|
||||
showMediaUI(art, [['loading', true], ['mask', false], ['controls', true]])
|
||||
})
|
||||
listenMedia(art, 'video:pause', () => {
|
||||
showMediaUI(art, [['controls', true], ['mask', true]])
|
||||
})
|
||||
listenMedia(art, 'video:play', () => {
|
||||
showMediaUI(art, [['mask', false]])
|
||||
if (!isClosing(art))
|
||||
setStyle($poster, 'display', 'none')
|
||||
})
|
||||
listenMedia(art, 'video:playing', () => showMediaUI(art, [['mask', false]]))
|
||||
listenMedia(art, 'video:progress', () => {
|
||||
if (art.playing)
|
||||
showMediaUI(art, [['loading', false]])
|
||||
})
|
||||
listenMedia(art, 'video:seeked', () => showMediaUI(art, [['loading', false], ['mask', true]]))
|
||||
listenMedia(art, 'video:seeking', () => showMediaUI(art, [['loading', true], ['mask', false]]))
|
||||
listenMedia(art, 'video:timeupdate', () => showMediaUI(art, [['mask', false]]))
|
||||
listenMedia(art, 'video:waiting', () => showMediaUI(art, [['loading', true], ['mask', false]]))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { MediaEventHost, Reconnect } from './types'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
import { listenMedia, showMediaUI } from './listen'
|
||||
|
||||
export function installReadiness(art: MediaEventHost, reconnect: Reconnect): void {
|
||||
listenMedia(art, 'video:canplay', () => {
|
||||
reconnect.reset()
|
||||
showMediaUI(art, [['loading', false]])
|
||||
})
|
||||
listenMedia(art, 'video:canplay', () => {
|
||||
showMediaUI(art, [['loading', false], ['controls', true], ['mask', true]])
|
||||
if (isClosing(art))
|
||||
return
|
||||
art.isReady = true
|
||||
art.emit('ready')
|
||||
}, true)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type ResourceScope from '../../lifecycle/scope'
|
||||
import type { MediaEventHost, Reconnect } from './types'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
import { wait } from '../../lifecycle/resources'
|
||||
import { assignUrl, beginSource, getSourceScope } from '../../source/operation'
|
||||
import { addClass, removeClass } from '../../utils'
|
||||
import { showMediaUI } from './listen'
|
||||
|
||||
export function createReconnect(art: MediaEventHost): Reconnect {
|
||||
const { option, constructor, i18n, notice, template: { $player } } = art
|
||||
let source = getSourceScope(art)
|
||||
let attempts = 0
|
||||
let pending: ResourceScope | undefined
|
||||
|
||||
const run = async (error: unknown) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = getSourceScope(art)
|
||||
if (current !== source) {
|
||||
source = current
|
||||
attempts = 0
|
||||
}
|
||||
if (pending && !pending.closed)
|
||||
return
|
||||
const attempt = current.child()
|
||||
pending = attempt
|
||||
attempt.add(() => {
|
||||
if (pending === attempt)
|
||||
pending = undefined
|
||||
})
|
||||
const active = () => !isClosing(art) && !attempt.closed && current === getSourceScope(art)
|
||||
const retry = attempts < constructor.RECONNECT_TIME_MAX
|
||||
try {
|
||||
if (!retry) {
|
||||
showMediaUI(art, [['mask', true], ['loading', false], ['controls', true]], active)
|
||||
if (!active())
|
||||
return
|
||||
addClass($player, 'art-error')
|
||||
}
|
||||
if (!await wait(attempt, constructor.RECONNECT_SLEEP_TIME) || !active())
|
||||
return
|
||||
if (retry) {
|
||||
const url = option.url
|
||||
if (!active())
|
||||
return
|
||||
attempts += 1
|
||||
const attemptNumber = attempts
|
||||
// End the old timer before creating the retry's new source generation.
|
||||
attempt.dispose()
|
||||
const operation = beginSource(art)
|
||||
source = getSourceScope(art)
|
||||
assignUrl(art, operation, url)
|
||||
if (!operation.active())
|
||||
return
|
||||
const message = `${i18n.get('Reconnect')}: ${attemptNumber}`
|
||||
if (!operation.active())
|
||||
return
|
||||
notice.show = message
|
||||
if (operation.active())
|
||||
art.emit('error', error, attemptNumber)
|
||||
}
|
||||
else {
|
||||
const message = i18n.get('Video Load Failed')
|
||||
if (active())
|
||||
notice.show = message
|
||||
}
|
||||
}
|
||||
finally {
|
||||
attempt.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reset() {
|
||||
attempts = 0
|
||||
pending?.dispose()
|
||||
removeClass($player, 'art-error')
|
||||
},
|
||||
loadStart() {
|
||||
removeClass($player, 'art-error')
|
||||
},
|
||||
schedule(error) {
|
||||
void run(error).catch((failure) => {
|
||||
console.warn('ArtPlayer reconnect failed:', failure)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export type MediaEventName = `video:${string}`
|
||||
export type MediaListener = (event?: unknown) => void
|
||||
|
||||
export interface MediaEvents {
|
||||
on: (name: MediaEventName, callback: MediaListener) => unknown
|
||||
once: (name: MediaEventName, callback: MediaListener) => unknown
|
||||
off: (name: MediaEventName, callback: MediaListener) => unknown
|
||||
emit: {
|
||||
(name: MediaEventName, event: Event): unknown
|
||||
(name: 'ready' | 'resize'): unknown
|
||||
(name: 'error', error: unknown, attempt: number): unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface MediaUI {
|
||||
loading: { set show(value: boolean) }
|
||||
controls: { set show(value: boolean) }
|
||||
mask: { set show(value: boolean) }
|
||||
}
|
||||
|
||||
export interface MediaEventHost extends MediaEvents, MediaUI {
|
||||
template: {
|
||||
$video: EventTarget
|
||||
$player: Pick<HTMLElement, 'classList'>
|
||||
$poster: Pick<HTMLElement, 'style'>
|
||||
}
|
||||
proxy: (target: EventTarget, name: string, callback: (event: Event) => void) => unknown
|
||||
isReady: boolean
|
||||
readonly playing: boolean
|
||||
url: string | null
|
||||
set seek(time: number)
|
||||
play: () => unknown
|
||||
option: { url: string, loop: boolean }
|
||||
i18n: { get: (key: string) => string }
|
||||
notice: { set show(message: string) }
|
||||
constructor: { RECONNECT_TIME_MAX: number, RECONNECT_SLEEP_TIME: number }
|
||||
}
|
||||
|
||||
export interface Reconnect {
|
||||
reset: () => void
|
||||
loadStart: () => void
|
||||
schedule: (error: unknown) => void
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import config from '../config'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { wait } from '../lifecycle/resources'
|
||||
import { addClass, isMobile, setStyle, silencePromise } from '../utils'
|
||||
|
||||
export default function eventInit(art) {
|
||||
const {
|
||||
i18n,
|
||||
notice,
|
||||
option,
|
||||
constructor,
|
||||
proxy,
|
||||
template: { $player, $video, $poster },
|
||||
} = art
|
||||
|
||||
let reconnectTime = 0
|
||||
|
||||
for (let index = 0; index < config.events.length; index++) {
|
||||
proxy($video, config.events[index], (event) => {
|
||||
art.emit(`video:${event.type}`, event)
|
||||
})
|
||||
}
|
||||
|
||||
// art.on('video:abort', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:canplay', () => {
|
||||
reconnectTime = 0
|
||||
art.loading.show = false
|
||||
})
|
||||
|
||||
art.once('video:canplay', () => {
|
||||
art.loading.show = false
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
art.isReady = true
|
||||
art.emit('ready')
|
||||
})
|
||||
|
||||
// art.on('video:canplaythrough', () => {
|
||||
|
||||
// });
|
||||
|
||||
// art.on('video:durationchange', () => {
|
||||
|
||||
// });
|
||||
|
||||
// art.on('video:emptied', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:ended', () => {
|
||||
if (option.loop) {
|
||||
art.seek = 0
|
||||
silencePromise(art.play())
|
||||
art.controls.show = false
|
||||
art.mask.show = false
|
||||
}
|
||||
else {
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
}
|
||||
})
|
||||
|
||||
art.on('video:error', async (error) => {
|
||||
if (reconnectTime < constructor.RECONNECT_TIME_MAX) {
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return
|
||||
reconnectTime += 1
|
||||
art.url = option.url
|
||||
notice.show = `${i18n.get('Reconnect')}: ${reconnectTime}`
|
||||
art.emit('error', error, reconnectTime)
|
||||
}
|
||||
else {
|
||||
art.mask.show = true
|
||||
art.loading.show = false
|
||||
art.controls.show = true
|
||||
addClass($player, 'art-error')
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return
|
||||
notice.show = i18n.get('Video Load Failed')
|
||||
}
|
||||
})
|
||||
|
||||
// art.on('video:loadeddata', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:loadedmetadata', () => {
|
||||
art.emit('resize')
|
||||
if (isMobile) {
|
||||
art.loading.show = false
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
}
|
||||
})
|
||||
|
||||
art.on('video:loadstart', () => {
|
||||
art.loading.show = true
|
||||
art.mask.show = false
|
||||
art.controls.show = true
|
||||
})
|
||||
|
||||
art.on('video:pause', () => {
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
})
|
||||
|
||||
art.on('video:play', () => {
|
||||
art.mask.show = false
|
||||
setStyle($poster, 'display', 'none')
|
||||
})
|
||||
|
||||
art.on('video:playing', () => {
|
||||
art.mask.show = false
|
||||
})
|
||||
|
||||
art.on('video:progress', () => {
|
||||
if (art.playing) {
|
||||
art.loading.show = false
|
||||
}
|
||||
})
|
||||
|
||||
// art.on('video:ratechange', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:seeked', () => {
|
||||
art.loading.show = false
|
||||
art.mask.show = true
|
||||
})
|
||||
|
||||
art.on('video:seeking', () => {
|
||||
art.loading.show = true
|
||||
art.mask.show = false
|
||||
})
|
||||
|
||||
// art.on('video:stalled', () => {
|
||||
|
||||
// });
|
||||
|
||||
// art.on('video:suspend', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:timeupdate', () => {
|
||||
art.mask.show = false
|
||||
})
|
||||
|
||||
// art.on('video:volumechange', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:waiting', () => {
|
||||
art.loading.show = true
|
||||
art.mask.show = false
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { MediaEventHost } from '../media/events/types'
|
||||
import { forwardMediaEvents } from '../media/events/forward'
|
||||
import { listenMedia } from '../media/events/listen'
|
||||
import { installEnded, installPlaybackUI } from '../media/events/playback'
|
||||
import { installReadiness } from '../media/events/readiness'
|
||||
import { createReconnect } from '../media/events/reconnect'
|
||||
|
||||
export default function eventInit(art: MediaEventHost): void {
|
||||
forwardMediaEvents(art)
|
||||
const reconnect = createReconnect(art)
|
||||
installReadiness(art, reconnect)
|
||||
installEnded(art)
|
||||
listenMedia(art, 'video:error', reconnect.schedule)
|
||||
installPlaybackUI(art, reconnect)
|
||||
}
|
||||
@@ -13,15 +13,28 @@ export interface SourceOperation {
|
||||
|
||||
const current = new WeakMap<object, SourceOperation>()
|
||||
const assignments = new WeakMap<object, SourceOperation>()
|
||||
const sources = new WeakMap<object, ResourceScope>()
|
||||
|
||||
export function getSourceScope(owner: object): ResourceScope {
|
||||
return sources.get(owner) || getScope(owner)
|
||||
}
|
||||
|
||||
export function captureSource(owner: object): () => boolean {
|
||||
const operation = current.get(owner)
|
||||
const source = sources.get(owner)
|
||||
return () => current.get(owner) === operation && (!operation || operation.active())
|
||||
&& sources.get(owner) === source && (!source || !source.closed)
|
||||
}
|
||||
|
||||
export function beginSource(owner: object): SourceOperation {
|
||||
const previous = current.get(owner)
|
||||
const scope = getScope(owner).child()
|
||||
const previous = sources.get(owner)
|
||||
const source = getScope(owner).child()
|
||||
const scope = source.child()
|
||||
sources.set(owner, source)
|
||||
source.add(() => {
|
||||
if (sources.get(owner) === source)
|
||||
sources.delete(owner)
|
||||
})
|
||||
const operation: SourceOperation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
@@ -35,7 +48,7 @@ export function beginSource(owner: object): SourceOperation {
|
||||
operation.onAssigned = undefined
|
||||
operation.onError = undefined
|
||||
})
|
||||
previous?.scope.dispose()
|
||||
previous?.dispose()
|
||||
return operation
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,41 @@
|
||||
# CORE-11 媒体事件、ready 与重连
|
||||
|
||||
状态:完成;源码、构建、安装消费与本地三浏览器检查通过。
|
||||
|
||||
## 模块与生命周期
|
||||
|
||||
player/eventInit.ts 仅负责按原顺序装配;media/events 下分为 types、listen、forward、readiness、playback、reconnect 六个 TS 模块。转发仍使用原 proxy 和配置事件名,发出同一个原生 Event 对象。内部 Emitter 监听归实例作用域,销毁时只移除自己的监听,用户订阅和原 snapshot 顺序保留。
|
||||
|
||||
source/operation.ts 现在分开资源代次与切源操作。实例作用域拥有当前资源作用域,后者拥有 SourceOperation.scope 及重连等待。切源失败可以结束操作并拒绝调用方 Promise,同时保留当前资源的自动重连;新的 URL 赋值或 destroy 则释放整个旧资源作用域。不能把重连绑定在失败即释放的 switch 作用域上。
|
||||
|
||||
播放有效性同时捕获资源与操作身份,覆盖“失败切源已清除操作后,再开始播放并销毁”的组合路径;迟到 Promise 仍保留原值/拒绝,过期通知、play 和互斥暂停停止执行。
|
||||
|
||||
## 兼容和缺陷修正
|
||||
|
||||
- 保留 API-04/05/07:配置事件顺序、原 Event、ready 的正常顺序和次数、resize/mobile UI 顺序、loop 和 play 拒绝处理。
|
||||
- 同一资源同时只排一个重试,保留第一条待处理错误。按原常量等待、保留原错误参数,最多按预算重试;canplay 重置预算。重试次数在开始该次重试时固定,避免同步代理 canplay 把错误回调的次数变成 0。
|
||||
- 保留等待结束时读取 option.url 的行为,使应用能在 native error 回调更新重试目标;若该 getter 重入切换资源,旧任务立即退出。
|
||||
- BASE-LIFE-07:旧待办重连在用户已换源后仍会重新加载并发 error。现在新 URL、canplay 或销毁会取消过期重试/失败通知;独立新资源使用新预算,恢复时移除 art-error。
|
||||
- BASE-LIFE-08:旧 ready 流程在 control 回调销毁实例后仍能继续标记 isReady 并发 ready。现在每步 UI 检查关闭状态,停止后续 ready;用户其他事件订阅仍保留。
|
||||
- 内部重连异步回调若抛错,通过 console.warn 带原异常报告,避免无人接收的 Promise 拒绝。直接用户 Emitter 回调的同步异常传播不统一吞掉。
|
||||
|
||||
## 验证与限制
|
||||
|
||||
test/media-events.test.js 通过真实 EventTarget 和受控计时验证预算、合并、资源代次、失败切源、恢复/销毁、UI 重入、同步代理和原错误身份;test/types/media-events.ts 验证最小媒体事件宿主。test/browser/media-events.spec.js 使用真实 HTTP 503 与本地媒体恢复,比较发布/候选,并验证 Android UA 元数据路径。
|
||||
|
||||
媒体事件没有资源 ID。第三方 customType/proxy 已启动的外部 SDK 任务及其伪装成当前资源的事件仍由适配器管理;这里不宣称能取消任意外部代码。UA 检查不是实体移动设备验收。
|
||||
|
||||
证据见 [media-events-validation.json](../baselines/media-events-validation.json),包括完整源码/测试/包文件 SHA、实际安装消费者和逐项浏览器结果。
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| yarn ci:check | 158 项:129 单元、4 工程、25 基线;54 个生产 TS 文件严格编译 |
|
||||
| 媒体事件专项 | 新增 13 项 Node,涵盖 EventTarget、定时器、源/操作生命周期及真实 playMix |
|
||||
| 安装包 | 核心/chapter 的 27 项运行时通过;五组类型模式零诊断 |
|
||||
| 安装 UMD | 三浏览器 222 项通过,无跳过、重试或 flaky |
|
||||
| 安装 legacy | 同套三浏览器 222 项通过,无跳过、重试或 flaky |
|
||||
| 新旧差异 | 真实 HTTP 恢复/旧重连取消、ghost ready、移动 UA 顺序均有发布/候选对照 |
|
||||
|
||||
BASE-LIFE-07/08 已关闭,旧发布基线未改。HTTP 503 是受控失败,产生的请求/控制台记录保留在原始浏览器证据;未处理 JS 异常为零。早期通过报告不替代包含源级播放守卫的最终包指纹与完整复验。
|
||||
|
||||
未运行远端 CI、实体设备或所有生态包的发布验收。无依赖、锁文件或版本变化;本任务单独本地 commit。回退须连同 source/operation 的资源代次改动、事件模块、产物和文档整体回退。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 168 / doing 0 / blocked 0 / done 46 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 167 / doing 0 / blocked 0 / done 47 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
| 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 | done |
|
||||
| CORE-10 | artplayer<br>迁移播放与时间/状态属性 | CORE-09 | play/pause/toggle/seek/volume/rate/duration 等模块 | 旧返回值、拒绝、mutex、位置恢复、属性描述符与事件通过;接续 CORE-07 / BASE-TYPE-04 的 toggle void 与实际 Promise 分支冲突,不吞拒绝 | H | done |
|
||||
| CORE-11 | artplayer<br>整理媒体事件、ready 与重连 | CORE-10 | 媒体转发、重连、UI 响应职责分离 | readiness 次数/顺序、过期重连、原错误参数有回归 | H | todo |
|
||||
| CORE-11 | artplayer<br>整理媒体事件、ready 与重连 | CORE-10 | 媒体转发、重连、UI 响应职责分离 | readiness 次数/顺序、过期重连、原错误参数有回归 | H | done |
|
||||
| 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 |
|
||||
| CORE-13 | artplayer<br>迁移组件注册与控件 | CORE-12, CORE-08 | Component/control/layer/contextmenu 的资源与 TS 边界 | add/update/remove、name、selector、mounted/beforeUnmount 通过;处理 BASE-DOM-02 窄容器控件裁切,保留 controls.update 的旧 undefined 返回值 | H | todo |
|
||||
| CORE-14 | artplayer<br>拆分设置树、渲染和布局 | CORE-13 | setting model/selection/render/layout 的分批实现 | 不强行统一控件返回值,嵌套选择、默认高亮、更新清理通过;结合 BASE-DOM-02 验证窄容器设置定位;接续 BASE-TYPE-07 的 find null、add/update item、remove undefined 声明冲突 | H | todo |
|
||||
@@ -445,6 +445,7 @@
|
||||
- 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)
|
||||
- CORE-10: [记录](changes/2026-09-11-CORE-10-playback-properties.md) [记录](baselines/playback-properties-validation.json)
|
||||
- CORE-11: [记录](changes/2026-09-11-CORE-11-media-events.md) [记录](baselines/media-events-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-11 媒体事件、ready 与重连完成
|
||||
|
||||
事件入口迁移 TS,拆出转发、ready、播放 UI、重连、订阅和宿主类型;资源代次与切源操作分离,失败切源仍可重试,换源/恢复/销毁取消过期工作。158 项 Node/基线、安装 UMD 与 legacy 各 222 项三浏览器、打包 27 项运行时及五组类型检查通过。见 [交付](changes/2026-09-11-CORE-11-media-events.md)。关闭新增 BASE-LIFE-07/08,保留原 Event、重试次数和正常 ready 顺序;第三方 SDK 事件仍由适配器负责。当前 214 项,47 完成、167 待办;核心 49 个 TS 文件、chapter 5 个。下一项 CORE-12 模板及公开资源入口。
|
||||
|
||||
## 当前实施:CORE-10 播放与时间/状态属性完成
|
||||
|
||||
八个 mixin 迁移 TS,新增最小播放属性宿主;保持转换、通知、存储、进度和状态优先级。新增 PlaybackControls 精确视图,旧 toggle 声明仍兼容。145 项 Node/基线、安装 UMD 与 legacy 各 195 项三浏览器、打包 27 项运行时和五组类型检查通过。见 [交付](changes/2026-09-11-CORE-10-playback-properties.md)。发现 BASE-TYPE-08 的只写属性虚构 getter 声明,已交 CORE-21;没有添加运行时 getter 来掩盖它。当前 214 项,46 完成、168 待办;核心 42 个 TS 文件、chapter 5 个。下一项 CORE-11 媒体事件、ready 与重连。
|
||||
|
||||
@@ -52,3 +52,5 @@
|
||||
| BASE-TYPE-06 | resolved / 已复现 | Public Option requires url and excludes numeric control HTML accepted by existing JS | CORE-07 |
|
||||
| BASE-TYPE-07 | open / 已复现 | Setting returns, subtitle contextual payloads and notice getter conflict with historical declarations | CORE-07, CORE-14, CORE-15, CORE-18, CORE-21 |
|
||||
| BASE-TYPE-08 | open / 已复现 | Write-only player commands have fictitious public getter declarations | CORE-21 |
|
||||
| BASE-LIFE-07 | resolved / 已复现 | Queued reconnect survives source replacement and reloads the new resource | CORE-11 |
|
||||
| BASE-LIFE-08 | resolved / 已复现 | Ready continues after a control callback destroys the instance | CORE-11 |
|
||||
@@ -891,6 +891,52 @@
|
||||
],
|
||||
"compatibleResolution": "Keep historical consumer acceptance, model source command reads as undefined, and provide an accurate public facade without inventing runtime getters.",
|
||||
"closureCriteria": "seek/forward/backward/switch/quality reads and writes have an explicit accurate compatibility strategy validated against old TypeScript consumers and real package descriptors."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-07",
|
||||
"title": "Queued reconnect survives source replacement and reloads the new resource",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-11"
|
||||
],
|
||||
"evidence": [
|
||||
"test/media-events.test.js",
|
||||
"test/browser/media-events.spec.js",
|
||||
"refactor/changes/2026-09-11-CORE-11-media-events.md"
|
||||
],
|
||||
"compatibleResolution": "Keep ordinary event order and error identity; scope retries to the current source and stop internal UI/ready work when its instance closes.",
|
||||
"closureCriteria": "Real published/candidate regression comparisons, controlled lifecycle tests and installed UMD/legacy validation pass without altering the frozen published baseline.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-11-media-events.md",
|
||||
"refactor/baselines/media-events-validation.json",
|
||||
"test/media-events.test.js",
|
||||
"test/browser/media-events.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Published/candidate real-browser comparisons reproduce stale retry or ghost ready and validate candidate cancellation. Owned source lifetimes preserve retries after switch rejection, while instance closure stops UI work; original Event/count and ordinary readiness order remain tested."
|
||||
},
|
||||
{
|
||||
"id": "BASE-LIFE-08",
|
||||
"title": "Ready continues after a control callback destroys the instance",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"CORE-11"
|
||||
],
|
||||
"evidence": [
|
||||
"test/media-events.test.js",
|
||||
"test/browser/media-events.spec.js",
|
||||
"refactor/changes/2026-09-11-CORE-11-media-events.md"
|
||||
],
|
||||
"compatibleResolution": "Keep ordinary event order and error identity; scope retries to the current source and stop internal UI/ready work when its instance closes.",
|
||||
"closureCriteria": "Real published/candidate regression comparisons, controlled lifecycle tests and installed UMD/legacy validation pass without altering the frozen published baseline.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-11-CORE-11-media-events.md",
|
||||
"refactor/baselines/media-events-validation.json",
|
||||
"test/media-events.test.js",
|
||||
"test/browser/media-events.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Published/candidate real-browser comparisons reproduce stale retry or ghost ready and validate candidate cancellation. Owned source lifetimes preserve retries after switch rejection, while instance closure stops UI work; original Event/count and ordinary readiness order remain tested."
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-2
@@ -949,11 +949,14 @@
|
||||
"dependsOn": [
|
||||
"CORE-10"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "媒体转发、重连、UI 响应职责分离",
|
||||
"acceptance": "readiness 次数/顺序、过期重连、原错误参数有回归",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-11-CORE-11-media-events.md",
|
||||
"baselines/media-events-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CORE-12",
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: HTTP failure retries the updated option URL with the original event and one ready`, async ({ page, request }, testInfo) => {
|
||||
const caseId = encodeURIComponent(testInfo.testId)
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate((id) => {
|
||||
window.Artplayer.RECONNECT_SLEEP_TIME = 0
|
||||
window.Artplayer.RECONNECT_TIME_MAX = 1
|
||||
window.createPlayer(`/test/fail.mp4?case=${id}`)
|
||||
const { art } = window
|
||||
window.recovery = { attempts: [], ready: 0, identities: [], order: [] }
|
||||
let original
|
||||
art.on('video:error', (event) => {
|
||||
original = event
|
||||
window.recovery.order.push('native-error')
|
||||
art.option.url = `/test/pattern.mp4?case=${id}`
|
||||
})
|
||||
art.on('error', (event, attempt) => {
|
||||
window.recovery.attempts.push(attempt)
|
||||
window.recovery.identities.push(event === original && event.target === art.video)
|
||||
window.recovery.order.push('retry')
|
||||
})
|
||||
art.on('ready', () => {
|
||||
window.recovery.ready++
|
||||
window.recovery.order.push('ready')
|
||||
})
|
||||
}, caseId)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const result = await page.evaluate(() => ({ ...window.recovery, path: new URL(window.art.video.currentSrc).pathname, duration: window.art.duration, mediaError: window.art.video.error?.code || 0 }))
|
||||
expect(result.attempts).toEqual([1])
|
||||
expect(result.identities).toEqual([true])
|
||||
expect(result.ready).toBe(1)
|
||||
expect(result.order).toEqual(['native-error', 'retry', 'ready'])
|
||||
expect(result.path).toBe('/test/pattern.mp4')
|
||||
expect(result.duration).toBeGreaterThan(0)
|
||||
expect(result.mediaError).toBe(0)
|
||||
const requests = await (await request.get(`/test/requests.json?case=${caseId}`)).json()
|
||||
expect(requests.some(item => item.path === '/test/fail.mp4')).toBe(true)
|
||||
expect(requests.some(item => item.path === '/test/pattern.mp4')).toBe(true)
|
||||
await testInfo.attach('reconnect-http', { body: JSON.stringify({ result, requests }), contentType: 'application/json' })
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core}: replacing a failed source cancels candidate retries`, async ({ page }, testInfo) => {
|
||||
const caseId = encodeURIComponent(testInfo.testId)
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate((id) => {
|
||||
window.Artplayer.RECONNECT_SLEEP_TIME = 0
|
||||
window.Artplayer.RECONNECT_TIME_MAX = 1
|
||||
window.createPlayer(`/test/fail.mp4?case=${id}`)
|
||||
window.retries = 0
|
||||
window.art.on('error', () => window.retries++)
|
||||
window.art.once('video:error', () => {
|
||||
window.art.url = `/test/pattern.mp4?case=${id}`
|
||||
})
|
||||
}, caseId)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
// The zero-delay retry's timer queue is observed after the replacement canplay.
|
||||
const retries = await page.evaluate(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
return window.retries
|
||||
})
|
||||
expect(retries).toBe(core === 'candidate' ? 0 : 1)
|
||||
expect(await page.evaluate(() => window.art.video.error)).toBeNull()
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: rejected real-media switch still allows current-source reconnect', async ({ page }, testInfo) => {
|
||||
const caseId = encodeURIComponent(testInfo.testId)
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(id => window.createPlayer(`/test/pattern.mp4?initial=${id}`), caseId)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.evaluate((id) => {
|
||||
const { art, Artplayer } = window
|
||||
Artplayer.RECONNECT_SLEEP_TIME = 0
|
||||
Artplayer.RECONNECT_TIME_MAX = 1
|
||||
window.switchRecovery = { rejected: false, sameError: false, attempts: [], restart: [] }
|
||||
let original
|
||||
art.on('video:error', (event) => {
|
||||
original = event
|
||||
art.option.url = `/test/pattern.mp4?recovered=${id}`
|
||||
})
|
||||
art.on('error', (event, count) => window.switchRecovery.attempts.push([event === original, count]))
|
||||
art.on('restart', url => window.switchRecovery.restart.push(url))
|
||||
art.switchUrl(`/test/fail.mp4?case=${id}`).catch((error) => {
|
||||
window.switchRecovery.rejected = true
|
||||
window.switchRecovery.sameError = error === original
|
||||
})
|
||||
}, caseId)
|
||||
await expect.poll(() => page.evaluate(() => window.switchRecovery.restart.length)).toBe(1)
|
||||
expect(await page.evaluate(() => window.switchRecovery)).toEqual({ rejected: true, sameError: true, attempts: [[true, 1]], restart: [`/test/pattern.mp4?recovered=${caseId}`] })
|
||||
expect(await page.evaluate(() => window.art.video.error)).toBeNull()
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: destroying from first-ready controls prevents a candidate ghost ready event`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const result = await page.evaluate(() => {
|
||||
const art = new window.Artplayer({ container: '.player', url: '' })
|
||||
let ready = 0
|
||||
art.on('ready', () => ready++)
|
||||
art.on('control', () => art.destroy(false))
|
||||
art.emit('video:canplay')
|
||||
return { ready, isReady: art.isReady, isDestroy: art.isDestroy, instances: window.Artplayer.instances.length }
|
||||
})
|
||||
expect(result).toEqual({ ready: core === 'candidate' ? 0 : 1, isReady: core !== 'candidate', isDestroy: true, instances: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('mobile UA metadata path (not physical-device validation)', () => {
|
||||
test.use({ userAgent: 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36' })
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: metadata emits resize before mobile visibility changes`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
const order = await page.evaluate(() => {
|
||||
const art = new window.Artplayer({ container: '.player', url: '' })
|
||||
const seen = []
|
||||
art.on('resize', () => seen.push('resize'))
|
||||
art.on('control', value => seen.push(['control', value]))
|
||||
art.on('video:loadedmetadata', () => seen.push('metadata'))
|
||||
art.emit('video:loadedmetadata')
|
||||
art.destroy()
|
||||
return seen
|
||||
})
|
||||
expect(order).toEqual(['resize', ['control', true], 'metadata'])
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,318 @@
|
||||
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, config, eventInit, urlMix, switchMix, playMix, beginLifecycle, getScope, getSourceScope } = await loadModules({
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
config: 'packages/artplayer/src/config/index',
|
||||
eventInit: 'packages/artplayer/src/player/eventInit',
|
||||
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' },
|
||||
getSourceScope: { file: 'packages/artplayer/src/source/operation', name: 'getSourceScope' },
|
||||
})
|
||||
|
||||
function createArt() {
|
||||
const ui = []
|
||||
const writes = []
|
||||
const forwarded = []
|
||||
const classes = new Set()
|
||||
const video = new EventTarget()
|
||||
let src = ''
|
||||
Object.defineProperty(video, 'src', { configurable: true, get: () => src, set(value) {
|
||||
src = value
|
||||
writes.push(value)
|
||||
} })
|
||||
const visibility = name => ({ get show() {
|
||||
return false
|
||||
}, set show(value) {
|
||||
ui.push([name, value])
|
||||
} })
|
||||
const art = Object.assign(new Emitter(), {
|
||||
template: { $video: video, $poster: { style: {} }, $player: { classList: { add: value => classes.add(value), remove: value => classes.delete(value) } } },
|
||||
option: { url: 'initial.mp4', type: '', customType: {}, loop: false },
|
||||
constructor: { RECONNECT_TIME_MAX: 2, RECONNECT_SLEEP_TIME: 10 },
|
||||
notice: { show: '' },
|
||||
i18n: { get: key => key },
|
||||
isReady: false,
|
||||
playing: false,
|
||||
currentTime: 3,
|
||||
aspectRatio: '16:9',
|
||||
playbackRate: 1,
|
||||
pause: () => {},
|
||||
play: mock.fn(() => Promise.resolve()),
|
||||
loading: visibility('loading'),
|
||||
controls: visibility('controls'),
|
||||
mask: visibility('mask'),
|
||||
proxy(target, name, callback) {
|
||||
forwarded.push([target, name, callback])
|
||||
target.addEventListener(name, callback)
|
||||
getScope(art).add(() => {
|
||||
target.removeEventListener(name, callback)
|
||||
})
|
||||
},
|
||||
})
|
||||
beginLifecycle(art)
|
||||
urlMix(art)
|
||||
switchMix(art)
|
||||
eventInit(art)
|
||||
art.url = art.option.url
|
||||
return { art, video, ui, writes, forwarded, classes, close: () => getScope(art).dispose() }
|
||||
}
|
||||
|
||||
async function tick(t, milliseconds = 10) {
|
||||
t.mock.timers.tick(milliseconds)
|
||||
await setImmediate()
|
||||
}
|
||||
|
||||
test('media forwarding uses configured names, original targets and exact native event objects', () => {
|
||||
const fixture = createArt()
|
||||
const { art, video, forwarded } = fixture
|
||||
assert.deepEqual(forwarded.map(item => item[1]), config.events)
|
||||
assert(forwarded.every(item => item[0] === video))
|
||||
const event = new Event('volumechange')
|
||||
const observed = []
|
||||
art.on('video:volumechange', value => observed.push(value))
|
||||
video.dispatchEvent(event)
|
||||
assert.deepEqual(observed, [event])
|
||||
fixture.close()
|
||||
// Even an externally retained forwarding callback must respect the closed owner.
|
||||
forwarded.find(item => item[1] === 'volumechange')[2](event)
|
||||
assert.deepEqual(observed, [event])
|
||||
})
|
||||
|
||||
test('readiness and media UI retain callback order and ready is emitted once', () => {
|
||||
const { art, ui, close } = createArt()
|
||||
const order = []
|
||||
art.on('resize', () => order.push('resize'))
|
||||
art.on('ready', () => order.push(['ready', art.isReady]))
|
||||
art.on('video:canplay', () => order.push('canplay'))
|
||||
art.emit('video:loadstart')
|
||||
assert.deepEqual(ui, [['loading', true], ['mask', false], ['controls', true]])
|
||||
ui.length = 0
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:canplay')
|
||||
art.emit('video:canplay')
|
||||
assert.deepEqual(order, ['resize', ['ready', true], 'canplay', 'canplay'])
|
||||
assert.deepEqual(ui, [['loading', false], ['loading', false], ['controls', true], ['mask', true], ['loading', false]])
|
||||
close()
|
||||
})
|
||||
|
||||
test('reconnect coalesces errors, preserves the first error identity and respects the attempt limit', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const { art, writes, classes, close } = createArt()
|
||||
const errors = []
|
||||
art.on('error', (...args) => errors.push(args))
|
||||
const first = new Event('error')
|
||||
art.emit('video:error', first)
|
||||
art.emit('video:error', new Event('error'))
|
||||
await tick(t)
|
||||
assert.deepEqual(writes, ['initial.mp4', 'initial.mp4'])
|
||||
assert.deepEqual(errors, [[first, 1]])
|
||||
assert.equal(art.notice.show, 'Reconnect: 1')
|
||||
const second = new Event('error')
|
||||
art.emit('video:error', second)
|
||||
await tick(t)
|
||||
assert.deepEqual(errors, [[first, 1], [second, 2]])
|
||||
art.emit('video:error', new Event('error'))
|
||||
assert(classes.has('art-error'))
|
||||
await tick(t)
|
||||
assert.equal(writes.length, 3)
|
||||
assert.equal(errors.length, 2)
|
||||
assert.equal(art.notice.show, 'Video Load Failed')
|
||||
close()
|
||||
})
|
||||
|
||||
test('source replacement cancels the old retry and starts a fresh attempt budget', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const { art, writes, close } = createArt()
|
||||
const attempts = []
|
||||
art.on('error', (_error, count) => attempts.push(count))
|
||||
art.emit('video:error', new Event('error'))
|
||||
await tick(t)
|
||||
const old = getSourceScope(art)
|
||||
art.emit('video:error', new Event('error'))
|
||||
art.url = 'replacement.mp4'
|
||||
art.notice.show = 'new source'
|
||||
assert.equal(old.closed, true)
|
||||
await tick(t)
|
||||
assert.deepEqual(writes, ['initial.mp4', 'initial.mp4', 'replacement.mp4'])
|
||||
assert.equal(art.notice.show, 'new source')
|
||||
art.emit('video:error', new Event('error'))
|
||||
await tick(t)
|
||||
assert.deepEqual(attempts, [1, 1])
|
||||
assert.equal(writes.at(-1), 'replacement.mp4')
|
||||
close()
|
||||
})
|
||||
|
||||
test('a failed switch rejects while its current source can still reconnect', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const { art, writes, close } = createArt()
|
||||
const error = new Event('error')
|
||||
const switching = art.switchUrl('failed.mp4')
|
||||
const source = getSourceScope(art)
|
||||
const rejected = assert.rejects(switching, actual => actual === error)
|
||||
art.emit('video:error', error)
|
||||
await rejected
|
||||
assert.equal(source.closed, false)
|
||||
await tick(t)
|
||||
assert.deepEqual(writes, ['initial.mp4', 'failed.mp4', 'failed.mp4'])
|
||||
assert.equal(art.notice.show, 'Reconnect: 1')
|
||||
close()
|
||||
})
|
||||
|
||||
test('canplay cancels pending failure notices and clears recovered error UI', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const { art, writes, classes, close } = createArt()
|
||||
art.constructor.RECONNECT_TIME_MAX = 0
|
||||
art.emit('video:error', new Event('error'))
|
||||
assert(classes.has('art-error'))
|
||||
art.emit('video:loadstart')
|
||||
assert.equal(classes.has('art-error'), false)
|
||||
art.notice.show = 'recovered'
|
||||
art.emit('video:canplay')
|
||||
await tick(t)
|
||||
assert.equal(art.notice.show, 'recovered')
|
||||
assert.equal(writes.length, 1)
|
||||
close()
|
||||
})
|
||||
|
||||
test('destroy during readiness stops remaining UI work and removes only owned media handlers', () => {
|
||||
const { art, ui, close } = createArt()
|
||||
const ready = mock.fn()
|
||||
const userCanplay = mock.fn()
|
||||
art.on('ready', ready)
|
||||
art.on('video:canplay', userCanplay)
|
||||
art.controls = { get show() {
|
||||
return false
|
||||
}, set show(value) {
|
||||
ui.push(['controls', value])
|
||||
close()
|
||||
} }
|
||||
art.emit('video:canplay')
|
||||
assert.equal(art.isReady, false)
|
||||
assert.equal(ready.mock.callCount(), 0)
|
||||
assert.equal(userCanplay.mock.callCount(), 1)
|
||||
assert.deepEqual(ui, [['loading', false], ['loading', false], ['controls', true]])
|
||||
art.emit('video:canplay')
|
||||
assert.equal(userCanplay.mock.callCount(), 2)
|
||||
assert.deepEqual(Object.keys(art.e).sort(), ['ready', 'video:canplay'])
|
||||
})
|
||||
|
||||
test('destroy cancels a pending reconnect and exhausted failure notice', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
for (const maximum of [0, 2]) {
|
||||
const { art, writes, close } = createArt()
|
||||
art.constructor.RECONNECT_TIME_MAX = maximum
|
||||
art.emit('video:error', new Event('error'))
|
||||
close()
|
||||
art.notice.show = 'closed'
|
||||
await tick(t)
|
||||
assert.deepEqual(writes, ['initial.mp4'])
|
||||
assert.equal(art.notice.show, 'closed')
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('owned reconnect listener failures are reported with the original error and never left unhandled', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const warning = t.mock.method(console, 'warn', () => {})
|
||||
const { art, close } = createArt()
|
||||
const failure = new Error('user retry observer')
|
||||
art.on('error', () => {
|
||||
throw failure
|
||||
})
|
||||
art.emit('video:error', new Event('error'))
|
||||
await tick(t)
|
||||
assert.equal(warning.mock.callCount(), 1)
|
||||
assert.deepEqual(warning.mock.calls[0].arguments, ['ArtPlayer reconnect failed:', failure])
|
||||
close()
|
||||
})
|
||||
|
||||
test('loop playback handles rejection and stops when seek reentrantly replaces the source', async () => {
|
||||
const { art, ui, close } = createArt()
|
||||
art.option.loop = true
|
||||
art.play = mock.fn(() => Promise.reject(new DOMException('interrupted', 'AbortError')))
|
||||
art.emit('video:ended')
|
||||
await setImmediate()
|
||||
assert.equal(art.play.mock.callCount(), 1)
|
||||
assert.deepEqual(ui, [['controls', false], ['mask', false]])
|
||||
ui.length = 0
|
||||
Object.defineProperty(art, 'seek', { get: () => undefined, set() {
|
||||
art.url = 'reentrant.mp4'
|
||||
} })
|
||||
art.emit('video:ended')
|
||||
assert.equal(art.play.mock.callCount(), 1)
|
||||
assert.deepEqual(ui, [])
|
||||
close()
|
||||
})
|
||||
|
||||
test('retry count remains stable when a proxy synchronously reports canplay during assignment', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const { art, video, close } = createArt()
|
||||
const counts = []
|
||||
art.on('error', (_error, count) => counts.push(count))
|
||||
const descriptor = Object.getOwnPropertyDescriptor(video, 'src')
|
||||
Object.defineProperty(video, 'src', {
|
||||
get: descriptor.get,
|
||||
set(value) {
|
||||
descriptor.set.call(video, value)
|
||||
video.dispatchEvent(new Event('canplay'))
|
||||
},
|
||||
})
|
||||
art.emit('video:error', new Event('error'))
|
||||
await tick(t)
|
||||
assert.deepEqual(counts, [1])
|
||||
assert.equal(art.isReady, true)
|
||||
close()
|
||||
})
|
||||
|
||||
test('an option URL getter that replaces the source cannot let an obsolete retry overwrite it', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const { art, writes, close } = createArt()
|
||||
let requested = 'initial.mp4'
|
||||
let reentered = false
|
||||
Object.defineProperty(art.option, 'url', {
|
||||
get() {
|
||||
if (!reentered) {
|
||||
reentered = true
|
||||
art.url = 'newest.mp4'
|
||||
}
|
||||
return requested
|
||||
},
|
||||
set(value) { requested = value },
|
||||
})
|
||||
art.emit('video:error', new Event('error'))
|
||||
await tick(t)
|
||||
assert.deepEqual(writes, ['initial.mp4', 'newest.mp4'])
|
||||
close()
|
||||
})
|
||||
|
||||
test('source lifetime guards late play after a failed switch has removed its operation', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] })
|
||||
const { art, video, close } = createArt()
|
||||
const error = new Event('error')
|
||||
const rejected = assert.rejects(art.switchUrl('failed.mp4'), actual => actual === error)
|
||||
art.emit('video:error', error)
|
||||
await rejected
|
||||
let resolveNative
|
||||
video.play = () => new Promise(resolve => resolveNative = resolve)
|
||||
const pauseOther = mock.fn()
|
||||
art.constructor.instances = [{ pause: pauseOther }]
|
||||
art.option.mutex = true
|
||||
playMix(art)
|
||||
const plays = mock.fn()
|
||||
art.on('play', plays)
|
||||
const pending = art.play()
|
||||
close()
|
||||
art.notice.show = 'closed'
|
||||
resolveNative(42)
|
||||
assert.equal(await pending, 42)
|
||||
assert.equal(art.notice.show, 'closed')
|
||||
assert.equal(plays.mock.callCount(), 0)
|
||||
assert.equal(pauseOther.mock.callCount(), 0)
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { MediaEventHost } from '../../packages/artplayer/src/media/events/types'
|
||||
import eventInit from '../../packages/artplayer/src/player/eventInit'
|
||||
|
||||
declare const art: MediaEventHost
|
||||
eventInit(art)
|
||||
art.emit('ready')
|
||||
art.emit('resize')
|
||||
art.emit('video:custom', new Event('custom'))
|
||||
art.emit('error', new Error('source'), 1)
|
||||
// @ts-expect-error Media forwarding retains the Event object, not an arbitrary payload.
|
||||
art.emit('video:canplay', 'canplay')
|
||||
// @ts-expect-error Retry errors include the attempt count.
|
||||
art.emit('error', new Error('source'))
|
||||
// @ts-expect-error UI visibility does not accept a source URL.
|
||||
art.loading.show = 'video.mp4'
|
||||
// @ts-expect-error A structural proxy still needs EventTarget listener capabilities.
|
||||
art.template.$video = { src: 'video.mp4' }
|
||||
Reference in new issue
Block a user